diff --git a/charger/homeassistant.go b/charger/homeassistant.go new file mode 100644 index 000000000..b55d1bbca --- /dev/null +++ b/charger/homeassistant.go @@ -0,0 +1,180 @@ +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)" + +import ( + "errors" + "fmt" + "math" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/homeassistant" +) + +// HomeAssistant charger implementation +type HomeAssistant struct { + conn *homeassistant.Connection + status string + enabled string + enable string + power string + energy string + currentsE []string + voltagesE []string + maxcurrent string +} + +func init() { + registry.Add("homeassistant", NewHomeAssistantFromConfig) +} + +// NewHomeAssistantFromConfig creates a HomeAssistant charger from generic config +func NewHomeAssistantFromConfig(other map[string]interface{}) (api.Charger, error) { + cc := struct { + BaseURL 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 + 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 { + return nil, err + } + + if cc.Status == "" { + return nil, errors.New("missing status sensor entity") + } + if cc.Enabled == "" { + return nil, errors.New("missing enabled sensor entity") + } + if cc.Enable == "" { + return nil, errors.New("missing enable switch entity") + } + + conn, err := homeassistant.NewConnection(cc.BaseURL, cc.Token) + if err != nil { + return nil, err + } + + c := &HomeAssistant{ + conn: conn, + status: cc.Status, + enabled: cc.Enabled, + enable: cc.Enable, + power: cc.Power, + energy: cc.Energy, + maxcurrent: cc.MaxCurrent, + } + + // 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 + } + + // Set up phase voltages (optional) + if len(cc.Voltages) > 0 { + res, err := homeassistant.ValidatePhaseEntities(cc.Voltages) + if err != nil { + return nil, fmt.Errorf("voltages: %w", err) + } + c.voltagesE = 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 +} + +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) +} + +// Enabled implements the api.Charger interface +func (c *HomeAssistant) Enabled() (bool, error) { + return c.conn.GetBoolState(c.enabled) +} + +// Enable implements the api.Charger interface +func (c *HomeAssistant) Enable(enable bool) error { + return c.conn.CallSwitchService(c.enable, enable) +} + +// MaxCurrent implements the api.CurrentController 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 new file mode 100644 index 000000000..6ae00d767 --- /dev/null +++ b/charger/homeassistant_decorators.go @@ -0,0 +1,371 @@ +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 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 { + switch { + case currentGetter == nil && meter == nil: + return base + + case currentGetter == nil && meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseVoltages == nil: + return &struct { + *HomeAssistant + api.Meter + }{ + HomeAssistant: base, + Meter: &decorateHomeAssistantMeterImpl{ + meter: meter, + }, + } + + case currentGetter == nil && meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseVoltages == nil: + return &struct { + *HomeAssistant + api.Meter + api.MeterEnergy + }{ + HomeAssistant: base, + Meter: &decorateHomeAssistantMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateHomeAssistantMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case currentGetter == nil && meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseVoltages == nil: + return &struct { + *HomeAssistant + api.Meter + api.PhaseCurrents + }{ + HomeAssistant: base, + Meter: &decorateHomeAssistantMeterImpl{ + meter: meter, + }, + PhaseCurrents: &decorateHomeAssistantPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case currentGetter == nil && meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseVoltages == nil: + return &struct { + *HomeAssistant + api.Meter + api.MeterEnergy + api.PhaseCurrents + }{ + HomeAssistant: base, + 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.Meter + api.PhaseVoltages + }{ + HomeAssistant: base, + Meter: &decorateHomeAssistantMeterImpl{ + meter: meter, + }, + PhaseVoltages: &decorateHomeAssistantPhaseVoltagesImpl{ + phaseVoltages: phaseVoltages, + }, + } + + case currentGetter == nil && meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseVoltages != nil: + return &struct { + *HomeAssistant + api.Meter + api.MeterEnergy + api.PhaseVoltages + }{ + HomeAssistant: base, + 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.Meter + api.PhaseCurrents + api.PhaseVoltages + }{ + HomeAssistant: base, + 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.Meter + api.MeterEnergy + api.PhaseCurrents + api.PhaseVoltages + }{ + HomeAssistant: base, + Meter: &decorateHomeAssistantMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateHomeAssistantMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateHomeAssistantPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseVoltages: &decorateHomeAssistantPhaseVoltagesImpl{ + 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) +} + +func (impl *decorateHomeAssistantMeterImpl) CurrentPower() (float64, error) { + return impl.meter() +} + +type decorateHomeAssistantMeterEnergyImpl struct { + meterEnergy func() (float64, error) +} + +func (impl *decorateHomeAssistantMeterEnergyImpl) TotalEnergy() (float64, error) { + return impl.meterEnergy() +} + +type decorateHomeAssistantPhaseCurrentsImpl struct { + phaseCurrents func() (float64, float64, float64, error) +} + +func (impl *decorateHomeAssistantPhaseCurrentsImpl) Currents() (float64, float64, float64, error) { + return impl.phaseCurrents() +} + +type decorateHomeAssistantPhaseVoltagesImpl struct { + phaseVoltages func() (float64, float64, float64, error) +} + +func (impl *decorateHomeAssistantPhaseVoltagesImpl) Voltages() (float64, float64, float64, error) { + return impl.phaseVoltages() +} diff --git a/meter/homeassistant.go b/meter/homeassistant.go new file mode 100644 index 000000000..b2edf6920 --- /dev/null +++ b/meter/homeassistant.go @@ -0,0 +1,144 @@ +package meter + +//go:generate go tool decorate -f decorateHomeAssistant -b *HomeAssistant -r api.Meter -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)" + +import ( + "errors" + "fmt" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/homeassistant" +) + +// HomeAssistant meter implementation +type HomeAssistant struct { + conn *homeassistant.Connection + power string + energy string + currentEntities []string + voltageEntities []string +} + +func init() { + registry.Add("homeassistant", NewHomeAssistantFromConfig) +} + +// 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"` + }{} + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.Power == "" { + return nil, errors.New("missing power sensor entity") + } + + conn, err := homeassistant.NewConnection(cc.BaseURL, cc.Token) + if err != nil { + return nil, err + } + + m := &HomeAssistant{ + conn: conn, + power: cc.Power, + energy: cc.Energy, + } + + // Set up phase currents (optional) + if len(cc.Currents) > 0 { + currents, err := homeassistant.ValidatePhaseEntities(cc.Currents) + if err != nil { + return nil, fmt.Errorf("currents: %w", err) + } + m.currentEntities = currents + } + + // Set up phase voltages (optional) + if len(cc.Voltages) > 0 { + voltages, err := homeassistant.ValidatePhaseEntities(cc.Voltages) + if err != nil { + return nil, fmt.Errorf("voltages: %w", err) + } + m.voltageEntities = voltages + } + + // decorators for optional interfaces + var meterEnergy func() (float64, error) + var phaseCurrents func() (float64, float64, float64, error) + var phaseVoltages func() (float64, float64, float64, error) + + if m.energy != "" { + meterEnergy = m.TotalEnergy + } + if m.currentEntities[0] != "" { + phaseCurrents = m.Currents + } + if m.voltageEntities[0] != "" { + phaseVoltages = 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 +} + +var _ api.Meter = (*HomeAssistant)(nil) + +// CurrentPower implements the api.Meter interface +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 + } + 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) +} + +// 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) +} diff --git a/meter/homeassistant_decorators.go b/meter/homeassistant_decorators.go new file mode 100644 index 000000000..dd826d7d5 --- /dev/null +++ b/meter/homeassistant_decorators.go @@ -0,0 +1,137 @@ +package meter + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/evcc-io/evcc/api" +) + +func decorateHomeAssistant(base *HomeAssistant, meterEnergy func() (float64, error), phaseCurrents func() (float64, float64, float64, error), phaseVoltages func() (float64, float64, float64, error)) api.Meter { + switch { + case meterEnergy == nil && phaseCurrents == nil && phaseVoltages == nil: + return base + + case meterEnergy != nil && phaseCurrents == nil && phaseVoltages == nil: + return &struct { + *HomeAssistant + api.MeterEnergy + }{ + HomeAssistant: base, + MeterEnergy: &decorateHomeAssistantMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case meterEnergy == nil && phaseCurrents != nil && phaseVoltages == nil: + return &struct { + *HomeAssistant + api.PhaseCurrents + }{ + HomeAssistant: base, + PhaseCurrents: &decorateHomeAssistantPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case meterEnergy != nil && phaseCurrents != nil && phaseVoltages == nil: + return &struct { + *HomeAssistant + api.MeterEnergy + api.PhaseCurrents + }{ + HomeAssistant: base, + MeterEnergy: &decorateHomeAssistantMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateHomeAssistantPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case meterEnergy == nil && phaseCurrents == nil && phaseVoltages != nil: + return &struct { + *HomeAssistant + api.PhaseVoltages + }{ + HomeAssistant: base, + PhaseVoltages: &decorateHomeAssistantPhaseVoltagesImpl{ + phaseVoltages: phaseVoltages, + }, + } + + case meterEnergy != nil && phaseCurrents == nil && phaseVoltages != nil: + return &struct { + *HomeAssistant + api.MeterEnergy + api.PhaseVoltages + }{ + HomeAssistant: base, + MeterEnergy: &decorateHomeAssistantMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseVoltages: &decorateHomeAssistantPhaseVoltagesImpl{ + phaseVoltages: phaseVoltages, + }, + } + + case meterEnergy == nil && phaseCurrents != nil && phaseVoltages != nil: + return &struct { + *HomeAssistant + api.PhaseCurrents + api.PhaseVoltages + }{ + HomeAssistant: base, + PhaseCurrents: &decorateHomeAssistantPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseVoltages: &decorateHomeAssistantPhaseVoltagesImpl{ + phaseVoltages: phaseVoltages, + }, + } + + case meterEnergy != nil && phaseCurrents != nil && phaseVoltages != nil: + return &struct { + *HomeAssistant + api.MeterEnergy + api.PhaseCurrents + api.PhaseVoltages + }{ + HomeAssistant: base, + MeterEnergy: &decorateHomeAssistantMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateHomeAssistantPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseVoltages: &decorateHomeAssistantPhaseVoltagesImpl{ + phaseVoltages: phaseVoltages, + }, + } + } + + return nil +} + +type decorateHomeAssistantMeterEnergyImpl struct { + meterEnergy func() (float64, error) +} + +func (impl *decorateHomeAssistantMeterEnergyImpl) TotalEnergy() (float64, error) { + return impl.meterEnergy() +} + +type decorateHomeAssistantPhaseCurrentsImpl struct { + phaseCurrents func() (float64, float64, float64, error) +} + +func (impl *decorateHomeAssistantPhaseCurrentsImpl) Currents() (float64, float64, float64, error) { + return impl.phaseCurrents() +} + +type decorateHomeAssistantPhaseVoltagesImpl struct { + phaseVoltages func() (float64, float64, float64, error) +} + +func (impl *decorateHomeAssistantPhaseVoltagesImpl) Voltages() (float64, float64, float64, error) { + return impl.phaseVoltages() +} diff --git a/util/homeassistant/connection.go b/util/homeassistant/connection.go new file mode 100644 index 000000000..1e6d6405d --- /dev/null +++ b/util/homeassistant/connection.go @@ -0,0 +1,224 @@ +package homeassistant + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "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" +) + +// Connection represents a Home Assistant API connection +type Connection struct { + *request.Helper + uri string +} + +// NewConnection creates a new Home Assistant connection +func NewConnection(baseURL, token string) (*Connection, error) { + if baseURL == "" { + return nil, errors.New("missing baseURL") + } + if token == "" { + return nil, errors.New("missing token") + } + + log := util.NewLogger("homeassistant") + c := &Connection{ + Helper: request.NewHelper(log.Redact(token)), + uri: strings.TrimSuffix(baseURL, "/"), + } + + // Set up authentication headers + c.Client.Transport = &transport.Decorator{ + Base: c.Client.Transport, + Decorator: transport.DecorateHeaders(map[string]string{ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }), + } + + return c, nil +} + +// StateResponse represents a Home Assistant entity state +type StateResponse struct { + State string `json:"state"` + Attributes map[string]interface{} `json:"attributes"` +} + +// GetState retrieves the state of an entity +func (c *Connection) GetState(entity string) (string, error) { + var res StateResponse + uri := fmt.Sprintf("%s/api/states/%s", c.uri, url.PathEscape(entity)) + + if err := c.GetJSON(uri, &res); err != nil { + return "", err + } + + if res.State == "unknown" || res.State == "unavailable" { + return "", api.ErrNotAvailable + } + + return res.State, nil +} + +// GetFloatState retrieves the state of an entity as float64 +func (c *Connection) GetFloatState(entity string) (float64, error) { + state, err := c.GetState(entity) + if err != nil { + return 0, err + } + + value, err := strconv.ParseFloat(state, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric state '%s' for entity %s: %w", state, entity, err) + } + + return value, nil +} + +// GetBoolState retrieves the state of an entity as boolean +func (c *Connection) GetBoolState(entity string) (bool, error) { + state, err := c.GetState(entity) + if err != nil { + return false, err + } + + state = strings.ToLower(state) + switch state { + case "on", "true", "1", "active", "yes": + return true, nil + case "off", "false", "0", "inactive", "no": + return false, nil + default: + return false, fmt.Errorf("invalid boolean state '%s' for entity %s", state, entity) + } +} + +// CallService calls a Home Assistant service +func (c *Connection) CallService(domain, service string, data map[string]interface{}) 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) + if err != nil { + return err + } + + _, err = c.DoBody(req) + return err +} + +// CallSwitchService is a convenience method for switch services +func (c *Connection) CallSwitchService(entity string, turnOn bool) error { + parts := strings.Split(entity, ".") + if len(parts) == 0 { + return fmt.Errorf("invalid entity format: %s", entity) + } + + domain := parts[0] + service := "turn_off" + if turnOn { + service = "turn_on" + } + + data := map[string]interface{}{ + "entity_id": entity, + } + + return c.CallService(domain, service, data) +} + +// CallNumberService is a convenience method for setting number entity values +func (c *Connection) CallNumberService(entity string, value float64) error { + data := map[string]interface{}{ + "entity_id": entity, + "value": value, + } + + 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) { + if len(entities) != 3 { + return 0, 0, 0, errors.New("invalid phase entities") + } + + var l1, l2, l3 float64 + var err error + + if l1, err = c.GetFloatState(entities[0]); err != nil { + return 0, 0, 0, fmt.Errorf("phase L1: %w", err) + } + + if l2, err = c.GetFloatState(entities[1]); err != nil { + return 0, 0, 0, fmt.Errorf("phase L2: %w", err) + } + + if l3, err = c.GetFloatState(entities[2]); err != nil { + return 0, 0, 0, fmt.Errorf("phase L3: %w", err) + } + + return l1, l2, l3, nil +} + +// ValidatePhaseEntities validates that phase entity arrays contain 1 or 3 entities +func ValidatePhaseEntities(entities []string) ([]string, error) { + switch len(entities) { + case 0: + return nil, nil + case 3: + return entities, nil + default: + 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) +}