From c837c1e281681e0780a282c2dcbfe11a7a0cc605 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 21 Jun 2026 13:54:35 +0200 Subject: [PATCH] EU Data Act: prefer valid battery_level_HV.value for SoC (#31092) --- vehicle/vw/eudataact/eudataact_test.go | 28 +++++++++++++++++++++++--- vehicle/vw/eudataact/provider.go | 9 ++++++++- vehicle/vw/eudataact/types.go | 7 ++++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/vehicle/vw/eudataact/eudataact_test.go b/vehicle/vw/eudataact/eudataact_test.go index 30fa69ec3..316ca2360 100644 --- a/vehicle/vw/eudataact/eudataact_test.go +++ b/vehicle/vw/eudataact/eudataact_test.go @@ -142,16 +142,16 @@ func TestSocFreshestField(t *testing.T) { // first datasets carry both SoC fields at 57, the high-priority field winning deliver(map[string]point{ FieldBatteryStateReportSoc: {Value: "57"}, - FieldHvBatteryLevel: {Value: "57.0"}, + FieldHvBatteryLevelValue: {Value: "57.0"}, }) deliver(map[string]point{ FieldBatteryStateReportSoc: {Value: "57"}, - FieldHvBatteryLevel: {Value: "57.0"}, + FieldHvBatteryLevelValue: {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}}) + deliver(map[string]point{FieldHvBatteryLevelValue: {Value: v}}) } soc, err := testProvider(data).Soc() @@ -159,6 +159,28 @@ func TestSocFreshestField(t *testing.T) { assert.Equal(t, 61.0, soc, "the still-updating fallback wins over the stale high-priority field") } +// TestSocHvBatteryLevelValid reproduces #31084: battery_level_HV.value (74) is +// preferred over ambiguous battery_state_report.soc (45) only when state is VALID. +func TestSocHvBatteryLevelValid(t *testing.T) { + raw := []dataPoint{ + {DataFieldName: FieldBatteryStateReportSoc, Value: "74"}, + {DataFieldName: FieldBatteryStateReportSoc, Value: "45"}, + {DataFieldName: FieldBatteryStateReportSoc, Value: "45"}, + {DataFieldName: FieldHvBatteryLevelValue, Value: "74.0"}, + } + + // without a VALID flag the ambiguous battery_state_report.soc still wins (45) + soc, err := testProvider(points(raw)).Soc() + require.NoError(t, err) + assert.Equal(t, 45.0, soc, "prior behaviour kept when HV level is not flagged valid") + + // with battery_level_HV.state VALID, battery_level_HV.value (74) is preferred + valid := append(raw, dataPoint{DataFieldName: FieldHvBatteryLevelState, Value: hvBatteryLevelValid}) + soc, err = testProvider(points(valid)).Soc() + require.NoError(t, err) + assert.Equal(t, 74.0, soc, "battery_level_HV.value wins when flagged VALID") +} + // 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) { diff --git a/vehicle/vw/eudataact/provider.go b/vehicle/vw/eudataact/provider.go index 591a2b052..7707500ad 100644 --- a/vehicle/vw/eudataact/provider.go +++ b/vehicle/vw/eudataact/provider.go @@ -81,7 +81,14 @@ func (v *Provider) Soc() (float64, error) { return 0, err } - if p := lookup(data, FieldBatteryStateReportSoc, FieldSoc, FieldHvSoc, FieldHvBatteryLevel); p != nil { + // use battery_level_HV.value when its state reports valid + if s, ok := data[FieldHvBatteryLevelState]; ok && s.Value == hvBatteryLevelValid { + if p, ok := data[FieldHvBatteryLevelValue]; ok { + return strconv.ParseFloat(p.Value, 64) + } + } + + if p := lookup(data, FieldBatteryStateReportSoc, FieldSoc, FieldHvSoc, FieldHvBatteryLevelValue); p != nil { return strconv.ParseFloat(p.Value, 64) } diff --git a/vehicle/vw/eudataact/types.go b/vehicle/vw/eudataact/types.go index 3c87158ab..e9413c265 100644 --- a/vehicle/vw/eudataact/types.go +++ b/vehicle/vw/eudataact/types.go @@ -110,7 +110,8 @@ const ( FieldBatteryStateReportSoc = "battery_state_report.soc" FieldSoc = "state_of_charge" FieldHvSoc = "hv_soc" - FieldHvBatteryLevel = "battery_level_HV.value" + FieldHvBatteryLevelValue = "battery_level_HV.value" + FieldHvBatteryLevelState = "battery_level_HV.state" // target soc FieldTargetSoc = "settings.target_soc" @@ -129,6 +130,10 @@ const ( FieldRemainingTime = "remaining_charging_time" ) +// hvBatteryLevelValid is the battery_level_HV.state value that marks +// battery_level_HV.value as a trustworthy SoC reading +const hvBatteryLevelValid = "VALID" + // 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{}{