Add Home Assistant meter and charger plugins (#24151)

Co-authored-by: Steven Cruysberghs <steven@smartenergycontrol.be>
This commit is contained in:
andig 2025-10-06 11:04:59 +02:00 • committed by GitHub
parent 768dace224
commit 98488e98cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1056 additions and 0 deletions

180
charger/homeassistant.go Normal file
View file

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

View file

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

144
meter/homeassistant.go Normal file
View file

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

View file

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

View file

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