From 1861f9a715e508ae7a6f78be02fd23f8b8a7fc2e Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 22 Jul 2026 11:26:54 +0200 Subject: [PATCH 1/4] Optimizer: configurable battery efficiency --- api/api.go | 5 ++ api/implement/implementations.go | 15 ++++++ cmd/implement/implement.go | 1 + core/site_optimizer.go | 35 +++++++++++-- core/site_optimizer_test.go | 28 ++++++++++ meter/config/config.go | 50 ++++++++++++++++++ meter/config/config_test.go | 60 ++++++++++++++++++++++ meter/template_test.go | 18 +++++++ util/templates/defaults.yaml | 13 +++++ util/templates/includes/battery-params.tpl | 5 ++ 10 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 meter/config/config_test.go diff --git a/api/api.go b/api/api.go index 3535b9dc73d..282e511dc5f 100644 --- a/api/api.go +++ b/api/api.go @@ -59,6 +59,11 @@ type BatterySocLimiter interface { GetSocLimits() (min, max float64) } +// BatteryEfficiency provides the battery charge/discharge efficiency in % +type BatteryEfficiency interface { + GetEfficiency() float64 +} + // MaxACPowerGetter provides max AC power in W type MaxACPowerGetter interface { MaxACPower() float64 diff --git a/api/implement/implementations.go b/api/implement/implementations.go index 610e8b7dfe2..01fe1ff4d06 100644 --- a/api/implement/implementations.go +++ b/api/implement/implementations.go @@ -53,6 +53,21 @@ func (i *iBatteryController) SetBatteryMode(p0 api.BatteryMode) error { return i.batteryController0(p0) } +func BatteryEfficiency(batteryEfficiency0 func() float64) api.BatteryEfficiency { + if batteryEfficiency0 == nil { + return nil + } + return &iBatteryEfficiency{batteryEfficiency0} +} + +type iBatteryEfficiency struct { + batteryEfficiency0 func() float64 +} + +func (i *iBatteryEfficiency) GetEfficiency() float64 { + return i.batteryEfficiency0() +} + func BatteryPowerLimiter(batteryPowerLimiter0 func() (float64, float64)) api.BatteryPowerLimiter { if batteryPowerLimiter0 == nil { return nil diff --git a/cmd/implement/implement.go b/cmd/implement/implement.go index ec034e1f4e1..7c4a596560d 100644 --- a/cmd/implement/implement.go +++ b/cmd/implement/implement.go @@ -61,6 +61,7 @@ func generate(out io.Writer) error { reflect.TypeFor[api.Battery](), reflect.TypeFor[api.BatteryCapacity](), reflect.TypeFor[api.BatteryController](), + reflect.TypeFor[api.BatteryEfficiency](), reflect.TypeFor[api.BatteryPowerLimiter](), reflect.TypeFor[api.BatterySocLimiter](), reflect.TypeFor[api.ChargeController](), diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 85ff2b828f6..75d5e0e832e 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -320,13 +320,15 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { ft = prorate(ftSlots, firstSlotDuration) } + batteryEta := site.batteryEta(battery) + req := optimizer.OptimizationInput{ Strategy: optimizer.OptimizerStrategy{ ChargingStrategy: optimizer.OptimizerStrategyChargingStrategy(site.GetOptimizerChargingStrategy()), DischargingStrategy: optimizer.OptimizerStrategyDischargingStrategyDischargeBeforeImport, }, - EtaC: eta, - EtaD: eta, + EtaC: batteryEta, + EtaD: batteryEta, TimeSeries: optimizer.TimeSeries{ Dt: dt, Gt: prorate(gt, firstSlotDuration), @@ -337,7 +339,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { } // end of horizon Wh value - pa := lo.Min(req.TimeSeries.PN) * eta * 0.99 + pa := lo.Min(req.TimeSeries.PN) * batteryEta * 0.99 details := requestDetails{ Timestamps: asTimestamps(dt, now), @@ -659,6 +661,33 @@ func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDurati return bat, detail } +// batteryEta returns the capacity-weighted charge/discharge efficiency of the home +// batteries. Weighted since the optimizer api only accepts a single global value. +func (site *Site) batteryEta(battery []types.Measurement) float32 { + var sum, capacity float64 + + for i, dev := range site.batteryMeters { + if i >= len(battery) || battery[i].Capacity == nil { + continue + } + c := *battery[i].Capacity + + e := float64(eta) + if m, ok := api.Cap[api.BatteryEfficiency](dev.Instance()); ok { + e = m.GetEfficiency() / 100 + } + + sum += e * c + capacity += c + } + + if capacity == 0 { + return eta + } + + return float32(sum / capacity) +} + func (site *Site) batteryRequest(dev config.Device[api.Meter], b types.Measurement, grid api.Rates, minLen int, firstSlotDuration time.Duration) (optimizer.BatteryConfig, batteryDetail) { bat := optimizer.BatteryConfig{ CMax: batteryPower, diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index 8b667b7c32c..e9df9b41c4b 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -322,3 +322,31 @@ func TestCurrentSlotSuggestion(t *testing.T) { // no result yields an empty suggestion assert.Empty(t, currentSlotSuggestion(batteryDetail{Type: batteryTypeBattery}, optimizer.BatteryResult{}, true, false, 1, "")) } + +type effMeter struct { + api.Meter +} + +func (m effMeter) GetEfficiency() float64 { + return 100 +} + +func TestBatteryEta(t *testing.T) { + device := func(m api.Meter) config.Device[api.Meter] { + return config.NewStaticDevice(config.Named{}, m) + } + measurement := func(capacity float64) types.Measurement { + return types.Measurement{Capacity: &capacity} + } + + // no batteries + assert.Equal(t, eta, (&Site{}).batteryEta(nil)) + + // unconfigured battery uses the default + site := &Site{batteryMeters: []config.Device[api.Meter]{device(nil)}} + assert.Equal(t, eta, site.batteryEta([]types.Measurement{measurement(10)})) + + // capacity-weighted average of 100% (10 kWh) and default 90% (30 kWh) + site = &Site{batteryMeters: []config.Device[api.Meter]{device(effMeter{}), device(nil)}} + assert.InDelta(t, 0.925, site.batteryEta([]types.Measurement{measurement(10), measurement(30)}), 1e-6) +} diff --git a/meter/config/config.go b/meter/config/config.go index d516142350a..45c885299b4 100644 --- a/meter/config/config.go +++ b/meter/config/config.go @@ -3,15 +3,22 @@ package config import ( "context" "fmt" + "maps" "strings" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/api/implement" "github.com/evcc-io/evcc/util" reg "github.com/evcc-io/evcc/util/registry" + "github.com/spf13/cast" ) var Registry = reg.New[api.Meter]("meter") +// efficiencyParam is the battery efficiency setting. It is handled here instead of +// in the device implementations since it is not used by the device itself. +const efficiencyParam = "efficiency" + // NewFromConfig creates meter from configuration func NewFromConfig(ctx context.Context, typ string, other map[string]any) (api.Meter, error) { factory, err := Registry.Get(strings.ToLower(typ)) @@ -19,10 +26,53 @@ func NewFromConfig(ctx context.Context, typ string, other map[string]any) (api.M return nil, err } + // templates are re-entered with the rendered config, hence handled there + var efficiency float64 + if !strings.EqualFold(typ, "template") { + if efficiency, other, err = extractEfficiency(other); err != nil { + return nil, fmt.Errorf("cannot create meter type '%s': %w", typ, err) + } + } + v, err := factory(ctx, other) if err != nil { return nil, fmt.Errorf("cannot create meter type '%s': %w", util.TypeWithTemplateName(typ, other), err) } + if efficiency > 0 { + caps, ok := v.(implement.Caps) + if !ok { + return nil, fmt.Errorf("cannot create meter type '%s': %s not supported", typ, efficiencyParam) + } + implement.Has(caps, implement.BatteryEfficiency(func() float64 { return efficiency })) + } + return v, nil } + +// extractEfficiency removes the efficiency setting from the config and returns its +// value in %. It returns 0 if the setting is not configured. +func extractEfficiency(other map[string]any) (float64, map[string]any, error) { + val, ok := other[efficiencyParam] + if !ok { + return 0, other, nil + } + + res := maps.Clone(other) + delete(res, efficiencyParam) + + if val == nil || val == "" { + return 0, res, nil + } + + efficiency, err := cast.ToFloat64E(val) + if err != nil { + return 0, nil, fmt.Errorf("%s: %w", efficiencyParam, err) + } + + if efficiency <= 0 || efficiency > 100 { + return 0, nil, fmt.Errorf("%s: invalid value %v", efficiencyParam, val) + } + + return efficiency, res, nil +} diff --git a/meter/config/config_test.go b/meter/config/config_test.go new file mode 100644 index 00000000000..aaa23d3a0e6 --- /dev/null +++ b/meter/config/config_test.go @@ -0,0 +1,60 @@ +package config + +import ( + "context" + "testing" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/api/implement" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testMeter struct { + implement.Caps +} + +func (m *testMeter) CurrentPower() (float64, error) { + return 0, nil +} + +func TestEfficiency(t *testing.T) { + var received map[string]any + + Registry.Add("test-efficiency", func(other map[string]any) (api.Meter, error) { + received = other + return &testMeter{Caps: implement.New()}, nil + }) + + for _, tc := range []struct { + val any + expected float64 + }{ + {nil, 0}, + {"", 0}, + {95, 95}, + {"95", 95}, + {95.5, 95.5}, + } { + m, err := NewFromConfig(context.TODO(), "test-efficiency", map[string]any{"efficiency": tc.val, "foo": "bar"}) + require.NoError(t, err, tc) + + // efficiency is not passed on to the device + assert.NotContains(t, received, "efficiency", tc) + assert.Contains(t, received, "foo", tc) + + eff, ok := api.Cap[api.BatteryEfficiency](m) + if tc.expected == 0 { + assert.False(t, ok, tc) + continue + } + + require.True(t, ok, tc) + assert.Equal(t, tc.expected, eff.GetEfficiency(), tc) + } + + for _, val := range []any{0, -1, 101, "foo"} { + _, err := NewFromConfig(context.TODO(), "test-efficiency", map[string]any{"efficiency": val}) + assert.Error(t, err, val) + } +} diff --git a/meter/template_test.go b/meter/template_test.go index 227ca0f063c..7f768dde1f7 100644 --- a/meter/template_test.go +++ b/meter/template_test.go @@ -6,6 +6,8 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util/templates" "github.com/evcc-io/evcc/util/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var acceptable = []string{ @@ -48,3 +50,19 @@ func TestTemplates(t *testing.T) { } }) } + +// TestTemplateEfficiency ensures the efficiency setting survives template rendering +func TestTemplateEfficiency(t *testing.T) { + m, err := NewFromConfig(t.Context(), "template", map[string]any{ + "template": "demo-battery", + "usage": "battery", + "power": 1000, + "soc": 50, + "efficiency": 95, + }) + require.NoError(t, err) + + eff, ok := api.Cap[api.BatteryEfficiency](m) + require.True(t, ok) + assert.Equal(t, 95.0, eff.GetEfficiency()) +} diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index 13971430a35..7843d2462bf 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -160,6 +160,18 @@ params: example: 95 type: int usages: ["battery"] + - name: efficiency + unit: "%" + description: + de: Wirkungsgrad + en: Efficiency + help: + de: Lade- und Entladewirkungsgrad der Batterie für die Optimierung + en: Charge and discharge efficiency of the battery used for optimization + default: 90 + advanced: true + type: int + usages: ["battery"] - name: mincurrent unit: A description: @@ -823,6 +835,7 @@ presets: - name: maxsoc - name: maxchargepower - name: maxdischargepower + - name: efficiency battery-capacity: - name: capacity diff --git a/util/templates/includes/battery-params.tpl b/util/templates/includes/battery-params.tpl index 4467af10e0e..3d63f0bb5ef 100644 --- a/util/templates/includes/battery-params.tpl +++ b/util/templates/includes/battery-params.tpl @@ -12,8 +12,13 @@ maxchargepower: {{ .maxchargepower }} # W maxdischargepower: {{ .maxdischargepower }} # W {{- end }} +{{ define "battery-efficiency" }} +efficiency: {{ .efficiency }} # % +{{- end }} + {{ define "battery-params" }} {{- include "battery-capacity" . }} {{- include "battery-minmaxsoc" . }} {{- include "battery-power" . }} +{{- include "battery-efficiency" . }} {{- end }} From 14c07d7c89207f89946ec487498c7277d0797d67 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 22 Jul 2026 11:37:27 +0200 Subject: [PATCH 2/4] Use integer percent efficiency --- api/api.go | 2 +- api/implement/implementations.go | 6 +++--- core/site_optimizer.go | 4 +++- core/site_optimizer_test.go | 2 +- meter/config/config.go | 8 ++++---- meter/config/config_test.go | 5 ++--- meter/template_test.go | 2 +- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/api/api.go b/api/api.go index 282e511dc5f..72e21151c44 100644 --- a/api/api.go +++ b/api/api.go @@ -61,7 +61,7 @@ type BatterySocLimiter interface { // BatteryEfficiency provides the battery charge/discharge efficiency in % type BatteryEfficiency interface { - GetEfficiency() float64 + Efficiency() int64 } // MaxACPowerGetter provides max AC power in W diff --git a/api/implement/implementations.go b/api/implement/implementations.go index 01fe1ff4d06..f4dcd0e9ae3 100644 --- a/api/implement/implementations.go +++ b/api/implement/implementations.go @@ -53,7 +53,7 @@ func (i *iBatteryController) SetBatteryMode(p0 api.BatteryMode) error { return i.batteryController0(p0) } -func BatteryEfficiency(batteryEfficiency0 func() float64) api.BatteryEfficiency { +func BatteryEfficiency(batteryEfficiency0 func() int64) api.BatteryEfficiency { if batteryEfficiency0 == nil { return nil } @@ -61,10 +61,10 @@ func BatteryEfficiency(batteryEfficiency0 func() float64) api.BatteryEfficiency } type iBatteryEfficiency struct { - batteryEfficiency0 func() float64 + batteryEfficiency0 func() int64 } -func (i *iBatteryEfficiency) GetEfficiency() float64 { +func (i *iBatteryEfficiency) Efficiency() int64 { return i.batteryEfficiency0() } diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 75d5e0e832e..16a40560aed 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -674,7 +674,9 @@ func (site *Site) batteryEta(battery []types.Measurement) float32 { e := float64(eta) if m, ok := api.Cap[api.BatteryEfficiency](dev.Instance()); ok { - e = m.GetEfficiency() / 100 + if v := m.Efficiency(); v > 0 { + e = float64(v) / 100 + } } sum += e * c diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index e9df9b41c4b..4d2bc37fe68 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -327,7 +327,7 @@ type effMeter struct { api.Meter } -func (m effMeter) GetEfficiency() float64 { +func (m effMeter) Efficiency() int64 { return 100 } diff --git a/meter/config/config.go b/meter/config/config.go index 45c885299b4..b5cb0102a12 100644 --- a/meter/config/config.go +++ b/meter/config/config.go @@ -27,7 +27,7 @@ func NewFromConfig(ctx context.Context, typ string, other map[string]any) (api.M } // templates are re-entered with the rendered config, hence handled there - var efficiency float64 + var efficiency int64 if !strings.EqualFold(typ, "template") { if efficiency, other, err = extractEfficiency(other); err != nil { return nil, fmt.Errorf("cannot create meter type '%s': %w", typ, err) @@ -44,7 +44,7 @@ func NewFromConfig(ctx context.Context, typ string, other map[string]any) (api.M if !ok { return nil, fmt.Errorf("cannot create meter type '%s': %s not supported", typ, efficiencyParam) } - implement.Has(caps, implement.BatteryEfficiency(func() float64 { return efficiency })) + implement.Has(caps, implement.BatteryEfficiency(func() int64 { return efficiency })) } return v, nil @@ -52,7 +52,7 @@ func NewFromConfig(ctx context.Context, typ string, other map[string]any) (api.M // extractEfficiency removes the efficiency setting from the config and returns its // value in %. It returns 0 if the setting is not configured. -func extractEfficiency(other map[string]any) (float64, map[string]any, error) { +func extractEfficiency(other map[string]any) (int64, map[string]any, error) { val, ok := other[efficiencyParam] if !ok { return 0, other, nil @@ -65,7 +65,7 @@ func extractEfficiency(other map[string]any) (float64, map[string]any, error) { return 0, res, nil } - efficiency, err := cast.ToFloat64E(val) + efficiency, err := cast.ToInt64E(val) if err != nil { return 0, nil, fmt.Errorf("%s: %w", efficiencyParam, err) } diff --git a/meter/config/config_test.go b/meter/config/config_test.go index aaa23d3a0e6..8071cd9985c 100644 --- a/meter/config/config_test.go +++ b/meter/config/config_test.go @@ -28,13 +28,12 @@ func TestEfficiency(t *testing.T) { for _, tc := range []struct { val any - expected float64 + expected int64 }{ {nil, 0}, {"", 0}, {95, 95}, {"95", 95}, - {95.5, 95.5}, } { m, err := NewFromConfig(context.TODO(), "test-efficiency", map[string]any{"efficiency": tc.val, "foo": "bar"}) require.NoError(t, err, tc) @@ -50,7 +49,7 @@ func TestEfficiency(t *testing.T) { } require.True(t, ok, tc) - assert.Equal(t, tc.expected, eff.GetEfficiency(), tc) + assert.Equal(t, tc.expected, eff.Efficiency(), tc) } for _, val := range []any{0, -1, 101, "foo"} { diff --git a/meter/template_test.go b/meter/template_test.go index 7f768dde1f7..3515907e4e7 100644 --- a/meter/template_test.go +++ b/meter/template_test.go @@ -64,5 +64,5 @@ func TestTemplateEfficiency(t *testing.T) { eff, ok := api.Cap[api.BatteryEfficiency](m) require.True(t, ok) - assert.Equal(t, 95.0, eff.GetEfficiency()) + assert.Equal(t, int64(95), eff.Efficiency()) } From 7e50029b4e3eb91e360a180e73026f5f1620dbed Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 22 Jul 2026 11:47:38 +0200 Subject: [PATCH 3/4] Use per-battery efficiency --- core/site_optimizer.go | 44 +++++++++---------------------------- core/site_optimizer_test.go | 29 ++++++++++-------------- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 25 insertions(+), 54 deletions(-) diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 16a40560aed..684f3b3e4ee 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -320,15 +320,13 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { ft = prorate(ftSlots, firstSlotDuration) } - batteryEta := site.batteryEta(battery) - req := optimizer.OptimizationInput{ Strategy: optimizer.OptimizerStrategy{ ChargingStrategy: optimizer.OptimizerStrategyChargingStrategy(site.GetOptimizerChargingStrategy()), DischargingStrategy: optimizer.OptimizerStrategyDischargingStrategyDischargeBeforeImport, }, - EtaC: batteryEta, - EtaD: batteryEta, + EtaC: eta, + EtaD: eta, TimeSeries: optimizer.TimeSeries{ Dt: dt, Gt: prorate(gt, firstSlotDuration), @@ -339,7 +337,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { } // end of horizon Wh value - pa := lo.Min(req.TimeSeries.PN) * batteryEta * 0.99 + pa := lo.Min(req.TimeSeries.PN) * eta * 0.99 details := requestDetails{ Timestamps: asTimestamps(dt, now), @@ -661,35 +659,6 @@ func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDurati return bat, detail } -// batteryEta returns the capacity-weighted charge/discharge efficiency of the home -// batteries. Weighted since the optimizer api only accepts a single global value. -func (site *Site) batteryEta(battery []types.Measurement) float32 { - var sum, capacity float64 - - for i, dev := range site.batteryMeters { - if i >= len(battery) || battery[i].Capacity == nil { - continue - } - c := *battery[i].Capacity - - e := float64(eta) - if m, ok := api.Cap[api.BatteryEfficiency](dev.Instance()); ok { - if v := m.Efficiency(); v > 0 { - e = float64(v) / 100 - } - } - - sum += e * c - capacity += c - } - - if capacity == 0 { - return eta - } - - return float32(sum / capacity) -} - func (site *Site) batteryRequest(dev config.Device[api.Meter], b types.Measurement, grid api.Rates, minLen int, firstSlotDuration time.Duration) (optimizer.BatteryConfig, batteryDetail) { bat := optimizer.BatteryConfig{ CMax: batteryPower, @@ -712,6 +681,13 @@ func (site *Site) batteryRequest(dev config.Device[api.Meter], b types.Measureme bat.DMax = float32(discharge) } + if m, ok := api.Cap[api.BatteryEfficiency](instance); ok { + if e := m.Efficiency(); e > 0 { + bat.EtaC = float32(e) / 100 + bat.EtaD = bat.EtaC + } + } + if m, ok := api.Cap[api.BatterySocLimiter](instance); ok { minSoc, maxSoc := m.GetSocLimits() if maxSoc == 0 { diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index 4d2bc37fe68..5c162c766a6 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -328,25 +328,20 @@ type effMeter struct { } func (m effMeter) Efficiency() int64 { - return 100 + return 95 } -func TestBatteryEta(t *testing.T) { - device := func(m api.Meter) config.Device[api.Meter] { - return config.NewStaticDevice(config.Named{}, m) - } - measurement := func(capacity float64) types.Measurement { - return types.Measurement{Capacity: &capacity} - } - - // no batteries - assert.Equal(t, eta, (&Site{}).batteryEta(nil)) +func TestBatteryEfficiency(t *testing.T) { + site := &Site{} + capacity, soc := 10.0, 50.0 + b := types.Measurement{Capacity: &capacity, Soc: &soc} - // unconfigured battery uses the default - site := &Site{batteryMeters: []config.Device[api.Meter]{device(nil)}} - assert.Equal(t, eta, site.batteryEta([]types.Measurement{measurement(10)})) + // unconfigured battery leaves the request default + bat, _ := site.batteryRequest(config.NewStaticDevice[api.Meter](config.Named{}, nil), b, nil, 1, time.Hour) + assert.Zero(t, bat.EtaC) + assert.Zero(t, bat.EtaD) - // capacity-weighted average of 100% (10 kWh) and default 90% (30 kWh) - site = &Site{batteryMeters: []config.Device[api.Meter]{device(effMeter{}), device(nil)}} - assert.InDelta(t, 0.925, site.batteryEta([]types.Measurement{measurement(10), measurement(30)}), 1e-6) + bat, _ = site.batteryRequest(config.NewStaticDevice[api.Meter](config.Named{}, effMeter{}), b, nil, 1, time.Hour) + assert.Equal(t, float32(0.95), bat.EtaC) + assert.Equal(t, float32(0.95), bat.EtaD) } diff --git a/go.mod b/go.mod index 6b5112e706f..9b67e3dd428 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/enbility/ship-go v0.6.1-0.20260720110450-0aa90f64ac76 github.com/enbility/spine-go v0.7.1-0.20260629113257-b3bcc643f323 github.com/evcc-io/openapi-mcp v0.6.1-0.20260701153510-26c442199ef4 - github.com/evcc-io/optimizer v0.0.0-20260719151136-b6c835a178a3 + github.com/evcc-io/optimizer v0.0.0-20260722084459-8e6909402d49 github.com/evcc-io/rct v0.2.0 github.com/evcc-io/tesla-proxy-client v0.0.0-20260722070723-f3604323d820 github.com/fatih/structs v1.1.0 diff --git a/go.sum b/go.sum index 17a4ea78fa4..f0bee6b19ff 100644 --- a/go.sum +++ b/go.sum @@ -152,8 +152,8 @@ github.com/evcc-io/ocpp-go v0.0.0-20251212212612-b7f92ee0443b h1:5vFr7wOwoi5ylyb github.com/evcc-io/ocpp-go v0.0.0-20251212212612-b7f92ee0443b/go.mod h1:2kcukDdhui4u730VfnYVWuwzDLgw+mBRGDir/QAyBhg= github.com/evcc-io/openapi-mcp v0.6.1-0.20260701153510-26c442199ef4 h1:aGM3NwDsKbnmiaO10ptbvEolRAUThWJEsObiAWFDU/c= github.com/evcc-io/openapi-mcp v0.6.1-0.20260701153510-26c442199ef4/go.mod h1:YyOx4zwr6xVUcchAETHERZ+75cZMIrdcjVIZFwBlE1Q= -github.com/evcc-io/optimizer v0.0.0-20260719151136-b6c835a178a3 h1:QXk6AKYrYWQ+PxXhJS/LcHnegKFCCClUQ/BlCuqbs9Y= -github.com/evcc-io/optimizer v0.0.0-20260719151136-b6c835a178a3/go.mod h1:cHMeq6GfoXQ8LxqY4LH1wn/hrgYEn+ka5RPI1CF37SY= +github.com/evcc-io/optimizer v0.0.0-20260722084459-8e6909402d49 h1:4rutNWH9/rAdfKEbKIif3u8XuUHR8TnGbZAGffoQR0U= +github.com/evcc-io/optimizer v0.0.0-20260722084459-8e6909402d49/go.mod h1:cHMeq6GfoXQ8LxqY4LH1wn/hrgYEn+ka5RPI1CF37SY= github.com/evcc-io/rct v0.2.0 h1:qjXKI7NKwW5YpEKEb27MwLMBaR4ne2Kd2cPV2PYTKn4= github.com/evcc-io/rct v0.2.0/go.mod h1:n6MTBU36QOadGlxxiADu86VaT5l0eYdbmFBHF14AD/s= github.com/evcc-io/tesla-proxy-client v0.0.0-20260722070723-f3604323d820 h1:qpLoOYEJ6dgD+UBEgwzsIiQ3WLPmi3Xb7EN4wFIypQY= From a968a6535a0573f228d3f26a06406365acac1f21 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 22 Jul 2026 13:10:13 +0200 Subject: [PATCH 4/4] Handle efficiency like other battery params --- meter/bosch_bpts5_hybrid.go | 6 +- meter/config/config.go | 50 ---------------- meter/config/config_test.go | 59 ------------------- meter/e3dc.go | 6 +- meter/ecoflow.go | 6 +- meter/homeassistant.go | 2 + meter/lgess.go | 6 +- meter/mbmd.go | 2 + meter/meter.go | 2 + meter/powerwall.go | 6 +- meter/rct.go | 6 +- meter/sma.go | 6 +- meter/usage_battery.go | 16 +++++ meter/zendure.go | 2 + .../definition/meter/tesla-powerwall.yaml | 2 + 15 files changed, 54 insertions(+), 123 deletions(-) delete mode 100644 meter/config/config_test.go diff --git a/meter/bosch_bpts5_hybrid.go b/meter/bosch_bpts5_hybrid.go index 1984ec4c179..3c2c2c5a720 100644 --- a/meter/bosch_bpts5_hybrid.go +++ b/meter/bosch_bpts5_hybrid.go @@ -25,6 +25,7 @@ func init() { func NewBoschBpts5HybridFromConfig(other map[string]any) (api.Meter, error) { var cc struct { batteryCapacity `mapstructure:",squash"` + batteryEfficiency `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` URI string @@ -40,11 +41,11 @@ func NewBoschBpts5HybridFromConfig(other map[string]any) (api.Meter, error) { return nil, errors.New("missing usage") } - return NewBoschBpts5Hybrid(cc.URI, cc.Usage, cc.Cache, cc.batteryCapacity.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator()) + return NewBoschBpts5Hybrid(cc.URI, cc.Usage, cc.Cache, cc.batteryCapacity.Decorator(), cc.batteryEfficiency.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator()) } // NewBoschBpts5Hybrid creates a Bosch BPT-S 5 Hybrid Meter -func NewBoschBpts5Hybrid(uri, usage string, cache time.Duration, capacity func() float64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (*BoschBpts5Hybrid, error) { +func NewBoschBpts5Hybrid(uri, usage string, cache time.Duration, capacity func() float64, efficiency func() int64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (*BoschBpts5Hybrid, error) { log := util.NewLogger("bosch-bpt") instance, exists := bosch.Instances.LoadOrStore(uri, bosch.NewLocal(log, uri, cache)) @@ -65,6 +66,7 @@ func NewBoschBpts5Hybrid(uri, usage string, cache time.Duration, capacity func() implement.May(m, implement.BatteryCapacity(capacity)) implement.May(m, implement.BatterySocLimiter(batterySocLimits)) implement.May(m, implement.BatteryPowerLimiter(batteryPowerLimits)) + implement.May(m, implement.BatteryEfficiency(efficiency)) } return m, nil diff --git a/meter/config/config.go b/meter/config/config.go index b5cb0102a12..d516142350a 100644 --- a/meter/config/config.go +++ b/meter/config/config.go @@ -3,22 +3,15 @@ package config import ( "context" "fmt" - "maps" "strings" "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/api/implement" "github.com/evcc-io/evcc/util" reg "github.com/evcc-io/evcc/util/registry" - "github.com/spf13/cast" ) var Registry = reg.New[api.Meter]("meter") -// efficiencyParam is the battery efficiency setting. It is handled here instead of -// in the device implementations since it is not used by the device itself. -const efficiencyParam = "efficiency" - // NewFromConfig creates meter from configuration func NewFromConfig(ctx context.Context, typ string, other map[string]any) (api.Meter, error) { factory, err := Registry.Get(strings.ToLower(typ)) @@ -26,53 +19,10 @@ func NewFromConfig(ctx context.Context, typ string, other map[string]any) (api.M return nil, err } - // templates are re-entered with the rendered config, hence handled there - var efficiency int64 - if !strings.EqualFold(typ, "template") { - if efficiency, other, err = extractEfficiency(other); err != nil { - return nil, fmt.Errorf("cannot create meter type '%s': %w", typ, err) - } - } - v, err := factory(ctx, other) if err != nil { return nil, fmt.Errorf("cannot create meter type '%s': %w", util.TypeWithTemplateName(typ, other), err) } - if efficiency > 0 { - caps, ok := v.(implement.Caps) - if !ok { - return nil, fmt.Errorf("cannot create meter type '%s': %s not supported", typ, efficiencyParam) - } - implement.Has(caps, implement.BatteryEfficiency(func() int64 { return efficiency })) - } - return v, nil } - -// extractEfficiency removes the efficiency setting from the config and returns its -// value in %. It returns 0 if the setting is not configured. -func extractEfficiency(other map[string]any) (int64, map[string]any, error) { - val, ok := other[efficiencyParam] - if !ok { - return 0, other, nil - } - - res := maps.Clone(other) - delete(res, efficiencyParam) - - if val == nil || val == "" { - return 0, res, nil - } - - efficiency, err := cast.ToInt64E(val) - if err != nil { - return 0, nil, fmt.Errorf("%s: %w", efficiencyParam, err) - } - - if efficiency <= 0 || efficiency > 100 { - return 0, nil, fmt.Errorf("%s: invalid value %v", efficiencyParam, val) - } - - return efficiency, res, nil -} diff --git a/meter/config/config_test.go b/meter/config/config_test.go deleted file mode 100644 index 8071cd9985c..00000000000 --- a/meter/config/config_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package config - -import ( - "context" - "testing" - - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/api/implement" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type testMeter struct { - implement.Caps -} - -func (m *testMeter) CurrentPower() (float64, error) { - return 0, nil -} - -func TestEfficiency(t *testing.T) { - var received map[string]any - - Registry.Add("test-efficiency", func(other map[string]any) (api.Meter, error) { - received = other - return &testMeter{Caps: implement.New()}, nil - }) - - for _, tc := range []struct { - val any - expected int64 - }{ - {nil, 0}, - {"", 0}, - {95, 95}, - {"95", 95}, - } { - m, err := NewFromConfig(context.TODO(), "test-efficiency", map[string]any{"efficiency": tc.val, "foo": "bar"}) - require.NoError(t, err, tc) - - // efficiency is not passed on to the device - assert.NotContains(t, received, "efficiency", tc) - assert.Contains(t, received, "foo", tc) - - eff, ok := api.Cap[api.BatteryEfficiency](m) - if tc.expected == 0 { - assert.False(t, ok, tc) - continue - } - - require.True(t, ok, tc) - assert.Equal(t, tc.expected, eff.Efficiency(), tc) - } - - for _, val := range []any{0, -1, 101, "foo"} { - _, err := NewFromConfig(context.TODO(), "test-efficiency", map[string]any{"efficiency": val}) - assert.Error(t, err, val) - } -} diff --git a/meter/e3dc.go b/meter/e3dc.go index a9eab925e1f..8218d9781c5 100644 --- a/meter/e3dc.go +++ b/meter/e3dc.go @@ -34,6 +34,7 @@ func init() { func NewE3dcFromConfig(other map[string]any) (api.Meter, error) { cc := struct { batteryCapacity `mapstructure:",squash"` + batteryEfficiency `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` pvMaxACPower `mapstructure:",squash"` @@ -71,12 +72,12 @@ func NewE3dcFromConfig(other map[string]any) (api.Meter, error) { ReceiveTimeout: cc.Timeout, } - return NewE3dc(cfg, cc.Usage, cc.DischargeLimit, cc.ExternalPower, cc.batteryCapacity.Decorator(), cc.pvMaxACPower.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator()) + return NewE3dc(cfg, cc.Usage, cc.DischargeLimit, cc.ExternalPower, cc.batteryCapacity.Decorator(), cc.pvMaxACPower.Decorator(), cc.batteryEfficiency.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator()) } var e3dcOnce sync.Once -func NewE3dc(cfg rscp.ClientConfig, usage templates.Usage, dischargeLimit uint32, externalPower bool, capacity, maxacpower func() float64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (api.Meter, error) { +func NewE3dc(cfg rscp.ClientConfig, usage templates.Usage, dischargeLimit uint32, externalPower bool, capacity, maxacpower func() float64, efficiency func() int64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (api.Meter, error) { e3dcOnce.Do(func() { log := util.NewLogger("e3dc") rscp.Log.SetLevel(logrus.DebugLevel) @@ -108,6 +109,7 @@ func NewE3dc(cfg rscp.ClientConfig, usage templates.Usage, dischargeLimit uint32 if usage == templates.UsageBattery { implement.May(m, implement.BatteryCapacity(capacity)) + implement.May(m, implement.BatteryEfficiency(efficiency)) implement.Has(m, implement.Battery(m.batterySoc)) implement.Has(m, implement.BatteryController(m.setBatteryMode)) } diff --git a/meter/ecoflow.go b/meter/ecoflow.go index 6109dbfc904..ff19fca4f54 100644 --- a/meter/ecoflow.go +++ b/meter/ecoflow.go @@ -34,6 +34,7 @@ func init() { func NewEcoFlowFromConfig(other map[string]any) (api.Meter, error) { cc := struct { batteryCapacity `mapstructure:",squash"` + batteryEfficiency `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` Usage string @@ -72,12 +73,12 @@ func NewEcoFlowFromConfig(other map[string]any) (api.Meter, error) { return nil, fmt.Errorf("invalid region: %s", cc.Region) } - return NewEcoFlow(cc.AccessKey, cc.SecretKey, cc.Serial, cc.Usage, uri, cc.Power, cc.Soc, cc.Cache, cc.batteryCapacity.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator()) + return NewEcoFlow(cc.AccessKey, cc.SecretKey, cc.Serial, cc.Usage, uri, cc.Power, cc.Soc, cc.Cache, cc.batteryCapacity.Decorator(), cc.batteryEfficiency.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator()) } // NewEcoFlow constructs the EcoFlow struct func NewEcoFlow(accessKey, secretKey, serial, usage, uri string, - power, soc string, cache time.Duration, capacity func() float64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (*EcoFlow, error) { + power, soc string, cache time.Duration, capacity func() float64, efficiency func() int64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (*EcoFlow, error) { log := util.NewLogger("ecoflow").Redact(accessKey, secretKey, serial) m := &EcoFlow{ @@ -100,6 +101,7 @@ func NewEcoFlow(accessKey, secretKey, serial, usage, uri string, implement.May(m, implement.BatteryCapacity(capacity)) implement.May(m, implement.BatterySocLimiter(batterySocLimits)) implement.May(m, implement.BatteryPowerLimiter(batteryPowerLimits)) + implement.May(m, implement.BatteryEfficiency(efficiency)) } return m, nil diff --git a/meter/homeassistant.go b/meter/homeassistant.go index 22e0233eb42..d9c82924d8a 100644 --- a/meter/homeassistant.go +++ b/meter/homeassistant.go @@ -32,6 +32,7 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) { // battery batteryCapacity `mapstructure:",squash"` + batteryEfficiency `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` @@ -85,6 +86,7 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) { implement.May(m, implement.BatteryCapacity(cc.batteryCapacity.Decorator())) implement.May(m, implement.BatterySocLimiter(cc.batterySocLimits.Decorator())) implement.May(m, implement.BatteryPowerLimiter(cc.batteryPowerLimits.Decorator())) + implement.May(m, implement.BatteryEfficiency(cc.batteryEfficiency.Decorator())) if cc.ModeHold != "" || cc.ModeCharge != "" { if cc.ModeNormal == "" { diff --git a/meter/lgess.go b/meter/lgess.go index 2052530f904..17735487b52 100644 --- a/meter/lgess.go +++ b/meter/lgess.go @@ -36,6 +36,7 @@ func NewLgEss15FromConfig(other map[string]any) (api.Meter, error) { func NewLgEssFromConfig(other map[string]any, essType lgpcs.Model) (api.Meter, error) { cc := struct { batteryCapacity `mapstructure:",squash"` + batteryEfficiency `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` URI, Usage string @@ -57,11 +58,11 @@ func NewLgEssFromConfig(other map[string]any, essType lgpcs.Model) (api.Meter, e return nil, errors.New("missing usage") } - return NewLgEss(cc.URI, cc.Usage, cc.Registration, cc.Password, cc.Cache, cc.batteryCapacity, cc.batterySocLimits, cc.batteryPowerLimits, essType) + return NewLgEss(cc.URI, cc.Usage, cc.Registration, cc.Password, cc.Cache, cc.batteryCapacity, cc.batteryEfficiency, cc.batterySocLimits, cc.batteryPowerLimits, essType) } // NewLgEss creates an LgEss Meter -func NewLgEss(uri, usage, registration, password string, cache time.Duration, batteryCapacity batteryCapacity, batterySocLimits batterySocLimits, batteryPowerLimits batteryPowerLimits, essType lgpcs.Model) (api.Meter, error) { +func NewLgEss(uri, usage, registration, password string, cache time.Duration, batteryCapacity batteryCapacity, batteryEfficiency batteryEfficiency, batterySocLimits batterySocLimits, batteryPowerLimits batteryPowerLimits, essType lgpcs.Model) (api.Meter, error) { conn, err := lgpcs.GetInstance(uri, registration, password, cache, essType) if err != nil { return nil, err @@ -83,6 +84,7 @@ func NewLgEss(uri, usage, registration, password string, cache time.Duration, ba if usage == "battery" { implement.Has(m, implement.Battery(m.batterySoc)) implement.May(m, implement.BatterySocLimiter(batterySocLimits.Decorator())) + implement.May(m, implement.BatteryEfficiency(batteryEfficiency.Decorator())) if version, err := conn.GetFirmwareVersion(); err == nil && version >= 7430 { implement.Has(m, implement.BatteryController(m.batteryMode(batterySocLimits))) diff --git a/meter/mbmd.go b/meter/mbmd.go index 98fb16cbb3f..42ddf1d2199 100644 --- a/meter/mbmd.go +++ b/meter/mbmd.go @@ -31,6 +31,7 @@ func NewMbmdFromConfig(ctx context.Context, other map[string]any) (api.Meter, er cc := struct { Model string batteryCapacity `mapstructure:",squash"` + batteryEfficiency `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` modbus.Settings `mapstructure:",squash"` @@ -124,6 +125,7 @@ func NewMbmdFromConfig(ctx context.Context, other map[string]any) (api.Meter, er implement.May(m, implement.BatteryCapacity(cc.batteryCapacity.Decorator())) implement.May(m, implement.BatterySocLimiter(cc.batterySocLimits.Decorator())) implement.May(m, implement.BatteryPowerLimiter(cc.batteryPowerLimits.Decorator())) + implement.May(m, implement.BatteryEfficiency(cc.batteryEfficiency.Decorator())) return m, nil } diff --git a/meter/meter.go b/meter/meter.go index 0a21ef4e314..d8b1999fbf2 100644 --- a/meter/meter.go +++ b/meter/meter.go @@ -28,6 +28,7 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.M // battery batteryCapacityCtx `mapstructure:",squash"` + batteryEfficiency `mapstructure:",squash"` batterySocLimitsCtx `mapstructure:",squash"` batteryPowerLimitsCtx `mapstructure:",squash"` Soc *plugin.Config // optional @@ -90,6 +91,7 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.M implement.May(m, implement.BatteryCapacity(capacity)) implement.May(m, implement.BatterySocLimiter(socLimiter)) implement.May(m, implement.BatteryPowerLimiter(powerLimiter)) + implement.May(m, implement.BatteryEfficiency(cc.batteryEfficiency.Decorator())) switch { case cc.Soc != nil && cc.LimitSoc != nil: diff --git a/meter/powerwall.go b/meter/powerwall.go index c84d1e2df41..6db01b8d3e2 100644 --- a/meter/powerwall.go +++ b/meter/powerwall.go @@ -40,6 +40,7 @@ func NewPowerWallFromConfig(other map[string]any) (api.Meter, error) { Cache time.Duration RefreshToken string SiteId int64 + batteryEfficiency `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` }{ @@ -74,11 +75,11 @@ func NewPowerWallFromConfig(other map[string]any) (api.Meter, error) { cc.Usage = "solar" } - return NewPowerWall(cc.URI, cc.Usage, cc.User, cc.Password, cc.Cache, cc.RefreshToken, cc.SiteId, cc.batterySocLimits, cc.batteryPowerLimits) + return NewPowerWall(cc.URI, cc.Usage, cc.User, cc.Password, cc.Cache, cc.RefreshToken, cc.SiteId, cc.batteryEfficiency, cc.batterySocLimits, cc.batteryPowerLimits) } // NewPowerWall creates a Tesla PowerWall Meter -func NewPowerWall(uri, usage, user, password string, cache time.Duration, refreshToken string, siteId int64, batterySocLimits batterySocLimits, batteryPowerLimits batteryPowerLimits) (api.Meter, error) { +func NewPowerWall(uri, usage, user, password string, cache time.Duration, refreshToken string, siteId int64, batteryEfficiency batteryEfficiency, batterySocLimits batterySocLimits, batteryPowerLimits batteryPowerLimits) (api.Meter, error) { log := util.NewLogger("powerwall").Redact(user, password, refreshToken) httpClient := &http.Client{ @@ -150,6 +151,7 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration, refres implement.Has(m, implement.Battery(m.batterySoc)) implement.May(m, implement.BatterySocLimiter(batterySocLimits.Decorator())) implement.May(m, implement.BatteryPowerLimiter(batteryPowerLimits.Decorator())) + implement.May(m, implement.BatteryEfficiency(batteryEfficiency.Decorator())) res, err := m.client.GetSystemStatus() if err != nil { diff --git a/meter/rct.go b/meter/rct.go index 8a323184fdb..d2910f5855e 100644 --- a/meter/rct.go +++ b/meter/rct.go @@ -69,6 +69,7 @@ func init() { // NewRCTFromConfig creates an RCT from generic config func NewRCTFromConfig(ctx context.Context, other map[string]any) (api.Meter, error) { cc := struct { + batteryEfficiency `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` pvMaxACPower `mapstructure:",squash"` @@ -97,11 +98,11 @@ func NewRCTFromConfig(ctx context.Context, other map[string]any) (api.Meter, err return nil, errors.New("missing usage") } - return NewRCT(ctx, cc.Uri, cc.Usage, cc.batterySocLimits, cc.batteryPowerLimits, cc.Cache, cc.ExternalPower, cc.Capacity, cc.Capacity2, cc.pvMaxACPower.Decorator()) + return NewRCT(ctx, cc.Uri, cc.Usage, cc.batteryEfficiency, cc.batterySocLimits, cc.batteryPowerLimits, cc.Cache, cc.ExternalPower, cc.Capacity, cc.Capacity2, cc.pvMaxACPower.Decorator()) } // NewRCT creates an RCT meter -func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocLimits, batteryPowerLimits batteryPowerLimits, cache time.Duration, externalPower bool, capacity, capacity2 float64, maxACPower func() float64) (api.Meter, error) { +func NewRCT(ctx context.Context, uri, usage string, batteryEfficiency batteryEfficiency, batterySocLimits batterySocLimits, batteryPowerLimits batteryPowerLimits, cache time.Duration, externalPower bool, capacity, capacity2 float64, maxACPower func() float64) (api.Meter, error) { log := util.NewLogger("rct") // re-use connections @@ -174,6 +175,7 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL implement.Has(m, implement.Battery(batterySoc)) implement.May(m, implement.BatterySocLimiter(batterySocLimits.Decorator())) implement.May(m, implement.BatteryPowerLimiter(batteryPowerLimits.Decorator())) + implement.May(m, implement.BatteryEfficiency(batteryEfficiency.Decorator())) if capacity != 0 { implement.Has(m, implement.BatteryCapacity(func() float64 { return capacity + capacity2 })) diff --git a/meter/sma.go b/meter/sma.go index 26a9f847b2a..ef78e40d46e 100644 --- a/meter/sma.go +++ b/meter/sma.go @@ -32,6 +32,7 @@ func init() { func NewSMAFromConfig(other map[string]any) (api.Meter, error) { cc := struct { batteryCapacity `mapstructure:",squash"` + batteryEfficiency `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` URI, Password, Interface string @@ -48,11 +49,11 @@ func NewSMAFromConfig(other map[string]any) (api.Meter, error) { return nil, err } - return NewSMA(cc.URI, cc.Password, cc.Interface, cc.Serial, cc.Scale, cc.Usage, cc.DC, cc.batteryCapacity.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator()) + return NewSMA(cc.URI, cc.Password, cc.Interface, cc.Serial, cc.Scale, cc.Usage, cc.DC, cc.batteryCapacity.Decorator(), cc.batteryEfficiency.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator()) } // NewSMA creates an SMA meter -func NewSMA(uri, password, iface string, serial uint32, scale float64, usage string, dc bool, capacity func() float64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (*SMA, error) { +func NewSMA(uri, password, iface string, serial uint32, scale float64, usage string, dc bool, capacity func() float64, efficiency func() int64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (*SMA, error) { sm := &SMA{ Caps: implement.New(), uri: uri, @@ -97,6 +98,7 @@ func NewSMA(uri, password, iface string, serial uint32, scale float64, usage str implement.May(sm, implement.BatteryCapacity(capacity)) implement.May(sm, implement.BatterySocLimiter(batterySocLimits)) implement.May(sm, implement.BatteryPowerLimiter(batteryPowerLimits)) + implement.May(sm, implement.BatteryEfficiency(efficiency)) } if isHybridInverter && usage == "pv" { diff --git a/meter/usage_battery.go b/meter/usage_battery.go index 213ca642850..038ab24405f 100644 --- a/meter/usage_battery.go +++ b/meter/usage_battery.go @@ -82,6 +82,22 @@ func floatOr0(g func() float64) float64 { return g() } +type batteryEfficiency struct { + Efficiency int64 // % +} + +// var _ api.BatteryEfficiency = (*batteryEfficiency)(nil) + +// Decorator returns an api.BatteryEfficiency decorator +func (m *batteryEfficiency) Decorator() func() int64 { + if m.Efficiency <= 0 || m.Efficiency > 100 { + return nil + } + return func() int64 { + return m.Efficiency + } +} + type batteryPowerLimits struct { MaxChargePower float64 MaxDischargePower float64 diff --git a/meter/zendure.go b/meter/zendure.go index 984bd693140..4245b1f62e4 100644 --- a/meter/zendure.go +++ b/meter/zendure.go @@ -25,6 +25,7 @@ type Zendure struct { func NewZendureFromConfig(other map[string]any) (api.Meter, error) { cc := struct { batteryCapacity `mapstructure:",squash"` + batteryEfficiency `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` Usage, Account, Serial, Region string @@ -55,6 +56,7 @@ func NewZendureFromConfig(other map[string]any) (api.Meter, error) { implement.May(m, implement.BatteryCapacity(cc.batteryCapacity.Decorator())) implement.May(m, implement.BatterySocLimiter(cc.batterySocLimits.Decorator())) implement.May(m, implement.BatteryPowerLimiter(cc.batteryPowerLimits.Decorator())) + implement.May(m, implement.BatteryEfficiency(cc.batteryEfficiency.Decorator())) } return m, nil diff --git a/templates/definition/meter/tesla-powerwall.yaml b/templates/definition/meter/tesla-powerwall.yaml index cddf39003c4..639ae545807 100644 --- a/templates/definition/meter/tesla-powerwall.yaml +++ b/templates/definition/meter/tesla-powerwall.yaml @@ -42,6 +42,7 @@ params: advanced: true - name: maxchargepower - name: maxdischargepower + - name: efficiency render: | type: powerwall uri: {{ .host }} @@ -54,3 +55,4 @@ render: | maxsoc: {{ .maxsoc }} maxchargepower: {{ .maxchargepower }} maxdischargepower: {{ .maxdischargepower }} + efficiency: {{ .efficiency }}