From bfb55dc2c4cd8caa6b6750bf8dd02c0bdc911505 Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:07:18 +0200 Subject: [PATCH] Shelly: drop grid energy and returnEnergy for 3-phase EM (#30534) --- meter/shelly.go | 19 +++++++++++++------ meter/shelly/connection.go | 1 + meter/shelly/gen1.go | 6 ++++++ meter/shelly/gen2.go | 5 +++++ templates/definition/meter/shelly-3em.yaml | 10 ++++++++++ .../definition/meter/shelly-pro-3em.yaml | 3 +++ 6 files changed, 38 insertions(+), 6 deletions(-) diff --git a/meter/shelly.go b/meter/shelly.go index 38a18e171..54ad929f4 100644 --- a/meter/shelly.go +++ b/meter/shelly.go @@ -14,7 +14,7 @@ import ( // Shelly meter considering usage type Shelly struct { implement.Caps - shelly.Connection + conn *shelly.Connection usage string } @@ -45,7 +45,14 @@ func NewShellyFromConfig(other map[string]any) (api.Meter, error) { return nil, err } - if phases, ok := c.Connection.Generation.(shelly.Phases); ok { + // Three-phase Shelly energy meters count each phase separately (non-balanced), + // making their totals unsuitable for bidirectional grid metering. + if !(c.usage == "grid" && c.conn.IsThreePhase()) { + implement.Has(c, implement.MeterEnergy(c.conn.TotalEnergy)) + implement.Has(c, implement.MeterReturnEnergy(c.conn.ReturnEnergy)) + } + + if phases, ok := c.conn.Generation.(shelly.Phases); ok { implement.Has(c, implement.PhaseVoltages(phases.Voltages)) implement.Has(c, implement.PhaseCurrents(phases.Currents)) implement.Has(c, implement.PhasePowers(phases.Powers)) @@ -61,9 +68,9 @@ func NewShelly(uri, user, password, usage string, channel int, cache time.Durati return nil, err } c := &Shelly{ - Caps: implement.New(), - Connection: *conn, - usage: usage, + Caps: implement.New(), + conn: conn, + usage: usage, } return c, nil } @@ -72,7 +79,7 @@ var _ api.Meter = (*Shelly)(nil) // CurrentPower implements the api.Meter interface func (c *Shelly) CurrentPower() (float64, error) { - power, err := c.Connection.CurrentPower() + power, err := c.conn.CurrentPower() if err != nil { return 0, err } diff --git a/meter/shelly/connection.go b/meter/shelly/connection.go index abee49f3c..8be96ff53 100644 --- a/meter/shelly/connection.go +++ b/meter/shelly/connection.go @@ -18,6 +18,7 @@ type Generation interface { api.Meter api.MeterEnergy api.MeterReturnEnergy + IsThreePhase() bool } type Phases interface { diff --git a/meter/shelly/gen1.go b/meter/shelly/gen1.go index 3856a5840..65175a15e 100644 --- a/meter/shelly/gen1.go +++ b/meter/shelly/gen1.go @@ -144,6 +144,12 @@ func (c *gen1) ReturnEnergy() (float64, error) { return c.energy(energy) / 1000, nil } +// IsThreePhase reports whether the device is a three-phase energy meter. +func (c *gen1) IsThreePhase() bool { + res, err := c.status.Get() + return err == nil && len(res.EMeters) == 3 +} + // gen1Energy in kWh func (c *gen1) energy(energy float64) float64 { // Gen 1 Shelly EM devices are providing Watt hours, Gen 1 Shelly PM devices are providing Watt minutes diff --git a/meter/shelly/gen2.go b/meter/shelly/gen2.go index 2551c07a8..42e1602e8 100644 --- a/meter/shelly/gen2.go +++ b/meter/shelly/gen2.go @@ -332,6 +332,11 @@ func (c *gen2) hasEMEndpoint() bool { return c.hasMethod("EM.GetStatus") } +// IsThreePhase reports whether the device is a three-phase energy meter +func (c *gen2) IsThreePhase() bool { + return c.hasEMEndpoint() +} + // Gen2+ models using EM1.GetStatus endpoint for power and EM1Data.GetStatus for energy // https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM1#em1getstatus-example // https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM1Data#em1datagetstatus-example diff --git a/templates/definition/meter/shelly-3em.yaml b/templates/definition/meter/shelly-3em.yaml index 6d04da1a2..c8fa69b30 100644 --- a/templates/definition/meter/shelly-3em.yaml +++ b/templates/definition/meter/shelly-3em.yaml @@ -3,6 +3,9 @@ products: - brand: Shelly description: generic: 3EM (Gen.1) +# 3-phase Shelly EM devices count each phase's energy separately (non-balanced), +# so the totals are unsuitable for bidirectional grid metering. Energy and +# returnEnergy are therefore omitted for grid usage (see #29727). params: - name: usage choice: ["grid", "pv", "charge"] @@ -20,11 +23,18 @@ render: | source: http uri: {{ include "uri" . }}/status jq: .emeters | map(.power) | add + {{- if ne .usage "grid" }} energy: source: http uri: {{ include "uri" . }}/status jq: .emeters | map(.total) | add scale: 0.001 + returnenergy: + source: http + uri: {{ include "uri" . }}/status + jq: .emeters | map(.total_returned) | add + scale: 0.001 + {{- end }} currents: - source: http uri: {{ include "uri" . }}/emeter/0 diff --git a/templates/definition/meter/shelly-pro-3em.yaml b/templates/definition/meter/shelly-pro-3em.yaml index b3402666e..c8a28556d 100644 --- a/templates/definition/meter/shelly-pro-3em.yaml +++ b/templates/definition/meter/shelly-pro-3em.yaml @@ -2,6 +2,9 @@ template: shelly-pro-3em products: - { brand: Shelly, description: { generic: Pro 3 EM } } - { brand: Shelly, description: { generic: 3 EM-63T/W Gen3 } } +# 3-phase Shelly EM devices count each phase's energy separately (non-balanced), +# so the totals are unsuitable for bidirectional grid metering. The shelly meter +# suppresses energy and returnEnergy for grid usage (see #29727). params: - name: usage choice: ["grid", "pv", "charge"]