diff --git a/charger/homeassistant-switch.go b/charger/homeassistant-switch.go deleted file mode 100644 index acc9e6dcc..000000000 --- a/charger/homeassistant-switch.go +++ /dev/null @@ -1,79 +0,0 @@ -package charger - -import ( - "errors" - - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/homeassistant" -) - -type HomeAssistantSwitch struct { - conn *homeassistant.Connection - enable string - power string - *switchSocket -} - -func init() { - registry.Add("homeassistant-switch", NewHomeAssistantSwitchFromConfig) -} - -func NewHomeAssistantSwitchFromConfig(other map[string]any) (api.Charger, error) { - var cc struct { - embed `mapstructure:",squash"` - homeassistant.Config `mapstructure:",squash"` - Enable string - Power string - StandbyPower float64 - } - - if err := util.DecodeOther(other, &cc); err != nil { - return nil, err - } - - return NewHomeAssistantSwitch(cc.embed, cc.URI, cc.Home_, cc.Insecure, cc.Enable, cc.Power, cc.StandbyPower) -} - -func NewHomeAssistantSwitch(embed embed, uri, home string, insecure bool, enable, power string, standbypower float64) (api.Charger, error) { - if enable == "" { - return nil, errors.New("missing enable switch entity") - } - - // standbypower < 0 ensures that currentPower is never used by the switch socket if not present - if power == "" && standbypower >= 0 { - return nil, errors.New("missing either power entity or negative standbypower") - } - - log := util.NewLogger("ha-switch") - - conn, err := homeassistant.NewConnection(log, uri, home, insecure) - if err != nil { - return nil, err - } - - c := &HomeAssistantSwitch{ - enable: enable, - power: power, - conn: conn, - } - - c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.currentPower, standbypower) - - return c, nil -} - -// Enabled implements the api.Charger interface -func (c *HomeAssistantSwitch) Enabled() (bool, error) { - return c.conn.GetBoolState(c.enable) -} - -// Enable implements the api.Charger interface -func (c *HomeAssistantSwitch) Enable(enable bool) error { - return c.conn.CallSwitchService(c.enable, enable) -} - -// currentPower implements the api.Meter interface (optional) -func (c *HomeAssistantSwitch) currentPower() (float64, error) { - return c.conn.GetFloatState(c.power) -} diff --git a/charger/switchsocket.go b/charger/switchsocket.go index 01294b77c..7a9eb0b15 100644 --- a/charger/switchsocket.go +++ b/charger/switchsocket.go @@ -2,9 +2,11 @@ package charger import ( "context" + "errors" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/api/implement" + "github.com/evcc-io/evcc/charger/measurement" "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/plugin" "github.com/evcc-io/evcc/util" @@ -23,13 +25,14 @@ type SwitchSocket struct { func NewSwitchSocketFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) { var cc struct { - embed `mapstructure:",squash"` - Enabled plugin.Config - Enable plugin.Config - Power plugin.Config - Energy *plugin.Config - Soc *plugin.Config - StandbyPower float64 + embed `mapstructure:",squash"` + Enabled plugin.Config + Enable plugin.Config + Power *plugin.Config + Energy *plugin.Config + Soc *plugin.Config + measurement.Temperature `mapstructure:",squash"` // optional, for heating devices + StandbyPower float64 } if err := util.DecodeOther(other, &cc); err != nil { @@ -51,6 +54,11 @@ func NewSwitchSocketFromConfig(ctx context.Context, other map[string]any) (api.C return nil, err } + // standbypower < 0 ensures that power is never used by the switch socket if not present + if power == nil && cc.StandbyPower >= 0 { + return nil, errors.New("missing either power or negative standbypower") + } + c := &SwitchSocket{ Caps: implement.New(), enabled: enabled, @@ -68,7 +76,17 @@ func NewSwitchSocketFromConfig(ctx context.Context, other map[string]any) (api.C if err != nil { return nil, err } + + // for heating devices, the soc slot holds temperature in °C — fall back to temp getter + temp, limitTemp, err := cc.Temperature.Configure(ctx) + if err != nil { + return nil, err + } + if soc == nil && temp != nil { + soc = temp + } implement.May(c, implement.Battery(soc)) + implement.May(c, implement.SocLimiter(limitTemp)) return c, nil } diff --git a/plugin/homeassistant.go b/plugin/homeassistant.go new file mode 100644 index 000000000..9d8e7d64d --- /dev/null +++ b/plugin/homeassistant.go @@ -0,0 +1,91 @@ +package plugin + +import ( + "context" + "errors" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/homeassistant" +) + +// HomeAssistant reads and writes Home Assistant entity states +type HomeAssistant struct { + conn *homeassistant.Connection + entity string +} + +func init() { + registry.AddCtx("homeassistant", NewHomeAssistantPluginFromConfig) +} + +// NewHomeAssistantPluginFromConfig creates a Home Assistant plugin +func NewHomeAssistantPluginFromConfig(ctx context.Context, other map[string]any) (Plugin, error) { + var cc struct { + homeassistant.Config `mapstructure:",squash"` + Entity string + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.Entity == "" { + return nil, errors.New("missing entity") + } + + conn, err := homeassistant.NewConnection(util.NewLogger("homeassistant"), cc.URI, cc.Home_, cc.Insecure) + if err != nil { + return nil, err + } + + return &HomeAssistant{ + conn: conn, + entity: cc.Entity, + }, nil +} + +var _ StringGetter = (*HomeAssistant)(nil) + +// StringGetter returns the entity state +func (p *HomeAssistant) StringGetter() (func() (string, error), error) { + return func() (string, error) { + res, err := p.conn.GetState(p.entity) + return res.State, err + }, nil +} + +var _ FloatGetter = (*HomeAssistant)(nil) + +// FloatGetter returns the entity state as float +func (p *HomeAssistant) FloatGetter() (func() (float64, error), error) { + return func() (float64, error) { + return p.conn.GetFloatState(p.entity) + }, nil +} + +var _ IntGetter = (*HomeAssistant)(nil) + +// IntGetter returns the entity state as int +func (p *HomeAssistant) IntGetter() (func() (int64, error), error) { + return func() (int64, error) { + return p.conn.GetIntState(p.entity) + }, nil +} + +var _ BoolGetter = (*HomeAssistant)(nil) + +// BoolGetter returns the entity state as bool +func (p *HomeAssistant) BoolGetter() (func() (bool, error), error) { + return func() (bool, error) { + return p.conn.GetBoolState(p.entity) + }, nil +} + +var _ BoolSetter = (*HomeAssistant)(nil) + +// BoolSetter invokes the entity's turn_on/turn_off service +func (p *HomeAssistant) BoolSetter(param string) (func(bool) error, error) { + return func(enable bool) error { + return p.conn.CallSwitchService(p.entity, enable) + }, nil +} diff --git a/templates/definition/charger/homeassistant-switch.yaml b/templates/definition/charger/homeassistant-switch.yaml index c77edce01..e28b94802 100644 --- a/templates/definition/charger/homeassistant-switch.yaml +++ b/templates/definition/charger/homeassistant-switch.yaml @@ -41,12 +41,63 @@ params: en: Entity ID for power measurement service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor example: sensor.smartsocket_power + - name: temp + description: + de: Entity ID für Temperaturmessung + en: Entity ID for temperature measurement + help: + de: Nur für Heizgeräte + en: Heating devices only + service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor + example: sensor.heatpump_temperature + advanced: true + - name: limittemp + description: + de: Entity ID für Temperaturbegrenzung + en: Entity ID for temperature limit + help: + de: Im Gerät konfigurierte Begrenzung, nur für Heizgeräte + en: Limit configured in the device, heating devices only + service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor + example: sensor.heatpump_target_temperature + advanced: true - preset: switchsocket render: | - type: homeassistant-switch - uri: {{ .uri }} - home: {{ .home }} # deprecated - insecure: {{ .insecure }} - enable: {{ .switch }} - power: {{ .power }} + type: switchsocket + enabled: + source: homeassistant + uri: {{ .uri }} + home: {{ .home }} # deprecated + insecure: {{ .insecure }} + entity: {{ .switch }} + enable: + source: homeassistant + uri: {{ .uri }} + home: {{ .home }} # deprecated + insecure: {{ .insecure }} + entity: {{ .switch }} + {{- if .power }} + power: + source: homeassistant + uri: {{ .uri }} + home: {{ .home }} # deprecated + insecure: {{ .insecure }} + entity: {{ .power }} + {{- end }} + {{- if .temp }} + temp: + source: homeassistant + uri: {{ .uri }} + home: {{ .home }} # deprecated + insecure: {{ .insecure }} + entity: {{ .temp }} + {{- end }} + {{- if .limittemp }} + limittemp: + source: homeassistant + uri: {{ .uri }} + home: {{ .home }} # deprecated + insecure: {{ .insecure }} + entity: {{ .limittemp }} + {{- end }} {{ include "switchsocket" . }}