package ankersolix // The account's other views of one charger. // // The merged inventory (chargers.go) answers "what chargers are there", and it // asks the views that list them. Four more endpoints answer only when a serial // is named: the station record the app opens on a charger, its cumulative // charging totals, which OCPP backend it is pointed at, and the RFID cards // authorised on it. None of them lists a charger, so none belongs in the merge — // and none of them was reachable from the app at all until this capability. // // What they answer with is not documented, by Anker or by the reference: the // endpoints are known, their payloads are not. So each view is relayed as the // fields it actually sent, flattened under the cloud's own keys, rather than // projected onto names invented here. A field that turns out to matter can be // named later, from evidence. import ( "context" "encoding/json" "fmt" ) // chargerDetailView is one endpoint's answer about one charger. A view that // fails carries its reason instead of its fields: an account that is not the // owner cannot read the cards, which is a fact about the account rather than an // error in the read. type chargerDetailView struct { ID string `json:"id"` Attrs map[string]string `json:"attrs,omitempty"` Error string `json:"error,omitempty"` } type chargerDetailsDoc struct { SN string `json:"sn"` Views []chargerDetailView `json:"views"` } // chargerDetails asks every per-charger endpoint and returns what each answered. // One failing view is reported in place; only losing all of them is an error, // for the same reason the inventory works that way — a charger the cloud will // half talk about is still worth showing. func (p *Plugin) chargerDetails(ctx context.Context, sn string) (json.RawMessage, error) { if _, err := p.ensureToken(ctx); err != nil { return nil, err } views := []struct { id string endpoint string payload map[string]any }{ {"station", epStationInfo, map[string]any{"evChargerSn": sn, "featuretype": 1}}, {"totals", epChargeStats, map[string]any{ "device_sn": sn, "date_type": "all", "start_date": "", "end_date": ""}}, {"ocpp", epOcppInfo, map[string]any{"device_sn": sn}}, {"rfid", epRfidCards, map[string]any{"device_sn": sn}}, } doc := chargerDetailsDoc{SN: sn, Views: make([]chargerDetailView, 0, len(views))} failed := 0 for _, v := range views { out := chargerDetailView{ID: v.id} body, err := p.apiRequest(ctx, v.endpoint, v.payload) if err != nil { out.Error, failed = shorten(err.Error()), failed+1 } else { out.Attrs = map[string]string{} flattenInto(out.Attrs, "", dataValue(body)) } doc.Views = append(doc.Views, out) } if failed == len(views) { return nil, fmt.Errorf("anker-solix: charger %s: no per-charger view answered", sn) } return json.Marshal(doc) } // dataValue returns a response's "data", whatever shape it came in: these views // answer with an object, and the card list answers with an array. func dataValue(body []byte) any { var env struct { Data any `json:"data"` } if err := json.Unmarshal(body, &env); err != nil { return nil } return env.Data }