From 36c33c62d60411cd12b2f8a19a7ca2e3cf1ecddc Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 2 Sep 2026 12:25:41 +1000 Subject: [PATCH] fix: add missing DetailedChargeState Calibrating value 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 --- AGENTS.md | 1 + teslemetry_stream/const.py | 1 + tests/test_enum_tables.py | 82 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 tests/test_enum_tables.py diff --git a/AGENTS.md b/AGENTS.md index 930c109..943230f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ This file is the project's committed home for project-intrinsic agent knowledge: - `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`. - `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`. - `Signal` in `const.py` tracks ; 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. +- 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. - 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_` 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. - 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. - `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. diff --git a/teslemetry_stream/const.py b/teslemetry_stream/const.py index 67b5707..021a217 100644 --- a/teslemetry_stream/const.py +++ b/teslemetry_stream/const.py @@ -575,6 +575,7 @@ def upper_options(self) -> list[str]: "Charging", "Complete", "Stopped", + "Calibrating", ], ) diff --git a/tests/test_enum_tables.py b/tests/test_enum_tables.py new file mode 100644 index 0000000..6ddad0e --- /dev/null +++ b/tests/test_enum_tables.py @@ -0,0 +1,82 @@ +"""Pin TeslemetryEnum value tables in const.py against tesla-protocol 1.4.0. + +These tables are hand-maintained, not derived at runtime (see AGENTS.md for +why), so this test is the thing that would catch drift against the proto +enum names on a manual re-check - it is not itself a live comparison. +""" +from __future__ import annotations + +from teslemetry_stream import const + +# name -> expected TeslemetryEnum.values, verified byte-for-byte against +# tesla_protocol.telemetry.vehicle_data_pb2's enum descriptors (1.4.0). +EXPECTED: dict[str, list[str]] = { + "DetailedChargeState": [ + "DetailedChargeStateUnknown", + "DetailedChargeStateDisconnected", + "DetailedChargeStateNoPower", + "DetailedChargeStateStarting", + "DetailedChargeStateCharging", + "DetailedChargeStateComplete", + "DetailedChargeStateStopped", + "DetailedChargeStateCalibrating", + ], + "ShiftState": [ + "ShiftStateUnknown", + "ShiftStateInvalid", + "ShiftStateP", + "ShiftStateR", + "ShiftStateN", + "ShiftStateD", + "ShiftStateSNA", + ], + "BMSState": [ + "BMSStateUnknown", + "BMSStateStandby", + "BMSStateDrive", + "BMSStateSupport", + "BMSStateCharge", + "BMSStateFEIM", + "BMSStateClearFault", + "BMSStateFault", + "BMSStateWeld", + "BMSStateTest", + "BMSStateSNA", + ], + "CarType": [ + "CarTypeUnknown", + "CarTypeModelS", + "CarTypeModelX", + "CarTypeModel3", + "CarTypeModelY", + "CarTypeSemiTruck", + "CarTypeCybertruck", + ], +} + + +def check(label: str, ok: bool, detail: str = "") -> bool: + print(f"{'PASS' if ok else 'FAIL'}: {label}" + (f" ({detail})" if detail and not ok else "")) + return ok + + +def main() -> None: + results = [] + for name, expected in EXPECTED.items(): + table = getattr(const, name) + results.append( + check( + f"{name}.values matches tesla-protocol 1.4.0", + table.values == expected, + f"got {table.values}", + ) + ) + + print("-" * 72) + print("ALL PASS" if all(results) else "FAILURES PRESENT") + if not all(results): + raise SystemExit(1) + + +if __name__ == "__main__": + main()