Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"golang.org/x/oauth2"
)

//go:generate go tool mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,Tariff
//go:generate go tool mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,MeterReturnEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,Tariff

// Meter provides total active power in W
type Meter interface {
Expand All @@ -21,6 +21,11 @@ type MeterEnergy interface {
TotalEnergy() (float64, error)
}

// MeterReturnEnergy provides total returned/exported energy in kWh
type MeterReturnEnergy interface {
ReturnEnergy() (float64, error)
}

// PhaseCurrents provides per-phase current A
type PhaseCurrents interface {
Currents() (float64, float64, float64, error)
Expand Down
15 changes: 15 additions & 0 deletions api/implement/implementations.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 41 additions & 2 deletions api/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions charger/charger.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,13 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.C
implement.May(c, implement.Battery(soc))
implement.May(c, implement.SocLimiter(limitsoc))

// decorate measurements
powerG, energyG, err := cc.Energy.Configure(ctx)
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
if err != nil {
return nil, err
}
implement.May(c, implement.Meter(powerG))
implement.May(c, implement.MeterEnergy(energyG))
implement.May(c, implement.MeterReturnEnergy(returnG))

currentsG, voltagesG, _, err := cc.Phases.Configure(ctx)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion charger/easee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ func TestChargingSession_AtStartup_ProtectsSessionEnergy(t *testing.T) {
assert.Equal(t, 9173.5, total) // totalEnergy updated
}

func TestLifetimeEnergy_DoesNotDecreaseTotalEnergy(t *testing.T) {
func TestLifetimeEnergy_DoesNotDecreaseImportEnergy(t *testing.T) {
e := newEasee()
e.totalEnergy = 9173.5

Expand Down
3 changes: 2 additions & 1 deletion charger/heatpump.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,13 @@ func NewHeatpumpFromConfig(ctx context.Context, other map[string]any) (api.Charg
return nil, err
}

powerG, energyG, err := cc.Energy.Configure(ctx)
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
if err != nil {
return nil, err
}
implement.May(res, implement.Meter(powerG))
implement.May(res, implement.MeterEnergy(energyG))
implement.May(res, implement.MeterReturnEnergy(returnG))

tempG, limitTempG, err := cc.Temperature.Configure(ctx)
if err != nil {
Expand Down
17 changes: 12 additions & 5 deletions charger/measurement/energy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,31 @@ import (
)

type Energy struct {
Power *plugin.Config // optional
Energy *plugin.Config // optional
Power *plugin.Config
Energy *plugin.Config // optional
ReturnEnergy *plugin.Config // optional
}

func (cc *Energy) Configure(ctx context.Context) (
func() (float64, error),
func() (float64, error),
func() (float64, error),
error,
) {
powerG, err := cc.Power.FloatGetter(ctx)
if err != nil {
return nil, nil, fmt.Errorf("power: %w", err)
return nil, nil, nil, fmt.Errorf("power: %w", err)
}

energyG, err := cc.Energy.FloatGetter(ctx)
if err != nil {
return nil, nil, fmt.Errorf("energy: %w", err)
return nil, nil, nil, fmt.Errorf("energy: %w", err)
}

returnG, err := cc.ReturnEnergy.FloatGetter(ctx)
if err != nil {
return nil, nil, nil, fmt.Errorf("returnEnergy: %w", err)
}

return powerG, energyG, nil
return powerG, energyG, returnG, nil
}
3 changes: 2 additions & 1 deletion charger/sgready-relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ func NewSgReadyRelayFromConfig(ctx context.Context, other map[string]any) (api.C
return nil, err
}

powerG, energyG, err := cc.Energy.Configure(ctx)
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
if err != nil {
return nil, err
}
implement.May(res, implement.Meter(powerG))
implement.May(res, implement.MeterEnergy(energyG))
implement.May(res, implement.MeterReturnEnergy(returnG))

tempG, limitTempG, err := cc.Temperature.Configure(ctx)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion charger/sgready.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,13 @@ func NewSgReadyFromConfig(ctx context.Context, other map[string]any) (api.Charge
return nil, err
}

powerG, energyG, err := cc.Energy.Configure(ctx)
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
if err != nil {
return nil, err
}
implement.May(res, implement.Meter(powerG))
implement.May(res, implement.MeterEnergy(energyG))
implement.May(res, implement.MeterReturnEnergy(returnG))

tempG, limitTempG, err := cc.Temperature.Configure(ctx)
if err != nil {
Expand Down
9 changes: 8 additions & 1 deletion cmd/dumper.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,19 @@ func (d *dumper) Dump(name string, v any) {
}

if v, ok := api.Cap[api.MeterEnergy](v); ok {
d.measureTime(w, "Energy", func() (string, error) {
d.measureTime(w, "Import", func() (string, error) {
energy, err := v.TotalEnergy()
return fmt.Sprintf("%.1fkWh", energy), err
})
}

if v, ok := api.Cap[api.MeterReturnEnergy](v); ok {
d.measureTime(w, "Export", func() (string, error) {
energy, err := v.ReturnEnergy()
return fmt.Sprintf("%.1fkWh", energy), err
})
}

if v, ok := api.Cap[api.PhaseCurrents](v); ok {
d.measureTime(w, "Current L1..L3", func() (string, error) {
i1, i2, i3, err := v.Currents()
Expand Down
1 change: 1 addition & 0 deletions cmd/implement/implement.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func generate(out io.Writer) error {
reflect.TypeFor[api.MaxACPowerGetter](),
reflect.TypeFor[api.Meter](),
reflect.TypeFor[api.MeterEnergy](),
reflect.TypeFor[api.MeterReturnEnergy](),
reflect.TypeFor[api.PhaseCurrents](),
reflect.TypeFor[api.PhaseGetter](),
reflect.TypeFor[api.PhasePowers](),
Expand Down
2 changes: 1 addition & 1 deletion core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ func (lp *Loadpoint) requestUpdate() {
}

// capableMeter wraps a meter with capability lookup from its source.
// This preserves capability checks (like MeterEnergy, PhaseCurrents, PhaseVoltages) when
// This preserves capability checks (like MeterEnergy, MeterReturnEnergy, PhaseCurrents, PhaseVoltages) when
// the meter was extracted from a decorated charger's capability registry.
type capableMeter struct {
api.Meter
Expand Down
4 changes: 2 additions & 2 deletions core/metrics/accumulator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)

func TestMeterEnergyMeterTotal(t *testing.T) {
func TestMeterImportMeterTotal(t *testing.T) {
clock := clock.NewMock()
clock.Set(now.BeginningOfDay())

Expand All @@ -23,7 +23,7 @@ func TestMeterEnergyMeterTotal(t *testing.T) {
assert.Equal(t, 1.0, me.Imported())
}

func TestMeterEnergyAddPower(t *testing.T) {
func TestMeterImportAddPower(t *testing.T) {
clock := clock.NewMock()
clock.Set(now.BeginningOfDay())

Expand Down
17 changes: 13 additions & 4 deletions core/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ func meterCapabilities(name string, meter any) string {
panic("not a meter: " + name)
}

energy := api.HasCap[api.MeterEnergy](meter)
energy := api.HasCap[api.MeterEnergy](meter) || api.HasCap[api.MeterReturnEnergy](meter)
currents := api.HasCap[api.PhaseCurrents](meter)

name += ":"
Expand Down Expand Up @@ -516,10 +516,19 @@ func (site *Site) collectMeters(key string, meters []config.Device[api.Meter]) [
site.log.ERROR.Printf("%s %d power: %v", key, i+1, err)
}

// energy (production)
// energy (production for pv/battery, consumption for aux/ext)
var energy float64
if m, ok := api.Cap[api.MeterEnergy](meter); err == nil && ok {
energy, err = m.TotalEnergy()
if err == nil {
switch key {
case "pv", "battery":
if m, ok := api.Cap[api.MeterReturnEnergy](meter); ok {
energy, err = m.ReturnEnergy()
}
default:
if m, ok := api.Cap[api.MeterEnergy](meter); ok {
energy, err = m.TotalEnergy()
}
}
if err != nil {
site.log.ERROR.Printf("%s %d energy: %v", key, i+1, err)
}
Expand Down
2 changes: 1 addition & 1 deletion meter/homematic.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ var _ api.MeterEnergy = (*CCU)(nil)
// TotalEnergy implements the api.MeterEnergy interface
func (c *CCU) TotalEnergy() (float64, error) {
if c.usage == "grid" {
return c.conn.GridTotalEnergy()
return c.conn.GridImportEnergy()
}
return c.conn.TotalEnergy()
}
4 changes: 2 additions & 2 deletions meter/homematic/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ func (c *Connection) GridCurrentPower() (float64, error) {
return res.FloatValue("IEC_POWER"), err
}

// GridTotalEnergy reads the homematic HM-ES-TX-WM grid meterchannel energy in kWh
func (c *Connection) GridTotalEnergy() (float64, error) {
// GridImportEnergy reads the homematic HM-ES-TX-WM grid meterchannel energy in kWh
func (c *Connection) GridImportEnergy() (float64, error) {
res, err := c.meterG.Get()
return res.FloatValue("IEC_ENERGY_COUNTER"), err
}
Expand Down
17 changes: 12 additions & 5 deletions meter/measurement/energy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,31 @@ import (
)

type Energy struct {
Power plugin.Config
Energy *plugin.Config // optional
Power plugin.Config
Energy *plugin.Config // optional
ReturnEnergy *plugin.Config // optional
}

func (cc *Energy) Configure(ctx context.Context) (
func() (float64, error),
func() (float64, error),
func() (float64, error),
error,
) {
powerG, err := cc.Power.FloatGetter(ctx)
if err != nil {
return nil, nil, fmt.Errorf("power: %w", err)
return nil, nil, nil, fmt.Errorf("power: %w", err)
}

energyG, err := cc.Energy.FloatGetter(ctx)
if err != nil {
return nil, nil, fmt.Errorf("energy: %w", err)
return nil, nil, nil, fmt.Errorf("energy: %w", err)
}

returnG, err := cc.ReturnEnergy.FloatGetter(ctx)
if err != nil {
return nil, nil, nil, fmt.Errorf("returnEnergy: %w", err)
}

return powerG, energyG, nil
return powerG, energyG, returnG, nil
}
3 changes: 2 additions & 1 deletion meter/meter.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.M
return nil, err
}

powerG, energyG, err := cc.Energy.Configure(ctx)
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
if err != nil {
return nil, err
}

m, _ := NewConfigurable(powerG)
implement.May(m, implement.MeterEnergy(energyG))
implement.May(m, implement.MeterReturnEnergy(returnG))

// decorate soc
socG, err := cc.Soc.FloatGetter(ctx)
Expand Down
15 changes: 11 additions & 4 deletions meter/meter_average.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,16 @@ func NewMovingAverageFromConfig(ctx context.Context, other map[string]any) (api.

meter, _ := NewConfigurable(mav.CurrentPower)

// decorate energy reading
var totalEnergy func() (float64, error)
// decorate import reading
var importEnergy func() (float64, error)
if m, ok := api.Cap[api.MeterEnergy](m); ok {
totalEnergy = m.TotalEnergy
importEnergy = m.TotalEnergy
}

// decorate export reading
var exportEnergy func() (float64, error)
if m, ok := api.Cap[api.MeterReturnEnergy](m); ok {
exportEnergy = m.ReturnEnergy
}

// decorate battery reading
Expand All @@ -71,7 +77,8 @@ func NewMovingAverageFromConfig(ctx context.Context, other map[string]any) (api.
powers = m.Powers
}

implement.May(meter, implement.MeterEnergy(totalEnergy))
implement.May(meter, implement.MeterEnergy(importEnergy))
implement.May(meter, implement.MeterReturnEnergy(exportEnergy))

if batterySoc != nil {
implement.Has(meter, implement.Battery(batterySoc))
Expand Down
Loading