Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions src/rivian/parallax.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def decode_cabin_temperatures(payload: str) -> dict[str, Any]:

Returns dict with keys:
- cabinClimateInteriorTemperature: float (Celsius)
- cabinClimateDriverTemperature: float (Celsius)
"""
if not payload:
return {}
Expand All @@ -154,8 +155,10 @@ def decode_cabin_temperatures(payload: str) -> dict[str, Any]:
result: dict[str, Any] = {}

for field_num, wire_type, value in fields:
if field_num == 4 and wire_type == 5: # interior temp (float, Celsius)
if field_num == 3 and wire_type == 5: # interior temp (float, Celsius)
result["cabinClimateInteriorTemperature"] = round(value, 1)
if field_num == 4 and wire_type == 5: # interior temp (float, Celsius)
result["cabinClimateDriverTemperature"] = round(value, 1)

return result
except Exception:
Expand Down Expand Up @@ -251,18 +254,19 @@ def decode_charging_graph_global(payload: str) -> dict[str, Any]:
]

first_seg = active_segments[0] if active_segments else segments[0]
last_seg = active_segments[-1] if active_segments else segments[-1]
result: dict[str, Any] = {}

if "start_ms" in first_seg:
st = datetime.fromtimestamp(first_seg["start_ms"] / 1000, timezone.utc)
result["startTime"] = st.strftime("%Y-%m-%dT%H:%M:%S.%f%z")

if active_segments and "start_ms" in first_seg and "end_ms" in last_seg:
result["timeElapsed"] = max(
0, int((last_seg["end_ms"] - first_seg["start_ms"]) / 1000)
if active_segments:
result["timeElapsed"] = sum(
max(0, int((s["end_ms"] - s["start_ms"]) / 1000))
for s in active_segments
if "end_ms" in s and "start_ms" in s
)
elif not active_segments:
else:
result["timeElapsed"] = 0

latest_segment = segments[-1]
Expand Down Expand Up @@ -431,8 +435,8 @@ def decode_locks(payload: str) -> dict[str, Any]:
state_val = in_val

if lid and lid in LOCK_MAP and state_val is not None:
# 1 = unlocked, 2 = locked
result[LOCK_MAP[lid]] = "locked" if state_val == 2 else "unlocked"
# 1 = locked, 2 = unlocked
result[LOCK_MAP[lid]] = "locked" if state_val == 1 else "unlocked"

return result
except Exception:
Expand All @@ -455,8 +459,8 @@ def decode_odometer(payload: str) -> dict[str, Any]:

for field_num, wire_type, value in fields:
if field_num == 1 and wire_type == 0:
# Value is distance in miles; HA expects meters (1 mile = 1609.344 meters)
result["vehicleMileage"] = round(value * 1609.344, 1)
# Value is distance in km; HA expects meters
result["vehicleMileage"] = value * 1000

return result
except Exception:
Expand Down Expand Up @@ -541,7 +545,7 @@ def decode_tires(payload: str) -> dict[str, Any]:

Returns dict with keys:
- tirePressureFrontLeft, tirePressureFrontRight, etc. (bar)
- tirePressureStatusFrontLeft, etc. ("Ok")
- tirePressureStatusFrontLeft, etc. ("OK")
"""
if not payload:
return {}
Expand All @@ -560,7 +564,7 @@ def decode_tires(payload: str) -> dict[str, Any]:
if in_num == 1 and in_type == 0:
pos = in_val
elif in_num == 2 and in_type == 0:
status = "Ok" if in_val == 1 else "Warning"
status = "OK" if in_val == 1 else "Warning"
elif in_num == 3 and in_type == 1: # 64-bit float (bar)
pressure = round(in_val, 2)

Expand Down
66 changes: 58 additions & 8 deletions tests/test_parallax.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,50 @@ def test_decode_charging_graph_global_stopped_state() -> None:
assert result.get("kilometersChargedPerHour") == 0.0


def test_decode_charging_graph_global_resumed_session() -> None:
"""Test charging graph when charging resumes after an idle pause."""
# Active Segment 1: 1785695977217 -> 1785696037217 (60s, 5.8 kW, state=3)
seg1 = (
b"\x08\x48"
+ bytes([21])
+ struct.pack("<f", 5.8)
+ b"\x18\x81\xfe\x98\x9e\xfc3"
+ b"\x20\xe1\xd2\x9c\x9e\xfc3"
+ b"\x30\x03"
)
# Idle Segment 2: 1785696037217 -> 1785696637217 (600s pause, state=8)
seg2 = (
b"\x08\x48"
+ b"\x18\xe1\xd2\x9c\x9e\xfc3"
+ b"\x20\xa1\xa9\xb8\x9e\xfc3"
+ b"\x30\x08"
)
# Active Segment 3: 1785696637217 -> 1785696757217 (120s resumed, 5.8 kW, state=3)
seg3 = (
b"\x08\x48"
+ bytes([21])
+ struct.pack("<f", 5.8)
+ b"\x18\xa1\xa2\xc1\x9e\xfc3"
+ b"\x20\xe1\xcb\xc8\x9e\xfc3"
+ b"\x30\x03"
)
outer = (
bytes([10, len(seg1)])
+ seg1
+ bytes([10, len(seg2)])
+ seg2
+ bytes([10, len(seg3)])
+ seg3
)
payload_b64 = base64.b64encode(outer).decode()

result = decode_charging_graph_global(payload_b64)
# timeElapsed must equal sum of active segments (60s + 120s = 180s), ignoring the 600s gap
assert result.get("timeElapsed") == 180
assert result.get("power") == 5.8
assert "startTime" in result


def test_decode_charging_session_status() -> None:
"""Test charging.session.status decoder."""
# field 1 = plugConnectionStatus (1), field 2 = displayStatus (3), field 3 = evseType (2)
Expand Down Expand Up @@ -215,16 +259,15 @@ def test_decode_odometer() -> None:
payload_b64 = base64.b64encode(raw).decode()

result = decode_odometer(payload_b64)
expected_meters = round(17114 * 1609.344, 1)
assert result.get("vehicleMileage") == expected_meters
assert result.get("vehicleMileage") == 17114000


def test_decode_tires() -> None:
"""Test dynamics.tires.state decoder."""
# Nested tire: pos=1 (FL), status=1 (Ok), pressure=3.48 (float)
# Nested tire: pos=1 (FL), status=1 (OK), pressure=3.48 (float)
inner = (
b"\x08\x01" # field 1 = 1 (FL)
+ b"\x10\x01" # field 2 = 1 (status Ok)
+ b"\x10\x01" # field 2 = 1 (status OK)
+ b"\x19"
+ struct.pack("<d", 3.48) # field 3 = 3.48 (float)
)
Expand All @@ -233,7 +276,7 @@ def test_decode_tires() -> None:

result = decode_tires(payload_b64)
assert result.get("tirePressureFrontLeft") == 3.48
assert result.get("tirePressureStatusFrontLeft") == "Ok"
assert result.get("tirePressureStatusFrontLeft") == "OK"


def test_decode_closures() -> None:
Expand All @@ -258,8 +301,8 @@ def test_decode_locks() -> None:
payload_b64 = base64.b64encode(outer).decode()

result = decode_locks(payload_b64)
assert result.get("doorFrontLeftLocked") == "locked"
assert result.get("doorFrontRightLocked") == "unlocked"
assert result.get("doorFrontLeftLocked") == "unlocked"
assert result.get("doorFrontRightLocked") == "locked"


def test_decode_cabin_temperatures() -> None:
Expand All @@ -269,7 +312,14 @@ def test_decode_cabin_temperatures() -> None:
payload_b64 = base64.b64encode(raw).decode()

result = decode_cabin_temperatures(payload_b64)
assert result.get("cabinClimateInteriorTemperature") == 23.5
assert result.get("cabinClimateDriverTemperature") == 23.5

raw = b"\x1d\x00\x00\xf8A%\x00\x00\xacA"
payload_b64 = base64.b64encode(raw).decode()

result = decode_cabin_temperatures(payload_b64)
assert result.get("cabinClimateInteriorTemperature") == 31
assert result.get("cabinClimateDriverTemperature") == 21.5


def test_decode_power_state() -> None:
Expand Down