Vehicle (EU Data Act): prefer most recently delivered value per field (#30916)
This commit is contained in:
parent
a70f4b35f1
commit
5304d98a8c
4 changed files with 52 additions and 13 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue