diff --git a/api/api.go b/api/api.go index 3535b9dc73d..72e21151c44 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 { + Efficiency() int64 +} + // 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..f4dcd0e9ae3 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() int64) api.BatteryEfficiency { + if batteryEfficiency0 == nil { + return nil + } + return &iBatteryEfficiency{batteryEfficiency0} +} + +type iBatteryEfficiency struct { + batteryEfficiency0 func() int64 +} + +func (i *iBatteryEfficiency) Efficiency() int64 { + 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..684f3b3e4ee 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -681,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 8b667b7c32c..5c162c766a6 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -322,3 +322,26 @@ 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) Efficiency() int64 { + return 95 +} + +func TestBatteryEfficiency(t *testing.T) { + site := &Site{} + capacity, soc := 10.0, 50.0 + b := types.Measurement{Capacity: &capacity, Soc: &soc} + + // 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) + + 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 84de61530ad..2ce18d127cd 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 6526b6c001b..95147b6bfa2 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= 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/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/template_test.go b/meter/template_test.go index 227ca0f063c..3515907e4e7 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, int64(95), eff.Efficiency()) +} 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 }} 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 }}