rivian-python-client 2.1.0b0__tar.gz → 2.2.0b1__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 (96) hide show
  1. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/PKG-INFO +3 -2
  2. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/pyproject.toml +7 -1
  3. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/__version__.py +1 -1
  4. rivian_python_client-2.2.0b1/src/rivian/parallax/__init__.py +70 -0
  5. rivian_python_client-2.2.0b1/src/rivian/parallax/body.py +179 -0
  6. rivian_python_client-2.2.0b1/src/rivian/parallax/charging.py +246 -0
  7. rivian_python_client-2.2.0b1/src/rivian/parallax/comfort.py +270 -0
  8. rivian_python_client-2.2.0b1/src/rivian/parallax/core.py +163 -0
  9. rivian_python_client-2.2.0b1/src/rivian/parallax/departure.py +105 -0
  10. rivian_python_client-2.2.0b1/src/rivian/parallax/device_table.py +59 -0
  11. rivian_python_client-2.2.0b1/src/rivian/parallax/dynamics.py +245 -0
  12. rivian_python_client-2.2.0b1/src/rivian/parallax/energy.py +93 -0
  13. rivian_python_client-2.2.0b1/src/rivian/parallax/energy_edge_compute.py +188 -0
  14. rivian_python_client-2.2.0b1/src/rivian/parallax/gearguard_streaming.py +68 -0
  15. rivian_python_client-2.2.0b1/src/rivian/parallax/geofence.py +30 -0
  16. rivian_python_client-2.2.0b1/src/rivian/parallax/holiday_celebration.py +72 -0
  17. rivian_python_client-2.2.0b1/src/rivian/parallax/navigation.py +335 -0
  18. rivian_python_client-2.2.0b1/src/rivian/parallax/ota.py +172 -0
  19. rivian_python_client-2.2.0b1/src/rivian/parallax/parallax.py +33 -0
  20. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/__init__.py +8 -0
  21. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/body.proto +163 -0
  22. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/body_pb2.py +64 -0
  23. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/body_pb2.pyi +255 -0
  24. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/charging.proto +144 -0
  25. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/charging_pb2.py +72 -0
  26. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/charging_pb2.pyi +209 -0
  27. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/comfort.proto +151 -0
  28. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/comfort_pb2.py +78 -0
  29. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/comfort_pb2.pyi +220 -0
  30. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/departure.proto +78 -0
  31. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/departure_pb2.py +54 -0
  32. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/departure_pb2.pyi +115 -0
  33. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/device_table.proto +44 -0
  34. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/device_table_pb2.py +46 -0
  35. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/device_table_pb2.pyi +61 -0
  36. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/dynamics.proto +149 -0
  37. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/dynamics_pb2.py +72 -0
  38. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/dynamics_pb2.pyi +225 -0
  39. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/energy.proto +58 -0
  40. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/energy_edge_compute.proto +77 -0
  41. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/energy_edge_compute_pb2.py +49 -0
  42. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/energy_edge_compute_pb2.pyi +111 -0
  43. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/energy_pb2.py +48 -0
  44. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/energy_pb2.pyi +83 -0
  45. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/gearguard_streaming.proto +31 -0
  46. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/gearguard_streaming_pb2.py +42 -0
  47. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/gearguard_streaming_pb2.pyi +44 -0
  48. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/geofence.proto +15 -0
  49. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/geofence_pb2.py +38 -0
  50. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/geofence_pb2.pyi +20 -0
  51. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/holiday_celebration.proto +51 -0
  52. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/holiday_celebration_pb2.py +44 -0
  53. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/holiday_celebration_pb2.pyi +73 -0
  54. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/navigation.proto +186 -0
  55. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/navigation_pb2.py +78 -0
  56. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/navigation_pb2.pyi +251 -0
  57. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/ota.proto +111 -0
  58. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/ota_pb2.py +64 -0
  59. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/ota_pb2.pyi +149 -0
  60. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/parallax.proto +17 -0
  61. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/parallax_pb2.py +38 -0
  62. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/parallax_pb2.pyi +23 -0
  63. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/secure_file_transfer.proto +9 -0
  64. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/secure_file_transfer_pb2.py +36 -0
  65. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/secure_file_transfer_pb2.pyi +9 -0
  66. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/security.proto +119 -0
  67. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/security_pb2.py +66 -0
  68. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/security_pb2.pyi +177 -0
  69. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/user_passcodes.proto +17 -0
  70. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/user_passcodes_pb2.py +38 -0
  71. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/user_passcodes_pb2.pyi +21 -0
  72. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/vehicle.proto +116 -0
  73. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/vehicle_access.proto +8 -0
  74. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/vehicle_access_pb2.py +36 -0
  75. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/vehicle_access_pb2.pyi +9 -0
  76. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/vehicle_pb2.py +62 -0
  77. rivian_python_client-2.2.0b1/src/rivian/parallax/proto/vehicle_pb2.pyi +180 -0
  78. rivian_python_client-2.2.0b1/src/rivian/parallax/secure_file_transfer.py +19 -0
  79. rivian_python_client-2.2.0b1/src/rivian/parallax/security.py +210 -0
  80. rivian_python_client-2.2.0b1/src/rivian/parallax/user_passcodes.py +32 -0
  81. rivian_python_client-2.2.0b1/src/rivian/parallax/vehicle.py +187 -0
  82. rivian_python_client-2.2.0b1/src/rivian/parallax/vehicle_access.py +18 -0
  83. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/rivian.py +20 -9
  84. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/utils.py +17 -0
  85. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/ws_monitor.py +31 -0
  86. rivian_python_client-2.1.0b0/src/rivian/parallax.py +0 -624
  87. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/.gitignore +0 -0
  88. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/README.md +0 -0
  89. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/__init__.py +0 -0
  90. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/ble.py +0 -0
  91. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/const.py +0 -0
  92. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/exceptions.py +0 -0
  93. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/py.typed +0 -0
  94. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/schemas/charging.graphql +0 -0
  95. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/src/rivian/schemas/gateway.graphql +0 -0
  96. {rivian_python_client-2.1.0b0 → rivian_python_client-2.2.0b1}/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.0b0
3
+ Version: 2.2.0b1
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,>=7.35.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'
@@ -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>=7.35.1,<8",
18
19
  ]
19
20
  dynamic = ["version"]
20
21
 
@@ -33,6 +34,8 @@ 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>=7.35.1.20260906",
38
+ "grpcio-tools==1.84.0",
36
39
  ]
37
40
 
38
41
  [tool.hatch.build.hooks.version]
@@ -57,10 +60,13 @@ cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true
57
60
  [tool.uv-dynamic-versioning]
58
61
  pattern = "default-unprefixed"
59
62
 
63
+ [tool.ruff]
64
+ extend-exclude = ["*_pb2.py", "*_pb2.pyi"]
65
+
60
66
  [tool.ruff.lint.isort]
61
67
  combine-as-imports = true
62
68
  split-on-trailing-comma = false
63
69
 
64
70
  [build-system]
65
71
  requires = ["hatchling", "uv-dynamic-versioning"]
66
- build-backend = "hatchling.build"
72
+ 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.0b0'
4
+ __version__ = VERSION = '2.2.0b1'
@@ -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,179 @@
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
+ 1: "doorFrontLeftClosed",
12
+ 2: "doorFrontRightClosed",
13
+ 3: "doorRearLeftClosed",
14
+ 4: "doorRearRightClosed",
15
+ 5: "closureFrunkClosed",
16
+ 6: "closureTailgateClosed",
17
+ 7: "closureLiftgateClosed",
18
+ 8: "closureSideBinLeftClosed",
19
+ 9: "closureSideBinRightClosed",
20
+ 10: "chargePortState",
21
+ 11: "closureTonneauClosed",
22
+ 12: "windowFrontLeftClosed",
23
+ 13: "windowFrontRightClosed",
24
+ 14: "windowRearLeftClosed",
25
+ 15: "windowRearRightClosed",
26
+ }
27
+
28
+ _CLOSURE_STATE_MAP: Final[dict[int, str]] = {
29
+ 1: "open",
30
+ 2: "closed",
31
+ 4: "opening",
32
+ 5: "closing",
33
+ }
34
+
35
+
36
+ def _enum_names(enum: Any, prefix: str) -> dict[int, str]:
37
+ """Map an enum's numbers to its lowercased names, minus `prefix`."""
38
+ return {
39
+ value.number: value.name.removeprefix(prefix).lower()
40
+ for value in enum.DESCRIPTOR.values
41
+ }
42
+
43
+
44
+ # Other side-bin values are unconfirmed, so they pass through as raw ints.
45
+ _SIDE_BIN_NEXT_ACTIONS: Final[dict[int, str]] = {
46
+ body_pb2.SIDE_BIN_SNA: "sna",
47
+ body_pb2.SIDE_BIN_OPEN_ALLOWED: "open_allowed",
48
+ }
49
+
50
+ # Closure id -> (next-action field, result key, value names).
51
+ _NEXT_ACTIONS: Final[dict[int, tuple[str, str, dict[int, str]]]] = {
52
+ body_pb2.FRUNK: (
53
+ "frunk_next_action",
54
+ "closureFrunkNextAction",
55
+ _enum_names(body_pb2.FrunkNextAction, "FRUNK_"),
56
+ ),
57
+ body_pb2.SIDE_BIN_LEFT: (
58
+ "side_bin_next_action",
59
+ "closureSideBinLeftNextAction",
60
+ _SIDE_BIN_NEXT_ACTIONS,
61
+ ),
62
+ body_pb2.SIDE_BIN_RIGHT: (
63
+ "side_bin_next_action",
64
+ "closureSideBinRightNextAction",
65
+ _SIDE_BIN_NEXT_ACTIONS,
66
+ ),
67
+ body_pb2.TAILGATE: (
68
+ "tailgate_next_action",
69
+ "closureTailgateNextAction",
70
+ _enum_names(body_pb2.TailgateNextAction, "TAILGATE_"),
71
+ ),
72
+ body_pb2.CHARGE_PORT: (
73
+ "charge_port_door_next_action",
74
+ "closureChargePortDoorNextAction",
75
+ _enum_names(body_pb2.ChargePortDoorNextAction, "CHARGE_PORT_DOOR_"),
76
+ ),
77
+ }
78
+
79
+ _LOCK_MAP: Final[dict[int, str]] = {
80
+ 1: "doorFrontLeftLocked",
81
+ 2: "doorFrontRightLocked",
82
+ 3: "doorRearLeftLocked",
83
+ 4: "doorRearRightLocked",
84
+ 5: "closureFrunkLocked",
85
+ 6: "closureTailgateLocked",
86
+ 7: "closureLiftgateLocked",
87
+ 8: "closureSideBinLeftLocked",
88
+ 9: "closureSideBinRightLocked",
89
+ 15: "closureTonneauLocked",
90
+ }
91
+
92
+ _TRAILER_PRESENCE_MAP: Final[dict[int, str]] = {
93
+ 1: "trailer_not_present",
94
+ 2: "trailer_present",
95
+ 3: "trailer_present_with_brakes",
96
+ 4: "trailer_invalid",
97
+ }
98
+
99
+
100
+ @RVMDecoder.register("body.closures.states", body_pb2.ClosuresState)
101
+ def decode_closures(m: body_pb2.ClosuresState) -> dict[str, Any]:
102
+ """body.closures.states — state of every door, window and closure.
103
+
104
+ Fields:
105
+ doorFrontLeftClosed, closureFrunkClosed, etc.: str
106
+ ("open" | "closed" | "opening" | "closing")
107
+ closureFrunkNextAction, closureTailgateNextAction,
108
+ closureSideBinLeftNextAction, closureSideBinRightNextAction,
109
+ closureChargePortDoorNextAction: str — e.g. "open_allowed" (shut),
110
+ "close_allowed" (open); for the charge port,
111
+ "close_not_available" means plugged in
112
+
113
+ `chargePortState` can lag the door (e.g. "opening" until plugged in),
114
+ so prefer its next action. After an app command, next actions briefly
115
+ drop to "sna" / "open_not_available".
116
+ """
117
+ result: dict[str, Any] = {}
118
+ for s in m.closure:
119
+ cid = s.id
120
+ state_val = _present(s, "state")
121
+ if cid in _CLOSURE_MAP:
122
+ result[_CLOSURE_MAP[cid]] = _enum(
123
+ _CLOSURE_STATE_MAP, state_val, what="closure state"
124
+ )
125
+ else:
126
+ _LOGGER.debug("Unknown closure id %s (state %s)", cid, state_val)
127
+ if cid in _NEXT_ACTIONS:
128
+ field, key, names = _NEXT_ACTIONS[cid]
129
+ if (next_action := _present(s, field)) is not None:
130
+ result[key] = _enum(names, next_action, what=f"{key} value")
131
+ return result
132
+
133
+
134
+ @RVMDecoder.register("body.locks.states", body_pb2.LocksState)
135
+ def decode_locks(m: body_pb2.LocksState) -> dict[str, Any]:
136
+ """body.locks.states — lock state of every lockable closure.
137
+
138
+ Fields:
139
+ doorFrontLeftLocked, closureFrunkLocked, etc.: str ("locked" | "unlocked")
140
+ """
141
+ result: dict[str, Any] = {}
142
+ for s in m.lock:
143
+ lid = s.id
144
+ state_val = _present(s, "state")
145
+ if state_val is None:
146
+ continue
147
+ if lid in _LOCK_MAP:
148
+ result[_LOCK_MAP[lid]] = "locked" if state_val == 1 else "unlocked"
149
+ else:
150
+ _LOGGER.debug("Unknown lock id %s (state %s)", lid, state_val)
151
+ return result
152
+
153
+
154
+ @RVMDecoder.register("body.trailer.state", body_pb2.TrailerState)
155
+ def decode_trailer_state(m: body_pb2.TrailerState) -> dict[str, Any]:
156
+ """body.trailer.state — trailer presence, with or without brakes.
157
+
158
+ Fields:
159
+ trailerStatus: str
160
+ """
161
+ result: dict[str, Any] = {}
162
+ presence = _enum(
163
+ _TRAILER_PRESENCE_MAP, _present(m, "presence"), what="trailer presence"
164
+ )
165
+ if presence is not None:
166
+ result["trailerStatus"] = presence
167
+ return result
168
+
169
+
170
+ @RVMDecoder.register("body.wipers.fluid_level", body_pb2.WiperFluidLevel)
171
+ def decode_wiper_fluid_level(m: body_pb2.WiperFluidLevel) -> dict[str, Any]:
172
+ """body.wipers.fluid_level — washer fluid level.
173
+
174
+ Fields:
175
+ _wiperFluidLevel: int — raw (1 so far, presumably normal)
176
+ """
177
+ if (v := _present(m, "field_2")) is None:
178
+ return {}
179
+ return {"_wiperFluidLevel": v}
@@ -0,0 +1,246 @@
1
+ """Decoders for `charging.*` RVM topics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Final
6
+
7
+ from .core import RVMDecoder, _enum, _present
8
+ from .proto import charging_pb2
9
+
10
+ _DAY_OF_WEEK_MAP: Final[dict[int, str]] = {
11
+ 1: "sunday",
12
+ 2: "monday",
13
+ 3: "tuesday",
14
+ 4: "wednesday",
15
+ 5: "thursday",
16
+ 6: "friday",
17
+ 7: "saturday",
18
+ }
19
+
20
+
21
+ @RVMDecoder.register("charging.schedule.time_window", charging_pb2.ScheduleTimeWindow)
22
+ def decode_schedule_time_window(m: charging_pb2.ScheduleTimeWindow) -> dict[str, Any]:
23
+ """charging.schedule.time_window — the scheduled charging window.
24
+
25
+ Fields:
26
+ chargingScheduleEnabled: bool — False when no schedule is set
27
+ chargingScheduleStartDay, chargingScheduleEndDay: str
28
+ ("sunday" ... "saturday")
29
+ chargingScheduleStartTime, chargingScheduleEndTime: int — minutes
30
+ after local midnight on that day (1440 = end of day)
31
+ chargingScheduleDuration: int — minutes (10080 for all week)
32
+ chargingScheduleAmps: int — current limit (A)
33
+ chargingScheduleLocation: {"latitude": float, "longitude": float}
34
+
35
+ The days are the window's current/next occurrence; its daily/weekdays
36
+ recurrence isn't sent.
37
+ """
38
+ result: dict[str, Any] = {"chargingScheduleEnabled": m.enabled}
39
+ if not m.HasField("window"):
40
+ return result
41
+ window = m.window
42
+ result["chargingScheduleStartDay"] = _enum(
43
+ _DAY_OF_WEEK_MAP, window.start_day or None, what="schedule start day"
44
+ )
45
+ result["chargingScheduleEndDay"] = _enum(
46
+ _DAY_OF_WEEK_MAP, window.end_day or None, what="schedule end day"
47
+ )
48
+ result["chargingScheduleStartTime"] = window.start_time
49
+ result["chargingScheduleEndTime"] = window.end_time
50
+ result["chargingScheduleDuration"] = window.duration
51
+ if window.HasField("amps"):
52
+ result["chargingScheduleAmps"] = window.amps
53
+ if window.HasField("location"):
54
+ result["chargingScheduleLocation"] = {
55
+ "latitude": round(window.location.latitude, 6),
56
+ "longitude": round(window.location.longitude, 6),
57
+ }
58
+ return result
59
+
60
+
61
+ @RVMDecoder.register("charging.session.notification", charging_pb2.SessionNotification)
62
+ def decode_session_notification(m: charging_pb2.SessionNotification) -> dict[str, Any]:
63
+ """charging.session.notification — unmapped; field 1 as `_field1`."""
64
+ return {"_field1": m.field1} if m.HasField("field1") else {}
65
+
66
+
67
+ _REMOTE_COMMAND_MAP: Final[dict[int, str]] = {
68
+ 1: "start",
69
+ 2: "stop",
70
+ }
71
+
72
+
73
+ @RVMDecoder.register(
74
+ "charging.session.remote_command", charging_pb2.SessionRemoteCommand
75
+ )
76
+ def decode_session_remote_command(
77
+ m: charging_pb2.SessionRemoteCommand,
78
+ ) -> dict[str, Any]:
79
+ """charging.session.remote_command — the last start/stop command.
80
+
81
+ Fields:
82
+ chargingRemoteCommand: str ("start" | "stop")
83
+
84
+ Also sent when the vehicle holds ("stop") or resumes ("start")
85
+ charging around a schedule or limit change.
86
+ """
87
+ if not m.HasField("command"):
88
+ return {}
89
+ return {
90
+ "chargingRemoteCommand": _enum(
91
+ _REMOTE_COMMAND_MAP, m.command, what="charging remote command"
92
+ )
93
+ }
94
+
95
+
96
+ @RVMDecoder.register("charging.session.soc_slider", charging_pb2.SocSlider)
97
+ def decode_soc_slider(m: charging_pb2.SocSlider) -> dict[str, Any]:
98
+ """charging.session.soc_slider — the charge limit.
99
+
100
+ Fields:
101
+ batteryLimit: int (percent, 0-100)
102
+ """
103
+ if not m.HasField("soc_limit"):
104
+ return {}
105
+ return {"batteryLimit": m.soc_limit}
106
+
107
+
108
+ @RVMDecoder.register("charging.session.trip_target", charging_pb2.TripTarget)
109
+ def decode_trip_target(m: charging_pb2.TripTarget) -> dict[str, Any]:
110
+ """charging.session.trip_target — unmapped; field 2 as `_field2`.
111
+
112
+ 0xFFFF seems to mean "not set".
113
+ """
114
+ return {"_field2": m.field2} if m.HasField("field2") else {}
115
+
116
+
117
+ @RVMDecoder.register(
118
+ "charging.smart_charging.settings", charging_pb2.SmartChargingSettings
119
+ )
120
+ def decode_smart_charging_settings(
121
+ _m: charging_pb2.SmartChargingSettings,
122
+ ) -> dict[str, Any]:
123
+ """charging.smart_charging.settings — unmapped."""
124
+ return {}
125
+
126
+
127
+ @RVMDecoder.register(
128
+ "charging.smart_charging.smart_charging_info", charging_pb2.SmartChargingInfo
129
+ )
130
+ def decode_smart_charging_info(_m: charging_pb2.SmartChargingInfo) -> dict[str, Any]:
131
+ """charging.smart_charging.smart_charging_info — unmapped."""
132
+ return {}
133
+
134
+
135
+ @RVMDecoder.register(
136
+ "charging.smart_charging.weighted_charging_forecast",
137
+ charging_pb2.WeightedChargingForecast,
138
+ )
139
+ def decode_weighted_charging_forecast(
140
+ _m: charging_pb2.WeightedChargingForecast,
141
+ ) -> dict[str, Any]:
142
+ """charging.smart_charging.weighted_charging_forecast — unmapped."""
143
+ return {}
144
+
145
+
146
+ _CONNECTION_STATE_MAP: Final[dict[int, str]] = {
147
+ 1: "disconnected",
148
+ 2: "connected",
149
+ }
150
+
151
+ _CHARGING_STATE_MAP: Final[dict[int, str]] = {
152
+ 1: "charging_ready",
153
+ 2: "charging_connecting",
154
+ 3: "charging_active",
155
+ 4: "charging_complete",
156
+ 5: "charging_scheduled",
157
+ 6: "charging_vehicle_error",
158
+ 7: "charging_station_error",
159
+ 8: "charging_stopped_by_user",
160
+ 9: "charging_stopped_by_station",
161
+ 10: "charging_payment_error",
162
+ 11: "charging_cert_error",
163
+ 12: "charging_tls_error",
164
+ 13: "charging_error_ac_adapter_used_on_dc",
165
+ 14: "charging_error_dc_adapter_used_on_ac",
166
+ 15: "charging_error_incompatible_charger",
167
+ 16: "charging_sd_compensation",
168
+ 17: "waiting_on_charger",
169
+ 18: "charging_error_not_ready_or_incompatible_charger",
170
+ 19: "charging_vehicle_stopped",
171
+ 20: "charging_payment_error_start_rivian_app",
172
+ 21: "charging_tls_error_unknown_charger",
173
+ 22: "charging_tls_error_unexpected_fail",
174
+ 23: "charging_tls_error_start_rivian_app",
175
+ }
176
+
177
+
178
+ @RVMDecoder.register("charging.session.status", charging_pb2.SessionStatus)
179
+ def decode_charging_status(m: charging_pb2.SessionStatus) -> dict[str, Any]:
180
+ """charging.session.status — plug and charging state.
181
+
182
+ Fields:
183
+ connectionState: str ("connected" | "disconnected")
184
+ chargerState: str — GraphQL chargerState values, e.g.
185
+ "charging_ready" (no session, or unplugged),
186
+ "charging_connecting", "charging_active", "charging_complete",
187
+ "charging_scheduled", "charging_stopped_by_user"
188
+ isActive: bool — true from a session's start until it ends
189
+ """
190
+ result: dict[str, Any] = {}
191
+ if m.HasField("connection_state"):
192
+ result["connectionState"] = _enum(
193
+ _CONNECTION_STATE_MAP, m.connection_state, what="charging connection state"
194
+ )
195
+ if m.HasField("charging_state"):
196
+ result["chargerState"] = _enum(
197
+ _CHARGING_STATE_MAP, m.charging_state, what="charging state"
198
+ )
199
+ result["isActive"] = m.is_active
200
+ return result
201
+
202
+
203
+ @RVMDecoder.register("charging.session.time_estimation", charging_pb2.TimeEstimation)
204
+ def decode_time_estimation(m: charging_pb2.TimeEstimation) -> dict[str, Any]:
205
+ """charging.session.time_estimation — time to the charge limit.
206
+
207
+ Fields:
208
+ timeToEndOfCharge: int (minutes; 0 when not charging)
209
+ """
210
+ return {"timeToEndOfCharge": m.estimated_time_remaining}
211
+
212
+
213
+ _CHARGER_STATUS_MAP: Final[dict[int, str]] = {
214
+ 1: "chrgr_sts_not_connected",
215
+ 2: "chrgr_sts_connected_no_chrg",
216
+ 3: "chrgr_sts_connected_charging",
217
+ }
218
+
219
+
220
+ @RVMDecoder.register("charging.energy.state", charging_pb2.EnergyState)
221
+ def decode_energy_state(m: charging_pb2.EnergyState) -> dict[str, Any]:
222
+ """charging.energy.state — charger connection status.
223
+
224
+ Fields:
225
+ chargerStatus: str — GraphQL chargerStatus values
226
+ ("chrgr_sts_not_connected" | "chrgr_sts_connected_no_chrg" |
227
+ "chrgr_sts_connected_charging"); inferred
228
+ _field1, _field3, _field11: int — raw
229
+ """
230
+ result: dict[str, Any] = {}
231
+ if (v := _present(m, "charger_status")) is not None:
232
+ result["chargerStatus"] = _enum(_CHARGER_STATUS_MAP, v, what="charger status")
233
+ for num in (1, 3, 11):
234
+ if (v := _present(m, f"field_{num}")) is not None:
235
+ result[f"_field{num}"] = v
236
+ return result
237
+
238
+
239
+ @RVMDecoder.register("charging.session.power", charging_pb2.SessionPower)
240
+ def decode_session_power(m: charging_pb2.SessionPower) -> dict[str, Any]:
241
+ """charging.session.power — live charging power, about every 5 seconds.
242
+
243
+ Fields:
244
+ power: float (kW; 0 when not charging)
245
+ """
246
+ return {"power": round(m.power, 2)}