diff --git a/templates/definition/vehicle/cardata.yaml b/templates/definition/vehicle/cardata.yaml index 3f1db2255..e025862c0 100644 --- a/templates/definition/vehicle/cardata.yaml +++ b/templates/definition/vehicle/cardata.yaml @@ -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 ``` diff --git a/vehicle/bmw/cardata/api.go b/vehicle/bmw/cardata/api.go index 11f5e1188..4165dea39 100644 --- a/vehicle/bmw/cardata/api.go +++ b/vehicle/bmw/cardata/api.go @@ -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", diff --git a/vehicle/bmw/cardata/provider.go b/vehicle/bmw/cardata/provider.go index 9bfbd7cdb..21a27b7f4 100644 --- a/vehicle/bmw/cardata/provider.go +++ b/vehicle/bmw/cardata/provider.go @@ -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 } diff --git a/vehicle/bmw/cardata/provider_test.go b/vehicle/bmw/cardata/provider_test.go index 601f21c30..4b813596b 100644 --- a/vehicle/bmw/cardata/provider_test.go +++ b/vehicle/bmw/cardata/provider_test.go @@ -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) +}