rivian-python-client 2.0.0__tar.gz → 2.1.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.
@@ -0,0 +1,34 @@
1
+ # Version file (auto-generated by Hatchling)
2
+ src/rivian/__version__.py
3
+
4
+ # IDE generated files
5
+ .vscode/
6
+
7
+ # Python generated files
8
+ __pycache__/
9
+ *.pyc
10
+ .pytest_cache
11
+
12
+ # Packaging generated files
13
+ *.egg-info
14
+
15
+ # Tox
16
+ .tox
17
+
18
+ # Code coverage
19
+ .coverage
20
+
21
+ # JS files
22
+ */node_modules/
23
+ *package-lock.json
24
+
25
+ dist/
26
+
27
+ # Environments
28
+ .env
29
+ .venv
30
+ env/
31
+ venv/
32
+ ENV/
33
+ env.bak/
34
+ venv.bak/
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: rivian-python-client
3
+ Version: 2.1.0
4
+ Summary: Rivian API Client (Unofficial)
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
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: aiohttp>=3.0.0
9
+ Requires-Dist: backports-strenum>=1.2.4; python_version < '3.11'
10
+ Requires-Dist: cryptography>=41.0.1
11
+ Requires-Dist: typing-extensions>=4.14.1; python_version < '3.11'
12
+ Provides-Extra: ble
13
+ Requires-Dist: bleak>=0.21; extra == 'ble'
14
+ Requires-Dist: dbus-fast>=2.11.0; (sys_platform == 'linux') and extra == 'ble'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Python: Rivian API Client
18
+
19
+ Currently a Work In Progress
20
+
21
+ ## Dependencies
22
+
23
+ [uv](https://docs.astral.sh/uv/)
24
+
25
+ ```
26
+ curl -LsSf https://astral.sh/uv/install.sh | sh
27
+ ```
28
+
29
+ ## Setup
30
+
31
+ Install project dependencies into the uv virtual environment and run pre-commit
32
+
33
+ ```
34
+ uv sync --all-extras
35
+ pre-commit install
36
+ ```
37
+
38
+ ## Run Tests
39
+
40
+ ```
41
+ uv run pytest
42
+ ```
@@ -0,0 +1,26 @@
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
+ ```
@@ -0,0 +1,66 @@
1
+ [project]
2
+ name = "rivian-python-client"
3
+ description = "Rivian API Client (Unofficial)"
4
+ authors = [
5
+ { name = "Brian Retterer", email = "bretterer@gmail.com" },
6
+ { name = "J.R. Gutierrez", email = "jrgutier@gmail.com" },
7
+ { name = "Nathan Spencer", email = "natekspencer@gmail.com" },
8
+ { name = "Trevor Mack", email = "drummer8001@gmail.com" },
9
+ ]
10
+ requires-python = ">=3.10"
11
+ readme = "README.md"
12
+ license = "MIT"
13
+ dependencies = [
14
+ "aiohttp (>=3.0.0)",
15
+ "cryptography (>=41.0.1)",
16
+ "backports-strenum (>=1.2.4) ; python_version < '3.11'",
17
+ "typing_extensions (>=4.14.1) ; python_version < '3.11'",
18
+ ]
19
+ dynamic = ["version"]
20
+
21
+ [project.optional-dependencies]
22
+ ble = [
23
+ "bleak (>=0.21)",
24
+ "dbus-fast (>=2.11.0) ; sys_platform == 'linux'",
25
+ ]
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "pytest>=9.0,<10.0",
30
+ "pytest-asyncio>=1.0,<1.5",
31
+ "python-dotenv>=1.0,<1.3",
32
+ "aresponses>=3.0,<4.0",
33
+ "mypy>=1.7,<3.0",
34
+ "ruff>=0.16,<0.17",
35
+ "pre-commit>=4.6.1,<5",
36
+ ]
37
+
38
+ [tool.hatch.build.hooks.version]
39
+ path = "src/rivian/__version__.py"
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ include = ["src/rivian"]
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ include = ["src/rivian"]
46
+
47
+ [tool.hatch.build.targets.wheel.sources]
48
+ "src/rivian" = "rivian"
49
+
50
+ [tool.hatch.version]
51
+ source = "uv-dynamic-versioning"
52
+
53
+ [tool.uv]
54
+ default-groups = "all"
55
+ cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true }}]
56
+
57
+ [tool.uv-dynamic-versioning]
58
+ pattern = "default-unprefixed"
59
+
60
+ [tool.ruff.lint.isort]
61
+ combine-as-imports = true
62
+ split-on-trailing-comma = false
63
+
64
+ [build-system]
65
+ requires = ["hatchling", "uv-dynamic-versioning"]
66
+ build-backend = "hatchling.build"
@@ -1,6 +1,7 @@
1
1
  """Asynchronous Python client for the Rivian API."""
2
2
 
3
+ from .__version__ import __version__
3
4
  from .const import VehicleCommand
4
5
  from .rivian import Rivian
5
6
 
6
- __all__ = ["Rivian", "VehicleCommand"]
7
+ __all__ = ["Rivian", "VehicleCommand", "__version__"]
@@ -0,0 +1,4 @@
1
+ # This file is auto-generated by Hatchling. As such, do not:
2
+ # - modify
3
+ # - track in version control e.g. be sure to add to .gitignore
4
+ __version__ = VERSION = '2.1.0'
@@ -33,7 +33,7 @@ class BleNotificationResponse:
33
33
 
34
34
  def __init__(self) -> None:
35
35
  """Initialize the BLE notification response helper."""
36
- self.data: bytes | None = None
36
+ self.data: bytearray | None = None
37
37
  self.event = asyncio.Event()
38
38
 
39
39
  def notification_handler(self, _, notification_data: bytearray) -> None:
@@ -115,7 +115,7 @@ async def pair_phone(
115
115
 
116
116
  _LOGGER.debug("Successfully paired with %s", device)
117
117
  return True
118
- except Exception as ex: # pylint: disable=broad-except
118
+ except Exception as ex: # pylint: disable=broad-except # noqa: BLE001
119
119
  _LOGGER.debug(
120
120
  "Couldn't connect to %s. "
121
121
  'Make sure you are in the correct vehicle and have selected "Set Up" for the appropriate key and try again'
@@ -157,10 +157,10 @@ async def set_bluez_pairable(device: BLEDevice) -> bool:
157
157
  introspection = await bus.introspect("org.bluez", path)
158
158
  pobject = bus.get_proxy_object("org.bluez", path, introspection)
159
159
  iface = pobject.get_interface("org.bluez.Adapter1")
160
- if not await iface.get_pairable():
161
- await iface.set_pairable(True)
160
+ if not await iface.get_pairable(): # type: ignore[attr-defined]
161
+ await iface.set_pairable(True) # type: ignore[attr-defined]
162
162
  bus.disconnect()
163
- except Exception as ex: # pylint: disable=broad-except
163
+ except Exception as ex: # pylint: disable=broad-except # noqa: BLE001
164
164
  _LOGGER.error(ex)
165
165
  return False
166
166
 
@@ -0,0 +1,628 @@
1
+ """Decoder for Rivian Parallax protobuf payloads.
2
+
3
+ Decodes base64-encoded protobuf binary payloads from the Parallax WebSocket
4
+ subscription into structured Python dicts.
5
+
6
+ Reference: https://github.com/kaedenbrinkman/rivian-api (RivDocs)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import logging
13
+ import struct
14
+ from collections.abc import Callable
15
+ from datetime import datetime, timezone
16
+ from typing import Any
17
+
18
+ _LOGGER = logging.getLogger(__name__)
19
+
20
+ CLOSURE_MAP = {
21
+ 1: "doorFrontLeftClosed",
22
+ 2: "doorFrontRightClosed",
23
+ 3: "doorRearLeftClosed",
24
+ 4: "doorRearRightClosed",
25
+ 5: "closureFrunkClosed",
26
+ 6: "closureSideBinLeftClosed",
27
+ 7: "closureLiftgateClosed",
28
+ }
29
+
30
+ LOCK_MAP = {
31
+ 1: "doorFrontLeftLocked",
32
+ 2: "doorFrontRightLocked",
33
+ 3: "doorRearLeftLocked",
34
+ 4: "doorRearRightLocked",
35
+ 5: "closureFrunkLocked",
36
+ 7: "closureLiftgateLocked",
37
+ }
38
+
39
+ POWER_STATE_MAP = {
40
+ 1: "sleep",
41
+ 2: "standby",
42
+ 3: "ready",
43
+ 4: "go",
44
+ }
45
+
46
+ TIRE_POSITION_MAP = {
47
+ 1: "FrontLeft",
48
+ 2: "FrontRight",
49
+ 3: "RearLeft",
50
+ 4: "RearRight",
51
+ }
52
+
53
+
54
+ def _decode_varint(data: bytes, offset: int) -> tuple[int, int]:
55
+ """Decode a protobuf varint, return (value, new_offset)."""
56
+ result = 0
57
+ shift = 0
58
+ while offset < len(data):
59
+ byte = data[offset]
60
+ result |= (byte & 0x7F) << shift
61
+ shift += 7
62
+ offset += 1
63
+ if not (byte & 0x80):
64
+ break
65
+ return result, offset
66
+
67
+
68
+ def _decode_protobuf_fields(data: bytes) -> list[tuple[int, int, Any]]:
69
+ """Decode raw protobuf bytes into a list of (field_number, wire_type, value) tuples."""
70
+ fields = []
71
+ i = 0
72
+ while i < len(data):
73
+ tag, i = _decode_varint(data, i)
74
+ field_num = tag >> 3
75
+ wire_type = tag & 0x07
76
+ value: Any
77
+
78
+ if wire_type == 0: # Varint
79
+ value, i = _decode_varint(data, i)
80
+ fields.append((field_num, wire_type, value))
81
+ elif wire_type == 1: # 64-bit float
82
+ if i + 8 <= len(data):
83
+ value = struct.unpack("<d", data[i : i + 8])[0]
84
+ i += 8
85
+ fields.append((field_num, wire_type, value))
86
+ else:
87
+ break
88
+ elif wire_type == 2: # Length-delimited
89
+ length, i = _decode_varint(data, i)
90
+ if i + length <= len(data):
91
+ value = data[i : i + length]
92
+ i += length
93
+ fields.append((field_num, wire_type, value))
94
+ else:
95
+ break
96
+ elif wire_type == 5: # 32-bit float
97
+ if i + 4 <= len(data):
98
+ value = struct.unpack("<f", data[i : i + 4])[0]
99
+ i += 4
100
+ fields.append((field_num, wire_type, value))
101
+ else:
102
+ break
103
+ else:
104
+ _LOGGER.debug("Unknown wire type %d for field %d", wire_type, field_num)
105
+ break
106
+
107
+ return fields
108
+
109
+
110
+ def decode_battery_state(payload: str) -> dict[str, Any]:
111
+ """Decode energy.high_voltage.battery_state.
112
+
113
+ Returns dict with keys:
114
+ - soc: float (percentage, 0-100)
115
+ - packEnergyKwh: float
116
+ - rangeKm: float (if present)
117
+ """
118
+ if not payload:
119
+ return {}
120
+ try:
121
+ data = base64.b64decode(payload)
122
+ fields = _decode_protobuf_fields(data)
123
+ result: dict[str, Any] = {}
124
+
125
+ for field_num, wire_type, value in fields:
126
+ if field_num == 1 and wire_type == 2:
127
+ # Nested charge_state message
128
+ inner_fields = _decode_protobuf_fields(value)
129
+ for inner_num, inner_wt, inner_val in inner_fields:
130
+ if inner_num == 1 and inner_wt == 1: # soc (float)
131
+ result["soc"] = round(inner_val, 2)
132
+ elif inner_num == 2 and inner_wt == 1: # packEnergyKwh (float)
133
+ result["packEnergyKwh"] = round(inner_val, 2)
134
+ elif inner_num == 3 and inner_wt == 5: # rangeKm (float)
135
+ result["rangeKm"] = round(inner_val, 1)
136
+
137
+ return result
138
+ except Exception:
139
+ _LOGGER.debug("Failed to decode battery_state payload", exc_info=True)
140
+ return {}
141
+
142
+
143
+ def decode_cabin_temperatures(payload: str) -> dict[str, Any]:
144
+ """Decode comfort.cabin.cabin_temperatures.
145
+
146
+ Returns dict with keys:
147
+ - cabinClimateInteriorTemperature: float (Celsius)
148
+ - cabinClimateDriverTemperature: float (Celsius)
149
+ """
150
+ if not payload:
151
+ return {}
152
+ try:
153
+ data = base64.b64decode(payload)
154
+ fields = _decode_protobuf_fields(data)
155
+ result: dict[str, Any] = {}
156
+
157
+ for field_num, wire_type, value in fields:
158
+ if field_num == 3 and wire_type == 5: # interior temp (float, Celsius)
159
+ result["cabinClimateInteriorTemperature"] = round(value, 1)
160
+ if field_num == 4 and wire_type == 5: # interior temp (float, Celsius)
161
+ result["cabinClimateDriverTemperature"] = round(value, 1)
162
+
163
+ return result
164
+ except Exception:
165
+ _LOGGER.debug("Failed to decode cabin temperatures payload", exc_info=True)
166
+ return {}
167
+
168
+
169
+ def decode_charge_session_breakdown(payload: str) -> dict[str, Any]:
170
+ """Decode energy_edge_compute.graphs.charge_session_breakdown.
171
+
172
+ Returns dict with keys matching legacy getLiveSessionData field names:
173
+ - totalChargedEnergy: float (kWh total)
174
+ - power: float (kW, current charge rate)
175
+ - timeElapsed: int (seconds, estimated)
176
+ - rangeAddedThisSession: float (km, estimated from energy)
177
+ """
178
+ if not payload:
179
+ return {}
180
+ try:
181
+ data = base64.b64decode(payload)
182
+ fields = _decode_protobuf_fields(data)
183
+ result: dict[str, Any] = {}
184
+
185
+ total_kwh = 0.0
186
+
187
+ for field_num, wire_type, value in fields:
188
+ if field_num == 1 and wire_type == 5: # totalKwh (float)
189
+ total_kwh = round(value, 4)
190
+ result["totalChargedEnergy"] = total_kwh
191
+ elif field_num == 9 and wire_type == 5: # currentPower (float, kW)
192
+ result["power"] = round(value, 2)
193
+ elif field_num == 7 and wire_type == 0: # timeRemainingMins or elapsed secs
194
+ result["_time_field_7"] = value
195
+ elif field_num == 10 and wire_type == 0: # charge power integer (kW)
196
+ if "power" not in result:
197
+ result["power"] = float(value)
198
+ elif field_num == 13 and wire_type == 0: # chargingState enum
199
+ result["_charging_state"] = value
200
+
201
+ # Estimate range added: ~3.5 km/kWh (~2.17 mi/kWh) is a typical Rivian average
202
+ if total_kwh > 0:
203
+ result["rangeAddedThisSession"] = round(total_kwh * 3.5, 1)
204
+
205
+ # Derive charge rate (km/h) from current power (kW)
206
+ if "power" in result:
207
+ p = result["power"]
208
+ result["kilometersChargedPerHour"] = round(p * 3.5, 1) if p > 0 else 0.0
209
+
210
+ return result
211
+ except Exception:
212
+ _LOGGER.debug(
213
+ "Failed to decode charge_session_breakdown payload", exc_info=True
214
+ )
215
+ return {}
216
+
217
+
218
+ def decode_charging_graph_global(payload: str) -> dict[str, Any]:
219
+ """Decode energy_edge_compute.graphs.charging_graph_global.
220
+
221
+ Returns dict with keys:
222
+ - startTime: str (ISO format timestamp of session start)
223
+ - timeElapsed: int (seconds elapsed since session start)
224
+ - power: float (kW, latest segment power)
225
+ """
226
+ if not payload:
227
+ return {}
228
+ try:
229
+ data = base64.b64decode(payload)
230
+ outer = _decode_protobuf_fields(data)
231
+ segments = []
232
+ for field_num, wire_type, value in outer:
233
+ if field_num == 1 and wire_type == 2:
234
+ inner = _decode_protobuf_fields(value)
235
+ seg: dict[str, Any] = {}
236
+ for in_num, in_wt, in_val in inner:
237
+ if in_num == 1 and in_wt == 0:
238
+ seg["soc"] = in_val
239
+ elif in_num == 2 and in_wt == 5:
240
+ seg["power"] = round(in_val, 2)
241
+ elif in_num == 3 and in_wt == 0:
242
+ seg["start_ms"] = in_val
243
+ elif in_num == 4 and in_wt == 0:
244
+ seg["end_ms"] = in_val
245
+ elif in_num == 6 and in_wt == 0:
246
+ seg["state"] = in_val
247
+ segments.append(seg)
248
+
249
+ if not segments:
250
+ return {}
251
+
252
+ active_segments = [
253
+ s for s in segments if s.get("power", 0) > 0 or s.get("state") == 3
254
+ ]
255
+
256
+ first_seg = active_segments[0] if active_segments else segments[0]
257
+ result: dict[str, Any] = {}
258
+
259
+ if "start_ms" in first_seg:
260
+ st = datetime.fromtimestamp(first_seg["start_ms"] / 1000, timezone.utc)
261
+ result["startTime"] = st.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
262
+
263
+ if active_segments:
264
+ result["timeElapsed"] = sum(
265
+ max(0, int((s["end_ms"] - s["start_ms"]) / 1000))
266
+ for s in active_segments
267
+ if "end_ms" in s and "start_ms" in s
268
+ )
269
+ else:
270
+ result["timeElapsed"] = 0
271
+
272
+ latest_segment = segments[-1]
273
+ if (
274
+ "power" in latest_segment
275
+ and latest_segment.get("power", 0) > 0
276
+ and latest_segment.get("state") != 8
277
+ ):
278
+ result["power"] = latest_segment["power"]
279
+ result["kilometersChargedPerHour"] = round(result["power"] * 3.5, 1)
280
+ else:
281
+ result["power"] = 0.0
282
+ result["kilometersChargedPerHour"] = 0.0
283
+
284
+ return result
285
+ except Exception:
286
+ _LOGGER.debug("Failed to decode charging_graph_global payload", exc_info=True)
287
+ return {}
288
+
289
+
290
+ def decode_charging_session_status(payload: str) -> dict[str, Any]:
291
+ """Decode charging.session.status.
292
+
293
+ Returns dict with keys:
294
+ - plugConnectionStatus: int (enum)
295
+ - displayStatus: int (enum)
296
+ - evseType: int (enum)
297
+ """
298
+ if not payload:
299
+ return {}
300
+ try:
301
+ data = base64.b64decode(payload)
302
+ fields = _decode_protobuf_fields(data)
303
+ result: dict[str, Any] = {}
304
+
305
+ for field_num, wire_type, value in fields:
306
+ if field_num == 1 and wire_type == 0:
307
+ result["plugConnectionStatus"] = value
308
+ elif field_num == 2 and wire_type == 0:
309
+ result["displayStatus"] = value
310
+ elif field_num == 3 and wire_type == 0:
311
+ result["evseType"] = value
312
+
313
+ return result
314
+ except Exception:
315
+ _LOGGER.debug("Failed to decode charging.session.status payload", exc_info=True)
316
+ return {}
317
+
318
+
319
+ def decode_closures(payload: str) -> dict[str, Any]:
320
+ """Decode body.closures.states.
321
+
322
+ Returns dict with keys:
323
+ - doorFrontLeftClosed, doorFrontRightClosed, closureFrunkClosed, etc. ("closed" / "open")
324
+ """
325
+ if not payload:
326
+ return {}
327
+ try:
328
+ data = base64.b64decode(payload)
329
+ fields = _decode_protobuf_fields(data)
330
+ result: dict[str, Any] = {}
331
+
332
+ for field_num, wire_type, value in fields:
333
+ if field_num == 1 and wire_type == 2: # Repeated nested closure state
334
+ inner = _decode_protobuf_fields(value)
335
+ cid = None
336
+ state_val = None
337
+ for in_num, in_type, in_val in inner:
338
+ if in_num == 1 and in_type == 0:
339
+ cid = in_val
340
+ elif in_num == 2 and in_type == 0:
341
+ state_val = in_val
342
+
343
+ if cid and cid in CLOSURE_MAP and state_val is not None:
344
+ # 1 = open, 2 = closed
345
+ result[CLOSURE_MAP[cid]] = "closed" if state_val == 2 else "open"
346
+
347
+ return result
348
+ except Exception:
349
+ _LOGGER.debug("Failed to decode closures payload", exc_info=True)
350
+ return {}
351
+
352
+
353
+ def decode_defrost(payload: str) -> dict[str, Any]:
354
+ """Decode comfort.cabin.defrost_defog_status.
355
+
356
+ Returns dict with keys:
357
+ - defrostDefogStatus: str ("Defrost", "Off")
358
+ """
359
+ if not payload:
360
+ return {}
361
+ try:
362
+ data = base64.b64decode(payload)
363
+ fields = _decode_protobuf_fields(data)
364
+ result: dict[str, Any] = {}
365
+ for field_num, wire_type, value in fields:
366
+ if field_num == 1 and wire_type == 0:
367
+ result["defrostDefogStatus"] = "Defrost" if value == 2 else "Off"
368
+ return result
369
+ except Exception:
370
+ _LOGGER.debug("Failed to decode defrost payload", exc_info=True)
371
+ return {}
372
+
373
+
374
+ def decode_gnss(payload: str) -> dict[str, Any]:
375
+ """Decode dynamics.vehicle.gnss.
376
+
377
+ Returns dict with keys:
378
+ - gnssLocation: {"latitude": float, "longitude": float, "timeStamp": str}
379
+ - gnssAltitude: float (meters)
380
+ """
381
+ if not payload:
382
+ return {}
383
+ try:
384
+ data = base64.b64decode(payload)
385
+ fields = _decode_protobuf_fields(data)
386
+ lat = None
387
+ lon = None
388
+ alt = None
389
+ for field_num, wire_type, value in fields:
390
+ if field_num == 1 and wire_type == 1: # latitude (float)
391
+ lat = round(value, 6)
392
+ elif field_num == 2 and wire_type == 1: # longitude (float)
393
+ lon = round(value, 6)
394
+ elif field_num == 3 and wire_type == 1: # altitude (float)
395
+ alt = round(value, 1)
396
+
397
+ result: dict[str, Any] = {}
398
+ if lat is not None and lon is not None:
399
+ now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f%z")
400
+ result["gnssLocation"] = {
401
+ "latitude": lat,
402
+ "longitude": lon,
403
+ "timeStamp": now_iso,
404
+ }
405
+ if alt is not None:
406
+ result["gnssAltitude"] = alt
407
+ return result
408
+ except Exception:
409
+ _LOGGER.debug("Failed to decode dynamics.vehicle.gnss payload", exc_info=True)
410
+ return {}
411
+
412
+
413
+ def decode_locks(payload: str) -> dict[str, Any]:
414
+ """Decode body.locks.states.
415
+
416
+ Returns dict with keys:
417
+ - doorFrontLeftLocked, closureFrunkLocked, etc. ("locked" / "unlocked")
418
+ """
419
+ if not payload:
420
+ return {}
421
+ try:
422
+ data = base64.b64decode(payload)
423
+ fields = _decode_protobuf_fields(data)
424
+ result: dict[str, Any] = {}
425
+
426
+ for field_num, wire_type, value in fields:
427
+ if field_num == 1 and wire_type == 2: # Repeated nested lock state
428
+ inner = _decode_protobuf_fields(value)
429
+ lid = None
430
+ state_val = None
431
+ for in_num, in_type, in_val in inner:
432
+ if in_num == 1 and in_type == 0:
433
+ lid = in_val
434
+ elif in_num == 2 and in_type == 0:
435
+ state_val = in_val
436
+
437
+ if lid and lid in LOCK_MAP and state_val is not None:
438
+ # 1 = locked, 2 = unlocked
439
+ result[LOCK_MAP[lid]] = "locked" if state_val == 1 else "unlocked"
440
+
441
+ return result
442
+ except Exception:
443
+ _LOGGER.debug("Failed to decode locks payload", exc_info=True)
444
+ return {}
445
+
446
+
447
+ def decode_odometer(payload: str) -> dict[str, Any]:
448
+ """Decode dynamics.vehicle.odometer.
449
+
450
+ Returns dict with keys:
451
+ - vehicleMileage: float (meters)
452
+ """
453
+ if not payload:
454
+ return {}
455
+ try:
456
+ data = base64.b64decode(payload)
457
+ fields = _decode_protobuf_fields(data)
458
+ result: dict[str, Any] = {}
459
+
460
+ for field_num, wire_type, value in fields:
461
+ if field_num == 1 and wire_type == 0:
462
+ # Value is distance in km; HA expects meters
463
+ result["vehicleMileage"] = value * 1000
464
+
465
+ return result
466
+ except Exception:
467
+ _LOGGER.debug("Failed to decode odometer payload", exc_info=True)
468
+ return {}
469
+
470
+
471
+ def decode_power_state(payload: str) -> dict[str, Any]:
472
+ """Decode vehicle.power.state.
473
+
474
+ Returns dict with keys:
475
+ - powerState: str ("sleep", "standby", "ready", "go")
476
+ """
477
+ if not payload:
478
+ return {}
479
+ try:
480
+ data = base64.b64decode(payload)
481
+ fields = _decode_protobuf_fields(data)
482
+ result: dict[str, Any] = {}
483
+
484
+ for field_num, wire_type, value in fields:
485
+ if field_num == 1 and wire_type == 0:
486
+ result["powerState"] = POWER_STATE_MAP.get(value, "standby")
487
+
488
+ return result
489
+ except Exception:
490
+ _LOGGER.debug("Failed to decode power state payload", exc_info=True)
491
+ return {}
492
+
493
+
494
+ def decode_preconditioning(payload: str) -> dict[str, Any]:
495
+ """Decode comfort.cabin.cabin_preconditioning_status.
496
+
497
+ Returns dict with keys:
498
+ - cabinPreconditioningStatus: str ("active", "initiate", "off")
499
+ """
500
+ if not payload:
501
+ return {"cabinPreconditioningStatus": "off"}
502
+ try:
503
+ data = base64.b64decode(payload)
504
+ fields = _decode_protobuf_fields(data)
505
+ status_val = None
506
+ for field_num, wire_type, value in fields:
507
+ if field_num == 1 and wire_type == 0:
508
+ status_val = value
509
+
510
+ if status_val == 4:
511
+ return {"cabinPreconditioningStatus": "active"}
512
+ elif status_val in (1, 2):
513
+ return {"cabinPreconditioningStatus": "initiate"}
514
+ return {"cabinPreconditioningStatus": "off"}
515
+ except Exception:
516
+ _LOGGER.debug("Failed to decode preconditioning payload", exc_info=True)
517
+ return {}
518
+
519
+
520
+ def decode_time_estimation(payload: str) -> dict[str, Any]:
521
+ """Decode charging.session.time_estimation.
522
+
523
+ Returns dict with keys:
524
+ - timeToEndOfCharge: int (seconds remaining)
525
+ """
526
+ if not payload:
527
+ return {}
528
+ try:
529
+ data = base64.b64decode(payload)
530
+ fields = _decode_protobuf_fields(data)
531
+ result: dict[str, Any] = {}
532
+
533
+ for field_num, wire_type, value in fields:
534
+ if field_num == 1 and wire_type == 0:
535
+ result["timeToEndOfCharge"] = value
536
+
537
+ return result
538
+ except Exception:
539
+ _LOGGER.debug("Failed to decode time_estimation payload", exc_info=True)
540
+ return {}
541
+
542
+
543
+ def decode_tires(payload: str) -> dict[str, Any]:
544
+ """Decode dynamics.tires.state.
545
+
546
+ Returns dict with keys:
547
+ - tirePressureFrontLeft, tirePressureFrontRight, etc. (bar)
548
+ - tirePressureStatusFrontLeft, etc. ("OK")
549
+ """
550
+ if not payload:
551
+ return {}
552
+ try:
553
+ data = base64.b64decode(payload)
554
+ fields = _decode_protobuf_fields(data)
555
+ result: dict[str, Any] = {}
556
+
557
+ for field_num, wire_type, value in fields:
558
+ if field_num == 2 and wire_type == 2: # Repeated nested tire state
559
+ inner = _decode_protobuf_fields(value)
560
+ pos = None
561
+ status = None
562
+ pressure = None
563
+ for in_num, in_type, in_val in inner:
564
+ if in_num == 1 and in_type == 0:
565
+ pos = in_val
566
+ elif in_num == 2 and in_type == 0:
567
+ status = "OK" if in_val == 1 else "Warning"
568
+ elif in_num == 3 and in_type == 1: # 64-bit float (bar)
569
+ pressure = round(in_val, 2)
570
+
571
+ if pos and pos in TIRE_POSITION_MAP:
572
+ suffix = TIRE_POSITION_MAP[pos]
573
+ if pressure is not None:
574
+ result[f"tirePressure{suffix}"] = pressure
575
+ if status is not None:
576
+ result[f"tirePressureStatus{suffix}"] = status
577
+
578
+ return result
579
+ except Exception:
580
+ _LOGGER.debug("Failed to decode tires payload", exc_info=True)
581
+ return {}
582
+
583
+
584
+ # Map of RVM topic -> decoder function
585
+ RVM_DECODERS: dict[str, Callable[[str], dict[str, Any]]] = {
586
+ "body.closures.states": decode_closures,
587
+ "body.locks.states": decode_locks,
588
+ "charging.session.status": decode_charging_session_status,
589
+ "charging.session.time_estimation": decode_time_estimation,
590
+ "comfort.cabin.cabin_preconditioning_status": decode_preconditioning,
591
+ "comfort.cabin.cabin_temperatures": decode_cabin_temperatures,
592
+ "comfort.cabin.defrost_defog_status": decode_defrost,
593
+ "dynamics.tires.state": decode_tires,
594
+ "dynamics.vehicle.gnss": decode_gnss,
595
+ "dynamics.vehicle.odometer": decode_odometer,
596
+ "energy.high_voltage.battery_state": decode_battery_state,
597
+ "energy_edge_compute.graphs.charge_session_breakdown": decode_charge_session_breakdown,
598
+ "energy_edge_compute.graphs.charging_graph_global": decode_charging_graph_global,
599
+ "vehicle.power.state": decode_power_state,
600
+ }
601
+
602
+ # Full list of Parallax RVMs subscribed for vehicle & charging telemetry
603
+ PARALLAX_RVMS: list[str] = list(RVM_DECODERS.keys())
604
+ CHARGING_RVMS: list[str] = [
605
+ "charging.session.notification",
606
+ "charging.session.remote_command",
607
+ "charging.session.soc_slider",
608
+ "charging.session.status",
609
+ "charging.session.time_estimation",
610
+ "energy.high_voltage.battery_state",
611
+ "energy_edge_compute.graphs.charge_session_breakdown",
612
+ "energy_edge_compute.graphs.charging_graph_global",
613
+ ]
614
+
615
+
616
+ def decode_parallax_message(
617
+ rvm: str, payload: str, **kwargs: Any
618
+ ) -> dict[str, Any] | None:
619
+ """Decode a Parallax message payload given its RVM topic.
620
+
621
+ Accepts the GraphQL message fields directly (rvm, payload, and optional kwargs/timestamp).
622
+ Returns a dict of decoded fields, or None if no decoder exists for this RVM.
623
+ """
624
+ decoder = RVM_DECODERS.get(rvm)
625
+ if decoder is None:
626
+ _LOGGER.warning("Unknown Parallax RVM topic %s", rvm)
627
+ return None
628
+ return decoder(payload)
@@ -8,8 +8,8 @@ import socket
8
8
  import sys
9
9
  import time
10
10
  import uuid
11
- from collections.abc import Callable
12
- from typing import Any, Type
11
+ from collections.abc import Awaitable, Callable
12
+ from typing import Any
13
13
  from warnings import warn
14
14
 
15
15
  import aiohttp
@@ -33,13 +33,17 @@ from .exceptions import (
33
33
  RivianTemporarilyLockedError,
34
34
  RivianUnauthenticated,
35
35
  )
36
+ from .parallax import PARALLAX_RVMS
36
37
  from .utils import generate_vehicle_command_hmac
37
38
  from .ws_monitor import WebSocketMonitor
38
39
 
39
40
  if sys.version_info >= (3, 11):
40
41
  import asyncio as async_timeout
42
+ from typing import Self
41
43
  else:
42
44
  import async_timeout
45
+ from typing_extensions import Self
46
+
43
47
 
44
48
  _LOGGER = logging.getLogger(__name__)
45
49
 
@@ -82,7 +86,7 @@ LIVE_SESSION_VALUE_RECORD_KEYS = {
82
86
  }
83
87
  VALUE_RECORD_TEMPLATE = "{ __typename value updatedAt }"
84
88
 
85
- ERROR_CODE_CLASS_MAP: dict[str, Type[RivianApiException]] = {
89
+ ERROR_CODE_CLASS_MAP: dict[str, type[RivianApiException]] = {
86
90
  "BAD_CURRENT_PASSWORD": RivianInvalidCredentials,
87
91
  "BAD_REQUEST_ERROR": RivianBadRequestError,
88
92
  "DATA_ERROR": RivianDataError,
@@ -444,6 +448,40 @@ class Rivian:
444
448
 
445
449
  return await self.__graphql_query(headers, url, graphql_json)
446
450
 
451
+ async def get_charging_schedules(self, vehicle_id: str) -> ClientResponse:
452
+ """Get charging schedules for a vehicle."""
453
+ url = GRAPHQL_GATEWAY
454
+ headers = BASE_HEADERS | {
455
+ "A-Sess": self._app_session_token,
456
+ "U-Sess": self._user_session_token,
457
+ }
458
+ graphql_json = {
459
+ "operationName": "getVehicleChargingSchedules",
460
+ "query": "query getVehicleChargingSchedules($vehicleId: String!) {\n getVehicle(id: $vehicleId) {\n chargingSchedules {\n weekDays\n startTime\n duration\n location {\n latitude\n longitude\n }\n amperage\n enabled\n }\n }\n}",
461
+ "variables": {"vehicleId": vehicle_id},
462
+ }
463
+ return await self.__graphql_query(headers, url, graphql_json)
464
+
465
+ async def set_charging_schedules(
466
+ self, vehicle_id: str, schedules: list[dict[str, Any]]
467
+ ) -> ClientResponse:
468
+ """Set charging schedules for a vehicle."""
469
+ url = GRAPHQL_GATEWAY
470
+ headers = BASE_HEADERS | {
471
+ "Csrf-Token": self._csrf_token,
472
+ "A-Sess": self._app_session_token,
473
+ "U-Sess": self._user_session_token,
474
+ }
475
+ graphql_json = {
476
+ "operationName": "setChargingSchedules",
477
+ "query": "mutation setChargingSchedules($vehicleId: String!, $chargingSchedules: [InputChargingSchedule!]!) {\n setChargingSchedules(vehicleId: $vehicleId, chargingSchedules: $chargingSchedules) {\n __typename\n success\n }\n}",
478
+ "variables": {
479
+ "vehicleId": vehicle_id,
480
+ "chargingSchedules": schedules,
481
+ },
482
+ }
483
+ return await self.__graphql_query(headers, url, graphql_json)
484
+
447
485
  async def get_vehicle_ota_update_details(self, vehicle_id: str) -> ClientResponse:
448
486
  """Get vehicle OTA update details."""
449
487
  url = GRAPHQL_GATEWAY
@@ -496,15 +534,14 @@ class Rivian:
496
534
  self, command: VehicleCommand | str, params: dict[str, Any] | None = None
497
535
  ) -> None:
498
536
  """Validate certian vehicle command/param combos."""
499
- if command == VehicleCommand.CHARGING_LIMITS:
500
- if not (
501
- params
502
- and isinstance((limit := params.get("SOC_limit")), int)
503
- and 50 <= limit <= 100
504
- ):
505
- raise RivianBadRequestError(
506
- "Charging limit must include parameter `SOC_limit` with a valid value between 50 and 100"
507
- )
537
+ if command == VehicleCommand.CHARGING_LIMITS and not (
538
+ params
539
+ and isinstance((limit := params.get("SOC_limit")), int)
540
+ and 50 <= limit <= 100
541
+ ):
542
+ raise RivianBadRequestError(
543
+ "Charging limit must include parameter `SOC_limit` with a valid value between 50 and 100"
544
+ )
508
545
  if command in (
509
546
  VehicleCommand.CABIN_HVAC_DEFROST_DEFOG,
510
547
  VehicleCommand.CABIN_HVAC_LEFT_SEAT_HEAT,
@@ -514,15 +551,14 @@ class Rivian:
514
551
  VehicleCommand.CABIN_HVAC_RIGHT_SEAT_HEAT,
515
552
  VehicleCommand.CABIN_HVAC_RIGHT_SEAT_VENT,
516
553
  VehicleCommand.CABIN_HVAC_STEERING_HEAT,
554
+ ) and not (
555
+ params
556
+ and isinstance((level := params.get("level")), int)
557
+ and 0 <= level <= 4
517
558
  ):
518
- if not (
519
- params
520
- and isinstance((level := params.get("level")), int)
521
- and 0 <= level <= 4
522
- ):
523
- raise RivianBadRequestError(
524
- "HVAC setting must include parameter `level` with a valid value between 0 and 4"
525
- )
559
+ raise RivianBadRequestError(
560
+ "HVAC setting must include parameter `level` with a valid value between 0 and 4"
561
+ )
526
562
  if command == VehicleCommand.CABIN_PRECONDITIONING_SET_TEMP:
527
563
  if not (
528
564
  params
@@ -616,14 +652,44 @@ class Rivian:
616
652
  unsubscribe = await self._ws_monitor.start_subscription(payload, callback)
617
653
  _LOGGER.debug("%s subscribed to updates", vehicle_id)
618
654
  return unsubscribe
619
- except Exception as ex: # pylint: disable=broad-except
655
+ except Exception as ex: # pylint: disable=broad-except # noqa: BLE001
620
656
  _LOGGER.error(ex)
621
657
  return None
622
658
 
623
- async def _ws_connect(self) -> ClientWebSocketResponse:
659
+ async def subscribe_for_parallax_messages(
660
+ self,
661
+ vehicle_id: str,
662
+ callback: Callable[[dict[str, Any]], None],
663
+ rvms: list[str] | None = None,
664
+ ) -> Callable[[], Awaitable[None]] | None:
665
+ """Open a web socket connection to receive Parallax message updates."""
666
+ if not rvms:
667
+ rvms = PARALLAX_RVMS
668
+
669
+ try:
670
+ await self._ws_connect()
671
+ assert self._ws_monitor
672
+ async with async_timeout.timeout(self.request_timeout):
673
+ await self._ws_monitor.connection_ack.wait()
674
+ payload = {
675
+ "operationName": "ParallaxMessages",
676
+ "query": "subscription ParallaxMessages($vehicleId: String!, $rvms: [String!]) { parallaxMessages(vehicleId: $vehicleId, rvms: $rvms) { payload timestamp rvm } }",
677
+ "variables": {
678
+ "vehicleId": vehicle_id,
679
+ "rvms": rvms,
680
+ },
681
+ }
682
+ unsubscribe = await self._ws_monitor.start_subscription(payload, callback)
683
+ _LOGGER.debug("%s subscribed to %d Parallax RVMs", vehicle_id, len(rvms))
684
+ return unsubscribe
685
+ except Exception as ex: # pylint: disable=broad-except # noqa: BLE001
686
+ _LOGGER.error(ex)
687
+ return None
688
+
689
+ async def _ws_connect(self) -> ClientWebSocketResponse[bool]:
624
690
  """Initiate a websocket connection."""
625
691
 
626
- async def connection_init(websocket: ClientWebSocketResponse) -> None:
692
+ async def connection_init(websocket: ClientWebSocketResponse[bool]) -> None:
627
693
  await websocket.send_json(
628
694
  {
629
695
  "payload": {
@@ -676,37 +742,34 @@ class Rivian:
676
742
  "Error occurred while communicating with Rivian."
677
743
  ) from exception
678
744
 
679
- try:
680
- response_json = await response.json()
681
- if errors := response_json.get("errors"):
682
- for error in errors:
683
- if extensions := error.get("extensions"):
684
- code = extensions["code"]
685
- if (code, extensions.get("reason")) in (
686
- ("BAD_USER_INPUT", "INVALID_OTP"),
687
- ("UNAUTHENTICATED", "OTP_TOKEN_EXPIRED"),
688
- ):
689
- raise RivianInvalidOTP(
690
- response.status, response_json, headers, body
691
- )
692
- if (code, extensions.get("reason")) == (
693
- "CONFLICT",
694
- "ENROLL_PHONE_LIMIT_REACHED",
695
- ):
696
- raise RivianPhoneLimitReachedError(
697
- response.status, response_json, headers, body
698
- )
699
- if err_cls := ERROR_CODE_CLASS_MAP.get(code):
700
- raise err_cls(response.status, response_json, headers, body)
701
- raise RivianApiException(
702
- "Error occurred while reading the graphql response from Rivian.",
703
- response.status,
704
- response_json,
705
- headers,
706
- body,
707
- )
708
- except Exception as exception:
709
- raise exception
745
+ response_json = await response.json()
746
+ if errors := response_json.get("errors"):
747
+ for error in errors:
748
+ if extensions := error.get("extensions"):
749
+ code = extensions["code"]
750
+ if (code, extensions.get("reason")) in (
751
+ ("BAD_USER_INPUT", "INVALID_OTP"),
752
+ ("UNAUTHENTICATED", "OTP_TOKEN_EXPIRED"),
753
+ ):
754
+ raise RivianInvalidOTP(
755
+ response.status, response_json, headers, body
756
+ )
757
+ if (code, extensions.get("reason")) == (
758
+ "CONFLICT",
759
+ "ENROLL_PHONE_LIMIT_REACHED",
760
+ ):
761
+ raise RivianPhoneLimitReachedError(
762
+ response.status, response_json, headers, body
763
+ )
764
+ if err_cls := ERROR_CODE_CLASS_MAP.get(code):
765
+ raise err_cls(response.status, response_json, headers, body)
766
+ raise RivianApiException(
767
+ "Error occurred while reading the graphql response from Rivian.",
768
+ response.status,
769
+ response_json,
770
+ headers,
771
+ body,
772
+ )
710
773
 
711
774
  return response
712
775
 
@@ -717,7 +780,7 @@ class Rivian:
717
780
  if self._session and self._close_session:
718
781
  await self._session.close()
719
782
 
720
- async def __aenter__(self) -> Rivian:
783
+ async def __aenter__(self) -> Self:
721
784
  """Async enter.
722
785
  Returns:
723
786
  The Rivian object.
@@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Any
14
14
  from uuid import uuid4
15
15
 
16
16
  from aiohttp import ClientWebSocketResponse, WSMessage, WSMsgType
17
+ from aiohttp.http_websocket import WSMessageTextBytes
17
18
 
18
19
  if sys.version_info >= (3, 11):
19
20
  import asyncio as async_timeout
@@ -44,7 +45,7 @@ class WebSocketMonitor:
44
45
  self,
45
46
  account: Rivian,
46
47
  url: str,
47
- connection_init: Callable[[ClientWebSocketResponse], Awaitable[None]],
48
+ connection_init: Callable[[ClientWebSocketResponse[bool]], Awaitable[None]],
48
49
  ) -> None:
49
50
  """Initialize a web socket monitor."""
50
51
  self._account = account
@@ -53,7 +54,7 @@ class WebSocketMonitor:
53
54
 
54
55
  self._connection_ack: asyncio.Event = asyncio.Event()
55
56
  self._disconnect = False
56
- self._ws: ClientWebSocketResponse | None = None
57
+ self._ws: ClientWebSocketResponse[bool] | None = None
57
58
  self._monitor_task: asyncio.Task | None = None
58
59
  self._receiver_task: asyncio.Task | None = None
59
60
  self._last_received: datetime | None = None
@@ -74,7 +75,7 @@ class WebSocketMonitor:
74
75
  return self._connection_ack
75
76
 
76
77
  @property
77
- def websocket(self) -> ClientWebSocketResponse | None:
78
+ def websocket(self) -> ClientWebSocketResponse[bool] | None:
78
79
  """Return the web socket."""
79
80
  return self._ws
80
81
 
@@ -179,7 +180,7 @@ class WebSocketMonitor:
179
180
  if not self._disconnect:
180
181
  try:
181
182
  await self.new_connection()
182
- except Exception as ex: # pylint: disable=broad-except
183
+ except Exception as ex: # pylint: disable=broad-except # noqa: BLE001
183
184
  self._log_message(ex, True)
184
185
  if not self._ws or self._ws.closed:
185
186
  await asyncio.sleep(min(1 * 2**attempt + uniform(0, 1), 300))
@@ -206,7 +207,9 @@ class WebSocketMonitor:
206
207
  await cancel_task(self._monitor_task, self._receiver_task)
207
208
 
208
209
  def _log_message(
209
- self, message: str | Exception | WSMessage, is_error: bool = False
210
+ self,
211
+ message: str | Exception | WSMessage | WSMessageTextBytes,
212
+ is_error: bool = False,
210
213
  ) -> None:
211
214
  """Log a message."""
212
215
  log_method = _LOGGER.error if is_error else _LOGGER.debug
@@ -1,49 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: rivian-python-client
3
- Version: 2.0.0
4
- Summary: Rivian API Client (Unofficial)
5
- License-Expression: MIT
6
- Author: Brian Retterer
7
- Author-email: bretterer@gmail.com
8
- Requires-Python: >=3.10
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Classifier: Programming Language :: Python :: 3.13
14
- Classifier: Programming Language :: Python :: 3.14
15
- Provides-Extra: ble
16
- Requires-Dist: aiohttp (>=3.0.0)
17
- Requires-Dist: backports-strenum (>=1.2.4) ; python_version < "3.11"
18
- Requires-Dist: bleak (>=0.21) ; extra == "ble"
19
- Requires-Dist: cryptography (>=41.0.1)
20
- Requires-Dist: dbus-fast (>=2.11.0) ; (sys_platform == "linux") and (extra == "ble")
21
- Description-Content-Type: text/markdown
22
-
23
- # Python: Rivian API Client
24
-
25
- Currently a Work In Progress
26
-
27
- ## Dependencies
28
-
29
- [Poetry](https://python-poetry.org/docs/)
30
-
31
- ```
32
- curl -sSL https://install.python-poetry.org | python3 -
33
- ```
34
-
35
- ## Setup
36
-
37
- Install project dependencies into the poetry virtual environment and run pre-commit
38
-
39
- ```
40
- poetry install --all-extras
41
- pre-commit install
42
- ```
43
-
44
- ## Run Tests
45
-
46
- ```
47
- poetry run pytest
48
- ```
49
-
@@ -1,26 +0,0 @@
1
- # Python: Rivian API Client
2
-
3
- Currently a Work In Progress
4
-
5
- ## Dependencies
6
-
7
- [Poetry](https://python-poetry.org/docs/)
8
-
9
- ```
10
- curl -sSL https://install.python-poetry.org | python3 -
11
- ```
12
-
13
- ## Setup
14
-
15
- Install project dependencies into the poetry virtual environment and run pre-commit
16
-
17
- ```
18
- poetry install --all-extras
19
- pre-commit install
20
- ```
21
-
22
- ## Run Tests
23
-
24
- ```
25
- poetry run pytest
26
- ```
@@ -1,53 +0,0 @@
1
- [project]
2
- name = "rivian-python-client"
3
- description = "Rivian API Client (Unofficial)"
4
- license = "MIT"
5
- readme = "README.md"
6
- requires-python = ">=3.10"
7
- authors = [
8
- { name = "Brian Retterer", email = "bretterer@gmail.com" },
9
- { name = "J.R. Gutierrez", email = "jrgutier@gmail.com" },
10
- { name = "Nathan Spencer", email = "natekspencer@gmail.com" },
11
- { name = "Trevor Mack", email = "drummer8001@gmail.com" },
12
- ]
13
- dependencies = [
14
- "aiohttp (>=3.0.0)",
15
- "cryptography (>=41.0.1)",
16
- "backports-strenum (>=1.2.4) ; python_version < '3.11'",
17
- ]
18
- dynamic = []
19
- version = "2.0.0"
20
-
21
- [project.optional-dependencies]
22
- ble = [
23
- "bleak (>=0.21)",
24
- "dbus-fast (>=2.11.0) ; sys_platform == 'linux'",
25
- ]
26
-
27
- [tool.poetry]
28
- packages = [
29
- { include = "rivian", from = "src" },
30
- ]
31
-
32
- [tool.poetry.group.dev.dependencies]
33
- pytest = ">=7.1.2,<9.0.0"
34
- pytest-asyncio = ">=0.18.3,<1.3.0"
35
- python-dotenv = ">=0.20,<1.3"
36
- aresponses = ">=2.1.5,<4.0.0"
37
- mypy = ">=1.7.0,<2.0.0"
38
- ruff = ">=0.4,<0.15"
39
- pre-commit = "^4.1.0"
40
-
41
- [tool.poetry-dynamic-versioning]
42
- enable = false
43
- vcs = "git"
44
- style = "semver"
45
- pattern = "default-unprefixed"
46
-
47
- [tool.ruff.lint.isort]
48
- combine-as-imports = true
49
- split-on-trailing-comma = false
50
-
51
- [build-system]
52
- requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
53
- build-backend = "poetry_dynamic_versioning.backend"