Each RFID card as a card, not as list[3]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
905eb24cc1
commit
928a39e03e
@@ -283,6 +283,28 @@
|
||||
"bleId": "Bluetooth-id",
|
||||
"blePassword": "Bluetooth-parringskode",
|
||||
"ownerId": "Ejer-id",
|
||||
"fields": {
|
||||
"sessions": "Sessioner",
|
||||
"chargeTime": "Opladningstid",
|
||||
"energy": "Opladet energi",
|
||||
"co2Saved": "CO2 sparet",
|
||||
"cost": "Omkostning",
|
||||
"costSaved": "Sparet beløb",
|
||||
"currency": "Valuta",
|
||||
"mileage": "Kilometertal",
|
||||
"page": "Side",
|
||||
"perPage": "Pr. side",
|
||||
"records": "Poster",
|
||||
"from": "Fra",
|
||||
"source": "Kilde",
|
||||
"timeZone": "Tidszone",
|
||||
"updated": "Opdateret",
|
||||
"added": "Tilføjet",
|
||||
"address": "Adresse",
|
||||
"name": "Navn",
|
||||
"cardName": "Kortnavn",
|
||||
"cardNumber": "Kortnummer"
|
||||
},
|
||||
"groups": {
|
||||
"device": "Enhed",
|
||||
"status": "Status",
|
||||
|
||||
@@ -269,6 +269,28 @@
|
||||
"bleId": "Bluetooth id",
|
||||
"blePassword": "Bluetooth pairing code",
|
||||
"ownerId": "Owner id",
|
||||
"fields": {
|
||||
"sessions": "Sessions",
|
||||
"chargeTime": "Time charging",
|
||||
"energy": "Energy charged",
|
||||
"co2Saved": "CO2 saved",
|
||||
"cost": "Cost",
|
||||
"costSaved": "Cost saved",
|
||||
"currency": "Currency",
|
||||
"mileage": "Mileage",
|
||||
"page": "Page",
|
||||
"perPage": "Per page",
|
||||
"records": "Records",
|
||||
"from": "From",
|
||||
"source": "Source",
|
||||
"timeZone": "Time zone",
|
||||
"updated": "Updated",
|
||||
"added": "Added",
|
||||
"address": "Address",
|
||||
"name": "Name",
|
||||
"cardName": "Card name",
|
||||
"cardNumber": "Card number"
|
||||
},
|
||||
"groups": {
|
||||
"device": "Device",
|
||||
"status": "Status",
|
||||
|
||||
@@ -285,6 +285,28 @@
|
||||
"bleId": "Identyfikator Bluetooth",
|
||||
"blePassword": "Kod parowania Bluetooth",
|
||||
"ownerId": "Identyfikator właściciela",
|
||||
"fields": {
|
||||
"sessions": "Sesje",
|
||||
"chargeTime": "Czas ładowania",
|
||||
"energy": "Naładowana energia",
|
||||
"co2Saved": "Oszczędność CO2",
|
||||
"cost": "Koszt",
|
||||
"costSaved": "Oszczędność kosztów",
|
||||
"currency": "Waluta",
|
||||
"mileage": "Przebieg",
|
||||
"page": "Strona",
|
||||
"perPage": "Na stronę",
|
||||
"records": "Rekordy",
|
||||
"from": "Od",
|
||||
"source": "Źródło",
|
||||
"timeZone": "Strefa czasowa",
|
||||
"updated": "Zaktualizowano",
|
||||
"added": "Dodano",
|
||||
"address": "Adres",
|
||||
"name": "Nazwa",
|
||||
"cardName": "Nazwa karty",
|
||||
"cardNumber": "Numer karty"
|
||||
},
|
||||
"groups": {
|
||||
"device": "Urządzenie",
|
||||
"status": "Status",
|
||||
|
||||
@@ -1018,6 +1018,99 @@ function chargerInfoGroups(c) {
|
||||
}));
|
||||
}
|
||||
|
||||
// The keys the views below answer with, in the card's words. Anker documents
|
||||
// none of these payloads either, but a field like page_size or create_time says
|
||||
// what it is once its key is read out, and those are named here rather than left
|
||||
// as keys. Keyed by the field with any list index taken out, so list[0].name and
|
||||
// list[3].name are one field asked about two records. Anything absent from this
|
||||
// table keeps its own key, for the same reason the box above does: a name
|
||||
// invented here would be a meaning invented here.
|
||||
const VIEW_FIELDS = {
|
||||
// What the account has counted for this charger.
|
||||
"total_stats.charge_count": "sessions",
|
||||
"total_stats.charge_time": "chargeTime",
|
||||
"total_stats.charge_total": "energy",
|
||||
"total_stats.co2_saving": "co2Saved",
|
||||
"total_stats.cost": "cost",
|
||||
"total_stats.cost_saving": "costSaved",
|
||||
"total_stats.cost_unit": "currency",
|
||||
"total_stats.mile_age": "mileage",
|
||||
// How much of a list the view answered with — a page of a history that is
|
||||
// empty is still the answer "there is nothing to page through".
|
||||
page: "page",
|
||||
page_num: "page",
|
||||
page_size: "perPage",
|
||||
total: "records",
|
||||
total_count: "records",
|
||||
start_use_time: { label: "from", time: true },
|
||||
// The backend the charger is pointed at, and when it last said so.
|
||||
source: "source",
|
||||
time_zone: "timeZone",
|
||||
timestamp: { label: "updated", time: true },
|
||||
create_time: { label: "added", time: true },
|
||||
// The records a list view answers with: one OCPP endpoint, one RFID card.
|
||||
"list[].address": "address",
|
||||
"list[].name": "name",
|
||||
"list[].source": "source",
|
||||
"list[].alias_name": "cardName",
|
||||
"list[].card_number": "cardNumber",
|
||||
"list[].create_time": { label: "added", time: true },
|
||||
};
|
||||
|
||||
// list[0].card_number split into the list, which record, and which field.
|
||||
const VIEW_LIST_KEY = /^([A-Za-z0-9_.]+)\[(\d+)\]\.(.+)$/;
|
||||
|
||||
// A unix second the cloud sent as a bare number, read as a date. Zero is how
|
||||
// these views say "never" rather than 1970, so it is left as it arrived.
|
||||
function viewTimeValue(v) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n) || n <= 0) return v;
|
||||
return formatDateTime(new Date(n * 1000));
|
||||
}
|
||||
|
||||
// One field of one view: named when the table above has a name for it, and
|
||||
// keyed by the cloud's own key when it does not. The key is kept either way —
|
||||
// it is what the row is drawn under and what makes it unique.
|
||||
function viewRow(key, lookup, value) {
|
||||
const spec = VIEW_FIELDS[lookup];
|
||||
const label = typeof spec === "string" ? spec : spec?.label;
|
||||
return {
|
||||
key,
|
||||
label: label ? t(`charging.info.fields.${label}`) : key,
|
||||
named: !!label,
|
||||
value: typeof spec === "object" && spec?.time ? viewTimeValue(value) : value,
|
||||
};
|
||||
}
|
||||
|
||||
// One view's fields, with the lists it answered with grouped a record at a time.
|
||||
// list[0].* and list[1].* are two RFID cards, two endpoints, two anything: read
|
||||
// as one alphabetical run of indexed keys they are unreadable, and as a block
|
||||
// each — under the record's own name, when it has one — they are the list the
|
||||
// view actually sent. The record's name becomes the heading rather than a row,
|
||||
// so it is said once.
|
||||
function viewRowGroups(attrs) {
|
||||
const rows = [];
|
||||
const items = new Map();
|
||||
for (const key of Object.keys(attrs).sort()) {
|
||||
const m = VIEW_LIST_KEY.exec(key);
|
||||
if (!m) {
|
||||
rows.push(viewRow(key, key, attrs[key]));
|
||||
continue;
|
||||
}
|
||||
const [, list, index, field] = m;
|
||||
const id = `${list}[${index}]`;
|
||||
if (!items.has(id)) {
|
||||
const label = attrs[`${id}.alias_name`] || attrs[`${id}.name`] || `#${Number(index) + 1}`;
|
||||
const titleKey = attrs[`${id}.alias_name`] ? `${id}.alias_name` : attrs[`${id}.name`] ? `${id}.name` : "";
|
||||
items.set(id, { key: id, index: Number(index), label, titleKey, rows: [] });
|
||||
}
|
||||
const item = items.get(id);
|
||||
if (key === item.titleKey) continue;
|
||||
item.rows.push(viewRow(key, `${list}[].${field}`, attrs[key]));
|
||||
}
|
||||
return { rows, items: [...items.values()].sort((a, b) => a.index - b.index) };
|
||||
}
|
||||
|
||||
// --- The account's other views of one charger ---
|
||||
//
|
||||
// Four endpoints answer only when a serial is named — the station record, the
|
||||
@@ -1063,9 +1156,7 @@ const chargerDetailViews = computed(() => {
|
||||
id: view.id,
|
||||
error: view.error || "",
|
||||
note: view.note || "",
|
||||
rows: Object.keys(view.attrs || {})
|
||||
.sort()
|
||||
.map((key) => ({ key, value: view.attrs[key] })),
|
||||
...viewRowGroups(view.attrs || {}),
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -2062,15 +2153,32 @@ onMounted(async () => {
|
||||
<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">
|
||||
<p v-else-if="!view.rows.length && !view.items.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>
|
||||
<div v-else class="mt-2 flex flex-col gap-2">
|
||||
<!-- What the view says about itself. A field the card has a name
|
||||
for is drawn like every other named row; one it does not is
|
||||
drawn under its own key, in the key's own typeface, so the
|
||||
two are never mistaken for each other. -->
|
||||
<dl v-if="view.rows.length" class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1">
|
||||
<template v-for="row in view.rows" :key="row.key">
|
||||
<dt class="break-all text-[11px] text-muted" :class="row.named ? '' : 'data'">{{ row.label }}</dt>
|
||||
<dd class="data break-all text-[11px] text-body">{{ row.value }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<!-- And the records it answered with, a block each under the
|
||||
record's own name. -->
|
||||
<div v-for="item in view.items" :key="item.key" class="rounded-control bg-card p-2">
|
||||
<p class="text-[11px] font-semibold text-strong">{{ item.label }}</p>
|
||||
<dl class="mt-1 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1">
|
||||
<template v-for="row in item.rows" :key="row.key">
|
||||
<dt class="break-all text-[11px] text-muted" :class="row.named ? '' : 'data'">{{ row.label }}</dt>
|
||||
<dd class="data break-all text-[11px] text-body">{{ row.value }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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>
|
||||
|
||||
Reference in New Issue
Block a user