Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AS-3125 Map current set of Tesla fields #94

Merged
merged 18 commits into from
Nov 13, 2024
Merged
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ tools: tools-golangci-lint
clickhouse:
go run ./cmd/clickhouse-container

generate: generate-nativestatus generate-ruptela # Generate all files for the repository
generate: generate-nativestatus generate-ruptela generate-tesla # Generate all files for the repository
go run ./cmd/codegen -generators=custom -custom.output-file=./pkg/vss/vehicle-structs.go -custom.template-file=./internal/generator/vehicle.tmpl -custom.format=true

generate-nativestatus: # Generate all files for nativestatus
Expand All @@ -85,4 +85,8 @@ generate-ruptela: # Generate all files for ruptela
go run ./cmd/codegen -convert.package=ruptela -generators=convert -convert.output-file=./pkg/ruptela/vehicle-convert-funcs_gen.go -definitions=./pkg/ruptela/schema/ruptela-definitions.yaml
go run ./cmd/codegen -generators=custom -custom.output-file=./pkg/ruptela/vehicle-v1-convert_gen.go -custom.template-file=./pkg/ruptela/codegen/convert-status.tmpl -custom.format=true -definitions=./pkg/ruptela/schema/ruptela-definitions.yaml
go run ./cmd/codegen -generators=custom -custom.output-file=./pkg/ruptela/vehicle-location-convert_gen.go -custom.template-file=./pkg/ruptela/codegen/convert-location.tmpl -custom.format=true -definitions=./pkg/ruptela/schema/ruptela-definitions.yaml
go run ./pkg/ruptela/codegen
go run ./pkg/ruptela/codegen

generate-tesla: # Generate all files for tesla
go run ./cmd/codegen -convert.package=tesla -generators=convert -convert.output-file=./pkg/tesla/vehicle-convert-funcs_gen.go -definitions=./pkg/tesla/schema/tesla-definitions.yaml
go run ./cmd/codegen -generators=custom -custom.output-file=./pkg/tesla/tesla-convert_gen.go -custom.template-file=./pkg/tesla/codegen/convert-status.tmpl -custom.format=true -definitions=./pkg/tesla/schema/tesla-definitions.yaml
3 changes: 3 additions & 0 deletions pkg/schema/spec/default-definitions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
- vspecName: Vehicle.Powertrain.Range
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA
- vspecName: Vehicle.Powertrain.TractionBattery.Charging.AddedEnergy
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA
- vspecName: Vehicle.Powertrain.TractionBattery.Charging.ChargeLimit
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
"Vehicle.Powertrain.TractionBattery.Charging.Timer","branch","","","","","","Properties related to timing of battery charging sessions.","","","","cd5b57ada627510e83f90832efed9d5a"
"Vehicle.Powertrain.TractionBattery.Charging.Timer.Mode","actuator","string","","","","","Defines timer mode for charging: INACTIVE - no timer set, charging may start as soon as battery is connected to a charger. START_TIME - charging shall start at Charging.Timer.Time. END_TIME - charging shall be finished (reach Charging.ChargeLimit) at Charging.Timer.Time. When charging is completed the vehicle shall change mode to 'inactive' or set a new Charging.Timer.Time. Charging shall start immediately if mode is 'starttime' or 'endtime' and Charging.Timer.Time is a time in the past.","","['INACTIVE', 'START_TIME', 'END_TIME']","","b09fb52261735977af275dda1904a7a1"
"Vehicle.Powertrain.TractionBattery.Charging.Timer.Time","actuator","string","","iso8601","","","Time for next charging-related action, formatted according to ISO 8601 with UTC time zone. Value has no significance if Charging.Timer.Mode is 'inactive'.","","","","c08dcaeda02b5e26aacd7e2542f0fc90"
"Vehicle.Powertrain.TractionBattery.Charging.AddedEnergy","sensor","float","","kWh","","","Amount of charge added to the high voltage battery during the current charging session, expressed in kilowatt-hours.","","","","cf83a8b31c8d5e60b328515ddff177bb"
"Vehicle.Powertrain.TractionBattery.DCDC","branch","","","","","","Properties related to DC/DC converter converting high voltage (from high voltage battery) to vehicle low voltage (supply voltage, typically 12 Volts).","","","","01f4943795b55cbd8f94e1bca137fc0a"
"Vehicle.Powertrain.TractionBattery.DCDC.PowerLoss","sensor","float","","W","","","Electrical energy lost by power dissipation to heat inside DC/DC converter.","","","","f29e37087cdf57ca998188c7b945a77b"
"Vehicle.Powertrain.TractionBattery.DCDC.Temperature","sensor","float","","celsius","","","Current temperature of DC/DC converter converting battery high voltage to vehicle low voltage (typically 12 Volts).","","","","4e587c3af2aa5fbb9205e42a64fc8d77"
Expand Down
81 changes: 81 additions & 0 deletions pkg/tesla/codegen/convert-status.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Code generated by github.com/DIMO-Network/model-garage DO NOT EDIT.
package tesla

var errNotFound = errors.New("field not found")

// SignalsFromV1Data creates a slice of vss.Signal from the given v1 status JSON data.
// On error, partial results may be returned.
func SignalsFromTesla(baseSignal vss.Signal, jsonData []byte) ([]vss.Signal, []error) {
var retSignals []vss.Signal
{{ $first := true -}}
{{- $root := . }}
{{- range $idx, $sig := .Signals }}
{{ if eq (len $sig.Conversions) 0 }} {{ continue }} {{ end -}}
{{ if $first -}}
var val any
var ts time.Time
var err error
var errs []error
{{ $first = false }} {{ end }}

val, ts, err = {{ $sig.GOName }}FromTesla(jsonData)
if err != nil {
if !errors.Is(err, errNotFound) {
errs = append(errs, fmt.Errorf("failed to convert '{{ $sig.GOName }}': %w", err))
}
} else {
sig := vss.Signal{
Name: "{{ $sig.JSONName }}",
TokenID: baseSignal.TokenID,
Timestamp: ts,
Source: baseSignal.Source,
}
sig.SetValue(val)
retSignals = append(retSignals, sig)
}
{{- end }}
return retSignals, errs
}

var zeroTime time.Time

{{- range $i, $sig := .Signals }}
// {{ $sig.GOName }}FromTesla converts the given JSON data to a {{ $sig.GOType }}.
func {{ $sig.GOName }}FromTesla(jsonData []byte) (ret {{ $sig.GOType }}, ts time.Time, err error) {
var errs error
var result gjson.Result

{{- range $j, $conv := .Conversions }}
result = gjson.GetBytes(jsonData, "data.{{ $conv.OriginalName }}")
if result.Exists() && result.Value() != nil {
val, ok := result.Value().({{ $conv.OriginalType }})
if ok {
retVal, err := To{{ $sig.GOName }}{{ $j }}(jsonData, val)
if err == nil {
endpoint, _, _ := strings.Cut("{{ $conv.OriginalName }}", ".")
result := gjson.GetBytes(jsonData, "data." + endpoint + ".timestamp")

if result.Exists() && result.Value() != nil {
if unix, ok := result.Value().(float64); ok {
ts := time.Unix(int64(unix), 0)

return retVal, ts, nil
}
}

errs = errors.Join(errs, fmt.Errorf("couldn't find a timestamp for 'data.{{ $conv.OriginalName }}'"))
}
errs = errors.Join(errs, fmt.Errorf("failed to convert 'data.{{ $conv.OriginalName }}': %w", err))
} else {
errs = errors.Join(errs, fmt.Errorf("%w, field 'data.{{ $conv.OriginalName }}' is not of type '{{ $conv.OriginalType }}' got '%v' of type '%T'", convert.InvalidTypeError(), result.Value(), result.Value()))
}
}
{{- end }}

if errs == nil {
return ret, zeroTime, fmt.Errorf("%w '{{ $sig.GOName }}'", errNotFound)
}

return ret, zeroTime, errs
}
{{- end }}
109 changes: 109 additions & 0 deletions pkg/tesla/schema/tesla-definitions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# This file defines mappings from Tesla /vehicle_data responses to VSS. See
# https://developer.tesla.com/docs/fleet-api/endpoints/vehicle-endpoints#vehicle-data

- vspecName: Vehicle.Chassis.Axle.Row1.Wheel.Left.Tire.Pressure
conversions:
- originalName: "vehicle_state.tpms_pressure_fl" # In bars
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Chassis.Axle.Row1.Wheel.Right.Tire.Pressure
conversions:
- originalName: "vehicle_state.tpms_pressure_fr" # In bars
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Chassis.Axle.Row2.Wheel.Left.Tire.Pressure
conversions:
- originalName: "vehicle_state.tpms_pressure_rl" # In bars
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Chassis.Axle.Row2.Wheel.Right.Tire.Pressure
conversions:
- originalName: "vehicle_state.tpms_pressure_rr" # In bars
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.CurrentLocation.Latitude
conversions:
- originalName: drive_state.latitude
originalType: float64
requiredPrivileges:
- VEHICLE_ALL_TIME_LOCATION

- vspecName: Vehicle.CurrentLocation.Longitude
conversions:
- originalName: drive_state.longitude
originalType: float64
requiredPrivileges:
- VEHICLE_ALL_TIME_LOCATION

- vspecName: Vehicle.Exterior.AirTemperature
conversions:
- originalName: "climate_state.outside_temp" # I believe this is in Celsius.
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Powertrain.Range
conversions:
- originalName: "charge_state.battery_range" # In miles
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Powertrain.TractionBattery.Charging.AddedEnergy
conversions:
- originalName: "charge_state.charge_energy_added" # In kilowatt-hours.
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Powertrain.TractionBattery.Charging.ChargeLimit
conversions:
- originalName: "charge_state.charge_limit_soc" # In percent
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Powertrain.TractionBattery.Charging.IsCharging
conversions:
- originalName: "charge_state.charging_state" # Observed values: "Disconnected", "NoPower", "Starting", "Charging", "Complete", "Stopped"
originalType: "string"
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Powertrain.TractionBattery.CurrentPower
conversions:
- originalName: "drive_state.power" # I believe this is in kilowatts. Need to check that this is just charge_state.charger_power but better.
# It's negative when charging, positive when expending energy driving. Note that because of regenerative braking
# this may be negative even while driving.
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Powertrain.TractionBattery.StateOfCharge.Current
conversions:
- originalName: "charge_state.battery_level" # In percent
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Powertrain.Transmission.TravelledDistance
conversions:
- originalName: "vehicle_state.odometer" # In miles.
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA

- vspecName: Vehicle.Speed
conversions:
- originalName: drive_state.speed # In miles per hour.
originalType: float64
requiredPrivileges:
- VEHICLE_NON_LOCATION_DATA
46 changes: 46 additions & 0 deletions pkg/tesla/status/convert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Package status converts Tesla CloudEvents to ClickHouse-ready slices of signals.
package status

import (
"encoding/json"
"fmt"

"github.com/DIMO-Network/model-garage/pkg/cloudevent"
"github.com/DIMO-Network/model-garage/pkg/convert"
"github.com/DIMO-Network/model-garage/pkg/tesla"
"github.com/DIMO-Network/model-garage/pkg/vss"
)

func Decode(msgBytes []byte) ([]vss.Signal, error) {
// Only interested in the top-level CloudEvent fields.
var ce cloudevent.CloudEvent[struct{}]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var ce cloudevent.CloudEvent[struct{}]
var ce cloudevent.CloudEventHeader

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Let's do that.


if err := json.Unmarshal(msgBytes, &ce); err != nil {
return nil, fmt.Errorf("failed to unmarshal payload: %w", err)
}

did, err := cloudevent.DecodeNFTDID(ce.Subject)
if err != nil {
return nil, fmt.Errorf("failed to decode subject DID: %w", err)
}

tokenID := did.TokenID
source := ce.Source

baseSignal := vss.Signal{
TokenID: tokenID,
Source: source,
}

sigs, errs := tesla.SignalsFromTesla(baseSignal, msgBytes)
if len(errs) != 0 {
return nil, convert.ConversionError{
TokenID: tokenID,
Source: source,
DecodedSignals: sigs,
Errors: errs,
}
}

return sigs, nil
}
71 changes: 71 additions & 0 deletions pkg/tesla/status/convert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package status

import (
"testing"
"time"

"github.com/DIMO-Network/model-garage/pkg/vss"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var baseDoc = []byte(`
{
"subject": "did:nft:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF_37",
"source": "0x983110309620D911731Ac0932219af06091b6744",
"data": {
"charge_state": {
"battery_level": 23,
"battery_range": 341,
"charge_energy_added": 42,
"charge_limit_soc": 80,
"charging_state": "Charging",
"timestamp": 1730728800
},
"climate_state": {
"outside_temp": 19,
"timestamp": 1730728802
},
"drive_state": {
"latitude": 38.89,
"longitude": 77.03,
"power": -7,
"speed": 25,
"timestamp": 1730738800
},
"vehicle_state": {
"odometer": 5633,
"tpms_pressure_fl": 3.12,
"tpms_pressure_fr": 3.09,
"tpms_pressure_rl": 2.98,
"tpms_pressure_rr": 2.99,
"timestamp": 1730728805
}
} }
`)

const teslaConnection = "0x983110309620D911731Ac0932219af06091b6744"

var expSignals = []vss.Signal{
{TokenID: 37, Timestamp: time.Unix(1730728805, 0), Name: "chassisAxleRow1WheelLeftTirePressure", ValueNumber: 312, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730728805, 0), Name: "chassisAxleRow1WheelRightTirePressure", ValueNumber: 309, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730728805, 0), Name: "chassisAxleRow2WheelLeftTirePressure", ValueNumber: 298, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730728805, 0), Name: "chassisAxleRow2WheelRightTirePressure", ValueNumber: 299, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730738800, 0), Name: "currentLocationLatitude", ValueNumber: 38.89, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730738800, 0), Name: "currentLocationLongitude", ValueNumber: 77.03, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730728802, 0), Name: "exteriorAirTemperature", ValueNumber: 19, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730728800, 0), Name: "powertrainRange", ValueNumber: 548.7863040000001, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730728800, 0), Name: "powertrainTractionBatteryChargingAddedEnergy", ValueNumber: 42, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730728800, 0), Name: "powertrainTractionBatteryChargingChargeLimit", ValueNumber: 80, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730728800, 0), Name: "powertrainTractionBatteryChargingIsCharging", ValueNumber: 1, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730738800, 0), Name: "powertrainTractionBatteryCurrentPower", ValueNumber: 7000, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730728800, 0), Name: "powertrainTractionBatteryStateOfChargeCurrent", ValueNumber: 23, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730728805, 0), Name: "powertrainTransmissionTravelledDistance", ValueNumber: 9065.434752000001, Source: teslaConnection},
{TokenID: 37, Timestamp: time.Unix(1730738800, 0), Name: "speed", ValueNumber: 40.2336, Source: teslaConnection},
}

func TestSignalsFromTesla(t *testing.T) {
computedSignals, err := Decode(baseDoc)
require.Empty(t, err, "Expected no errors.")
assert.ElementsMatch(t, computedSignals, expSignals)
}
Loading
Loading