576df58776dc0d106b1535cf13b7d9acf1fa0a90
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
576df58776 |
Go the way the owner's phone already goes
Control had two transports and neither fitted the ordinary customer. OCPP waits for the charger to dial in, which needs a public endpoint it can reach, a certificate, and a firmware willing to talk to our CSMS. Modbus TCP dials the charger, which needs the server on the charger's own network. Between them they cover a charger we host and a charger we stand next to; the common case is a charger behind someone else's router, and that had nothing. It was never unreachable, though. The charger holds a connection open to Anker's own broker — it is how the mobile app drives it from anywhere, and it is the mqttStatus register the Modbus snapshot has been reporting all along. So a third control mode joins that broker as the account: get_user_mqtt_info issues a client certificate, mTLS to aiot-mqtt-eu.anker.com:8883, and commands go out on the same topics the app publishes on. Nothing on the customer's side has to be forwarded, addressed or certificated. What travels is not an API call. The payload is a JSON envelope around a base64 binary frame the device itself speaks — marker, little-endian length, message type, name/length/type/value fields, XOR checksum — so mqttframe.go is a codec rather than a client, written from the message maps in anker-solix-api and anchored on the one frame that project documents byte for byte. A frame whose fields do not tile exactly up to the checksum is refused rather than half-read: these arrive over a link we do not control, and a truncated frame must not read as a charger reporting zeros. Two of the charger's habits shape the rest. It publishes nothing unless asked, so a status read arms a telemetry trigger and waits for the next frame, and a poll inside that window answers from what has since arrived. And a broker connection costs a fetched certificate and a TLS handshake while the plugin manager builds a throwaway instance per request — so the connection lives on the account's shared session beside the auth token, for exactly the reason the token lives there, and closes itself after five idle minutes. The transport also sees two signals no other one does: the boost flag, and the plug and start countdowns. The package doc has said since the first commit that they are never set and the derived mode must do without them. Here they are set, so a charger that has been told to start and is counting down a delay says so rather than sitting in "preparing", and "skip the delay" is offered only while there is a delay to skip. The clients generalise instead of growing a second layout. Both snapshots name the same quantities the same way, so what was Modbus-only in the readouts is now whichever transport read the charger — ModbusStatus becomes ChargerStatus on the phone, mb becomes dev on the web. What each transport can be *told* still differs, and the buttons branch on that: reset and clear-limit stay with OCPP, the timeout and phase registers with Modbus, skip-delay with the cloud. A command a transport has no equivalent for is refused by name, saying which one has it. The cost is worth saying plainly. This leans on Anker's cloud being up and on an unofficial protocol the app may change under us, where Modbus leans on nothing but the LAN. And it is checked against the reference implementation's own worked example rather than against hardware — there is no charger on this end to point it at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f7472bada3 |
Reach the charger where it is, instead of waiting for it to call
OCPP asks the charger to dial us: a public endpoint, a TLS certificate, and a route in through the customer's router. Our own handler then demanded two more things the V1 does not offer — TLS on a charger that connects over ws://, and Basic auth credentials the Anker app has no field for — so every connection was turned away before the upgrade. Anker publishes a Modbus TCP register map for this charger, and it inverts the problem: we dial the charger, on its own network, with no inbound reachability to arrange. That works for a charger behind a router that OCPP cannot reach at all. internal/modbus is the protocol, hand-rolled against the spec like the MQTT and WebSocket clients beside it. The plugin's modbus.go is the V1's map: the same 0-8 status enum the cloud already reports, per-phase measurements, and the writable registers behind start, stop, current limit, boost and phase mode. A new "modbus" control mode routes the existing control endpoints down it, so the REST surface, the rate limit, the confirmation step and the audit trail are the ones already there. The commands the register map has no equivalent for say so by name rather than failing as unknown, and a current below the charger's 6 A floor is refused because it pauses the charge rather than slowing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
190ae923a6 |
The Anker token outlives the request that fetched it
The manager builds a throwaway plugin instance for every per-user call —
HealthCheckWith, InvokeWith, InvokeBatchWith each construct, Init, probe and
Shutdown. The auth token lived on that instance, so it died with the HTTP request
that fetched it: opening the Anker panel signed in once for the health probe and
again for the charger list, and a page that also asked for OCPP info signed in a
third time. Every refresh, a fresh login.
Anker throttles passport/login per IP per minute and answers code 26161 ("Failed
to request.") once tripped, so this is the shape of the failure the panel has been
reporting; the cloud has also historically kept one token per account, so each of
those logins could evict the one the mobile app was holding.
Tokens and the login backoff now live in a package-level session keyed by the
account signing in, so every instance configured for that account shares one
login. Re-configuring the same credentials keeps the token; a different account,
or the same account on the other regional server, gets its own session. Sessions
unused for a fortnight are pruned, so an edited password does not leave its entry
behind for the life of the process.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
c1aee0fac1 |
One refused sign-in, not five: the chargers poll no longer locks the account
A chargers poll asks four cloud views. Each called apiRequest, each found no token, and each ran its own login — so a login Anker refuses was offered four times in one poll, and the next poll spent the fifth. Five is what disables the account for ten minutes, which is how "code 26161: Failed to request." turned into "your account has been disabled" on the very next attempt. The plugin now remembers a refused login instead of repeating it: the failure is cached and replayed to every caller until a backoff window passes — a minute at first, doubling to fifteen, or the full ten minutes when Anker says it has already locked the account (code 10019). New credentials clear it, so a fixed password is tried at once. chargerInventory signs in once up front. A login the cloud refuses is not four views failing, so it is reported as itself rather than as three warnings with the lockout notice buried in the last one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a809980d8b |
Anker health: count the chargers the panel lists, not the ones one endpoint admits to
The probe still asked get_user_bind_and_not_in_station_evchargers and read its userBindEvChargersCount, so it reported "0 EV charger(s) bound to account" for an account whose two chargers the panel was listing directly underneath — the same blind spot the capability was just moved off, left behind in the health check. It now takes the same inventory the chargers capability returns and counts that. Authenticated with nothing on the account is degraded rather than ok, following Greencell's rule: the half we address answers, and the empty half is the account or the country that picks the regional server, so the message says so instead of reporting a healthy connection to nothing. A count reached with some view missing says how many views stayed silent, because the number is then a floor rather than a total. The web panel colours degraded amber, as it already did for Greencell. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e138fad3f4 |
Anker: every charger on the account, not just the ones outside a station
get_user_bind_and_not_in_station_evchargers is the only list the connector ever asked for, and its name says exactly what it withholds. A charger that belongs to a system is not in it. Its userBindEvChargersCount, though, counts every charger bound to the account — so an owner with two chargers in a system got "authenticated; 2 EV charger(s) bound to account" from the health probe and an empty list from the capability that is supposed to show them. A working login that finds nothing. So the capability now asks every view the cloud has and merges them by serial. The standalone list still answers for chargers standing on their own; get_site_list walks the systems and reads each one through get_scen_info, falling back to get_system_running_info where that is silent — the power-service / HES split charger-state already knows; and get_relate_and_bind_devices contributes model, firmware and the Wi-Fi flag, and discovers anything in the A519 family that the first two missed. Whichever way a charger was registered, one of the three has it. The merge is first-writer-wins per field rather than last view overwriting: the standalone record knows the name, the site record knows the live state, and neither should blank what the other established. A view that fails is a warning on the document instead of an error on the call, because one dead endpoint should not cost the chargers the other two found. Only losing all three is a failure. When nothing comes back at all the response says so in its own words and names the remaining suspect — country picks the regional server, and the wrong one authenticates happily and shows an empty account. The other half of "not showing any chargers" was that neither client ever showed a list. The serial was a text box, and the number is printed on a charger hanging on a wall. Both apps now list what the account holds — name, serial, model, site, state, an offline badge — and hand the serial to the OCPP control card instead of asking anyone to go and read it. Where control is off the list still stands on its own, as the answer to the first question an owner has after entering credentials. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e648634ce1 |
The plugin list, grouped by what a plugin actually is
Category has been in the plugin contract since it was written — apis-external, drives-external, drives-local — and every builtin declared the same one, so it grouped nothing. Two of the three talk to a wallbox and one talks to a car manufacturer, and those are different questions an operator arrives with: the Toyota card is where a driver's account gets linked, the Anker and Greencell cards are where a charger's broker and credentials live. So vehicles and chargers join the constants and the three builtins say which they are. The panel groups on that field rather than on a list of names, which is what keeps an external plugin from needing panel code. Tab order mirrors the constants; a category with nothing in it gets no tab, and a single group hides the bar entirely, so an install with one connector looks exactly as it did. A category the panel does not recognise — or an empty one — falls to the external-APIs tab rather than vanishing, because a plugin nobody can see is a plugin nobody can disable. The selected tab falls back to the first group when its own goes away, which is what removing the last external plugin does. Registration still asks only for name, base URL and provider, so a plugin registered at runtime lands under Other APIs until its manifest names a category. That path already works and is the honest default: the panel is guessing about a service it has never spoken to, and the service can say. The header lockup is the other half. It was a copy of the Web App's mark rather than the same mark, and copies drift — a 32px icon against 28, a 24px wordmark against 21.6, "Driver" at text-strong instead of white, "Vault" a step lighter than brand-400. The Web App's Logo.vue moves in verbatim, props included. The one thing it cannot inherit is which variant to render: the Web App's rail is always dark, while this panel flips with its own theme toggle, so on-dark is bound to the theme and the hand-rolled bar fills that existed to survive that flip are gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5e6b8b4b1c |
Anker Solix: the charger's mode, and the modes it can be moved into
The connector was written against anker-solix-api v3.7.0 and upstream is at 3.8.1 now. The reassuring half of the check first: nothing we depend on moved. The passport/login ECDH exchange, the headers, and every endpoint path this plugin calls are identical across v3.7.0...v3.8.1 — the only apitypes movement touching an EV charger was get_device_rfid_cards being reordered within its own dict. The 400 new lines in charger.py are the A2345 USB charger, which shares a filename with our device and nothing else. What did land for the V1 is two entries in the release notes, and both are MQTT: 3.8.0 gave standalone chargers the usage-mode entity they were missing, 3.8.1 added a switch that reads those modes as a plain on/off so EVCC and its like have a binary to hold. We control chargers over OCPP, not MQTT, so the command path is not ours to port. The reading of state underneath it is, and that half does come over the cloud. So charger-state. The status code arrives under two different names depending on which system family a site belongs to — operating_state inside a scene's charging_pile_list, evChargerStatus inside HES system running info — and upstream's poller quietly renames both to ev_charger_status on ingest, which is the tell that they are the same number. We ask both and merge, because a site answering only one of them is the normal case rather than a fault; the call fails only when neither view is there. chargerMode and chargerModeOptions then follow ev_charger_mode_state and ev_charger_mode_options as written, including the rule that a stopped charger is startable only from standby, and the binary is the same one 3.8.1 chose: everything that is not stop_charge counts as on. The gap worth naming is that the boost flag and the plug and start countdowns reach upstream over MQTT and never over the cloud, so three of the six modes cannot occur here. That is not a bug to be found later — chargerMode takes them as parameters and the callers pass their zero values, so the day an MQTT source exists the derivation is already correct and only its inputs change. The package doc says so in the scope list beside the other limits. Five endpoints upstream has had all along and we never exposed come with it, all EV-charger-scoped: the site scene, energy_analysis under device_type ev_charger, a charger's RFID cards, Anker's own OCPP endpoint list, and one vehicle's details. charger-status takes the featuretype it was hardcoding at 1, since upstream's exporter asks for both 1 and 2 and there was never a reason for us to see only half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a1519f6e89 |
Add OCPP control for the Anker Solix EV charger (Own/Proxy CSMS)
The Anker Solix connector was read-only (cloud monitoring only). Add an
OCPP 1.6J control path with a per-user, cascading control mode:
- off monitoring only (default, unchanged behavior)
- own DriverVault is the charger's Central System (full control)
- proxy DriverVault relays to Anker's cloud and injects commands
New internal/ocpp subsystem (stdlib-only, hand-rolled RFC 6455): a CSMS
with session management, inbound dispatch, and typed control commands
(RemoteStart/Stop, SetChargingProfile current limit, ChangeAvailability,
Reset, UnlockConnector, TriggerMessage, Get/ChangeConfiguration). Own- and
proxy-mode paths are verified end-to-end against a simulated charge point.
The charger connects to /ocpp/{serial}, authenticated with OCPP Basic auth
(serial + a per-charger control token) resolved to the owning user via an
in-memory token index. Control REST endpoints mirror the monitoring ones and
reuse the same cascade gate plus a live-session check. controlMode is a new
cascade field (global -> org -> user) advertised as a select on the plugin.
Frontend: control-mode select + provisioning card in Settings, and a real
Start/Stop/limit/reset control panel in Charging, gated on the active mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
0793b5ec8e |
Add a built-in Anker Solix V1 Smart EV Charger plugin
A Go re-implementation of the auth and read-only data flow from thomluther/anker-solix-api, scoped to the V1 Smart EV Charger (A5191) and adapted to DriverVault's plugin contract. Login is a custom ECDH (P-256) + AES-256-CBC password exchange against passport/login, yielding a ~7-day auth token plus gtoken = md5(user_id) for subsequent requests; a fresh login covers expiry and 401/403. The country code routes to the EU or global Anker server. Exposes read-only capabilities (chargers, charger-status, charge-stats, charge-orders, ocpp-info, devices, sites, vehicles) with a health check that reports the bound-charger count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |