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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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.
- 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_<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.
- 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.
Expand Down
1 change: 1 addition & 0 deletions teslemetry_stream/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ def upper_options(self) -> list[str]:
"Charging",
"Complete",
"Stopped",
"Calibrating",
],
)

Expand Down
82 changes: 82 additions & 0 deletions tests/test_enum_tables.py
Original file line number Diff line number Diff line change
@@ -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()
Loading