Skip to content

feat(vag_vehicle): read VW Group charge state from the EU Data Act portal - #141

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

frahlg merged 4 commits into
mainfrom
feat/vag-vehicle

Conversation

@frahlg

@frahlg frahlg commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Adds vag_vehicle, a read-only driver that reads charge state for VW, Audi, Škoda, SEAT and Cupra cars from the VW Group EU Data Act portal. This is path B for srcfl/ftw#1442: a vendor driver, with no Home Assistant ingest in Core. The other vehicle drivers are #139 (TeslaMate) and #140 (Tesla Fleet API).

How it works

VW has closed We Connect's third-party APIs. The EU Data Act portal is the door left: the owner orders their own car data there, and the portal delivers it as files.

  1. The owner links the car on the portal and orders a continuous "All Data" delivery every 15 minutes.
  2. The portal then writes a zip file about every 15 minutes, holding one JSON document: a list of data points (key, dataFieldName, value, timestampUtc). While the car sleeps the file is an empty _no_content_found.zip.
  3. Every 5 minutes the driver lists the files, downloads the newest one with content, unzips it in Lua (the host has no unzip), and emits vehicle (DerVehicle): soc, charge_limit_pct, charging_state in Tesla's words, time_to_full_min, stale and soc_fresh.

The portal needs a VW ID login (OIDC), which the Lua host cannot do: it has no cookie jar. The owner pastes the Cookie header of a logged-in portal tab into YAML, masked by config_secrets. When that session ends the driver stops and the owner must paste a new one. evcc, which reads the same portal, logs in with email and password instead.

No wake, charge start or climate control. read_only = true, verification_status = "experimental". Porsche is not on this portal and is refused.

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 commits were applied unchanged from Fredrik's machine. The third commit fixes what review found.

Fixed in review

  • The first reading after a start could pass as fresh when it was hours old. When the car has slept, every newer file is _no_content_found, so the newest content file can be old, and the host has no wall clock to tell. The newest file at the first listing is now reported stale until a newer file arrives.
  • Memory. The unzip kept one table slot per output byte, and all drivers share FTW's process. The newest 32 KiB now stay per byte for back-references and older text is flushed into chunks. For 1 MB of JSON, allocation fell from 9 MB to 3 MB (C Lua 5.5 on a Mac; about 0.1 s). A dataset over 4 MiB is refused with a log line.
  • Streamed zips. A zip written by a streaming writer (Java's ZipOutputStream, for one) puts its sizes after the data and 0 in the header. The driver could not read those; it now can.
  • A file without SoC was downloaded again on every poll. Each file is now read once.
  • The file list is fetched every 5 minutes instead of 2, and every poll sets the interval, since FTW's host ignores driver_poll's return value.
  • As in evcc, the newest point among candidate fields wins, including the battery care threshold while that mode is on.
  • The data-point keys were checked against VW's data dictionary V5.0 (evcc keeps a parsed copy). All but the Enyaq SoC key, which comes from evcc, are in it. upstream_docs now watches that dictionary instead of the portal's web app.
  • evcc's client confirms the paths and that type and filename go as request headers.

Evidence

  • make test-driver ID=vag_vehicle: 36 passed, 16 skipped
  • make check: 4639 passed, 883 skipped
  • Mocks only. Neither the portal nor a car has run this driver, so support status stays not_recorded.

Still open

  • Limited use to Core today. Core uses a vehicle SoC for 5 minutes after a fresh reading. Portal data arrive about every 15 minutes and are already some minutes old, so Core will mostly show this SoC rather than steer by it.
  • The pasted cookie. Nobody knows yet how long a portal session lasts.
  • Speed on box hardware. FTW runs gopher-lua, several times slower than C Lua, and a poll has 10 seconds. A few hundred kB of JSON should be fine; the size of a real All Data file is unknown.
  • A host.unzip in Core would remove the Lua unzip. That is a Core change and not needed to test.

How to test

drivers:
  - name: id4
    lua: drivers/vag_vehicle.lua
    capabilities:
      http:
        allowed_hosts: ["eu-data-act.drivesomethinggreater.com"]
    config:
      vin: "<your VIN>"
      brand: volkswagen        # audi | skoda | seat | cupra
      cookie: "name=value; ..." # from DevTools → Network → a portal request

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. Report how long the cookie lasts, the size of a real file, and whether SoC and charging state match the car.

Merge notes

🤖 Generated with Claude Code

Read-only VW / Audi / Škoda / SEAT / Cupra SoC from the VW Group EU
Data Act portal. We Connect third-party APIs are blocked. The driver
emits DerVehicle, binds VIN identity, ages vendor datasets and stops
emitting when they are stale. No wake or charge_start.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Set make/serial from brand+VIN even when the portal cookie is missing
so the car still appears. Sort portal files by createdOn instead of
array order. No wake or charge_start.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
frahlg and others added 2 commits September 26, 2026 13:00
The newest file at the first listing after start was reported as a
fresh reading, though it can be hours old when the car has slept. The
host has no wall clock, so that file is now reported stale until a
newer one arrives.

Unzipping kept one table slot per output byte. The newest 32 KiB now
stay per byte for back-references and older text is flushed into
chunks, which cut allocation for 1 MB of JSON from 9 MB to 3 MB, and a
dataset over 4 MiB is refused. Entries from streaming writers, which
put their sizes in a data descriptor after the data, now decode too.

A file without SoC was downloaded again on every poll; each file is now
read once. The file list is fetched every 5 minutes instead of 2, and
every poll sets the interval, since the host ignores driver_poll's
return value. As in evcc, the newest point among candidate fields wins.
The data-point keys were checked against VW's data dictionary V5.0, and
upstream_docs now watches that dictionary instead of the portal's web
app.

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:12
@frahlg
frahlg merged commit 6e7a6ad into main Sep 26, 2026
5 checks passed
@frahlg
frahlg deleted the feat/vag-vehicle branch September 26, 2026 11:12
@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:15:43.003685Z 086259b 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: 086259b00e

ℹ️ 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".

Comment on lines +868 to +869
local known_age = name ~= baseline_file
remember(soc, limit, state, ttf, known_age)

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 Avoid treating files found after an outage as fresh

If list polling fails for an extended period while one newer dataset is produced, the next successful poll treats that dataset as fresh solely because its name differs from the startup baseline, regardless of its createdOn or data-point timestamps. This resets the local age and emits soc_fresh=true even when the file is hours old, allowing Core to use stale SoC as a new observation; track the last successful listing interval or otherwise downgrade files discovered after a long gap.

AGENTS.md reference: AGENTS.md:L75-L75

Useful? React with 👍 / 👎.

Comment on lines +799 to +800
elseif es:match("HTTP 404") then
host.log("debug", "vag: no dataset files yet")

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 Re-resolve the request ID after list 404s

When an owner cancels and recreates the continuous data request, the cached identifier can start returning 404, but this branch leaves request_id intact. Every later poll therefore reuses the obsolete identifier because ensure_request_id() returns its cache, so the driver never discovers the replacement request until it is restarted; invalidate the cached ID on this response so metadata can be queried again.

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