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 }}