From 89c45015c9b4cb01f3cc3a411fbc4ac958812a27 Mon Sep 17 00:00:00 2001 From: Niklas <7442307+niklaswa@users.noreply.github.com> Date: Sun, 25 May 2025 10:59:21 +0200 Subject: [PATCH] Add Home Assistant Switch as switchsockets charger (#21358) --- charger/homeassistant-switch.go | 106 ++++++++++++++++++ charger/homeassistant-switch_decorators.go | 35 ++++++ .../charger/homeassistant-switch.yaml | 34 ++++++ 3 files changed, 175 insertions(+) create mode 100644 charger/homeassistant-switch.go create mode 100644 charger/homeassistant-switch_decorators.go create mode 100644 templates/definition/charger/homeassistant-switch.yaml diff --git a/charger/homeassistant-switch.go b/charger/homeassistant-switch.go new file mode 100644 index 000000000..224687e09 --- /dev/null +++ b/charger/homeassistant-switch.go @@ -0,0 +1,106 @@ +package charger + +import ( + "fmt" + "net/http" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" +) + +type HomeAssistantSwitch struct { + baseURL string + switchEntity string + powerEntity string + *request.Helper + *switchSocket +} + +func init() { + registry.Add("homeassistant-switch", NewHomeAssistantSwitchFromConfig) +} + +func NewHomeAssistantSwitchFromConfig(other map[string]interface{}) (api.Charger, error) { + var cc struct { + embed `mapstructure:",squash"` + BaseURL string + Token string + SwitchEntity string + PowerEntity string + StandbyPower float64 + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewHomeAssistantSwitch(cc.embed, cc.BaseURL, cc.Token, cc.SwitchEntity, cc.PowerEntity, cc.StandbyPower) +} + +//go:generate go tool decorate -f decorateHomeAssistantSwitch -b *HomeAssistantSwitch -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" + +func NewHomeAssistantSwitch(embed embed, baseURL, token, switchEntity, powerEntity string, standbypower float64) (api.Charger, error) { + c := &HomeAssistantSwitch{ + baseURL: baseURL, + switchEntity: switchEntity, + powerEntity: powerEntity, + Helper: request.NewHelper(util.NewLogger("ha-switch")), + } + + c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.CurrentPower, standbypower) + c.Helper.Client.Transport = &transport.Decorator{ + Decorator: transport.DecorateHeaders(map[string]string{ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }), + Base: c.Helper.Client.Transport, + } + + var currentPower func() (float64, error) + if powerEntity != "" { + currentPower = c.currentPower + } + + return decorateHomeAssistantSwitch(c, currentPower), nil +} + +// Enabled implements the api.Charger interface +func (c *HomeAssistantSwitch) Enabled() (bool, error) { + var res struct { + State string `json:"state"` + } + + uri := fmt.Sprintf("%s/api/states/%s", c.baseURL, c.switchEntity) + err := c.Helper.GetJSON(uri, &res) + + return res.State == "on", err +} + +// Enable implements the api.Charger interface +func (c *HomeAssistantSwitch) Enable(enable bool) error { + service := "turn_off" + if enable { + service = "turn_on" + } + + data := map[string]any{"entity_id": c.switchEntity} + + uri := fmt.Sprintf("%s/api/services/switch/%s", c.baseURL, service) + req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + + return c.Helper.DoJSON(req, nil) +} + +// currentPower implements the api.Meter interface (optional) +func (c *HomeAssistantSwitch) currentPower() (float64, error) { + var res struct { + State float64 `json:"state,string"` + } + + uri := fmt.Sprintf("%s/api/states/%s", c.baseURL, c.powerEntity) + err := c.Helper.GetJSON(uri, &res) + + return res.State, err +} diff --git a/charger/homeassistant-switch_decorators.go b/charger/homeassistant-switch_decorators.go new file mode 100644 index 000000000..978f196e6 --- /dev/null +++ b/charger/homeassistant-switch_decorators.go @@ -0,0 +1,35 @@ +package charger + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/evcc-io/evcc/api" +) + +func decorateHomeAssistantSwitch(base *HomeAssistantSwitch, meter func() (float64, error)) api.Charger { + switch { + case meter == nil: + return base + + case meter != nil: + return &struct { + *HomeAssistantSwitch + api.Meter + }{ + HomeAssistantSwitch: base, + Meter: &decorateHomeAssistantSwitchMeterImpl{ + meter: meter, + }, + } + } + + return nil +} + +type decorateHomeAssistantSwitchMeterImpl struct { + meter func() (float64, error) +} + +func (impl *decorateHomeAssistantSwitchMeterImpl) CurrentPower() (float64, error) { + return impl.meter() +} diff --git a/templates/definition/charger/homeassistant-switch.yaml b/templates/definition/charger/homeassistant-switch.yaml new file mode 100644 index 000000000..28d00ec30 --- /dev/null +++ b/templates/definition/charger/homeassistant-switch.yaml @@ -0,0 +1,34 @@ +template: homeassistant-switch +products: + - brand: Home Assistant Switch +group: switchsockets +params: + - name: baseurl + description: + de: Basis-URL der Home Assistant Instanz (z.B. http://homeassistant.local:8123) + en: Base URL of the Home Assistant instance (e.g. http://homeassistant.local:8123) + required: true + default: http://homeassistant.local:8123 + - name: token + mask: true + required: true + description: + de: Long-Lived Access Token aus Home Assistant Profil + en: Long-Lived Access Token from Home Assistant profile + - name: switchentity + required: true + description: + de: Entity ID des schaltbaren Geräts (z.B. switch.smartsocket) + en: Entity ID of the switch device (e.g. switch.smartsocket) + - name: powerentity + description: + de: Entity ID für Leistungsmessung (optional, z.B. sensor.smartsocket_power) + en: Entity ID for power measurement (optional, e.g. sensor.smartsocket_power) + - preset: switchsocket +render: | + type: homeassistant-switch + baseurl: {{ .baseurl }} + token: {{ .token }} + switchentity: {{ .switchentity }} + powerentity: {{ .powerentity }} + {{ include "switchsocket" . }}