Repository: evcc-io/evcc (go)

Issue:
SMA Hybrid Begrenzung der Netzladung
**Is your feature request related to a problem? Please describe.**
Currently forced charging of batteries with SMA STP SE series does not work properly. When setting a charging power above ~50% of inverter power the charging power starts to oscillate nearly full scale between max and 1kW with 20s intervals (also reported e.g. in #16972). This is clearly a problem in SMA firmware and not really EVCC related. It works without oscillations for charging power below 5kW. There are tickets open at SMA, but not from me.
For pictures of the bahaviour see [photovoltaik forum](https://www.photovoltaikforum.com/thread/236843-sma-sunny-tripower-5-0-se-byd-hvm22-probleme-mit-schwankender-ladeleistung/?postID=4029845#post4029845)

**Describe the solution you'd like**
Make battery charging power a configuration parameter in [sma-hybrid template](https://github.com/evcc-io/evcc/blob/3398312e772c314e201691407da5cde64059ea64/templates/definition/meter/sma-hybrid.yaml#L170) instead of virtual infinity.

**Describe alternatives you've considered**
I manually copied the contents of template to evcc.yaml, changed the value for modbus address 40149 to -5000 and charging works correctly.

**Additional context**
This is (hopefully) a firmware problem from SMA side. If addressed from their side, this parameter is not really necessary. I don't know if there are usecases from other users to limit the charging power.

Pull request description:
add chargepower to sma-hybrid
Fix #17235

Patch:
diff --git a/meter/shelly/connection.go b/meter/shelly/connection.go
index 9fafe415..388b2d7a 100644
--- a/meter/shelly/connection.go
+++ b/meter/shelly/connection.go
@@ -19,6 +19,8 @@ type Connection struct {
 	channel    int
 	gen        int    // Shelly api generation
 	devicetype string // Shelly device type
+	app        string // Shelly device app code
+	profile    string // Shelly device profile
 }
 
 // NewConnection creates a new Shelly device connection.
@@ -45,6 +47,8 @@ func NewConnection(uri, user, password string, channel int) (*Connection, error)
 		channel:    channel,
 		gen:        resp.Gen,
 		devicetype: resp.Type,
+		app:        resp.App,
+		profile:    resp.Profile,
 	}
 
 	conn.Client.Transport = request.NewTripper(log, transport.Insecure())
diff --git a/meter/shelly/switch.go b/meter/shelly/switch.go
index 48dad7fc..26795a9c 100644
--- a/meter/shelly/switch.go
+++ b/meter/shelly/switch.go
@@ -42,18 +42,25 @@ func (sh *Switch) CurrentPower() (float64, error) {
 		}
 
 	default:
+		var resem Gen2EmStatusResponse
 		var res Gen2StatusResponse
-		if err := d.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
-			return 0, err
+		if d.app == "Pro3EM" && d.profile == "monophase" {
+			if err := d.execGen2Cmd("Shelly.GetStatus", false, &resem); err != nil {
+				return 0, err
+			}
+		} else {
+			if err := d.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
+				return 0, err
+			}
 		}
 
 		switch d.channel {
 		case 1:
-			power = res.Switch1.Apower + res.Pm1.Apower
+			power = res.Switch1.Apower + res.Pm1.Apower + resem.Em1.ActPower
 		case 2:
-			power = res.Switch2.Apower + res.Pm2.Apower
+			power = res.Switch2.Apower + res.Pm2.Apower + resem.Em2.ActPower
 		default:
-			power = res.Switch0.Apower + res.Pm0.Apower
+			power = res.Switch0.Apower + res.Pm0.Apower + resem.Em0.ActPower
 		}
 	}
 
@@ -123,18 +130,25 @@ func (sh *Switch) TotalEnergy() (float64, error) {
 		energy = gen1Energy(d.devicetype, energy)
 
 	default:
+		var resem Gen2EmStatusResponse
 		var res Gen2StatusResponse
-		if err := d.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
-			return 0, err
+		if d.app == "Pro3EM" && d.profile == "monophase" {
+			if err := d.execGen2Cmd("Shelly.GetStatus", false, &resem); err != nil {
+				return 0, err
+			}
+		} else {
+			if err := d.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
+				return 0, err
+			}
 		}
 
 		switch d.channel {
 		case 1:
-			energy = res.Switch1.Aenergy.Total + res.Pm1.Aenergy.Total
+			energy = res.Switch1.Aenergy.Total + res.Pm1.Aenergy.Total + resem.Em1Data.TotalActEnergy - resem.Em1Data.TotalActRetEnergy
 		case 2:
-			energy = res.Switch2.Aenergy.Total + res.Pm2.Aenergy.Total
+			energy = res.Switch2.Aenergy.Total + res.Pm2.Aenergy.Total + resem.Em2Data.TotalActEnergy - resem.Em2Data.TotalActRetEnergy
 		default:
-			energy = res.Switch0.Aenergy.Total + res.Pm0.Aenergy.Total
+			energy = res.Switch0.Aenergy.Total + res.Pm0.Aenergy.Total + resem.Em0Data.TotalActEnergy - resem.Em0Data.TotalActRetEnergy
 		}
 	}
 
diff --git a/meter/shelly/types.go b/meter/shelly/types.go
index c06228c5..5824d981 100644
--- a/meter/shelly/types.go
+++ b/meter/shelly/types.go
@@ -8,9 +8,11 @@ type DeviceInfo struct {
 	Model     string `json:"model"`
 	Type      string `json:"type"`
 	Mac       string `json:"mac"`
+	App       string `json:"app"`
 	Auth      bool   `json:"auth"`
 	AuthEn    bool   `json:"auth_en"`
 	NumMeters int    `json:"num_meters"`
+	Profile   string `json:"profile"`
 }
 
 type Gen2RpcPost struct {
@@ -40,17 +42,34 @@ type Gen2StatusResponse struct {
 	Pm2     Gen2Switch `json:"pm3:2"`
 }
 
+type Gen2Em struct {
+	Current  float64 `json:"current"`
+	Voltage  float64 `json:"voltage"`
+	ActPower float64 `json:"act_power"`
+}
+
+type Gen2EmData struct {
+	TotalActEnergy    float64 `json:"total_act_energy"`
+	TotalActRetEnergy float64 `json:"total_act_ret_energy"`
+}
+
 type Gen2EmStatusResponse struct {
-	TotalPower float64 `json:"total_act_power"`
-	CurrentA   float64 `json:"a_current"`
-	CurrentB   float64 `json:"b_current"`
-	CurrentC   float64 `json:"c_current"`
-	VoltageA   float64 `json:"a_voltage"`
-	VoltageB   float64 `json:"b_voltage"`
-	VoltageC   float64 `json:"c_voltage"`
-	PowerA     float64 `json:"a_act_power"`
-	PowerB     float64 `json:"b_act_power"`
-	PowerC     float64 `json:"c_act_power"`
+	TotalPower float64    `json:"total_act_power"`
+	CurrentA   float64    `json:"a_current"`
+	CurrentB   float64    `json:"b_current"`
+	CurrentC   float64    `json:"c_current"`
+	VoltageA   float64    `json:"a_voltage"`
+	VoltageB   float64    `json:"b_voltage"`
+	VoltageC   float64    `json:"c_voltage"`
+	PowerA     float64    `json:"a_act_power"`
+	PowerB     float64    `json:"b_act_power"`
+	PowerC     float64    `json:"c_act_power"`
+	Em0        Gen2Em     `json:"em1:0"`
+	Em1        Gen2Em     `json:"em1:1"`
+	Em2        Gen2Em     `json:"em1:2"`
+	Em0Data    Gen2EmData `json:"em1data:0"`
+	Em1Data    Gen2EmData `json:"em1data:1"`
+	Em2Data    Gen2EmData `json:"em1data:2"`
 }
 
 type Gen2EmDataStatusResponse struct {
diff --git a/templates/definition/meter/shelly-1pm.yaml b/templates/definition/meter/shelly-1pm.yaml
index 3dd14314..bb638349 100644
--- a/templates/definition/meter/shelly-1pm.yaml
+++ b/templates/definition/meter/shelly-1pm.yaml
@@ -2,11 +2,11 @@ template: shelly-1pm
 products:
   - brand: Shelly
     description:
-      generic: 1PM, EM, Plug S
+      generic: 1PM, EM, Plug S, Pro 3EM in monophase mode
 group: switchsockets
 params:
   - name: usage
-    choice: ["pv", "charge"]
+    choice: ["grid", "pv", "charge"]
   - name: host
   - name: user
   - name: password
diff --git a/templates/definition/meter/sma-hybrid.yaml b/templates/definition/meter/sma-hybrid.yaml
index cb23c740..0e6aeeb3 100644
--- a/templates/definition/meter/sma-hybrid.yaml
+++ b/templates/definition/meter/sma-hybrid.yaml
@@ -23,6 +23,13 @@ params:
     type: duration
     default: 30s
     advanced: true
+  - name: chargepower
+    default: 100000  # 100kW, inverter will use it's maximum by default
+    advanced: true
+    help:
+      de: Ladeleistung für Netzladen in W
+      en: Charging power for grid charging in W
+    usages: ["battery"]
 render: |
   type: custom
   {{- if eq .usage "grid" }}
@@ -167,7 +174,7 @@ render: |
           source: sequence
           set:
           - source: const
-            value: -2147483647 # Wirkleistungsvorgabe
+            value: -{{ .chargepower }} # Wirkleistungsvorgabe
             set:
               source: modbus
               {{- include "modbus" . | indent 12 }}


Test patch:
diff --git a/meter/shelly/types_test.go b/meter/shelly/types_test.go
index ad400674..2e4e968e 100644
--- a/meter/shelly/types_test.go
+++ b/meter/shelly/types_test.go
@@ -67,4 +67,44 @@ func TestUnmarshalGen2StatusResponse(t *testing.T) {
 		assert.Equal(t, 3551.682, res.Pm0.Aenergy.Total)
 		assert.Equal(t, 1780.1, res.Pm0.Apower)
 	}
+
+	{
+		// Shelly Pro 3EM
+		var res Gen2EmStatusResponse
+
+		jsonstr := `{"ble":{},"bthome":{"errors":["bluetooth_disabled"]},"cloud":{"connected":true},"em1:0":{"id":0,"current":3.705,"voltage":242.8,"act_power":598.9,"aprt_power":900.6,"pf":0.66,"freq":50.0,"calibration":"factory"},"em1:1":{"id":1,"current":0.194,"voltage":242.8,"act_power":0.0,"aprt_power":47.2,"pf":0.00,"freq":50.0,"calibration":"factory"},"em1:2":{"id":2,"current":0.027,"voltage":242.8,"act_power":0.0,"aprt_power":6.6,"pf":0.00,"freq":50.0,"calibration":"factory"},"em1data:0":{"id":0,"total_act_energy":3458.24,"total_act_ret_energy":1605.24},"em1data:1":{"id":1,"total_act_energy":2768.67,"total_act_ret_energy":25.49},"em1data:2":{"id":2,"total_act_energy":3.09,"total_act_ret_energy":0.71},"eth":{"ip":null},"modbus":{},"mqtt":{"connected":false},"sys":{"mac":"FCE8C0DBA850","restart_required":false,"time":"19:46","unixtime":1731404780,"uptime":563,"ram_size":247148,"ram_free":110596,"fs_size":524288,"fs_free":176128,"cfg_rev":21,"kvs_rev":0,"schedule_rev":3,"webhook_rev":1,"available_updates":{},"reset_reason":3},"temperature:0":{"id": 0,"tC":39.0, "tF":102.2},"wifi":{"sta_ip":"192.168.40.174","status":"got ip","ssid":"IoT","rssi":-67},"ws":{"connected":false}}`
+		require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
+		// Channel 0 (1)
+		assert.Equal(t, 598.9, res.Em0.ActPower)
+		assert.Equal(t, 3.705, res.Em0.Current)
+		assert.Equal(t, 242.8, res.Em0.Voltage)
+		assert.Equal(t, 3458.24, res.Em0Data.TotalActEnergy)
+		assert.Equal(t, 1605.24, res.Em0Data.TotalActRetEnergy)
+		// Channel 1 (2)
+		assert.Equal(t, 0.0, res.Em1.ActPower)
+		assert.Equal(t, 0.194, res.Em1.Current)
+		assert.Equal(t, 242.8, res.Em1.Voltage)
+		assert.Equal(t, 2768.67, res.Em1Data.TotalActEnergy)
+		assert.Equal(t, 25.49, res.Em1Data.TotalActRetEnergy)
+		// Channel 2 (3)
+		assert.Equal(t, 0.0, res.Em2.ActPower)
+		assert.Equal(t, 0.027, res.Em2.Current)
+		assert.Equal(t, 242.8, res.Em2.Voltage)
+		assert.Equal(t, 3.09, res.Em2Data.TotalActEnergy)
+		assert.Equal(t, 0.71, res.Em2Data.TotalActRetEnergy)
+	}
+}
+
+// Test Shelly device info
+func TestUnmarshalDeviceInfoResponse(t *testing.T) {
+	{
+		// Shelly Pro 3EM
+		var res DeviceInfo
+
+		jsonstr := `{"name":null,"id":"shellypro3em-fce8c0dba900","mac":"FCE8C0DBA900","slot":1,"model":"SPEM-003CEBEU","gen":2,"fw_id":"20241011-114455/1.4.4-g6d2a586","ver":"1.4.4","app":"Pro3EM","auth_en":false,"auth_domain":null,"profile":"monophase"}`
+		require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
+
+		assert.Equal(t, "Pro3EM", res.App)
+		assert.Equal(t, "monophase", res.Profile)
+	}
 }