Vehicle (EU Data Act): prefer most recently delivered value per field (#30916)

This commit is contained in:
andig 2026-06-16 11:03:36 +02:00 • committed by GitHub
parent a70f4b35f1
commit 5304d98a8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 52 additions and 13 deletions

View file

@ -99,7 +99,7 @@ func TestMerge(t *testing.T) {
FieldRangeSecondary: {Value: "200", Timestamp: t1}, // new field -> added
}
merge(dst, src)
merge(dst, src, 1)
assert.Equal(t, "80", dst[FieldSoc].Value, "newer dataset wins, even with an older timestampUtc")
assert.Equal(t, "100", dst[FieldOdometer].Value, "field absent from src is retained")
@ -117,18 +117,48 @@ func TestMergeDeliveryOrder(t *testing.T) {
data := map[string]point{}
// datasets in delivery order; capture timestamps go backwards as SoC rises
for _, d := range []struct{ value, capture string }{
for i, d := range []struct{ value, capture string }{
{"60", "2026-06-13 14:03:37"}, // delivered 14:10
{"65", "2026-06-13 12:44:31"}, // delivered 14:55, older capture
{"66", "2026-06-13 13:03:16"}, // delivered 15:11
{"75", "2026-06-13 13:52:11"}, // delivered 16:54
} {
merge(data, map[string]point{FieldSoc: {Value: d.value, Timestamp: parse(d.capture)}})
merge(data, map[string]point{FieldSoc: {Value: d.value, Timestamp: parse(d.capture)}}, uint64(i+1))
}
assert.Equal(t, "75", data[FieldSoc].Value, "the newest delivered SoC wins")
}
// TestSocFreshestField reproduces issue #30877: a higher-priority SoC field that
// stops being delivered must not shadow a lower-priority field that keeps rising.
func TestSocFreshestField(t *testing.T) {
data := map[string]point{}
var seq uint64
deliver := func(fields map[string]point) {
seq++
merge(data, fields, seq)
}
// first datasets carry both SoC fields at 57, the high-priority field winning
deliver(map[string]point{
FieldBatteryStateReportSoc: {Value: "57"},
FieldHvBatteryLevel: {Value: "57.0"},
})
deliver(map[string]point{
FieldBatteryStateReportSoc: {Value: "57"},
FieldHvBatteryLevel: {Value: "57.0"},
})
// later datasets only refresh the fallback field as the car charges
for _, v := range []string{"58.0", "59.0", "61.0"} {
deliver(map[string]point{FieldHvBatteryLevel: {Value: v}})
}
soc, err := testProvider(data).Soc()
require.NoError(t, err)
assert.Equal(t, 61.0, soc, "the still-updating fallback wins over the stale high-priority field")
}
// TestPoints guards that a data point with a generic field name ("value") is
// indexed by its unique key while the name stays indexed (and thus logged).
func TestPoints(t *testing.T) {

View file

@ -60,14 +60,16 @@ func resetDelay(ts time.Time, cache time.Duration) time.Duration {
return portalLatency
}
// lookup returns the first present, non-empty value among the given field names
// lookup returns the freshest present value among the given field names (most to
// least authoritative); the highest Seq wins, equal Seq keeps the priority order.
func lookup(data map[string]point, fields ...string) *point {
var best *point
for _, f := range fields {
if v, ok := data[f]; ok {
return new(v)
if v, ok := data[f]; ok && (best == nil || v.Seq > best.Seq) {
best = new(v)
}
}
return nil
return best
}
var _ api.Battery = (*Provider)(nil)

View file

@ -29,6 +29,7 @@ type vehicleState struct {
identifier string
data map[string]point
after time.Time
seq uint64 // delivery counter, incremented per merged dataset
}
var (
@ -127,7 +128,8 @@ func (s *store) update(log *log.Logger, vin string) (time.Time, error) {
v.after = d.CreatedOn
}
merge(v.data, data)
v.seq++
merge(v.data, data, v.seq)
if !initial {
logData(s.api.log, data)
@ -187,8 +189,11 @@ func pending(content []dataset, after time.Time) []dataset {
return res
}
// merge lets src (the newer dataset) win per field. The portal's per-field
// timestampUtc is unreliable, so delivery order decides, not the timestamp.
func merge(dst, src map[string]point) {
maps.Copy(dst, src)
// merge lets src (the newer dataset) win per field and stamps each field with
// seq, the dataset's delivery sequence (timestampUtc is unreliable).
func merge(dst, src map[string]point, seq uint64) {
for k, p := range src {
p.Seq = seq
dst[k] = p
}
}

View file

@ -90,10 +90,12 @@ type dataPoint struct {
TimestampUtc *time.Time `json:"timestampUtc"`
}
// point is a decoded data point: its value and the time it was recorded
// point is a decoded data point: its value, the time it was recorded and the
// delivery sequence of the dataset it last arrived in (higher Seq is newer).
type point struct {
Value string
Timestamp time.Time
Seq uint64
}
// datasetFile is the JSON document contained in a dataset zip archive