Home Assistant: add plugin source, support temp for heating switch devices (BC) (#31915)

This commit is contained in:
andig 2026-07-19 01:22:45 +02:00 • committed by GitHub
parent 28d36a51f0
commit a5956a6804
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 173 additions and 92 deletions

View file

@ -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)
}

View file

@ -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"
@ -26,9 +28,10 @@ func NewSwitchSocketFromConfig(ctx context.Context, other map[string]any) (api.C
embed `mapstructure:",squash"`
Enabled plugin.Config
Enable plugin.Config
Power plugin.Config
Power *plugin.Config
Energy *plugin.Config
Soc *plugin.Config
measurement.Temperature `mapstructure:",squash"` // optional, for heating devices
StandbyPower float64
}
@ -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
}

91
plugin/homeassistant.go Normal file
View file

@ -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
}

View file

@ -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
type: switchsocket
enabled:
source: homeassistant
uri: {{ .uri }}
home: {{ .home }} # deprecated
insecure: {{ .insecure }}
enable: {{ .switch }}
power: {{ .power }}
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" . }}