Skip to content

Instantly share code, notes, and snippets.

@nalditopr
Last active March 6, 2026 03:54
Show Gist options
  • Select an option

  • Save nalditopr/129c811172c3e4e1d633308da05267e4 to your computer and use it in GitHub Desktop.

Select an option

Save nalditopr/129c811172c3e4e1d633308da05267e4 to your computer and use it in GitHub Desktop.
v1r Follower Routing — Technical Deep Dive

v1r Follower Routing — Technical Deep Dive

Problem

In v1r (wired LAN RSA) mode, per-device queries to follower Powerwalls return the leader's data. The gateway ignores the follower DIN in the inner protobuf recipient.din and processes the query against its own components. This causes get_pw3_vitals() to produce duplicate PVAC/PVS/TEPINV/TEPOD entries — 3 copies of the leader's data instead of distinct per-device data.

Current workaround: Skip follower battery blocks when self.v1r is set. System-level aggregates (SOE, meters, battery_level) are unaffected since they come from the leader's gateway-level data. WiFi mode is completely unaffected.


System Topology (Example: 3 PW3 + 1 Expansion)

Role IP DIN Type
Leader 10.42.1.x 1707000-xx-K--TGxxxxxxxxxx Powerwall3
Follower 1 10.42.1.y 1707000-xx-K--TGyyyyyyyyyy Powerwall3Follower
Follower 2 10.42.1.z 1707000-xx-K--TGzzzzzzzzzz Powerwall3Follower

Expansion pack (1807000-xx-B--TGeeeeeeeeee) is physically attached to the leader (no separate IP, shows up as additional BMS/HVP components in leader queries).

Firmware 25.42.2 on all units.


What We Tried

Approach 1: domain=7 with inner recipient.din = follower

  • Result: HTTP 200, returns data — but it's always leader data.
  • Verified by checking PCH serial number: always the leader's serial regardless of which follower DIN is targeted.
  • PV/battery values vary slightly (queries are seconds apart) but BMS data is identical across all three "different" responses.
  • This is what the code was doing before the fix — the proxy showed 3 PVAC, 3 PVS, 3 TEPINV, 6 TEPOD, all duplicates.

Approach 2: routing_address = follower DIN

  • Result: MESSAGEFAULT_ERROR_TIMEOUT (fault code 2).
  • The leader does attempt to forward — it recognizes routing_address as a routing instruction and tries to deliver.
  • But the follower never responds. Timeout after ~20 seconds.
  • Tested with follower full DIN, follower serial only, leader+follower from_destination combos — all timeout.

All 18 Routing Variants Tested

Variant outer to_destination inner recipient Result
domain=7, recip=leader domain=7 leader OK (leader data)
domain=7, recip=follower domain=7 follower OK (leader data)
routing_address=follower DIN follower DIN follower TIMEOUT
routing_address=follower DIN follower DIN leader TIMEOUT
ra=follower, sign with follower DIN follower DIN follower TIMEOUT
ra=follower, from_ra=leader follower DIN follower TIMEOUT
domain=0 (BROADCAST) domain=0 follower TIMEOUT
domain=8 (ENERGY_DEVICE_AUTH) domain=8 follower TIMEOUT
ra=follower serial bytes serial bytes follower TIMEOUT
ra=expansion DIN expansion DIN expansion TIMEOUT

Plus 8 more minor variations of the above — all either timeout or return leader data.


What the Followers Actually Support

Tested by connecting directly to follower IPs on the wired LAN:

Endpoints That Work on Followers

Endpoint Method Status Notes
/api/login/Basic POST 200 Each follower accepts its own customer password
/tedapi/din GET 200 Returns follower's own DIN (confirmed correct)
/tedapi/info GET 200 Returns firmware version, DIN, deviceType (no auth needed)
/tedapi/v1r POST 200 Endpoint exists and accepts POSTs

Endpoints That Don't Work on Followers

Endpoint Status Notes
/api/meters/aggregates 502 Followers don't run the full gateway
/api/system_status/soe 502 Same
/api/system_status/grid_status 502 Same
/tedapi/v1 403 Interface-blocked (same as leader on wired LAN)
/tedapi/device/* 403 Interface-blocked wildcard

v1r Direct Response from Followers

When sending RSA-signed v1r requests directly to a follower IP:

Signing DIN Response
Follower's own DIN "v1r: client not authorized" (36 bytes)
Leader DIN MESSAGEFAULT_ERROR_TIMEOUT

The "client not authorized" response confirms:

  • Followers DO process v1r requests (not just returning HTTP 200 blindly)
  • They validate RSA signatures but our public key isn't in their keychain
  • The v1r endpoint is fully functional on followers — just needs key registration

/tedapi/info Discovery

A previously undocumented endpoint works on all Powerwalls (leader + followers) with no authentication required:

GET /tedapi/info

Response:

{
  "din": "1707000-21-K--TGxxxxxxxxxx",
  "firmwareVersion": {
    "version": "25.42.2",
    "githash": "FConoYHQfw/J8aXdKkaEYY6pTNs="
  },
  "deviceType": 4,
  "tegDeviceType": 7
}

Works on wired LAN (10.42.1.x) and internal networks (192.168.90.x). Does not work on home WiFi network (returns 403).


Port 8087 — mTLS Inter-Device Port

All Powerwalls (leader + followers) expose port 8087 on the wired LAN.

TLS Certificate

  • TLS 1.3 with Tesla-signed certificate
  • Subject CN = device DIN
  • Issuer: Tesla Energy Product Issuing CA
  • Extended Key Usage includes TLS Web Client Authentication
  • Custom OID: 1.3.6.1.4.1.49279.2.5.2

Mutual TLS Required

  • Connection without client cert: SSL alert number 116: certificate required
  • Accepted client CAs: Tesla Product Access Issuing CA, Tesla China Product Access Issuing CA
  • Self-signed fake Tesla CA + client cert: TLS alert number 48: unknown ca
  • The Powerwall validates against the real Tesla CA — no bypass possible without a legitimate Tesla-signed client certificate

Significance

This is likely the cloud ↔ device management channel. Inter-PW communication does not use this port (zero packets observed between PWs on wired LAN). Followers communicate with the leader via internal bus (CAN or power-line), not Ethernet.


Network Interfaces & Access Control

Interface-Level Blocking

PW3 firmware enforces access control based on the physical incoming network interface, not IP addresses or routing. This cannot be bypassed by IP spoofing or bridge tricks.

Interface Login /tedapi/din /tedapi/v1 /tedapi/v1r /tedapi/device/*
Wired LAN (10.42.1.x) 200 200 403 200 403
WiFi AP (192.168.91.1) 200 200 200 200 200
Home WiFi 401 403 403 200* 403
Internal (192.168.90.x) 200 200 403 200 403

* v1r returns HTTP 200 on home WiFi but all queries TIMEOUT (including leader-to-leader)

192.168.90.x — Internal Interconnect Network

  • Only Follower 1 has an IP on this subnet (192.168.90.2)
  • Reachable via routing through any PW (leader, follower 1, or follower 2)
  • MAC confirms it's the same device as 10.42.1.y (Follower 1)
  • Access controls identical to wired LAN interface
  • No other devices on this subnet (scanned full /24)

192.168.91.x — Tesla WiFi AP

  • Leader runs WiFi AP (SSID: TeslaPW_xxxxxx) on 192.168.91.1
  • Only interface where /tedapi/v1 and /tedapi/device/*/v1 are accessible
  • Requires WiFi connection to the PW's own AP
  • Adding 192.168.91.200 to wired bridge + routing does not bypass interface check

Inter-PW Communication

Zero packets observed between Powerwalls on the wired LAN (tcpdump on bridge monitoring all 3 PW IPs). The PWs communicate via internal bus (CAN bus or power-line communication), not Ethernet. This means there's no inter-PW traffic to intercept or MITM for extracting mTLS certificates.


API Endpoint Fuzzing Results

Comprehensive fuzzing performed on the leader's wired LAN interface:

  • 577 paths with Tesla-specific wordlist (tedapi, hermes, protobuf, grpc, etc.)
  • 704 paths with non-standard prefixes (/rpc/, /grpc/, /energy/, /device/, etc.)
  • Multiple HTTP methods (GET, POST, PUT, DELETE, PATCH, OPTIONS)
  • Various auth combinations (none, Bearer, Basic, cookies)

Result: Zero new endpoints discovered. Only the 5 known endpoints + the /tedapi/device/* wildcard 403 catchall exist on port 443.


RSA Key Registration Gap

During the Fleet API key registration process (fleet_register.py / python -m pypowerwall register), we only toggled the leader Powerwall's breaker to authorize the RSA key pairing. The followers were never power-cycled or individually registered.

The registration flow:

  1. Generate RSA-4096 key pair
  2. Upload public key to Tesla Fleet API
  3. Send ENERGY_DEVICE_AUTH command via Fleet API to register key on-device
  4. Toggle Powerwall breaker (physical authorization)
  5. Wait for state=3 (key accepted)

We only did steps 4-5 for the leader. The followers were connected but their breakers were never toggled.

Why Follower Routing Times Out

When using routing_address to forward to a follower:

  1. Layer 1 (RSA) passes on the leader — our key is registered
  2. The leader forwards the message internally to the follower
  3. Layer 1 on the follower fails — our key isn't registered there
  4. The follower silently drops the message -> timeout

When connecting directly to a follower's /tedapi/v1r:

  • The follower explicitly responds "v1r: client not authorized"
  • Confirms the RSA key is not in the follower's keychain

Fleet API Limitations

  • POST /api/1/energy_sites/{site_id}/command uses identifier_type: 1 which targets the gateway (leader) only
  • No device-level command endpoints exist (tested: /devices/{id}/command -> 404)
  • Different identifier_type values (0, 2, 3) return 500 errors
  • Record updates via Fleet API only support description changes
  • Per-device key registration via Fleet API does not appear possible

Two Auth Layers in v1r

Understanding why follower queries fail requires understanding the two independent auth layers:

Layer 1: RSA Signature (v1r transport auth)

The outer RoutableMessage contains an RSA signature (SignatureData) that authenticates the request. This is verified by the Powerwall's firmware against its registered key list.

  • Signing uses PKCS1v15 + SHA512 over a TLV-encoded metadata block
  • TLV includes: signature type (RSA=7), domain, personalization (DIN), epoch, expiry, and the inner protobuf bytes
  • Personalization must be the leader DIN — signing with a follower DIN returns fault or "client not authorized"
  • HTTP auth (Bearer token) is completely ignored by /tedapi/v1r

Layer 2: ECDSA CODE (query authorization)

The inner TEDAPI message contains pb.message.payload.send.code — a pre-signed ECDSA signature over the exact query text. This is hard-coded in pypowerwall and was presumably generated by Tesla.

  • If you modify the query text (add/remove fields, change whitespace), the inner response contains "invalid signature"
  • The RSA v1r signature IS accepted (fault=0), but the inner ECDSA check fails independently
  • Must use the exact original query text from pypowerwall source
  • CRITICAL FINDING: The ECDSA signs the query TEXT only — NOT the variables (pb.message.payload.send.b.value). Variables are freely modifiable without breaking the signature.

ECDSA Variable Manipulation — Deep Dive

What Is and Isn't Signed

Protobuf Field Signed by ECDSA? Notes
payload.send.payload.text (query) YES Cannot modify a single character
payload.send.b.value (variables) NO Freely changeable
payload.send.code (ECDSA sig) N/A Must match the query text

This means we can change:

  • Component type filters ("types": [...])
  • Signal name lists (*SignalNames: [...])
  • Any other GraphQL variable parameter

Three ECDSA-Signed Queries Available

pypowerwall contains 3 distinct (query_text, ecdsa_code) pairs:

  1. DeviceControllerQuery v1 — Large formatted query with esCan.bus, batteryBlocks, neurio, phaseDetection, inverterSelfTests. Variables: "{}" (none).

  2. DeviceControllerQuery v2 — Minified query adding teslaRemoteMeter, ieee20305, and a components { msa: components(filter: $msaComp) } section. Variables: $msaComp (component type filter), $msaSignals (signal name list).

  3. ComponentsQuery — PW3-specific, 5 independent filter slots: $pchComponentsFilter, $bmsComponentsFilter, $hvpComponentsFilter, $pwsComponentsFilter, $baggrComponentsFilter, each with a corresponding *SignalNames list.

Valid Component Types Discovered (13 total)

Enumerated by testing each type individually via ComponentsQuery:

Type Entries Has Live Data Notes
PCH 1 YES Leader inverter. pn=1758000-03-J sn=FTIxxxxxxxxxxxxx
PW3BMS 4 2 live + 2 ghost Battery management. Leader + expansion live; 2 ghosts from Dec 2025
PW3HVP 4 2 live + 2 ghost High voltage packs. Leader (1800420-11-F/TGxxxxxxxxxxxxx) + expansion (1807000-20-B/TGyyyyyyyyyyyyy)
PW3SAF 1 YES Safety module. pn=1758000-03-J sn=FTIxxxxxxxxxxxxx
BAGGR 1 YES Battery aggregator. NumBatteriesConnected=2, Expected=2
TEMSA 2 1 live System meter. pn=1841000-01-C sn=GFxxxxxxxxxxxxx
STSTSM 1 Alerts only System manager. pn=1707000-21-K sn=TGzzzzzzzzzzzzz (leader DIN)
TESYNC 1 NO Empty (no serial, no data)
TEPINV 10 NO PW2 stub. Empty placeholder slots
TEPOD 10 NO PW2 stub. Empty placeholder slots
TETHC 10 NO PW2 stub. Empty placeholder slots
PVAC 8 NO PW2 stub. Empty placeholder slots
PVS 8 NO PW2 stub. Empty placeholder slots

Total: 61 component entries (13 with data from PW3 types, 48 empty PW2 stubs)

ComponentFilter Fields Discovered

The GraphQL ComponentFilter input type accepts multiple fields beyond just types:

Filter Field Syntax Works? Effect
types ["PCH", "PW3BMS"] YES Filters by component type
dins ["1707000-xx-K--TG..."] YES Filters by component DIN. Returns 0 for follower DINs
serialNumbers ["TG..."] Accepted Does not error, but doesn't filter differently
parentDins ["1707000-xx-K--TG..."] Accepted Appears to be ignored (returns all)
parentDin "1707000-xx-K--TG..." Accepted Appears to be ignored (returns all)
devices ["1707000-xx-K--TG..."] Accepted Does not error, but doesn't filter differently
componentIds ["TG..."] Accepted Does not error, but doesn't filter differently
partNumbers ["1758000-03-J"] Accepted Does not error, but doesn't filter differently

Key finding: dins genuinely filters and returns 0 entries when no component matches the given DIN. This proves the follower data does not exist in the leader's component database — there's nothing to filter down to.

BMS Ghost Entries Explained

BMS[2-3] and HVP[2-3] appear in ComponentsQuery but are stale ghosts:

Field BMS[0-1] (live) BMS[2-3] (ghost)
Timestamp 2026-03-01 (current) 2025-12-17 (2.5 months old)
Energy remaining 3-5 kWh (varies) 0.2 kWh (static)
Full pack energy 14.25 kWh 14.47 kWh
HVP serial Present Empty
HVP state HVP_ACTIVE null
Alerts Normal NVRAM_Config_Error, HVP_HVI_Comms, Nvram_Filecache
BAGGR status CONNECTED NOT_TARGETED_NOT_PRESENT

The BAGGR confirms: NumBatteriesConnected=2, NumBatteriesExpected=2, slots 2-3 = NOT_TARGETED_NOT_PRESENT. The leader firmware only manages its own 2 packs (leader + expansion). Ghost entries are likely from initial commissioning in December 2025.

esCan Bus — Completely Dead on PW3

DeviceControllerQuery returns esCan.bus with pre-allocated arrays:

  • PINV: 10 entries, ALL isMIA=true, state=PINV_Off, all zeros
  • POD: 10 entries, ALL isMIA=true, energy=0
  • PVAC: 8 entries, ALL isMIA=true, state=PVAC_Init
  • PVS: 8 entries, ALL isMIA=true
  • THC: 10 entries, ALL isMIA=true
  • MSA: null
  • SYNC: empty (no serial, no measurements)

The PW2-era CAN bus interface is non-functional on PW3. All data comes through the PW3 components system instead.

Hardware Identifiers Discovered

Component Part Number Serial Number Role
PCH (inverter) 1758000-03-J FTIxxxxxxxxxxxxx Leader inverter
PW3SAF (safety) 1758000-03-J FTIxxxxxxxxxxxxx Same physical unit as PCH
PW3HVP (battery) 1800420-11-F TGxxxxxxxxxxxxx Leader battery pack
PW3HVP (expansion) 1807000-20-B TGyyyyyyyyyyyyy Expansion battery pack
TEMSA (meter) 1841000-01-C GFxxxxxxxxxxxxx System power meter
STSTSM (system) 1707000-21-K TGzzzzzzzzzzzzz System manager (= leader DIN)

PCH serial FTIxxxxxxxxxxxxx is encoded on the CAN bus as hex ASCII: PCH_packageSerialNumber_1_7 = "3142ITF" (hex) ("3142ITF"), PCH_packageSerialNumber_8_14 = "7210000" (hex) ("7210000"). Read in reverse byte order per chunk to recover the original serial.


Wired LAN Endpoint Enumeration (Leader)

Full scan performed on the leader's wired LAN IP.

Active Endpoints

Endpoint Method Auth Notes
/api/login/Basic POST None Returns token + cookies
/api/meters/aggregates GET Bearer System-level power data
/api/system_status/soe GET Bearer Battery percentage
/api/system_status/grid_status GET Bearer Grid connected/islanded
/tedapi/din GET Bearer DIN plain text
/tedapi/info GET None Firmware version, DIN, device type
/tedapi/v1r POST RSA only Ignores HTTP auth entirely

Blocked Endpoints (403 — interface-level)

  • /tedapi/v1 — WiFi-only (192.168.91.1)
  • /tedapi/device/*wildcard catch-all, returns 403 for ANY path under it, even garbage paths like /tedapi/device/GARBAGE/anything
  • Tested 7 HTTP methods x 5 auth types x 3 body variants = ALL 403
  • This is firmware-level interface blocking, not an auth issue

Endpoints That Don't Exist on PW3

Tested on all 3 Powerwalls (leader + both followers), with and without auth:

  • /summary?mode=kiosk -> 404
  • /api/site_info -> 404
  • /api/status -> 404
  • /api/powerwalls -> 404
  • /api/devices/vitals -> 404

These are PW2/Gateway endpoints that don't exist on PW3 firmware.


FileStore Domain Enumeration

Via v1r, queried all 7 FileStore API domains on the leader:

Domain Value Files Found
CONFIG_JSON 1 config.json
GRID_CODE_REGIONS 6 grid_code_regions.csv
All others 0,2,3,4,5 Empty / no files

No authorization, device registry, or key management files discoverable through FileStore.


Summary: Why Wired LAN Follower Data Is Not Currently Possible

  1. v1r via leader routing — Leader ignores follower DIN (returns own data) or times out when using routing_address
  2. v1r direct to followers — RSA key not registered on followers, and Fleet API provides no per-device registration mechanism
  3. Standard API on followers — Returns 502 (no gateway service running)
  4. Port 8087 mTLS — Requires Tesla-signed client certificate (no bypass)
  5. Interface spoofing — PW3 checks physical NIC, not IP addresses
  6. Home WiFi network — Even more locked down (login 401, everything 403, v1r all TIMEOUT including leader queries)
  7. Internal 192.168.90.x — Same access controls as wired LAN
  8. API fuzzing — No hidden endpoints (1,281 paths tested)
  9. Inter-PW traffic — Zero Ethernet traffic between PWs (internal bus only)
  10. ECDSA variable manipulation — Can freely change component filters and signal names, but the leader's database simply doesn't contain follower component data. 13 valid types enumerated, all empty PW2 stubs or leader-only PW3 data.
  11. ComponentFilter dins field — Genuinely filters components, returns 0 for follower DINs. Proves follower data is absent, not hidden.
  12. esCan bus — All 48 PW2-style entries (PINV, POD, PVAC, PVS, THC) are isMIA=true with zero values. PW2 CAN bus is dead on PW3.
  13. PW2 endpoints/summary, /api/site_info, /api/status, /api/powerwalls, /api/devices/vitals all return 404 on PW3.

The Only Working Path for Per-Device Follower Data

WiFi TEDapi (/tedapi/device/{din}/v1 on 192.168.91.1) via the leader's WiFi AP. This uses HTTP Basic Auth (no RSA) and supports per-device queries. Requires maintaining a WiFi connection to the Powerwall's AP.

Open Questions

  1. Can toggling each follower's breaker during Fleet API registration register the RSA key on followers individually?
  2. Does the Fleet API command endpoint support any undocumented per-device targeting parameters?
  3. Will future firmware updates add follower routing support to v1r?
  4. Could a Tesla-internal tool (like the installer app) register keys on followers?
@Nexarian
Copy link

Nexarian commented Mar 6, 2026

Silly question: Why do you need the FleetAPI to register a key? I thought you send the commands to the powerwalls locally through the wifi connection to do that, and all you need do is power toggle to get the RSA key accepted?

https://github.com/Nexarian/tesla-solar-reporter/blob/main/pvi_status/register_local_client.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment