tescmd 0.1.2__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tescmd/__init__.py +1 -1
- tescmd/api/client.py +49 -5
- tescmd/api/command.py +1 -1
- tescmd/api/errors.py +13 -0
- tescmd/api/signed_command.py +19 -14
- tescmd/api/vehicle.py +19 -1
- tescmd/auth/oauth.py +5 -1
- tescmd/auth/server.py +6 -1
- tescmd/auth/token_store.py +8 -1
- tescmd/cache/response_cache.py +11 -3
- tescmd/cli/_client.py +142 -20
- tescmd/cli/_options.py +2 -4
- tescmd/cli/auth.py +121 -11
- tescmd/cli/energy.py +2 -0
- tescmd/cli/key.py +149 -14
- tescmd/cli/main.py +70 -7
- tescmd/cli/mcp_cmd.py +153 -0
- tescmd/cli/nav.py +3 -1
- tescmd/cli/openclaw.py +169 -0
- tescmd/cli/security.py +7 -1
- tescmd/cli/serve.py +923 -0
- tescmd/cli/setup.py +244 -25
- tescmd/cli/sharing.py +2 -0
- tescmd/cli/status.py +1 -1
- tescmd/cli/trunk.py +8 -17
- tescmd/cli/user.py +16 -1
- tescmd/cli/vehicle.py +156 -20
- tescmd/crypto/__init__.py +3 -1
- tescmd/crypto/ecdh.py +9 -0
- tescmd/crypto/schnorr.py +191 -0
- tescmd/deploy/github_pages.py +8 -0
- tescmd/deploy/tailscale_serve.py +154 -0
- tescmd/mcp/__init__.py +7 -0
- tescmd/mcp/server.py +648 -0
- tescmd/models/__init__.py +0 -2
- tescmd/models/auth.py +24 -2
- tescmd/models/config.py +1 -0
- tescmd/models/energy.py +0 -9
- tescmd/openclaw/__init__.py +23 -0
- tescmd/openclaw/bridge.py +330 -0
- tescmd/openclaw/config.py +167 -0
- tescmd/openclaw/dispatcher.py +522 -0
- tescmd/openclaw/emitter.py +175 -0
- tescmd/openclaw/filters.py +123 -0
- tescmd/openclaw/gateway.py +687 -0
- tescmd/openclaw/telemetry_store.py +53 -0
- tescmd/output/rich_output.py +46 -14
- tescmd/protocol/commands.py +2 -2
- tescmd/protocol/encoder.py +16 -13
- tescmd/protocol/payloads.py +132 -11
- tescmd/protocol/session.py +18 -8
- tescmd/protocol/signer.py +3 -17
- tescmd/telemetry/__init__.py +28 -0
- tescmd/telemetry/cache_sink.py +154 -0
- tescmd/telemetry/csv_sink.py +180 -0
- tescmd/telemetry/dashboard.py +227 -0
- tescmd/telemetry/decoder.py +284 -0
- tescmd/telemetry/fanout.py +49 -0
- tescmd/telemetry/fields.py +427 -0
- tescmd/telemetry/flatbuf.py +162 -0
- tescmd/telemetry/mapper.py +239 -0
- tescmd/telemetry/protos/__init__.py +4 -0
- tescmd/telemetry/protos/vehicle_alert.proto +31 -0
- tescmd/telemetry/protos/vehicle_alert_pb2.py +42 -0
- tescmd/telemetry/protos/vehicle_alert_pb2.pyi +44 -0
- tescmd/telemetry/protos/vehicle_connectivity.proto +23 -0
- tescmd/telemetry/protos/vehicle_connectivity_pb2.py +40 -0
- tescmd/telemetry/protos/vehicle_connectivity_pb2.pyi +33 -0
- tescmd/telemetry/protos/vehicle_data.proto +768 -0
- tescmd/telemetry/protos/vehicle_data_pb2.py +136 -0
- tescmd/telemetry/protos/vehicle_data_pb2.pyi +1336 -0
- tescmd/telemetry/protos/vehicle_error.proto +23 -0
- tescmd/telemetry/protos/vehicle_error_pb2.py +44 -0
- tescmd/telemetry/protos/vehicle_error_pb2.pyi +39 -0
- tescmd/telemetry/protos/vehicle_metric.proto +22 -0
- tescmd/telemetry/protos/vehicle_metric_pb2.py +44 -0
- tescmd/telemetry/protos/vehicle_metric_pb2.pyi +37 -0
- tescmd/telemetry/server.py +300 -0
- tescmd/telemetry/setup.py +468 -0
- tescmd/telemetry/tailscale.py +300 -0
- tescmd/telemetry/tui.py +1716 -0
- tescmd/triggers/__init__.py +18 -0
- tescmd/triggers/manager.py +264 -0
- tescmd/triggers/models.py +93 -0
- {tescmd-0.1.2.dist-info → tescmd-0.3.1.dist-info}/METADATA +125 -40
- tescmd-0.3.1.dist-info/RECORD +128 -0
- tescmd-0.1.2.dist-info/RECORD +0 -81
- {tescmd-0.1.2.dist-info → tescmd-0.3.1.dist-info}/WHEEL +0 -0
- {tescmd-0.1.2.dist-info → tescmd-0.3.1.dist-info}/entry_points.txt +0 -0
- {tescmd-0.1.2.dist-info → tescmd-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Fleet Telemetry field name registry and preset configurations.
|
|
2
|
+
|
|
3
|
+
Field IDs and names sourced from Tesla's ``vehicle_data.proto`` Field enum
|
|
4
|
+
(https://github.com/teslamotors/fleet-telemetry/blob/main/protos/vehicle_data.proto).
|
|
5
|
+
|
|
6
|
+
Presets define commonly-used field groups with appropriate polling
|
|
7
|
+
intervals for different use cases.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from tescmd.api.errors import ConfigError
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Field enum → human-readable name (from vehicle_data.proto)
|
|
16
|
+
#
|
|
17
|
+
# IDs and names match the proto exactly. Excluded:
|
|
18
|
+
# - Unknown (0)
|
|
19
|
+
# - Deprecated_1 (162), Deprecated_2 (100), Deprecated_3 (257)
|
|
20
|
+
# - Experimental_1-15 (119-122, 168-178)
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
FIELD_NAMES: dict[int, str] = {
|
|
24
|
+
# --- Drive / Motion ---
|
|
25
|
+
1: "DriveRail",
|
|
26
|
+
4: "VehicleSpeed",
|
|
27
|
+
5: "Odometer",
|
|
28
|
+
10: "Gear",
|
|
29
|
+
12: "PedalPosition",
|
|
30
|
+
13: "BrakePedal",
|
|
31
|
+
21: "Location",
|
|
32
|
+
22: "GpsState",
|
|
33
|
+
23: "GpsHeading",
|
|
34
|
+
98: "LateralAcceleration",
|
|
35
|
+
99: "LongitudinalAcceleration",
|
|
36
|
+
101: "CruiseSetSpeed",
|
|
37
|
+
106: "BrakePedalPos",
|
|
38
|
+
126: "CruiseFollowDistance",
|
|
39
|
+
129: "SpeedLimitWarning",
|
|
40
|
+
# --- Battery / Energy ---
|
|
41
|
+
6: "PackVoltage",
|
|
42
|
+
7: "PackCurrent",
|
|
43
|
+
8: "Soc",
|
|
44
|
+
9: "DCDCEnable",
|
|
45
|
+
11: "IsolationResistance",
|
|
46
|
+
24: "NumBrickVoltageMax",
|
|
47
|
+
25: "BrickVoltageMax",
|
|
48
|
+
26: "NumBrickVoltageMin",
|
|
49
|
+
27: "BrickVoltageMin",
|
|
50
|
+
28: "NumModuleTempMax",
|
|
51
|
+
29: "ModuleTempMax",
|
|
52
|
+
30: "NumModuleTempMin",
|
|
53
|
+
31: "ModuleTempMin",
|
|
54
|
+
32: "RatedRange",
|
|
55
|
+
33: "Hvil",
|
|
56
|
+
40: "EstBatteryRange",
|
|
57
|
+
41: "IdealBatteryRange",
|
|
58
|
+
42: "BatteryLevel",
|
|
59
|
+
55: "BatteryHeaterOn",
|
|
60
|
+
56: "NotEnoughPowerToHeat",
|
|
61
|
+
102: "LifetimeEnergyUsed",
|
|
62
|
+
103: "LifetimeEnergyUsedDrive",
|
|
63
|
+
134: "LifetimeEnergyGainedRegen",
|
|
64
|
+
158: "EnergyRemaining",
|
|
65
|
+
160: "BMSState",
|
|
66
|
+
# --- Charging ---
|
|
67
|
+
2: "ChargeState",
|
|
68
|
+
3: "BmsFullchargecomplete",
|
|
69
|
+
34: "DCChargingEnergyIn",
|
|
70
|
+
35: "DCChargingPower",
|
|
71
|
+
36: "ACChargingEnergyIn",
|
|
72
|
+
37: "ACChargingPower",
|
|
73
|
+
38: "ChargeLimitSoc",
|
|
74
|
+
39: "FastChargerPresent",
|
|
75
|
+
43: "TimeToFullCharge",
|
|
76
|
+
44: "ScheduledChargingStartTime",
|
|
77
|
+
45: "ScheduledChargingPending",
|
|
78
|
+
46: "ScheduledDepartureTime",
|
|
79
|
+
47: "PreconditioningEnabled",
|
|
80
|
+
48: "ScheduledChargingMode",
|
|
81
|
+
49: "ChargeAmps",
|
|
82
|
+
50: "ChargeEnableRequest",
|
|
83
|
+
51: "ChargerPhases",
|
|
84
|
+
52: "ChargePortColdWeatherMode",
|
|
85
|
+
53: "ChargeCurrentRequest",
|
|
86
|
+
54: "ChargeCurrentRequestMax",
|
|
87
|
+
57: "SuperchargerSessionTripPlanner",
|
|
88
|
+
117: "ChargePort",
|
|
89
|
+
118: "ChargePortLatch",
|
|
90
|
+
179: "DetailedChargeState",
|
|
91
|
+
183: "ChargePortDoorOpen",
|
|
92
|
+
184: "ChargerVoltage",
|
|
93
|
+
185: "ChargingCableType",
|
|
94
|
+
190: "EstimatedHoursToChargeTermination",
|
|
95
|
+
193: "FastChargerType",
|
|
96
|
+
256: "ChargeRateMilePerHour",
|
|
97
|
+
# --- Climate / HVAC ---
|
|
98
|
+
85: "InsideTemp",
|
|
99
|
+
86: "OutsideTemp",
|
|
100
|
+
87: "SeatHeaterLeft",
|
|
101
|
+
88: "SeatHeaterRight",
|
|
102
|
+
89: "SeatHeaterRearLeft",
|
|
103
|
+
90: "SeatHeaterRearRight",
|
|
104
|
+
91: "SeatHeaterRearCenter",
|
|
105
|
+
92: "AutoSeatClimateLeft",
|
|
106
|
+
93: "AutoSeatClimateRight",
|
|
107
|
+
186: "ClimateKeeperMode",
|
|
108
|
+
187: "DefrostForPreconditioning",
|
|
109
|
+
188: "DefrostMode",
|
|
110
|
+
196: "HvacACEnabled",
|
|
111
|
+
197: "HvacAutoMode",
|
|
112
|
+
198: "HvacFanSpeed",
|
|
113
|
+
199: "HvacFanStatus",
|
|
114
|
+
200: "HvacLeftTemperatureRequest",
|
|
115
|
+
201: "HvacPower",
|
|
116
|
+
202: "HvacRightTemperatureRequest",
|
|
117
|
+
203: "HvacSteeringWheelHeatAuto",
|
|
118
|
+
204: "HvacSteeringWheelHeatLevel",
|
|
119
|
+
211: "RearDisplayHvacEnabled",
|
|
120
|
+
237: "ClimateSeatCoolingFrontLeft",
|
|
121
|
+
238: "ClimateSeatCoolingFrontRight",
|
|
122
|
+
254: "SeatVentEnabled",
|
|
123
|
+
255: "RearDefrostEnabled",
|
|
124
|
+
180: "CabinOverheatProtectionMode",
|
|
125
|
+
181: "CabinOverheatProtectionTemperatureLimit",
|
|
126
|
+
# --- Security / Doors / Windows ---
|
|
127
|
+
58: "DoorState",
|
|
128
|
+
59: "Locked",
|
|
129
|
+
60: "FdWindow",
|
|
130
|
+
61: "FpWindow",
|
|
131
|
+
62: "RdWindow",
|
|
132
|
+
63: "RpWindow",
|
|
133
|
+
64: "VehicleName",
|
|
134
|
+
65: "SentryMode",
|
|
135
|
+
66: "SpeedLimitMode",
|
|
136
|
+
67: "CurrentLimitMph",
|
|
137
|
+
68: "Version",
|
|
138
|
+
94: "DriverSeatBelt",
|
|
139
|
+
95: "PassengerSeatBelt",
|
|
140
|
+
96: "DriverSeatOccupied",
|
|
141
|
+
123: "GuestModeEnabled",
|
|
142
|
+
124: "PinToDriveEnabled",
|
|
143
|
+
125: "PairedPhoneKeyAndKeyFobQty",
|
|
144
|
+
159: "ServiceMode",
|
|
145
|
+
161: "GuestModeMobileAccessState",
|
|
146
|
+
182: "CenterDisplay",
|
|
147
|
+
213: "RemoteStartEnabled",
|
|
148
|
+
226: "ValetModeEnabled",
|
|
149
|
+
# --- Tires ---
|
|
150
|
+
69: "TpmsPressureFl",
|
|
151
|
+
70: "TpmsPressureFr",
|
|
152
|
+
71: "TpmsPressureRl",
|
|
153
|
+
72: "TpmsPressureRr",
|
|
154
|
+
81: "TpmsLastSeenPressureTimeFl",
|
|
155
|
+
82: "TpmsLastSeenPressureTimeFr",
|
|
156
|
+
83: "TpmsLastSeenPressureTimeRl",
|
|
157
|
+
84: "TpmsLastSeenPressureTimeRr",
|
|
158
|
+
224: "TpmsHardWarnings",
|
|
159
|
+
225: "TpmsSoftWarnings",
|
|
160
|
+
# --- Drive Inverter (per-motor diagnostics) ---
|
|
161
|
+
14: "DiStateR",
|
|
162
|
+
15: "DiHeatsinkTR",
|
|
163
|
+
16: "DiAxleSpeedR",
|
|
164
|
+
17: "DiTorquemotor",
|
|
165
|
+
18: "DiStatorTempR",
|
|
166
|
+
19: "DiVBatR",
|
|
167
|
+
20: "DiMotorCurrentR",
|
|
168
|
+
135: "DiStateF",
|
|
169
|
+
136: "DiStateREL",
|
|
170
|
+
137: "DiStateRER",
|
|
171
|
+
138: "DiHeatsinkTF",
|
|
172
|
+
139: "DiHeatsinkTREL",
|
|
173
|
+
140: "DiHeatsinkTRER",
|
|
174
|
+
141: "DiAxleSpeedF",
|
|
175
|
+
142: "DiAxleSpeedREL",
|
|
176
|
+
143: "DiAxleSpeedRER",
|
|
177
|
+
144: "DiSlaveTorqueCmd",
|
|
178
|
+
145: "DiTorqueActualR",
|
|
179
|
+
146: "DiTorqueActualF",
|
|
180
|
+
147: "DiTorqueActualREL",
|
|
181
|
+
148: "DiTorqueActualRER",
|
|
182
|
+
149: "DiStatorTempF",
|
|
183
|
+
150: "DiStatorTempREL",
|
|
184
|
+
151: "DiStatorTempRER",
|
|
185
|
+
152: "DiVBatF",
|
|
186
|
+
153: "DiVBatREL",
|
|
187
|
+
154: "DiVBatRER",
|
|
188
|
+
155: "DiMotorCurrentF",
|
|
189
|
+
156: "DiMotorCurrentREL",
|
|
190
|
+
157: "DiMotorCurrentRER",
|
|
191
|
+
164: "DiInverterTR",
|
|
192
|
+
165: "DiInverterTF",
|
|
193
|
+
166: "DiInverterTREL",
|
|
194
|
+
167: "DiInverterTRER",
|
|
195
|
+
# --- Navigation / Route ---
|
|
196
|
+
107: "RouteLastUpdated",
|
|
197
|
+
108: "RouteLine",
|
|
198
|
+
109: "MilesToArrival",
|
|
199
|
+
110: "MinutesToArrival",
|
|
200
|
+
111: "OriginLocation",
|
|
201
|
+
112: "DestinationLocation",
|
|
202
|
+
163: "DestinationName",
|
|
203
|
+
215: "RouteTrafficMinutesDelay",
|
|
204
|
+
192: "ExpectedEnergyPercentAtTripArrival",
|
|
205
|
+
# --- Vehicle Info / Config ---
|
|
206
|
+
113: "CarType",
|
|
207
|
+
114: "Trim",
|
|
208
|
+
115: "ExteriorColor",
|
|
209
|
+
116: "RoofColor",
|
|
210
|
+
189: "EfficiencyPackage",
|
|
211
|
+
191: "EuropeVehicle",
|
|
212
|
+
214: "RightHandDrive",
|
|
213
|
+
227: "WheelType",
|
|
214
|
+
228: "WiperHeatEnabled",
|
|
215
|
+
# --- Safety / ADAS ---
|
|
216
|
+
127: "AutomaticBlindSpotCamera",
|
|
217
|
+
128: "BlindSpotCollisionWarningChime",
|
|
218
|
+
130: "ForwardCollisionWarning",
|
|
219
|
+
131: "LaneDepartureAvoidance",
|
|
220
|
+
132: "EmergencyLaneDepartureAvoidance",
|
|
221
|
+
133: "AutomaticEmergencyBrakingOff",
|
|
222
|
+
# --- Powershare ---
|
|
223
|
+
206: "PowershareHoursLeft",
|
|
224
|
+
207: "PowershareInstantaneousPowerKW",
|
|
225
|
+
208: "PowershareStatus",
|
|
226
|
+
209: "PowershareStopReason",
|
|
227
|
+
210: "PowershareType",
|
|
228
|
+
# --- Homelink ---
|
|
229
|
+
194: "HomelinkDeviceCount",
|
|
230
|
+
195: "HomelinkNearby",
|
|
231
|
+
# --- Software Updates ---
|
|
232
|
+
216: "SoftwareUpdateDownloadPercentComplete",
|
|
233
|
+
217: "SoftwareUpdateExpectedDurationMinutes",
|
|
234
|
+
218: "SoftwareUpdateInstallationPercentComplete",
|
|
235
|
+
219: "SoftwareUpdateScheduledStartTime",
|
|
236
|
+
220: "SoftwareUpdateVersion",
|
|
237
|
+
# --- Tonneau ---
|
|
238
|
+
221: "TonneauOpenPercent",
|
|
239
|
+
222: "TonneauPosition",
|
|
240
|
+
223: "TonneauTentMode",
|
|
241
|
+
# --- Location Context ---
|
|
242
|
+
229: "LocatedAtHome",
|
|
243
|
+
230: "LocatedAtWork",
|
|
244
|
+
231: "LocatedAtFavorite",
|
|
245
|
+
# --- Settings ---
|
|
246
|
+
232: "SettingDistanceUnit",
|
|
247
|
+
233: "SettingTemperatureUnit",
|
|
248
|
+
234: "Setting24HourTime",
|
|
249
|
+
235: "SettingTirePressureUnit",
|
|
250
|
+
236: "SettingChargeUnit",
|
|
251
|
+
# --- Lights ---
|
|
252
|
+
239: "LightsHazardsActive",
|
|
253
|
+
240: "LightsTurnSignal",
|
|
254
|
+
241: "LightsHighBeams",
|
|
255
|
+
# --- Media ---
|
|
256
|
+
242: "MediaPlaybackStatus",
|
|
257
|
+
243: "MediaPlaybackSource",
|
|
258
|
+
244: "MediaAudioVolume",
|
|
259
|
+
245: "MediaNowPlayingDuration",
|
|
260
|
+
246: "MediaNowPlayingElapsed",
|
|
261
|
+
247: "MediaNowPlayingArtist",
|
|
262
|
+
248: "MediaNowPlayingTitle",
|
|
263
|
+
249: "MediaNowPlayingAlbum",
|
|
264
|
+
250: "MediaNowPlayingStation",
|
|
265
|
+
251: "MediaAudioVolumeIncrement",
|
|
266
|
+
252: "MediaAudioVolumeMax",
|
|
267
|
+
# --- Misc ---
|
|
268
|
+
205: "OffroadLightbarPresent",
|
|
269
|
+
212: "RearSeatHeaters",
|
|
270
|
+
253: "SunroofInstalled",
|
|
271
|
+
258: "MilesSinceReset",
|
|
272
|
+
259: "SelfDrivingMilesSinceReset",
|
|
273
|
+
# --- Semi-truck (included for completeness, excluded from presets) ---
|
|
274
|
+
73: "SemitruckTpmsPressureRe1L0",
|
|
275
|
+
74: "SemitruckTpmsPressureRe1L1",
|
|
276
|
+
75: "SemitruckTpmsPressureRe1R0",
|
|
277
|
+
76: "SemitruckTpmsPressureRe1R1",
|
|
278
|
+
77: "SemitruckTpmsPressureRe2L0",
|
|
279
|
+
78: "SemitruckTpmsPressureRe2L1",
|
|
280
|
+
79: "SemitruckTpmsPressureRe2R0",
|
|
281
|
+
80: "SemitruckTpmsPressureRe2R1",
|
|
282
|
+
97: "SemitruckPassengerSeatFoldPosition",
|
|
283
|
+
104: "SemitruckTractorParkBrakeStatus",
|
|
284
|
+
105: "SemitruckTrailerParkBrakeStatus",
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
# Fields that exist in vehicle_data.proto but should be excluded from the
|
|
289
|
+
# "all" preset. Semi-truck fields won't work on consumer vehicles.
|
|
290
|
+
# LifetimeEnergyGainedRegen returns "unsupported_field" on many vehicles.
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
_NON_STREAMABLE_FIELDS: frozenset[str] = frozenset(
|
|
294
|
+
{name for name in FIELD_NAMES.values() if name.startswith("Semitruck")}
|
|
295
|
+
| {
|
|
296
|
+
"LifetimeEnergyGainedRegen", # returns "unsupported_field" on many vehicles
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Fields that require minimum_delta instead of interval_seconds.
|
|
301
|
+
# Tesla's API rejects these if minimum_delta is not explicitly set >= 1.
|
|
302
|
+
_DELTA_FIELDS: frozenset[str] = frozenset(
|
|
303
|
+
{
|
|
304
|
+
"MilesSinceReset",
|
|
305
|
+
"SelfDrivingMilesSinceReset",
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# Preset field configurations
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
DEFAULT_FIELDS: dict[str, dict[str, int]] = {
|
|
314
|
+
"Soc": {"interval_seconds": 10},
|
|
315
|
+
"VehicleSpeed": {"interval_seconds": 1},
|
|
316
|
+
"Location": {"interval_seconds": 5},
|
|
317
|
+
"ChargeState": {"interval_seconds": 10},
|
|
318
|
+
"InsideTemp": {"interval_seconds": 30},
|
|
319
|
+
"OutsideTemp": {"interval_seconds": 60},
|
|
320
|
+
"Odometer": {"interval_seconds": 60},
|
|
321
|
+
"BatteryLevel": {"interval_seconds": 10},
|
|
322
|
+
"Gear": {"interval_seconds": 1},
|
|
323
|
+
"PackVoltage": {"interval_seconds": 10},
|
|
324
|
+
"PackCurrent": {"interval_seconds": 10},
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
PRESETS: dict[str, dict[str, dict[str, int]]] = {
|
|
328
|
+
"default": DEFAULT_FIELDS,
|
|
329
|
+
"driving": {
|
|
330
|
+
"VehicleSpeed": {"interval_seconds": 1},
|
|
331
|
+
"Location": {"interval_seconds": 1},
|
|
332
|
+
"Gear": {"interval_seconds": 1},
|
|
333
|
+
"GpsHeading": {"interval_seconds": 1},
|
|
334
|
+
"Odometer": {"interval_seconds": 10},
|
|
335
|
+
"BatteryLevel": {"interval_seconds": 10},
|
|
336
|
+
"Soc": {"interval_seconds": 10},
|
|
337
|
+
"PackCurrent": {"interval_seconds": 5},
|
|
338
|
+
"PackVoltage": {"interval_seconds": 5},
|
|
339
|
+
"CruiseSetSpeed": {"interval_seconds": 5},
|
|
340
|
+
"LateralAcceleration": {"interval_seconds": 5},
|
|
341
|
+
"LongitudinalAcceleration": {"interval_seconds": 5},
|
|
342
|
+
"BrakePedalPos": {"interval_seconds": 5},
|
|
343
|
+
"PedalPosition": {"interval_seconds": 5},
|
|
344
|
+
},
|
|
345
|
+
"charging": {
|
|
346
|
+
"Soc": {"interval_seconds": 5},
|
|
347
|
+
"BatteryLevel": {"interval_seconds": 5},
|
|
348
|
+
"PackVoltage": {"interval_seconds": 5},
|
|
349
|
+
"PackCurrent": {"interval_seconds": 5},
|
|
350
|
+
"ChargeState": {"interval_seconds": 5},
|
|
351
|
+
"ChargeAmps": {"interval_seconds": 5},
|
|
352
|
+
"ChargerVoltage": {"interval_seconds": 5},
|
|
353
|
+
"ChargerPhases": {"interval_seconds": 30},
|
|
354
|
+
"ACChargingPower": {"interval_seconds": 5},
|
|
355
|
+
"DCChargingPower": {"interval_seconds": 5},
|
|
356
|
+
"TimeToFullCharge": {"interval_seconds": 30},
|
|
357
|
+
"ChargeLimitSoc": {"interval_seconds": 60},
|
|
358
|
+
"ChargePortDoorOpen": {"interval_seconds": 60},
|
|
359
|
+
"BatteryHeaterOn": {"interval_seconds": 30},
|
|
360
|
+
"InsideTemp": {"interval_seconds": 60},
|
|
361
|
+
},
|
|
362
|
+
"climate": {
|
|
363
|
+
"InsideTemp": {"interval_seconds": 10},
|
|
364
|
+
"OutsideTemp": {"interval_seconds": 30},
|
|
365
|
+
"HvacLeftTemperatureRequest": {"interval_seconds": 30},
|
|
366
|
+
"HvacRightTemperatureRequest": {"interval_seconds": 30},
|
|
367
|
+
"HvacPower": {"interval_seconds": 10},
|
|
368
|
+
"HvacFanStatus": {"interval_seconds": 10},
|
|
369
|
+
"SeatHeaterLeft": {"interval_seconds": 30},
|
|
370
|
+
"SeatHeaterRight": {"interval_seconds": 30},
|
|
371
|
+
"HvacSteeringWheelHeatLevel": {"interval_seconds": 30},
|
|
372
|
+
"CabinOverheatProtectionMode": {"interval_seconds": 60},
|
|
373
|
+
"DefrostMode": {"interval_seconds": 30},
|
|
374
|
+
"PreconditioningEnabled": {"interval_seconds": 30},
|
|
375
|
+
},
|
|
376
|
+
"all": {
|
|
377
|
+
name: (
|
|
378
|
+
{"interval_seconds": 30, "minimum_delta": 1}
|
|
379
|
+
if name in _DELTA_FIELDS
|
|
380
|
+
else {"interval_seconds": 30}
|
|
381
|
+
)
|
|
382
|
+
for name in FIELD_NAMES.values()
|
|
383
|
+
if name not in _NON_STREAMABLE_FIELDS
|
|
384
|
+
},
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
# Reverse lookup: name → ID
|
|
388
|
+
_NAME_TO_ID: dict[str, int] = {v: k for k, v in FIELD_NAMES.items()}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def resolve_fields(
|
|
392
|
+
spec: str,
|
|
393
|
+
interval_override: int | None = None,
|
|
394
|
+
) -> dict[str, dict[str, int]]:
|
|
395
|
+
"""Resolve a ``--fields`` argument to a field configuration dict.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
spec: A preset name (e.g. ``"default"``, ``"charging"``) or a
|
|
399
|
+
comma-separated list of field names (e.g. ``"Soc,VehicleSpeed"``).
|
|
400
|
+
interval_override: If set, overrides ``interval_seconds`` for all fields.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
A dict mapping field names to ``{"interval_seconds": N}``.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
ConfigError: If a field name or preset is unrecognized.
|
|
407
|
+
"""
|
|
408
|
+
if spec in PRESETS:
|
|
409
|
+
fields = dict(PRESETS[spec])
|
|
410
|
+
else:
|
|
411
|
+
# Comma-separated field names
|
|
412
|
+
fields = {}
|
|
413
|
+
for name in spec.split(","):
|
|
414
|
+
name = name.strip()
|
|
415
|
+
if not name:
|
|
416
|
+
continue
|
|
417
|
+
if name not in _NAME_TO_ID:
|
|
418
|
+
raise ConfigError(
|
|
419
|
+
f"Unknown telemetry field: '{name}'. "
|
|
420
|
+
f"Available presets: {', '.join(sorted(PRESETS.keys()))}"
|
|
421
|
+
)
|
|
422
|
+
fields[name] = {"interval_seconds": 10} # reasonable default
|
|
423
|
+
|
|
424
|
+
if interval_override is not None:
|
|
425
|
+
fields = {name: {"interval_seconds": interval_override} for name in fields}
|
|
426
|
+
|
|
427
|
+
return fields
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Minimal Flatbuffers reader for Tesla Fleet Telemetry envelopes.
|
|
2
|
+
|
|
3
|
+
Tesla vehicles send telemetry data wrapped in a Flatbuffers envelope:
|
|
4
|
+
|
|
5
|
+
WebSocket binary frame
|
|
6
|
+
└─ FlatbuffersEnvelope
|
|
7
|
+
├── txid (bytes)
|
|
8
|
+
├── topic (bytes) — "V", "alerts", "errors", "connectivity"
|
|
9
|
+
├── messageType (uint8)
|
|
10
|
+
├── messageId (bytes)
|
|
11
|
+
└── FlatbuffersStream (union)
|
|
12
|
+
├── createdAt (uint32) — epoch seconds
|
|
13
|
+
├── payload (bytes) — protobuf content
|
|
14
|
+
├── deviceId (bytes) — VIN
|
|
15
|
+
└── ...
|
|
16
|
+
|
|
17
|
+
This module provides a zero-dependency reader for these two tables.
|
|
18
|
+
The protobuf payload inside ``FlatbuffersStream.payload`` is decoded
|
|
19
|
+
separately by :mod:`tescmd.telemetry.decoder`.
|
|
20
|
+
|
|
21
|
+
Schema source:
|
|
22
|
+
https://github.com/teslamotors/fleet-telemetry/tree/main/messages/tesla
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import struct
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class StreamMessage:
|
|
33
|
+
"""Unwrapped Fleet Telemetry envelope."""
|
|
34
|
+
|
|
35
|
+
topic: bytes
|
|
36
|
+
device_id: bytes # VIN
|
|
37
|
+
created_at: int # epoch seconds
|
|
38
|
+
payload: bytes # protobuf content
|
|
39
|
+
txid: bytes = b""
|
|
40
|
+
message_id: bytes = b""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse_envelope(buf: bytes) -> StreamMessage:
|
|
44
|
+
"""Parse a FlatbuffersEnvelope from raw WebSocket bytes.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If the buffer is too small or structurally invalid.
|
|
48
|
+
"""
|
|
49
|
+
if len(buf) < 8:
|
|
50
|
+
raise ValueError(f"Buffer too small for Flatbuffers envelope ({len(buf)} bytes)")
|
|
51
|
+
|
|
52
|
+
# Root table offset (uint32 LE at byte 0)
|
|
53
|
+
root_pos = _read_u32(buf, 0)
|
|
54
|
+
if root_pos >= len(buf):
|
|
55
|
+
raise ValueError(f"Root offset {root_pos} exceeds buffer size {len(buf)}")
|
|
56
|
+
|
|
57
|
+
# Envelope vtable
|
|
58
|
+
vtable_pos = _vtable_pos(buf, root_pos)
|
|
59
|
+
|
|
60
|
+
# Envelope fields (vtable offsets: txid=4, topic=6, messageType=8, message=10, messageId=12)
|
|
61
|
+
txid = _read_vector(buf, root_pos, vtable_pos, vtable_offset=4)
|
|
62
|
+
topic = _read_vector(buf, root_pos, vtable_pos, vtable_offset=6)
|
|
63
|
+
message_id = _read_vector(buf, root_pos, vtable_pos, vtable_offset=12)
|
|
64
|
+
|
|
65
|
+
# Navigate to FlatbuffersStream (union at vtable_offset=10)
|
|
66
|
+
stream_pos = _read_indirect(buf, root_pos, vtable_pos, vtable_offset=10)
|
|
67
|
+
if stream_pos is None:
|
|
68
|
+
raise ValueError("FlatbuffersEnvelope has no message field")
|
|
69
|
+
|
|
70
|
+
# Stream vtable
|
|
71
|
+
stream_vtable = _vtable_pos(buf, stream_pos)
|
|
72
|
+
|
|
73
|
+
# Stream fields (vtable offsets: createdAt=4, senderId=6, payload=8,
|
|
74
|
+
# deviceType=10, deviceId=12, deliveredAtEpochMs=14)
|
|
75
|
+
created_at = _read_u32_field(buf, stream_pos, stream_vtable, vtable_offset=4)
|
|
76
|
+
payload = _read_vector(buf, stream_pos, stream_vtable, vtable_offset=8)
|
|
77
|
+
device_id = _read_vector(buf, stream_pos, stream_vtable, vtable_offset=12)
|
|
78
|
+
|
|
79
|
+
return StreamMessage(
|
|
80
|
+
topic=topic,
|
|
81
|
+
device_id=device_id,
|
|
82
|
+
created_at=created_at,
|
|
83
|
+
payload=payload,
|
|
84
|
+
txid=txid,
|
|
85
|
+
message_id=message_id,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Low-level Flatbuffers primitives
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _read_u16(buf: bytes, pos: int) -> int:
|
|
95
|
+
result: int = struct.unpack_from("<H", buf, pos)[0]
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _read_u32(buf: bytes, pos: int) -> int:
|
|
100
|
+
result: int = struct.unpack_from("<I", buf, pos)[0]
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _read_i32(buf: bytes, pos: int) -> int:
|
|
105
|
+
result: int = struct.unpack_from("<i", buf, pos)[0]
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _vtable_pos(buf: bytes, table_pos: int) -> int:
|
|
110
|
+
"""Compute vtable position from a table position.
|
|
111
|
+
|
|
112
|
+
The first 4 bytes of a Flatbuffers table are a signed offset (soffset32)
|
|
113
|
+
pointing back to its vtable.
|
|
114
|
+
"""
|
|
115
|
+
soffset = _read_i32(buf, table_pos)
|
|
116
|
+
return table_pos - soffset
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _field_offset(buf: bytes, vtable_pos: int, vtable_offset: int) -> int:
|
|
120
|
+
"""Read a field's offset from the vtable.
|
|
121
|
+
|
|
122
|
+
Returns 0 if the field is absent (vtable_offset beyond vtable size
|
|
123
|
+
or stored offset is 0).
|
|
124
|
+
"""
|
|
125
|
+
vtable_size = _read_u16(buf, vtable_pos)
|
|
126
|
+
if vtable_offset >= vtable_size:
|
|
127
|
+
return 0
|
|
128
|
+
return _read_u16(buf, vtable_pos + vtable_offset)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _read_vector(buf: bytes, table_pos: int, vtable_pos: int, *, vtable_offset: int) -> bytes:
|
|
132
|
+
"""Read a byte-vector field from a Flatbuffers table."""
|
|
133
|
+
voff = _field_offset(buf, vtable_pos, vtable_offset)
|
|
134
|
+
if voff == 0:
|
|
135
|
+
return b""
|
|
136
|
+
# Follow indirect offset to vector
|
|
137
|
+
field_pos = table_pos + voff
|
|
138
|
+
vec_pos = field_pos + _read_u32(buf, field_pos)
|
|
139
|
+
length = _read_u32(buf, vec_pos)
|
|
140
|
+
return bytes(buf[vec_pos + 4 : vec_pos + 4 + length])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _read_u32_field(buf: bytes, table_pos: int, vtable_pos: int, *, vtable_offset: int) -> int:
|
|
144
|
+
"""Read a uint32 scalar field from a Flatbuffers table."""
|
|
145
|
+
voff = _field_offset(buf, vtable_pos, vtable_offset)
|
|
146
|
+
if voff == 0:
|
|
147
|
+
return 0
|
|
148
|
+
return _read_u32(buf, table_pos + voff)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _read_indirect(
|
|
152
|
+
buf: bytes, table_pos: int, vtable_pos: int, *, vtable_offset: int
|
|
153
|
+
) -> int | None:
|
|
154
|
+
"""Follow an offset field to a child table (used for unions).
|
|
155
|
+
|
|
156
|
+
Returns the child table position, or ``None`` if absent.
|
|
157
|
+
"""
|
|
158
|
+
voff = _field_offset(buf, vtable_pos, vtable_offset)
|
|
159
|
+
if voff == 0:
|
|
160
|
+
return None
|
|
161
|
+
field_pos = table_pos + voff
|
|
162
|
+
return field_pos + _read_u32(buf, field_pos)
|