-
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 1 commit
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,72 @@ 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 whole-document view combining site_info and tariff. | ||
|
|
||
| Merges the latest slim `site_info` with the last known | ||
| `tariff_content_v2` piece under a `tariff_content_v2` key, so | ||
| consumers get the same shape the REST site_info endpoint returns | ||
| without hand-assembling it from two separate listeners. 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,18 @@ 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 preserves | ||
| legacy-all behavior: every applicable event is delivered | ||
| unfiltered, forever. | ||
| """ | ||
| 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(topics) if topics is not None else None | ||
| self._listeners: dict[ | ||
| Callable[..., Any], tuple[Callable[[dict[str, Any]], None], dict[str, Any] | None] | ||
| ] = {} | ||
|
|
@@ -214,9 +220,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