diff --git a/meter/homeassistant.go b/meter/homeassistant.go index 58c14175b..8f42dad89 100644 --- a/meter/homeassistant.go +++ b/meter/homeassistant.go @@ -3,6 +3,7 @@ package meter import ( "errors" "fmt" + "strings" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/api/implement" @@ -34,6 +35,11 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) { batteryCapacity `mapstructure:",squash"` batterySocLimits `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` + + // battery mode control - optional switch-like entities per mode + ModeNormal string + ModeHold string + ModeCharge string }{ batterySocLimits: batterySocLimits{ MinSoc: 20, @@ -98,6 +104,26 @@ 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())) + + if cc.ModeHold != "" || cc.ModeCharge != "" { + if cc.ModeNormal == "" { + return nil, errors.New("modeNormal is required when modeHold or modeCharge is configured") + } + modes := map[api.BatteryMode]string{ + api.BatteryNormal: cc.ModeNormal, + api.BatteryHold: cc.ModeHold, + api.BatteryCharge: cc.ModeCharge, + } + for _, entity := range modes { + if entity != "" && !strings.HasPrefix(entity, "script.") { + return nil, fmt.Errorf("battery mode entity must be a script: %s", entity) + } + } + implement.Has(m, implement.BatteryController(batteryModeController(conn, modes))) + } else if cc.ModeNormal != "" { + return nil, errors.New("modeNormal alone has no effect; configure modeHold and/or modeCharge") + } + return m, nil } @@ -108,3 +134,19 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) { return m, nil } + +// batteryModeController returns a BatteryController function that activates +// the switch-like Home Assistant entity configured for the requested evcc +// battery mode. Each mode is self-contained: evcc only triggers the matching +// entity and never deactivates others - any mutual exclusion is the HA side's +// responsibility. modeHold and modeCharge are optional and return +// api.ErrNotAvailable when requested without a backing entity. +func batteryModeController(conn *homeassistant.Connection, modes map[api.BatteryMode]string) func(api.BatteryMode) error { + return func(mode api.BatteryMode) error { + target, ok := modes[mode] + if !ok || target == "" { + return api.ErrNotAvailable + } + return conn.CallSwitchService(target, true) + } +} diff --git a/templates/definition/meter/homeassistant.yaml b/templates/definition/meter/homeassistant.yaml index 97a44a8a6..f62309f2b 100644 --- a/templates/definition/meter/homeassistant.yaml +++ b/templates/definition/meter/homeassistant.yaml @@ -117,6 +117,39 @@ params: help: en: Entity ID for battery state of charge in percent de: Entitäts-ID für Batterieladestand in Prozent + - name: modeNormal + usages: ["battery"] + description: + de: Normalbetrieb Skript + en: Normal Mode Script + service: homeassistant/entities?uri={uri}&domain=script + example: "script.battery_normal" + advanced: true + help: + en: Script to put home battery to normal operation. Required when modeHold or modeCharge is set. The script is responsible for any necessary state changes on the HA side. + de: Skript um die Heimbatterie in den Normalbetrieb zu versetzen. Erforderlich, wenn modeHold oder modeCharge gesetzt ist. Das Skript ist verantwortlich für alle notwendigen Statusänderungen auf der HA-Seite. + - name: modeHold + usages: ["battery"] + description: + de: Halten Skript + en: Hold Mode Script + service: homeassistant/entities?uri={uri}&domain=script + example: "script.battery_hold" + advanced: true + help: + en: Optional script to prevent the home battery from discharging. + de: Optionales Skript um Entladen der Heimbatterie zu verhindern. + - name: modeCharge + usages: ["battery"] + description: + de: Netzladung Skript + en: Grid Charge Mode Script + service: homeassistant/entities?uri={uri}&domain=script + example: "script.battery_grid_charge" + advanced: true + help: + en: Optional script to grid-charge the home battery. + de: Optionales Skript um die Heimbatterie aus dem Netz zu laden. - name: maxacpower - preset: battery-params render: | @@ -136,5 +169,8 @@ render: | maxacpower: {{ .maxacpower }} # W {{- if eq .usage "battery" }} soc: {{ .soc }} # W + modeNormal: {{ .modeNormal }} + modeHold: {{ .modeHold }} + modeCharge: {{ .modeCharge }} {{- include "battery-params" . }} {{- end }}