feat(ev): add Zaptec Cloud charger support - #977
Conversation
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>
There was a problem hiding this comment.
💡 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".
| // Mirrors go/internal/config/config.go EVCharger.Validate. | ||
| var EV_PROVIDERS = [ | ||
| { value: 'easee', label: 'Easee', transport: 'http' }, | ||
| { value: 'zaptec', label: 'Zaptec', transport: 'http' }, |
There was a problem hiding this comment.
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 👍 / 👎.
| function driver_default_mode() | ||
| -- Cloud charger keeps its last setpoint when FTW goes away. | ||
| end |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 4 potential issues.
❌ 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 |
There was a problem hiding this comment.
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)
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 |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 0c375da. Configure here.
| 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; |
There was a problem hiding this comment.
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").
Reviewed by Cursor Bugbot for commit 0c375da. Configure here.
| paused_state = false | ||
| end | ||
| end | ||
| return true |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 0c375da. Configure here.




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.
evcloudproviderzapteclists chargers after an OAuth2 password grant againsthttps://api.zaptec.com(POST /oauth/token,GET /api/chargers). 4xx login errors are status-only so credentials never leak.zaptecwith the same HTTP-cloud rules as Easee (empty creds allowed as a wizard placeholder; Modbus rejected).go/internal/drivers/testdata/zaptec_cloud.lua(not underdrivers/, which is a generated pin fromsrcfl/device-drivers): pollGET /api/chargers/{id}/state, set current viaPOST .../update(maxChargeCurrent), pausesendCommand/506, resumesendCommand/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.zaptec_cloud.lua→zaptec.Operators can drop
zaptec_cloud.luain 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
drivers//BUNDLED_SOURCE.json, OCPP, local Zaptec APIs, live-hardware sign-off.HTTP Nwithout echoing the request body. The Lua driver redacts HTTP errors the same way.driver_default_mode; for this cloud charger that is a no-op (Zaptec keeps the last current).Verification
go test ./internal/evcloud ./internal/config ./internal/drivers -count=1fromgo/— Zaptec list/login timeout tests, config validation, Lua init/poll/ev_set_current11040 W → 16 A, pause 506 / resume 507, password not in init errors.node --test web/setup.test.mjs— Zaptec is inEV_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