Skip to content

feat(tesla_cloud): read Tesla charge state from the Fleet API - #140

Merged
frahlg merged 4 commits into
mainfrom
feat/tesla-cloud-vehicle
Sep 26, 2026
Merged

frahlg merged 4 commits into
mainfrom
feat/tesla-cloud-vehicle

Conversation

@frahlg

@frahlg frahlg commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

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

  • Refreshes an OAuth token at fleet-auth.prd.vn.cloud.tesla.com and stores the rotated refresh token with host.persist_secret. The refresh token and client secret go only to that host.
  • Checks the car's state with GET /api/1/vehicles/{vin} every 5 minutes. Tesla does not bill that call and it makes no live call to the car.
  • Reads 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.
  • Emits vehicle (DerVehicle): soc, charge_limit_pct, charging_state, time_to_full_min, charge_amps, charger_actual_current, stale and soc_fresh. Between reads it replays the last reading with soc_fresh=false, and it stops 25 minutes after the last read.
  • Needs only the scopes openid offline_access vehicle_device_data and never sends a command. read_only = true, verification_status = "experimental". Leaves tesla_vehicle (BLE proxy) untouched.

The second commit widens a guard test

test_signing_in_is_declared_or_it_does_not_happen lets only myuplink POST. The second commit adds tesla_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 main at 92adaf0. The Cursor GitHub App can only reach srcfl/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

  • Auth hammering. FTW's 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: 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.
  • Cost and sleep. It used to call vehicle_data every 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.
  • Replays were marked stale, which would have dropped the car from Core between two reads. They now carry soc_fresh=false only.
  • config.base_url, config.auth_url and config.access_token are gone. The first two could send the secrets elsewhere; the third expired after 5 minutes.
  • The model came from display_name, the owner's name for the car. It now comes from the VIN's fourth character.
  • The driver comment said the Fleet API docs need a login. They are public; they stay out of upstream_docs because the watcher hashes raw bytes and the page HTML changes with every deploy.

Evidence

  • make test-driver ID=tesla_cloud: 36 passed, 16 skipped
  • make check: 4639 passed, 883 skipped
  • Mocks only. No Fleet API account or car has run this driver, so support status stays not_recorded.

Still open

  • Getting a refresh token needs a Tesla developer app and a manual auth-code exchange. There is no connect flow in Settings.
  • Settings still scaffolds vehicles as {ip, vin}, so owners must write the YAML by hand.
  • The pace is a first guess. Real use will show whether 4 minutes while charging is enough for planning.

How to test

drivers:
  - name: tesla-cloud
    lua: drivers/tesla_cloud.lua
    capabilities:
      http:
        allowed_hosts:
          - fleet-auth.prd.vn.cloud.tesla.com
          - fleet-api.prd.eu.vn.cloud.tesla.com
    config:
      client_id: "..."
      client_secret: "..."
      refresh_token: "..."
      vin: "<your VIN>"      # optional; first car on the account if omitted
      region: eu             # na | eu | cn

After merge the driver is in the signed drivers-beta channel. 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

🤖 Generated with Claude Code

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>
frahlg and others added 2 commits September 26, 2026 13:00
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>
@frahlg
frahlg marked this pull request as ready for review September 26, 2026 11:10
@frahlg
frahlg merged commit 223fc24 into main Sep 26, 2026
5 checks passed
@frahlg
frahlg deleted the feat/tesla-cloud-vehicle branch September 26, 2026 11:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-26T11:14:13.324922Z 620a7c1 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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

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 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 👍 / 👎.

Comment on lines +439 to +443
if err and tostring(err):match("HTTP 401") then
token_expires_at = 0
if ensure_auth() then
row, err = vehicle_row()
end

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 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 👍 / 👎.

Comment on lines +302 to +306
if err and tostring(err):match("HTTP 401") then
return nil, err
end
end
local rows, lerr = list_vehicles()

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 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 👍 / 👎.

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.

2 participants