Skip to content
7 changes: 5 additions & 2 deletions templates/definition/vehicle/cardata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ requirements:
vehicle.vehicle.travelledDistance
```

Aktualisierung der Daten erfolgt einmalig bei Neustart und wenn Streamingdaten eingehen. Dies ist ausschließlich der Fall,
wenn das Fahrzeug aktiv Daten erzeugt.
Aktualisierung der Daten erfolgt einmalig bei Neustart und wenn Streamingdaten eingehen. Dies ist ausschließlich der Fall, wenn das Fahrzeug aktiv Daten erzeugt.

Sollte die Fahrzeugerkennung nicht zuverlässig funktionieren, sollte `welcomecharge` (Laden bei Verbindung) aktiviert werden.
en: |
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):

Expand All @@ -51,6 +52,8 @@ requirements:
```

Data will be updated on restart and when received using streaming. This only happens when the vehicle actively creates new updates.

In case the vehicle detection does not work reliably, `welcomecharge` (charge on connection) should be enabled.
params:
- preset: vehicle-common
- name: vin
Expand Down
3 changes: 2 additions & 1 deletion vehicle/bmw/cardata/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const ApiURL = "https://api-cardata.bmwgroup.com"
// https://mybmwweb-utilities.api.bmw/de-de/utilities/bmw/api/cd/catalogue/file
var requiredKeys = []string{
"vehicle.body.chargingPort.status",
"vehicle.body.chargingPort.combinedStatus",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"vehicle.cabin.infotainment.navigation.currentLocation.latitude",
"vehicle.cabin.infotainment.navigation.currentLocation.longitude",
"vehicle.cabin.hvac.preconditioning.status.comfortState",
Expand All @@ -32,7 +33,7 @@ var requiredKeys = []string{
"vehicle.vehicle.travelledDistance",
}

const requiredVersion = "v5"
const requiredVersion = "v6"

type API struct {
*request.Helper
Expand Down
44 changes: 29 additions & 15 deletions vehicle/bmw/cardata/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,13 @@ func (v *Provider) any(key string) (any, error) {
return nil, api.ErrNotAvailable
}

func isNilOrEmtpy(val any) bool {
func isNilOrEmpty(val any) bool {
Comment thread
mfuchs1984 marked this conversation as resolved.
return val == nil || val == ""
}

func (v *Provider) String(key string) (string, error) {
res, err := v.any(key)
if err != nil || isNilOrEmtpy(res) {
if err != nil || isNilOrEmpty(res) {
return "", api.ErrNotAvailable
}

Expand All @@ -156,7 +156,7 @@ func (v *Provider) String(key string) (string, error) {

func (v *Provider) Int(key string) (int64, error) {
res, err := v.any(key)
if err != nil || isNilOrEmtpy(res) {
if err != nil || isNilOrEmpty(res) {
return 0, api.ErrNotAvailable
}

Expand All @@ -165,7 +165,7 @@ func (v *Provider) Int(key string) (int64, error) {

func (v *Provider) Float(key string) (float64, error) {
res, err := v.any(key)
if err != nil || isNilOrEmtpy(res) {
if err != nil || isNilOrEmpty(res) {
return 0, api.ErrNotAvailable
}

Expand All @@ -186,22 +186,23 @@ var _ api.ChargeState = (*Provider)(nil)

// Status implements the api.ChargeState interface
func (v *Provider) Status() (api.ChargeStatus, error) {
Comment thread
mfuchs1984 marked this conversation as resolved.
port, err := v.String("vehicle.body.chargingPort.status")
if err != nil {
return api.StatusNone, err
}

status := api.StatusA // disconnected
if port == "CONNECTED" {
status = api.StatusB
}

// evaluate status first, since it's usually available through
// 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, err = v.String("vehicle.drivetrain.electricEngine.charging.hvStatus")
cs, _ = v.String("vehicle.drivetrain.electricEngine.charging.hvStatus")
}

if slices.Contains([]string{
"INITIALIZATION", // vehicle.drivetrain.electricEngine.charging.status
"CHARGINGPAUSED", // vehicle.drivetrain.electricEngine.charging.status
"CHARGINGENDED", // vehicle.drivetrain.electricEngine.charging.status
"WAITING_FOR_CHARGING", // vehicle.drivetrain.electricEngine.charging.hvStatus
"FINISHED_FULLY_CHARGED", // vehicle.drivetrain.electricEngine.charging.hvStatus
"FINISHED_NOT_FULL", // vehicle.drivetrain.electricEngine.charging.hvStatus
}, cs) {
return api.StatusB, nil
}

if slices.Contains([]string{
Expand All @@ -211,6 +212,19 @@ func (v *Provider) Status() (api.ChargeStatus, error) {
return api.StatusC, nil
}

port, err := v.String("vehicle.body.chargingPort.status")
if err != nil {
port, err = v.String("vehicle.body.chargingPort.combinedStatus")
if err != nil {
return api.StatusNone, err
}
}

status := api.StatusA // disconnected
if port == "CONNECTED" {
status = api.StatusB
}

return status, err
}

Expand Down
157 changes: 131 additions & 26 deletions vehicle/bmw/cardata/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"
"time"

"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
Expand Down Expand Up @@ -58,40 +59,40 @@ func TestSocFallback(t *testing.T) {
// prevent container panic
p.updated = time.Now()

keySocOld := "vehicle.drivetrain.batteryManagement.header"
keySocNew := "vehicle.powertrain.electric.battery.stateOfCharge.displayed"
keySocPrimary := "vehicle.drivetrain.batteryManagement.header"
keySocFallback := "vehicle.powertrain.electric.battery.stateOfCharge.displayed"

// Case 1: Old key is missing, new key is present
// Case 1: Primary key is missing, fallback key is present
p.rest = map[string]TelematicData{
keySocNew: {Value: "80"},
keySocFallback: {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
// Case 2: Primary key is empty (null in JSON), fallback key is present
p.rest = map[string]TelematicData{
keySocOld: {Value: ""},
keySocNew: {Value: "90"},
keySocPrimary: {Value: ""},
keySocFallback: {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
// Case 3: Primary key is nil in streaming, fallback key is present
p.rest = nil
p.streaming = map[string]StreamingData{
keySocOld: {Value: nil},
keySocNew: {Value: 95.0},
keySocPrimary: {Value: nil},
keySocFallback: {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
// Case 4: Primary key is present, fallback key is also present, primary key is preferred
p.rest = map[string]TelematicData{
keySocOld: {Value: "42"},
keySocNew: {Value: "90"},
keySocPrimary: {Value: "42"},
keySocFallback: {Value: "90"},
}
p.streaming = nil
soc, err = p.Soc()
Expand All @@ -115,40 +116,40 @@ func TestRangeFallback(t *testing.T) {
// prevent container panic
p.updated = time.Now()

keyRangeOld := "vehicle.drivetrain.electricEngine.kombiRemainingElectricRange"
keyRangeNew := "vehicle.drivetrain.lastRemainingRange"
keyRangePrimary := "vehicle.drivetrain.electricEngine.kombiRemainingElectricRange"
keyRangeFallback := "vehicle.drivetrain.lastRemainingRange"

// Case 1: Old key is missing, new key is present
// Case 1: Primary key is missing, fallback key is present
p.rest = map[string]TelematicData{
keyRangeNew: {Value: "200"},
keyRangeFallback: {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
// Case 2: Primary key is empty (null in JSON), fallback key is present
p.rest = map[string]TelematicData{
keyRangeOld: {Value: ""},
keyRangeNew: {Value: "150"},
keyRangePrimary: {Value: ""},
keyRangeFallback: {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
// Case 3: Primary key is nil in streaming, fallback key is present
p.rest = nil
p.streaming = map[string]StreamingData{
keyRangeOld: {Value: nil},
keyRangeNew: {Value: 120.0},
keyRangePrimary: {Value: nil},
keyRangeFallback: {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
// Case 4: Primary key is present, fallback key is also present, primary key is preferred
p.rest = map[string]TelematicData{
keyRangeOld: {Value: "300"},
keyRangeNew: {Value: "150"},
keyRangePrimary: {Value: "300"},
keyRangeFallback: {Value: "150"},
}
p.streaming = nil
rng, err = p.Range()
Expand All @@ -161,3 +162,107 @@ func TestRangeFallback(t *testing.T) {
require.Error(t, err)
require.Equal(t, int64(0), rng)
}

func TestStatusFallback(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()

keyChargingStatusPrimary := "vehicle.drivetrain.electricEngine.charging.status"
keyChargingStatusFallback := "vehicle.drivetrain.electricEngine.charging.hvStatus"
keyPortStatusPrimary := "vehicle.body.chargingPort.status"
keyPortStatusFallback := "vehicle.body.chargingPort.combinedStatus"

// Case 1: Charging status fallback works
p.rest = map[string]TelematicData{
keyChargingStatusFallback: {Value: "CHARGING"},
}
status, err := p.Status()
require.NoError(t, err)
require.Equal(t, api.StatusC, status)

// Case 2: charging status primary is present, fallback is present, primary is preferred
p.rest = map[string]TelematicData{
keyChargingStatusPrimary: {Value: "CHARGINGACTIVE"},
keyChargingStatusFallback: {Value: "WAITING_FOR_CHARGING"}, // would be StatusB
}
status, err = p.Status()
require.NoError(t, err)
require.Equal(t, api.StatusC, status)

// Case 3: charging status empty, port primary is missing, port fallback is present -> StatusB
p.rest = map[string]TelematicData{
keyPortStatusFallback: {Value: "CONNECTED"},
}
status, err = p.Status()
require.NoError(t, err)
require.Equal(t, api.StatusB, status)

// Case 4: charging status empty, port primary is empty, port fallback is present -> StatusA (disconnected)
p.rest = map[string]TelematicData{
keyPortStatusPrimary: {Value: ""},
keyPortStatusFallback: {Value: "DISCONNECTED"},
}
status, err = p.Status()
require.NoError(t, err)
require.Equal(t, api.StatusA, status)

// Case 5: charging status empty, port primary nil in streaming, port fallback is present -> StatusB
p.rest = nil
p.streaming = map[string]StreamingData{
keyPortStatusPrimary: {Value: nil},
keyPortStatusFallback: {Value: "CONNECTED"},
}
status, err = p.Status()
require.NoError(t, err)
require.Equal(t, api.StatusB, status)

// Case 6: port primary is present, fallback is also present, primary is preferred
p.rest = map[string]TelematicData{
keyPortStatusPrimary: {Value: "CONNECTED"},
keyPortStatusFallback: {Value: "DISCONNECTED"},
}
p.streaming = nil
status, err = p.Status()
require.NoError(t, err)
require.Equal(t, api.StatusB, status)

// Case 7: Both port keys are absent, returns error
p.rest = map[string]TelematicData{}
status, err = p.Status()
require.Error(t, err)
require.Equal(t, api.ErrNotAvailable, err)
require.Equal(t, api.StatusNone, status)

// Case 8: charging status state takes precedence over port state (charging status = C, port = DISCONNECTED)
p.rest = map[string]TelematicData{
keyChargingStatusPrimary: {Value: "CHARGINGACTIVE"},
keyPortStatusPrimary: {Value: "DISCONNECTED"},
}
status, err = p.Status()
require.NoError(t, err)
require.Equal(t, api.StatusC, status)

// Case 9: charging status state takes precedence over port state (charging status = B, port = DISCONNECTED)
p.rest = map[string]TelematicData{
keyChargingStatusPrimary: {Value: "INITIALIZATION"},
keyPortStatusPrimary: {Value: "DISCONNECTED"},
}
status, err = p.Status()
require.NoError(t, err)
require.Equal(t, api.StatusB, status)

// Case 10: charging status state is "NOCHARGING", which allows no distinction between charging status A and B, so fall back to port state
p.rest = map[string]TelematicData{
keyChargingStatusPrimary: {Value: "NOCHARGING"},
keyPortStatusPrimary: {Value: "CONNECTED"},
}
status, err = p.Status()
require.NoError(t, err)
require.Equal(t, api.StatusB, status)
}
Loading