From f46b8e22a0ec2d6d435d9a6db1a018a971b4309a Mon Sep 17 00:00:00 2001 From: Lars Schneider Date: Sat, 28 Feb 2026 12:09:59 +0100 Subject: [PATCH 1/2] 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 and SOC-based battery limit controllers are also updated to handle the new mode. 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 | 3 +++ 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, 79 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 925d9560f6c..aa7890bb67b 100644 --- a/meter/e3dc.go +++ b/meter/e3dc.go @@ -227,6 +227,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..787dc3a12f8 100644 --- a/meter/usage_battery.go +++ b/meter/usage_battery.go @@ -68,6 +68,9 @@ func (m *batterySocLimits) LimitController(socG func() (float64, error), limitSo case api.BatteryCharge: return limitSocS(m.MaxSoc) + case api.BatteryHoldCharge: + return limitSocS(m.MinSoc) + default: return api.ErrNotAvailable } diff --git a/templates/definition/meter/fronius-gen24.yaml b/templates/definition/meter/fronius-gen24.yaml index 1bec978f086..becbeb590ae 100644 --- a/templates/definition/meter/fronius-gen24.yaml +++ b/templates/definition/meter/fronius-gen24.yaml @@ -248,5 +248,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 b856539400c..f11a67e5161 100644 --- a/templates/definition/meter/fronius-vertoplus.yaml +++ b/templates/definition/meter/fronius-vertoplus.yaml @@ -244,5 +244,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 579f65fae849e407af95d6cedc1194ba5775e62e Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 27 May 2026 15:34:35 +0200 Subject: [PATCH 2/2] Battery: rename Hold to HoldDischarge The existing BatteryHold mode prevents discharge while allowing charge. With the addition of BatteryHoldCharge (prevents charge, allows discharge), the asymmetric naming was confusing. Rename BatteryHold to BatteryHoldDischarge consistently across Go API, enumer, core, meter, cmd, TypeScript enum, Vue components, and i18n keys. Breaking change: the wire-format value sent to the REST endpoint POST /batterymode/{value}, the CLI flag --battery-mode, and the MQTT batteryMode topic changes from "hold" to "holddischarge". Integrations referencing the old value must be updated. --- api/batterymode.go | 2 +- api/batterymode_enumer.go | 28 +++++++++---------- assets/js/components/BottomTabs/Bar.vue | 6 ++-- .../js/components/Energyflow/Energyflow.vue | 12 ++++---- .../components/Energyflow/Visualization.vue | 8 ++++-- .../Loadpoints/BatteryBoostButton.vue | 14 +++++----- assets/js/types/evcc.ts | 3 +- cmd/flags.go | 2 +- core/site_battery.go | 8 +++--- core/site_battery_test.go | 18 ++++++------ core/site_test.go | 4 +-- i18n/bg.json | 2 +- i18n/cs.json | 2 +- i18n/da.json | 2 +- i18n/de.json | 2 +- i18n/el.json | 2 +- i18n/en.json | 2 +- i18n/es.json | 2 +- i18n/fi.json | 2 +- i18n/fr.json | 2 +- i18n/hr.json | 2 +- i18n/hu.json | 2 +- i18n/it.json | 2 +- i18n/ja.json | 2 +- i18n/lb.json | 2 +- i18n/lt.json | 2 +- i18n/nl.json | 2 +- i18n/no.json | 2 +- i18n/pl.json | 2 +- i18n/pt.json | 2 +- i18n/ro.json | 2 +- i18n/ru.json | 2 +- i18n/sl.json | 2 +- i18n/sv.json | 2 +- i18n/ta.json | 2 +- i18n/tr.json | 2 +- i18n/uk.json | 2 +- i18n/zh-Hans.json | 2 +- meter/e3dc.go | 2 +- meter/homeassistant.go | 6 ++-- meter/lgess.go | 2 +- meter/rct.go | 2 +- meter/usage_battery.go | 2 +- 43 files changed, 90 insertions(+), 83 deletions(-) diff --git a/api/batterymode.go b/api/batterymode.go index e2163ad652e..42855afdaff 100644 --- a/api/batterymode.go +++ b/api/batterymode.go @@ -7,7 +7,7 @@ type BatteryMode int const ( BatteryUnknown BatteryMode = iota BatteryNormal - BatteryHold + BatteryHoldDischarge BatteryCharge BatteryHoldCharge ) diff --git a/api/batterymode_enumer.go b/api/batterymode_enumer.go index c828092b2c3..1093cee33ee 100644 --- a/api/batterymode_enumer.go +++ b/api/batterymode_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _BatteryModeName = "unknownnormalholdchargeholdcharge" +const _BatteryModeName = "unknownnormalholddischargechargeholdcharge" -var _BatteryModeIndex = [...]uint8{0, 7, 13, 17, 23, 33} +var _BatteryModeIndex = [...]uint8{0, 7, 13, 26, 32, 42} -const _BatteryModeLowerName = "unknownnormalholdchargeholdcharge" +const _BatteryModeLowerName = "unknownnormalholddischargechargeholdcharge" func (i BatteryMode) String() string { if i < 0 || i >= BatteryMode(len(_BatteryModeIndex)-1) { @@ -26,32 +26,32 @@ func _BatteryModeNoOp() { var x [1]struct{} _ = x[BatteryUnknown-(0)] _ = x[BatteryNormal-(1)] - _ = x[BatteryHold-(2)] + _ = x[BatteryHoldDischarge-(2)] _ = x[BatteryCharge-(3)] _ = x[BatteryHoldCharge-(4)] } -var _BatteryModeValues = []BatteryMode{BatteryUnknown, BatteryNormal, BatteryHold, BatteryCharge, BatteryHoldCharge} +var _BatteryModeValues = []BatteryMode{BatteryUnknown, BatteryNormal, BatteryHoldDischarge, BatteryCharge, BatteryHoldCharge} var _BatteryModeNameToValueMap = map[string]BatteryMode{ _BatteryModeName[0:7]: BatteryUnknown, _BatteryModeLowerName[0:7]: BatteryUnknown, _BatteryModeName[7:13]: BatteryNormal, _BatteryModeLowerName[7:13]: BatteryNormal, - _BatteryModeName[13:17]: BatteryHold, - _BatteryModeLowerName[13:17]: BatteryHold, - _BatteryModeName[17:23]: BatteryCharge, - _BatteryModeLowerName[17:23]: BatteryCharge, - _BatteryModeName[23:33]: BatteryHoldCharge, - _BatteryModeLowerName[23:33]: BatteryHoldCharge, + _BatteryModeName[13:26]: BatteryHoldDischarge, + _BatteryModeLowerName[13:26]: BatteryHoldDischarge, + _BatteryModeName[26:32]: BatteryCharge, + _BatteryModeLowerName[26:32]: BatteryCharge, + _BatteryModeName[32:42]: BatteryHoldCharge, + _BatteryModeLowerName[32:42]: BatteryHoldCharge, } var _BatteryModeNames = []string{ _BatteryModeName[0:7], _BatteryModeName[7:13], - _BatteryModeName[13:17], - _BatteryModeName[17:23], - _BatteryModeName[23:33], + _BatteryModeName[13:26], + _BatteryModeName[26:32], + _BatteryModeName[32:42], } // BatteryModeString retrieves an enum value from the enum constants string name. diff --git a/assets/js/components/BottomTabs/Bar.vue b/assets/js/components/BottomTabs/Bar.vue index aa88cd504ae..ecf958ae7d1 100644 --- a/assets/js/components/BottomTabs/Bar.vue +++ b/assets/js/components/BottomTabs/Bar.vue @@ -14,7 +14,7 @@ class="tab-icon" :soc="batterySoc || 0" :grid-charge="batteryGridChargeActive" - :hold="batteryHold" + :hold="batteryHoldDischarge" /> @@ -85,8 +85,8 @@ export default defineComponent({ batterySoc() { return this.battery?.soc; }, - batteryHold() { - return this.batteryMode === "hold"; + batteryHoldDischarge() { + return this.batteryMode === "holddischarge"; }, batteryConfigured() { return (this.battery?.devices?.length ?? 0) > 0; diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index 5dd1265dabe..75ad7b35ace 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -19,7 +19,7 @@ :batteryCharge="batteryCharge" :batteryDischarge="batteryDischarge" :batteryGridCharge="batteryGridChargeActive" - :batteryHold="batteryHold" + :batteryHoldDischarge="batteryHoldDischarge" :pvProduction="pvProduction" :homePower="homePower" :batterySoc="batterySoc" @@ -114,7 +114,7 @@ :power="batteryDischarge" :powerUnit="powerUnit" :iconProps="{ - hold: batteryHold, + hold: batteryHoldDischarge, soc: batterySoc, gridCharge: batteryGridChargeActive, }" @@ -448,10 +448,12 @@ export default defineComponent({ return this.$t("main.energyflow.batteryCharge"); }, batteryDischargeLabel() { - return this.$t(`main.energyflow.battery${this.batteryHold ? "Hold" : "Discharge"}`); + return this.$t( + `main.energyflow.battery${this.batteryHoldDischarge ? "HoldDischarge" : "Discharge"}` + ); }, - batteryHold() { - return this.batteryMode === "hold"; + batteryHoldDischarge() { + return this.batteryMode === "holddischarge"; }, consumption() { return this.homePower + this.batteryCharge + this.loadpointsPower; diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue index f448fdbe55d..67a54e74b2f 100644 --- a/assets/js/components/Energyflow/Visualization.vue +++ b/assets/js/components/Energyflow/Visualization.vue @@ -102,7 +102,11 @@
Out
- + @@ -131,7 +135,7 @@ export default defineComponent({ batterySoc: { type: Number }, batteryCharge: { type: Number, default: 0 }, batteryDischarge: { type: Number, default: 0 }, - batteryHold: { type: Boolean, default: false }, + batteryHoldDischarge: { type: Boolean, default: false }, batteryGridCharge: { type: Boolean, default: false }, pvProduction: { type: Number, default: 0 }, homePower: { type: Number, default: 0 }, diff --git a/assets/js/components/Loadpoints/BatteryBoostButton.vue b/assets/js/components/Loadpoints/BatteryBoostButton.vue index ffc29cf0344..71d0db74b53 100644 --- a/assets/js/components/Loadpoints/BatteryBoostButton.vue +++ b/assets/js/components/Loadpoints/BatteryBoostButton.vue @@ -2,7 +2,7 @@