rivian-python-client 2.2.0b0__tar.gz → 2.2.0b2__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 (106) hide show
  1. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/PKG-INFO +1 -1
  2. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/pyproject.toml +8 -0
  3. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/__version__.py +1 -1
  4. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/__init__.py +2 -0
  5. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/body.py +70 -46
  6. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/charging.py +63 -43
  7. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/comfort.py +54 -45
  8. rivian_python_client-2.2.0b2/src/rivian/parallax/device_table.py +99 -0
  9. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/dynamics.py +34 -34
  10. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/energy.py +6 -6
  11. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/gearguard_streaming.py +9 -9
  12. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/geofence.py +11 -5
  13. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/holiday_celebration.py +46 -6
  14. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/navigation.py +6 -3
  15. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/ota.py +37 -24
  16. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/body.proto +86 -27
  17. rivian_python_client-2.2.0b2/src/rivian/parallax/proto/body_pb2.py +72 -0
  18. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/body_pb2.pyi +154 -40
  19. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/charging.proto +16 -7
  20. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/charging_pb2.py +12 -10
  21. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/charging_pb2.pyi +19 -6
  22. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/comfort.proto +33 -26
  23. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/comfort_pb2.py +24 -24
  24. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/comfort_pb2.pyi +57 -43
  25. rivian_python_client-2.2.0b2/src/rivian/parallax/proto/device_table.proto +64 -0
  26. rivian_python_client-2.2.0b2/src/rivian/parallax/proto/device_table_pb2.py +50 -0
  27. rivian_python_client-2.2.0b2/src/rivian/parallax/proto/device_table_pb2.pyi +92 -0
  28. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/geofence.proto +9 -2
  29. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/geofence_pb2.py +5 -3
  30. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/geofence_pb2.pyi +16 -4
  31. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/holiday_celebration.proto +24 -10
  32. rivian_python_client-2.2.0b2/src/rivian/parallax/proto/holiday_celebration_pb2.py +50 -0
  33. rivian_python_client-2.2.0b2/src/rivian/parallax/proto/holiday_celebration_pb2.pyi +92 -0
  34. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/navigation.proto +8 -1
  35. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/navigation_pb2.py +19 -17
  36. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/navigation_pb2.pyi +15 -4
  37. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/ota.proto +24 -12
  38. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/ota_pb2.py +17 -13
  39. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/ota_pb2.pyi +30 -18
  40. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/vehicle.proto +5 -5
  41. rivian_python_client-2.2.0b2/src/rivian/parallax/proto/vehicle_access.proto +19 -0
  42. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/vehicle_access_pb2.py +5 -3
  43. rivian_python_client-2.2.0b2/src/rivian/parallax/proto/vehicle_access_pb2.pyi +25 -0
  44. rivian_python_client-2.2.0b2/src/rivian/parallax/proto/vehicle_pb2.py +62 -0
  45. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/vehicle_pb2.pyi +20 -20
  46. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/security.py +37 -37
  47. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/vehicle.py +29 -23
  48. rivian_python_client-2.2.0b2/src/rivian/parallax/vehicle_access.py +39 -0
  49. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/ws_monitor.py +31 -0
  50. rivian_python_client-2.2.0b0/src/rivian/parallax/device_table.py +0 -59
  51. rivian_python_client-2.2.0b0/src/rivian/parallax/proto/body_pb2.py +0 -64
  52. rivian_python_client-2.2.0b0/src/rivian/parallax/proto/device_table.proto +0 -44
  53. rivian_python_client-2.2.0b0/src/rivian/parallax/proto/device_table_pb2.py +0 -46
  54. rivian_python_client-2.2.0b0/src/rivian/parallax/proto/device_table_pb2.pyi +0 -61
  55. rivian_python_client-2.2.0b0/src/rivian/parallax/proto/holiday_celebration_pb2.py +0 -44
  56. rivian_python_client-2.2.0b0/src/rivian/parallax/proto/holiday_celebration_pb2.pyi +0 -67
  57. rivian_python_client-2.2.0b0/src/rivian/parallax/proto/vehicle_access.proto +0 -8
  58. rivian_python_client-2.2.0b0/src/rivian/parallax/proto/vehicle_access_pb2.pyi +0 -9
  59. rivian_python_client-2.2.0b0/src/rivian/parallax/proto/vehicle_pb2.py +0 -62
  60. rivian_python_client-2.2.0b0/src/rivian/parallax/vehicle_access.py +0 -18
  61. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/.gitignore +0 -0
  62. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/README.md +0 -0
  63. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/__init__.py +0 -0
  64. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/ble.py +0 -0
  65. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/const.py +0 -0
  66. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/exceptions.py +0 -0
  67. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/core.py +0 -0
  68. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/departure.py +0 -0
  69. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/energy_edge_compute.py +0 -0
  70. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/parallax.py +0 -0
  71. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/__init__.py +0 -0
  72. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/departure.proto +0 -0
  73. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/departure_pb2.py +0 -0
  74. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/departure_pb2.pyi +0 -0
  75. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/dynamics.proto +0 -0
  76. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/dynamics_pb2.py +0 -0
  77. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/dynamics_pb2.pyi +0 -0
  78. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/energy.proto +0 -0
  79. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/energy_edge_compute.proto +0 -0
  80. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/energy_edge_compute_pb2.py +0 -0
  81. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/energy_edge_compute_pb2.pyi +0 -0
  82. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/energy_pb2.py +0 -0
  83. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/energy_pb2.pyi +0 -0
  84. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/gearguard_streaming.proto +0 -0
  85. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/gearguard_streaming_pb2.py +0 -0
  86. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/gearguard_streaming_pb2.pyi +0 -0
  87. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/parallax.proto +0 -0
  88. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/parallax_pb2.py +0 -0
  89. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/parallax_pb2.pyi +0 -0
  90. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/secure_file_transfer.proto +0 -0
  91. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/secure_file_transfer_pb2.py +0 -0
  92. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/secure_file_transfer_pb2.pyi +0 -0
  93. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/security.proto +0 -0
  94. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/security_pb2.py +0 -0
  95. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/security_pb2.pyi +0 -0
  96. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/user_passcodes.proto +0 -0
  97. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/user_passcodes_pb2.py +0 -0
  98. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/proto/user_passcodes_pb2.pyi +0 -0
  99. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/secure_file_transfer.py +0 -0
  100. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/parallax/user_passcodes.py +0 -0
  101. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/py.typed +0 -0
  102. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/rivian.py +0 -0
  103. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/schemas/charging.graphql +0 -0
  104. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/schemas/gateway.graphql +0 -0
  105. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/schemas/orders.graphql +0 -0
  106. {rivian_python_client-2.2.0b0 → rivian_python_client-2.2.0b2}/src/rivian/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rivian-python-client
3
- Version: 2.2.0b0
3
+ Version: 2.2.0b2
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
@@ -36,8 +36,16 @@ dev = [
36
36
  "pre-commit>=4.6.1,<5",
37
37
  "types-protobuf>=7.35.1.20260906",
38
38
  "grpcio-tools==1.84.0",
39
+ "pytest-cov>=7.0,<8",
39
40
  ]
40
41
 
42
+ [tool.coverage.run]
43
+ source = ["rivian"]
44
+ omit = ["*_pb2.py"]
45
+
46
+ [tool.coverage.report]
47
+ exclude_also = ["if TYPE_CHECKING:"]
48
+
41
49
  [tool.hatch.build.hooks.version]
42
50
  path = "src/rivian/__version__.py"
43
51
 
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '2.2.0b0'
4
+ __version__ = VERSION = '2.2.0b2'
@@ -50,7 +50,9 @@ from .core import (
50
50
  PARALLAX_RVMS: list[str] = list(RVMDecoder.decoders)
51
51
 
52
52
  CHARGING_RVMS: list[str] = [
53
+ "charging.energy.state",
53
54
  "charging.session.notification",
55
+ "charging.session.power",
54
56
  "charging.session.remote_command",
55
57
  "charging.session.soc_slider",
56
58
  "charging.session.status",
@@ -8,28 +8,30 @@ from .core import _LOGGER, RVMDecoder, _enum, _present
8
8
  from .proto import body_pb2
9
9
 
10
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",
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",
26
27
  }
27
28
 
28
29
  _CLOSURE_STATE_MAP: Final[dict[int, str]] = {
29
- 1: "open",
30
- 2: "closed",
31
- 4: "opening",
32
- 5: "closing",
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",
33
35
  }
34
36
 
35
37
 
@@ -41,11 +43,7 @@ def _enum_names(enum: Any, prefix: str) -> dict[int, str]:
41
43
  }
42
44
 
43
45
 
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
- }
46
+ _SIDE_BIN_NEXT_ACTIONS: Final = _enum_names(body_pb2.SideBinNextAction, "SIDE_BIN_")
49
47
 
50
48
  # Closure id -> (next-action field, result key, value names).
51
49
  _NEXT_ACTIONS: Final[dict[int, tuple[str, str, dict[int, str]]]] = {
@@ -54,6 +52,11 @@ _NEXT_ACTIONS: Final[dict[int, tuple[str, str, dict[int, str]]]] = {
54
52
  "closureFrunkNextAction",
55
53
  _enum_names(body_pb2.FrunkNextAction, "FRUNK_"),
56
54
  ),
55
+ body_pb2.LIFTGATE: (
56
+ "liftgate_next_action",
57
+ "closureLiftgateNextAction",
58
+ _enum_names(body_pb2.LiftgateNextAction, "LIFTGATE_"),
59
+ ),
57
60
  body_pb2.SIDE_BIN_LEFT: (
58
61
  "side_bin_next_action",
59
62
  "closureSideBinLeftNextAction",
@@ -74,26 +77,42 @@ _NEXT_ACTIONS: Final[dict[int, tuple[str, str, dict[int, str]]]] = {
74
77
  "closureChargePortDoorNextAction",
75
78
  _enum_names(body_pb2.ChargePortDoorNextAction, "CHARGE_PORT_DOOR_"),
76
79
  ),
80
+ body_pb2.GROUP_WINDOWS: (
81
+ "windows_next_action",
82
+ "windowsNextAction",
83
+ _enum_names(body_pb2.WindowsNextAction, "WINDOWS_"),
84
+ ),
77
85
  }
78
86
 
79
87
  _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",
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",
90
109
  }
91
110
 
92
111
  _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",
112
+ body_pb2.TRAILER_NOT_PRESENT: "trailer_not_present",
113
+ body_pb2.TRAILER_PRESENT: "trailer_present",
114
+ body_pb2.TRAILER_PRESENT_WITH_BRAKES: "trailer_present_with_brakes",
115
+ body_pb2.TRAILER_INVALID: "trailer_invalid",
97
116
  }
98
117
 
99
118
 
@@ -103,10 +122,11 @@ def decode_closures(m: body_pb2.ClosuresState) -> dict[str, Any]:
103
122
 
104
123
  Fields:
105
124
  doorFrontLeftClosed, closureFrunkClosed, etc.: str
106
- ("open" | "closed" | "opening" | "closing")
107
- closureFrunkNextAction, closureTailgateNextAction,
108
- closureSideBinLeftNextAction, closureSideBinRightNextAction,
109
- closureChargePortDoorNextAction: str — e.g. "open_allowed" (shut),
125
+ ("open" | "closed" | "ajar" | "opening" | "closing")
126
+ closureFrunkNextAction, closureLiftgateNextAction,
127
+ closureTailgateNextAction, closureSideBinLeftNextAction,
128
+ closureSideBinRightNextAction, closureChargePortDoorNextAction,
129
+ windowsNextAction: str — e.g. "open_allowed" (shut),
110
130
  "close_allowed" (open); for the charge port,
111
131
  "close_not_available" means plugged in
112
132
 
@@ -122,7 +142,7 @@ def decode_closures(m: body_pb2.ClosuresState) -> dict[str, Any]:
122
142
  result[_CLOSURE_MAP[cid]] = _enum(
123
143
  _CLOSURE_STATE_MAP, state_val, what="closure state"
124
144
  )
125
- else:
145
+ elif cid != body_pb2.GROUP_WINDOWS:
126
146
  _LOGGER.debug("Unknown closure id %s (state %s)", cid, state_val)
127
147
  if cid in _NEXT_ACTIONS:
128
148
  field, key, names = _NEXT_ACTIONS[cid]
@@ -133,10 +153,12 @@ def decode_closures(m: body_pb2.ClosuresState) -> dict[str, Any]:
133
153
 
134
154
  @RVMDecoder.register("body.locks.states", body_pb2.LocksState)
135
155
  def decode_locks(m: body_pb2.LocksState) -> dict[str, Any]:
136
- """body.locks.states — lock state of every lockable closure.
156
+ """body.locks.states — lock state of every lock.
137
157
 
138
158
  Fields:
139
- doorFrontLeftLocked, closureFrunkLocked, etc.: str ("locked" | "unlocked")
159
+ doorFrontLeftLocked, closureFrunkLocked, gearGuardLocked (the Gear
160
+ Guard cable), etc.: str ("locked" | "unlocked" |
161
+ "partially_unlocked")
140
162
  """
141
163
  result: dict[str, Any] = {}
142
164
  for s in m.lock:
@@ -145,7 +167,9 @@ def decode_locks(m: body_pb2.LocksState) -> dict[str, Any]:
145
167
  if state_val is None:
146
168
  continue
147
169
  if lid in _LOCK_MAP:
148
- result[_LOCK_MAP[lid]] = "locked" if state_val == 1 else "unlocked"
170
+ result[_LOCK_MAP[lid]] = _enum(
171
+ _LOCK_STATE_MAP, state_val, what="lock state"
172
+ )
149
173
  else:
150
174
  _LOGGER.debug("Unknown lock id %s (state %s)", lid, state_val)
151
175
  return result
@@ -8,13 +8,13 @@ from .core import RVMDecoder, _enum, _present
8
8
  from .proto import charging_pb2
9
9
 
10
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",
11
+ charging_pb2.ScheduleTimeWindow.SUNDAY: "sunday",
12
+ charging_pb2.ScheduleTimeWindow.MONDAY: "monday",
13
+ charging_pb2.ScheduleTimeWindow.TUESDAY: "tuesday",
14
+ charging_pb2.ScheduleTimeWindow.WEDNESDAY: "wednesday",
15
+ charging_pb2.ScheduleTimeWindow.THURSDAY: "thursday",
16
+ charging_pb2.ScheduleTimeWindow.FRIDAY: "friday",
17
+ charging_pb2.ScheduleTimeWindow.SATURDAY: "saturday",
18
18
  }
19
19
 
20
20
 
@@ -65,8 +65,8 @@ def decode_session_notification(m: charging_pb2.SessionNotification) -> dict[str
65
65
 
66
66
 
67
67
  _REMOTE_COMMAND_MAP: Final[dict[int, str]] = {
68
- 1: "start",
69
- 2: "stop",
68
+ charging_pb2.REMOTE_COMMAND_START: "start",
69
+ charging_pb2.REMOTE_COMMAND_STOP: "stop",
70
70
  }
71
71
 
72
72
 
@@ -144,34 +144,34 @@ def decode_weighted_charging_forecast(
144
144
 
145
145
 
146
146
  _CONNECTION_STATE_MAP: Final[dict[int, str]] = {
147
- 1: "disconnected",
148
- 2: "connected",
147
+ charging_pb2.CONNECTION_STATE_DISCONNECTED: "disconnected",
148
+ charging_pb2.CONNECTION_STATE_CONNECTED: "connected",
149
149
  }
150
150
 
151
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",
152
+ charging_pb2.CHARGING_READY: "charging_ready",
153
+ charging_pb2.CHARGING_CONNECTING: "charging_connecting",
154
+ charging_pb2.CHARGING_ACTIVE: "charging_active",
155
+ charging_pb2.CHARGING_COMPLETE: "charging_complete",
156
+ charging_pb2.CHARGING_SCHEDULED: "charging_scheduled",
157
+ charging_pb2.CHARGING_VEHICLE_ERROR: "charging_vehicle_error",
158
+ charging_pb2.CHARGING_STATION_ERROR: "charging_station_error",
159
+ charging_pb2.CHARGING_USER_STOPPED: "charging_stopped_by_user",
160
+ charging_pb2.CHARGING_STATION_STOPPED: "charging_stopped_by_station",
161
+ charging_pb2.CHARGING_PAYMENT_ERROR: "charging_payment_error",
162
+ charging_pb2.CHARGING_CERT_ERROR: "charging_cert_error",
163
+ charging_pb2.CHARGING_TLS_ERROR: "charging_tls_error",
164
+ charging_pb2.CHARGING_ERROR_AC_ADAPTER_USED_ON_DC: "charging_error_ac_adapter_used_on_dc",
165
+ charging_pb2.CHARGING_ERROR_DC_ADAPTER_USED_ON_AC: "charging_error_dc_adapter_used_on_ac",
166
+ charging_pb2.CHARGING_ERROR_INCOMPATIBLE_CHARGER: "charging_error_incompatible_charger",
167
+ charging_pb2.CHARGING_SD_COMPENSATION: "charging_sd_compensation",
168
+ charging_pb2.WAITING_ON_CHARGER: "waiting_on_charger",
169
+ charging_pb2.CHARGER_NOT_READY_OR_INCOMPATIBLE: "charging_error_not_ready_or_incompatible_charger",
170
+ charging_pb2.CHARGING_VEHICLE_STOPPED: "charging_vehicle_stopped",
171
+ charging_pb2.CHARGING_PAYMENT_ERROR_START_RIVIAN_APP: "charging_payment_error_start_rivian_app",
172
+ charging_pb2.CHARGING_TLS_ERROR_UNKNOWN_CHARGER: "charging_tls_error_unknown_charger",
173
+ charging_pb2.CHARGING_TLS_ERROR_UNEXPECTED_FAIL: "charging_tls_error_unexpected_fail",
174
+ charging_pb2.CHARGING_TLS_ERROR_START_RIVIAN_APP: "charging_tls_error_start_rivian_app",
175
175
  }
176
176
 
177
177
 
@@ -210,17 +210,37 @@ def decode_time_estimation(m: charging_pb2.TimeEstimation) -> dict[str, Any]:
210
210
  return {"timeToEndOfCharge": m.estimated_time_remaining}
211
211
 
212
212
 
213
+ _CHARGER_STATUS_MAP: Final[dict[int, str]] = {
214
+ charging_pb2.CHARGER_STATUS_NOT_CONNECTED: "chrgr_sts_not_connected",
215
+ charging_pb2.CHARGER_STATUS_CONNECTED_NO_CHARGE: "chrgr_sts_connected_no_chrg",
216
+ charging_pb2.CHARGER_STATUS_CONNECTED_CHARGING: "chrgr_sts_connected_charging",
217
+ }
218
+
219
+
213
220
  @RVMDecoder.register("charging.energy.state", charging_pb2.EnergyState)
214
221
  def decode_energy_state(m: charging_pb2.EnergyState) -> dict[str, Any]:
215
- """charging.energy.state — unmapped; `_field1`-`_field3`, `_field11` raw."""
216
- return {
217
- f"_field{num}": v
218
- for num in (1, 2, 3, 11)
219
- if (v := _present(m, f"field_{num}")) is not None
220
- }
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
221
237
 
222
238
 
223
239
  @RVMDecoder.register("charging.session.power", charging_pb2.SessionPower)
224
- def decode_session_power(_m: charging_pb2.SessionPower) -> dict[str, Any]:
225
- """charging.session.power — unmapped; empty so far."""
226
- return {}
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)}
@@ -34,60 +34,69 @@ def decode_user_modes_state(m: comfort_pb2.UserModesState) -> dict[str, Any]:
34
34
 
35
35
 
36
36
  _CLIMATE_HOLD_AVAILABILITY_MAP: Final[dict[int, str]] = {
37
- 0: "unspecified",
38
- 1: "available",
39
- 2: "controllable",
40
- 3: "unavailable",
37
+ comfort_pb2.CLIMATE_HOLD_AVAILABILITY_UNSPECIFIED: "unspecified",
38
+ comfort_pb2.CLIMATE_HOLD_AVAILABLE: "available",
39
+ comfort_pb2.CLIMATE_HOLD_CONTROLLABLE: "controllable",
40
+ comfort_pb2.CLIMATE_HOLD_UNAVAILABLE: "unavailable",
41
41
  }
42
42
 
43
43
  _CLIMATE_HOLD_STATUS_MAP: Final[dict[int, str]] = {
44
- 0: "unspecified",
45
- 1: "unavailable",
46
- 2: "off",
47
- 3: "on",
48
- 4: "fault",
44
+ comfort_pb2.CLIMATE_HOLD_STATUS_UNSPECIFIED: "unspecified",
45
+ comfort_pb2.CLIMATE_HOLD_STATUS_UNAVAILABLE: "unavailable",
46
+ comfort_pb2.CLIMATE_HOLD_STATUS_OFF: "off",
47
+ comfort_pb2.CLIMATE_HOLD_STATUS_ON: "on",
48
+ comfort_pb2.CLIMATE_HOLD_STATUS_FAULT: "fault",
49
49
  }
50
50
 
51
51
  _CLIMATE_HOLD_UNAVAILABILITY_REASON_MAP: Final[dict[int, str]] = {
52
- 0: "unspecified",
53
- 1: "unknown",
54
- 2: "low_soc",
52
+ comfort_pb2.CLIMATE_HOLD_UNAVAILABILITY_UNSPECIFIED: "unspecified",
53
+ comfort_pb2.CLIMATE_HOLD_UNAVAILABILITY_UNKNOWN: "unknown",
54
+ comfort_pb2.CLIMATE_HOLD_UNAVAILABILITY_LOW_SOC: "low_soc",
55
55
  }
56
56
 
57
57
  _PET_MODE_STATE_MAP: Final[dict[int, str]] = {
58
- 0: "off",
59
- 1: "on",
60
- 2: "disabled",
61
- 3: "faulty",
58
+ comfort_pb2.PET_MODE_OFF: "off",
59
+ comfort_pb2.PET_MODE_ON: "on",
60
+ comfort_pb2.PET_MODE_DISABLED: "disabled",
61
+ comfort_pb2.PET_MODE_FAULTY: "faulty",
62
62
  }
63
63
 
64
64
  _PET_MODE_TEMPERATURE_MAP: Final[dict[int, str]] = {
65
- 0: "default",
66
- 1: "cold",
67
- 2: "hot",
68
- 3: "faulty",
65
+ comfort_pb2.PET_MODE_TEMPERATURE_DEFAULT: "default",
66
+ comfort_pb2.PET_MODE_TEMPERATURE_COLD: "cold",
67
+ comfort_pb2.PET_MODE_TEMPERATURE_HOT: "hot",
68
+ comfort_pb2.PET_MODE_TEMPERATURE_FAULTY: "faulty",
69
69
  }
70
70
 
71
- _SEAT_CONDITIONING_MAP: Final[dict[int, str]] = {
72
- 1: "steeringWheel",
73
- 5: "seatFrontLeft",
74
- 7: "seatFrontRight", # inferred
75
- 8: "seatRearLeft",
76
- 10: "seatRearRight",
77
- 11: "seatThirdRowLeft", # R1S
78
- 13: "seatThirdRowRight", # R1S
71
+ # Seat keys use the GraphQL names: row 1 is "Front", row 2 "Rear", row 3
72
+ # "ThirdRow".
73
+ _CABIN_SURFACE_MAP: Final[dict[int, str]] = {
74
+ comfort_pb2.STEERING_WHEEL: "steeringWheel",
75
+ comfort_pb2.REAR_GLASS: "rearGlass",
76
+ comfort_pb2.FRONT_GLASS: "frontGlass",
77
+ comfort_pb2.SIDEVIEW_MIRRORS: "sideviewMirrors",
78
+ comfort_pb2.SEAT_ROW_1_LEFT: "seatFrontLeft",
79
+ comfort_pb2.SEAT_ROW_1_MIDDLE: "seatFrontMiddle",
80
+ comfort_pb2.SEAT_ROW_1_RIGHT: "seatFrontRight",
81
+ comfort_pb2.SEAT_ROW_2_LEFT: "seatRearLeft",
82
+ comfort_pb2.SEAT_ROW_2_MIDDLE: "seatRearMiddle",
83
+ comfort_pb2.SEAT_ROW_2_RIGHT: "seatRearRight",
84
+ comfort_pb2.SEAT_ROW_3_LEFT: "seatThirdRowLeft",
85
+ comfort_pb2.SEAT_ROW_3_MIDDLE: "seatThirdRowMiddle",
86
+ comfort_pb2.SEAT_ROW_3_RIGHT: "seatThirdRowRight",
87
+ comfort_pb2.WIPER_AREA: "wiperArea",
79
88
  }
80
89
 
81
- _SEAT_CONDITIONING_STATUS_MAP: Final[dict[int | None, str]] = {
90
+ _CONDITIONING_LEVEL_MAP: Final[dict[int | None, str]] = {
82
91
  None: "Off",
83
- 1: "Level_1",
84
- 2: "Level_2",
85
- 3: "Level_3",
92
+ comfort_pb2.CONDITIONING_LEVEL_1: "Level_1",
93
+ comfort_pb2.CONDITIONING_LEVEL_2: "Level_2",
94
+ comfort_pb2.CONDITIONING_LEVEL_3: "Level_3",
86
95
  }
87
96
 
88
- _SEAT_CONDITIONING_TYPE_MAP: Final[dict[int, str]] = {
89
- 1: "Heat",
90
- 2: "Vent",
97
+ _CONDITIONING_TYPE_MAP: Final[dict[int, str]] = {
98
+ comfort_pb2.CONDITIONING_HEAT: "Heat",
99
+ comfort_pb2.CONDITIONING_VENT: "Vent",
91
100
  }
92
101
 
93
102
 
@@ -247,23 +256,23 @@ def decode_seat_conditioning(m: comfort_pb2.SeatConditioningStatus) -> dict[str,
247
256
  seatThirdRowLeftHeat, seatThirdRowRightHeat: str
248
257
  ("Off" | "Level_1" | "Level_2" | "Level_3")
249
258
  steeringWheelHeat: str ("Off" | "Level_1")
259
+ plus <surface>Heat/Vent for any other surface sent (middle seats,
260
+ rearGlass, frontGlass, sideviewMirrors, wiperArea)
250
261
 
251
262
  Entries without a state are skipped.
252
263
  """
253
264
  result: dict[str, Any] = {}
254
- for s in m.seat:
255
- cid, hvac_type = s.id, s.type
256
- if (state_val := _present(s, "state")) is None:
265
+ for surface in m.surface:
266
+ surface_id, hvac_type = surface.id, surface.type
267
+ if (state_val := _present(surface, "state")) is None:
257
268
  continue
258
- if cid in _SEAT_CONDITIONING_MAP and hvac_type in _SEAT_CONDITIONING_TYPE_MAP:
259
- key = (
260
- f"{_SEAT_CONDITIONING_MAP[cid]}{_SEAT_CONDITIONING_TYPE_MAP[hvac_type]}"
261
- )
262
- result[key] = _SEAT_CONDITIONING_STATUS_MAP.get(state_val, state_val)
269
+ if surface_id in _CABIN_SURFACE_MAP and hvac_type in _CONDITIONING_TYPE_MAP:
270
+ key = f"{_CABIN_SURFACE_MAP[surface_id]}{_CONDITIONING_TYPE_MAP[hvac_type]}"
271
+ result[key] = _CONDITIONING_LEVEL_MAP.get(state_val, state_val)
263
272
  else:
264
273
  _LOGGER.debug(
265
- "Unknown seat conditioning status id %s (hvac_type %s; state %s)",
266
- cid,
274
+ "Unknown cabin surface %s (hvac_type %s; state %s)",
275
+ surface_id,
267
276
  hvac_type,
268
277
  state_val,
269
278
  )
@@ -0,0 +1,99 @@
1
+ """Decoders for `device_table.*` RVM topics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import Any, Final
7
+
8
+ from .core import RVMDecoder, _enum, _present
9
+ from .proto import device_table_pb2
10
+
11
+ _Devices = device_table_pb2.VasKeyperDevices
12
+ _KEY_STATUS_MAP: Final[dict[int, str]] = {
13
+ _Devices.KEY_STATUS_ACTIVE: "active",
14
+ _Devices.KEY_STATUS_INACTIVE: "inactive",
15
+ _Devices.KEY_STATUS_WAITING_TO_PAIR: "waiting_to_pair",
16
+ _Devices.KEY_STATUS_PAIRING: "pairing",
17
+ }
18
+
19
+ _KEY_TYPE_MAP: Final[dict[int, str]] = {
20
+ _Devices.KEY_TYPE_PHONE: "phone",
21
+ _Devices.KEY_TYPE_KEY_CARD: "key_card",
22
+ _Devices.KEY_TYPE_KEY_FOB: "key_fob",
23
+ }
24
+
25
+
26
+ @RVMDecoder.register(
27
+ "device_table.vas_keyper.devices", device_table_pb2.VasKeyperDevices
28
+ )
29
+ def decode_vas_keyper_devices(m: device_table_pb2.VasKeyperDevices) -> dict[str, Any]:
30
+ """device_table.vas_keyper.devices — a key (phone, key card or fob).
31
+
32
+ Covers every driver's keys. A deleted key isn't removed; it's resent
33
+ as inactive.
34
+
35
+ Fields (partial updates, so each only when sent):
36
+ deviceName: str — phone keys, e.g. the phone's name
37
+ keyType: str ("phone" | "key_card" | "key_fob"; key_fob inferred)
38
+ mappedIdentityId: str — the GraphQL device's `mappedIdentityId`
39
+ hrid: str — short human-readable id (key card)
40
+ profileId: str — the driver profile the key belongs to
41
+ publicKey: str — hex; phone keys
42
+ keyRevision: int — key table revision when the entry was last written
43
+ keyStatus: str ("active" | "inactive" | "waiting_to_pair" |
44
+ "pairing"); inactive covers unpaired, no longer paired and
45
+ deleted keys
46
+ infoC: int — 1 while active, 2147483647 while inactive, else the
47
+ pairing attempt number
48
+ infoA, infoB: int — unknown; always equal
49
+ deviceId: str — hex; for a key card, the GraphQL `devices[].id`
50
+ phoneId: str — the phone's UUID; phone keys
51
+ credentialHex: str — hex; likely key material
52
+ active: bool
53
+ """
54
+ result: dict[str, Any] = {}
55
+ if m.HasField("label"):
56
+ if (v := _present(m.label, "name")) is not None:
57
+ result["deviceName"] = v
58
+ if (v := _present(m.label, "key_type")) is not None:
59
+ result["keyType"] = _enum(_KEY_TYPE_MAP, v, what="key type")
60
+ if m.HasField("device"):
61
+ for field, key in (
62
+ ("mapped_identity_id", "mappedIdentityId"),
63
+ ("hrid", "hrid"),
64
+ ("profile_id", "profileId"),
65
+ ("public_key", "publicKey"),
66
+ ("revision", "keyRevision"),
67
+ ):
68
+ if (v := _present(m.device, field)) is not None:
69
+ result[key] = v
70
+
71
+ if not m.HasField("credentials"):
72
+ return result
73
+ credentials = m.credentials
74
+ if credentials.HasField("info"):
75
+ for field, key in (
76
+ ("a", "infoA"),
77
+ ("c", "infoC"),
78
+ ("b", "infoB"),
79
+ ):
80
+ if (v := _present(credentials.info, field)) is not None:
81
+ result[key] = v
82
+ if (v := _present(credentials.info, "status")) is not None:
83
+ result["keyStatus"] = _enum(_KEY_STATUS_MAP, v, what="key status")
84
+ for field in ("card", "fob", "phone"):
85
+ if not credentials.HasField(field):
86
+ continue
87
+ material = getattr(credentials, field)
88
+ if (ident := _present(material, "id")) is not None:
89
+ if field == "phone" and len(ident) == 16:
90
+ result["phoneId"] = str(uuid.UUID(bytes=ident))
91
+ else:
92
+ result["deviceId"] = ident.hex()
93
+ if (cred := _present(material, "credential")) is not None:
94
+ result["credentialHex"] = cred.hex()
95
+ if (v := _present(credentials, "key_type")) is not None:
96
+ result["keyType"] = _enum(_KEY_TYPE_MAP, v, what="key type")
97
+ if (v := _present(credentials, "active")) is not None:
98
+ result["active"] = v
99
+ return result