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

@ -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.