From a809980d8b737d08ddda8358ab2fc92906dc2e64 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:46:02 +0200 Subject: [PATCH] Anker health: count the chargers the panel lists, not the ones one endpoint admits to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The probe still asked get_user_bind_and_not_in_station_evchargers and read its userBindEvChargersCount, so it reported "0 EV charger(s) bound to account" for an account whose two chargers the panel was listing directly underneath — the same blind spot the capability was just moved off, left behind in the health check. It now takes the same inventory the chargers capability returns and counts that. Authenticated with nothing on the account is degraded rather than ok, following Greencell's rule: the half we address answers, and the empty half is the account or the country that picks the regional server, so the message says so instead of reporting a healthy connection to nothing. A count reached with some view missing says how many views stayed silent, because the number is then a floor rather than a total. The web panel colours degraded amber, as it already did for Greencell. Co-Authored-By: Claude Opus 5 --- .../plugins/builtin/ankersolix/ankersolix.go | 26 ++++++++++--- .../plugins/builtin/ankersolix/chargers.go | 39 ++++++++++++++----- Web App/web/src/views/Settings.vue | 4 +- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/API Server/internal/plugins/builtin/ankersolix/ankersolix.go b/API Server/internal/plugins/builtin/ankersolix/ankersolix.go index adabbb5..12d08f9 100644 --- a/API Server/internal/plugins/builtin/ankersolix/ankersolix.go +++ b/API Server/internal/plugins/builtin/ankersolix/ankersolix.go @@ -238,17 +238,33 @@ func (p *Plugin) Init(_ context.Context, config map[string]string) error { return nil } -// HealthCheck logs in (if needed) and lists the account's EV chargers. +// HealthCheck logs in (if needed) and counts the account's EV chargers. It takes +// the same inventory the chargers capability returns rather than the standalone +// list alone: that list omits every charger that belongs to a system, so probing +// it reported "0 chargers" for an account whose chargers the panel was, at the +// same moment, listing. +// +// Authenticated but holding no charger is degraded, not down — the half we +// address works, and the missing half is the account (or the country, which +// picks the regional server). func (p *Plugin) HealthCheck(ctx context.Context) plugins.Health { start := time.Now() - body, err := p.apiRequest(ctx, epStandaloneChargers, map[string]any{}) + doc, err := p.chargerInventory(ctx) lat := time.Since(start).Milliseconds() if err != nil { return plugins.Health{Status: plugins.StatusDown, LatencyMs: lat, Detail: shorten(err.Error())} } - detail := "authenticated; EV chargers reachable" - if n, ok := countChargers(body); ok { - detail = fmt.Sprintf("authenticated; %d EV charger(s) bound to account", n) + if doc.Count == 0 { + detail := "authenticated; no EV charger on the account — if it does own one, check the country setting, which picks the Anker server" + if len(doc.Warnings) > 0 { + detail = "authenticated; no EV charger found: " + strings.Join(doc.Warnings, "; ") + } + return plugins.Health{Status: plugins.StatusDegraded, LatencyMs: lat, Detail: shorten(detail)} + } + detail := fmt.Sprintf("authenticated; %d EV charger(s) on the account", doc.Count) + if len(doc.Warnings) > 0 { + // Some view failed; the count still stands, but say it is a floor. + detail += fmt.Sprintf(" (%d of the cloud's views did not answer)", len(doc.Warnings)) } return plugins.Health{Status: plugins.StatusOK, LatencyMs: lat, Detail: detail} } diff --git a/API Server/internal/plugins/builtin/ankersolix/chargers.go b/API Server/internal/plugins/builtin/ankersolix/chargers.go index 9aea526..7ea45d1 100644 --- a/API Server/internal/plugins/builtin/ankersolix/chargers.go +++ b/API Server/internal/plugins/builtin/ankersolix/chargers.go @@ -190,10 +190,32 @@ func (p *Plugin) siteList(ctx context.Context) ([]siteRef, error) { return out, nil } -// accountChargers lists every EV charger on the account by merging the views +// chargersDoc is what the chargers capability answers, and what the health +// check counts. boundCount is the standalone view's own tally of the account's +// chargers, which is not the same number as len(chargers) — it misses whatever +// only the site and bound-device views can see, so it is reported alongside +// rather than instead. +type chargersDoc struct { + Chargers []accountCharger `json:"chargers"` + Count int `json:"count"` + BoundCount *int `json:"boundCount,omitempty"` + Warnings []string `json:"warnings,omitempty"` + Detail string `json:"detail,omitempty"` +} + +// accountChargers is the chargers capability: the inventory as a JSON document. +func (p *Plugin) accountChargers(ctx context.Context) (json.RawMessage, error) { + doc, err := p.chargerInventory(ctx) + if err != nil { + return nil, err + } + return json.Marshal(doc) +} + +// chargerInventory lists every EV charger on the account by merging the views // described at the top of this file. A view that fails is recorded as a warning // and the others still answer; only losing all of them is an error. -func (p *Plugin) accountChargers(ctx context.Context) (json.RawMessage, error) { +func (p *Plugin) chargerInventory(ctx context.Context) (chargersDoc, error) { inv := newInventory() var warnings []string views, failed := 0, 0 @@ -242,23 +264,20 @@ func (p *Plugin) accountChargers(ctx context.Context) (json.RawMessage, error) { } if failed == views { - return nil, fmt.Errorf("anker-solix: chargers: every cloud view failed: %s", strings.Join(warnings, "; ")) + return chargersDoc{}, fmt.Errorf("anker-solix: chargers: every cloud view failed: %s", strings.Join(warnings, "; ")) } out := inv.list() - doc := map[string]any{"chargers": out, "count": len(out)} + doc := chargersDoc{Chargers: out, Count: len(out), Warnings: warnings} if boundCount >= 0 { - doc["boundCount"] = boundCount - } - if len(warnings) > 0 { - doc["warnings"] = warnings + doc.BoundCount = &boundCount } if len(out) == 0 { // An account that owns chargers but exposes none through any view is // almost always pointed at the wrong regional server. - doc["detail"] = "No EV charger was returned by any view. If the account does own one, check the country setting — it selects the Anker server, and the wrong one logs in but shows nothing." + doc.Detail = "No EV charger was returned by any view. If the account does own one, check the country setting — it selects the Anker server, and the wrong one logs in but shows nothing." } - return json.Marshal(doc) + return doc, nil } // setChargerStatus records a raw operating-state code and its name; the first diff --git a/Web App/web/src/views/Settings.vue b/Web App/web/src/views/Settings.vue index c7fdb52..0198511 100644 --- a/Web App/web/src/views/Settings.vue +++ b/Web App/web/src/views/Settings.vue @@ -1474,7 +1474,9 @@ onBeforeUnmount(() => {

{{ ankerHealth.detail || ankerHealth.status }}