rivian-python-client 2.2.0b11__py3-none-any.whl → 2.3.0__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.
Files changed (45) hide show
  1. rivian/__version__.py +1 -1
  2. rivian/const.py +1 -27
  3. rivian/parallax/__init__.py +13 -0
  4. rivian/parallax/body.py +3 -3
  5. rivian/parallax/charging.py +3 -1
  6. rivian/parallax/comfort.py +22 -16
  7. rivian/parallax/departure.py +21 -21
  8. rivian/parallax/device_table.py +24 -6
  9. rivian/parallax/dynamics.py +14 -8
  10. rivian/parallax/holiday_celebration.py +42 -9
  11. rivian/parallax/navigation.py +4 -4
  12. rivian/parallax/operation.py +39 -0
  13. rivian/parallax/ota.py +17 -18
  14. rivian/parallax/proto/body.proto +10 -9
  15. rivian/parallax/proto/body_pb2.py +49 -49
  16. rivian/parallax/proto/charging.proto +5 -0
  17. rivian/parallax/proto/charging_pb2.py +22 -22
  18. rivian/parallax/proto/charging_pb2.pyi +4 -2
  19. rivian/parallax/proto/comfort.proto +1 -1
  20. rivian/parallax/proto/device_table.proto +43 -8
  21. rivian/parallax/proto/device_table_pb2.py +21 -13
  22. rivian/parallax/proto/device_table_pb2.pyi +54 -4
  23. rivian/parallax/proto/dynamics.proto +1 -1
  24. rivian/parallax/proto/dynamics_pb2.py +27 -27
  25. rivian/parallax/proto/dynamics_pb2.pyi +4 -4
  26. rivian/parallax/proto/holiday_celebration.proto +19 -6
  27. rivian/parallax/proto/holiday_celebration_pb2.py +22 -20
  28. rivian/parallax/proto/holiday_celebration_pb2.pyi +19 -4
  29. rivian/parallax/proto/navigation.proto +2 -2
  30. rivian/parallax/proto/navigation_pb2.py +1 -1
  31. rivian/parallax/proto/navigation_pb2.pyi +4 -4
  32. rivian/parallax/proto/operation.proto +31 -0
  33. rivian/parallax/proto/operation_pb2.py +44 -0
  34. rivian/parallax/proto/operation_pb2.pyi +45 -0
  35. rivian/parallax/proto/ota.proto +4 -4
  36. rivian/parallax/proto/ota_pb2.py +41 -41
  37. rivian/parallax/proto/ota_pb2.pyi +6 -6
  38. rivian/parallax/proto/vehicle.proto +2 -2
  39. rivian/rivian.py +261 -63
  40. rivian/schemas/charging.graphql +0 -21
  41. rivian/utils.py +2 -2
  42. rivian/ws_monitor.py +37 -22
  43. {rivian_python_client-2.2.0b11.dist-info → rivian_python_client-2.3.0.dist-info}/METADATA +28 -4
  44. {rivian_python_client-2.2.0b11.dist-info → rivian_python_client-2.3.0.dist-info}/RECORD +45 -41
  45. {rivian_python_client-2.2.0b11.dist-info → rivian_python_client-2.3.0.dist-info}/WHEEL +0 -0
rivian/__version__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '2.2.0b11'
4
+ __version__ = VERSION = '2.3.0'
rivian/const.py CHANGED
@@ -2,34 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import sys
5
+ from enum import StrEnum
6
6
  from typing import Final
7
7
 
8
- if sys.version_info >= (3, 11):
9
- from enum import StrEnum
10
- else:
11
- from backports.strenum import StrEnum
12
-
13
- LIVE_SESSION_PROPERTIES: Final[set[str]] = {
14
- "chargerId",
15
- "current",
16
- "currentCurrency",
17
- "currentMiles",
18
- "currentPrice",
19
- "isFreeSession",
20
- "isRivianCharger",
21
- "kilometersChargedPerHour",
22
- "locationId",
23
- "power",
24
- "rangeAddedThisSession",
25
- "soc",
26
- "startTime",
27
- "timeElapsed",
28
- "timeRemaining",
29
- "totalChargedEnergy",
30
- "vehicleChargerState",
31
- }
32
-
33
8
  VEHICLE_STATE_PROPERTIES: Final[set[str]] = {
34
9
  # VehicleCloudConnection
35
10
  "cloudConnection",
@@ -38,7 +13,6 @@ VEHICLE_STATE_PROPERTIES: Final[set[str]] = {
38
13
  "gnssLocation",
39
14
  "gnssError",
40
15
  # TimeStamped(String|[Nullable]Float|Int)
41
- "activeDriverName",
42
16
  "alarmSoundStatus",
43
17
  "batteryCapacity",
44
18
  "batteryCellType",
@@ -7,6 +7,9 @@ Public API:
7
7
  Same, for a raw `subscribe_for_parallax_messages` message.
8
8
  PARALLAX_RVMS: list[str] — every RVM with a registered decoder
9
9
  CHARGING_RVMS: list[str] — the subset relevant to a charging session
10
+ encode_vehicle_operation(rvm, payload=b"", ...) -> str
11
+ Builds a `send_vehicle_operation` request; an empty payload reads
12
+ the RVM, anything else (e.g. `encode_climate_hold_setting`) writes it.
10
13
 
11
14
  Layout:
12
15
  core.py - registry, payload parsing and shared helpers
@@ -41,11 +44,17 @@ from . import ( # noqa: F401 (imported to register their decoders)
41
44
  vehicle,
42
45
  vehicle_access,
43
46
  )
47
+ from .comfort import (
48
+ CLIMATE_HOLD_SETTING_RVM,
49
+ CLIMATE_HOLD_STATUS_RVM,
50
+ encode_climate_hold_setting,
51
+ )
44
52
  from .core import (
45
53
  RVMDecoder,
46
54
  decode_parallax_message,
47
55
  decode_parallax_subscription_message,
48
56
  )
57
+ from .operation import encode_vehicle_operation
49
58
 
50
59
  PARALLAX_RVMS: list[str] = list(RVMDecoder.decoders)
51
60
 
@@ -64,7 +73,11 @@ CHARGING_RVMS: list[str] = [
64
73
 
65
74
  __all__ = [
66
75
  "CHARGING_RVMS",
76
+ "CLIMATE_HOLD_SETTING_RVM",
77
+ "CLIMATE_HOLD_STATUS_RVM",
67
78
  "PARALLAX_RVMS",
68
79
  "decode_parallax_message",
69
80
  "decode_parallax_subscription_message",
81
+ "encode_climate_hold_setting",
82
+ "encode_vehicle_operation",
70
83
  ]
rivian/parallax/body.py CHANGED
@@ -126,8 +126,8 @@ _WINDOW_MAP: Final[dict[int, str]] = {
126
126
 
127
127
  # GraphQL's windowXCalibrated values.
128
128
  _CALIBRATION_MAP: Final[dict[int, str]] = {
129
- body_pb2.CALIBRATION_STATUS_CALIBRATED: "Calibrated",
130
- body_pb2.CALIBRATION_STATUS_NOT_CALIBRATED: "Not_Calibrated",
129
+ body_pb2.CALIBRATION_STATUS_CALIBRATED: "calibrated",
130
+ body_pb2.CALIBRATION_STATUS_NOT_CALIBRATED: "not_calibrated",
131
131
  }
132
132
 
133
133
  _TRAILER_PRESENCE_MAP: Final[dict[int, str]] = {
@@ -204,7 +204,7 @@ def decode_windows(m: body_pb2.WindowsState) -> dict[str, Any]:
204
204
  Fields:
205
205
  windowFrontLeftCalibrated, windowFrontRightCalibrated,
206
206
  windowRearLeftCalibrated, windowRearRightCalibrated,
207
- windowRearCalibrated: str ("Calibrated" | "Not_Calibrated")
207
+ windowRearCalibrated: str ("calibrated" | "not_calibrated")
208
208
  """
209
209
  return {
210
210
  _WINDOW_MAP[w.instance]: _enum(
@@ -374,7 +374,7 @@ def decode_energy_state(m: charging_pb2.EnergyState) -> dict[str, Any]:
374
374
  energyChargerState, energyConnectionState: str — as
375
375
  charging.session.status's chargerState / connectionState, which
376
376
  these can lag
377
- _field3: int — raw
377
+ _field3, _field8: int — raw
378
378
  """
379
379
  result: dict[str, Any] = {}
380
380
  if (v := _present(m, "charging_state")) is not None:
@@ -389,6 +389,8 @@ def decode_energy_state(m: charging_pb2.EnergyState) -> dict[str, Any]:
389
389
  result["chargerStatus"] = _enum(_CHARGER_STATUS_MAP, v, what="charger status")
390
390
  if (v := _present(m, "field_3")) is not None:
391
391
  result["_field3"] = v
392
+ if (v := _present(m, "field_8")) is not None:
393
+ result["_field8"] = v
392
394
  return result
393
395
 
394
396
 
@@ -7,6 +7,9 @@ from typing import Any, Final
7
7
  from .core import _LOGGER, RVMDecoder, _enum, _present
8
8
  from .proto import comfort_pb2
9
9
 
10
+ CLIMATE_HOLD_SETTING_RVM: Final = "comfort.cabin.climate_hold_setting"
11
+ CLIMATE_HOLD_STATUS_RVM: Final = "comfort.cabin.climate_hold_status"
12
+
10
13
 
11
14
  @RVMDecoder.register(
12
15
  "comfort.cabin.hvac_settings_status", comfort_pb2.HvacSettingsStatus
@@ -63,10 +66,10 @@ _CLIMATE_HOLD_UNAVAILABILITY_REASON_MAP: Final[dict[int, str]] = {
63
66
  }
64
67
 
65
68
  _DEFROST_MAP: Final[dict[int, str]] = {
66
- comfort_pb2.DEFROST_DEFOG: "Defog",
67
- comfort_pb2.DEFROST_ACTIVE: "Defrost",
68
- comfort_pb2.DEFROST_DEFOG_DEFROST: "Defog_Defrost",
69
- comfort_pb2.DEFROST_OFF: "Off",
69
+ comfort_pb2.DEFROST_DEFOG: "defog",
70
+ comfort_pb2.DEFROST_ACTIVE: "defrost",
71
+ comfort_pb2.DEFROST_DEFOG_DEFROST: "defog_defrost",
72
+ comfort_pb2.DEFROST_OFF: "off",
70
73
  }
71
74
 
72
75
  # GraphQL's cabinPreconditioningStatus / cabinPreconditioningType values.
@@ -132,10 +135,10 @@ _CABIN_SURFACE_MAP: Final[dict[int, str]] = {
132
135
  }
133
136
 
134
137
  _CONDITIONING_LEVEL_MAP: Final[dict[int | None, str]] = {
135
- None: "Off",
136
- comfort_pb2.CONDITIONING_LEVEL_1: "Level_1",
137
- comfort_pb2.CONDITIONING_LEVEL_2: "Level_2",
138
- comfort_pb2.CONDITIONING_LEVEL_3: "Level_3",
138
+ None: "off",
139
+ comfort_pb2.CONDITIONING_LEVEL_1: "level_1",
140
+ comfort_pb2.CONDITIONING_LEVEL_2: "level_2",
141
+ comfort_pb2.CONDITIONING_LEVEL_3: "level_3",
139
142
  }
140
143
 
141
144
  _CONDITIONING_TYPE_MAP: Final[dict[int, str]] = {
@@ -200,9 +203,12 @@ def decode_cabin_ventilation(m: comfort_pb2.CabinVentilationSetting) -> dict[str
200
203
  return {"cabinVentilationEnabled": m.auto_cabin_ventilation_enabled}
201
204
 
202
205
 
203
- @RVMDecoder.register(
204
- "comfort.cabin.climate_hold_setting", comfort_pb2.ClimateHoldSetting
205
- )
206
+ def encode_climate_hold_setting(duration_seconds: int) -> bytes:
207
+ """Encode a comfort.cabin.climate_hold_setting payload."""
208
+ return comfort_pb2.ClimateHoldSetting(duration=duration_seconds).SerializeToString()
209
+
210
+
211
+ @RVMDecoder.register(CLIMATE_HOLD_SETTING_RVM, comfort_pb2.ClimateHoldSetting)
206
212
  def decode_climate_hold_setting(m: comfort_pb2.ClimateHoldSetting) -> dict[str, Any]:
207
213
  """comfort.cabin.climate_hold_setting — configured climate-hold duration.
208
214
 
@@ -212,7 +218,7 @@ def decode_climate_hold_setting(m: comfort_pb2.ClimateHoldSetting) -> dict[str,
212
218
  return {"climateHoldDuration": m.duration}
213
219
 
214
220
 
215
- @RVMDecoder.register("comfort.cabin.climate_hold_status", comfort_pb2.ClimateHoldStatus)
221
+ @RVMDecoder.register(CLIMATE_HOLD_STATUS_RVM, comfort_pb2.ClimateHoldStatus)
216
222
  def decode_climate_hold_status(m: comfort_pb2.ClimateHoldStatus) -> dict[str, Any]:
217
223
  """comfort.cabin.climate_hold_status — current climate-hold status.
218
224
 
@@ -248,8 +254,8 @@ def decode_defrost_status(m: comfort_pb2.DefrostDefogStatus) -> dict[str, Any]:
248
254
  """comfort.cabin.defrost_defog_status — windshield defrost state.
249
255
 
250
256
  Fields:
251
- defrostDefogStatus: str | None ("Defog" | "Defrost" | "Defog_Defrost"
252
- | "Off")
257
+ defrostDefogStatus: str | None ("defog" | "defrost" | "defog_defrost"
258
+ | "off")
253
259
  """
254
260
  return {
255
261
  "defrostDefogStatus": _enum(
@@ -293,8 +299,8 @@ def decode_seat_conditioning(m: comfort_pb2.SeatConditioningStatus) -> dict[str,
293
299
  seatFrontLeftHeat, seatFrontLeftVent, seatFrontRightHeat,
294
300
  seatFrontRightVent, seatRearLeftHeat, seatRearRightHeat,
295
301
  seatThirdRowLeftHeat, seatThirdRowRightHeat: str
296
- ("Off" | "Level_1" | "Level_2" | "Level_3")
297
- steeringWheelHeat: str ("Off" | "Level_1")
302
+ ("off" | "level_1" | "level_2" | "level_3")
303
+ steeringWheelHeat: str ("off" | "level_1")
298
304
  plus <surface>Heat/Vent for any other surface sent (middle seats,
299
305
  rearGlass, frontGlass, sideviewMirrors, wiperArea)
300
306
 
@@ -12,28 +12,28 @@ _Schedules = departure_pb2.DepartureSchedules
12
12
 
13
13
  # Days field -> gateway WeekDay value.
14
14
  _WEEKDAYS = (
15
- ("monday", "Monday"),
16
- ("tuesday", "Tuesday"),
17
- ("wednesday", "Wednesday"),
18
- ("thursday", "Thursday"),
19
- ("friday", "Friday"),
20
- ("saturday", "Saturday"),
21
- ("sunday", "Sunday"),
15
+ ("monday", "monday"),
16
+ ("tuesday", "tuesday"),
17
+ ("wednesday", "wednesday"),
18
+ ("thursday", "thursday"),
19
+ ("friday", "friday"),
20
+ ("saturday", "saturday"),
21
+ ("sunday", "sunday"),
22
22
  )
23
23
 
24
24
  _DEFOG_DEFROST = {
25
- departure_pb2.DEFOG_DEFROST_OFF: "Off",
26
- departure_pb2.DEFOG_DEFROST_DEFOG: "Defog",
27
- departure_pb2.DEFOG_DEFROST_DEFROST: "Defrost",
25
+ departure_pb2.DEFOG_DEFROST_OFF: "off",
26
+ departure_pb2.DEFOG_DEFROST_DEFOG: "defog",
27
+ departure_pb2.DEFOG_DEFROST_DEFROST: "defrost",
28
28
  }
29
29
  _SURFACE_LEVEL = {
30
- departure_pb2.SURFACE_OFF: "Off",
31
- departure_pb2.SURFACE_HEAT_1: "Heat1",
32
- departure_pb2.SURFACE_HEAT_2: "Heat2",
33
- departure_pb2.SURFACE_HEAT_3: "Heat3",
34
- departure_pb2.SURFACE_VENT_1: "Vent1",
35
- departure_pb2.SURFACE_VENT_2: "Vent2",
36
- departure_pb2.SURFACE_VENT_3: "Vent3",
30
+ departure_pb2.SURFACE_OFF: "off",
31
+ departure_pb2.SURFACE_HEAT_1: "heat_1",
32
+ departure_pb2.SURFACE_HEAT_2: "heat_2",
33
+ departure_pb2.SURFACE_HEAT_3: "heat_3",
34
+ departure_pb2.SURFACE_VENT_1: "vent_1",
35
+ departure_pb2.SURFACE_VENT_2: "vent_2",
36
+ departure_pb2.SURFACE_VENT_3: "vent_3",
37
37
  }
38
38
  _SURFACES = (
39
39
  ("front_left_seat", "frontLeftSeat"),
@@ -86,14 +86,14 @@ def decode_departure_schedules(m: departure_pb2.DepartureSchedules) -> dict[str,
86
86
  Fields:
87
87
  departureSchedules: list[dict] — the gateway's DepartureSchedule shape:
88
88
  id: str, name: str, isEnabled: bool
89
- occurrence: {"days": list[str] ("Monday" ... "Sunday"; order
89
+ occurrence: {"days": list[str] ("monday" ... "sunday"; order
90
90
  inferred), "startsAtMin": int (minutes after local midnight)}
91
91
  departureSettings: {"shouldOverrideChargeSchedule": bool,
92
92
  "comfortSettings": {"cabinTempCelsius": float,
93
- "frontDefogDefrost": "Off" | "Defog" | "Defrost",
93
+ "frontDefogDefrost": "off" | "defog" | "defrost",
94
94
  "surfaceHeatVentLevels": {frontLeftSeat, frontRightSeat,
95
- rearLeftSeat, rearRightSeat, steeringWheel: "Off" |
96
- "Heat1" ... "Vent3"}}}
95
+ rearLeftSeat, rearRightSeat, steeringWheel: "off" |
96
+ "heat_1" ... "vent_3"}}}
97
97
  departureSchedulesUpdatedAt: datetime
98
98
  """
99
99
  result: dict[str, Any] = {
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import uuid
6
6
  from typing import Any, Final
7
7
 
8
+ from ..utils import from_epoch
8
9
  from .core import RVMDecoder, _enum, _present
9
10
  from .proto import device_table_pb2
10
11
 
@@ -59,9 +60,14 @@ def decode_vas_keyper_devices(m: device_table_pb2.VasKeyperDevices) -> dict[str,
59
60
  infoC: int — 1 while active, 2147483647 while inactive, else the
60
61
  pairing attempt number
61
62
  infoA, infoB: int — unknown
63
+ keyPermissions: int — raw
64
+ keyCreatedAt, keyExpiresAt: datetime — when sent and non-zero
65
+ keyMaterial: str — which key material is set ("owner" | "friend" |
66
+ "card" | "fob" | "phone" | "fob2")
62
67
  deviceId: str — hex; for a key card, the GraphQL `devices[].id`
68
+ nfcDeviceId: str — hex; fob2's NFC id (deviceId is its BLE id)
63
69
  phoneId: str — the phone's UUID; phone keys
64
- credentialHex: str — hex; likely key material
70
+ credentialHex: str — hex; the wrapped shared secret
65
71
  deviceOem: str ("unknown" | "rivian" | "apple" | "google" |
66
72
  "samsung" | "vw") — the device maker
67
73
  """
@@ -100,17 +106,29 @@ def decode_vas_keyper_devices(m: device_table_pb2.VasKeyperDevices) -> dict[str,
100
106
  result[key] = v
101
107
  if (v := _present(credentials.info, "status")) is not None:
102
108
  result["keyStatus"] = _enum(_KEY_STATUS_MAP, v, what="key status")
103
- for field in ("card", "fob", "phone"):
104
- if not credentials.HasField(field):
105
- continue
109
+ if (v := _present(credentials.info, "permissions")) is not None:
110
+ result["keyPermissions"] = v
111
+ for field, key in (
112
+ ("created_at", "keyCreatedAt"),
113
+ ("expires_at", "keyExpiresAt"),
114
+ ):
115
+ if v := _present(credentials.info, field):
116
+ result[key] = from_epoch(v)
117
+ if (field := credentials.WhichOneof("material")) is not None:
118
+ result["keyMaterial"] = field
106
119
  material = getattr(credentials, field)
107
- if (ident := _present(material, "id")) is not None:
120
+ id_field, cred_field = (
121
+ ("ble_id", "ble_credential") if field == "fob2" else ("id", "credential")
122
+ )
123
+ if (ident := _present(material, id_field)) is not None:
108
124
  if field == "phone" and len(ident) == 16:
109
125
  result["phoneId"] = str(uuid.UUID(bytes=ident))
110
126
  else:
111
127
  result["deviceId"] = ident.hex()
112
- if (cred := _present(material, "credential")) is not None:
128
+ if (cred := _present(material, cred_field)) is not None:
113
129
  result["credentialHex"] = cred.hex()
130
+ if field == "fob2" and (ident := _present(material, "nfc_id")) is not None:
131
+ result["nfcDeviceId"] = ident.hex()
114
132
  if (v := _present(credentials, "key_type")) is not None:
115
133
  result["keyType"] = _enum(_KEY_TYPE_MAP, v, what="key type")
116
134
  if (v := _present(credentials, "device_oem")) is not None:
@@ -53,9 +53,8 @@ _TEMPERATURE_IMPACT_MAP: Final[dict[int, str]] = {
53
53
  dynamics_pb2.TEMPERATURE_COLD_IMPACT: "cold_impact",
54
54
  }
55
55
 
56
- # "OK" is the GraphQL value for normal.
57
56
  _TIRE_PRESSURE_STATUS_MAP: Final[dict[int, str]] = {
58
- dynamics_pb2.TIRE_PRESSURE_STATUS_NORMAL: "OK",
57
+ dynamics_pb2.TIRE_PRESSURE_STATUS_NORMAL: "ok",
59
58
  dynamics_pb2.TIRE_PRESSURE_STATUS_WARNING_HARD: "warning_hard",
60
59
  dynamics_pb2.TIRE_PRESSURE_STATUS_WARNING_SOFT: "warning_soft",
61
60
  dynamics_pb2.TIRE_PRESSURE_STATUS_WARNING_PUNCTURE: "warning_puncture",
@@ -195,15 +194,21 @@ def decode_range(m: dynamics_pb2.Range) -> dict[str, Any]:
195
194
  }
196
195
 
197
196
 
197
+ # The app's enum class for tire validity was stripped; only 0 and 1 are known.
198
+ _TIRE_VALID: Final = 0
199
+ _TIRE_VALIDITY_MAP: Final[dict[int, str]] = {_TIRE_VALID: "valid", 1: "invalid"}
200
+
201
+
198
202
  @RVMDecoder.register("dynamics.tires.state", dynamics_pb2.TiresState)
199
203
  def decode_tires(m: dynamics_pb2.TiresState) -> dict[str, Any]:
200
204
  """dynamics.tires.state — per-tire pressure, status and validity.
201
205
 
202
206
  Fields:
203
207
  tirePressureFrontLeft, tirePressureFrontRight, etc.: float (bar)
204
- tirePressureStatusFrontLeft, etc.: str ("OK" | "warning_hard" |
208
+ tirePressureStatusFrontLeft, etc.: str ("ok" | "warning_hard" |
205
209
  "warning_soft" | "warning_puncture")
206
- tirePressureStatusValidFrontLeft, etc.: str ("valid" | "invalid")
210
+ tirePressureStatusValidFrontLeft, etc.: str ("valid" | "invalid"),
211
+ or the raw int for an unmapped validity
207
212
  """
208
213
  result: dict[str, Any] = {}
209
214
  for s in m.tire:
@@ -212,10 +217,11 @@ def decode_tires(m: dynamics_pb2.TiresState) -> dict[str, Any]:
212
217
  continue
213
218
  suffix = _TIRE_POSITION_MAP[s.pos]
214
219
 
215
- # Pressure and status are stale while invalid.
216
- valid = not s.invalid
217
- result[f"tirePressureStatusValid{suffix}"] = "valid" if valid else "invalid"
218
- if not valid:
220
+ # Pressure and status are stale unless valid.
221
+ result[f"tirePressureStatusValid{suffix}"] = _enum(
222
+ _TIRE_VALIDITY_MAP, s.validity, what="tire validity"
223
+ )
224
+ if s.validity != _TIRE_VALID:
219
225
  continue
220
226
 
221
227
  if (pressure := _present(s, "pressure")) is not None:
@@ -14,11 +14,34 @@ from .proto import holiday_celebration_pb2
14
14
  holiday_celebration_pb2.HolidayCelebrationEnabled,
15
15
  )
16
16
  def decode_holiday_celebration_enabled(
17
- _m: holiday_celebration_pb2.HolidayCelebrationEnabled,
17
+ m: holiday_celebration_pb2.HolidayCelebrationEnabled,
18
18
  ) -> dict[str, Any]:
19
- """holiday_celebration.car_costume.holiday_celebration_enabled — unmapped."""
20
- return {}
19
+ """holiday_celebration.car_costume.holiday_celebration_enabled.
21
20
 
21
+ The names are inferred; the app has no class for this topic.
22
+
23
+ Fields:
24
+ holidayCelebrationAvailable: bool — the (seasonal) feature is offered
25
+ holidayCelebrationEnabled: bool — the user's on/off switch
26
+ """
27
+ return {
28
+ "holidayCelebrationAvailable": m.available,
29
+ "holidayCelebrationEnabled": m.enabled,
30
+ }
31
+
32
+
33
+ # Best guess; 2 hasn't been seen.
34
+ _CAR_COSTUME_AVAILABILITY_MAP: Final[dict[int, str]] = {
35
+ 1: "available",
36
+ 3: "unavailable",
37
+ }
38
+
39
+
40
+ _COSTUME_THEME_MAP: Final[dict[int, str]] = {
41
+ holiday_celebration_pb2.CAR_COSTUME_THEME_NONE: "none",
42
+ holiday_celebration_pb2.CAR_COSTUME_THEME_GHOSTBUSTERS: "ghostbusters",
43
+ holiday_celebration_pb2.CAR_COSTUME_THEME_GHOSTBUSTERS_DISPLAY: "ghostbusters_display",
44
+ }
22
45
 
23
46
  _COSTUME_EFFECT_TRIGGER_MAP: Final[dict[int, str]] = {
24
47
  holiday_celebration_pb2.COSTUME_EFFECT_TRIGGER_MANUAL: "manual",
@@ -45,7 +68,7 @@ def decode_car_costume_settings(
45
68
  costumeInteriorLightShowEnabled: bool
46
69
  costumeInteriorOverheadLightsEnabled: bool
47
70
  costumeLightsColor: int
48
- costumeEffect: int
71
+ costumeEffect: int (1 none, 3 the display show; inferred)
49
72
  costumeEffectTrigger: str ("manual" | "motion"), None when unset
50
73
  """
51
74
  return {
@@ -80,15 +103,25 @@ def decode_car_costume_state(
80
103
  Names come from the app; the int values are unmapped enums.
81
104
 
82
105
  Fields:
83
- carCostumeAvailability: int
84
- costumeTheme: int
106
+ carCostumeAvailability: str | int ("available" | "unavailable"; a
107
+ best guess), the raw int when unmapped
108
+ costumeTheme: str | None ("none" | "ghostbusters" |
109
+ "ghostbusters_display"; inferred), None when unset
85
110
  costumeMotionTriggerDetected: bool
86
- activeCostumeEffect: int
111
+ activeCostumeEffect: int (1 none, 3 the display show; inferred)
87
112
  costumeStartTime: datetime, only when sent
88
113
  """
89
114
  result: dict[str, Any] = {
90
- "carCostumeAvailability": m.car_costume_availability,
91
- "costumeTheme": m.costume_theme,
115
+ "carCostumeAvailability": _enum(
116
+ _CAR_COSTUME_AVAILABILITY_MAP,
117
+ m.car_costume_availability,
118
+ what="car costume availability",
119
+ ),
120
+ "costumeTheme": (
121
+ _enum(_COSTUME_THEME_MAP, m.costume_theme, what="costume theme")
122
+ if m.costume_theme
123
+ else None
124
+ ),
92
125
  "costumeMotionTriggerDetected": m.motion_trigger_detected,
93
126
  "activeCostumeEffect": m.active_costume_effect,
94
127
  }
@@ -85,7 +85,7 @@ def _decode_origin(origin: navigation_pb2.GpsFix) -> dict[str, Any]:
85
85
  result = _decode_geocoordinate(
86
86
  _present(origin, "location"), "originLatitude", "originLongitude"
87
87
  )
88
- result["originHeading"] = origin.heading
88
+ result["originBearing"] = origin.bearing
89
89
  if (origin_epoch := _present(origin, "time")) is not None:
90
90
  result["originTime"] = from_epoch(origin_epoch)
91
91
  return result
@@ -282,7 +282,7 @@ def decode_trip_info(m: navigation_pb2.TripInfo) -> dict[str, Any]:
282
282
  tripId: str — changes on every route computation, so it can't
283
283
  correlate messages
284
284
  originLatitude, originLongitude: float
285
- originHeading: float (degrees)
285
+ originBearing: float (degrees)
286
286
  originTime: datetime
287
287
  originStateOfCharge: float (percent)
288
288
  distance, duration: float — driving totals (meters, seconds),
@@ -354,7 +354,7 @@ def decode_trip_progress(m: navigation_pb2.TripProgress) -> dict[str, Any]:
354
354
  (meters, seconds)
355
355
  latitude, longitude: float
356
356
  speed: float (m/s)
357
- heading: float (degrees)
357
+ bearing: float (degrees)
358
358
  locationTime: datetime — time of the position fix
359
359
  """
360
360
  result: dict[str, Any] = {}
@@ -371,7 +371,7 @@ def decode_trip_progress(m: navigation_pb2.TripProgress) -> dict[str, Any]:
371
371
  _decode_geocoordinate(_present(fix, "location"), "latitude", "longitude")
372
372
  )
373
373
  result["speed"] = fix.speed
374
- result["heading"] = fix.heading
374
+ result["bearing"] = fix.bearing
375
375
  if (fix_epoch := _present(fix, "time")) is not None:
376
376
  result["locationTime"] = from_epoch(fix_epoch)
377
377
  return result
@@ -0,0 +1,39 @@
1
+ """Encoder for `sendVehicleOperation` requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import uuid
7
+
8
+ from .proto import operation_pb2
9
+
10
+ _Request = operation_pb2.VehicleOperationRequest
11
+
12
+
13
+ def encode_vehicle_operation(
14
+ rvm: str,
15
+ payload: bytes = b"",
16
+ phone_id: bytes = bytes(16),
17
+ request_id: str | None = None,
18
+ ) -> str:
19
+ """Encode a `sendVehicleOperation` request as a base64 string.
20
+
21
+ An empty payload asks the vehicle to publish the current value of the RVM,
22
+ anything else writes it. The vehicle accepts an all-zero phone id.
23
+ """
24
+ request = _Request(
25
+ metadata=_Request.Metadata(
26
+ phone_info=_Request.Metadata.PhoneInfo(field_1=1, phone_id=phone_id),
27
+ request_id=request_id or str(uuid.uuid4()),
28
+ ),
29
+ operation=_Request.Operation(
30
+ rvm=rvm,
31
+ type=(
32
+ operation_pb2.OPERATION_WRITE
33
+ if payload
34
+ else operation_pb2.OPERATION_READ
35
+ ),
36
+ payload=payload,
37
+ ),
38
+ )
39
+ return base64.b64encode(request.SerializeToString()).decode("utf-8")
rivian/parallax/ota.py CHANGED
@@ -27,24 +27,23 @@ _OTA_STATUS_MAP: Final[dict[int, str]] = {
27
27
 
28
28
 
29
29
  _Deployment = ota_pb2.DeploymentState
30
- # The app's GraphQL-style strings.
31
30
  _CURRENT_STATUS_MAP: Final[dict[int, str]] = {
32
- _Deployment.CURRENT_STATUS_INSTALL_SUCCESS: "Install_Success",
33
- _Deployment.CURRENT_STATUS_INSTALL_FAILED: "Install_Failed",
34
- _Deployment.CURRENT_STATUS_INSTALL_UNABLE_TO_START: "Install_Unable_To_Start",
31
+ _Deployment.CURRENT_STATUS_INSTALL_SUCCESS: "install_success",
32
+ _Deployment.CURRENT_STATUS_INSTALL_FAILED: "install_failed",
33
+ _Deployment.CURRENT_STATUS_INSTALL_UNABLE_TO_START: "install_unable_to_start",
35
34
  }
36
35
 
37
36
  _DEPLOYMENT_INTENT_MAP: Final[dict[int, str]] = {
38
- _Deployment.DEPLOYMENT_INTENT_PERFORMANCE_UPGRADE: "Performance_Upgrade",
39
- _Deployment.DEPLOYMENT_INTENT_BUG_FIX: "Bug_Fix",
40
- _Deployment.DEPLOYMENT_INTENT_SECURITY_UPDATE: "Security_Update",
41
- _Deployment.DEPLOYMENT_INTENT_FEATURE_ADDITION: "Feature_Addition",
37
+ _Deployment.DEPLOYMENT_INTENT_PERFORMANCE_UPGRADE: "performance_upgrade",
38
+ _Deployment.DEPLOYMENT_INTENT_BUG_FIX: "bug_fix",
39
+ _Deployment.DEPLOYMENT_INTENT_SECURITY_UPDATE: "security_update",
40
+ _Deployment.DEPLOYMENT_INTENT_FEATURE_ADDITION: "feature_addition",
42
41
  }
43
42
 
44
43
  _SOFTWARE_CATEGORY_MAP: Final[dict[int, str]] = {
45
- _Deployment.SOFTWARE_CATEGORY_FIRMWARE: "Firmware",
46
- _Deployment.SOFTWARE_CATEGORY_HD_MAPS: "HD_Maps",
47
- _Deployment.SOFTWARE_CATEGORY_VEHICLE_CONFIG: "Vehicle_Config",
44
+ _Deployment.SOFTWARE_CATEGORY_FIRMWARE: "firmware",
45
+ _Deployment.SOFTWARE_CATEGORY_HD_MAPS: "hd_maps",
46
+ _Deployment.SOFTWARE_CATEGORY_VEHICLE_CONFIG: "vehicle_config",
48
47
  }
49
48
 
50
49
 
@@ -56,8 +55,8 @@ def _decode_version(
56
55
  for field, key in (
57
56
  ("version_string", "Version"),
58
57
  ("version_year", "VersionYear"),
59
- ("version_build", "VersionWeek"),
60
- ("build_id", "VersionGitHash"),
58
+ ("version_week", "VersionWeek"),
59
+ ("git_hash", "VersionGitHash"),
61
60
  ):
62
61
  if (v := _present(version, field)) is not None:
63
62
  result[f"{prefix}{key}"] = v
@@ -80,7 +79,7 @@ def decode_deployment_state(m: ota_pb2.DeploymentState) -> dict[str, Any]:
80
79
  otaCurrentVersionYear, otaCurrentVersionWeek,
81
80
  otaCurrentVersionNumber: int
82
81
  otaCurrentVersionGitHash: str
83
- otaSoftwareCategory: str ("Firmware" | "HD_Maps" | "Vehicle_Config")
82
+ otaSoftwareCategory: str ("firmware" | "hd_maps" | "vehicle_config")
84
83
  otaUpdateInProgress: bool
85
84
  otaDeploymentId: str — UUID, only while an update is in flight
86
85
  otaAvailableVersion, otaAvailableVersionYear,
@@ -94,13 +93,13 @@ def decode_deployment_state(m: ota_pb2.DeploymentState) -> dict[str, Any]:
94
93
  otaDownloadProgress, otaInstallProgress: int (0-100)
95
94
  otaTimeRemaining: int (seconds; counts down during
96
95
  "install_countdown", a static default otherwise)
97
- otaCurrentStatus: str | None ("Install_Success" | "Install_Failed" |
98
- "Install_Unable_To_Start") — the last install's result
96
+ otaCurrentStatus: str | None ("install_success" | "install_failed" |
97
+ "install_unable_to_start") — the last install's result
99
98
  otaInstallReady: str ("ota_available" | "ota_not_available")
100
99
  otaInstallDuration: int (minutes)
101
100
  otaInstallTimeOfDay: int (minutes after local midnight), when set
102
- otaDeploymentIntent: str | None ("Performance_Upgrade" | "Bug_Fix" |
103
- "Security_Update" | "Feature_Addition")
101
+ otaDeploymentIntent: str | None ("performance_upgrade" | "bug_fix" |
102
+ "security_update" | "feature_addition")
104
103
  otaSkipAllowed: bool, otaSkipCount: int
105
104
  otaPendingReasons: list[str] — what's blocking an install, e.g.
106
105
  "not_parked", "unplugged", "lv_batt"
@@ -12,16 +12,17 @@ message ClosuresState {
12
12
  int32 open_position_percent = 3;
13
13
  // Unset on doors and windows; briefly GENERAL while re-reporting.
14
14
  ClosureFault fault = 4;
15
- optional WindowsNextAction windows_next_action = 5; // on GROUP_WINDOWS
16
-
17
- // One field per closure type. Briefly SNA / not-available while the
15
+ // One member per closure type. Briefly SNA / not-available while the
18
16
  // vehicle re-reports after an app command.
19
- optional FrunkNextAction frunk_next_action = 6;
20
- optional LiftgateNextAction liftgate_next_action = 7;
21
- optional SideBinNextAction side_bin_next_action = 8;
22
- optional TailgateNextAction tailgate_next_action = 9;
23
- optional ChargePortDoorNextAction charge_port_door_next_action = 10;
24
- int32 tonneau_next_action = 11; // enum type unknown
17
+ oneof next_action {
18
+ WindowsNextAction windows_next_action = 5; // on GROUP_WINDOWS
19
+ FrunkNextAction frunk_next_action = 6;
20
+ LiftgateNextAction liftgate_next_action = 7;
21
+ SideBinNextAction side_bin_next_action = 8;
22
+ TailgateNextAction tailgate_next_action = 9;
23
+ ChargePortDoorNextAction charge_port_door_next_action = 10;
24
+ int32 tonneau_next_action = 11; // the app's enum (class stripped)
25
+ }
25
26
  }
26
27
  repeated Closure closure = 1;
27
28
  }