From 8849fcfb40fb8b6b401fff5a2a9e29c79ba38997 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:22:35 +0200 Subject: [PATCH] Anker Solix: what upstream v3.9.0 changed for a charger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit anker-solix-api v3.9.0 (2026-09-29) is mostly PPS work — time-of-use plans over the cloud API, device energy queries, a condensed V2 command map — and none of that reaches an A5191; its MQTT map and command definitions are unchanged from v3.8.1. Three things do: - Singapore accounts live on the EU server (ha-anker-solix #593), so SG leaves the .com list. - A charger still showing a finished session can be started again. The app offers Start while status is completed; the mode options did not (ha-anker-solix #629, #630). - A re-login after a rejected token drops the payload handshake with the token, as upstream now does on a failed login. It was never reused — a successful login cleared it and ensureCipher refuses without a token — but it no longer outlives the token it was run under. Co-Authored-By: Claude Opus 5.5 --- .../plugins/builtin/ankersolix/ankersolix.go | 31 ++++++++++++------- .../builtin/ankersolix/ankersolix_test.go | 5 +-- .../plugins/builtin/ankersolix/mqttframe.go | 2 +- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/API Server/internal/plugins/builtin/ankersolix/ankersolix.go b/API Server/internal/plugins/builtin/ankersolix/ankersolix.go index cfc1335..c02ddd6 100644 --- a/API Server/internal/plugins/builtin/ankersolix/ankersolix.go +++ b/API Server/internal/plugins/builtin/ankersolix/ankersolix.go @@ -2,14 +2,16 @@ // scoped to the Anker Solix V1 Smart EV Charger (product A5191). It is a Go // re-implementation of the authentication and read-only data flow from the // anker-solix-api project (https://github.com/thomluther/anker-solix-api), -// adapted to DriverVault's plugin contract. It tracks upstream v3.8.2 -// (ha-anker-solix 3.8.2); the login exchange and every endpoint used here are -// unchanged since v3.7.0. What v3.8.2 added that reaches a charger is the app's -// REST payload encryption, ported in payloadcrypto.go and off by default as -// upstream leaves it; its other changes (Solarbank expansion sizing, the Gen 2 -// smart meter's MQTT map, new site parameter types) touch devices this -// connector does not address, and the A5191 MQTT map is byte-for-byte the one -// v3.8.1 had. +// adapted to DriverVault's plugin contract. It tracks upstream v3.9.0 +// (ha-anker-solix 3.9.0); the login exchange and every endpoint used here are +// unchanged since v3.7.0. v3.8.2 added the app's REST payload encryption, +// ported in payloadcrypto.go and off by default as upstream leaves it. v3.9.0 +// reaches a charger in three small places: Singapore accounts log in on the +// EU server, Start is offered while a finished session still shows completed, +// and a failed login drops the payload handshake with the token. The rest of +// v3.9.0 (PPS time-of-use plans, device energy queries, the condensed V2 +// command map) touches devices this connector does not address, and the A5191 +// MQTT map is byte-for-byte the one v3.8.1 had. // // Scope & limitations: // - The REST half is read-only. Every endpoint below retrieves EV-charger @@ -203,12 +205,14 @@ var ocppConnStatus = map[int]string{0: "disconnected", 1: "connecting", 2: "conn const tokenExpiryMargin = 5 * time.Minute // comCountries are the ISO country IDs served by the global (".com") server. -// Any country not listed here is served by the EU server. +// Any country not listed here is served by the EU server — Singapore among +// them since upstream v3.9.0 (ha-anker-solix #593), where SG accounts turned +// out to live. var comCountries = map[string]bool{ "DZ": true, "LB": true, "SY": true, "EG": true, "LY": true, "TN": true, "MA": true, "JO": true, "PS": true, "AR": true, "AU": true, "BR": true, "HK": true, "IN": true, "MX": true, "NG": true, "NZ": true, "RU": true, - "SG": true, "ZA": true, "KR": true, "TW": true, "US": true, "CA": true, + "ZA": true, "KR": true, "TW": true, "US": true, "CA": true, } func init() { @@ -1034,8 +1038,10 @@ func chargerModeOptions(mode, statusDesc string) []string { case modeBoostCharge: set[modeStopCharge] = true default: - // Only a charger sitting in standby can be told to start. - if statusDesc == stateStandby { + // A charger sitting in standby can be told to start, and so can one still + // showing a finished session: completed can last until the cable is pulled, + // and the app offers Start throughout (upstream v3.9.0, ha-anker-solix #629). + if statusDesc == stateStandby || statusDesc == stateCompleted { set[modeStartCharge] = true } } @@ -1357,6 +1363,7 @@ func (p *Plugin) apiCall(ctx context.Context, endpoint string, send func(tokenIn if status == http.StatusUnauthorized || status == http.StatusForbidden || isAuthCode(body) { p.sess.mu.Lock() p.sess.tok = nil + p.sess.enc = nil // the handshake goes with the token it was run under lerr := p.attemptLogin(ctx) var newTok tokenInfo if lerr == nil { diff --git a/API Server/internal/plugins/builtin/ankersolix/ankersolix_test.go b/API Server/internal/plugins/builtin/ankersolix/ankersolix_test.go index 12b1105..407e5ce 100644 --- a/API Server/internal/plugins/builtin/ankersolix/ankersolix_test.go +++ b/API Server/internal/plugins/builtin/ankersolix/ankersolix_test.go @@ -111,6 +111,7 @@ func TestServerSelection(t *testing.T) { "GB": serverEU, // not in the COM list → EU default "US": serverCOM, "AU": serverCOM, + "SG": serverEU, // moved off the COM list in upstream v3.9.0 "": serverEU, // empty → defaults to DE → EU } for country, want := range cases { @@ -348,9 +349,9 @@ func TestChargerModeOptions(t *testing.T) { {modeWaitStart, statePreparing, []string{modeSkipDelay, modeStopCharge, modeWaitStart}}, {modeWaitPlug, statePreparing, []string{modeStopCharge, modeWaitPlug}}, {modeBoostCharge, stateCharging, []string{modeBoostCharge, modeStopCharge}}, - // Stopped: startable only from standby. + // Stopped: startable from standby, and from completed since v3.9.0. {modeStopCharge, stateStandby, []string{modeStartCharge, modeStopCharge}}, - {modeStopCharge, stateCompleted, []string{modeStopCharge}}, + {modeStopCharge, stateCompleted, []string{modeStartCharge, modeStopCharge}}, {modeStopCharge, stateError, []string{modeStopCharge}}, {"", stateUnknown, []string{}}, } diff --git a/API Server/internal/plugins/builtin/ankersolix/mqttframe.go b/API Server/internal/plugins/builtin/ankersolix/mqttframe.go index 91bd2e3..89900bc 100644 --- a/API Server/internal/plugins/builtin/ankersolix/mqttframe.go +++ b/API Server/internal/plugins/builtin/ankersolix/mqttframe.go @@ -7,7 +7,7 @@ package ankersolix // That frame is the same one the app sends, so a command is not a documented API // call but a byte layout, reproduced here from the message maps in // anker-solix-api (src/anker_solix_api/mqtttypes.py, mqttmap.py, mqttcmdmap.py at -// v3.8.1, unchanged for the A5191 in v3.8.2) and checked against a live A5191. +// v3.8.1, unchanged for the A5191 through v3.9.0) and checked against a live A5191. // // ff 09 2-byte marker on every Anker Solix frame // xx xx total length in bytes, little endian, counting the checksum