diff --git a/util/homeassistant/connection.go b/util/homeassistant/connection.go index 796b702c0..7e9c7022e 100644 --- a/util/homeassistant/connection.go +++ b/util/homeassistant/connection.go @@ -7,6 +7,7 @@ import ( "net/url" "strconv" "strings" + "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" @@ -59,19 +60,19 @@ func (c *Connection) GetStates() ([]StateResponse, error) { } // GetState retrieves the state of an entity -func (c *Connection) GetState(entity string) (string, error) { +func (c *Connection) GetState(entity string) (StateResponse, error) { var res StateResponse uri := fmt.Sprintf("%s/api/states/%s", c.instance.URI(), url.PathEscape(entity)) if err := c.GetJSON(uri, &res); err != nil { - return "", err + return res, err } if res.State == "unknown" || res.State == "unavailable" { - return "", api.ErrNotAvailable + return res, api.ErrNotAvailable } - return res.State, nil + return res, nil } // GetIntState retrieves the state of an entity as int64 @@ -81,9 +82,9 @@ func (c *Connection) GetIntState(entity string) (int64, error) { return 0, err } - value, err := strconv.ParseInt(state, 10, 64) + value, err := strconv.ParseInt(state.State, 10, 64) if err != nil { - return 0, fmt.Errorf("invalid numeric state '%s' for entity %s: %w", state, entity, err) + return 0, fmt.Errorf("invalid numeric state '%s' for entity %s: %w", state.State, entity, err) } return value, nil @@ -96,12 +97,17 @@ func (c *Connection) GetFloatState(entity string) (float64, error) { return 0, err } - value, err := strconv.ParseFloat(state, 64) + value, err := strconv.ParseFloat(state.State, 64) if err != nil { - return 0, fmt.Errorf("invalid numeric state '%s' for entity %s: %w", state, entity, err) + return 0, fmt.Errorf("invalid numeric state '%s' for entity %s: %w", state.State, entity, err) } - return value, nil + scale, err := state.scale() + if err != nil { + return 0, fmt.Errorf("%w for entity %s", err, entity) + } + + return scale * value, nil } // GetBoolState retrieves the state of an entity as boolean @@ -111,8 +117,8 @@ func (c *Connection) GetBoolState(entity string) (bool, error) { return false, err } - state = strings.ToLower(state) - switch state { + res := strings.ToLower(state.State) + switch res { case "on", "true", "1", "active", "yes": return true, nil case "off", "false", "0", "inactive", "no": @@ -122,6 +128,20 @@ func (c *Connection) GetBoolState(entity string) (bool, error) { } } +// GetTimeState retrieves the state of an entity as time +func (c *Connection) GetTimeState(entity string) (time.Time, error) { + state, err := c.GetState(entity) + if err != nil { + return time.Time{}, err + } + + if ts, err := strconv.ParseInt(state.State, 10, 64); err == nil { + return time.Unix(ts, 0), nil + } + + return time.Parse(time.RFC3339, state.State) +} + // chargeStatusMap maps Home Assistant states to EVCC charge status var chargeStatusMap = map[string]api.ChargeStatus{ // Status C - Charging @@ -166,7 +186,7 @@ func (c *Connection) GetChargeStatus(entity string) (api.ChargeStatus, error) { return api.StatusNone, err } - if status, ok := chargeStatusMap[strings.ToLower(strings.TrimSpace(state))]; ok { + if status, ok := chargeStatusMap[strings.ToLower(strings.TrimSpace(state.State))]; ok { return status, nil } diff --git a/util/homeassistant/types.go b/util/homeassistant/types.go index f23311056..5a16d3c2e 100644 --- a/util/homeassistant/types.go +++ b/util/homeassistant/types.go @@ -1,5 +1,10 @@ package homeassistant +import ( + "fmt" + "strings" +) + type StateResponse struct { EntityId string `json:"entity_id"` State string `json:"state"` @@ -9,3 +14,26 @@ type StateResponse struct { FriendlyName string `json:"friendly_name"` } `json:"attributes"` } + +func (state StateResponse) scale() (float64, error) { + if unit, ok := strings.CutSuffix(state.Attributes.UnitOfMeasurement, "W"); ok { + switch unit { + case "": + case "k": + return 1e3, nil + default: + return 0, fmt.Errorf("invalid unit '%s'", state.Attributes.UnitOfMeasurement) + } + } else if unit, ok := strings.CutSuffix(state.Attributes.UnitOfMeasurement, "Wh"); ok { + switch unit { + case "": + return 1e-3, nil + case "k": + case "M": + return 1e3, nil + default: + return 0, fmt.Errorf("invalid unit '%s'", state.Attributes.UnitOfMeasurement) + } + } + return 1, nil +} diff --git a/vehicle/homeassistant.go b/vehicle/homeassistant.go index 9b484e223..3b18ff6ea 100644 --- a/vehicle/homeassistant.go +++ b/vehicle/homeassistant.go @@ -2,7 +2,6 @@ package vehicle import ( "errors" - "strconv" "time" "github.com/evcc-io/evcc/api" @@ -66,7 +65,7 @@ func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error soc: cc.Sensors.Soc, } - // prepare optional feature functions with concise names + // optional features var ( limitSoc func() (int64, error) status func() (api.ChargeStatus, error) @@ -101,7 +100,7 @@ func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error climater = func() (bool, error) { return conn.GetBoolState(cc.Sensors.Climater) } } if cc.Sensors.FinishTime != "" { - finish = func() (time.Time, error) { return res.finishTime(cc.Sensors.FinishTime) } + finish = func() (time.Time, error) { return conn.GetTimeState(cc.Sensors.FinishTime) } } if cc.Services.Start != "" && cc.Services.Stop != "" { enable = func(enable bool) error { return res.enable(cc.Services.Start, cc.Services.Stop, enable) } @@ -133,19 +132,6 @@ func (v *HomeAssistant) Soc() (float64, error) { return v.conn.GetFloatState(v.soc) } -func (v *HomeAssistant) finishTime(entity string) (time.Time, error) { - s, err := v.conn.GetState(entity) - if err != nil { - return time.Time{}, err - } - - if ts, err := strconv.ParseInt(s, 10, 64); err == nil { - return time.Unix(ts, 0), nil - } - - return time.Parse(time.RFC3339, s) -} - func (v *HomeAssistant) enable(on, off string, enable bool) error { if enable { return v.conn.CallSwitchService(on, true)