Skip to content

feat(ev): add Zaptec Cloud charger support - #977

Merged
frahlg merged 1 commit into
masterfrom
cursor/zaptec-cloud-7a7a
Aug 24, 2026
Merged

feat(ev): add Zaptec Cloud charger support#977
frahlg merged 1 commit into
masterfrom
cursor/zaptec-cloud-7a7a

Conversation

@frahlg

@frahlg frahlg commented Aug 24, 2026

Copy link
Copy Markdown
Member

Accepted text proposal

Issue or Discussion: operator request to add Zaptec EV charger support in FTW, using evcc as a protocol reference (not as a source to copy).

Maintainer comment that accepted this scope:

Fredrik asked for Zaptec in FTW. The Zaptec module in evcc is not MIT, so this reimplements the public Zaptec Cloud API along the existing Easee / MyUplink patterns.

What changed

Zaptec Go, Go 2 and Pro can be added as a cloud HTTP EV charger next to Easee.

  • New evcloud provider zaptec lists chargers after an OAuth2 password grant against https://api.zaptec.com (POST /oauth/token, GET /api/chargers). 4xx login errors are status-only so credentials never leak.
  • Config validation accepts zaptec with the same HTTP-cloud rules as Easee (empty creds allowed as a wizard placeholder; Modbus rejected).
  • Lua driver in go/internal/drivers/testdata/zaptec_cloud.lua (not under drivers/, which is a generated pin from srcfl/device-drivers): poll GET /api/chargers/{id}/state, set current via POST .../update (maxChargeCurrent), pause sendCommand/506, resume sendCommand/507 (Zaptec 528 “already running” treated as success). Positive W is charging. Below 6 A the driver pauses. Cloud keeps the last setpoint on default mode.
  • Setup wizard and Settings → EV offer Zaptec; Connect still derives the provider from zaptec_cloud.luazaptec.
  • Changeset: minor, experimental until a live charger is exercised.

Operators can drop zaptec_cloud.lua in the user-drivers directory until the device-drivers pin includes it. Settings → Devices Connect then lists chargers through the new provider.

Why

FTW already had Easee as a cloud HTTP charger. Zaptec uses the same wizard and dispatch path (ev_set_current / pause / resume) but a different OAuth form grant and observation/command API. evcc was used only as a protocol map of public Zaptec Cloud endpoints; no evcc charger source was copied.

Boundaries and safety

  • Out of scope: bundling the Lua file in drivers/ / BUNDLED_SOURCE.json, OCPP, local Zaptec APIs, live-hardware sign-off.
  • Stale site-meter data still stops dispatch (unchanged core invariant). Planner output is not sent to the charger.
  • Login/list failures return HTTP N without echoing the request body. The Lua driver redacts HTTP errors the same way.
  • A failed/stale driver still gets driver_default_mode; for this cloud charger that is a no-op (Zaptec keeps the last current).
  • Minimum offer is 6 A (IEC 61851). Experimental until a live charger has been run.

Verification

  • go test ./internal/evcloud ./internal/config ./internal/drivers -count=1 from go/ — Zaptec list/login timeout tests, config validation, Lua init/poll/ev_set_current 11040 W → 16 A, pause 506 / resume 507, password not in init errors.
  • node --test web/setup.test.mjs — Zaptec is in EV_PROVIDERS.
  • make verify — Go + Python suites, compose migration, vet, build.

Not verified: wizard/settings against a real Zaptec account (no hardware or live credentials in this environment). A human still needs to look at the Settings → EV provider list and the setup wizard EV step in a browser.

Checklist

  • The diff implements one accepted scope and does not add follow-on work.
  • I checked open pull requests that touch the same files. (OCPP feat(ocpp): OCPP 1.6J + 2.0.1 support — chargers connect with no driver #732 is unrelated EV work and was left alone.)
  • Tests cover the changed behaviour and its failure path.
  • A human reviewed every changed web/UI view in a browser, or no UI changed.
  • A Changeset is included, or the change is exempt.
  • Every commit has a DCO sign-off.
Open in Web Open in Cursor 

Register Zaptec as an HTTP cloud EV provider next to Easee. The
wizard and Settings list chargers via OAuth password grant against
api.zaptec.com. The Lua driver lives in testdata until the
device-drivers pin includes it; operators can drop the same file in
the user-drivers directory. Experimental until a live charger is
exercised.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
@frahlg
frahlg marked this pull request as ready for review August 24, 2026 14:20

@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: 0c375da10f

ℹ️ 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 thread web/setup.js
// Mirrors go/internal/config/config.go EVCharger.Validate.
var EV_PROVIDERS = [
{ value: 'easee', label: 'Easee', transport: 'http' },
{ value: 'zaptec', label: 'Zaptec', transport: 'http' },

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 Ship and configure the Zaptec runtime driver

Selecting Zaptec in the setup wizard only writes cfg.ev_charger; it never adds an entry to cfg.drivers, which is the collection started by main.go. A repo-wide file search also shows that the advertised drivers/zaptec_cloud.lua does not exist—the only implementation is test data—so a normal installation can list a charger but cannot poll or control it after setup. Add the canonical driver through the generated driver repository and ensure setup creates the corresponding driver/loadpoint before exposing this option.

AGENTS.md reference: AGENTS.md:L12-L18

Useful? React with 👍 / 👎.

Comment on lines +414 to +416
function driver_default_mode()
-- Cloud charger keeps its last setpoint when FTW goes away.
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.

P1 Badge Restore an autonomous mode before confirming the default

When an operator copies this implementation into the user-drivers directory, startup, stale telemetry, command failure, and shutdown all invoke this function and treat a nil return as confirmation that autonomous mode was restored. This no-op instead leaves Zaptec at FTW's last current limit or pause state, so a charger paused for zero surplus can remain disabled indefinitely after FTW fails, while Core incorrectly unblocks future control as if recovery succeeded. The default must actively release FTW's setpoint or otherwise restore a documented autonomous state.

AGENTS.md reference: AGENTS.md:L35-L36

Useful? React with 👍 / 👎.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0c375da. Configure here.

local ok = send_command(CMD_STOP)
if ok then paused_state = true end
return ok
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.

Phase mode ignored, low power pauses

High Severity

ev_set_current always converts watts with the configured phases (default 3) and never reads phase_mode from the command table that loadpoint actually sends. Offers below about 4140 W become under 6 A on 3φ, get floored to 0, and pause—so surplus-only and other low-power charging never start.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0c375da. Configure here.

if err == nil then return "request failed" end
local s = tostring(err)
return s:match("^(HTTP %d+)") or "request failed"
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.

Useful errors become request failed

Medium Severity

redact_http_err keeps only an HTTP N prefix and otherwise returns request failed. Non-HTTP failures from resolve_charger and login decoding (no chargers on account, charger not found, no access_token) are wiped, so init errors no longer tell operators what went wrong.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0c375da. Configure here.

Comment thread web/settings/tabs/ev.js
config.ev_charger.provider = "easee";
config.ev_charger.email = drv.config.email || "";
if ((drv.name === "easee" || drv.name === "zaptec") && drv.config) {
config.ev_charger.provider = drv.name;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

EV tab misses zaptec-cloud name

Medium Severity

EV-tab hydration requires drv.name === "zaptec", but the new driver declares id = "zaptec-cloud", which is the catalog default name. A catalog-added Zaptec driver is skipped, so Settings → EV does not pre-fill provider or credentials. The status path correctly uses indexOf("zaptec").

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0c375da. Configure here.

paused_state = false
end
end
return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Failed resume still reports success

Low Severity

After a successful maxChargeCurrent update, if the follow-up resume command fails, paused_state stays true but ev_set_current still returns true. Core treats the setpoint as applied while the charger remains paused.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0c375da. Configure here.

@cursor cursor 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.

Left a non-blocking comment; not approved because Cursor Bugbot reported 4 unresolved issues, including a high-severity charging-control finding. Reviewers were assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor
cursor Bot requested review from Leitet and erikarenhill August 24, 2026 14:26
@frahlg
frahlg merged commit 53ca4ae into master Aug 24, 2026
18 checks passed
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