Cardata: support "New Class" vehicles (#29709)

This commit is contained in:
mfuchs1984 2026-06-07 11:42:27 +02:00 • committed by GitHub
parent 4dcff007d7
commit 2f7126b005
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 146 additions and 13 deletions

View file

@ -9,7 +9,7 @@ products:
requirements:
description:
de: |
Benötigt CarData Einrichtung im BMW/Mini portal. Die folgenden Datenpunkte müssen für Streaming konfiguriert werden:
Benötigt CarData Einrichtung im BMW/Mini portal. Die folgenden Datenpunkte müssen für Streaming konfiguriert werden (die Verfügbarkeit der Datenpunkte kann je nach Fahrzeugmodell variieren):
```
vehicle.body.chargingPort.status
@ -21,7 +21,9 @@ requirements:
vehicle.drivetrain.electricEngine.charging.status
vehicle.drivetrain.electricEngine.charging.timeRemaining
vehicle.drivetrain.electricEngine.kombiRemainingElectricRange
vehicle.drivetrain.lastRemainingRange
vehicle.powertrain.electric.battery.stateOfCharge.target
vehicle.powertrain.electric.battery.stateOfCharge.displayed
vehicle.vehicle.preConditioning.activity
vehicle.vehicle.travelledDistance
```
@ -29,7 +31,7 @@ requirements:
Aktualisierung der Daten erfolgt einmalig bei Neustart und wenn Streamingdaten eingehen. Dies ist ausschließlich der Fall,
wenn das Fahrzeug aktiv Daten erzeugt.
en: |
Requires CarData activation in BMW/Mini portal. The following data points need to be configured for streaming access:
Requires CarData activation in BMW/Mini portal. The following data points need to be configured for streaming access (the availability of data points may vary depending on the vehicle model):
```
vehicle.body.chargingPort.status
@ -41,7 +43,9 @@ requirements:
vehicle.drivetrain.electricEngine.charging.status
vehicle.drivetrain.electricEngine.charging.timeRemaining
vehicle.drivetrain.electricEngine.kombiRemainingElectricRange
vehicle.drivetrain.lastRemainingRange
vehicle.powertrain.electric.battery.stateOfCharge.target
vehicle.powertrain.electric.battery.stateOfCharge.displayed
vehicle.vehicle.preConditioning.activity
vehicle.vehicle.travelledDistance
```

View file

@ -25,6 +25,8 @@ var requiredKeys = []string{
"vehicle.drivetrain.electricEngine.charging.status",
"vehicle.drivetrain.electricEngine.charging.timeRemaining",
"vehicle.drivetrain.electricEngine.kombiRemainingElectricRange",
"vehicle.drivetrain.lastRemainingRange",
"vehicle.powertrain.electric.battery.stateOfCharge.displayed",
"vehicle.powertrain.electric.battery.stateOfCharge.target",
"vehicle.vehicle.preConditioning.activity",
"vehicle.vehicle.travelledDistance",

View file

@ -141,10 +141,14 @@ func (v *Provider) any(key string) (any, error) {
return nil, api.ErrNotAvailable
}
func isNilOrEmtpy(val any) bool {
return val == nil || val == ""
}
func (v *Provider) String(key string) (string, error) {
res, err := v.any(key)
if err != nil {
return "", err
if err != nil || isNilOrEmtpy(res) {
return "", api.ErrNotAvailable
}
return cast.ToStringE(res)
@ -152,8 +156,8 @@ func (v *Provider) String(key string) (string, error) {
func (v *Provider) Int(key string) (int64, error) {
res, err := v.any(key)
if err != nil {
return 0, err
if err != nil || isNilOrEmtpy(res) {
return 0, api.ErrNotAvailable
}
return cast.ToInt64E(res)
@ -161,8 +165,8 @@ func (v *Provider) Int(key string) (int64, error) {
func (v *Provider) Float(key string) (float64, error) {
res, err := v.any(key)
if err != nil {
return 0, err
if err != nil || isNilOrEmtpy(res) {
return 0, api.ErrNotAvailable
}
return cast.ToFloat64E(res)
@ -172,7 +176,10 @@ var _ api.Battery = (*Provider)(nil)
// Soc implements the api.Vehicle interface
func (v *Provider) Soc() (float64, error) {
return v.Float("vehicle.drivetrain.batteryManagement.header")
if res, err := v.Float("vehicle.drivetrain.batteryManagement.header"); err == nil {
return res, nil
}
return v.Float("vehicle.powertrain.electric.battery.stateOfCharge.displayed")
}
var _ api.ChargeState = (*Provider)(nil)
@ -193,7 +200,7 @@ func (v *Provider) Status() (api.ChargeStatus, error) {
// mqtt, while hvStatus might only be available through rest
// (https://github.com/evcc-io/evcc/pull/26235)
cs, err := v.String("vehicle.drivetrain.electricEngine.charging.status")
if err != nil || cs == "" {
if err != nil {
cs, err = v.String("vehicle.drivetrain.electricEngine.charging.hvStatus")
}
@ -212,14 +219,20 @@ var _ api.VehicleFinishTimer = (*Provider)(nil)
// FinishTime implements the api.VehicleFinishTimer interface
func (v *Provider) FinishTime() (time.Time, error) {
res, err := v.Int("vehicle.drivetrain.electricEngine.charging.timeRemaining")
return time.Now().Add(time.Duration(res) * time.Minute), err
if err != nil {
return time.Time{}, err
}
return time.Now().Add(time.Duration(res) * time.Minute), nil
}
var _ api.VehicleRange = (*Provider)(nil)
// Range implements the api.VehicleRange interface
func (v *Provider) Range() (int64, error) {
return v.Int("vehicle.drivetrain.electricEngine.kombiRemainingElectricRange")
if res, err := v.Int("vehicle.drivetrain.electricEngine.kombiRemainingElectricRange"); err == nil {
return res, nil
}
return v.Int("vehicle.drivetrain.lastRemainingRange")
}
var _ api.VehicleOdometer = (*Provider)(nil)
@ -243,7 +256,7 @@ func (v *Provider) Climater() (bool, error) {
activeStates := []string{"HEATING", "COOLING", "VENTILATION", "DEFROST"}
res, err := v.String("vehicle.cabin.hvac.preconditioning.status.comfortState")
if err == nil && res != "" {
if err == nil {
return slices.Contains(activeStates, strings.TrimPrefix(strings.ToUpper(res), "COMFORT_")), nil
}

View file

@ -47,3 +47,117 @@ func TestCardataStreaming(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 47.0, soc)
}
func TestSocFallback(t *testing.T) {
ctx := t.Context()
p := NewProvider(ctx, util.NewLogger("foo"), nil, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: "at",
}), "client", "vin", 0)
// prevent container panic
p.updated = time.Now()
keySocOld := "vehicle.drivetrain.batteryManagement.header"
keySocNew := "vehicle.powertrain.electric.battery.stateOfCharge.displayed"
// Case 1: Old key is missing, new key is present
p.rest = map[string]TelematicData{
keySocNew: {Value: "80"},
}
soc, err := p.Soc()
require.NoError(t, err)
require.Equal(t, 80.0, soc)
// Case 2: Old key is empty (null in JSON), new key is present
p.rest = map[string]TelematicData{
keySocOld: {Value: ""},
keySocNew: {Value: "90"},
}
soc, err = p.Soc()
require.NoError(t, err)
require.Equal(t, 90.0, soc)
// Case 3: Old key is nil in streaming, new key is present
p.rest = nil
p.streaming = map[string]StreamingData{
keySocOld: {Value: nil},
keySocNew: {Value: 95.0},
}
soc, err = p.Soc()
require.NoError(t, err)
require.Equal(t, 95.0, soc)
// Case 4: Old key is present, new key is also present, old key is preferred
p.rest = map[string]TelematicData{
keySocOld: {Value: "42"},
keySocNew: {Value: "90"},
}
p.streaming = nil
soc, err = p.Soc()
require.NoError(t, err)
require.Equal(t, 42.0, soc)
// Case 5: Both keys are absent, returns error
p.rest = map[string]TelematicData{}
soc, err = p.Soc()
require.Error(t, err)
require.Equal(t, 0.0, soc)
}
func TestRangeFallback(t *testing.T) {
ctx := t.Context()
p := NewProvider(ctx, util.NewLogger("foo"), nil, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: "at",
}), "client", "vin", 0)
// prevent container panic
p.updated = time.Now()
keyRangeOld := "vehicle.drivetrain.electricEngine.kombiRemainingElectricRange"
keyRangeNew := "vehicle.drivetrain.lastRemainingRange"
// Case 1: Old key is missing, new key is present
p.rest = map[string]TelematicData{
keyRangeNew: {Value: "200"},
}
rng, err := p.Range()
require.NoError(t, err)
require.Equal(t, int64(200), rng)
// Case 2: Old key is empty (null in JSON), new key is present
p.rest = map[string]TelematicData{
keyRangeOld: {Value: ""},
keyRangeNew: {Value: "150"},
}
rng, err = p.Range()
require.NoError(t, err)
require.Equal(t, int64(150), rng)
// Case 3: Old key is nil in streaming, new key is present
p.rest = nil
p.streaming = map[string]StreamingData{
keyRangeOld: {Value: nil},
keyRangeNew: {Value: 120.0},
}
rng, err = p.Range()
require.NoError(t, err)
require.Equal(t, int64(120), rng)
// Case 4: Old key is present, new key is also present, old key is preferred
p.rest = map[string]TelematicData{
keyRangeOld: {Value: "300"},
keyRangeNew: {Value: "150"},
}
p.streaming = nil
rng, err = p.Range()
require.NoError(t, err)
require.Equal(t, int64(300), rng)
// Case 5: Both keys are absent, returns error
p.rest = map[string]TelematicData{}
rng, err = p.Range()
require.Error(t, err)
require.Equal(t, int64(0), rng)
}