Home Assistant: configurable charge status values (BC) (#32136)
This commit is contained in:
parent
36a789b5c0
commit
c8b27650ca
6 changed files with 204 additions and 35 deletions
|
|
@ -167,55 +167,72 @@ func (c *Connection) GetTimeState(entity string) (time.Time, error) {
|
|||
return time.Parse(time.RFC3339, state.State)
|
||||
}
|
||||
|
||||
// chargeStatusMap maps Home Assistant states to EVCC charge status
|
||||
// chargeStatusMap maps unambiguous Home Assistant states to evcc charge status.
|
||||
// Vendor-specific states are configured per device, see NewStatusMap.
|
||||
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
|
||||
"a": api.StatusA,
|
||||
"disconnected": api.StatusA,
|
||||
"not_plugged": api.StatusA,
|
||||
"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,
|
||||
"no_power": api.StatusB,
|
||||
"complete": api.StatusB,
|
||||
"stopped": api.StatusB,
|
||||
"starting": api.StatusB,
|
||||
"stopped": api.StatusB,
|
||||
"paused": 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,
|
||||
"complete": api.StatusB,
|
||||
"charging_completed": api.StatusB,
|
||||
"c": api.StatusC,
|
||||
"charging": api.StatusC,
|
||||
}
|
||||
|
||||
// GetChargeStatus maps Home Assistant states to api.ChargeStatus
|
||||
func (c *Connection) GetChargeStatus(entity string) (api.ChargeStatus, error) {
|
||||
// StatusMap maps device-specific Home Assistant states to evcc charge status
|
||||
type StatusMap map[string]api.ChargeStatus
|
||||
|
||||
// NewStatusMap creates a status map from comma-separated, case-insensitive lists
|
||||
// of states. It extends the built-in mapping, overriding it only for states
|
||||
// explicitly mapped to a different status.
|
||||
func NewStatusMap(a, b, c string) (StatusMap, error) {
|
||||
res := make(StatusMap)
|
||||
|
||||
for _, e := range []struct {
|
||||
status api.ChargeStatus
|
||||
states string
|
||||
}{
|
||||
{api.StatusA, a},
|
||||
{api.StatusB, b},
|
||||
{api.StatusC, c},
|
||||
} {
|
||||
for _, s := range strings.Split(e.states, ",") {
|
||||
if s = strings.ToLower(strings.TrimSpace(s)); s != "" {
|
||||
if status, ok := res[s]; ok {
|
||||
return nil, fmt.Errorf("status %s: duplicate state '%s', already mapped to %s", e.status, s, status)
|
||||
}
|
||||
res[s] = e.status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetChargeStatus maps Home Assistant states to api.ChargeStatus. The
|
||||
// device-specific status map extends the built-in mapping and takes precedence.
|
||||
func (c *Connection) GetChargeStatus(entity string, states StatusMap) (api.ChargeStatus, error) {
|
||||
state, err := c.GetState(entity)
|
||||
if err != nil {
|
||||
return api.StatusNone, err
|
||||
}
|
||||
|
||||
if status, ok := chargeStatusMap[strings.ToLower(strings.TrimSpace(state.State))]; ok {
|
||||
s := strings.ToLower(strings.TrimSpace(state.State))
|
||||
|
||||
if status, ok := states[s]; ok {
|
||||
return status, nil
|
||||
}
|
||||
if status, ok := chargeStatusMap[s]; ok {
|
||||
return status, nil
|
||||
}
|
||||
|
||||
return api.StatusNone, fmt.Errorf("unknown charge status: %s", state)
|
||||
return api.StatusNone, fmt.Errorf("unknown charge status '%s' for entity %s", state.State, entity)
|
||||
}
|
||||
|
||||
// CallService calls a Home Assistant service
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package homeassistant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -19,6 +21,78 @@ func newTestConnection(baseURL string) *Connection {
|
|||
}
|
||||
}
|
||||
|
||||
// newStateConnection returns a connection serving state for any entity
|
||||
func newStateConnection(t *testing.T, state string) *Connection {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, `{"entity_id":"sensor.foo","state":%q}`, state)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
return newTestConnection(srv.URL)
|
||||
}
|
||||
|
||||
func TestGetChargeStatus(t *testing.T) {
|
||||
states, err := NewStatusMap("not_plugged", "Charging_Stopped, charging_error", "instant_charging")
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
state string
|
||||
want api.ChargeStatus
|
||||
}{
|
||||
{"A", api.StatusA},
|
||||
{"connected", api.StatusB},
|
||||
{" charging ", api.StatusC},
|
||||
{"not_plugged", api.StatusA},
|
||||
{"CHARGING_STOPPED", api.StatusB},
|
||||
{"charging_error", api.StatusB},
|
||||
{"instant_charging", api.StatusC},
|
||||
{"paused", api.StatusB},
|
||||
{"preparing", api.StatusNone}, // no longer built-in
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.state, func(t *testing.T) {
|
||||
status, err := newStateConnection(t, tc.state).GetChargeStatus("sensor.foo", states)
|
||||
assert.Equal(t, tc.want, status)
|
||||
if tc.want == api.StatusNone {
|
||||
assert.ErrorContains(t, err, "unknown charge status '"+tc.state+"' for entity sensor.foo")
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStatusMapDuplicate(t *testing.T) {
|
||||
_, err := NewStatusMap("foo", "foo", "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// TestStatusMapExtendsBuiltin verifies that configured states extend the
|
||||
// built-in mapping and only override the states they explicitly redefine.
|
||||
func TestStatusMapExtendsBuiltin(t *testing.T) {
|
||||
states, err := NewStatusMap("", "charging", "instant_charging")
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
state string
|
||||
want api.ChargeStatus
|
||||
}{
|
||||
{"charging", api.StatusB}, // redefined
|
||||
{"paused", api.StatusB}, // built-in, untouched
|
||||
{"c", api.StatusC}, // built-in, untouched
|
||||
{"instant_charging", api.StatusC}, // added
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.state, func(t *testing.T) {
|
||||
status, err := newStateConnection(t, tc.state).GetChargeStatus("sensor.foo", states)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCallSwitchService_DomainDispatch verifies that CallSwitchService picks
|
||||
// the correct service per Home Assistant domain — switches use turn_on /
|
||||
// turn_off, but the stateless button / input_button domains expose only
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue