rivian-python-client 2.1.0__tar.gz → 2.2.0__tar.gz

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 (97) hide show
  1. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/PKG-INFO +29 -2
  2. rivian_python_client-2.2.0/README.md +52 -0
  3. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/pyproject.toml +15 -1
  4. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/__version__.py +1 -1
  5. rivian_python_client-2.2.0/src/rivian/parallax/__init__.py +70 -0
  6. rivian_python_client-2.2.0/src/rivian/parallax/body.py +260 -0
  7. rivian_python_client-2.2.0/src/rivian/parallax/charging.py +407 -0
  8. rivian_python_client-2.2.0/src/rivian/parallax/comfort.py +317 -0
  9. rivian_python_client-2.2.0/src/rivian/parallax/core.py +163 -0
  10. rivian_python_client-2.2.0/src/rivian/parallax/departure.py +105 -0
  11. rivian_python_client-2.2.0/src/rivian/parallax/device_table.py +136 -0
  12. rivian_python_client-2.2.0/src/rivian/parallax/dynamics.py +288 -0
  13. rivian_python_client-2.2.0/src/rivian/parallax/energy.py +148 -0
  14. rivian_python_client-2.2.0/src/rivian/parallax/energy_edge_compute.py +193 -0
  15. rivian_python_client-2.2.0/src/rivian/parallax/gearguard_streaming.py +66 -0
  16. rivian_python_client-2.2.0/src/rivian/parallax/geofence.py +35 -0
  17. rivian_python_client-2.2.0/src/rivian/parallax/holiday_celebration.py +193 -0
  18. rivian_python_client-2.2.0/src/rivian/parallax/navigation.py +377 -0
  19. rivian_python_client-2.2.0/src/rivian/parallax/ota.py +242 -0
  20. rivian_python_client-2.2.0/src/rivian/parallax/parallax.py +33 -0
  21. rivian_python_client-2.2.0/src/rivian/parallax/proto/__init__.py +8 -0
  22. rivian_python_client-2.2.0/src/rivian/parallax/proto/body.proto +268 -0
  23. rivian_python_client-2.2.0/src/rivian/parallax/proto/body_pb2.py +84 -0
  24. rivian_python_client-2.2.0/src/rivian/parallax/proto/body_pb2.pyi +436 -0
  25. rivian_python_client-2.2.0/src/rivian/parallax/proto/charging.proto +239 -0
  26. rivian_python_client-2.2.0/src/rivian/parallax/proto/charging_pb2.py +86 -0
  27. rivian_python_client-2.2.0/src/rivian/parallax/proto/charging_pb2.pyi +361 -0
  28. rivian_python_client-2.2.0/src/rivian/parallax/proto/comfort.proto +190 -0
  29. rivian_python_client-2.2.0/src/rivian/parallax/proto/comfort_pb2.py +84 -0
  30. rivian_python_client-2.2.0/src/rivian/parallax/proto/comfort_pb2.pyi +287 -0
  31. rivian_python_client-2.2.0/src/rivian/parallax/proto/departure.proto +78 -0
  32. rivian_python_client-2.2.0/src/rivian/parallax/proto/departure_pb2.py +54 -0
  33. rivian_python_client-2.2.0/src/rivian/parallax/proto/departure_pb2.pyi +115 -0
  34. rivian_python_client-2.2.0/src/rivian/parallax/proto/device_table.proto +111 -0
  35. rivian_python_client-2.2.0/src/rivian/parallax/proto/device_table_pb2.py +60 -0
  36. rivian_python_client-2.2.0/src/rivian/parallax/proto/device_table_pb2.pyi +162 -0
  37. rivian_python_client-2.2.0/src/rivian/parallax/proto/dynamics.proto +158 -0
  38. rivian_python_client-2.2.0/src/rivian/parallax/proto/dynamics_pb2.py +74 -0
  39. rivian_python_client-2.2.0/src/rivian/parallax/proto/dynamics_pb2.pyi +238 -0
  40. rivian_python_client-2.2.0/src/rivian/parallax/proto/energy.proto +97 -0
  41. rivian_python_client-2.2.0/src/rivian/parallax/proto/energy_edge_compute.proto +93 -0
  42. rivian_python_client-2.2.0/src/rivian/parallax/proto/energy_edge_compute_pb2.py +53 -0
  43. rivian_python_client-2.2.0/src/rivian/parallax/proto/energy_edge_compute_pb2.pyi +138 -0
  44. rivian_python_client-2.2.0/src/rivian/parallax/proto/energy_pb2.py +58 -0
  45. rivian_python_client-2.2.0/src/rivian/parallax/proto/energy_pb2.pyi +148 -0
  46. rivian_python_client-2.2.0/src/rivian/parallax/proto/gearguard_streaming.proto +29 -0
  47. rivian_python_client-2.2.0/src/rivian/parallax/proto/gearguard_streaming_pb2.py +42 -0
  48. rivian_python_client-2.2.0/src/rivian/parallax/proto/gearguard_streaming_pb2.pyi +40 -0
  49. rivian_python_client-2.2.0/src/rivian/parallax/proto/geofence.proto +21 -0
  50. rivian_python_client-2.2.0/src/rivian/parallax/proto/geofence_pb2.py +40 -0
  51. rivian_python_client-2.2.0/src/rivian/parallax/proto/geofence_pb2.pyi +30 -0
  52. rivian_python_client-2.2.0/src/rivian/parallax/proto/holiday_celebration.proto +86 -0
  53. rivian_python_client-2.2.0/src/rivian/parallax/proto/holiday_celebration_pb2.py +56 -0
  54. rivian_python_client-2.2.0/src/rivian/parallax/proto/holiday_celebration_pb2.pyi +128 -0
  55. rivian_python_client-2.2.0/src/rivian/parallax/proto/navigation.proto +201 -0
  56. rivian_python_client-2.2.0/src/rivian/parallax/proto/navigation_pb2.py +76 -0
  57. rivian_python_client-2.2.0/src/rivian/parallax/proto/navigation_pb2.pyi +312 -0
  58. rivian_python_client-2.2.0/src/rivian/parallax/proto/ota.proto +173 -0
  59. rivian_python_client-2.2.0/src/rivian/parallax/proto/ota_pb2.py +78 -0
  60. rivian_python_client-2.2.0/src/rivian/parallax/proto/ota_pb2.pyi +253 -0
  61. rivian_python_client-2.2.0/src/rivian/parallax/proto/parallax.proto +17 -0
  62. rivian_python_client-2.2.0/src/rivian/parallax/proto/parallax_pb2.py +38 -0
  63. rivian_python_client-2.2.0/src/rivian/parallax/proto/parallax_pb2.pyi +23 -0
  64. rivian_python_client-2.2.0/src/rivian/parallax/proto/secure_file_transfer.proto +9 -0
  65. rivian_python_client-2.2.0/src/rivian/parallax/proto/secure_file_transfer_pb2.py +36 -0
  66. rivian_python_client-2.2.0/src/rivian/parallax/proto/secure_file_transfer_pb2.pyi +9 -0
  67. rivian_python_client-2.2.0/src/rivian/parallax/proto/security.proto +131 -0
  68. rivian_python_client-2.2.0/src/rivian/parallax/proto/security_pb2.py +68 -0
  69. rivian_python_client-2.2.0/src/rivian/parallax/proto/security_pb2.pyi +198 -0
  70. rivian_python_client-2.2.0/src/rivian/parallax/proto/user_passcodes.proto +17 -0
  71. rivian_python_client-2.2.0/src/rivian/parallax/proto/user_passcodes_pb2.py +38 -0
  72. rivian_python_client-2.2.0/src/rivian/parallax/proto/user_passcodes_pb2.pyi +21 -0
  73. rivian_python_client-2.2.0/src/rivian/parallax/proto/vehicle.proto +165 -0
  74. rivian_python_client-2.2.0/src/rivian/parallax/proto/vehicle_access.proto +30 -0
  75. rivian_python_client-2.2.0/src/rivian/parallax/proto/vehicle_access_pb2.py +42 -0
  76. rivian_python_client-2.2.0/src/rivian/parallax/proto/vehicle_access_pb2.pyi +38 -0
  77. rivian_python_client-2.2.0/src/rivian/parallax/proto/vehicle_pb2.py +72 -0
  78. rivian_python_client-2.2.0/src/rivian/parallax/proto/vehicle_pb2.pyi +259 -0
  79. rivian_python_client-2.2.0/src/rivian/parallax/secure_file_transfer.py +19 -0
  80. rivian_python_client-2.2.0/src/rivian/parallax/security.py +224 -0
  81. rivian_python_client-2.2.0/src/rivian/parallax/user_passcodes.py +32 -0
  82. rivian_python_client-2.2.0/src/rivian/parallax/vehicle.py +240 -0
  83. rivian_python_client-2.2.0/src/rivian/parallax/vehicle_access.py +68 -0
  84. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/rivian.py +20 -9
  85. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/utils.py +17 -0
  86. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/ws_monitor.py +31 -0
  87. rivian_python_client-2.1.0/README.md +0 -26
  88. rivian_python_client-2.1.0/src/rivian/parallax.py +0 -628
  89. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/.gitignore +0 -0
  90. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/__init__.py +0 -0
  91. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/ble.py +0 -0
  92. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/const.py +0 -0
  93. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/exceptions.py +0 -0
  94. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/py.typed +0 -0
  95. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/schemas/charging.graphql +0 -0
  96. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/schemas/gateway.graphql +0 -0
  97. {rivian_python_client-2.1.0 → rivian_python_client-2.2.0}/src/rivian/schemas/orders.graphql +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: rivian-python-client
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Rivian API Client (Unofficial)
5
5
  Author-email: Brian Retterer <bretterer@gmail.com>, "J.R. Gutierrez" <jrgutier@gmail.com>, Nathan Spencer <natekspencer@gmail.com>, Trevor Mack <drummer8001@gmail.com>
6
6
  License-Expression: MIT
@@ -8,6 +8,7 @@ Requires-Python: >=3.10
8
8
  Requires-Dist: aiohttp>=3.0.0
9
9
  Requires-Dist: backports-strenum>=1.2.4; python_version < '3.11'
10
10
  Requires-Dist: cryptography>=41.0.1
11
+ Requires-Dist: protobuf<8,>=6.31.1
11
12
  Requires-Dist: typing-extensions>=4.14.1; python_version < '3.11'
12
13
  Provides-Extra: ble
13
14
  Requires-Dist: bleak>=0.21; extra == 'ble'
@@ -40,3 +41,29 @@ pre-commit install
40
41
  ```
41
42
  uv run pytest
42
43
  ```
44
+
45
+ ## Parallax
46
+
47
+ `subscribe_for_parallax_messages` streams the vehicle's Parallax RVM topics,
48
+ whose payloads are protobuf. `rivian.parallax` decodes them into dicts keyed
49
+ like the GraphQL vehicle state where a matching property exists:
50
+
51
+ ```python
52
+ from rivian import parallax
53
+
54
+
55
+ def on_message(data):
56
+ if decoded := parallax.decode_parallax_subscription_message(data):
57
+ print(decoded)
58
+ ```
59
+
60
+ `parallax.PARALLAX_RVMS` lists every topic with a decoder, and
61
+ `parallax.CHARGING_RVMS` the ones relevant to a charging session.
62
+
63
+ The schemas in `src/rivian/parallax/proto/*.proto` are reverse-engineered. After
64
+ editing one, regenerate its `*_pb2` modules (don't edit those by hand):
65
+
66
+ ```
67
+ uv run python -m grpc_tools.protoc --proto_path=src --python_out=src \
68
+ --pyi_out=src src/rivian/parallax/proto/*.proto
69
+ ```
@@ -0,0 +1,52 @@
1
+ # Python: Rivian API Client
2
+
3
+ Currently a Work In Progress
4
+
5
+ ## Dependencies
6
+
7
+ [uv](https://docs.astral.sh/uv/)
8
+
9
+ ```
10
+ curl -LsSf https://astral.sh/uv/install.sh | sh
11
+ ```
12
+
13
+ ## Setup
14
+
15
+ Install project dependencies into the uv virtual environment and run pre-commit
16
+
17
+ ```
18
+ uv sync --all-extras
19
+ pre-commit install
20
+ ```
21
+
22
+ ## Run Tests
23
+
24
+ ```
25
+ uv run pytest
26
+ ```
27
+
28
+ ## Parallax
29
+
30
+ `subscribe_for_parallax_messages` streams the vehicle's Parallax RVM topics,
31
+ whose payloads are protobuf. `rivian.parallax` decodes them into dicts keyed
32
+ like the GraphQL vehicle state where a matching property exists:
33
+
34
+ ```python
35
+ from rivian import parallax
36
+
37
+
38
+ def on_message(data):
39
+ if decoded := parallax.decode_parallax_subscription_message(data):
40
+ print(decoded)
41
+ ```
42
+
43
+ `parallax.PARALLAX_RVMS` lists every topic with a decoder, and
44
+ `parallax.CHARGING_RVMS` the ones relevant to a charging session.
45
+
46
+ The schemas in `src/rivian/parallax/proto/*.proto` are reverse-engineered. After
47
+ editing one, regenerate its `*_pb2` modules (don't edit those by hand):
48
+
49
+ ```
50
+ uv run python -m grpc_tools.protoc --proto_path=src --python_out=src \
51
+ --pyi_out=src src/rivian/parallax/proto/*.proto
52
+ ```
@@ -15,6 +15,7 @@ dependencies = [
15
15
  "cryptography (>=41.0.1)",
16
16
  "backports-strenum (>=1.2.4) ; python_version < '3.11'",
17
17
  "typing_extensions (>=4.14.1) ; python_version < '3.11'",
18
+ "protobuf>=6.31.1,<8",
18
19
  ]
19
20
  dynamic = ["version"]
20
21
 
@@ -33,8 +34,18 @@ dev = [
33
34
  "mypy>=1.7,<3.0",
34
35
  "ruff>=0.16,<0.17",
35
36
  "pre-commit>=4.6.1,<5",
37
+ "types-protobuf>=6.32.1.20260221",
38
+ "grpcio-tools==1.80.0",
39
+ "pytest-cov>=7.0,<8",
36
40
  ]
37
41
 
42
+ [tool.coverage.run]
43
+ source = ["rivian"]
44
+ omit = ["*_pb2.py"]
45
+
46
+ [tool.coverage.report]
47
+ exclude_also = ["if TYPE_CHECKING:"]
48
+
38
49
  [tool.hatch.build.hooks.version]
39
50
  path = "src/rivian/__version__.py"
40
51
 
@@ -57,10 +68,13 @@ cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true
57
68
  [tool.uv-dynamic-versioning]
58
69
  pattern = "default-unprefixed"
59
70
 
71
+ [tool.ruff]
72
+ extend-exclude = ["*_pb2.py", "*_pb2.pyi"]
73
+
60
74
  [tool.ruff.lint.isort]
61
75
  combine-as-imports = true
62
76
  split-on-trailing-comma = false
63
77
 
64
78
  [build-system]
65
79
  requires = ["hatchling", "uv-dynamic-versioning"]
66
- build-backend = "hatchling.build"
80
+ build-backend = "hatchling.build"
@@ -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.1.0'
4
+ __version__ = VERSION = '2.2.0'
@@ -0,0 +1,70 @@
1
+ """Decoders for Rivian Parallax protobuf payloads.
2
+
3
+ Public API:
4
+ decode_parallax_message(rvm, payload, timestamp=None, **kwargs) -> dict | None
5
+ Adds `timestamp` (epoch milliseconds) as a UTC datetime if given.
6
+ decode_parallax_subscription_message(data) -> dict | None
7
+ Same, for a raw `subscribe_for_parallax_messages` message.
8
+ PARALLAX_RVMS: list[str] — every RVM with a registered decoder
9
+ CHARGING_RVMS: list[str] — the subset relevant to a charging session
10
+
11
+ Layout:
12
+ core.py - registry, payload parsing and shared helpers
13
+ proto/ - .proto schemas and their generated *_pb2 modules
14
+ <topic prefix>.py - one module per RVM topic prefix (body.*, etc.)
15
+
16
+ To add a decoder, decorate it with
17
+ `@RVMDecoder.register("the.rvm.topic", some_pb2.Message)` in its prefix's
18
+ module; a new module also needs adding to the import list below.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from . import ( # noqa: F401 (imported to register their decoders)
24
+ body,
25
+ charging,
26
+ comfort,
27
+ departure,
28
+ device_table,
29
+ dynamics,
30
+ energy,
31
+ energy_edge_compute,
32
+ gearguard_streaming,
33
+ geofence,
34
+ holiday_celebration,
35
+ navigation,
36
+ ota,
37
+ parallax,
38
+ secure_file_transfer,
39
+ security,
40
+ user_passcodes,
41
+ vehicle,
42
+ vehicle_access,
43
+ )
44
+ from .core import (
45
+ RVMDecoder,
46
+ decode_parallax_message,
47
+ decode_parallax_subscription_message,
48
+ )
49
+
50
+ PARALLAX_RVMS: list[str] = list(RVMDecoder.decoders)
51
+
52
+ CHARGING_RVMS: list[str] = [
53
+ "charging.energy.state",
54
+ "charging.session.notification",
55
+ "charging.session.power",
56
+ "charging.session.remote_command",
57
+ "charging.session.soc_slider",
58
+ "charging.session.status",
59
+ "charging.session.time_estimation",
60
+ "energy.high_voltage.battery_state",
61
+ "energy_edge_compute.graphs.charge_session_breakdown",
62
+ "energy_edge_compute.graphs.charging_graph_global",
63
+ ]
64
+
65
+ __all__ = [
66
+ "CHARGING_RVMS",
67
+ "PARALLAX_RVMS",
68
+ "decode_parallax_message",
69
+ "decode_parallax_subscription_message",
70
+ ]
@@ -0,0 +1,260 @@
1
+ """Decoders for `body.*` RVM topics: closures, locks, trailer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Final
6
+
7
+ from .core import _LOGGER, RVMDecoder, _enum, _present
8
+ from .proto import body_pb2
9
+
10
+ _CLOSURE_MAP: Final[dict[int, str]] = {
11
+ body_pb2.DOOR_FRONT_LEFT: "doorFrontLeftClosed",
12
+ body_pb2.DOOR_FRONT_RIGHT: "doorFrontRightClosed",
13
+ body_pb2.DOOR_REAR_LEFT: "doorRearLeftClosed",
14
+ body_pb2.DOOR_REAR_RIGHT: "doorRearRightClosed",
15
+ body_pb2.FRUNK: "closureFrunkClosed",
16
+ body_pb2.TAILGATE: "closureTailgateClosed",
17
+ body_pb2.LIFTGATE: "closureLiftgateClosed",
18
+ body_pb2.SIDE_BIN_LEFT: "closureSideBinLeftClosed",
19
+ body_pb2.SIDE_BIN_RIGHT: "closureSideBinRightClosed",
20
+ body_pb2.CHARGE_PORT: "chargePortState",
21
+ body_pb2.TONNEAU: "closureTonneauClosed",
22
+ body_pb2.WINDOW_FRONT_LEFT: "windowFrontLeftClosed",
23
+ body_pb2.WINDOW_FRONT_RIGHT: "windowFrontRightClosed",
24
+ body_pb2.WINDOW_REAR_LEFT: "windowRearLeftClosed",
25
+ body_pb2.WINDOW_REAR_RIGHT: "windowRearRightClosed",
26
+ body_pb2.WINDOW_REAR: "windowRearClosed",
27
+ }
28
+
29
+ _CLOSURE_STATE_MAP: Final[dict[int, str]] = {
30
+ body_pb2.CLOSURE_STATE_OPEN: "open",
31
+ body_pb2.CLOSURE_STATE_CLOSED: "closed",
32
+ body_pb2.CLOSURE_STATE_AJAR: "ajar",
33
+ body_pb2.CLOSURE_STATE_OPENING: "opening",
34
+ body_pb2.CLOSURE_STATE_CLOSING: "closing",
35
+ }
36
+
37
+
38
+ def _enum_names(enum: Any, prefix: str) -> dict[int, str]:
39
+ """Map an enum's numbers to its lowercased names, minus `prefix`."""
40
+ return {
41
+ value.number: value.name.removeprefix(prefix).lower()
42
+ for value in enum.DESCRIPTOR.values
43
+ }
44
+
45
+
46
+ _SIDE_BIN_NEXT_ACTIONS: Final = _enum_names(body_pb2.SideBinNextAction, "SIDE_BIN_")
47
+
48
+ # Closure id -> (next-action field, result key, value names).
49
+ _NEXT_ACTIONS: Final[dict[int, tuple[str, str, dict[int, str]]]] = {
50
+ body_pb2.FRUNK: (
51
+ "frunk_next_action",
52
+ "closureFrunkNextAction",
53
+ _enum_names(body_pb2.FrunkNextAction, "FRUNK_"),
54
+ ),
55
+ body_pb2.LIFTGATE: (
56
+ "liftgate_next_action",
57
+ "closureLiftgateNextAction",
58
+ _enum_names(body_pb2.LiftgateNextAction, "LIFTGATE_"),
59
+ ),
60
+ body_pb2.SIDE_BIN_LEFT: (
61
+ "side_bin_next_action",
62
+ "closureSideBinLeftNextAction",
63
+ _SIDE_BIN_NEXT_ACTIONS,
64
+ ),
65
+ body_pb2.SIDE_BIN_RIGHT: (
66
+ "side_bin_next_action",
67
+ "closureSideBinRightNextAction",
68
+ _SIDE_BIN_NEXT_ACTIONS,
69
+ ),
70
+ body_pb2.TAILGATE: (
71
+ "tailgate_next_action",
72
+ "closureTailgateNextAction",
73
+ _enum_names(body_pb2.TailgateNextAction, "TAILGATE_"),
74
+ ),
75
+ body_pb2.CHARGE_PORT: (
76
+ "charge_port_door_next_action",
77
+ "closureChargePortDoorNextAction",
78
+ _enum_names(body_pb2.ChargePortDoorNextAction, "CHARGE_PORT_DOOR_"),
79
+ ),
80
+ body_pb2.GROUP_WINDOWS: (
81
+ "windows_next_action",
82
+ "windowsNextAction",
83
+ _enum_names(body_pb2.WindowsNextAction, "WINDOWS_"),
84
+ ),
85
+ }
86
+
87
+ _LOCK_MAP: Final[dict[int, str]] = {
88
+ body_pb2.LOCK_DOOR_FRONT_LEFT: "doorFrontLeftLocked",
89
+ body_pb2.LOCK_DOOR_FRONT_RIGHT: "doorFrontRightLocked",
90
+ body_pb2.LOCK_DOOR_REAR_LEFT: "doorRearLeftLocked",
91
+ body_pb2.LOCK_DOOR_REAR_RIGHT: "doorRearRightLocked",
92
+ body_pb2.LOCK_FRUNK: "closureFrunkLocked",
93
+ body_pb2.LOCK_TAILGATE: "closureTailgateLocked",
94
+ body_pb2.LOCK_LIFTGATE: "closureLiftgateLocked",
95
+ body_pb2.LOCK_SIDE_BIN_LEFT: "closureSideBinLeftLocked",
96
+ body_pb2.LOCK_SIDE_BIN_RIGHT: "closureSideBinRightLocked",
97
+ body_pb2.LOCK_CHARGE_PORT: "closureChargePortLocked",
98
+ body_pb2.LOCK_TRUNK_SECURITY: "closureTrunkSecurityLocked",
99
+ body_pb2.LOCK_CENTER_CONSOLE: "closureCenterConsoleLocked",
100
+ body_pb2.LOCK_GLOVE_BOX: "closureGloveBoxLocked",
101
+ body_pb2.LOCK_GEAR_GUARD: "gearGuardLocked",
102
+ body_pb2.LOCK_TONNEAU: "closureTonneauLocked",
103
+ }
104
+
105
+ _LOCK_STATE_MAP: Final[dict[int, str]] = {
106
+ body_pb2.LOCK_STATE_LOCKED: "locked",
107
+ body_pb2.LOCK_STATE_UNLOCKED: "unlocked",
108
+ body_pb2.LOCK_STATE_PARTIALLY_UNLOCKED: "partially_unlocked",
109
+ }
110
+
111
+ _REAR_HITCH_STATUS_MAP: Final[dict[int, str]] = {
112
+ body_pb2.REAR_HITCH_STATUS_NOT_PRESENT: "not_present",
113
+ body_pb2.REAR_HITCH_STATUS_ACCESSORY: "accessory",
114
+ body_pb2.REAR_HITCH_STATUS_TRAILER1: "trailer1",
115
+ body_pb2.REAR_HITCH_STATUS_TRAILER2: "trailer2",
116
+ body_pb2.REAR_HITCH_STATUS_TRAILER3: "trailer3",
117
+ }
118
+
119
+ _WINDOW_MAP: Final[dict[int, str]] = {
120
+ body_pb2.WINDOW_INSTANCE_FRONT_LEFT: "windowFrontLeftCalibrated",
121
+ body_pb2.WINDOW_INSTANCE_FRONT_RIGHT: "windowFrontRightCalibrated",
122
+ body_pb2.WINDOW_INSTANCE_REAR_LEFT: "windowRearLeftCalibrated",
123
+ body_pb2.WINDOW_INSTANCE_REAR_RIGHT: "windowRearRightCalibrated",
124
+ body_pb2.WINDOW_INSTANCE_REAR: "windowRearCalibrated",
125
+ }
126
+
127
+ # GraphQL's windowXCalibrated values.
128
+ _CALIBRATION_MAP: Final[dict[int, str]] = {
129
+ body_pb2.CALIBRATION_STATUS_CALIBRATED: "calibrated",
130
+ body_pb2.CALIBRATION_STATUS_NOT_CALIBRATED: "not_calibrated",
131
+ }
132
+
133
+ _TRAILER_PRESENCE_MAP: Final[dict[int, str]] = {
134
+ body_pb2.TRAILER_NOT_PRESENT: "trailer_not_present",
135
+ body_pb2.TRAILER_PRESENT: "trailer_present",
136
+ body_pb2.TRAILER_PRESENT_WITH_BRAKES: "trailer_present_with_brakes",
137
+ body_pb2.TRAILER_INVALID: "trailer_invalid",
138
+ }
139
+
140
+
141
+ @RVMDecoder.register("body.closures.states", body_pb2.ClosuresState)
142
+ def decode_closures(m: body_pb2.ClosuresState) -> dict[str, Any]:
143
+ """body.closures.states — state of every door, window and closure.
144
+
145
+ Fields:
146
+ doorFrontLeftClosed, closureFrunkClosed, etc.: str
147
+ ("open" | "closed" | "ajar" | "opening" | "closing")
148
+ closureFrunkNextAction, closureLiftgateNextAction,
149
+ closureTailgateNextAction, closureSideBinLeftNextAction,
150
+ closureSideBinRightNextAction, closureChargePortDoorNextAction,
151
+ windowsNextAction: str — e.g. "open_allowed" (shut),
152
+ "close_allowed" (open); for the charge port,
153
+ "close_not_available" means plugged in
154
+
155
+ `chargePortState` can lag the door (e.g. "opening" until plugged in),
156
+ so prefer its next action. After an app command, next actions briefly
157
+ drop to "sna" / "open_not_available".
158
+ """
159
+ result: dict[str, Any] = {}
160
+ for s in m.closure:
161
+ cid = s.id
162
+ state_val = _present(s, "state")
163
+ if cid in _CLOSURE_MAP:
164
+ result[_CLOSURE_MAP[cid]] = _enum(
165
+ _CLOSURE_STATE_MAP, state_val, what="closure state"
166
+ )
167
+ elif cid != body_pb2.GROUP_WINDOWS:
168
+ _LOGGER.debug("Unknown closure id %s (state %s)", cid, state_val)
169
+ if cid in _NEXT_ACTIONS:
170
+ field, key, names = _NEXT_ACTIONS[cid]
171
+ if (next_action := _present(s, field)) is not None:
172
+ result[key] = _enum(names, next_action, what=f"{key} value")
173
+ return result
174
+
175
+
176
+ @RVMDecoder.register("body.locks.states", body_pb2.LocksState)
177
+ def decode_locks(m: body_pb2.LocksState) -> dict[str, Any]:
178
+ """body.locks.states — lock state of every lock.
179
+
180
+ Fields:
181
+ doorFrontLeftLocked, closureFrunkLocked, gearGuardLocked (the Gear
182
+ Guard cable), etc.: str ("locked" | "unlocked" |
183
+ "partially_unlocked")
184
+ """
185
+ result: dict[str, Any] = {}
186
+ for s in m.lock:
187
+ lid = s.id
188
+ state_val = _present(s, "state")
189
+ if state_val is None:
190
+ continue
191
+ if lid in _LOCK_MAP:
192
+ result[_LOCK_MAP[lid]] = _enum(
193
+ _LOCK_STATE_MAP, state_val, what="lock state"
194
+ )
195
+ else:
196
+ _LOGGER.debug("Unknown lock id %s (state %s)", lid, state_val)
197
+ return result
198
+
199
+
200
+ @RVMDecoder.register("body.windows.states", body_pb2.WindowsState)
201
+ def decode_windows(m: body_pb2.WindowsState) -> dict[str, Any]:
202
+ """body.windows.states — window calibration.
203
+
204
+ Fields:
205
+ windowFrontLeftCalibrated, windowFrontRightCalibrated,
206
+ windowRearLeftCalibrated, windowRearRightCalibrated,
207
+ windowRearCalibrated: str ("calibrated" | "not_calibrated")
208
+ """
209
+ return {
210
+ _WINDOW_MAP[w.instance]: _enum(
211
+ _CALIBRATION_MAP, w.calibration_status or None, what="window calibration"
212
+ )
213
+ for w in m.window
214
+ if w.instance in _WINDOW_MAP
215
+ }
216
+
217
+
218
+ @RVMDecoder.register("body.trailer.state", body_pb2.TrailerState)
219
+ def decode_trailer_state(m: body_pb2.TrailerState) -> dict[str, Any]:
220
+ """body.trailer.state — trailer presence and rear hitch status.
221
+
222
+ Fields:
223
+ trailerStatus: str
224
+ rearHitchStatus: str | None ("not_present" | "accessory" |
225
+ "trailer1" | "trailer2" | "trailer3")
226
+ """
227
+ result: dict[str, Any] = {
228
+ "rearHitchStatus": _enum(
229
+ _REAR_HITCH_STATUS_MAP, m.rear_hitch_status or None, what="rear hitch"
230
+ )
231
+ }
232
+ presence = _enum(
233
+ _TRAILER_PRESENCE_MAP, _present(m, "presence"), what="trailer presence"
234
+ )
235
+ if presence is not None:
236
+ result["trailerStatus"] = presence
237
+ return result
238
+
239
+
240
+ @RVMDecoder.register("body.wipers.fluid_level", body_pb2.WiperFluidLevel)
241
+ def decode_wiper_fluid_level(m: body_pb2.WiperFluidLevel) -> dict[str, Any]:
242
+ """body.wipers.fluid_level — washer fluid level.
243
+
244
+ The value numbering is a best guess.
245
+
246
+ Fields:
247
+ wiperFluidState: str | None ("normal" | "low" | "empty")
248
+ """
249
+ return {
250
+ "wiperFluidState": _enum(
251
+ _WIPER_FLUID_STATE_MAP, m.state or None, what="wiper fluid state"
252
+ )
253
+ }
254
+
255
+
256
+ _WIPER_FLUID_STATE_MAP: Final[dict[int, str]] = {
257
+ body_pb2.WIPER_FLUID_STATE_NORMAL: "normal",
258
+ body_pb2.WIPER_FLUID_STATE_LOW: "low",
259
+ body_pb2.WIPER_FLUID_STATE_EMPTY: "empty",
260
+ }