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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This file is the project's committed home for project-intrinsic agent knowledge:
- 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.
- Energy site events (`teslemetry_stream/energysite.py`) are shaped differently from vehicle signals: `live_status`/`site_info` are flat top-level envelopes (`{createdAt, site_id, isCache?, live_status|site_info}`), not nested under `data`, and the payload is a full opaque document rather than a field delta - there is no per-field config to enable, the server auto-polls subscribed sites. Contract source: Teslemetry/api PR 310 (`src/routes/sse/index.ts`, `liveStatusSchema.ts`, `siteInfoSchema.ts`), flag-gated server-side as of this writing - `tests/test_energysite_events.py` fixtures mirror that PR's schemas.
- `energy_totals` (Teslemetry/api PR 316) is shaped differently again: the site id rides the `id` field, not `site_id`, alongside `product_type: "energy_site"` and `topic: "energy_totals"` - filter on those three keys, not `site_id`. It carries a compact cumulative `totals` object (`EnergyHistoryTotals` in `const.py`) instead of a document, fires only when the server's periodic `calendar_history` poll detects a change (silence is not staleness), and has no snapshot-on-connect delivery. The `url` field is the canonical REST path to GET the full time series.
- `energy_totals` (Teslemetry/api PR 316, trimmed by PR 321) is shaped differently again: the site id rides the `id` field, not `site_id` - filter on `id` and `totals`, not `site_id`. It carries a compact cumulative `totals` object (`EnergyHistoryTotals` in `const.py`) instead of a document, fires only when the server's periodic `calendar_history` poll detects a change (silence is not staleness), and has no snapshot-on-connect delivery. As of PR 321 the wire payload is trimmed to `id`/`createdAt`/`totals` plus `isCache` only when true (`product_type`/`topic`/`url` were dropped as redundant with the event's own topic name and site id); `Key.PRODUCT_TYPE`/`Key.TOPIC`/`Key.URL` in `const.py` remain defined for other event kinds but are no longer part of the energy_totals filter.
- `site_info` events no longer carry `tariff_content`/`tariff_content_v2` (Teslemetry/api PR 318); the V2 tariff is its own `tariff_content_v2` event/listener (`listen_TariffContentV2`), same envelope shape as `site_info`, with a `None` body meaning an explicit server-side removal rather than "not received yet". Both share the same silence-means-no-change contract - freshness lives in REST, never in event cadence. There is deliberately no library helper recombining `site_info` and `tariff_content_v2` into one document - that would only ever cover the V2 tariff (legacy V1 `tariff_content` has no SSE topic and stays REST-only by design), so it can't actually promise the whole REST-shaped document; a consumer wanting both tariffs together should use the REST site_info endpoint.
- `TeslemetryStream(topics=...)` (Teslemetry/api PR 319) is an optional exact SSE wire-event allowlist sent as the connection's `topics` query param; `SseTopic` in `const.py` is the closed set the server recognizes (must stay in sync with the api's `SSE_TOPICS`), and `SSE_VEHICLE_TOPICS`/`SSE_ENERGY_TOPICS`/`SSE_ALL_TOPICS` are client-side presets - flat per-product-kind lists of exact wire names, deliberately not further split by whether a topic happens to have a connect-time snapshot server-side; that's upstream server behavior, not something this library encodes. Omitting `topics` (`None`) is legacy-all forever - every applicable event delivered unfiltered - and existing callers that never pass it are unaffected. An explicitly empty iterable is rejected with `ValueError` at construction time rather than silently falling back to legacy-all - "no topics" must not mean "all topics", mirroring the server's own 400 on an empty `topics` value. A bare `str`/`SseTopic` is accepted as a single topic rather than iterated character-by-character - `topics` type-checks `str | Iterable[str] | None` precisely because a lone string also satisfies `Iterable[str]`, the classic footgun. `tests/test_sse_topics.py` covers the tariff listener, its null-removal signal, the `topics` param's URL construction, the empty-iterable rejection, and the bare-string/bare-`SseTopic` case.

Expand Down
10 changes: 4 additions & 6 deletions teslemetry_stream/energysite.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable

from .const import EnergyHistoryTotals, Key, ProductType, RefreshTopic
from .const import EnergyHistoryTotals, Key

if TYPE_CHECKING:
from .stream import TeslemetryStream
Expand Down Expand Up @@ -80,15 +80,13 @@ def listen_EnergyTotals(
Unlike live_status/site_info, this event carries no full document -
just cumulative totals and the event fires only when the server's
5-minute poll actually detects a change. Silence means no change,
never staleness; there is no snapshot-on-connect delivery. A
consumer wanting the full time series must GET the event's `url`
via their own REST client - this listener only exposes the totals.
never staleness; there is no snapshot-on-connect delivery. This
listener only exposes the totals.
Comment on lines +83 to +84

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the obsolete URL instructions from the README

When users consult the public API documentation, README.md lines 170-173 and 312-313 still say that energy_totals carries a url that they should GET. The trimmed event no longer contains that field, and this listener exposes only the totals, so those instructions cannot be followed; update both README passages to omit the URL or name the REST endpoint directly.

AGENTS.md reference: AGENTS.md:L13-L13

Useful? React with 👍 / 👎.

"""
return self.stream.async_add_listener(
lambda x: callback(EnergyHistoryTotals.from_dict(x[Key.TOTALS])),
{
Key.ID: self.site_id,
Key.PRODUCT_TYPE: ProductType.ENERGY_SITE,
Key.TOPIC: RefreshTopic.ENERGY_TOTALS,
Key.TOTALS: None,
},
)
12 changes: 3 additions & 9 deletions tests/test_energysite_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
Fixtures mirror the `liveStatusSchema`/`siteInfoSchema` from Teslemetry/api
PR 310: a flat envelope of `createdAt`, `site_id`, optional `isCache`, and
the full document under `live_status`/`site_info` (opaque, not a delta).
`energy_totals` fixtures mirror PR 316's notification schema (renamed
from `calendar_history_refreshed` to `energy_totals` post-merge): the
uniform notification shape
(`id`/`product_type`/`topic`/`url`/`createdAt`/`isCache`) plus a compact
`totals` object - no `site_id` key, the site id rides `id` instead.
`energy_totals` fixtures mirror PR 321's trimmed notification schema:
`id`/`createdAt`/`totals`, with `isCache` present only when true - no
`site_id`, `product_type`, `topic`, or `url` keys.
"""
from __future__ import annotations

Expand Down Expand Up @@ -87,11 +85,7 @@

ENERGY_TOTALS_EVENT: dict[str, Any] = {
"id": SITE_A,
"product_type": "energy_site",
"topic": "energy_totals",
"url": f"/api/1/energy_sites/{SITE_A}/calendar_history?kind=energy&period=day",
"createdAt": "2026-07-29T10:16:00.000Z",
"isCache": False,
"totals": ENERGY_TOTALS_FIXTURE,
}

Expand Down
Loading