api: let Identify return multiple identities (#32949)

This commit is contained in:
andig 2026-08-18 17:21:32 +02:00 • committed by GitHub
parent 0ae85295dc
commit 0be2e0d803
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 318 additions and 200 deletions

View file

@ -130,9 +130,10 @@ type ChargeRater interface {
ChargedEnergy() (float64, error)
}
// Identifier identifies a vehicle and is implemented by the charger
// Identifier identifies a vehicle and is implemented by the charger.
// A charger may know more than one identity, e.g. an RFID tag and a vehicle id.
type Identifier interface {
Identify() (string, error)
Identify() ([]string, error)
}
// Authorizer authorizes a charging session by supplying RFID credentials

View file

@ -228,7 +228,7 @@ func (i *iDimmer) Dimmed() (bool, error) {
return i.dimmer1()
}
func Identifier(identifier0 func() (string, error)) api.Identifier {
func Identifier(identifier0 func() ([]string, error)) api.Identifier {
if identifier0 == nil {
return nil
}
@ -236,10 +236,10 @@ func Identifier(identifier0 func() (string, error)) api.Identifier {
}
type iIdentifier struct {
identifier0 func() (string, error)
identifier0 func() ([]string, error)
}
func (i *iIdentifier) Identify() (string, error) {
func (i *iIdentifier) Identify() ([]string, error) {
return i.identifier0()
}

View file

@ -396,10 +396,10 @@ func (m *MockIdentifier) EXPECT() *MockIdentifierMockRecorder {
}
// Identify mocks base method.
func (m *MockIdentifier) Identify() (string, error) {
func (m *MockIdentifier) Identify() ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Identify")
ret0, _ := ret[0].(string)
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}

View file

@ -22,6 +22,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"github.com/evcc-io/evcc/api"
@ -367,21 +368,24 @@ func (wb *AlpitronicHYC) TotalEnergy() (float64, error) {
var _ api.Identifier = (*AlpitronicHYC)(nil)
// Identify implements the api.Identifier interface
func (wb *AlpitronicHYC) Identify() (string, error) {
func (wb *AlpitronicHYC) Identify() ([]string, error) {
b, err := wb.inputG()
if err != nil {
return "", err
return nil, err
}
var res []string
// the idTag register holds the rfid uid as null-padded ascii string
if idTag := hycInput(b, hycRegIdTag, 20); !allZero(idTag) {
res = append(res, strings.ToLower(strings.TrimRight(string(idTag), "\x00")))
}
if vid := hycInput(b, hycRegVID, 8); !allZero(vid) {
return hex.EncodeToString(vid), nil
res = append(res, hex.EncodeToString(vid))
}
if idTag := hycInput(b, hycRegIdTag, 20); !allZero(idTag) {
return hex.EncodeToString(idTag), nil
}
return "", nil
return res, nil
}
var _ api.Battery = (*AlpitronicHYC)(nil)

View file

@ -3,7 +3,6 @@ package charger
import (
"context"
"net"
"strings"
"sync"
"testing"
"time"
@ -218,7 +217,7 @@ func TestAlpitronicIdentify(t *testing.T) {
require.NoError(t, err)
assert.Empty(t, id)
// vehicle id takes precedence over the tag
// tag before vehicle id
regs := hycRegs(1, hycStateCharging)
regs[hycReg(1, hycRegVID)+1] = 0xAABB
regs[hycReg(1, hycRegVID)+2] = 0xCCDD
@ -229,9 +228,9 @@ func TestAlpitronicIdentify(t *testing.T) {
id, err = wb.Identify()
require.NoError(t, err)
assert.Equal(t, "0000aabbccddeeff", id)
assert.Equal(t, []string{"ab", "0000aabbccddeeff"}, id)
// tag as fallback
// tag only
regs = hycRegs(1, hycStateCharging)
regs[hycReg(1, hycRegIdTag)] = 0x4142
@ -239,7 +238,17 @@ func TestAlpitronicIdentify(t *testing.T) {
id, err = wb.Identify()
require.NoError(t, err)
assert.Equal(t, "4142"+strings.Repeat("0", 36), id)
assert.Equal(t, []string{"ab"}, id)
// vehicle id only
regs = hycRegs(1, hycStateCharging)
regs[hycReg(1, hycRegVID)+3] = 0xEEFF
wb, _ = hycTestCharger(t, 1, regs)
id, err = wb.Identify()
require.NoError(t, err)
assert.Equal(t, []string{"000000000000eeff"}, id)
}
func TestAlpitronicEnable(t *testing.T) {

View file

@ -299,13 +299,13 @@ func (wb *Amperfied) Voltages() (float64, float64, float64, error) {
var _ api.Identifier = (*Amperfied)(nil)
// Identify implements the api.Identifier interface
func (wb *Amperfied) Identify() (string, error) {
func (wb *Amperfied) Identify() ([]string, error) {
b, err := wb.conn.ReadInputRegisters(ampRegRfidUID, 6)
if err != nil {
return "", err
return nil, err
}
return hex.EncodeToString(b), nil
return []string{hex.EncodeToString(b)}, nil
}
var _ api.Diagnosis = (*Amperfied)(nil)

View file

@ -504,24 +504,33 @@ func (wb *BenderCC) getPhases() (int, error) {
}
// identify implements the api.Identifier interface
func (wb *BenderCC) identify() (string, error) {
func (wb *BenderCC) identify() ([]string, error) {
var ids []string
if !wb.legacy && !wb.mennekes4 {
b, err := wb.conn.ReadHoldingRegisters(bendRegSmartVehicleDetected, 1)
if err == nil && binary.BigEndian.Uint16(b) != 0 {
b, err = wb.conn.ReadHoldingRegisters(bendRegEVCCID, 6)
}
if err != nil {
return nil, err
}
if id := bytesAsString(b); id != "" || err != nil {
return id, err
if id := bytesAsString(b); id != "" {
ids = append(ids, id)
}
}
b, err := wb.conn.ReadHoldingRegisters(bendRegUserID, 10)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(b), nil
if id := bytesAsString(b); id != "" {
ids = append(ids, id)
}
return ids, nil
}
// soc implements the api.Battery interface

View file

@ -242,11 +242,11 @@ func (wb *CfosPowerBrain) WakeUp() error {
var _ api.Identifier = (*CfosPowerBrain)(nil)
// Identify implements the api.Identifier interface
func (wb *CfosPowerBrain) Identify() (string, error) {
func (wb *CfosPowerBrain) Identify() ([]string, error) {
b, err := wb.conn.ReadHoldingRegisters(cfosRegLastRfid, 15)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(b), nil
return []string{bytesAsString(b)}, nil
}

View file

@ -118,7 +118,12 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.C
if err != nil {
return nil, fmt.Errorf("identify: %w", err)
}
implement.May(c, implement.Identifier(identify))
if identify != nil {
implement.Has(c, implement.Identifier(func() ([]string, error) {
id, err := identify()
return []string{id}, err
}))
}
// decorate wakeup
if cc.Wakeup != nil {

View file

@ -289,13 +289,13 @@ func (wb *Compleo) ChargeDuration() (time.Duration, error) {
var _ api.Identifier = (*Compleo)(nil)
// Identify implements the api.Identifier interface
func (wb *Compleo) Identify() (string, error) {
func (wb *Compleo) Identify() ([]string, error) {
b, err := wb.conn.ReadInputRegisters(compleoRegIdTag+wb.offset, 10)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(b), nil
return []string{bytesAsString(b)}, nil
}
var _ loadpoint.Controller = (*Compleo)(nil)

View file

@ -291,13 +291,13 @@ func (wb *Dadapower) Phases1p3p(phases int) error {
var _ api.Identifier = (*Dadapower)(nil)
// Identify implements the api.Identifier interface
func (wb *Dadapower) Identify() (string, error) {
func (wb *Dadapower) Identify() ([]string, error) {
u, err := wb.conn.ReadInputRegisters(dadapowerRegIdentification+wb.regOffset, 20)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(u), nil
return []string{bytesAsString(u)}, nil
}
var _ api.Diagnosis = (*Dadapower)(nil)

View file

@ -318,12 +318,14 @@ func (wb *DaheimLaden) Voltages() (float64, float64, float64, error) {
var _ api.Identifier = (*DaheimLaden)(nil)
// Identify implements the api.Identifier interface. Only usable with PRO
func (wb *DaheimLaden) Identify() (string, error) {
func (wb *DaheimLaden) Identify() ([]string, error) {
b, err := wb.conn.ReadHoldingRegisters(dlRegCardId, 16)
if err != nil {
return "", err
return nil, err
}
return utf16BEBytesAsString(b)
id, err := utf16BEBytesAsString(b)
return []string{id}, err
}
// phases1p3p implements the api.PhaseSwitcher interface

View file

@ -354,13 +354,13 @@ func (wb *Delta) CurrentPower() (float64, error) {
var _ api.Identifier = (*Delta)(nil)
// Identify implements the api.Identifier interface
func (wb *Delta) Identify() (string, error) {
func (wb *Delta) Identify() ([]string, error) {
b, err := wb.conn.ReadInputRegisters(wb.base+deltaRegEvseRfidUID, 20)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(b), nil
return []string{bytesAsString(b)}, nil
}
var _ api.Diagnosis = (*Delta)(nil)

View file

@ -748,16 +748,17 @@ var _ api.Identifier = (*E3dc)(nil)
// Identify implements the api.Identifier interface
// Returns the RFID tag ID from WB_SESSION_AUTH_DATA if a session is active
func (wb *E3dc) Identify() (string, error) {
func (wb *E3dc) Identify() ([]string, error) {
wb.mu.Lock()
defer wb.mu.Unlock()
msg, found, err := wb.sessionMessage(rscp.WB_SESSION_AUTH_DATA)
if err != nil || !found {
return "", err
return nil, err
}
return rscpString(msg)
id, err := rscpString(msg)
return []string{id}, err
}
var _ api.PhaseSwitcher = (*E3dc)(nil)

View file

@ -850,11 +850,11 @@ func (c *Easee) StatusReason() (api.Reason, error) {
var _ api.Identifier = (*Easee)(nil)
// Currents implements the api.PhaseCurrents interface
func (c *Easee) Identify() (string, error) {
func (c *Easee) Identify() ([]string, error) {
c.mux.Lock()
defer c.mux.Unlock()
return c.rfid, nil
return []string{c.rfid}, nil
}
// Set smart charging status to update the chargers led (smart=blue, fast=white)

View file

@ -520,19 +520,26 @@ func (c *EEBus) currents() (float64, float64, float64, error) {
var _ api.Identifier = (*EEBus)(nil)
// Identify implements the api.Identifier interface
func (c *EEBus) Identify() (string, error) {
func (c *EEBus) Identify() ([]string, error) {
evEntity, ok := c.isEvConnected()
if !ok {
return "", nil
return nil, nil
}
if identification, err := c.cem.EvCC.Identifications(evEntity); err == nil && len(identification) > 0 {
// return the first identification for now
// later this could be multiple, e.g. MAC Address and PCID
return identification[0].Value, nil
identification, err := c.cem.EvCC.Identifications(evEntity)
if err != nil {
return nil, nil
}
return "", nil
// may be multiple, e.g. MAC address and PCID
var res []string
for _, i := range identification {
if i.Value != "" {
res = append(res, i.Value)
}
}
return res, nil
}
var _ api.Battery = (*EEBus)(nil)

View file

@ -233,13 +233,13 @@ func (wb *Enovates) Voltages() (float64, float64, float64, error) {
var _ api.Identifier = (*Enovates)(nil)
// Identify implements the api.Identifier interface
func (wb *Enovates) Identify() (string, error) {
func (wb *Enovates) Identify() ([]string, error) {
b, err := wb.conn.ReadHoldingRegisters(enovatesRegToken, 16)
if err != nil {
return "", err
return nil, err
}
return trimModbusString(b), nil
return []string{trimModbusString(b)}, nil
}
var _ api.Diagnosis = (*Enovates)(nil)

View file

@ -378,11 +378,11 @@ func (wb *EVECUBE) phases1p3p(phases int) error {
}
// identify implements the api.Identifier interface
func (wb *EVECUBE) identify() (string, error) {
func (wb *EVECUBE) identify() ([]string, error) {
status, err := wb.getAutomationStatus()
if err != nil {
return "", err
return nil, err
}
return status.AuthTag.Tag, nil
return []string{status.AuthTag.Tag}, nil
}

View file

@ -229,14 +229,14 @@ func (wb *EVSEWifi) voltages() (float64, float64, float64, error) {
}
// Identify implements the api.Identifier interface
func (wb *EVSEWifi) identify() (string, error) {
func (wb *EVSEWifi) identify() ([]string, error) {
params, err := wb.paramG.Get()
if err != nil {
return "", err
return nil, err
}
// we can rely on RFIDUID != nil here since identify() is only exposed if the EVSE API supports that property
return *params.RFIDUID, nil
return []string{*params.RFIDUID}, nil
}
var _ api.Resurrector = (*EVSEWifi)(nil)

View file

@ -599,17 +599,17 @@ func (wb *FoxESSEVC) Voltages() (float64, float64, float64, error) {
var _ api.Identifier = (*FoxESSEVC)(nil)
// Identify implements the api.Identifier interface
func (wb *FoxESSEVC) Identify() (string, error) {
func (wb *FoxESSEVC) Identify() ([]string, error) {
id, err := wb.readUint32(foxRegRFID)
if err != nil {
return "", err
return nil, err
}
if id == 0 {
return "", nil
return nil, nil
}
return fmt.Sprintf("%08X", id), nil
return []string{fmt.Sprintf("%08X", id)}, nil
}
// phases1p3p implements the api.PhaseSwitcher interface

View file

@ -135,8 +135,9 @@ func (c *Wattpilot) Voltages() (float64, float64, float64, error) {
var _ api.Identifier = (*Wattpilot)(nil)
// Identify implements the api.Identifier interface
func (c *Wattpilot) Identify() (string, error) {
return c.api.GetRFID()
func (c *Wattpilot) Identify() ([]string, error) {
id, err := c.api.GetRFID()
return []string{id}, err
}
var _ api.PhaseSwitcher = (*Wattpilot)(nil)

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"github.com/enbility/spine-go/model"
"github.com/evcc-io/evcc/api"
@ -112,14 +113,22 @@ func NewGhostEEBus(ctx context.Context, ski, ip, user, password string, hasMeter
var _ api.Identifier = (*GhostEEBus)(nil)
// Identify implements api.Identifier, preferring RFID over EEBUS identification
func (wb *GhostEEBus) Identify() (string, error) {
// Identify implements api.Identifier, reporting RFID before EEBUS identification
func (wb *GhostEEBus) Identify() ([]string, error) {
var res []string
if wb.hasRFID {
if id, err := wb.identify(); err == nil && id != "" {
return id, nil
if ids, err := wb.identify(); err == nil {
res = append(res, ids...)
}
}
return wb.EEBus.Identify()
ids, err := wb.EEBus.Identify()
if err != nil {
return nil, err
}
return slices.DeleteFunc(append(res, ids...), func(id string) bool { return id == "" }), nil
}
// getJSONCtx executes a context-aware GET request and decodes the JSON response.
@ -215,8 +224,8 @@ func (wb *GhostEEBus) getPhases() (int, error) {
}
// identify implements RFID identification via REST API.
func (wb *GhostEEBus) identify() (string, error) {
func (wb *GhostEEBus) identify() ([]string, error) {
var res ghostone.RfidCardLastRead
err := wb.GetJSON(wb.uri+"/rfid-cards/last-read", &res)
return res.UUID, err
return []string{res.UUID}, err
}

View file

@ -332,17 +332,17 @@ func TestGhostEEBus_Identify(t *testing.T) {
tests := []struct {
name string
uuid string
wantID string
wantID []string
}{
{
name: "with_uuid",
uuid: "ABC123",
wantID: "ABC123",
wantID: []string{"ABC123"},
},
{
name: "empty",
uuid: "",
wantID: "",
wantID: []string{""},
},
}
@ -378,13 +378,17 @@ func TestGhostEEBus_IdentifyFallback(t *testing.T) {
id, err := wb.Identify()
require.NoError(t, err)
assert.Equal(t, "MAC-001", id)
assert.Equal(t, []string{"MAC-001"}, id)
})
t.Run("rfid_returns_id", func(t *testing.T) {
wb, _, _ := newTestGhostEEBusWithEEBus(t)
t.Run("rfid_and_eebus", func(t *testing.T) {
wb, evccMock, evEntity := newTestGhostEEBusWithEEBus(t)
wb.hasRFID = true
// both identities are reported
evccMock.EXPECT().EVConnected(evEntity).Return(true)
evccMock.EXPECT().Identifications(evEntity).Return([]ucapi.IdentificationItem{{Value: "MAC-001"}}, nil)
httpmock.ActivateNonDefault(wb.Client)
defer httpmock.DeactivateAndReset()
@ -397,7 +401,7 @@ func TestGhostEEBus_IdentifyFallback(t *testing.T) {
id, err := wb.Identify()
require.NoError(t, err)
assert.Equal(t, "RFID-42", id)
assert.Equal(t, []string{"RFID-42", "MAC-001"}, id)
})
t.Run("rfid_empty_falls_back_to_eebus", func(t *testing.T) {
@ -421,6 +425,6 @@ func TestGhostEEBus_IdentifyFallback(t *testing.T) {
id, err := wb.Identify()
require.NoError(t, err)
assert.Equal(t, "MAC-001", id)
assert.Equal(t, []string{"MAC-001"}, id)
})
}

View file

@ -199,12 +199,12 @@ func (c *GoE) Voltages() (float64, float64, float64, error) {
var _ api.Identifier = (*GoE)(nil)
// Identify implements the api.Identifier interface
func (c *GoE) Identify() (string, error) {
func (c *GoE) Identify() ([]string, error) {
resp, err := c.api.Status()
if err != nil {
return "", err
return nil, err
}
return resp.Identify(), nil
return []string{resp.Identify()}, nil
}
var _ api.MeterEnergy = (*GoE)(nil)

View file

@ -270,12 +270,12 @@ func (wb *GoodWe) phases1p3p(phases int) error {
var _ api.Identifier = (*GoodWe)(nil)
// Identify implements api.Identifier (RFID UID, 14-byte NUL-padded ASCII).
func (wb *GoodWe) Identify() (string, error) {
func (wb *GoodWe) Identify() ([]string, error) {
b, err := wb.conn.ReadHoldingRegisters(goodweRegRfid, 7)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(bytes.Trim(b, "\x00")), nil
return []string{bytesAsString(bytes.Trim(b, "\x00"))}, nil
}
var _ loadpoint.Controller = (*GoodWe)(nil)

View file

@ -248,6 +248,6 @@ func (wb *HardyBarth) Voltages() (float64, float64, float64, error) {
// var _ api.Identifier = (*HardyBarth)(nil)
// // Identify implements the api.Identifier interface
// func (wb *HardyBarth) Identify() (string, error) {
// return "", api.ErrNotAvailable
// func (wb *HardyBarth) Identify() ([]string, error) {
// return nil, api.ErrNotAvailable
// }

View file

@ -279,12 +279,12 @@ func (wb *Salia) currents() (float64, float64, float64, error) {
return i.L1.Actual / 1e3, i.L2.Actual / 1e3, i.L3.Actual / 1e3, err
}
func (wb *Salia) Identify() (string, error) {
func (wb *Salia) Identify() ([]string, error) {
res, err := wb.apiG.Get()
if err != nil {
return "", err
return nil, err
}
return res.Secc.Port0.RFID.AuthorizationRequest.Key, nil
return []string{res.Secc.Port0.RFID.AuthorizationRequest.Key}, nil
}
func (wb *Salia) getPhases() (int, error) {

View file

@ -399,29 +399,29 @@ func (wb *Kathrein) StatusReason() (api.Reason, error) {
var _ api.Identifier = (*Kathrein)(nil)
// Identify implements the api.Identifier interface
func (wb *Kathrein) Identify() (string, error) {
func (wb *Kathrein) Identify() ([]string, error) {
s, err := wb.conn.ReadHoldingRegisters(kathreinRegChargingState, 1)
if err != nil {
return "", err
return nil, err
}
state := binary.BigEndian.Uint16(s)
if state < 3 || state > 6 {
return "", nil
return nil, nil
}
b, err := wb.conn.ReadHoldingRegisters(kathreinRegRfid, 24)
if err != nil {
return "", err
return nil, err
}
rfid := string(bytes.TrimRight(b, "\x00"))
if strings.HasPrefix(rfid, "RFID:") {
return rfid[5:], nil
return []string{rfid[5:]}, nil
}
return rfid, nil
return []string{rfid}, nil
}
var _ api.Diagnosis = (*Kathrein)(nil)

View file

@ -358,10 +358,10 @@ func (wb *Keba) currents() (float64, float64, float64, error) {
}
// identify implements the api.Identifier interface
func (wb *Keba) identify() (string, error) {
func (wb *Keba) identify() ([]string, error) {
b, err := wb.conn.ReadHoldingRegisters(kebaRegRfid, 2)
if err != nil {
return "", err
return nil, err
}
id := hex.EncodeToString(b)
@ -369,7 +369,7 @@ func (wb *Keba) identify() (string, error) {
id = ""
}
return id, nil
return []string{id}, nil
}
// phases1p3p implements the api.PhaseSwitcher interface

View file

@ -305,10 +305,10 @@ func (c *KebaUdp) currents() (float64, float64, float64, error) {
var _ api.Identifier = (*KebaUdp)(nil)
// Identify implements the api.Identifier interface
func (c *KebaUdp) Identify() (string, error) {
func (c *KebaUdp) Identify() ([]string, error) {
var kr keba.Report100
err := c.roundtrip("report", 100, &kr)
return kr.RFIDTag, err
return []string{kr.RFIDTag}, err
}
var _ api.Diagnosis = (*KebaUdp)(nil)

View file

@ -263,13 +263,13 @@ func (wb *KSE) getPhases() (int, error) {
}
// Identify implements the api.Identifier interface
func (wb *KSE) identify() (string, error) {
func (wb *KSE) identify() ([]string, error) {
b, err := wb.conn.ReadHoldingRegisters(kseRegNFCTransactionID, 4)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(b), nil
return []string{bytesAsString(b)}, nil
}
var _ api.Diagnosis = (*KSE)(nil)

View file

@ -414,8 +414,8 @@ func (c *OCPP) phases1p3p(phases int) error {
var _ api.Identifier = (*OCPP)(nil)
// Identify implements the api.Identifier interface
func (c *OCPP) Identify() (string, error) {
return c.conn.IdTag(), nil
func (c *OCPP) Identify() ([]string, error) {
return []string{c.conn.IdTag()}, nil
}
var _ api.Diagnosis = (*OCPP)(nil)

View file

@ -322,7 +322,7 @@ func (suite *ocppTestSuite) TestAutoStart() {
id, err := c1.Identify()
suite.Require().NoError(err)
suite.Require().Equal(expectedIdTag, id)
suite.Require().Equal([]string{expectedIdTag}, id)
conn1 := c1.Connector()
_, err = conn1.TransactionID()

View file

@ -227,10 +227,10 @@ func (wb *OpenWB20) WakeUp() error {
}
// Identify implements the api.Identifier interface
func (wb *OpenWB20) identify() (string, error) {
func (wb *OpenWB20) identify() ([]string, error) {
b, err := wb.conn.ReadInputRegisters(wb.base+openwbRegRfid, 10)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(b), nil
return []string{bytesAsString(b)}, nil
}

View file

@ -202,6 +202,6 @@ func (wb *OpenWbNative) gpioSwitchPhases(phases int) error {
}
// Identify implements the api.Identifier interface
func (wb *OpenWbNative) identify() (string, error) {
return wb.rfId.Get(), nil
func (wb *OpenWbNative) identify() ([]string, error) {
return []string{wb.rfId.Get()}, nil
}

View file

@ -232,17 +232,21 @@ func (wb *OpenWBPro) Phases1p3p(phases int) error {
var _ api.Identifier = (*OpenWBPro)(nil)
// Identify implements the api.Identifier interface
func (wb *OpenWBPro) Identify() (string, error) {
func (wb *OpenWBPro) Identify() ([]string, error) {
res, err := wb.statusG.Get()
if err != nil || res.VehicleID == "--" {
return "", err
if err != nil {
return nil, err
}
if res.VehicleID != "" {
return res.VehicleID, nil
var ids []string
if res.VehicleID != "" && res.VehicleID != "--" {
ids = append(ids, res.VehicleID)
}
if res.RfidTag != "" {
ids = append(ids, res.RfidTag)
}
return res.RfidTag, nil
return ids, nil
}
func (wb *OpenWBPro) wakeup() error {

View file

@ -267,22 +267,27 @@ func (wb *PhoenixCharx) getPhaseValues(reg uint16) (float64, float64, float64, e
var _ api.Identifier = (*PhoenixCharx)(nil)
// Identify implements the api.Identifier interface
func (wb *PhoenixCharx) Identify() (string, error) {
func (wb *PhoenixCharx) Identify() ([]string, error) {
b, err := wb.conn.ReadHoldingRegisters(wb.register(charxRegEvid), 10)
if err != nil {
return "", err
return nil, err
}
if res := bytesAsString(b); res != "" {
return res, nil
var ids []string
if evid := bytesAsString(b); evid != "" {
ids = append(ids, evid)
}
b, err = wb.conn.ReadHoldingRegisters(wb.register(charxRegRfid), 10)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(b), nil
if rfid := bytesAsString(b); rfid != "" {
ids = append(ids, rfid)
}
return ids, nil
}
var _ api.Diagnosis = (*PhoenixCharx)(nil)

View file

@ -240,13 +240,13 @@ func (wb *PhoenixEVEth) getPhaseValues(reg uint16) (float64, float64, float64, e
}
// identify implements the api.Identifier interface
func (wb *PhoenixEVEth) identify() (string, error) {
func (wb *PhoenixEVEth) identify() ([]string, error) {
b, err := wb.conn.ReadHoldingRegisters(phxRegCardUID, 16)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(b), nil
return []string{bytesAsString(b)}, nil
}
var _ api.Diagnosis = (*PhoenixEVEth)(nil)

View file

@ -381,13 +381,13 @@ func (wb *SmartEVSE3) statusReason() (api.Reason, error) {
}
// identify implements the api.Identifier interface
func (wb *SmartEVSE3) identify() (string, error) {
func (wb *SmartEVSE3) identify() ([]string, error) {
res, err := wb.apiG.Get()
if err != nil {
return "", err
return nil, err
}
return res.Evse.RFIDLastRead, nil
return []string{res.Evse.RFIDLastRead}, nil
}
var _ api.Diagnosis = (*SmartEVSE3)(nil)

View file

@ -319,12 +319,14 @@ func (wb *Vestel) getPhases() (int, error) {
}
// Identify implements the api.Identifier interface
func (wb *Vestel) identify() (string, error) {
func (wb *Vestel) identify() ([]string, error) {
b, err := wb.conn.ReadInputRegisters(vestelRegRFID, 15)
if err != nil {
return "", err
return nil, err
}
return utf16BEBytesAsString(b)
id, err := utf16BEBytesAsString(b)
return []string{id}, err
}
var _ api.Diagnosis = (*Vestel)(nil)

View file

@ -455,14 +455,20 @@ func (w *WarpWS) voltages() (float64, float64, float64, error) {
return w.meter.Voltages[0], w.meter.Voltages[1], w.meter.Voltages[2], nil
}
// identify prefers the vehicle mac read via ISO 15118 over the RFID tag
func (w *WarpWS) identify() (string, error) {
// identify reports the vehicle mac read via ISO 15118 before the RFID tag
func (w *WarpWS) identify() ([]string, error) {
w.mu.RLock()
defer w.mu.RUnlock()
var ids []string
if w.evState != nil && w.evState.Mac != "" {
return w.evState.Mac, nil
ids = append(ids, w.evState.Mac)
}
return w.chargeTracker.AuthorizationInfo.TagId, nil
if tag := w.chargeTracker.AuthorizationInfo.TagId; tag != "" {
ids = append(ids, tag)
}
return ids, nil
}
// soc implements the api.Battery interface

View file

@ -282,10 +282,10 @@ func (wb *Warp2) voltages() (float64, float64, float64, error) {
return res[0], res[1], res[2], nil
}
func (wb *Warp2) identify() (string, error) {
func (wb *Warp2) identify() ([]string, error) {
var res warp.ChargeTrackerCurrentCharge
err := wb.chargeG(&res)
return res.AuthorizationInfo.TagId, err
return []string{res.AuthorizationInfo.TagId}, err
}
func (wb *Warp2) emState() (warp.PmState, error) {

View file

@ -242,13 +242,13 @@ func (wb *WebastoNext) Currents() (float64, float64, float64, error) {
var _ api.Identifier = (*WebastoNext)(nil)
// Identify implements the api.Identifier interface
func (wb *WebastoNext) Identify() (string, error) {
func (wb *WebastoNext) Identify() ([]string, error) {
b, err := wb.conn.ReadHoldingRegisters(tqRegUserID, 10)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(b), nil
return []string{bytesAsString(b)}, nil
}
var _ api.Diagnosis = (*WebastoNext)(nil)

View file

@ -245,12 +245,12 @@ func (wb *Weidmüller) Voltages() (float64, float64, float64, error) {
var _ api.Identifier = (*Weidmüller)(nil)
// Identify implements the api.Identifier interface
func (wb *Weidmüller) Identify() (string, error) {
func (wb *Weidmüller) Identify() ([]string, error) {
b, err := wb.conn.ReadHoldingRegisters(wmRegCardId, 11)
if err != nil {
return "", err
return nil, err
}
return bytesAsString(b), nil
return []string{bytesAsString(b)}, nil
}
var _ api.PhaseSwitcher = (*Weidmüller)(nil)

View file

@ -444,17 +444,17 @@ func (c *Zaptec) switchPhases(phases int) error {
var _ api.Identifier = (*Zaptec)(nil)
// Identify implements the api.Identifier interface
func (c *Zaptec) Identify() (string, error) {
func (c *Zaptec) Identify() ([]string, error) {
res, err := c.statusG.Get()
if err != nil {
return "", err
return nil, err
}
if id := res.ObservationByID(zaptec.ChargerCurrentUserUuid); id != nil {
return id.ValueAsString, nil
return []string{id.ValueAsString}, nil
}
return "", nil
return nil, nil
}
func (c *Zaptec) installation() (zaptec.Installation, error) {

View file

@ -313,7 +313,8 @@ func (d *dumper) Dump(name string, v any) {
if v, ok := api.Cap[api.Identifier](v); ok {
d.measureTime(w, "Identifier", func() (string, error) {
id, err := v.Identify()
ids, err := v.Identify()
id := strings.Join(ids, ", ")
if err == nil && id == "" {
id = "<none>"
}

View file

@ -38,6 +38,10 @@ type typeStruct struct {
func getTypeImport(t reflect.Type) string {
n := t.Name()
if n == "" {
// unnamed type, e.g. []string
return t.String()
}
if p := t.PkgPath(); p != "" {
if s := strings.Split(p, "github.com/evcc-io/evcc/"); len(s) == 2 {
return fmt.Sprintf("%s.%s", s[1], n)

View file

@ -2,6 +2,7 @@ package core
import (
"errors"
"slices"
"time"
"github.com/evcc-io/evcc/api"
@ -48,8 +49,10 @@ func (lp *Loadpoint) createSession() {
}
if c, ok := api.Cap[api.Identifier](lp.charger); ok {
if id, err := c.Identify(); err == nil {
lp.session.Identifier = id
if ids, err := c.Identify(); err == nil {
if idx := slices.IndexFunc(ids, func(id string) bool { return id != "" }); idx >= 0 {
lp.session.Identifier = ids[idx]
}
}
}

View file

@ -3,6 +3,7 @@ package core
import (
"errors"
"regexp"
"slices"
"strings"
"time"
@ -59,63 +60,79 @@ func (lp *Loadpoint) identifyVehicle() {
return
}
id, err := identifier.Identify()
ids, err := identifier.Identify()
if err != nil {
lp.log.ERROR.Println("charger vehicle id:", err)
return
}
if lp.vehicleIdentifier == id {
ids = slices.DeleteFunc(ids, func(id string) bool { return id == "" })
// vehicle removed
if len(ids) == 0 {
lp.setVehicleIdentifier("")
return
}
// vehicle found or removed
// identity unchanged
if slices.Contains(ids, lp.vehicleIdentifier) {
return
}
lp.log.DEBUG.Println("charger vehicle id:", strings.Join(ids, ", "))
// the charger may report multiple identities, e.g. rfid tag and vehicle id
vehicle, id := lp.selectVehicleByID(ids...)
if id == "" {
id = ids[0]
}
lp.setVehicleIdentifier(id)
if id != "" {
lp.log.DEBUG.Println("charger vehicle id:", id)
if vehicle != nil {
lp.stopVehicleDetection()
if vehicle := lp.selectVehicleByID(id); vehicle != nil {
lp.stopVehicleDetection()
// already active via a different detection path - avoid reapplying its mode
if lp.GetVehicle() != vehicle {
lp.setActiveVehicle(vehicle)
}
// already active via a different detection path - avoid reapplying its mode
if lp.GetVehicle() != vehicle {
lp.setActiveVehicle(vehicle)
}
}
}
// selectVehicleByID selects the vehicle with the given ID
func (lp *Loadpoint) selectVehicleByID(id string) api.Vehicle {
// selectVehicleByID selects the vehicle matching any of the given IDs and returns the matching ID
func (lp *Loadpoint) selectVehicleByID(ids ...string) (api.Vehicle, string) {
vehicles := lp.coordinatedVehicles()
// find exact match
for _, vehicle := range vehicles {
for _, vid := range vehicle.Identifiers() {
if strings.EqualFold(id, vid) {
return vehicle
for _, id := range ids {
for _, vehicle := range vehicles {
if slices.ContainsFunc(vehicle.Identifiers(), func(vid string) bool {
return strings.EqualFold(id, vid)
}) {
return vehicle, id
}
}
}
// find placeholder match
for _, vehicle := range vehicles {
for _, vid := range vehicle.Identifiers() {
// case insensitive match
re, err := regexp.Compile("(?i)" + strings.ReplaceAll(vid, "*", ".*?"))
if err != nil {
lp.log.ERROR.Printf("vehicle id: %v", err)
continue
}
for _, id := range ids {
for _, vehicle := range vehicles {
if slices.ContainsFunc(vehicle.Identifiers(), func(vid string) bool {
// case insensitive match
re, err := regexp.Compile("(?i)" + strings.ReplaceAll(vid, "*", ".*?"))
if err != nil {
lp.log.ERROR.Printf("vehicle id: %v", err)
return false
}
if re.MatchString(id) {
return vehicle
return re.MatchString(id)
}) {
return vehicle, id
}
}
}
return nil
return nil, ""
}
// setActiveVehicle assigns currently active vehicle, configures soc estimator

View file

@ -201,42 +201,62 @@ func TestVehicleDetectByID(t *testing.T) {
type testcase struct {
string
id, i1, i2 string
res api.Vehicle
prepare func(testcase)
ids []string
i1, i2 string
res api.Vehicle
match string
prepare func(testcase)
}
tc := []testcase{
{"1/_/_->0", "1", "", "", nil, func(tc testcase) {
{"1/_/_->0", []string{"1"}, "", "", nil, "", func(tc testcase) {
v1.EXPECT().Identifiers().Return(nil)
v2.EXPECT().Identifiers().Return(nil)
v1.EXPECT().Identifiers().Return(nil)
v2.EXPECT().Identifiers().Return(nil)
}},
{"1/1/2->1", "1", "1", "2", v1, func(tc testcase) {
{"1/1/2->1", []string{"1"}, "1", "2", v1, "1", func(tc testcase) {
v1.EXPECT().Identifiers().Return([]string{tc.i1})
}},
{"2/1/2->2", "2", "1", "2", v2, func(tc testcase) {
{"2/1/2->2", []string{"2"}, "1", "2", v2, "2", func(tc testcase) {
v1.EXPECT().Identifiers().Return([]string{tc.i1})
v2.EXPECT().Identifiers().Return([]string{tc.i2})
}},
{"11/1*/2->1", "11", "1*", "2", v1, func(tc testcase) {
{"11/1*/2->1", []string{"11"}, "1*", "2", v1, "11", func(tc testcase) {
v1.EXPECT().Identifiers().Return([]string{tc.i1})
v2.EXPECT().Identifiers().Return([]string{tc.i2})
v1.EXPECT().Identifiers().Return([]string{tc.i1})
// v2.EXPECT().Identifiers().Return([]string{tc.i2})
}},
{"22/1*/2*->2", "22", "1*", "2*", v2, func(tc testcase) {
{"22/1*/2*->2", []string{"22"}, "1*", "2*", v2, "22", func(tc testcase) {
v1.EXPECT().Identifiers().Return([]string{tc.i1})
v2.EXPECT().Identifiers().Return([]string{tc.i2})
v1.EXPECT().Identifiers().Return([]string{tc.i1})
v2.EXPECT().Identifiers().Return([]string{tc.i2})
}},
{"2/_/*->2", "2", "", "*", v2, func(tc testcase) {
{"2/_/*->2", []string{"2"}, "", "*", v2, "2", func(tc testcase) {
v1.EXPECT().Identifiers().Return(nil)
v2.EXPECT().Identifiers().Return([]string{tc.i2})
v1.EXPECT().Identifiers().Return(nil)
v2.EXPECT().Identifiers().Return([]string{tc.i2})
}},
// second identity matches, e.g. rfid tag and vehicle id
{"x,2/1/2->2", []string{"x", "2"}, "1", "2", v2, "2", func(tc testcase) {
v1.EXPECT().Identifiers().Return([]string{tc.i1})
v2.EXPECT().Identifiers().Return([]string{tc.i2})
v1.EXPECT().Identifiers().Return([]string{tc.i1})
v2.EXPECT().Identifiers().Return([]string{tc.i2})
}},
// wildcard matches the second identity
{"x,22/1/2*->2", []string{"x", "22"}, "1", "2*", v2, "22", func(tc testcase) {
v1.EXPECT().Identifiers().Return([]string{tc.i1})
v2.EXPECT().Identifiers().Return([]string{tc.i2})
v1.EXPECT().Identifiers().Return([]string{tc.i1})
v2.EXPECT().Identifiers().Return([]string{tc.i2})
v1.EXPECT().Identifiers().Return([]string{tc.i1})
v2.EXPECT().Identifiers().Return([]string{tc.i2})
v1.EXPECT().Identifiers().Return([]string{tc.i1})
v2.EXPECT().Identifiers().Return([]string{tc.i2})
}},
}
for _, tc := range tc {
@ -252,9 +272,13 @@ func TestVehicleDetectByID(t *testing.T) {
tc.prepare(tc)
}
if res := lp.selectVehicleByID(tc.id); tc.res != res {
res, match := lp.selectVehicleByID(tc.ids...)
if tc.res != res {
t.Errorf("expected %v, got %v", tc.res, res)
}
if tc.match != match {
t.Errorf("expected match %q, got %q", tc.match, match)
}
}
}
@ -332,7 +356,7 @@ func (c *idCharger) Status() (api.ChargeStatus, error) { return api.StatusB, nil
func (c *idCharger) Enabled() (bool, error) { return false, nil }
func (c *idCharger) Enable(bool) error { return nil }
func (c *idCharger) MaxCurrent(int64) error { return nil }
func (c *idCharger) Identify() (string, error) { return c.id, nil }
func (c *idCharger) Identify() ([]string, error) { return []string{c.id}, nil }
// TestReidentifyActiveVehicleKeepsMode is a regression test for #31499:
// re-identifying the already-active vehicle must not reapply its default mode.

View file

@ -470,7 +470,7 @@ func testInstance(ctx context.Context, instance any) map[string]testResult {
wg.Go(func() {
if dev, ok := api.Cap[api.Identifier](instance); ok {
val, err := dev.Identify()
makeResult("identifier", val, err)
makeResult("identifier", strings.Join(val, ", "), err)
}
})