-
Notifications
You must be signed in to change notification settings - Fork 3
feat(sse): support topics allowlist and tariff_content_v2 topic #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
67afbbc
f698194
f656202
da1130e
1b070c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,16 +44,75 @@ def listen_SiteInfo( | |
| ) -> Callable[[], None]: | ||
| """Listen for energy site info. | ||
|
|
||
| The callback receives the full site_info document. On connect (and | ||
| whenever a snapshot exists), an initial event is delivered with | ||
| `isCache` set, matching the same snapshot-then-live semantics as | ||
| vehicle state. | ||
| The callback receives the site_info document. This document no | ||
| longer carries `tariff_content`/`tariff_content_v2` - subscribe to | ||
| `listen_TariffContentV2` for the V2 tariff, or use the REST | ||
| site_info endpoint for the full Tesla-shaped document including | ||
| both tariffs. On connect (and whenever a snapshot exists), an | ||
| initial event is delivered with `isCache` set, matching the same | ||
| snapshot-then-live semantics as vehicle state. | ||
| """ | ||
| return self.stream.async_add_listener( | ||
| lambda x: callback(x[Key.SITE_INFO]), | ||
| {Key.SITE_ID: self.site_id, Key.SITE_INFO: None}, | ||
| ) | ||
|
|
||
| def listen_TariffContentV2( | ||
| self, callback: Callable[[dict[str, Any] | None], None] | ||
| ) -> Callable[[], None]: | ||
| """Listen for the site's V2 tariff document. | ||
|
|
||
| The callback receives the `tariff_content_v2` document verbatim, or | ||
| `None` when the server sends an explicit removal signal (the | ||
| site's V2 tariff was cleared). Published only when it changes - | ||
| silence means no change, never staleness, matching `listen_SiteInfo`. | ||
| """ | ||
| return self.stream.async_add_listener( | ||
| lambda x: callback(x[Key.TARIFF_CONTENT_V2]), | ||
| {Key.SITE_ID: self.site_id, Key.TARIFF_CONTENT_V2: None}, | ||
| ) | ||
|
|
||
| def listen_ComposedSiteInfo( | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drop this method |
||
| self, callback: Callable[[dict[str, Any]], None] | ||
| ) -> Callable[[], None]: | ||
| """Listen for a view composing the two streamed site_info pieces. | ||
|
|
||
| Merges the latest slim `site_info` with the last known | ||
| `tariff_content_v2` piece under a `tariff_content_v2` key, so | ||
| consumers don't have to hand-assemble the two separate listeners | ||
| themselves. This only ever carries what the stream itself carries - | ||
| slim `site_info` plus the V2 tariff - never the legacy V1 | ||
| `tariff_content`, which has no SSE topic and stays REST-only by | ||
| design; a consumer needing V1 must fetch the REST site_info | ||
| endpoint directly. Fires whenever either half updates; nothing is | ||
| emitted until the first `site_info` document has arrived. | ||
| `tariff_content_v2` is `None` until a value has been received, and | ||
| again after an explicit removal. | ||
| """ | ||
| state: dict[str, Any] = {"site_info": None, "tariff_content_v2": None} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For sites whose REST AGENTS.md reference: AGENTS.md:L14-L14 Useful? React with 👍 / 👎. |
||
|
|
||
| def emit() -> None: | ||
| if state["site_info"] is None: | ||
| return | ||
| callback({**state["site_info"], Key.TARIFF_CONTENT_V2: state["tariff_content_v2"]}) | ||
|
|
||
| def on_site_info(site_info: dict[str, Any]) -> None: | ||
| state["site_info"] = site_info | ||
| emit() | ||
|
|
||
| def on_tariff(tariff_content_v2: dict[str, Any] | None) -> None: | ||
| state["tariff_content_v2"] = tariff_content_v2 | ||
| emit() | ||
|
|
||
| remove_site_info = self.listen_SiteInfo(on_site_info) | ||
| remove_tariff = self.listen_TariffContentV2(on_tariff) | ||
|
|
||
| def remove_listener() -> None: | ||
| remove_site_info() | ||
| remove_tariff() | ||
|
|
||
| return remove_listener | ||
|
|
||
| def listen_EnergyTotals( | ||
| self, callback: Callable[[EnergyHistoryTotals], None] | ||
| ) -> Callable[[], None]: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
| import json | ||
| import logging | ||
| from datetime import datetime, timezone | ||
| from typing import Any, Awaitable, Callable, cast | ||
| from typing import Any, Awaitable, Callable, Iterable, cast | ||
|
|
||
| import aiohttp | ||
|
|
||
|
|
@@ -27,6 +27,7 @@ def __init__( | |
| vin: str | None = None, | ||
| parse_timestamp: bool = False, | ||
| manual: bool = False, | ||
| topics: Iterable[str] | None = None, | ||
| ): | ||
| """ | ||
| Initialize the TeslemetryStream client. | ||
|
|
@@ -37,13 +38,28 @@ def __init__( | |
| :param vin: Vehicle Identification Number. | ||
| :param parse_timestamp: Whether to parse timestamps. | ||
| :param manual: Whether to start listening manually. | ||
| :param topics: Exact SSE wire event names (see `SseTopic` and its | ||
| presets in `const.py`) to subscribe to. Omitting this (`None`) | ||
| preserves legacy-all behavior: every applicable event is | ||
| delivered unfiltered, forever. An explicitly empty iterable is | ||
| rejected - it means "no topics", not "all topics", mirroring | ||
| the server's own 400 on an empty `topics` value. | ||
| """ | ||
| if server and not server.endswith(".teslemetry.com"): | ||
| raise ValueError("Server must be on the teslemetry.com domain") | ||
|
|
||
| self.active: bool = False | ||
| self.server = server | ||
| self.vin = vin | ||
| self.topics: list[str] | None | ||
| if topics is not None: | ||
| self.topics = list(topics) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a caller passes AGENTS.md reference: AGENTS.md:L15-L15 Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, this should enforce the enum types in a list, or handle the self crafted string. |
||
| if not self.topics: | ||
| raise ValueError( | ||
| "topics must not be empty - omit it (None) for legacy-all behavior" | ||
| ) | ||
| else: | ||
| self.topics = None | ||
| self._listeners: dict[ | ||
| Callable[..., Any], tuple[Callable[[dict[str, Any]], None], dict[str, Any] | None] | ||
| ] = {} | ||
|
|
@@ -214,9 +230,11 @@ async def connect(self) -> None: | |
| if self.vin: | ||
| url += f"/{self.vin}" | ||
| headers = await self.headers() | ||
| params = {"topics": ",".join(self.topics)} if self.topics else None | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a caller dynamically constructs AGENTS.md reference: AGENTS.md:L15-L15 Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. None would have no value, no topics is no data?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed - |
||
| self._response = await self._session.get( | ||
| url, | ||
| headers=headers, | ||
| params=params, | ||
| raise_for_status=True, | ||
| timeout=aiohttp.ClientTimeout( | ||
| connect=5, sock_connect=5, sock_read=30, total=None | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is snapshot now, but why is this distinction even useful in the library? This is upstream behaviour that shouldn't hardcode in the library