Repository: evcc-io/evcc

Issue:
LG ESS: battery control broken since version 0.202.0
### Describe the bug

I used LG ESS Battery Control in evcc version 0.201.0, and it worked. So, not only did it display battery control, but I also tried charging from the mains, for example. From version 0.202.0 up to the latest nightly, the battery control is no longer available.
I think the LG ESS firmware version test is faulty.

Attached is an excerpt from my log, where the different versions of the individual LG ESS components are visible.

### Steps to reproduce

1. Configuration
2. Battery
3. No entry for controllable


### Configuration details

```yaml
meters:
- type: template
  template: lg-ess-home-8-10
  usage: grid
  host: 192.168.178.69
  password: ***
  name: grid1
- type: template
  template: lg-ess-home-8-10
  usage: pv
  host: 192.168.178.69
  password: ***
  name: pv2
- type: template
  template: lg-ess-home-8-10
  usage: battery
  host: 192.168.178.69
  password: ***
  name: battery3
  minsoc: 20
  maxsoc: 90
```

### Log details

```text
[lgess-https://] TRACE 2025/07/03 09:40:20 POST https://192.168.178.69/v1/user/setting/systeminfo
[lgess-https://] TRACE 2025/07/03 09:40:20 {"auth_key":"***"} -- { "pms": { "model": "D008KE1N211", "serialno": "DE200***", "ac_input_power": "13500", "ac_output_power": "8", "install_date": "2020-04-29" }, "batt": { "capacity": "150", "type": "hbc", "nameplate_energy_1": "15000", "hbc_cycle_count_1": "189", "hbc_remaining_cap_1": "36104", "hbc_dischg_rate_1": "0.0154", "hbc_chg_energy_1": "3322672", "hbc_dischg_energy_1": "3208095", "hbc_chg_cap_1": "8011.200000000001", "hbc_dischg_cap_1": "7825.8", "hbc_deep_dischg_cnt_1": "0", "hbc_over_chg_cnt_1": "0", "nameplate_energy_2": "0", "hbc_cycle_count_2": "0", "hbc_remaining_cap_2": "0", "hbc_dischg_rate_2": "0", "hbc_chg_energy_2": "0", "hbc_dischg_energy_2": "0", "hbc_chg_cap_2": "0", "hbc_dischg_cap_2": "0", "hbc_deep_dischg_cnt_2": "0", "hbc_over_chg_cnt_2": "0", "hbc_a_serials": "", "hbc_b_serials": "", "hbc_a_pack_dates": "", "hbc_b_pack_dates": "", "install_date": "2023-05-20" }, "version": { "pms_version": "10.05.7433", "pms_build_date": "2024-10-04 R2159", "pcs_version": "LG 05.00.01.00 R198 1.194.3", "bms_version": "BMS 00.04.00.05 ", "bms_unit1_version": "BMS 00.04.00.05 ", "bms_unit2_version": " " } }
[site ] WARN 2025/07/03 09:40:23 vehicle 'e-UP' does not support automatic detection
[site ] INFO 2025/07/03 09:40:23 site config:
[site ] INFO 2025/07/03 09:40:23 meters: grid ✓ pv ✓ battery ✓
[site ] INFO 2025/07/03 09:40:23 grid: power ✓ energy ✓ currents ✗
[site ] INFO 2025/07/03 09:40:23 pv 1: power ✓ energy ✗ currents ✗
[site ] INFO 2025/07/03 09:40:23 battery 1: power ✓ energy ✗ currents ✗ soc ✓ capacity ✗
[site ] INFO 2025/07/03 09:40:23 vehicles:
[site ] INFO 2025/07/03 09:40:23 vehicle 1: range ✗ finish ✗ status ✗ climate ✗ wakeup ✗
[site ] INFO 2025/07/03 09:40:23 vehicle 2: range ✓ finish ✗ status ✓ climate ✓ wakeup ✓
[site ] INFO 2025/07/03 09:40:23 tariffs:
[site ] INFO 2025/07/03 09:40:23 grid: priceforecast
[site ] INFO 2025/07/03 09:40:23 feed-in: pricestatic
[site ] INFO 2025/07/03 09:40:23 co2: ✓
[site ] INFO 2025/07/03 09:40:23 solar: ✓
[site ] DEBUG 2025/07/03 09:40:23 set buffer soc: 85
[site ] DEBUG 2025/07/03 09:40:23 set buffer start soc: 95
[site ] DEBUG 2025/07/03 09:40:23 set priority soc: 50
[site ] DEBUG 2025/07/03 09:40:23 set battery discharge control: false 
```

### What type of operating system or environment does evcc run on?

Linux

### External automation

- [x] I have made sure that no external automation like HomeAssistant or Node-RED is active or accessing any of the mentioned devices when this issue occurs.

### Nightly build

- [x] I have verified that the issue is reproducible with the latest nightly build

### Version

_No response_

Hints from issue comments:

Solution patch:
diff --git a/.github/workflows/claude-issue-triage.yml b/.github/workflows/claude-issue-triage.yml
index e4fd0c99..fa64fcd0 100644
--- a/.github/workflows/claude-issue-triage.yml
+++ b/.github/workflows/claude-issue-triage.yml
@@ -15,31 +15,23 @@ jobs:
       - name: Checkout repository
         uses: actions/checkout@v4
 
-      - name: Setup GitHub MCP Server
-        run: |
-          mkdir -p /tmp/mcp-config
-          cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
-          {
-            "mcpServers": {
-              "github": {
-                "command": "npx",
-                "args": ["-y", "@modelcontextprotocol/server-github"],
-                "env": {
-                "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
-                }
-              }
-            }
-          }
-
-          EOF
-          cat /tmp/mcp-config/mcp-servers.json
-
       - name: Run Claude Code
         uses: anthropics/claude-code-base-action@beta
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         with:
-          mcp_config: /tmp/mcp-config/mcp-servers.json
+          mcp_config: |
+            {
+              "mcpServers": {
+                "github": {
+                  "command": "npx",
+                  "args": ["-y", "@modelcontextprotocol/server-github"],
+                  "env": {
+                  "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
+                  }
+                }
+              }
+            }
           prompt: |
             You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
 
@@ -57,7 +49,7 @@ jobs:
                 - You have access to these tools:
                 - mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
                 - mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
-                - mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
+                - mcp__github__update_issue: Use this to apply labels to the issue or set issue type (do not use this for commenting)
                 - mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
                 - mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
                 - Start by using mcp__github__get_issue to get the issue details
@@ -75,7 +67,11 @@ jobs:
                 - Be specific but comprehensive
                 - If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
 
-            5. Apply the selected labels:
+            5. Use issue type instead of labels if the issue is a feature request or bug report:
+                - If the issue is a feature request, set the issue type to "feature request". Do not apply the "enhancement" label.
+                - If the issue is a bug report, set the issue type to "bug". Do not apply the "bug" label.
+
+            6. Apply the selected labels:
                 - Use mcp__github__update_issue to apply your selected labels
                 - DO NOT post any comments explaining your decision
                 - DO NOT communicate directly with users
diff --git a/charger/easee.go b/charger/easee.go
index 8814d271..eea68d29 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -313,7 +313,11 @@ func (c *Easee) ProductUpdate(i json.RawMessage) {
 	case easee.TOTAL_POWER:
 		c.currentPower = 1e3 * value.(float64)
 	case easee.SESSION_ENERGY:
-		c.sessionEnergy = value.(float64)
+		// SESSION_ENERGY must not be set to 0 by Productupdates, they occur erratic
+		// Reset to 0 is done in case CHARGER_OP_MODE
+		if value.(float64) != 0 {
+			c.sessionEnergy = value.(float64)
+		}
 	case easee.LIFETIME_ENERGY:
 		c.totalEnergy = value.(float64)
 	case easee.IN_CURRENT_T3:
@@ -335,7 +339,16 @@ func (c *Easee) ProductUpdate(i json.RawMessage) {
 	case easee.DYNAMIC_CHARGER_CURRENT:
 		c.dynamicChargerCurrent = value.(float64)
 	case easee.CHARGER_OP_MODE:
-		c.opMode = value.(int)
+		opMode := value.(int)
+
+		// New charging session pending, reset internal value of SESSION_ENERGY to 0, and its observation timestamp to "now".
+		// This should be done in a proper way by the api, but it's not.
+		if c.opMode <= easee.ModeDisconnected && opMode >= easee.ModeAwaitingStart {
+			c.sessionEnergy = 0
+			c.obsTime[easee.SESSION_ENERGY] = time.Now()
+		}
+
+		c.opMode = opMode
 
 		// startup completed
 		c.startDone()
diff --git a/cmd/charger.go b/cmd/charger.go
index f29351ae..d7580948 100644
--- a/cmd/charger.go
+++ b/cmd/charger.go
@@ -3,6 +3,7 @@ package cmd
 import (
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util/config"
@@ -27,6 +28,7 @@ func init() {
 	chargerCmd.Flags().Bool(flagDiagnose, false, flagDiagnoseDescription)
 	chargerCmd.Flags().BoolP(flagWakeup, "w", false, flagWakeupDescription)
 	chargerCmd.Flags().IntP(flagPhases, "p", 0, flagPhasesDescription)
+	chargerCmd.Flags().Bool(flagHeartbeat, false, flagHeartbeatDescription)
 }
 
 func runCharger(cmd *cobra.Command, args []string) {
@@ -117,6 +119,11 @@ func runCharger(cmd *cobra.Command, args []string) {
 		}
 	}
 
+	if ok, _ := cmd.Flags().GetBool(flagHeartbeat); flagUsed && ok {
+		log.INFO.Println("running heartbeat until interrupted (Ctrl-C to stop)")
+		time.Sleep(time.Hour)
+	}
+
 	if !flagUsed {
 		d := dumper{len: len(chargers)}
 		flag := cmd.Flag(flagDiagnose).Changed
@@ -129,6 +136,9 @@ func runCharger(cmd *cobra.Command, args []string) {
 				d.DumpDiagnosis(v)
 			}
 		}
+	} else if ok, _ := cmd.Flags().GetBool(flagHeartbeat); ok {
+		log.INFO.Println("running heartbeat (if any) until interrupted (Ctrl-C to stop)")
+		time.Sleep(time.Hour)
 	}
 
 	// wait for shutdown
diff --git a/cmd/flags.go b/cmd/flags.go
index 5d34301a..c4a2736f 100644
--- a/cmd/flags.go
+++ b/cmd/flags.go
@@ -65,6 +65,9 @@ const (
 	flagRepeatInterval            = "repeat-interval"
 	flagRepeatIntervalDescription = "Interval between repetitions"
 
+	flagHeartbeat            = "heartbeat"
+	flagHeartbeatDescription = "After command, continue running device heartbeats (if any) until interrupted"
+
 	flagDigits = "digits"
 	flagDelay  = "delay"
 	flagForce  = "force"
diff --git a/cmd/meter.go b/cmd/meter.go
index d2f0c573..e07be80f 100644
--- a/cmd/meter.go
+++ b/cmd/meter.go
@@ -22,6 +22,7 @@ func init() {
 	meterCmd.Flags().DurationP(flagBatteryModeWait, "w", 0, flagBatteryModeWaitDescription)
 	meterCmd.Flags().BoolP(flagRepeat, "r", false, flagRepeatDescription)
 	meterCmd.Flags().Duration(flagRepeatInterval, 0, flagRepeatIntervalDescription)
+	meterCmd.Flags().Bool(flagHeartbeat, false, flagHeartbeatDescription)
 }
 
 func runMeter(cmd *cobra.Command, args []string) {
@@ -83,6 +84,9 @@ func runMeter(cmd *cobra.Command, args []string) {
 			}
 			goto REPEAT
 		}
+	} else if ok, _ := cmd.Flags().GetBool(flagHeartbeat); ok {
+		log.INFO.Println("running heartbeat (if any) until interrupted (Ctrl-C to stop)")
+		time.Sleep(time.Hour)
 	}
 
 	// wait for shutdown
diff --git a/go.mod b/go.mod
index 309b0a20..2b2bbb25 100644
--- a/go.mod
+++ b/go.mod
@@ -57,7 +57,7 @@ require (
 	github.com/jarcoal/httpmock v1.4.0
 	github.com/jeremywohl/flatten v1.0.1
 	github.com/jinzhu/now v1.1.5
-	github.com/joeshaw/carwings v0.0.0-20250124122309-e366d592915c
+	github.com/joeshaw/carwings v0.0.0-20250704173606-1708e349f36c
 	github.com/joho/godotenv v1.5.1
 	github.com/jpfielding/go-http-digest v0.0.0-20240123121450-cffc47d5d6d8
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
diff --git a/go.sum b/go.sum
index c26aaaf1..a37ae5e2 100644
--- a/go.sum
+++ b/go.sum
@@ -380,8 +380,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
-github.com/joeshaw/carwings v0.0.0-20250124122309-e366d592915c h1:0B5ZN4KU2Zy6xn21hSnovLsXQX1pF+vNYG7rlh5m3iM=
-github.com/joeshaw/carwings v0.0.0-20250124122309-e366d592915c/go.mod h1:tB0OlpicmRVTL1Vksc5XRiYo+wkK2kl/GI7eGMIl6Rs=
+github.com/joeshaw/carwings v0.0.0-20250704173606-1708e349f36c h1:qAJHVJ+s+pbLuvNiYhYVblTfgkFiPwUuAJMUNKh0uJc=
+github.com/joeshaw/carwings v0.0.0-20250704173606-1708e349f36c/go.mod h1:rNwwhAzMSNqlTsRQZ7a+jXHu4Mgn2ztr3aYCqRgWGlA=
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
diff --git a/meter/lgpcs/lgpcs.go b/meter/lgpcs/lgpcs.go
index 5068bcb1..3346809b 100644
--- a/meter/lgpcs/lgpcs.go
+++ b/meter/lgpcs/lgpcs.go
@@ -162,8 +162,9 @@ func (m *Com) GetFirmwareVersion() (int, error) {
 	if err != nil {
 		return 0, err
 	}
+
 	// extract the patch number behind a dot that is always followed by at least 4 digits
-	if match := regexp.MustCompile(`.(\d{4})`).FindStringSubmatch(systemInfo.Version.PMSVersion); len(match) > 1 {
+	if match := regexp.MustCompile(`\.(\d{4})$`).FindStringSubmatch(systemInfo.Version.PMSVersion); len(match) > 1 {
 		return strconv.Atoi(match[1])
 	}
 
diff --git a/meter/lgpcs/types.go b/meter/lgpcs/types.go
index 2843229a..3ae03df8 100644
--- a/meter/lgpcs/types.go
+++ b/meter/lgpcs/types.go
@@ -44,33 +44,33 @@ type SystemInfoResponse struct {
 }
 
 type BatteryInfo struct {
-	Capacity          int    `json:"capacity,string"`
-	HBCAPackDates     string `json:"hbc_a_pack_dates"`
-	HBCASerials       string `json:"hbc_a_serials"`
-	HBCBPackDates     string `json:"hbc_b_pack_dates"`
-	HBCBSerials       string `json:"hbc_b_serials"`
-	HBCChgCap1        int    `json:"hbc_chg_cap_1,string"`
-	HBCChgCap2        int    `json:"hbc_chg_cap_2,string"`
-	HBCChgEnergy1     int    `json:"hbc_chg_energy_1,string"`
-	HBCChgEnergy2     int    `json:"hbc_chg_energy_2,string"`
-	HBCCycleCount1    int    `json:"hbc_cycle_count_1,string"`
-	HBCCycleCount2    int    `json:"hbc_cycle_count_2,string"`
-	HBCDeepDischgCnt1 int    `json:"hbc_deep_dischg_cnt_1,string"`
-	HBCDeepDischgCnt2 int    `json:"hbc_deep_dischg_cnt_2,string"`
-	HBCDischgCap1     int    `json:"hbc_dischg_cap_1,string"`
-	HBCDischgCap2     int    `json:"hbc_dischg_cap_2,string"`
-	HBCDischgEnergy1  int    `json:"hbc_dischg_energy_1,string"`
-	HBCDischgEnergy2  int    `json:"hbc_dischg_energy_2,string"`
-	HBCDischgRate1    int    `json:"hbc_dischg_rate_1,string"`
-	HBCDischgRate2    int    `json:"hbc_dischg_rate_2,string"`
-	HBCOVerChgCnt1    int    `json:"hbc_over_chg_cnt_1,string"`
-	HBCOVerChgCnt2    int    `json:"hbc_over_chg_cnt_2,string"`
-	HBCRemainingCap1  int    `json:"hbc_remaining_cap_1,string"`
-	HBCRemainingCap2  int    `json:"hbc_remaining_cap_2,string"`
-	InstallDate       string `json:"install_date"`
-	NameplateEnergy1  int    `json:"nameplate_energy_1,string"`
-	NameplateEnergy2  int    `json:"nameplate_energy_2,string"`
-	BatteryType       string `json:"type"`
+	Capacity          float64 `json:"capacity,string"`
+	HBCAPackDates     string  `json:"hbc_a_pack_dates"`
+	HBCASerials       string  `json:"hbc_a_serials"`
+	HBCBPackDates     string  `json:"hbc_b_pack_dates"`
+	HBCBSerials       string  `json:"hbc_b_serials"`
+	HBCChgCap1        float64 `json:"hbc_chg_cap_1,string"`
+	HBCChgCap2        float64 `json:"hbc_chg_cap_2,string"`
+	HBCChgEnergy1     float64 `json:"hbc_chg_energy_1,string"`
+	HBCChgEnergy2     float64 `json:"hbc_chg_energy_2,string"`
+	HBCCycleCount1    int     `json:"hbc_cycle_count_1,string"`
+	HBCCycleCount2    int     `json:"hbc_cycle_count_2,string"`
+	HBCDeepDischgCnt1 int     `json:"hbc_deep_dischg_cnt_1,string"`
+	HBCDeepDischgCnt2 int     `json:"hbc_deep_dischg_cnt_2,string"`
+	HBCDischgCap1     float64 `json:"hbc_dischg_cap_1,string"`
+	HBCDischgCap2     float64 `json:"hbc_dischg_cap_2,string"`
+	HBCDischgEnergy1  float64 `json:"hbc_dischg_energy_1,string"`
+	HBCDischgEnergy2  float64 `json:"hbc_dischg_energy_2,string"`
+	HBCDischgRate1    float64 `json:"hbc_dischg_rate_1,string"`
+	HBCDischgRate2    float64 `json:"hbc_dischg_rate_2,string"`
+	HBCOVerChgCnt1    int     `json:"hbc_over_chg_cnt_1,string"`
+	HBCOVerChgCnt2    int     `json:"hbc_over_chg_cnt_2,string"`
+	HBCRemainingCap1  float64 `json:"hbc_remaining_cap_1,string"`
+	HBCRemainingCap2  float64 `json:"hbc_remaining_cap_2,string"`
+	InstallDate       string  `json:"install_date"`
+	NameplateEnergy1  int     `json:"nameplate_energy_1,string"`
+	NameplateEnergy2  int     `json:"nameplate_energy_2,string"`
+	BatteryType       string  `json:"type"`
 }
 
 type PMS struct {



Test patch:
diff --git a/charger/easee_test.go b/charger/easee_test.go
index 68e23f39..15dab35f 100644
--- a/charger/easee_test.go
+++ b/charger/easee_test.go
@@ -61,13 +61,30 @@ func TestProductUpdate_IgnoreOutdatedProductUpdate(t *testing.T) {
 	assert.Equal(t, 2, e.opMode)
 }
 
-func TestProductUpdate_InitialStateCheck(t *testing.T) {
-	now := time.Now().UTC().Truncate(0) //truncate removes sub nanos
+func TestProductUpdate_IgnoreZeroSessionEnergy(t *testing.T) {
+	e := newEasee()
+
+	now := time.Now().UTC().Truncate(0)
+	e.ProductUpdate(createPayload(easee.SESSION_ENERGY, now, easee.Double, "20"))
+
+	assert.Equal(t, now, e.obsTime[easee.SESSION_ENERGY])
+	assert.Equal(t, float64(20), e.sessionEnergy)
+
+	t2 := time.Now().UTC().Truncate(0)
+	e.ProductUpdate(createPayload(easee.SESSION_ENERGY, t2, easee.Double, "0.0"))
 
+	//expect observation timestamp updated, value however not
+	assert.Equal(t, t2, e.obsTime[easee.SESSION_ENERGY])
+	assert.Equal(t, float64(20), e.sessionEnergy)
+}
+
+func TestProductUpdate_LifetimeEnergyAndSessionStartEnergy(t *testing.T) {
 	e := newEasee()
 
 	assert.False(t, e.optionalStatePresent())
 
+	now := time.Now().UTC().Truncate(0) //truncate removes sub nanos
+
 	tc := []struct {
 		obsId           easee.ObservationID
 		dataType        easee.DataType