feat(tesla_cloud): read Tesla charge state from the Fleet API - #140
Conversation
Optional cloud SoC next to tesla_vehicle (BLE proxy). Emits DerVehicle, binds Tesla+VIN, ages vendor data, and never wakes or commands the car. Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Read-only Fleet API telemetry still has to refresh a token. The signed channel already scopes that POST to auth_post_path; pin tesla_cloud next to myuplink. Signed-off-by: Cursor Agent <cursoragent@cursor.com>
The host ignores driver_poll's return value and keeps the interval last set with host.set_poll_interval. Several error paths never set it, so a rejected refresh token left the 500 ms startup interval in place and the driver would post to Tesla's auth host twice a second. Every poll now sets a 60 s interval, and requests to Tesla are paced on their own: - the car's state (free, no live call) every 5 minutes; - vehicle_data (billed, live) every 4 minutes while charging and every 20 minutes otherwise, and not on the first check after the car wakes; - 5 minutes after an error, 15 after a failed refresh or a 429. Replays were marked stale, which would drop the car from Core between reads; they now carry soc_fresh=false only. config.base_url, config.auth_url and config.access_token are gone: the refresh token and client secret go only to Tesla's auth host. The model comes from the VIN, not display_name, which is the owner's name for the car. The Fleet API docs are public; the driver comment says so, and why they stay out of upstream_docs. Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md # devices.yaml
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 620a7c1c09
ℹ️ 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".
| local REGION_URL = { | ||
| na = "https://fleet-api.prd.na.vn.cloud.tesla.com", | ||
| eu = "https://fleet-api.prd.eu.vn.cloud.tesla.com", | ||
| cn = "https://fleet-api.prd.cn.vn.cloud.tesla.com.cn", |
There was a problem hiding this comment.
Use Tesla's actual China Fleet API hostname
For every configuration with region: cn, requests are sent to fleet-api.prd.cn.vn.cloud.tesla.com.cn, but Tesla's China Fleet API host is fleet-api.prd.cn.vn.cloud.tesla.cn. The current hostname also appears in DRIVER.http_hosts, so the advertised CN variant cannot retrieve any telemetry until both values use the documented host.
Useful? React with 👍 / 👎.
| firmware_versions: "" | ||
| notes: "Read-only vehicle SoC, charge limit and charging state via the official Tesla Fleet API. Optional next to tesla_vehicle (BLE proxy). Does not wake or command the car. Not yet run against a car." | ||
| min_driver_version: "0.1.0" | ||
| min_host_version: "2.0.0" |
There was a problem hiding this comment.
Register the public Fleet API documents
The source lists three public Tesla documents used to implement this driver, but the manifest omits upstream_docs. That prevents the repository's weekly watcher from detecting changes or disappearance of the API, authentication, and billing contracts; add the most durable public URLs even if their rendered HTML changes frequently.
AGENTS.md reference: AGENTS.md:L76-L82
Useful? React with 👍 / 👎.
|
|
||
| local state = tostring(row.state or ""):lower() | ||
| local online = state == "online" | ||
| local woke_just_now = online and was_online == false |
There was a problem hiding this comment.
Defer the first online live read
When the driver starts or reloads during one of the car's short self-wakes, was_online is nil, so this condition is false and the same poll immediately makes the billed vehicle_data call. This bypasses the stated protection against prolonging short wakes specifically when no prior state has been observed; treat the initial unknown-to-online observation like a wake transition and wait for the next state check.
Useful? React with 👍 / 👎.
| if err and tostring(err):match("HTTP 401") then | ||
| token_expires_at = 0 | ||
| if ensure_auth() then | ||
| row, err = vehicle_row() | ||
| end |
There was a problem hiding this comment.
Preserve backoff when a 401 refresh fails
If an otherwise active session receives a 401 and Tesla then rejects or fails the forced refresh, ensure_auth() returns false but the old 401 remains in err; execution falls through to the generic five-minute error delay instead of the fifteen-minute refresh-failure backoff. This makes a revoked or invalid rotated token retry authentication three times as often as intended; return with BACKOFF_MS when the forced refresh fails.
Useful? React with 👍 / 👎.
| if err and tostring(err):match("HTTP 401") then | ||
| return nil, err | ||
| end | ||
| end | ||
| local rows, lerr = list_vehicles() |
There was a problem hiding this comment.
Stop falling back to the list endpoint after rate limits
When the configured-VIN status request returns HTTP 429, this block ignores that error because only 401 is returned directly and immediately issues another request to /api/1/vehicles. Under an account-wide rate limit this needlessly doubles traffic and can extend throttling; propagate 429 so driver_poll can apply the long backoff rather than trying another Fleet endpoint.
Useful? React with 👍 / 👎.
Adds
tesla_cloud, a read-only driver that reads a Tesla's charge state from the official Fleet API. It is one of three vehicle drivers for srcfl/ftw#1442, next to #139 (TeslaMate) and #141 (VW Group).What it does
fleet-auth.prd.vn.cloud.tesla.comand stores the rotated refresh token withhost.persist_secret. The refresh token and client secret go only to that host.GET /api/1/vehicles/{vin}every 5 minutes. Tesla does not bill that call and it makes no live call to the car.vehicle_data?endpoints=charge_state(billed, a live call) only from a car that is already awake: every 4 minutes while it charges, every 20 minutes otherwise, and not on the first check after it wakes, so it does not stretch the car's own short wakes.vehicle(DerVehicle):soc,charge_limit_pct,charging_state,time_to_full_min,charge_amps,charger_actual_current,staleandsoc_fresh. Between reads it replays the last reading withsoc_fresh=false, and it stops 25 minutes after the last read.openid offline_access vehicle_device_dataand never sends a command.read_only = true,verification_status = "experimental". Leavestesla_vehicle(BLE proxy) untouched.The second commit widens a guard test
test_signing_in_is_declared_or_it_does_not_happenlets onlymyuplinkPOST. The second commit addstesla_cloud, because the driver must POST to refresh its token. The channel build still limits that POST to URLs that end in/oauth2/v3/token, and the driver no longer lets config change the auth URL. The guard exists so that each such driver is a reviewed choice, so please approve or reject this one on its own.Where it came from
A Cursor cloud agent wrote the first two commits against
mainat92adaf0. The Cursor GitHub App can only reachsrcfl/ftw, so its push here failed with 403, and the patches were applied unchanged from Fredrik's machine. The third commit fixes what review found.Fixed in review
driver_poll's return value and keeps the interval last set withhost.set_poll_interval. Several error paths never set it, so a rejected refresh token left the 500 ms startup interval in place: the driver would have posted to Tesla's auth host twice a second. Every poll now sets a 60 s interval, and requests to Tesla are paced on their own, with 5 minutes after an error and 15 after a failed refresh or a 429.vehicle_dataevery 60 s whenever the car was online. Tesla's docs say not to poll it regularly. At the new pace, three hours of charging a day come to about 1,500 billed calls a month, around $3 against Tesla's $10 monthly credit.soc_fresh=falseonly.config.base_url,config.auth_urlandconfig.access_tokenare gone. The first two could send the secrets elsewhere; the third expired after 5 minutes.display_name, the owner's name for the car. It now comes from the VIN's fourth character.upstream_docsbecause the watcher hashes raw bytes and the page HTML changes with every deploy.Evidence
make test-driver ID=tesla_cloud: 36 passed, 16 skippedmake check: 4639 passed, 883 skippednot_recorded.Still open
{ip, vin}, so owners must write the YAML by hand.How to test
After merge the driver is in the signed
drivers-betachannel. Until an FTW release bundles it, put the Lua file on the box as a local override at the path the YAML names. Check the SoC while charging, that the car still falls asleep overnight, and the Fleet API usage in Tesla's developer dashboard after a day.Merge notes
CHANGELOG.md,index.yaml,devices.yaml,support-status.jsonandSUPPORT_STATUS.md. Merge one, rebase the next and rerun the generators;make checkshows any drift.drivers/BUNDLED_SOURCE.json.🤖 Generated with Claude Code