Skip to content

feat(sse): support topics allowlist and tariff_content_v2 topic - #16

Merged
Bre77 merged 5 commits into
mainfrom
fm/pts-sse-protocol-support
Jul 29, 2026
Merged

feat(sse): support topics allowlist and tariff_content_v2 topic#16
Bre77 merged 5 commits into
mainfrom
fm/pts-sse-protocol-support

Conversation

@Bre77

@Bre77 Bre77 commented Jul 29, 2026

Copy link
Copy Markdown
Member

Intent

Adds Pythonic client support for Teslemetry/api PR 318 (tariff split out of site_info) and PR 319 (SSE topics allowlist), both additive - no existing listener contract changes.

  • TeslemetryStream(topics=...) sends the connection's exact comma-separated SSE topics allowlist (?topics=...); omitting it preserves today's legacy-all behavior forever. SseTopic in const.py mirrors the api's closed SSE_TOPICS set, and SSE_VEHICLE_TOPICS/SSE_ENERGY_TOPICS/SSE_ALL_TOPICS are convenience presets that expand client-side to exact names.
  • site_info events no longer carry tariff_content/tariff_content_v2 (PR 318) - listen_SiteInfo keeps working unmodified against the slimmer shape.
  • TeslemetryStreamEnergySite.listen_TariffContentV2 is a new typed listener for the site's V2 tariff, carrying the document verbatim on change; a None body is the server's explicit removal signal, surfaced distinctly rather than conflated with "no data yet".
  • listen_ComposedSiteInfo merges the latest site_info with the last known tariff_content_v2 piece into a whole-document view, so consumers who want both halves together don't have to hand-assemble them.
  • Both site_info and tariff_content_v2 share the same silence-means-no-change contract: event cadence never signals freshness, only REST does. Documented in the README alongside the new topics-selection example.

Contract taken from Teslemetry/api PRs 318/319 at their merged HEAD (src/lib/sseTopics.ts, src/routes/sse/tariffContentV2Schema.ts, src/routes/sse/siteInfoSchema.ts).

tests/test_sse_topics.py covers topics URL construction (present, omitted, plain strings vs SseTopic), the tariff listener including null-as-removal, site_info ignoring tariff events, and listen_ComposedSiteInfo's merge/removal/withhold-until-first-site_info/listener-teardown behavior.

Adds Pythonic client support for api PR 318 (tariff_content_v2 split
out of site_info) and PR 319 (topics allowlist query param), fully
additive - existing listener contracts are unchanged.

- TeslemetryStream(topics=...) sends the exact comma-separated SSE
  topics allowlist; omitting it preserves legacy-all behavior forever.
  SseTopic plus SSE_VEHICLE_TOPICS/SSE_ENERGY_TOPICS/SSE_ALL_TOPICS
  presets expand client-side to exact wire names.
- listen_TariffContentV2 exposes the V2 tariff document verbatim, with
  a null body surfaced as an explicit removal signal (None).
- listen_SiteInfo keeps working against the now-slim site_info shape.
- listen_ComposedSiteInfo merges the latest site_info with the last
  known tariff piece into a whole-document view.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67afbbcb13

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if self.vin:
url += f"/{self.vin}"
headers = await self.headers()
params = {"topics": ",".join(self.topics)} if self.topics else None

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 Reject empty topic lists instead of enabling all topics

When a caller dynamically constructs topics and the iterable is empty, this truthiness check treats it the same as None and omits the query parameter. Because omission explicitly enables legacy-all behavior, an exact empty allowlist silently receives every applicable event instead of none, potentially increasing traffic and exposing data the caller intended to filter. Distinguish None from an empty list and either send the server-supported empty representation or reject the empty list explicitly.

AGENTS.md reference: AGENTS.md:L15-L15

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

None would have no value, no topics is no data?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed - topics=[] now raises ValueError at construction time instead of falling through to legacy-all. None still means omit the param (legacy-all); an explicit empty iterable is now rejected client-side, mirroring the server's 400 on an empty topics value. Covered in tests/test_sse_topics.py (f698194).

Per PR review: a truthiness check on self.topics treated an empty
iterable the same as None, silently falling back to legacy-all instead
of subscribing to nothing. Distinguish the two at construction time and
raise ValueError on empty, mirroring the server's own 400.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Bre77
Bre77 marked this pull request as draft July 29, 2026 08:34
@Bre77
Bre77 marked this pull request as ready for review July 29, 2026 08:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f69819440e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread teslemetry_stream/energysite.py Outdated
until a value has been received, and again after an explicit
removal.
"""
state: dict[str, Any] = {"site_info": None, "tariff_content_v2": None}

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 Preserve the legacy tariff in the composed document

For sites whose REST site_info contains the legacy tariff_content, this state only tracks the slim SSE document and tariff_content_v2, so listen_ComposedSiteInfo can never produce the promised whole REST-shaped document and silently drops v1 tariff data. Either supply the legacy tariff from another source or describe this API as composing only site_info plus V2 rather than as restoring the whole document.

AGENTS.md reference: AGENTS.md:L14-L14

Useful? React with 👍 / 👎.

@Bre77
Bre77 marked this pull request as draft July 29, 2026 08:43
Per PR review: the docstring/README claimed the composed view produces
"the whole REST-shaped document" / "whole Tesla-shaped document," but
the stream never carries the legacy V1 tariff_content - it has no SSE
topic and stays REST-only by design. Reword to describe the helper as
composing only what the stream itself carries (slim site_info + V2
tariff), and point consumers needing V1 at the REST endpoint. No
behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Bre77

Bre77 commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Re the composed-site-info tariff scoping finding (energysite.py:89): fixed by honest scoping, not by adding a REST fetch. listen_ComposedSiteInfo's docstring/README now describe it as composing only the two streamed pieces - slim site_info plus tariff_content_v2 - and explicitly note that legacy V1 tariff_content has no SSE topic and stays REST-only by design, pointing consumers needing it at the REST site_info endpoint. No behavior change, docs/comments only. Pushed in f656202.

@Bre77
Bre77 marked this pull request as ready for review July 29, 2026 08:46
Comment thread teslemetry_stream/energysite.py Outdated
{Key.SITE_ID: self.site_id, Key.TARIFF_CONTENT_V2: None},
)

def listen_ComposedSiteInfo(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Drop this method

Comment thread teslemetry_stream/const.py Outdated
)

# Energy site topics that are live-only - never part of a connect-time snapshot.
SSE_ENERGY_LIVE_ONLY_TOPICS: tuple[SseTopic, ...] = (SseTopic.ENERGY_TOTALS,)

Copy link
Copy Markdown
Member Author

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

@Bre77
Bre77 marked this pull request as draft July 29, 2026 08:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f656202c5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread teslemetry_stream/stream.py Outdated
self.vin = vin
self.topics: list[str] | None
if topics is not None:
self.topics = list(topics)

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 Treat a single string as one SSE topic

When a caller passes topics="state" or topics=SseTopic.STATE, both values satisfy the advertised Iterable[str] type, but list(topics) splits them into characters, so connect() sends topics=s,t,a,t,e and the server rejects the connection instead of selecting state. Normalize string-like inputs to a one-element list or reject them explicitly at construction.

AGENTS.md reference: AGENTS.md:L15-L15

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.

…ive-only in topic presets

Per captain review:

- Drop listen_ComposedSiteInfo entirely. It could never actually
  produce the whole REST-shaped site_info document (legacy V1
  tariff_content has no SSE topic and stays REST-only by design), so
  the promise it made was structurally unkeepable - not just a docs
  wording problem. A consumer wanting both tariffs together should use
  the REST site_info endpoint.
- Collapse SSE_VEHICLE_SNAPSHOT_TOPICS/SSE_VEHICLE_LIVE_ONLY_TOPICS and
  their energy counterparts into flat SSE_VEHICLE_TOPICS/
  SSE_ENERGY_TOPICS/SSE_ALL_TOPICS presets. Whether a topic happens to
  have a connect-time snapshot is server-side behavior the library has
  no reason to hardcode - the presets only need to know which topics
  apply to which product kind to expand client-side into exact names.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Bre77

Bre77 commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Re "Drop this method" (energysite.py:75, listen_ComposedSiteInfo): done. Removed the method along with its now-unused test coverage and README/AGENTS.md mentions - it could never produce the whole REST-shaped document (legacy V1 tariff_content has no SSE topic and stays REST-only), so the composed helper was structurally unable to keep its promise, not just a wording problem. Pushed in da1130e.

@Bre77

Bre77 commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Re the snapshot/live-only distinction (const.py:375): agreed, and fixed. Collapsed SSE_VEHICLE_SNAPSHOT_TOPICS/SSE_VEHICLE_LIVE_ONLY_TOPICS and their energy counterparts into flat SSE_VEHICLE_TOPICS/SSE_ENERGY_TOPICS/SSE_ALL_TOPICS presets grouped only by product kind (vehicle/energy/account) - which a topic inherently belongs to - not by whether the server happens to give it a connect-time snapshot, which is upstream implementation detail the library has no reason to hardcode. Pushed in da1130e.

@Bre77
Bre77 marked this pull request as ready for review July 29, 2026 08:52
@Bre77
Bre77 marked this pull request as draft July 29, 2026 08:53
Per PR review: topics="state" (or a bare SseTopic member) satisfies
Iterable[str], so list(topics) split it into ['s','t','a','t','e'],
sent as a garbled topics query param the server would reject. Accept
str | Iterable[str] | None and wrap a lone string as a single-element
list before validating emptiness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Bre77

Bre77 commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Re "Treat a single string as one SSE topic" (stream.py:56): fixed. topics now type-checks str | Iterable[str] | None; a bare str (or SseTopic member, itself a str) is wrapped as a single-element list before the empty check runs, instead of being iterated character-by-character. Added coverage for both a bare string and a bare SseTopic in tests/test_sse_topics.py. Pushed in 1b070c4.

@Bre77
Bre77 marked this pull request as ready for review July 29, 2026 08:55
@Bre77
Bre77 merged commit a448704 into main Jul 29, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant