diff --git a/api/api.go b/api/api.go index 3535b9dc7..c5c735462 100644 --- a/api/api.go +++ b/api/api.go @@ -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 diff --git a/api/implement/implementations.go b/api/implement/implementations.go index 7c3ce1da1..882ecb1ac 100644 --- a/api/implement/implementations.go +++ b/api/implement/implementations.go @@ -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() } diff --git a/api/mock.go b/api/mock.go index 22470c2e3..dd59b7f5f 100644 --- a/api/mock.go +++ b/api/mock.go @@ -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 } diff --git a/charger/alpitronic.go b/charger/alpitronic.go index e55a64a86..0fbff62e7 100644 --- a/charger/alpitronic.go +++ b/charger/alpitronic.go @@ -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) diff --git a/charger/alpitronic_test.go b/charger/alpitronic_test.go index 3f1ce0574..911b660c8 100644 --- a/charger/alpitronic_test.go +++ b/charger/alpitronic_test.go @@ -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) { diff --git a/charger/amperfied.go b/charger/amperfied.go index ba77a23e4..ce339818e 100644 --- a/charger/amperfied.go +++ b/charger/amperfied.go @@ -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) diff --git a/charger/bender.go b/charger/bender.go index 285ba1353..f28554e12 100644 --- a/charger/bender.go +++ b/charger/bender.go @@ -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 diff --git a/charger/cfos.go b/charger/cfos.go index 06794d2f2..26018ff47 100644 --- a/charger/cfos.go +++ b/charger/cfos.go @@ -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 } diff --git a/charger/charger.go b/charger/charger.go index dbaaec800..41a700254 100644 --- a/charger/charger.go +++ b/charger/charger.go @@ -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 { diff --git a/charger/compleo.go b/charger/compleo.go index 5fb2067a8..595263e39 100644 --- a/charger/compleo.go +++ b/charger/compleo.go @@ -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) diff --git a/charger/dadapower.go b/charger/dadapower.go index b6ec31200..101d54821 100644 --- a/charger/dadapower.go +++ b/charger/dadapower.go @@ -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) diff --git a/charger/daheimladen.go b/charger/daheimladen.go index f48e92146..9c5cccdb6 100644 --- a/charger/daheimladen.go +++ b/charger/daheimladen.go @@ -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 diff --git a/charger/delta.go b/charger/delta.go index d266f3afa..776d01a37 100644 --- a/charger/delta.go +++ b/charger/delta.go @@ -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) diff --git a/charger/e3dc.go b/charger/e3dc.go index c51bc8996..42314c91d 100644 --- a/charger/e3dc.go +++ b/charger/e3dc.go @@ -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) diff --git a/charger/easee.go b/charger/easee.go index 49cb282f5..f607df372 100644 --- a/charger/easee.go +++ b/charger/easee.go @@ -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) diff --git a/charger/eebus-evse.go b/charger/eebus-evse.go index 43f608aa3..952365f7e 100644 --- a/charger/eebus-evse.go +++ b/charger/eebus-evse.go @@ -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) diff --git a/charger/enovates.go b/charger/enovates.go index faabe683c..9fd487ecc 100644 --- a/charger/enovates.go +++ b/charger/enovates.go @@ -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) diff --git a/charger/evecube.go b/charger/evecube.go index 06582c1f9..6292cff1e 100644 --- a/charger/evecube.go +++ b/charger/evecube.go @@ -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 } diff --git a/charger/evsewifi.go b/charger/evsewifi.go index 498385146..2861616e6 100644 --- a/charger/evsewifi.go +++ b/charger/evsewifi.go @@ -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) diff --git a/charger/foxess-evc.go b/charger/foxess-evc.go index cc34b1208..fcfbe0d23 100644 --- a/charger/foxess-evc.go +++ b/charger/foxess-evc.go @@ -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 diff --git a/charger/fronius-wattpilot.go b/charger/fronius-wattpilot.go index 181b42315..8830f67d6 100644 --- a/charger/fronius-wattpilot.go +++ b/charger/fronius-wattpilot.go @@ -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) diff --git a/charger/ghosteebus.go b/charger/ghosteebus.go index 4fa9fe6d8..b8a959570 100644 --- a/charger/ghosteebus.go +++ b/charger/ghosteebus.go @@ -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 } diff --git a/charger/ghosteebus_test.go b/charger/ghosteebus_test.go index 3f5f7f4e2..fc9f2ded5 100644 --- a/charger/ghosteebus_test.go +++ b/charger/ghosteebus_test.go @@ -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) }) } diff --git a/charger/go-e.go b/charger/go-e.go index 9597e2b36..9564ed87c 100644 --- a/charger/go-e.go +++ b/charger/go-e.go @@ -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) diff --git a/charger/goodwe.go b/charger/goodwe.go index 919685649..9c3851dc5 100644 --- a/charger/goodwe.go +++ b/charger/goodwe.go @@ -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) diff --git a/charger/hardybarth-ecb1.go b/charger/hardybarth-ecb1.go index 07db7d7a5..20482e41c 100644 --- a/charger/hardybarth-ecb1.go +++ b/charger/hardybarth-ecb1.go @@ -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 // } diff --git a/charger/hardybarth-salia.go b/charger/hardybarth-salia.go index f5d5f66a0..d2a8dcedd 100644 --- a/charger/hardybarth-salia.go +++ b/charger/hardybarth-salia.go @@ -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) { diff --git a/charger/kathrein.go b/charger/kathrein.go index b3f951dad..eda7df925 100644 --- a/charger/kathrein.go +++ b/charger/kathrein.go @@ -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) diff --git a/charger/keba-modbus.go b/charger/keba-modbus.go index 4e3e9ce50..f62de558c 100644 --- a/charger/keba-modbus.go +++ b/charger/keba-modbus.go @@ -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 diff --git a/charger/keba-udp.go b/charger/keba-udp.go index 9e20b0cef..7ee768ec7 100644 --- a/charger/keba-udp.go +++ b/charger/keba-udp.go @@ -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) diff --git a/charger/kse.go b/charger/kse.go index d862aba60..1838803f4 100644 --- a/charger/kse.go +++ b/charger/kse.go @@ -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) diff --git a/charger/ocpp.go b/charger/ocpp.go index 597b13c06..a55c18b1a 100644 --- a/charger/ocpp.go +++ b/charger/ocpp.go @@ -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) diff --git a/charger/ocpp_test.go b/charger/ocpp_test.go index 5588a5bff..d764471f8 100644 --- a/charger/ocpp_test.go +++ b/charger/ocpp_test.go @@ -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() diff --git a/charger/openwb-2.0.go b/charger/openwb-2.0.go index dec6ccdcc..a76e1634d 100644 --- a/charger/openwb-2.0.go +++ b/charger/openwb-2.0.go @@ -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 } diff --git a/charger/openwb-native_linux.go b/charger/openwb-native_linux.go index f27f1f5e7..8c4c9931d 100644 --- a/charger/openwb-native_linux.go +++ b/charger/openwb-native_linux.go @@ -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 } diff --git a/charger/openwb-pro.go b/charger/openwb-pro.go index 5844064e0..c4c98d6b9 100644 --- a/charger/openwb-pro.go +++ b/charger/openwb-pro.go @@ -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 { diff --git a/charger/phoenix-charx.go b/charger/phoenix-charx.go index a661459e3..2a31d3dd6 100644 --- a/charger/phoenix-charx.go +++ b/charger/phoenix-charx.go @@ -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) diff --git a/charger/phoenix-ev-eth.go b/charger/phoenix-ev-eth.go index cdc09a9b7..50d48c877 100644 --- a/charger/phoenix-ev-eth.go +++ b/charger/phoenix-ev-eth.go @@ -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) diff --git a/charger/smart-evse.go b/charger/smart-evse.go index 3c97d6252..c684869bc 100644 --- a/charger/smart-evse.go +++ b/charger/smart-evse.go @@ -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) diff --git a/charger/vestel.go b/charger/vestel.go index 58c07e8f3..f05e6f5d5 100644 --- a/charger/vestel.go +++ b/charger/vestel.go @@ -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) diff --git a/charger/warp-ws.go b/charger/warp-ws.go index cfcec5bfe..d74e3a396 100644 --- a/charger/warp-ws.go +++ b/charger/warp-ws.go @@ -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 diff --git a/charger/warp2-mqtt.go b/charger/warp2-mqtt.go index 8ed3890a5..e2c9d5957 100644 --- a/charger/warp2-mqtt.go +++ b/charger/warp2-mqtt.go @@ -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) { diff --git a/charger/webasto-next.go b/charger/webasto-next.go index 3e151bf3c..66dbcf4b9 100644 --- a/charger/webasto-next.go +++ b/charger/webasto-next.go @@ -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) diff --git a/charger/weidmüller.go b/charger/weidmüller.go index d9053b030..e94c44a13 100644 --- a/charger/weidmüller.go +++ b/charger/weidmüller.go @@ -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) diff --git a/charger/zaptec.go b/charger/zaptec.go index 43fad7506..423bb0c22 100644 --- a/charger/zaptec.go +++ b/charger/zaptec.go @@ -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) { diff --git a/cmd/dumper.go b/cmd/dumper.go index 21e59036d..248cc97ea 100644 --- a/cmd/dumper.go +++ b/cmd/dumper.go @@ -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 = "" } diff --git a/cmd/implement/implement.go b/cmd/implement/implement.go index 2659f5108..1f5319450 100644 --- a/cmd/implement/implement.go +++ b/cmd/implement/implement.go @@ -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) diff --git a/core/loadpoint_session.go b/core/loadpoint_session.go index d566d9b27..9e0857c2e 100644 --- a/core/loadpoint_session.go +++ b/core/loadpoint_session.go @@ -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] + } } } diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go index 1860df617..c156682cf 100644 --- a/core/loadpoint_vehicle.go +++ b/core/loadpoint_vehicle.go @@ -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 diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go index 028e447c9..7db0ed926 100644 --- a/core/loadpoint_vehicle_test.go +++ b/core/loadpoint_vehicle_test.go @@ -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. diff --git a/server/http_config_helper.go b/server/http_config_helper.go index 9acc17a1d..90c40cd6e 100644 --- a/server/http_config_helper.go +++ b/server/http_config_helper.go @@ -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) } })