From db5c58dfce339b84e0b8e2d118fc2d0a6cf275a0 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 Oct 2025 13:31:41 +0200 Subject: [PATCH] Homeassistant: align implementations (BC) (#24237) --- charger/homeassistant-switch.go | 73 ++---- charger/homeassistant.go | 105 +++------ charger/homeassistant_decorators.go | 207 +---------------- meter/homeassistant.go | 75 ++----- .../charger/homeassistant-switch.yaml | 7 +- .../definition/vehicle/homeassistant.yaml | 5 +- util/homeassistant/connection.go | 126 ++++++----- vehicle/homeassistant.go | 212 ++++-------------- 8 files changed, 193 insertions(+), 617 deletions(-) diff --git a/charger/homeassistant-switch.go b/charger/homeassistant-switch.go index 91cef2952..884247581 100644 --- a/charger/homeassistant-switch.go +++ b/charger/homeassistant-switch.go @@ -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) } diff --git a/charger/homeassistant.go b/charger/homeassistant.go index b55d1bbca..7c200616e 100644 --- a/charger/homeassistant.go +++ b/charger/homeassistant.go @@ -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) -} diff --git a/charger/homeassistant_decorators.go b/charger/homeassistant_decorators.go index 6ae00d767..377ad60d4 100644 --- a/charger/homeassistant_decorators.go +++ b/charger/homeassistant_decorators.go @@ -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) } diff --git a/meter/homeassistant.go b/meter/homeassistant.go index b2edf6920..697209e35 100644 --- a/meter/homeassistant.go +++ b/meter/homeassistant.go @@ -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) } diff --git a/templates/definition/charger/homeassistant-switch.yaml b/templates/definition/charger/homeassistant-switch.yaml index 739b2b01b..fc01a0ae2 100644 --- a/templates/definition/charger/homeassistant-switch.yaml +++ b/templates/definition/charger/homeassistant-switch.yaml @@ -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 }} diff --git a/templates/definition/vehicle/homeassistant.yaml b/templates/definition/vehicle/homeassistant.yaml index 56bb7692e..451d730a9 100644 --- a/templates/definition/vehicle/homeassistant.yaml +++ b/templates/definition/vehicle/homeassistant.yaml @@ -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 diff --git a/util/homeassistant/connection.go b/util/homeassistant/connection.go index 1e6d6405d..b342316d8 100644 --- a/util/homeassistant/connection.go +++ b/util/homeassistant/connection.go @@ -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) -} diff --git a/vehicle/homeassistant.go b/vehicle/homeassistant.go index 7084d72d1..7eef46550 100644 --- a/vehicle/homeassistant.go +++ b/vehicle/homeassistant.go @@ -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/ 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) }