pagraham.co.uk

Home energy · Node-RED

My Tesla Powerwall flow, and getting OAuth to work from a home server

How a small Linux box running Node-RED reads my Powerwall and solar through the Tesla Fleet API, sends alerts to Telegram, and keeps its Tesla login alive without me touching it.

Runs on
Debian Linux box, Node-RED (no Docker)
Talks to
Tesla Fleet API, energy endpoints
Alerts
Telegram bot
Auth domain
energy.pagraham.co.uk
Data flow Powerwall reports to Tesla's cloud. Node-RED on a home Linux box polls the Fleet API and sends Telegram alerts, and can send commands back. Powerwall + solar at home Tesla cloud Fleet API (EU region) Node-RED Linux box at home Telegram alerts to phone poll data energy.pagraham.co.uk public key · OAuth redirect Tesla checks key auth code
Solid lines carry data every few minutes. Dashed lines are only used when the flow is set up or re-authorised.

What the flow does

  • Live status. Every few minutes it reads the energy site's live status: solar generation, home load, battery charge level, grid import/export and whether the grid is up.
  • Alerts that matter. Telegram messages when the battery reaches full, drops below a threshold, or the grid goes down and the house switches to battery. Alerts only fire on a change of state, so it doesn't nag.
  • Daily summary. A short end-of-day message with what the panels made, what the house used and how much went to or from the grid.
  • Commands. With the energy command scope, the flow can change the backup reserve or operating mode, for example holding charge before a planned outage.

Why the auth is the hard part

Tesla's Fleet API is built for apps with a website and a server, not for one person with a Raspberry Pi-class box. To use it you need a registered developer application with a domain you control. Tesla uses that domain twice:

  1. It downloads a public key from a fixed path on the domain when you register as a partner.
  2. After you sign in, it sends you back to a redirect URL on that domain with a one-time code.

Node-RED sits behind my home router with no public address, so neither of those can point straight at it. The answer is to let a small public website on my own domain handle the parts Tesla needs to reach, and let Node-RED do everything else.

I originally hosted the key and redirect on another of my domains. I've moved them to energy.pagraham.co.uk so the Tesla setup lives with the rest of my personal projects.

The one-time setup

  1. Create a key pair. Tesla wants an EC key on the prime256v1 curve. The private key stays on the Node-RED box and is never published.
    openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
    openssl ec -in private-key.pem -pubout -out public-key.pem
  2. Publish the public key on the domain at the exact path Tesla expects:
    https://energy.pagraham.co.uk/.well-known/appspecific/com.tesla.3p.public-key.pem
  3. Register the app on developer.tesla.com with pagraham.co.uk as an allowed origin, the redirect URL below, and the scopes openid offline_access energy_device_data energy_cmds. offline_access is the important one: without it Tesla doesn't issue a refresh token.
  4. Register as a partner for the EU region. Node-RED gets a partner token using the client credentials, then calls POST /api/1/partner_accounts with the domain. Tesla fetches the public key and, if it matches, the domain is registered. GET /api/1/partner_accounts/public_key?domain=… confirms it worked.
  5. Sign in once. Open Tesla's authorise page in a browser, log in and approve. Tesla redirects to the callback page on my domain with a code.
  6. Exchange the code. The code goes to Node-RED, which swaps it for an access token and a refresh token at Tesla's token endpoint.

The sign-in request

https://auth.tesla.com/oauth2/v3/authorize
  ?response_type=code
  &client_id=<CLIENT_ID>
  &redirect_uri=https://energy.pagraham.co.uk/callback
  &scope=openid offline_access energy_device_data energy_cmds
  &state=<random value, checked on return>

Swapping the code for tokens

POST https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=<CLIENT_ID>
&client_secret=<CLIENT_SECRET>
&code=<code from the callback>
&audience=https://fleet-api.prd.eu.vn.cloud.tesla.com
&redirect_uri=https://energy.pagraham.co.uk/callback

Getting the code from the website to Node-RED

There are two ways to bridge the public callback page and the private Node-RED box:

  • Copy and paste. The callback page just shows the code. I paste it into an inject node or a small dashboard form in Node-RED, which does the exchange. Simple, and nothing at home is exposed. It's only needed once, or again if the refresh token is ever lost.
  • Tunnel. A Cloudflare Tunnel exposes a single Node-RED http in endpoint at the callback URL, so the exchange happens automatically. Neater, but it opens a path into the home network, so that endpoint must check state and do nothing else.

Keeping it signed in

Access tokens are short-lived, so the flow refreshes them on a timer and whenever an API call returns 401. Tesla's refresh tokens have two rules that catch people out:

  • Single use. Every refresh returns a new refresh token, and the old one stops working. The new one must be saved straight away, before anything else happens.
  • Three-month life. A refresh token expires after about three months if it isn't used, so the flow refreshing regularly keeps the chain alive indefinitely. Tesla allows the previous token for up to 24 hours as a safety net if a save fails.
POST https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token

grant_type=refresh_token
&client_id=<CLIENT_ID>
&refresh_token=<latest refresh token>

In Node-RED the tokens live in persistent context (the localfilesystem context store in settings.js), so they survive restarts and power cuts. A 401 login_required that a refresh can't fix sends a Telegram message asking me to sign in again.

The endpoints the flow uses

CallUsed for
GET /api/1/productsFind the energy site ID (once)
GET /api/1/energy_sites/{id}/live_statusSolar, load, battery %, grid status, every few minutes
GET /api/1/energy_sites/{id}/site_infoCurrent reserve and operating mode
GET /api/1/energy_sites/{id}/calendar_historyDaily energy totals for the summary
POST /api/1/energy_sites/{id}/backupSet backup reserve %
POST /api/1/energy_sites/{id}/operationSwitch between self-powered and time-based control

All calls go to the EU base URL, https://fleet-api.prd.eu.vn.cloud.tesla.com, with Authorization: Bearer <access token>.

Lessons learned

  • Region matters. UK accounts live in the EU region. Using the North America URL gives confusing errors.
  • Save the new refresh token first. Most "it worked for a week then stopped" problems are a refresh token that was used but whose replacement was never stored.
  • Keep secrets out of flows. Client secret and private key live in environment variables or credentials, not in function nodes, so exporting a flow to share never leaks them.
  • Poll politely. Fleet API usage is metered. Every few minutes is plenty for a house.
  • Keep the key published. If the public key disappears from the domain, Tesla can reject the app later.