feat(teslemetry): add find_gateway_address() typed accessor for Powerwall LAN discovery#81
Merged
Merged
Conversation
added 3 commits
July 14, 2026 16:59
Discovers the gateway's LAN IPv4 from networking_status so Home Assistant's local-control config flow can pre-fill the host instead of requiring the user to type it. Follows the find_authorized_clients() typed-accessor precedent: eth/wifi only (never gsm), prefers the active-route interface, falls back to the first with a decodable address, and decodes ipv4_config's raw big-endian uint32 fields into dotted-quad form.
…d local-control docs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Add TeslemetryEnergySite.find_gateway_address(), a typed accessor that discovers the Powerwall gateway's LAN IPv4 from the networking_status command, so Home Assistant's local-control config flow (core PR #176328) can pre-fill the host instead of requiring the user to type it manually. Follows the exact precedent of the existing find_authorized_clients() typed accessor (module-level helper functions, key-presence checks, InvalidResponse only on null body or unrecognized shape, None for a well-formed-but-empty result). Real payload shape came from a live Powerwall 3 capture (networking_status response with wifi_config, and per-interface wifi/eth/gsm blocks each carrying ipv4_config). Key technical decision: ipv4_config.address/subnet_mask/gateway are raw big-endian uint32 integers on the wire, not dotted-quad strings - verified 3232235914 decodes big-endian to 192.168.1.138 (a plausible RFC1918 address) and confirmed against a matching gateway value; little-endian was ruled out as implausible. Selection heuristic (deliberately specified, matches the analogous Home Assistant Teslemetry integration's own _extract_host helper): consider only eth and wifi interfaces, never gsm (cellular is not a LAN path); prefer whichever has active_route set and a decodable address; otherwise fall back to the first of eth/wifi (in that order) with any decodable address; return None when neither yields a usable address. Added tests/test_teslemetry_gateway_address.py with 11 cases: the real captured sample (anonymized identifiers, structure verbatim) selecting the active-route wifi address, an eth-preferred case, two no-active-route fallback cases, gsm-only returning None, empty/missing-interface cases returning None, a bare-body (no response envelope) case, and null-body/list-body/unrecognized-shape cases raising InvalidResponse. Full test suite (358 tests) passes, ruff check/format and pyright strict are clean on the changed files. Also added a knowledge entry to AGENTS.md documenting the uint32-decoding gotcha and the selection heuristic, alongside the existing find_authorized_clients entry it extends. Note for the PR: this change should mention it unblocks HA core PR #176328's local-control IP auto-discovery, and that release 1.7.5 follows immediately once merged.
What Changed
TeslemetryEnergySite.find_gateway_address()(tesla_fleet_api/teslemetry/energysite.py), a typed accessor over thenetworking_statuscommand that returns the Powerwall gateway's LAN IPv4 as a dotted-quad string. It decodesipv4_config.addressas a raw big-endian uint32 (verified against a live Powerwall 3 capture where3232235914decodes to192.168.1.138), considers onlyeth/wifiinterfaces (nevergsm), prefers the interface withactive_routeset, and falls back to the first of eth/wifi with a decodable address.{"response": null}envelope raisesInvalidResponse(matching the siblingfind_authorized_clients()accessor and the endpoint's known intermittent null-body mode), while0/0xFFFFFFFFaddresses are treated as undecodable so an unconfigured interface never shadows a real one with0.0.0.0; a well-formed response with no usable interface returnsNone.tests/test_teslemetry_gateway_address.py(15 cases covering the real capture, eth/wifi preference and fallback, gsm exclusion, zero/broadcast rejection, and malformed-shape raises) and documented the accessor indocs/teslemetry.mdanddocs/energy_local_control.md. This unblocks host auto-discovery in Home Assistant's local-control config flow (core PR #176328).Risk Assessment
✅ Low: A small, additive, well-tested typed accessor following an established in-repo pattern; all prior review findings were verifiably fixed with pinned regression tests, and the only remaining note is optional test-helper dedup.
Testing
Ran the 15 new gateway-address tests and the full 362-test suite (all pass), then demonstrated the feature end-to-end by pointing a real Teslemetry client at a local HTTP server serving the live Powerwall 3 capture: find_gateway_address() decoded the raw big-endian uint32 wire value to 192.168.1.138 from the active-route wifi interface (gsm ignored), and the intermittent {"response": null} malformed mode raised InvalidResponse as designed.
Evidence: End-to-end demo transcript (real HTTP server + real client stack)
Live-captured wire values (raw big-endian uint32, not strings): wifi.ipv4_config = {"dhcp_enabled": true, "address": 3232235914, "subnet_mask": 4294967040, "gateway": 3232235777} === Case 1: real Powerwall 3 capture (wifi active_route, eth inactive, gsm ignored) === [server] serving networking_status at http://127.0.0.1:44311/api/1/energy_sites/12345/command/networking_status [server] GET /api/1/energy_sites/12345/command/networking_status [server] Authorization: Bearer demo-token [client] find_gateway_address() -> '192.168.1.138' [HA config flow] would pre-fill local-control host field with: 192.168.1.138 === Case 2: intermittent malformed {"response": null} body === [server] serving networking_status at http://127.0.0.1:39253/api/1/energy_sites/12345/command/networking_status [server] GET /api/1/energy_sites/12345/command/networking_status [server] Authorization: Bearer demo-token [client] find_gateway_address() raised InvalidResponse: InvalidResponse('The response from Tesla was invalid.')Evidence: End-to-end demo script
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
tesla_fleet_api/teslemetry/energysite.py:196- A response of {"response": null} silently returns None instead of raising InvalidResponse: _networking_status_body only unwraps the envelope when response is a dict, so a null nested under the envelope leaves body as {"response": None}, the eth/wifi lookups miss, and the result is indistinguishable from a well-formed no-LAN-interface response. The sibling _authorized_clients_list raises for the analogous shape, and AGENTS.md documents these Teslemetry command endpoints intermittently returning null payloads, so this collapses a known malformed-data mode into "no address". The docstring suggests the any-dict-is-well-formed boundary was deliberate, so confirm intent; if changed, add a test pinning the {"response": null} behavior.tesla_fleet_api/teslemetry/energysite.py:168- _decode_ipv4(0) returns "0.0.0.0", which _interface_address treats as a usable address. In the no-active-route fallback (eth tried before wifi), an unconfigured eth block reporting address: 0 (e.g. unplugged port, no DHCP lease) would shadow a real wifi address and pre-fill 0.0.0.0 as the local-control host. Consider rejecting 0 (and possibly 0xFFFFFFFF) as undecodable. Speculative about actual wire behavior - the live capture's inactive eth carried a real address - so flagging for the author's judgment rather than as a defect.AGENTS.md:146- The new AGENTS.md entry says find_gateway_address() "decodes ipv4_config.address/subnet_mask/gateway", but the code only reads and decodes ipv4_config.address; subnet_mask/gateway are never touched. Reword to say the wire format applies to all three fields but only address is decoded, so future sessions aren't misled about the accessor's surface.🔧 Fix: reject null envelope and 0/broadcast addresses in gateway lookup
1 info still open:
tests/test_teslemetry_gateway_address.py:30- The _fake_response/_make_session/_make_site mock helpers are duplicated verbatim from tests/test_teslemetry_authorized_clients.py. With a third typed accessor likely to follow the same pattern, extracting them into a shared test helper module (e.g. tests/teslemetry_mock_helpers.py) would remove ~30 duplicated lines. Purely optional dedup; both copies are correct.✅ **Test** - passed
✅ No issues found.
uv run pytest tests/test_teslemetry_gateway_address.py -v (15 passed: real capture selection, eth/wifi preference and fallback, gsm exclusion, 0/broadcast address rejection, None for well-formed-but-empty, InvalidResponse for null body/null envelope/list/scalar shapes)uv run pytest tests (full suite, 362 passed, no regressions)End-to-end manual check: real aiohttp server impersonating api.teslemetry.com serving the captured networking_status payload, real Teslemetry client + energySites.create(12345) calling find_gateway_address() over an actual HTTP round trip -> '192.168.1.138'; second run with {"response": null} -> InvalidResponse raised✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.