From bcd75510d0ee9ddb7e93ae9b4d5d8ac6c1ed186f Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 15 Jun 2026 07:08:52 +0200 Subject: [PATCH] EU Data Act: identify ID.3 cruising range by data point key (#30851) --- vehicle/vw/eudataact/eudataact_test.go | 13 ++++++++ vehicle/vw/eudataact/provider.go | 2 +- vehicle/vw/eudataact/types.go | 44 ++++++++++++++++++++------ 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/vehicle/vw/eudataact/eudataact_test.go b/vehicle/vw/eudataact/eudataact_test.go index b5cd1d7da..7eb356d20 100644 --- a/vehicle/vw/eudataact/eudataact_test.go +++ b/vehicle/vw/eudataact/eudataact_test.go @@ -128,3 +128,16 @@ func TestMergeDeliveryOrder(t *testing.T) { assert.Equal(t, "75", data[FieldSoc].Value, "the newest delivered SoC wins") } + +// 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) { + data := points([]dataPoint{ + {Key: KeyRangeID3, DataFieldName: "value", Value: "317"}, + {DataFieldName: FieldOdometer, Value: "22164"}, + }) + + assert.Equal(t, "317", data[KeyRangeID3].Value, "ID.3 range indexed by key") + assert.Equal(t, "317", data["value"].Value, "field name remains indexed") + assert.Equal(t, "22164", data[FieldOdometer].Value, "named field indexed by name") +} diff --git a/vehicle/vw/eudataact/provider.go b/vehicle/vw/eudataact/provider.go index eacf954f4..c6fd2b0bc 100644 --- a/vehicle/vw/eudataact/provider.go +++ b/vehicle/vw/eudataact/provider.go @@ -95,7 +95,7 @@ func (v *Provider) Range() (int64, error) { return 0, err } - if p := lookup(data, FieldRangeSecondary, FieldRangePrimary, FieldRangeCombined); p != nil { + if p := lookup(data, FieldRangeSecondary, FieldRangePrimary, FieldRangeCombined, KeyRangeID3); p != nil { f, err := strconv.ParseFloat(p.Value, 64) return int64(f), err } diff --git a/vehicle/vw/eudataact/types.go b/vehicle/vw/eudataact/types.go index ca25b82bf..bbc9e2aca 100644 --- a/vehicle/vw/eudataact/types.go +++ b/vehicle/vw/eudataact/types.go @@ -81,8 +81,10 @@ type dataset struct { CreatedOn time.Time `json:"createdOn"` } -// dataPoint is a single data point as delivered in the dataset JSON document +// dataPoint is a single data point as delivered in the dataset JSON document. +// Key is the data point's unique GUID, used when DataFieldName is generic (e.g. "value"). type dataPoint struct { + Key string `json:"key"` DataFieldName string `json:"dataFieldName"` Value string `json:"value"` TimestampUtc *time.Time `json:"timestampUtc"` @@ -121,6 +123,7 @@ const ( FieldRangeCombined = "cruising_range_combined" FieldRangePrimary = "cruising_range_primary_engine" FieldRangeSecondary = "cruising_range_secondary_engine" + KeyRangeID3 = "0ca40e18-0564-3eda-bcc0-7aee9ef44f04" // VW ID.3 cruising range, delivered as "value" // odo FieldOdometer = "mileage" @@ -130,6 +133,12 @@ const ( FieldRemainingTime = "remaining_charging_time" ) +// knownKeys lists data point GUIDs that are indexed by their key instead of the +// generic, non-unique DataFieldName they are delivered with +var knownKeys = map[string]struct{}{ + KeyRangeID3: {}, +} + // contentDatasets returns the datasets that actually carry content, with their // delivery time parsed into Timestamp and sorted from oldest to newest. The // portal emits "..._no_content_found.zip" placeholders while the vehicle is @@ -192,9 +201,26 @@ func parseDataset(log *log.Logger, b []byte) (map[string]point, error) { return nil, err } - res := make(map[string]point, len(ds.Data)) - for _, p := range ds.Data { - if p.DataFieldName == "" || p.Value == "" { + return points(ds.Data), nil +} + +// points indexes data points by field name (newest timestamp wins), and known +// data points additionally by their unique key, as their name is not unique. +func points(data []dataPoint) map[string]point { + res := make(map[string]point, len(data)) + + set := func(name string, p point) { + if name == "" { + return + } + if cur, ok := res[name]; ok && cur.Timestamp.After(p.Timestamp) { + return + } + res[name] = p + } + + for _, p := range data { + if p.Value == "" { continue } @@ -202,13 +228,13 @@ func parseDataset(log *log.Logger, b []byte) (map[string]point, error) { if p.TimestampUtc != nil { ts = *p.TimestampUtc } + pt := point{Value: p.Value, Timestamp: ts} - if cur, ok := res[p.DataFieldName]; ok && cur.Timestamp.After(ts) { - continue + set(p.DataFieldName, pt) + if _, ok := knownKeys[p.Key]; ok { + set(p.Key, pt) } - - res[p.DataFieldName] = point{Value: p.Value, Timestamp: ts} } - return res, nil + return res }