From 8a72cb979e0ca89997de7a36b27ee550b6560340 Mon Sep 17 00:00:00 2001 From: Lars Schneider Date: Sat, 28 Feb 2026 12:09:59 +0100 Subject: [PATCH 1/3] battery: add BatteryHoldCharge mode Introduce a new hardware-level battery mode that prevents charging while still allowing discharge. This is implemented via the SunSpec Model 124 InWRte register (charge rate limit set to 0%), which has been verified on Fronius GEN24 hardware. The mode is mapped as case 4 in battery controller templates: - StorCtl_Mod=1 (limit charge rate) - InWRte=0% (zero charge rate) Template support is added for Fronius GEN24, Fronius Verto Plus, and the generic SunSpec inverter control template. The Solar API template returns ErrNotAvailable as it has no equivalent HTTP endpoint. E3DC is updated to support the new mode. SOC-based battery limit controllers return ErrNotAvailable as they cannot provide charge rate limiting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/batterymode.go | 1 + api/batterymode_enumer.go | 12 ++++++---- meter/e3dc.go | 5 +++++ meter/usage_battery.go | 2 ++ templates/definition/meter/fronius-gen24.yaml | 18 +++++++++++++++ .../definition/meter/fronius-solarapi-v1.yaml | 4 ++++ .../definition/meter/fronius-vertoplus.yaml | 18 +++++++++++++++ .../meter/sunspec-inverter-control.yaml | 22 +++++++++++++++++++ 8 files changed, 78 insertions(+), 4 deletions(-) diff --git a/api/batterymode.go b/api/batterymode.go index 4ca00b3f120..e2163ad652e 100644 --- a/api/batterymode.go +++ b/api/batterymode.go @@ -9,4 +9,5 @@ const ( BatteryNormal BatteryHold BatteryCharge + BatteryHoldCharge ) diff --git a/api/batterymode_enumer.go b/api/batterymode_enumer.go index 7fe4dbc6702..c828092b2c3 100644 --- a/api/batterymode_enumer.go +++ b/api/batterymode_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _BatteryModeName = "unknownnormalholdcharge" +const _BatteryModeName = "unknownnormalholdchargeholdcharge" -var _BatteryModeIndex = [...]uint8{0, 7, 13, 17, 23} +var _BatteryModeIndex = [...]uint8{0, 7, 13, 17, 23, 33} -const _BatteryModeLowerName = "unknownnormalholdcharge" +const _BatteryModeLowerName = "unknownnormalholdchargeholdcharge" func (i BatteryMode) String() string { if i < 0 || i >= BatteryMode(len(_BatteryModeIndex)-1) { @@ -28,9 +28,10 @@ func _BatteryModeNoOp() { _ = x[BatteryNormal-(1)] _ = x[BatteryHold-(2)] _ = x[BatteryCharge-(3)] + _ = x[BatteryHoldCharge-(4)] } -var _BatteryModeValues = []BatteryMode{BatteryUnknown, BatteryNormal, BatteryHold, BatteryCharge} +var _BatteryModeValues = []BatteryMode{BatteryUnknown, BatteryNormal, BatteryHold, BatteryCharge, BatteryHoldCharge} var _BatteryModeNameToValueMap = map[string]BatteryMode{ _BatteryModeName[0:7]: BatteryUnknown, @@ -41,6 +42,8 @@ var _BatteryModeNameToValueMap = map[string]BatteryMode{ _BatteryModeLowerName[13:17]: BatteryHold, _BatteryModeName[17:23]: BatteryCharge, _BatteryModeLowerName[17:23]: BatteryCharge, + _BatteryModeName[23:33]: BatteryHoldCharge, + _BatteryModeLowerName[23:33]: BatteryHoldCharge, } var _BatteryModeNames = []string{ @@ -48,6 +51,7 @@ var _BatteryModeNames = []string{ _BatteryModeName[7:13], _BatteryModeName[13:17], _BatteryModeName[17:23], + _BatteryModeName[23:33], } // BatteryModeString retrieves an enum value from the enum constants string name. diff --git a/meter/e3dc.go b/meter/e3dc.go index b17fa1ac8a2..a9eab925e1f 100644 --- a/meter/e3dc.go +++ b/meter/e3dc.go @@ -225,6 +225,11 @@ func (m *E3dc) setBatteryMode(mode api.BatteryMode) error { e3dcDischargeBatteryLimit(false, 0), e3dcBatteryCharge(50000), // max. 50kWh } + case api.BatteryHoldCharge: + messages = []rscp.Message{ + e3dcDischargeBatteryLimit(false, 0), + e3dcBatteryCharge(0), + } default: return api.ErrNotAvailable } diff --git a/meter/usage_battery.go b/meter/usage_battery.go index 9cf56337bc2..b1dfdf8a377 100644 --- a/meter/usage_battery.go +++ b/meter/usage_battery.go @@ -68,6 +68,8 @@ func (m *batterySocLimits) LimitController(socG func() (float64, error), limitSo case api.BatteryCharge: return limitSocS(m.MaxSoc) + // BatteryHoldCharge is not handled explicitly as it requires charge + // rate limiting, which this implementation does not provide. default: return api.ErrNotAvailable } diff --git a/templates/definition/meter/fronius-gen24.yaml b/templates/definition/meter/fronius-gen24.yaml index 9410b06107d..250e78e27f6 100644 --- a/templates/definition/meter/fronius-gen24.yaml +++ b/templates/definition/meter/fronius-gen24.yaml @@ -264,5 +264,23 @@ render: | uri: {{ joinHostPort .host .port }} id: 1 value: 124:0:OutWRte + - case: 4 # holdcharge + set: + source: sequence + set: + - source: const + value: 1 + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:StorCtl_Mod + - source: const + value: 0 # % + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:InWRte {{- include "battery-params" . }} {{- end }} diff --git a/templates/definition/meter/fronius-solarapi-v1.yaml b/templates/definition/meter/fronius-solarapi-v1.yaml index d620889eb04..047ee8147b4 100644 --- a/templates/definition/meter/fronius-solarapi-v1.yaml +++ b/templates/definition/meter/fronius-solarapi-v1.yaml @@ -110,6 +110,10 @@ render: | body: '{"timeofuse":[]}' - source: error error: ErrNotAvailable + - case: 4 # holdcharge (not implemented) + set: + source: error + error: ErrNotAvailable {{- end }} {{- include "battery-params" . }} {{- end }} diff --git a/templates/definition/meter/fronius-vertoplus.yaml b/templates/definition/meter/fronius-vertoplus.yaml index 54e5f601e28..26e348ea9cd 100644 --- a/templates/definition/meter/fronius-vertoplus.yaml +++ b/templates/definition/meter/fronius-vertoplus.yaml @@ -260,5 +260,23 @@ render: | uri: {{ joinHostPort .host .port }} id: 1 value: 124:0:OutWRte + - case: 4 # holdcharge + set: + source: sequence + set: + - source: const + value: 1 + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:StorCtl_Mod + - source: const + value: 0 # % + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:InWRte {{- include "battery-params" . }} {{- end }} diff --git a/templates/definition/meter/sunspec-inverter-control.yaml b/templates/definition/meter/sunspec-inverter-control.yaml index a7d4f871730..3615b35e4e4 100644 --- a/templates/definition/meter/sunspec-inverter-control.yaml +++ b/templates/definition/meter/sunspec-inverter-control.yaml @@ -104,5 +104,27 @@ render: | source: sunspec {{- include "modbus" . | indent 10 }} value: 124:0:InOutWRte_RvrtTms + - case: 4 # holdcharge + set: + source: sequence + set: + - source: const + value: 1 + set: + source: sunspec + {{- include "modbus" . | indent 10 }} + value: 124:0:StorCtl_Mod + - source: const + value: 0 # % + set: + source: sunspec + {{- include "modbus" . | indent 10 }} + value: 124:0:InWRte + - source: const + value: 0 # s + set: + source: sunspec + {{- include "modbus" . | indent 10 }} + value: 124:0:InOutWRte_RvrtTms {{- include "battery-params" . }} {{- end }} From b5595425a91e7e86ceda2c568733a7159df79c77 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 7 Jun 2026 18:44:50 +0200 Subject: [PATCH 2/3] Apply suggestion from @andig --- meter/usage_battery.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meter/usage_battery.go b/meter/usage_battery.go index b1dfdf8a377..aaadc387240 100644 --- a/meter/usage_battery.go +++ b/meter/usage_battery.go @@ -68,8 +68,7 @@ func (m *batterySocLimits) LimitController(socG func() (float64, error), limitSo case api.BatteryCharge: return limitSocS(m.MaxSoc) - // BatteryHoldCharge is not handled explicitly as it requires charge - // rate limiting, which this implementation does not provide. + // BatteryHoldCharge implementable via limit soc default: return api.ErrNotAvailable } From 55d2f1f2e77c99d2c122150ce48a28ca5f3c5f5a Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 7 Jun 2026 18:48:45 +0200 Subject: [PATCH 3/3] cli: advertise holdcharge battery mode in meter command --- cmd/flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/flags.go b/cmd/flags.go index c3e49f990da..0880e94efb3 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -29,7 +29,7 @@ const ( flagCustomCssDescription = "Additional user-defined CSS file for custom styling. No compatibility guarantees." flagBatteryMode = "battery-mode" - flagBatteryModeDescription = "Set battery mode (normal, hold, charge)" + flagBatteryModeDescription = "Set battery mode (normal, hold, charge, holdcharge)" flagBatteryModeWait = "battery-mode-wait" flagBatteryModeWaitDescription = "Wait given duration during which potential watchdogs are active"