Skip to content

Commit 2efc279

Browse files
authored
fix: add missing DetailedChargeState Calibrating value (#43)
Investigated deriving const.py's TeslemetryEnum value tables from the tesla-protocol package's proto enum descriptors. Nearly every table matches its proto enum byte-for-byte, but adopting tesla-protocol as a runtime dependency would pull in protobuf + googleapis-common-protos - disproportionate for sourcing ~40 static string lists, and risky for Home Assistant consumers sensitive to protobuf version pinning. Not adopted; see AGENTS.md for the recorded rationale. DetailedChargeState was missing the Calibrating value present in the proto, so that's fixed by hand, with a pinning test for the tables most likely to drift. Claude-Session: https://claude.ai/code/session_01AvS7kW3yp5jgSiMNuyj9cQ
1 parent 6fb5309 commit 2efc279

3 files changed

Lines changed: 84 additions & 0 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This file is the project's committed home for project-intrinsic agent knowledge:
99
- `tests/` files are plain scripts (`if __name__ == "__main__"`), not pytest-based - pytest would collect zero tests here. Run each directly, e.g. `uv run python tests/test_config_events.py`.
1010
- `pyproject.toml` has a `[tool.mypy]` config but no dev-dependency group declares mypy, so `uv sync` alone won't install it. CI installs it ephemerally via `uv run --with mypy mypy teslemetry_stream`.
1111
- `Signal` in `const.py` tracks <https://api.teslemetry.com/fields.json>; the config route rejects names it does not know with `fst_err_validation`. Fields the API has retired are not rejected - it accepts the request and names them in a top-level `ignoredFields` list - so the library can lag the published list without breaking.
12+
- The `TeslemetryEnum` value tables in `const.py` (`ShiftState`, `BMSState`, `DetailedChargeState`, etc.) are hand-maintained against the `tesla-protocol` PyPI package's proto enum descriptors (`tesla_protocol.telemetry.vehicle_data_pb2`), not derived from it at runtime: nearly every table matches its proto enum byte-for-byte under simple prefix-stripping, but `tesla-protocol` requires `protobuf` + `googleapis-common-protos` as runtime dependencies, which is disproportionate for sourcing ~40 static string lists in a library whose only current dependency is `aiohttp` and whose consumers (Home Assistant integrations) are sensitive to protobuf version pinning. `ChargeState` is the one table that does not match the proto at all (already commented in `const.py` - deprecated field). `tests/test_enum_tables.py` pins the tables most likely to drift against the proto names actually observed in `tesla-protocol` 1.4.0, as a manual re-check aid, not a live comparison.
1213
- Adding a new streamable field (e.g. from an upstream `teslamotors/fleet-telemetry` `Field` enum addition): give it a `Signal` entry in `const.py` in alphabetical order by the Python constant name, but append its `listen_<Field>` method to the *end* of `TeslemetryStreamVehicle` in `vehicle.py`, not alphabetically re-inserted - the method order there is chronological-by-addition (see the tail of the class), not sorted. Pick `make_int`/`make_float`/`make_bool`/`make_dict` by matching the closest existing field of the same shape (unit suffix, boolean vs measurement, etc.); there is no per-field firmware-version metadata tracked anywhere in the library, so omit it.
1314
- Config responses are shaped inconsistently: success is flat, `{"updated_vehicles": n}` plus `ignoredFields` when some were dropped, while errors are wrapped, `{"response": null, "error": ...}`. Do not look for `updated_vehicles` under `response`; that lookup silently never matches.
1415
- `update_config` funnels every caller through one per-vehicle single-flight flush (`TeslemetryStreamVehicle._flush`): the first caller starts it, later callers merge into the same pending config and await it rather than starting their own PATCH. This exists because a batch of listeners scheduled at once (e.g. HA integration setup) must produce one PATCH, not one per listener - see `tests/test_batch_retry_storm.py`. A body-shaped error (`{"error": ...}`) is terminal for that batch: it is not replayed, but the pending config is kept for the next explicit `update_config` call. A transport-level failure (`aiohttp.ClientError`/timeout) gets one bounded retry inside the same flush. `tests/test_config_update.py` covers the response-shape handling.

‎teslemetry_stream/const.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ def upper_options(self) -> list[str]:
575575
"Charging",
576576
"Complete",
577577
"Stopped",
578+
"Calibrating",
578579
],
579580
)
580581

‎tests/test_enum_tables.py‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Pin TeslemetryEnum value tables in const.py against tesla-protocol 1.4.0.
2+
3+
These tables are hand-maintained, not derived at runtime (see AGENTS.md for
4+
why), so this test is the thing that would catch drift against the proto
5+
enum names on a manual re-check - it is not itself a live comparison.
6+
"""
7+
from __future__ import annotations
8+
9+
from teslemetry_stream import const
10+
11+
# name -> expected TeslemetryEnum.values, verified byte-for-byte against
12+
# tesla_protocol.telemetry.vehicle_data_pb2's enum descriptors (1.4.0).
13+
EXPECTED: dict[str, list[str]] = {
14+
"DetailedChargeState": [
15+
"DetailedChargeStateUnknown",
16+
"DetailedChargeStateDisconnected",
17+
"DetailedChargeStateNoPower",
18+
"DetailedChargeStateStarting",
19+
"DetailedChargeStateCharging",
20+
"DetailedChargeStateComplete",
21+
"DetailedChargeStateStopped",
22+
"DetailedChargeStateCalibrating",
23+
],
24+
"ShiftState": [
25+
"ShiftStateUnknown",
26+
"ShiftStateInvalid",
27+
"ShiftStateP",
28+
"ShiftStateR",
29+
"ShiftStateN",
30+
"ShiftStateD",
31+
"ShiftStateSNA",
32+
],
33+
"BMSState": [
34+
"BMSStateUnknown",
35+
"BMSStateStandby",
36+
"BMSStateDrive",
37+
"BMSStateSupport",
38+
"BMSStateCharge",
39+
"BMSStateFEIM",
40+
"BMSStateClearFault",
41+
"BMSStateFault",
42+
"BMSStateWeld",
43+
"BMSStateTest",
44+
"BMSStateSNA",
45+
],
46+
"CarType": [
47+
"CarTypeUnknown",
48+
"CarTypeModelS",
49+
"CarTypeModelX",
50+
"CarTypeModel3",
51+
"CarTypeModelY",
52+
"CarTypeSemiTruck",
53+
"CarTypeCybertruck",
54+
],
55+
}
56+
57+
58+
def check(label: str, ok: bool, detail: str = "") -> bool:
59+
print(f"{'PASS' if ok else 'FAIL'}: {label}" + (f" ({detail})" if detail and not ok else ""))
60+
return ok
61+
62+
63+
def main() -> None:
64+
results = []
65+
for name, expected in EXPECTED.items():
66+
table = getattr(const, name)
67+
results.append(
68+
check(
69+
f"{name}.values matches tesla-protocol 1.4.0",
70+
table.values == expected,
71+
f"got {table.values}",
72+
)
73+
)
74+
75+
print("-" * 72)
76+
print("ALL PASS" if all(results) else "FAILURES PRESENT")
77+
if not all(results):
78+
raise SystemExit(1)
79+
80+
81+
if __name__ == "__main__":
82+
main()

0 commit comments

Comments
 (0)