423bc2ab16db2d3ba7340edff50b7c5e60daf35c
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
ee4ac441be |
Plugins: drop the plugins.json migration, and the volume it needed
The project has no public installs, so there is nothing to migrate from. MigrateLegacyFile, the file-backed Store it read through, PLUGINS_FILE and the legacy path threaded through the Server all go. What is left is one store, PocketBase, and a plugins package that touches no filesystem at all. That was the last thing keeping api_data alive, so the volume goes too. All four compose files now declare exactly one volume, pb_data, and the standalone API Server compose declares none - it talks to an external PocketBase and has nothing of its own to keep. Backing up the stack is backing up one path again. Both images get simpler for it. The API Server image loses VOLUME /data and the su-exec entrypoint that existed only to fix a mounted volume's ownership, so it goes back to a plain USER app; its working directory is now /app and holds nothing. The AIO image loses its second volume and chowns only /pb/pb_data. One consequence worth stating plainly, because it is a small regression rather than a no-op. The panel's Settings -> PocketBase and Settings -> Web App screens write .env in the working directory, which is now ephemeral. In the multi-container stack that changes nothing: compose sets all five of those keys as container environment, and loadDotEnv only applies a key that is not already set, so the file could never win a restart there anyway. In the AIO image it did win for POCKETBASE_ADMIN_EMAIL/_PASSWORD, which are not in that container's environment - so a service account fixed from the panel now lasts only until the container is recreated. Both READMEs say so. Moving those two screens into the app_settings singleton would close it properly; the PocketBase URL and credentials cannot follow, since they are how the database is reached in the first place. go build, go vet and go test ./... pass; the compose files parse and each resolves to a single pb_data volume. Not verified: no Docker CLI here, so neither image was built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9bd5c523c4 |
Plugins: the global layer moves into the database, beside the other two
The integration cascade stored its top layer differently from the two below it: org (L2) and user (L3) plugin config lived in PocketBase, in a pluginSettings field, while the global (L1) layer sat in a plugins.json next to the binary. That split was accretion rather than design - the file was the whole store in the v1 MVP, and the per-tenant layers were later built on PocketBase and layered on top of it instead of replacing it. It also cost something real. plugins.json was a second state store with different durability from pb_data: its own volume, its own ownership, its own backup. Losing pb_data is unmissable; losing api_data was silent, which is how "every plugin comes back disabled after a redeploy" happened. L1 now lives in the app_settings collection - one record keyed "global", holding its settings in a pluginSettings field, the same mechanism and the same field name the layers below use. The documents still differ in shape, because only L1 carries enable state and the registration of external plugins, but the storage is no longer a special case. The Manager grows a Store seam (PocketBase in production, file for the import, memory for tests) and, more importantly, a loaded gate. Settings in a database mean the store can be unreachable at boot - a cold stack, or a service account still to be set from the panel. That must not read as "no plugins configured", or the first save would write emptiness over real settings. So until a read succeeds the Manager stays unloaded, every mutation is refused, /api/admin/plugins* answers 503, and a background retry backs off to two minutes. The same gate covers a document that will not parse: it is never replaced by one built from an empty map, which is a stronger guarantee than the .corrupt backup it replaces. Writing to a store also revealed a hole in the previous fix. Classifying a save failure as errPersist was left to each Store, and a store that returned a plain error would fall through to the "saved, but the plugin failed to start" branch and be reported as a 200 - the same silent-success bug through a different door. The Manager now classifies, whatever the Store returns; a test pins it. Upgrades are automatic: on the first boot that finds no settings in the database, an existing plugins.json is imported and renamed to plugins.json.migrated. The import is refused if the store is merely unreachable, or if the file does not parse, so a stale or broken file can never overwrite live settings. /data is still needed - the panel rewrites .env there when it retargets PocketBase - but plugin settings no longer depend on it. 21 tests in internal/plugins cover both stores, including the production path against a fake PocketBase: create-then-update of the singleton, round-trip across a restart, an outage that leaves settings intact, a missing collection reading as not-ready rather than empty, and the import running exactly once. go build, go vet and go test ./... pass. Schema changes are mirrored into scripts/setup-pocketbase.mjs as that file requires. Not verified: no Docker CLI here, so no image was built and the bootstrap of app_settings against a real PocketBase is untested outside the fake. 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> |