Homeassistant: align implementations (BC) (#24237)

This commit is contained in:
andig 2025-10-08 13:31:41 +02:00 • committed by GitHub
parent fc8cad205f
commit db5c58dfce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 193 additions and 617 deletions

View file

@ -2,21 +2,16 @@ package charger
import (
"errors"
"fmt"
"net/http"
"strings"
"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"
"github.com/evcc-io/evcc/util/homeassistant"
)
type HomeAssistantSwitch struct {
baseURL string
conn *homeassistant.Connection
switchEntity string
powerEntity string
*request.Helper
*switchSocket
}
@ -27,7 +22,7 @@ func init() {
func NewHomeAssistantSwitchFromConfig(other map[string]interface{}) (api.Charger, error) {
var cc struct {
embed `mapstructure:",squash"`
BaseURL string
URI string
Token string
SwitchEntity string
PowerEntity string
@ -38,17 +33,10 @@ func NewHomeAssistantSwitchFromConfig(other map[string]interface{}) (api.Charger
return nil, err
}
return NewHomeAssistantSwitch(cc.embed, cc.BaseURL, cc.Token, cc.SwitchEntity, cc.PowerEntity, cc.StandbyPower)
return NewHomeAssistantSwitch(cc.embed, cc.URI, cc.Token, cc.SwitchEntity, cc.PowerEntity, cc.StandbyPower)
}
func NewHomeAssistantSwitch(embed embed, baseURL, token, switchEntity, powerEntity string, standbypower float64) (api.Charger, error) {
c := &HomeAssistantSwitch{
baseURL: strings.TrimSuffix(baseURL, "/"),
switchEntity: switchEntity,
powerEntity: powerEntity,
Helper: request.NewHelper(util.NewLogger("ha-switch")),
}
func NewHomeAssistantSwitch(embed embed, uri, token, switchEntity, powerEntity string, standbypower float64) (api.Charger, error) {
if switchEntity == "" {
return nil, errors.New("missing switch entity")
}
@ -58,55 +46,34 @@ func NewHomeAssistantSwitch(embed embed, baseURL, token, switchEntity, powerEnti
return nil, errors.New("missing either power entity or negative standbypower")
}
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,
log := util.NewLogger("ha-switch")
conn, err := homeassistant.NewConnection(log, uri, token)
if err != nil {
return nil, err
}
c := &HomeAssistantSwitch{
switchEntity: switchEntity,
powerEntity: powerEntity,
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) {
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
return c.conn.GetBoolState(c.switchEntity)
}
// 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}
// the domain must not be necessary a 'switch' - it can be also an `input_boolean`
domain := strings.Split(c.switchEntity, ".")[0]
uri := fmt.Sprintf("%s/api/services/%s/%s", c.baseURL, domain, service)
req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
return c.Helper.DoJSON(req, nil)
return c.conn.CallSwitchService(c.switchEntity, enable)
}
// 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
return c.conn.GetFloatState(c.powerEntity)
}

View file

@ -1,11 +1,11 @@
package charger
//go:generate go tool decorate -f decorateHomeAssistant -b *HomeAssistant -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.CurrentGetter,GetMaxCurrent,func() (float64, error)"
//go:generate go tool decorate -f decorateHomeAssistant -b *HomeAssistant -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)"
// -t "api.CurrentGetter,GetMaxCurrent,func() (float64, error)"
import (
"errors"
"fmt"
"math"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
@ -18,10 +18,6 @@ type HomeAssistant struct {
status string
enabled string
enable string
power string
energy string
currentsE []string
voltagesE []string
maxcurrent string
}
@ -32,16 +28,16 @@ func init() {
// NewHomeAssistantFromConfig creates a HomeAssistant charger from generic config
func NewHomeAssistantFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
BaseURL string
URI string
Token string
Status string // required - sensor for charge status
Enabled string // required - sensor for enabled state
Enable string // required - switch/input_boolean for enable/disable
MaxCurrent string // required - number entity for setting max current
Power string // optional - power sensor
Energy string // optional - energy sensor
Currents []string // optional - current sensors for L1, L2, L3
Voltages []string // optional - voltage sensors for L1, L2, L3
MaxCurrent string // optional - number entity for setting max current
}{}
if err := util.DecodeOther(other, &cc); err != nil {
@ -58,7 +54,8 @@ func NewHomeAssistantFromConfig(other map[string]interface{}) (api.Charger, erro
return nil, errors.New("missing enable switch entity")
}
conn, err := homeassistant.NewConnection(cc.BaseURL, cc.Token)
log := util.NewLogger("ha-charger")
conn, err := homeassistant.NewConnection(log, cc.URI, cc.Token)
if err != nil {
return nil, err
}
@ -68,18 +65,29 @@ func NewHomeAssistantFromConfig(other map[string]interface{}) (api.Charger, erro
status: cc.Status,
enabled: cc.Enabled,
enable: cc.Enable,
power: cc.Power,
energy: cc.Energy,
maxcurrent: cc.MaxCurrent,
}
// decorators for optional interfaces
var power, energy func() (float64, error)
var currents, voltages func() (float64, float64, float64, error)
if cc.Power != "" {
power = func() (float64, error) { return conn.GetFloatState(cc.Power) }
}
if cc.Energy != "" {
energy = func() (float64, error) { return conn.GetFloatState(cc.Energy) }
}
// Set up phase currents (optional)
if len(cc.Currents) > 0 {
res, err := homeassistant.ValidatePhaseEntities(cc.Currents)
if err != nil {
return nil, fmt.Errorf("currents: %w", err)
}
c.currentsE = res
currents = func() (float64, float64, float64, error) {
return conn.GetPhaseFloatStates(res)
}
}
// Set up phase voltages (optional)
@ -88,45 +96,19 @@ func NewHomeAssistantFromConfig(other map[string]interface{}) (api.Charger, erro
if err != nil {
return nil, fmt.Errorf("voltages: %w", err)
}
c.voltagesE = res
voltages = func() (float64, float64, float64, error) {
return conn.GetPhaseFloatStates(res)
}
}
// decorators for optional interfaces
var meter func() (float64, error)
var meterEnergy func() (float64, error)
var phaseCurrents func() (float64, float64, float64, error)
var phaseVoltages func() (float64, float64, float64, error)
var currentGetter func() (float64, error)
if c.maxcurrent != "" {
currentGetter = c.getMaxCurrent
}
if c.power != "" {
meter = c.currentPower
}
if c.energy != "" {
meterEnergy = c.totalEnergy
}
if c.currentsE != nil {
phaseCurrents = c.currents
}
if c.voltagesE != nil {
phaseVoltages = c.voltages
}
return decorateHomeAssistant(c, meter, meterEnergy, phaseCurrents, phaseVoltages, currentGetter), nil
return decorateHomeAssistant(c, power, energy, currents, voltages), nil
}
var _ api.Charger = (*HomeAssistant)(nil)
// Status implements the api.ChargeState interface
func (c *HomeAssistant) Status() (api.ChargeStatus, error) {
state, err := c.conn.GetState(c.status)
if err != nil {
return api.StatusNone, err
}
return homeassistant.ParseChargeStatus(state)
return c.conn.GetChargeStatus(c.status)
}
// Enabled implements the api.Charger interface
@ -139,42 +121,7 @@ func (c *HomeAssistant) Enable(enable bool) error {
return c.conn.CallSwitchService(c.enable, enable)
}
// MaxCurrent implements the api.CurrentController interface
// MaxCurrent implements the api.Charger interface
func (c *HomeAssistant) MaxCurrent(current int64) error {
if c.maxcurrent == "" {
return api.ErrNotAvailable
}
return c.conn.CallNumberService(c.maxcurrent, float64(current))
}
// getMaxCurrent implements the api.CurrentGetter interface
func (c *HomeAssistant) getMaxCurrent() (float64, error) {
value, err := c.conn.GetFloatState(c.maxcurrent)
if err != nil {
return 0, err
}
// Return value as integer amperes
return math.Round(value), nil
}
// currentPower implements the api.Meter interface
func (c *HomeAssistant) currentPower() (float64, error) {
return c.conn.GetFloatState(c.power)
}
// totalEnergy implements the api.MeterEnergy interface
func (c *HomeAssistant) totalEnergy() (float64, error) {
return c.conn.GetFloatState(c.energy)
}
// currents implements the api.PhaseCurrents interface
func (c *HomeAssistant) currents() (float64, float64, float64, error) {
return c.conn.GetPhaseStates(c.currentsE)
}
// voltages implements the api.PhaseVoltages interface
func (c *HomeAssistant) voltages() (float64, float64, float64, error) {
return c.conn.GetPhaseStates(c.voltagesE)
}

View file

@ -6,12 +6,12 @@ import (
"github.com/evcc-io/evcc/api"
)
func decorateHomeAssistant(base *HomeAssistant, meter func() (float64, error), meterEnergy func() (float64, error), phaseCurrents func() (float64, float64, float64, error), phaseVoltages func() (float64, float64, float64, error), currentGetter func() (float64, error)) api.Charger {
func decorateHomeAssistant(base *HomeAssistant, meter func() (float64, error), meterEnergy func() (float64, error), phaseCurrents func() (float64, float64, float64, error), phaseVoltages func() (float64, float64, float64, error)) api.Charger {
switch {
case currentGetter == nil && meter == nil:
case meter == nil:
return base
case currentGetter == nil && meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseVoltages == nil:
case meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseVoltages == nil:
return &struct {
*HomeAssistant
api.Meter
@ -22,7 +22,7 @@ func decorateHomeAssistant(base *HomeAssistant, meter func() (float64, error), m
},
}
case currentGetter == nil && meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseVoltages == nil:
case meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseVoltages == nil:
return &struct {
*HomeAssistant
api.Meter
@ -37,7 +37,7 @@ func decorateHomeAssistant(base *HomeAssistant, meter func() (float64, error), m
},
}
case currentGetter == nil && meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseVoltages == nil:
case meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseVoltages == nil:
return &struct {
*HomeAssistant
api.Meter
@ -52,7 +52,7 @@ func decorateHomeAssistant(base *HomeAssistant, meter func() (float64, error), m
},
}
case currentGetter == nil && meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseVoltages == nil:
case meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseVoltages == nil:
return &struct {
*HomeAssistant
api.Meter
@ -71,7 +71,7 @@ func decorateHomeAssistant(base *HomeAssistant, meter func() (float64, error), m
},
}
case currentGetter == nil && meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseVoltages != nil:
case meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseVoltages != nil:
return &struct {
*HomeAssistant
api.Meter
@ -86,7 +86,7 @@ func decorateHomeAssistant(base *HomeAssistant, meter func() (float64, error), m
},
}
case currentGetter == nil && meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseVoltages != nil:
case meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseVoltages != nil:
return &struct {
*HomeAssistant
api.Meter
@ -105,7 +105,7 @@ func decorateHomeAssistant(base *HomeAssistant, meter func() (float64, error), m
},
}
case currentGetter == nil && meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseVoltages != nil:
case meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseVoltages != nil:
return &struct {
*HomeAssistant
api.Meter
@ -124,7 +124,7 @@ func decorateHomeAssistant(base *HomeAssistant, meter func() (float64, error), m
},
}
case currentGetter == nil && meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseVoltages != nil:
case meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseVoltages != nil:
return &struct {
*HomeAssistant
api.Meter
@ -146,198 +146,11 @@ func decorateHomeAssistant(base *HomeAssistant, meter func() (float64, error), m
phaseVoltages: phaseVoltages,
},
}
case currentGetter != nil && meter == nil:
return &struct {
*HomeAssistant
api.CurrentGetter
}{
HomeAssistant: base,
CurrentGetter: &decorateHomeAssistantCurrentGetterImpl{
currentGetter: currentGetter,
},
}
case currentGetter != nil && meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseVoltages == nil:
return &struct {
*HomeAssistant
api.CurrentGetter
api.Meter
}{
HomeAssistant: base,
CurrentGetter: &decorateHomeAssistantCurrentGetterImpl{
currentGetter: currentGetter,
},
Meter: &decorateHomeAssistantMeterImpl{
meter: meter,
},
}
case currentGetter != nil && meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseVoltages == nil:
return &struct {
*HomeAssistant
api.CurrentGetter
api.Meter
api.MeterEnergy
}{
HomeAssistant: base,
CurrentGetter: &decorateHomeAssistantCurrentGetterImpl{
currentGetter: currentGetter,
},
Meter: &decorateHomeAssistantMeterImpl{
meter: meter,
},
MeterEnergy: &decorateHomeAssistantMeterEnergyImpl{
meterEnergy: meterEnergy,
},
}
case currentGetter != nil && meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseVoltages == nil:
return &struct {
*HomeAssistant
api.CurrentGetter
api.Meter
api.PhaseCurrents
}{
HomeAssistant: base,
CurrentGetter: &decorateHomeAssistantCurrentGetterImpl{
currentGetter: currentGetter,
},
Meter: &decorateHomeAssistantMeterImpl{
meter: meter,
},
PhaseCurrents: &decorateHomeAssistantPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
}
case currentGetter != nil && meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseVoltages == nil:
return &struct {
*HomeAssistant
api.CurrentGetter
api.Meter
api.MeterEnergy
api.PhaseCurrents
}{
HomeAssistant: base,
CurrentGetter: &decorateHomeAssistantCurrentGetterImpl{
currentGetter: currentGetter,
},
Meter: &decorateHomeAssistantMeterImpl{
meter: meter,
},
MeterEnergy: &decorateHomeAssistantMeterEnergyImpl{
meterEnergy: meterEnergy,
},
PhaseCurrents: &decorateHomeAssistantPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
}
case currentGetter != nil && meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseVoltages != nil:
return &struct {
*HomeAssistant
api.CurrentGetter
api.Meter
api.PhaseVoltages
}{
HomeAssistant: base,
CurrentGetter: &decorateHomeAssistantCurrentGetterImpl{
currentGetter: currentGetter,
},
Meter: &decorateHomeAssistantMeterImpl{
meter: meter,
},
PhaseVoltages: &decorateHomeAssistantPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
case currentGetter != nil && meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseVoltages != nil:
return &struct {
*HomeAssistant
api.CurrentGetter
api.Meter
api.MeterEnergy
api.PhaseVoltages
}{
HomeAssistant: base,
CurrentGetter: &decorateHomeAssistantCurrentGetterImpl{
currentGetter: currentGetter,
},
Meter: &decorateHomeAssistantMeterImpl{
meter: meter,
},
MeterEnergy: &decorateHomeAssistantMeterEnergyImpl{
meterEnergy: meterEnergy,
},
PhaseVoltages: &decorateHomeAssistantPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
case currentGetter != nil && meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseVoltages != nil:
return &struct {
*HomeAssistant
api.CurrentGetter
api.Meter
api.PhaseCurrents
api.PhaseVoltages
}{
HomeAssistant: base,
CurrentGetter: &decorateHomeAssistantCurrentGetterImpl{
currentGetter: currentGetter,
},
Meter: &decorateHomeAssistantMeterImpl{
meter: meter,
},
PhaseCurrents: &decorateHomeAssistantPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
PhaseVoltages: &decorateHomeAssistantPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
case currentGetter != nil && meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseVoltages != nil:
return &struct {
*HomeAssistant
api.CurrentGetter
api.Meter
api.MeterEnergy
api.PhaseCurrents
api.PhaseVoltages
}{
HomeAssistant: base,
CurrentGetter: &decorateHomeAssistantCurrentGetterImpl{
currentGetter: currentGetter,
},
Meter: &decorateHomeAssistantMeterImpl{
meter: meter,
},
MeterEnergy: &decorateHomeAssistantMeterEnergyImpl{
meterEnergy: meterEnergy,
},
PhaseCurrents: &decorateHomeAssistantPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
PhaseVoltages: &decorateHomeAssistantPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
}
return nil
}
type decorateHomeAssistantCurrentGetterImpl struct {
currentGetter func() (float64, error)
}
func (impl *decorateHomeAssistantCurrentGetterImpl) GetMaxCurrent() (float64, error) {
return impl.currentGetter()
}
type decorateHomeAssistantMeterImpl struct {
meter func() (float64, error)
}

View file

@ -27,12 +27,12 @@ func init() {
// NewHomeAssistantFromConfig creates a HomeAssistant meter from generic config
func NewHomeAssistantFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
BaseURL string `mapstructure:"baseurl"`
Token string `mapstructure:"token"`
Power string `mapstructure:"power"`
Energy string `mapstructure:"energy"`
Currents []string `mapstructure:"currents"`
Voltages []string `mapstructure:"voltages"`
URI string
Token string
Power string
Energy string
Currents []string
Voltages []string
}{}
if err := util.DecodeOther(other, &cc); err != nil {
@ -43,7 +43,8 @@ func NewHomeAssistantFromConfig(other map[string]interface{}) (api.Meter, error)
return nil, errors.New("missing power sensor entity")
}
conn, err := homeassistant.NewConnection(cc.BaseURL, cc.Token)
log := util.NewLogger("ha-meter")
conn, err := homeassistant.NewConnection(log, cc.URI, cc.Token)
if err != nil {
return nil, err
}
@ -73,43 +74,20 @@ func NewHomeAssistantFromConfig(other map[string]interface{}) (api.Meter, error)
}
// decorators for optional interfaces
var meterEnergy func() (float64, error)
var phaseCurrents func() (float64, float64, float64, error)
var phaseVoltages func() (float64, float64, float64, error)
var energy func() (float64, error)
var currents, voltages func() (float64, float64, float64, error)
if m.energy != "" {
meterEnergy = m.TotalEnergy
energy = m.totalEnergy
}
if m.currentEntities[0] != "" {
phaseCurrents = m.Currents
currents = m.currents
}
if m.voltageEntities[0] != "" {
phaseVoltages = m.Voltages
voltages = m.voltages
}
return decorateHomeAssistant(m, meterEnergy, phaseCurrents, phaseVoltages), nil
}
// NewHomeAssistant creates HomeAssistant meter
func NewHomeAssistant(baseURL, token, power, energy string, currents, voltages []string) (*HomeAssistant, error) {
if power == "" {
return nil, errors.New("missing power sensor entity")
}
conn, err := homeassistant.NewConnection(baseURL, token)
if err != nil {
return nil, err
}
m := &HomeAssistant{
conn: conn,
power: power,
energy: energy,
currentEntities: currents,
voltageEntities: voltages,
}
return m, nil
return decorateHomeAssistant(m, energy, currents, voltages), nil
}
var _ api.Meter = (*HomeAssistant)(nil)
@ -119,26 +97,17 @@ func (m *HomeAssistant) CurrentPower() (float64, error) {
return m.conn.GetFloatState(m.power)
}
// TotalEnergy implements the api.MeterEnergy interface
func (m *HomeAssistant) TotalEnergy() (float64, error) {
if m.energy == "" {
return 0, api.ErrNotAvailable
}
// totalEnergy implements the api.MeterEnergy interface
func (m *HomeAssistant) totalEnergy() (float64, error) {
return m.conn.GetFloatState(m.energy)
}
// Currents implements the api.PhaseCurrents interface
func (m *HomeAssistant) Currents() (float64, float64, float64, error) {
if m.currentEntities[0] == "" {
return 0, 0, 0, api.ErrNotAvailable
}
return m.conn.GetPhaseStates(m.currentEntities)
// currents implements the api.PhaseCurrents interface
func (m *HomeAssistant) currents() (float64, float64, float64, error) {
return m.conn.GetPhaseFloatStates(m.currentEntities)
}
// Voltages implements the api.PhaseVoltages interface
func (m *HomeAssistant) Voltages() (float64, float64, float64, error) {
if m.voltageEntities[0] == "" {
return 0, 0, 0, api.ErrNotAvailable
}
return m.conn.GetPhaseStates(m.voltageEntities)
// voltages implements the api.PhaseVoltages interface
func (m *HomeAssistant) voltages() (float64, float64, float64, error) {
return m.conn.GetPhaseFloatStates(m.voltageEntities)
}

View file

@ -9,9 +9,8 @@ requirements:
evcc: ["skiptest"]
params:
- name: baseurl
description:
de: Basis-URL der Home Assistant Instanz
en: Base URL of the Home Assistant instance
deprecated: true
- name: uri
required: true
example: http://homeassistant.local:8123
- name: token
@ -37,7 +36,7 @@ params:
- preset: switchsocket
render: |
type: homeassistant-switch
baseurl: {{ .baseurl }}
uri: {{ if (eq .uri "") }}{{ .baseurl }}{{ else }}{{ .uri }}{{ end }}
token: {{ .token }}
switchentity: {{ .switchentity }}
powerentity: {{ .powerentity }}

View file

@ -11,11 +11,8 @@ requirements:
params:
- preset: vehicle-common
- name: uri
description:
de: Home Assistant URI
en: Home Assistant URI
example: "http://192.168.1.10:8123"
required: true
example: http://homeassistant.local:8123
- name: token
description:
de: Home Assistant Long-Lived Access Token

View file

@ -21,18 +21,17 @@ type Connection struct {
}
// NewConnection creates a new Home Assistant connection
func NewConnection(baseURL, token string) (*Connection, error) {
if baseURL == "" {
return nil, errors.New("missing baseURL")
func NewConnection(log *util.Logger, uri, token string) (*Connection, error) {
if uri == "" {
return nil, errors.New("missing uri")
}
if token == "" {
return nil, errors.New("missing token")
}
log := util.NewLogger("homeassistant")
c := &Connection{
Helper: request.NewHelper(log.Redact(token)),
uri: strings.TrimSuffix(baseURL, "/"),
uri: strings.TrimSuffix(uri, "/"),
}
// Set up authentication headers
@ -49,8 +48,8 @@ func NewConnection(baseURL, token string) (*Connection, error) {
// StateResponse represents a Home Assistant entity state
type StateResponse struct {
State string `json:"state"`
Attributes map[string]interface{} `json:"attributes"`
State string `json:"state"`
Attributes map[string]any `json:"attributes"`
}
// GetState retrieves the state of an entity
@ -69,6 +68,21 @@ func (c *Connection) GetState(entity string) (string, error) {
return res.State, nil
}
// GetIntState retrieves the state of an entity as int64
func (c *Connection) GetIntState(entity string) (int64, error) {
state, err := c.GetState(entity)
if err != nil {
return 0, err
}
value, err := strconv.ParseInt(state, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid numeric state '%s' for entity %s: %w", state, entity, err)
}
return value, nil
}
// GetFloatState retrieves the state of an entity as float64
func (c *Connection) GetFloatState(entity string) (float64, error) {
state, err := c.GetState(entity)
@ -102,8 +116,54 @@ func (c *Connection) GetBoolState(entity string) (bool, error) {
}
}
// chargeStatusMap maps Home Assistant states to EVCC charge status
var chargeStatusMap = map[string]api.ChargeStatus{
// Status C - Charging
"c": api.StatusC,
"charging": api.StatusC,
"on": api.StatusC,
"true": api.StatusC,
"active": api.StatusC,
"1": api.StatusC,
// Status B - Connected/Ready
"b": api.StatusB,
"connected": api.StatusB,
"ready": api.StatusB,
"plugged": api.StatusB,
"charging_completed": api.StatusB,
"initialising": api.StatusB,
"preparing": api.StatusB,
"2": api.StatusB,
// Status A - Disconnected
"a": api.StatusA,
"disconnected": api.StatusA,
"off": api.StatusA,
"none": api.StatusA,
"unavailable": api.StatusA,
"unknown": api.StatusA,
"notreadyforcharging": api.StatusA,
"not_plugged": api.StatusA,
"0": api.StatusA,
}
// GetChargeStatus maps Home Assistant states to api.ChargeStatus
func (c *Connection) GetChargeStatus(entity string) (api.ChargeStatus, error) {
state, err := c.GetState(entity)
if err != nil {
return api.StatusNone, err
}
if status, ok := chargeStatusMap[strings.ToLower(strings.TrimSpace(state))]; ok {
return status, nil
}
return api.StatusNone, fmt.Errorf("unknown charge status: %s", state)
}
// CallService calls a Home Assistant service
func (c *Connection) CallService(domain, service string, data map[string]interface{}) error {
func (c *Connection) CallService(domain, service string, data map[string]any) error {
uri := fmt.Sprintf("%s/api/services/%s/%s", c.uri, domain, service)
req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
@ -128,7 +188,7 @@ func (c *Connection) CallSwitchService(entity string, turnOn bool) error {
service = "turn_on"
}
data := map[string]interface{}{
data := map[string]any{
"entity_id": entity,
}
@ -137,7 +197,7 @@ func (c *Connection) CallSwitchService(entity string, turnOn bool) error {
// CallNumberService is a convenience method for setting number entity values
func (c *Connection) CallNumberService(entity string, value float64) error {
data := map[string]interface{}{
data := map[string]any{
"entity_id": entity,
"value": value,
}
@ -145,8 +205,8 @@ func (c *Connection) CallNumberService(entity string, value float64) error {
return c.CallService("number", "set_value", data)
}
// GetPhaseStates retrieves three phase values (currents, voltages, etc.)
func (c *Connection) GetPhaseStates(entities []string) (float64, float64, float64, error) {
// GetPhaseFloatStates retrieves three phase values (currents, voltages, etc.)
func (c *Connection) GetPhaseFloatStates(entities []string) (float64, float64, float64, error) {
if len(entities) != 3 {
return 0, 0, 0, errors.New("invalid phase entities")
}
@ -180,45 +240,3 @@ func ValidatePhaseEntities(entities []string) ([]string, error) {
return nil, fmt.Errorf("must contain three-phase entities (L1, L2, L3), got %d", len(entities))
}
}
// chargeStatusMap maps Home Assistant states to EVCC charge status
var chargeStatusMap = map[string]api.ChargeStatus{
// Status C - Charging
"c": api.StatusC,
"charging": api.StatusC,
"on": api.StatusC,
"true": api.StatusC,
"active": api.StatusC,
"1": api.StatusC,
// Status B - Connected/Ready
"b": api.StatusB,
"connected": api.StatusB,
"ready": api.StatusB,
"plugged": api.StatusB,
"charging_completed": api.StatusB,
"initialising": api.StatusB,
"preparing": api.StatusB,
"2": api.StatusB,
// Status A - Disconnected
"a": api.StatusA,
"disconnected": api.StatusA,
"off": api.StatusA,
"none": api.StatusA,
"unavailable": api.StatusA,
"unknown": api.StatusA,
"notreadyforcharging": api.StatusA,
"not_plugged": api.StatusA,
"0": api.StatusA,
}
// ParseChargeStatus maps Home Assistant states to EVCC charge status
func ParseChargeStatus(state string) (api.ChargeStatus, error) {
normalized := strings.ToLower(strings.TrimSpace(state))
if status, ok := chargeStatusMap[normalized]; ok {
return status, nil
}
return api.StatusNone, fmt.Errorf("unknown charge status: %s", state)
}

View file

@ -2,25 +2,18 @@ package vehicle
import (
"errors"
"fmt"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"time"
"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"
"github.com/evcc-io/evcc/util/homeassistant"
)
type HomeAssistant struct {
*embed
*request.Helper
uri string
soc string
conn *homeassistant.Connection
soc string
}
// Register on startup
@ -64,66 +57,57 @@ func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error
return nil, errors.New("missing soc sensor")
}
res := &HomeAssistant{
embed: &cc.embed,
Helper: request.NewHelper(util.NewLogger("ha-vehicle").Redact(cc.Token)),
uri: strings.TrimSuffix(cc.URI, "/"),
soc: cc.Sensors.Soc,
log := util.NewLogger("ha-vehicle")
conn, err := homeassistant.NewConnection(log, cc.URI, cc.Token)
if err != nil {
return nil, err
}
res.Client.Transport = &transport.Decorator{
Base: res.Client.Transport,
Decorator: transport.DecorateHeaders(map[string]string{
"Authorization": "Bearer " + cc.Token,
}),
res := &HomeAssistant{
embed: &cc.embed,
conn: conn,
soc: cc.Sensors.Soc,
}
// prepare optional feature functions with concise names
var (
limitSoc func() (int64, error)
status func() (api.ChargeStatus, error)
rng func() (int64, error)
odo func() (float64, error)
climater func() (bool, error)
finish func() (time.Time, error)
chargeEnable func(bool) error
wakeup func() error
maxCurrent func(int64) error
getMaxCurrent func() (float64, error)
limitSoc func() (int64, error)
status func() (api.ChargeStatus, error)
rng func() (int64, error)
odo func() (float64, error)
climater func() (bool, error)
finish func() (time.Time, error)
enable func(bool) error
wakeup func() error
maxcurrent func(int64) error
)
if cc.Sensors.LimitSoc != "" {
limitSoc = func() (int64, error) { return res.getIntSensor(cc.Sensors.LimitSoc) }
limitSoc = func() (int64, error) { return conn.GetIntState(cc.Sensors.LimitSoc) }
}
if cc.Sensors.Status != "" {
status = func() (api.ChargeStatus, error) { return res.status(cc.Sensors.Status) }
status = func() (api.ChargeStatus, error) { return conn.GetChargeStatus(cc.Sensors.Status) }
}
if cc.Sensors.Range != "" {
rng = func() (int64, error) { return res.getIntSensor(cc.Sensors.Range) }
rng = func() (int64, error) { return conn.GetIntState(cc.Sensors.Range) }
}
if cc.Sensors.Odometer != "" {
odo = func() (float64, error) { return res.getFloatSensor(cc.Sensors.Odometer) }
odo = func() (float64, error) { return conn.GetFloatState(cc.Sensors.Odometer) }
}
if cc.Sensors.Climater != "" {
climater = func() (bool, error) { return res.getBoolSensor(cc.Sensors.Climater) }
climater = func() (bool, error) { return conn.GetBoolState(cc.Sensors.Climater) }
}
if cc.Sensors.FinishTime != "" {
finish = func() (time.Time, error) { return res.getTimeSensor(cc.Sensors.FinishTime) }
finish = func() (time.Time, error) { return res.finishTime(cc.Sensors.FinishTime) }
}
if cc.Services.Start != "" && cc.Services.Stop != "" {
chargeEnable = func(enable bool) error {
if enable {
return res.callScript(cc.Services.Start)
}
return res.callScript(cc.Services.Stop)
}
enable = func(enable bool) error { return res.enable(cc.Services.Start, cc.Services.Stop, enable) }
}
if cc.Services.Wakeup != "" {
wakeup = func() error { return res.callScript(cc.Services.Wakeup) }
wakeup = func() error { return conn.CallSwitchService(cc.Services.Wakeup, true) }
}
if cc.Services.SetMaxCurrent != "" {
maxCurrent = func(current int64) error { return res.setMaxCurrent(cc.Services.SetMaxCurrent, current) }
getMaxCurrent = func() (float64, error) { return res.getFloatSensor(cc.Services.SetMaxCurrent) }
maxcurrent = func(current int64) error { return conn.CallNumberService(cc.Services.SetMaxCurrent, float64(current)) }
}
// decorate all features
@ -134,109 +118,20 @@ func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error
rng,
odo,
climater,
maxCurrent,
getMaxCurrent,
maxcurrent,
nil,
finish,
wakeup,
chargeEnable,
enable,
), nil
}
func (v *HomeAssistant) Soc() (float64, error) {
return v.getFloatSensor(v.soc)
return v.conn.GetFloatState(v.soc)
}
// Calls /api/states/<entity> and returns .state
func (v *HomeAssistant) getState(entity string) (string, error) {
var res struct {
State string `json:"state"`
}
uri := fmt.Sprintf("%s/api/states/%s", v.uri, url.PathEscape(entity))
if err := v.GetJSON(uri, &res); err != nil {
return "", err
}
if res.State == "unknown" || res.State == "unavailable" {
return "", api.ErrNotAvailable
}
return res.State, nil
}
func (v *HomeAssistant) callScript(script string) error {
// All configured services are scripts, so always use script.turn_on
payload := fmt.Sprintf(`{"entity_id": "%s"}`, script)
uri := fmt.Sprintf("%s/api/services/script/turn_on", v.uri)
req, _ := request.New(http.MethodPost, uri, strings.NewReader(payload))
_, err := v.DoBody(req)
return err
}
func (v *HomeAssistant) setMaxCurrent(entity string, current int64) error {
// Determine service domain from entity prefix
parts := strings.SplitN(entity, ".", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid entity format: %s", entity)
}
domain := parts[0]
var service string
switch domain {
case "number":
service = "set_value"
case "input_number":
service = "set_value"
default:
return fmt.Errorf("unsupported entity domain: %s", domain)
}
data := map[string]interface{}{
"entity_id": entity,
"value": current,
}
uri := fmt.Sprintf("%s/api/services/%s/%s", v.uri, domain, service)
req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
_, err := v.DoBody(req)
return err
}
// generic helpers for fetching and parsing sensor values
func (v *HomeAssistant) getFloatSensor(entity string) (float64, error) {
s, err := v.getState(entity)
if err != nil {
return 0, err
}
return strconv.ParseFloat(s, 64)
}
func (v *HomeAssistant) getIntSensor(entity string) (int64, error) {
s, err := v.getState(entity)
if err != nil {
return 0, err
}
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, err
}
return int64(f), nil // truncation
}
func (v *HomeAssistant) getBoolSensor(entity string) (bool, error) {
s, err := v.getState(entity)
if err != nil {
return false, err
}
return slices.Contains([]string{"on", "true", "1", "active"}, strings.ToLower(s)), nil
}
func (v *HomeAssistant) getTimeSensor(entity string) (time.Time, error) {
s, err := v.getState(entity)
func (v *HomeAssistant) finishTime(entity string) (time.Time, error) {
s, err := v.conn.GetState(entity)
if err != nil {
return time.Time{}, err
}
@ -248,39 +143,10 @@ func (v *HomeAssistant) getTimeSensor(entity string) (time.Time, error) {
return time.Parse(time.RFC3339, s)
}
// status returns evcc charge status (optional, private)
func (v *HomeAssistant) status(sensor string) (api.ChargeStatus, error) {
var haStatusMap = map[string]api.ChargeStatus{
"c": api.StatusC,
"charging": api.StatusC,
"on": api.StatusC,
"true": api.StatusC,
"active": api.StatusC,
"b": api.StatusB,
"connected": api.StatusB,
"ready": api.StatusB,
"plugged": api.StatusB,
"charging_completed": api.StatusB,
"initialising": api.StatusB,
"a": api.StatusA,
"disconnected": api.StatusA,
"off": api.StatusA,
"none": api.StatusA,
"unavailable": api.StatusA,
"unknown": api.StatusA,
"notreadyforcharging": api.StatusA,
"not_plugged": api.StatusA,
func (v *HomeAssistant) enable(on, off string, enable bool) error {
if enable {
return v.conn.CallSwitchService(on, true)
}
s, err := v.getState(sensor)
if err != nil {
return api.StatusNone, err
}
state := strings.ToLower(s)
if mapped, ok := haStatusMap[state]; ok {
return mapped, nil
}
return api.StatusNone, errors.New("invalid state: " + s)
return v.conn.CallSwitchService(off, true)
}