Cardata: support "New Class" vehicles (#29709)
This commit is contained in:
parent
4dcff007d7
commit
2f7126b005
4 changed files with 146 additions and 13 deletions
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue