Caught on the charger's own command topic while a card was removed and added back in the Anker app. Enrolling at the charger is three MQTT messages and no REST call: open the reader, take the UID the tap publishes, write it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
155 lines
5.1 KiB
Go
155 lines
5.1 KiB
Go
package ankersolix
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"testing"
|
|
)
|
|
|
|
// The card-list frame, captured from a live A5191 on firmware 1.0.6.1. The nine
|
|
// UIDs in it are the nine the account's own card list answered with, which is
|
|
// what named this frame in the first place.
|
|
const capturedCardFrame = "ff09540003010f090400a10132a2020109a3080404dfe672151a90a4050457c4df2a" +
|
|
"a505046745bb4ea6050477dfd24ea70504d7b1c44ea80504778acd4ea905048a3cbc3f" +
|
|
"aa0504da7cb93fab05044754dc2a00"
|
|
|
|
func TestDecodeCardListFrame(t *testing.T) {
|
|
data, err := hex.DecodeString(capturedCardFrame)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msgType, values, err := decodeFrame(data)
|
|
if err != nil {
|
|
t.Fatalf("decodeFrame: %v", err)
|
|
}
|
|
if msgType != msgEVCards {
|
|
t.Fatalf("msgType = %q, want %q", msgType, msgEVCards)
|
|
}
|
|
if n, _ := values["rfidCardCount"].(float64); n != 9 {
|
|
t.Errorf("rfidCardCount = %v, want 9", values["rfidCardCount"])
|
|
}
|
|
cards, ok := values["rfidCards"].([]string)
|
|
if !ok {
|
|
t.Fatalf("rfidCards = %T, want []string", values["rfidCards"])
|
|
}
|
|
// In the charger's own order: newest first, which is the reverse of the way
|
|
// the account lists them.
|
|
want := []string{
|
|
"04DFE672151A90", "57C4DF2A", "6745BB4E", "77DFD24E", "D7B1C44E",
|
|
"778ACD4E", "8A3CBC3F", "DA7CB93F", "4754DC2A",
|
|
}
|
|
if len(cards) != len(want) {
|
|
t.Fatalf("got %d cards, want %d: %v", len(cards), len(want), cards)
|
|
}
|
|
for i, w := range want {
|
|
if cards[i] != w {
|
|
t.Errorf("card %d = %q, want %q", i, cards[i], w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A UID is an identifier, not a quantity: read as a big-endian number, 57C4DF2A
|
|
// becomes 1472519978 and matches nothing the account ever prints.
|
|
func TestCardUIDsAreHexNotNumbers(t *testing.T) {
|
|
v, ok := decodeValue(typeBytes, []byte{0x57, 0xc4, 0xdf, 0x2a}, mqttField{})
|
|
if !ok || v != "57C4DF2A" {
|
|
t.Fatalf("decodeValue = %v (%T), want \"57C4DF2A\"", v, v)
|
|
}
|
|
}
|
|
|
|
// The charger's own view of its OCPP backend, from the same capture.
|
|
func TestDecodeOcppInfoFrame(t *testing.T) {
|
|
data, err := hex.DecodeString("ff096a0003010f091100a10132a20600416e6b6572a331007773733a2f2f" +
|
|
"6368617267696e672d6f63707070726f78792d65752d70726f642e616e6b65722e636f6d2f6f6370706a" +
|
|
"a4020100a51200415432444b325a31463438333030313134a60100a70100a80100a7")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msgType, values, err := decodeFrame(data)
|
|
if err != nil {
|
|
t.Fatalf("decodeFrame: %v", err)
|
|
}
|
|
if msgType != msgEVOcppInfo {
|
|
t.Fatalf("msgType = %q, want %q", msgType, msgEVOcppInfo)
|
|
}
|
|
if got := values["ocppBackendName"]; got != "Anker" {
|
|
t.Errorf("ocppBackendName = %v, want Anker", got)
|
|
}
|
|
if got := values["ocppBackendUrl"]; got != "wss://charging-ocppproxy-eu-prod.anker.com/ocppj" {
|
|
t.Errorf("ocppBackendUrl = %v", got)
|
|
}
|
|
if got, _ := values["ocppSource"].(float64); got != 0 {
|
|
t.Errorf("ocppSource = %v, want 0 — the number the account's OCPP view leaves bare", values["ocppSource"])
|
|
}
|
|
if got := values["ocppChargePointId"]; got != "AT2DK2Z1F48300114" {
|
|
t.Errorf("ocppChargePointId = %v", got)
|
|
}
|
|
}
|
|
|
|
// The tap itself, captured while a card was enrolled at the charger: the reader
|
|
// reports the UID the moment the card touches it.
|
|
func TestDecodeCardReadFrame(t *testing.T) {
|
|
data, err := hex.DecodeString("ff091b0003010f090800a10132a20107a3080404dfe672151a90a8")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msgType, values, err := decodeFrame(data)
|
|
if err != nil {
|
|
t.Fatalf("decodeFrame: %v", err)
|
|
}
|
|
if msgType != msgEVCardRead {
|
|
t.Fatalf("msgType = %q, want %q", msgType, msgEVCardRead)
|
|
}
|
|
if got := values["rfidCardRead"]; got != "04DFE672151A90" {
|
|
t.Errorf("rfidCardRead = %v, want 04DFE672151A90", got)
|
|
}
|
|
}
|
|
|
|
// The same frame when the window closed with nothing tapped: no UID, and the
|
|
// absence is the answer.
|
|
func TestCardReadWithoutACard(t *testing.T) {
|
|
data, err := hex.DecodeString("ff09110003010f090801a10132a20107dc")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msgType, values, err := decodeFrame(data)
|
|
if err != nil {
|
|
t.Fatalf("decodeFrame: %v", err)
|
|
}
|
|
if msgType != msgEVCardRead {
|
|
t.Fatalf("msgType = %q, want %q", msgType, msgEVCardRead)
|
|
}
|
|
if _, ok := values["rfidCardRead"]; ok {
|
|
t.Errorf("rfidCardRead = %v, want it absent", values["rfidCardRead"])
|
|
}
|
|
}
|
|
|
|
// The two writes the Anker app sent, caught on the charger's own command topic:
|
|
// the same card removed and then added back, the action the only difference.
|
|
func TestDecodeCardWriteCommands(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, hex string
|
|
action float64
|
|
}{
|
|
{"delete", "ff091f0003000f0103a10122a2020102a3020101a4080404dfe672151a901f", 2},
|
|
{"add", "ff091f0003000f0103a10122a2020101a3020101a4080404dfe672151a901c", 1},
|
|
} {
|
|
data, err := hex.DecodeString(tc.hex)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msgType, values, err := decodeFrame(data)
|
|
if err != nil {
|
|
t.Fatalf("%s: decodeFrame: %v", tc.name, err)
|
|
}
|
|
if msgType != msgEVCardWrite {
|
|
t.Fatalf("%s: msgType = %q, want %q", tc.name, msgType, msgEVCardWrite)
|
|
}
|
|
if got, _ := values["cardWriteAction"].(float64); got != tc.action {
|
|
t.Errorf("%s: cardWriteAction = %v, want %v", tc.name, values["cardWriteAction"], tc.action)
|
|
}
|
|
if got := values["cardWritten"]; got != "04DFE672151A90" {
|
|
t.Errorf("%s: cardWritten = %v, want the UID", tc.name, got)
|
|
}
|
|
}
|
|
}
|