diff --git a/meter/shelly.go b/meter/shelly.go index 54ad929f4..fe54f2cca 100644 --- a/meter/shelly.go +++ b/meter/shelly.go @@ -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) } diff --git a/meter/shelly/connection.go b/meter/shelly/connection.go index 8be96ff53..46150008a 100644 --- a/meter/shelly/connection.go +++ b/meter/shelly/connection.go @@ -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 } diff --git a/meter/shelly/gen2.go b/meter/shelly/gen2.go index 42e1602e8..e98841cfc 100644 --- a/meter/shelly/gen2.go +++ b/meter/shelly/gen2.go @@ -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) diff --git a/meter/shelly/gen2_test.go b/meter/shelly/gen2_test.go index 678b929ac..b0e577486 100644 --- a/meter/shelly/gen2_test.go +++ b/meter/shelly/gen2_test.go @@ -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) { { diff --git a/meter/shelly_test.go b/meter/shelly_test.go new file mode 100644 index 000000000..7ebf35cd4 --- /dev/null +++ b/meter/shelly_test.go @@ -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)) + }) + } +}