Shelly: fix PM1 Gen3 energy readings (#31957)

This commit is contained in:
Markus Thierolf 2026-07-20 12:28:12 +02:00 • committed by GitHub
parent 7b159d614c
commit f937ebec1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 66 additions and 8 deletions

View file

@ -48,8 +48,13 @@ func NewShellyFromConfig(other map[string]any) (api.Meter, error) {
// 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))
total, ret := c.conn.TotalEnergy, c.conn.ReturnEnergy
if c.usage == "pv" {
// reverse direction
total, ret = ret, total
}
implement.Has(c, implement.MeterEnergy(total))
implement.Has(c, implement.MeterReturnEnergy(ret))
}
if phases, ok := c.conn.Generation.(shelly.Phases); ok {
@ -83,8 +88,16 @@ func (c *Shelly) CurrentPower() (float64, error) {
if err != nil {
return 0, err
}
if c.usage == "pv" {
power = math.Abs(power)
}
return power, nil
return c.currentPowerForUsage(power, c.conn.SignedPower()), nil
}
// PV usage inverts directional power, otherwise the magnitude is used.
func (c *Shelly) currentPowerForUsage(power float64, signed bool) float64 {
if c.usage != "pv" {
return power
}
if signed {
return -power
}
return math.Abs(power)
}

View file

@ -30,6 +30,12 @@ type Phases interface {
// Connection is the Shelly connection
type Connection struct {
Generation
gen int
}
// SignedPower reports whether the device returns directional (signed) power.
func (c *Connection) SignedPower() bool {
return c.gen >= 3
}
// NewConnection creates a new Shelly device connection.
@ -79,7 +85,7 @@ func NewConnection(uri, user, password string, channel int, cache time.Duration)
}
}
conn := &Connection{gen}
conn := &Connection{Generation: gen, gen: resp.Gen}
return conn, nil
}

View file

@ -232,7 +232,10 @@ func (c *gen2) TotalEnergy() (float64, error) {
case c.hasSwitchEndpoint():
res, err := c.switchstatus.Get()
return res.Aenergy.Total / 1000, err
// https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch#status
// NOTE: ret_aenergy - the active energy added to this container is also added to aenergy container.
// All the consumed energy is collected in aenergy regardless of the direction(consumed or returned) of the active energy.
return max(0, res.Aenergy.Total-res.Ret_Aenergy.Total) / 1000, err
default:
return 0, fmt.Errorf("unknown shelly model: %s", c.model)

View file

@ -8,6 +8,12 @@ import (
"github.com/stretchr/testify/require"
)
func TestSignedPower(t *testing.T) {
assert.False(t, (&Connection{gen: 1}).SignedPower())
assert.False(t, (&Connection{gen: 2}).SignedPower())
assert.True(t, (&Connection{gen: 3}).SignedPower())
}
// Test Gen2+ status responses
func TestUnmarshalGen2StatusResponse(t *testing.T) {
{

30
meter/shelly_test.go Normal file
View file

@ -0,0 +1,30 @@
package meter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestShellyCurrentPowerForUsage(t *testing.T) {
tests := []struct {
name string
usage string
signed bool
power float64
want float64
}{
{name: "grid keeps sign", usage: "grid", power: -350, want: -350},
{name: "unsigned pv uses absolute value", usage: "pv", power: -350, want: 350},
{name: "unsigned pv keeps positive values", usage: "pv", power: 350, want: 350},
{name: "signed pv inverts positive values", usage: "pv", signed: true, power: 350, want: -350},
{name: "signed pv inverts negative values", usage: "pv", signed: true, power: -350, want: 350},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
m := &Shelly{usage: tc.usage}
assert.Equal(t, tc.want, m.currentPowerForUsage(tc.power, tc.signed))
})
}
}