feat(teslemetry): add energy gateway pairing wait helper - #108
Conversation
Combines authorized-client state polling with an optional verify-by-use local read: resolves on VERIFIED, fails distinctly and immediately on the terminal PENDING_VERIFICATION_TIMEOUT state instead of polling it out, and enforces its own bounded overall wait as a separate, retryable timeout.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d25878ed60
ℹ️ 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".
| except asyncio.TimeoutError as exc: | ||
| raise AuthorizedClientWaitExpired( | ||
| {"public_key": target, "state": last_state} | ||
| ) from exc |
There was a problem hiding this comment.
Keep polling after per-attempt timeouts
When find_authorized_clients() raises its own TimeoutError before the overall deadline—for example, due to a transient HTTP timeout—asyncio.wait_for() propagates that same exception into this handler, which incorrectly reports that the entire pairing wait expired instead of retrying. The identical issue affects verify_by_use(), where a transient signed-read timeout is explicitly supposed to mean “not yet confirmed.” Distinguish expiry of the outer deadline from an inner operation timeout and continue polling while time remains.
AGENTS.md reference: AGENTS.md:L151-L151
Useful? React with 👍 / 👎.
Intent
Implement the captain-approved wait_until_paired() verify-by-use helper for Tesla energy-gateway key pairing (TeslemetryEnergySite.wait_until_paired in teslemetry/energysite.py). Live hardware findings (relayed by the captain from an aiopowerwall key-testing agent) confirmed the corrected AuthorizedClientState enum from PR #107 is accurate: PENDING_VERIFICATION=1 means still waiting, PENDING_VERIFICATION_TIMEOUT=2 is a TERMINAL failure (the ~9-minute presence-proof window expired, never poll through it), VERIFIED=3 is success. The helper polls find_authorized_clients() for the entry matching the given public_key, returns immediately on VERIFIED, and raises a new AuthorizedClientPairingTimedOut exception immediately on PENDING_VERIFICATION_TIMEOUT rather than treating it as in-progress. It also enforces its own bounded overall timeout (default 600s / 10 minutes, well above the ~9-minute window but a hard ceiling) via a new AuthorizedClientWaitExpired exception, distinct from the terminal-state exception, when the registration is still merely pending when time runs out. Docstring documents the retry path explicitly: re-registering the SAME public key resets the presence-proof window without creating a duplicate record - this is deliberate, never generate a new key on timeout. An optional async verify_by_use callable (e.g. an aiopowerwall client's live_status()) lets a caller with local network access require an actual successful signed local read as confirmation on top of the cloud VERIFIED state, per the captain's original design shape: authorized-client state is the primary signal, verify-by-use is confirmation only where available (i.e. optional) - if verify_by_use is omitted, cloud VERIFIED alone is treated as success; if provided, a failing call while state is already VERIFIED is treated as 'not yet confirmed, keep polling' rather than a hard failure, since local usability can trail the cloud-reported state slightly. Only RSA keys are relevant for verify-by-use since ECC cannot authenticate the LAN TEDapi v1r protocol (documented elsewhere in the codebase already; this helper doesn't add ECC support). Implemented only on TeslemetryEnergySite (not the base EnergySite or Tessie) because it's built on the existing typed find_authorized_clients()/AuthorizedClient accessor, which is Teslemetry-specific - the base EnergySite.list_authorized_clients() returns an unparsed gRPC-shaped dict with no typed parsing today. Added 9 unit tests (tests/test_teslemetry_wait_until_paired.py) covering: immediate success on VERIFIED, polling through PENDING_VERIFICATION to VERIFIED, immediate distinct failure on PENDING_VERIFICATION_TIMEOUT, overall-timeout enforcement while still pending, verify_by_use confirming after VERIFIED, verify_by_use failing once then succeeding, verify_by_use never succeeding (raises wait-expired), no matching client entry (raises wait-expired), and accepting raw public-key bytes vs an already-base64 string. Also updated docs/energy_local_control.md's pairing-verification section to point at the new helper (keeping the existing manual polling example as a reference for callers without the typed Teslemetry accessor) and added an AGENTS.md entry documenting the helper's scope and behavior. Cancellation is handled by letting asyncio.CancelledError propagate normally, per Python/asyncio convention - no custom cancelled-state exception was added.
What Changed
TeslemetryEnergySite.wait_until_paired()with bounded polling, raw or base64 public-key matching, and optional signed local-read verification.AuthorizedClientPairingTimedOutandAuthorizedClientWaitExpiredexceptions.Risk Assessment
✅ Low: The follow-up consistently applies the shared remaining-time deadline to both client lookup and verify-by-use, preserves terminal-state and cancellation behavior, and adds focused regression coverage.
Testing
Inspected the targeted change, ran all focused pairing-helper tests successfully, and captured an end-user-style API transcript proving verify-by-use retries after cloud verification and immediate distinct failure on the gateway’s terminal timeout state; no issues were found and the worktree remained clean.
Evidence: End-user pairing-helper demonstration
signed local read attempt 1: not usable yet signed local read attempt 2: success result: key=demo-rsa-public-key-base64, state=VERIFIED, cloud_polls=3 terminal state: AuthorizedClientPairingTimedOut; polls=1Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
tesla_fleet_api/teslemetry/energysite.py:491- The required “bounded overall timeout … a hard ceiling” is not enforced around either awaited operation.find_authorized_clients()can hang past the deadline, and after a VERIFIED resultverify_by_use()can do the same; the deadline is checked only after each await returns. Wrap the whole polling operation, or each await using the remaining deadline, at the earliest shared boundary and translate expiry toAuthorizedClientWaitExpired.🔧 Fix: Enforce pairing timeout across awaited operations
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
Inspectedgit diff 0cf9962a090319df9449d01ddc79b1a922cc6ecb f3c1c9df1cf823d3212bf4fa317bb6b5e6f2cd7dfor the helper, exceptions, documentation, and focused tests.Ranuv run pytest -q tests/test_teslemetry_wait_until_paired.py.Ran an async public-API demonstration usingTeslemetryEnergySite.wait_until_paired()that exercised pending → verified, a temporarily failing then successful signed local read, and immediate terminal-state failure.Rangit status --shortto confirm testing left no transient worktree changes.✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.