A dash printed above the value it was missing

The Charger information card read "—" beside State and OCPP status while the raw
block three rows below it printed chargerStatus 1 and ocpp_connect_status 2. The
account had answered both. The merge asked for the state as evChargerStatus,
operating_state or status, which is how the standalone and station views spell
it, and the bound-device view — the one this account actually answers from —
spells it chargerStatus. The OCPP state it never asked that view for at all. All
three views now read through one fillDevice, which tries every spelling a view is
known to use, so a value any of them sends reaches the row that was drawing a
dash for want of it.

The same views were carrying the whole box-on-the-wall half unread: the Wi-Fi
network and its MAC, the signal strength, the Bluetooth MAC, the time zone, when
the account bound the charger, how the app can reach it — BLE, Wi-Fi — and the
product shot for the model, which now sits beside the charger's name in both
apps. Named rows, in three languages, the way the register map's readings are
named.

One field wanted the opposite treatment. The device record carries blue_password,
the charger's own Bluetooth pairing password, and the card was printing it in
clear into every screenshot anyone takes of that page. Any leaf key holding a
password, secret, token, private key or certificate is now masked in the raw
block: that the field exists is worth reporting, its value is not.

Four endpoints answer only when a serial is named, so none of them could belong
to the list the card is drawn from, and nothing had ever called them. The station
record, the charging totals, the OCPP backend and the RFID cards now arrive
through a charger-details capability behind
GET …/anker-solix/chargers/{sn}/details, asked for the charger being looked at,
best effort, each view reporting its own failure — an account that is not the
owner cannot read the cards, which is a fact about the account rather than an
error in the read.

Those four are shown under the cloud's own keys, and that is not an oversight.
The REST map documents which endpoints exist and what each is for; it does not
document a single one of their payloads. Naming those fields is the next commit,
made from what actually comes back, now that there is somewhere to see it.

Not touched: the endpoints the map marks ready but unwired — session history,
site price, OTA, sharing, notifications — each a feature rather than a row on this
card; and the unmapped ones, which the map warns delete sessions and unbind
devices with payloads nobody has ever seen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-09-02 21:45:08 +02:00
co-authored by Claude Opus 5
parent 8e2073fc4c
commit 1a7f04cba0
18 changed files with 670 additions and 36 deletions
+75
View File
@@ -1399,6 +1399,18 @@ class ProviderCharger {
final int? ocppStatus;
final String ocppStatusDesc;
/// What the account knows about the box on the wall, as opposed to the
/// charging: which networks it is on, where it thinks it is, when it was
/// bound, and the picture the app shows for it.
final String wifiName;
final String wifiMac;
final int? wifiRssi;
final String bleMac;
final String timeZone;
final double? linkedAt; // unix seconds
final String imageUrl;
final List<String> relatedBy; // ble, wifi — how the app reaches it
/// Everything else the service said about this charger, under its own field
/// names. The fields above are the ones DriverVault has a name for; this is
/// the remainder, kept rather than dropped — the service documents none of it,
@@ -1423,6 +1435,14 @@ class ProviderCharger {
this.power = "",
this.ocppStatus,
this.ocppStatusDesc = "",
this.wifiName = "",
this.wifiMac = "",
this.wifiRssi,
this.bleMac = "",
this.timeZone = "",
this.linkedAt,
this.imageUrl = "",
this.relatedBy = const [],
this.attrs = const {},
this.linkedChargerId = "",
});
@@ -1441,6 +1461,14 @@ class ProviderCharger {
power: _asStr(j["power"]),
ocppStatus: _asIntOrNull(j["ocppStatus"]),
ocppStatusDesc: _asStr(j["ocppStatusDesc"]),
wifiName: _asStr(j["wifiName"]),
wifiMac: _asStr(j["wifiMac"]),
wifiRssi: _asIntOrNull(j["wifiRssi"]),
bleMac: _asStr(j["bleMac"]),
timeZone: _asStr(j["timeZone"]),
linkedAt: _asDoubleOrNull(j["linkedAt"]),
imageUrl: _asStr(j["imageUrl"]),
relatedBy: _asStrList(j["relatedBy"]),
attrs: _asStrMap(j["attrs"]),
linkedChargerId: _asStr(j["linkedChargerId"]),
);
@@ -1457,6 +1485,53 @@ class ProviderCharger {
}
}
/// One of the account's per-charger views — the station record, the charging
/// totals, the OCPP backend, the RFID cards. Anker documents none of these
/// payloads, so the fields arrive under the cloud's own keys; a view the account
/// cannot read carries its reason instead.
class ChargerDetailView {
final String id;
final Map<String, String> attrs;
final String error;
const ChargerDetailView({required this.id, this.attrs = const {}, this.error = ""});
factory ChargerDetailView.fromJson(Map<String, dynamic> j) => ChargerDetailView(
id: _asStr(j["id"]),
attrs: _asStrMap(j["attrs"]),
error: _asStr(j["error"]),
);
/// The fields, sorted, so the same charger reads the same way every time.
List<(String, String)> get rows {
final keys = attrs.keys.toList()..sort();
return [for (final key in keys) (key, attrs[key]!)];
}
}
/// Every per-charger view for one charger, from
/// …/anker-solix/chargers/{sn}/details.
class ChargerDetails {
final String sn;
final List<ChargerDetailView> views;
const ChargerDetails({this.sn = "", this.views = const []});
factory ChargerDetails.fromJson(Map<String, dynamic> j) {
final raw = j["views"];
return ChargerDetails(
sn: _asStr(j["sn"]),
views: raw is List
? raw
.whereType<Map>()
.map((v) => ChargerDetailView.fromJson(Map<String, dynamic>.from(v)))
.where((v) => v.attrs.isNotEmpty || v.error.isNotEmpty)
.toList()
: const [],
);
}
}
/// The chargers on one provider account, plus the reason the list may be empty:
/// a gate that is off answers 200 with nothing and a sentence, so the UI can say
/// "connect this in Settings" rather than show a blank panel.