Tap the card on the charger and the number fills itself in

The enrolment the Anker app does, done here: 0108 a2=7 opens the reader,
0908 brings back the UID. The frames this sends are byte-for-byte the
ones the app was captured sending — checksum included — which is what the
new test asserts.

Adding and removing now write the charger as well as the account: the
device write is the app's own message, the account write is the inferred
one that carries the name, and either may fail without the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5 committed 2026-09-03 13:45:00 +02:00
1 parent 4c73d4ac05
commit 7176867eb3
13 files changed
+473 -19

No files matched your search

+7
View File
@@ -345,6 +345,13 @@ export const api = {
method: "POST",
body: JSON.stringify({ cardNumber, cardName }),
}),
// Opens the charger's own card reader and waits for a tap — the request is in
// flight for the whole twenty-second window, and answers whether or not a card
// arrived.
scanAnkerRfidCard: (sn) =>
request(`/integrations/anker-solix/chargers/${encodeURIComponent(sn)}/rfid-cards/scan`, {
method: "POST",
}),
deleteAnkerRfidCard: (sn, cardNumber) =>
request(
`/integrations/anker-solix/chargers/${encodeURIComponent(sn)}/rfid-cards/${encodeURIComponent(cardNumber)}`,
+4
View File
@@ -69,6 +69,10 @@
"none": "Ingen kort er godkendt til denne lader.",
"unsupported": "Tjenesten, som denne lader kommer fra, rapporterer ikke RFID-kort.",
"add": "Tilføj kort",
"tap": "Hold kortet mod laderen",
"tapping": "Hold kortet mod læseren… {n}s",
"tapHint": "Læseren er åben. Hold kortet mod laderen.",
"tapNone": "Der blev ikke holdt et kort mod læseren, før den lukkede.",
"addTitle": "Tilføj et kort",
"remove": "Fjern",
"removeConfirm": "Fjern {name} fra denne lader?",
+4
View File
@@ -338,6 +338,10 @@
"none": "No cards are authorised on this charger.",
"unsupported": "The service this charger came from does not report RFID cards.",
"add": "Add card",
"tap": "Tap card at the charger",
"tapping": "Hold the card against the reader… {n}s",
"tapHint": "The reader is open. Hold the card against the charger.",
"tapNone": "No card was tapped before the reader closed.",
"addTitle": "Add a card",
"remove": "Remove",
"removeConfirm": "Remove {name} from this charger?",
+4
View File
@@ -69,6 +69,10 @@
"none": "Na tej ładowarce nie autoryzowano żadnej karty.",
"unsupported": "Usługa, z której pochodzi ta ładowarka, nie zgłasza kart RFID.",
"add": "Dodaj kartę",
"tap": "Przyłóż kartę do ładowarki",
"tapping": "Przytrzymaj kartę przy czytniku… {n}s",
"tapHint": "Czytnik jest otwarty. Przytrzymaj kartę przy ładowarce.",
"tapNone": "Nie przyłożono karty, zanim czytnik się zamknął.",
"addTitle": "Dodaj kartę",
"remove": "Usuń",
"removeConfirm": "Usunąć {name} z tej ładowarki?",
+43 -1
View File
@@ -1242,7 +1242,8 @@ const rfidCards = computed(() => {
return rfidCardsFrom(view?.attrs);
});
const rfidBusy = ref(""); // the card being written, or "new" while one is added
const rfidBusy = ref(""); // the card being written, "new", or "scan" while the reader is open
const rfidCountdown = ref(0); // seconds left of the reader's window, while it is open
const rfidError = ref("");
const newCardNumber = ref("");
const newCardName = ref("");
@@ -1272,6 +1273,35 @@ async function addRfidCard() {
}
}
// Asking the charger to read a card, which is what the Anker app's second way of
// adding one does: the reader opens for twenty seconds, and whatever is held
// against it comes back as a number. Nothing is written by this — the card lands
// in the form, and adding it is still a decision.
async function scanRfidCard() {
const sn = detailSn.value;
if (!sn || rfidBusy.value) return;
rfidBusy.value = "scan";
rfidError.value = "";
rfidCountdown.value = 20;
const tick = setInterval(() => {
rfidCountdown.value = Math.max(0, rfidCountdown.value - 1);
}, 1000);
try {
const res = await api.scanAnkerRfidCard(sn);
if (res?.tapped && res.card) {
newCardNumber.value = res.card;
} else {
rfidError.value = t("charging.rfid.tapNone");
}
} catch (e) {
rfidError.value = e.message;
} finally {
clearInterval(tick);
rfidCountdown.value = 0;
rfidBusy.value = "";
}
}
// Removing one asks first — a card that is gone can only be put back by whoever
// still has it in their hand.
async function removeRfidCard(card) {
@@ -1877,6 +1907,18 @@ onMounted(async () => {
{{ rfidBusy === "new" ? t("common.loading") : t("charging.rfid.add") }}
</button>
</div>
<!-- The other way to fill that field in: hold the card against the
charger. The reader opens for twenty seconds and the number
arrives on its own, which beats reading it off the card. -->
<button
type="button"
class="dh-btn dh-btn-ghost mt-2 w-full text-xs"
:disabled="rfidBusy !== ''"
@click="scanRfidCard"
>
{{ rfidBusy === "scan" ? t("charging.rfid.tapping", { n: rfidCountdown }) : t("charging.rfid.tap") }}
</button>
<p v-if="rfidBusy === 'scan'" class="mt-1 text-[11px] text-muted">{{ t("charging.rfid.tapHint") }}</p>
</form>
<p class="mt-3 text-[11px] text-muted">{{ t("charging.rfid.inferred") }}</p>
</div>