Four questions asked of an account that answers twenty-one

The connector called ten endpoints of the read surface the map lists, and
the charger card showed four views. Everything else an EV charger can
reach is now a capability too: the sessions and the history, the savings,
the sharing, the binding, the group, the Wi-Fi, the firmware and its
update log, the tamper records, the site's own detail, price, networks
and energy — plus the vehicle catalogue, dynamic pricing, the currencies
and the notification views. Thirty-eight endpoints, one action each. The
two message views are GET, so the request path grew a GET half that shares
the login retry with the POST one.

The per-charger fan-out asks all of them, six at a time rather than one
after another, and a charger that belongs to a site brings that site's
four views with it once the by-serial lookup has found it. A view that
answers with nothing now says so instead of vanishing: the station record
is empty for a standalone charger because it has no station, which is an
answer worth reading. And "source 0" in the OCPP box carries the address
the account's endpoint list gives it.

Anker's account-level writes stay out, as do the endpoints whose payloads
were only ever read out of the app package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-09-02 22:53:36 +02:00
co-authored by Claude Opus 5
parent fb42791f8d
commit 3f9d5b943f
15 changed files with 839 additions and 70 deletions
+4 -3
View File
@@ -328,9 +328,10 @@ export const api = {
// with an empty list and a reason when a gate is off, so the caller can show
// the reason rather than an error.
listAnkerChargers: () => request("/integrations/anker-solix/chargers"),
// Every per-charger view the account holds — the station record, the charging
// totals, the OCPP backend and the RFID cards — asked for one charger at a
// time, because none of those endpoints lists chargers.
// Every view the account holds about one charger — the station record, the
// totals, the history, the sessions, the OCPP backend, the cards, the sharing,
// the firmware and the rest, plus its site's views when it has a site — asked
// for one charger at a time, because none of those endpoints lists chargers.
getAnkerChargerDetails: (sn) =>
request(`/integrations/anker-solix/chargers/${encodeURIComponent(sn)}/details`),
+19 -1
View File
@@ -278,10 +278,28 @@
},
"views": {
"station": "Stationspost",
"stationFeature": "Stationsfunktioner",
"totals": "Ladetotaler",
"orders": "Ladehistorik",
"sessions": "Ladesessioner",
"income": "Besparelse og indtægt",
"ocpp": "OCPP-backend",
"rfid": "RFID-kort"
"ocppEndpoints": "OCPP-endepunkter hos Anker",
"rfid": "RFID-kort",
"shared": "Delt med",
"binding": "Kontotilknytning",
"group": "Enhedsgruppe",
"wifi": "Wi-Fi-detaljer",
"ota": "Firmwarestatus",
"upgrades": "Opdateringshistorik",
"tamper": "Manipulationslog",
"siteBySn": "Systemopslag",
"site": "Systemdetaljer",
"sitePrice": "Pris og CO2-faktor",
"siteWifi": "Netværk systemet kan se",
"energy": "Energianalyse"
},
"viewEmpty": "Denne visning svarede, men uden indhold.",
"offline": "Offline",
"refresh": "Opdater",
"rawTitle": "Som tjenesten melder det",
+19 -1
View File
@@ -264,10 +264,28 @@
},
"views": {
"station": "Station record",
"stationFeature": "Station features",
"totals": "Charging totals",
"orders": "Charging history",
"sessions": "Charging sessions",
"income": "Savings and income",
"ocpp": "OCPP backend",
"rfid": "RFID cards"
"ocppEndpoints": "OCPP endpoints Anker uses",
"rfid": "RFID cards",
"shared": "Shared with",
"binding": "Account binding",
"group": "Device group",
"wifi": "Wi-Fi details",
"ota": "Firmware status",
"upgrades": "Update history",
"tamper": "Tamper records",
"siteBySn": "Site lookup",
"site": "Site detail",
"sitePrice": "Price and CO2 factor",
"siteWifi": "Networks the site sees",
"energy": "Energy analysis"
},
"viewEmpty": "This view answered, with nothing in it.",
"offline": "Offline",
"refresh": "Refresh",
"rawTitle": "As the service reports it",
+19 -1
View File
@@ -280,10 +280,28 @@
},
"views": {
"station": "Rekord stacji",
"stationFeature": "Funkcje stacji",
"totals": "Sumy ładowania",
"orders": "Historia ładowania",
"sessions": "Sesje ładowania",
"income": "Oszczędności i przychód",
"ocpp": "Backend OCPP",
"rfid": "Karty RFID"
"ocppEndpoints": "Punkty OCPP używane przez Ankera",
"rfid": "Karty RFID",
"shared": "Udostępniono",
"binding": "Powiązanie z kontem",
"group": "Grupa urządzeń",
"wifi": "Szczegóły Wi-Fi",
"ota": "Stan firmware'u",
"upgrades": "Historia aktualizacji",
"tamper": "Rejestr naruszeń",
"siteBySn": "Wyszukanie systemu",
"site": "Szczegóły systemu",
"sitePrice": "Cena i współczynnik CO2",
"siteWifi": "Sieci widziane przez system",
"energy": "Analiza energii"
},
"viewEmpty": "Ten widok odpowiedział, ale nic nie zawierał.",
"offline": "Offline",
"refresh": "Odśwież",
"rawTitle": "Tak, jak podaje to usługa",
+19 -5
View File
@@ -954,7 +954,13 @@ async function loadChargerDetails(force = false) {
// One view's fields, sorted, or the reason it could not be read. The keys are
// the cloud's own: Anker documents none of these payloads, so a name invented
// here would be a meaning invented here.
// here would be a meaning invented here. Note is the exception, and the server
// says which: a number the account elsewhere gives a meaning to, resolved.
//
// Every view the server asked for is drawn, the ones that answered with nothing
// included. A standalone charger has no station record and no site; that its
// site views are empty is the answer to "which of these does this account hold",
// and a box that quietly disappears cannot say it.
const chargerDetailViews = computed(() => {
const c = selectedHomeCharger.value;
const sn = c?.providerChargerId || c?.serial || "";
@@ -962,10 +968,11 @@ const chargerDetailViews = computed(() => {
return (doc?.views || []).map((view) => ({
id: view.id,
error: view.error || "",
note: view.note || "",
rows: Object.keys(view.attrs || {})
.sort()
.map((key) => ({ key, value: view.attrs[key] })),
})).filter((view) => view.rows.length || view.error);
}));
});
// Everything else the service said about this charger, under its own field
@@ -1941,18 +1948,25 @@ onMounted(async () => {
</section>
<!-- The views that answer per charger rather than per account. Each
says what it knows, or why it could not be read an account
that is not the charger's owner cannot read the cards, which is
a fact about the account rather than a failure. -->
says what it knows, why it could not be read an account that
is not the charger's owner cannot read the cards, which is a
fact about the account rather than a failure — or that it
answered with nothing, which is equally an answer. -->
<section v-for="view in chargerDetailViews" :key="view.id" class="rounded-control bg-sunken p-3">
<h4 class="eyebrow">{{ t(`charging.info.views.${view.id}`) }}</h4>
<p v-if="view.error" class="mt-2 text-[11px] text-muted">{{ view.error }}</p>
<p v-else-if="!view.rows.length" class="mt-2 text-[11px] text-muted">
{{ t("charging.info.viewEmpty") }}
</p>
<dl v-else class="mt-2 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1">
<template v-for="row in view.rows" :key="row.key">
<dt class="data break-all text-[11px] text-muted">{{ row.key }}</dt>
<dd class="data break-all text-[11px] text-body">{{ row.value }}</dd>
</template>
</dl>
<!-- The one line in DriverVault's own words: a number this view
reports that another view gives an address. -->
<p v-if="view.note" class="data mt-2 text-[11px] text-muted">{{ view.note }}</p>
</section>
</div>