evcc-io/meter/homeassistant.go

157 lines
4.6 KiB
Go

package meter
import (
"errors"
"fmt"
"strings"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/api/implement"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/homeassistant"
)
func init() {
registry.Add("homeassistant", NewHomeAssistantFromConfig)
}
// NewHomeAssistantFromConfig creates a HomeAssistant meter from generic config
func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) {
cc := struct {
homeassistant.Config `mapstructure:",squash"`
Power string
Energy string
ReturnEnergy string
Currents []string
Voltages []string
Powers []string
Soc string
// pv
pvMaxACPower `mapstructure:",squash"`
// battery
batteryCapacity `mapstructure:",squash"`
batterySocLimits `mapstructure:",squash"`
batteryPowerLimits `mapstructure:",squash"`
// battery mode control - optional switch-like entities per mode
ModeNormal string
ModeHold string
ModeCharge string
}{
batterySocLimits: batterySocLimits{
MinSoc: 20,
MaxSoc: 95,
},
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.Power == "" {
return nil, errors.New("missing power sensor entity")
}
log := util.NewLogger("ha-meter")
conn, err := cc.Config.NewConnection(log)
if err != nil {
return nil, err
}
m, _ := NewConfigurable(func() (float64, error) {
return conn.GetFloatState(cc.Power)
})
// energy
if cc.Energy != "" {
implement.Has(m, implement.MeterEnergy(func() (float64, error) {
return conn.GetFloatState(cc.Energy)
}))
}
if cc.ReturnEnergy != "" {
implement.Has(m, implement.MeterReturnEnergy(func() (float64, error) {
return conn.GetFloatState(cc.ReturnEnergy)
}))
}
if cc.Soc != "" {
socG := func() (float64, error) { return conn.GetFloatState(cc.Soc) }
implement.Has(m, implement.Battery(socG))
implement.May(m, implement.BatteryCapacity(cc.batteryCapacity.Decorator()))
implement.May(m, implement.BatterySocLimiter(cc.batterySocLimits.Decorator()))
implement.May(m, implement.BatteryPowerLimiter(cc.batteryPowerLimits.Decorator()))
if cc.ModeHold != "" || cc.ModeCharge != "" {
if cc.ModeNormal == "" {
return nil, errors.New("modeNormal is required when modeHold or modeCharge is configured")
}
modes := map[api.BatteryMode]string{
api.BatteryNormal: cc.ModeNormal,
api.BatteryHold: cc.ModeHold,
api.BatteryCharge: cc.ModeCharge,
}
for _, entity := range modes {
if entity != "" && !strings.HasPrefix(entity, "script.") {
return nil, fmt.Errorf("battery mode entity must be a script: %s", entity)
}
}
implement.Has(m, implement.BatteryController(batteryModeController(conn, modes)))
} else if cc.ModeNormal != "" {
return nil, errors.New("modeNormal alone has no effect; configure modeHold and/or modeCharge")
}
return m, nil
}
// phase currents (optional)
if phases, err := homeassistant.ValidatePhaseEntities(cc.Currents); err != nil {
return nil, fmt.Errorf("currents: %w", err)
} else if len(phases) > 0 {
implement.Has(m, implement.PhaseCurrents(func() (float64, float64, float64, error) {
return conn.GetPhaseFloatStates(phases)
}))
}
// phase voltages (optional)
if phases, err := homeassistant.ValidatePhaseEntities(cc.Voltages); err != nil {
return nil, fmt.Errorf("voltages: %w", err)
} else if len(phases) > 0 {
implement.Has(m, implement.PhaseVoltages(func() (float64, float64, float64, error) {
return conn.GetPhaseFloatStates(phases)
}))
}
// phase powers (optional)
if phases, err := homeassistant.ValidatePhaseEntities(cc.Powers); err != nil {
return nil, fmt.Errorf("powers: %w", err)
} else if len(phases) > 0 {
implement.Has(m, implement.PhasePowers(func() (float64, float64, float64, error) {
return conn.GetPhaseFloatStates(phases)
}))
}
implement.May(m, implement.MaxACPowerGetter(cc.pvMaxACPower.Decorator()))
return m, nil
}
// batteryModeController returns a BatteryController function that activates
// the switch-like Home Assistant entity configured for the requested evcc
// battery mode. Each mode is self-contained: evcc only triggers the matching
// entity and never deactivates others - any mutual exclusion is the HA side's
// responsibility. modeHold and modeCharge are optional and return
// api.ErrNotAvailable when requested without a backing entity.
func batteryModeController(conn *homeassistant.Connection, modes map[api.BatteryMode]string) func(api.BatteryMode) error {
return func(mode api.BatteryMode) error {
target, ok := modes[mode]
if !ok || target == "" {
return api.ErrNotAvailable
}
return conn.CallSwitchService(target, true)
}
}