diff --git a/core/loadpoint.go b/core/loadpoint.go index b76e73283..6f5c98c00 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1800,9 +1800,10 @@ func (lp *Loadpoint) publishSocAndRange() { // https://github.com/evcc-io/evcc/issues/16180 socEstimator := lp.socEstimator - socAndLimit := func(typ string, dev any) (*float64, *int64) { + socAndLimit := func(typ string, dev any) (*float64, *int64, error) { var socR *float64 var limitR *int64 + var socErr error if battery, ok := api.Cap[api.Battery](dev); ok { if soc, err := soc.Guard(battery.Soc()); err == nil { @@ -1822,18 +1823,28 @@ func (lp *Loadpoint) publishSocAndRange() { lp.log.ERROR.Printf("%s soc limit: %v", typ, err) } } - } else if !loadpoint.AcceptableError(err) { - lp.log.ERROR.Printf("charger soc: %v", err) + } else { + socErr = err + + if !loadpoint.AcceptableError(err) { + lp.log.ERROR.Printf("charger soc: %v", err) + } } } - return socR, limitR + return socR, limitR, socErr } - socR, limitR := socAndLimit("charger", lp.charger) + socR, limitR, _ := socAndLimit("charger", lp.charger) if socR == nil && (lp.vehicleSocPollAllowed() || lp.chargerHasFeature(api.IntegratedDevice)) { - lp.socUpdated = lp.clock.Now() - socR, limitR = socAndLimit("vehicle", lp.GetVehicle()) + var socErr error + socR, limitR, socErr = socAndLimit("vehicle", lp.GetVehicle()) + + // keep polling async vehicle APIs that return ErrMustRetry until a SoC + // arrives, instead of waiting for the next interval + if !errors.Is(socErr, api.ErrMustRetry) { + lp.socUpdated = lp.clock.Now() + } // range if vs, ok := api.Cap[api.VehicleRange](lp.GetVehicle()); ok { diff --git a/vehicle/saic/api.go b/vehicle/saic/api.go index 1412d0821..4364eb1f7 100644 --- a/vehicle/saic/api.go +++ b/vehicle/saic/api.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "sync" "time" "github.com/evcc-io/evcc/api" @@ -13,28 +14,21 @@ import ( "github.com/evcc-io/evcc/vehicle/saic/requests" ) -const ( - StatRunning = iota - StatValid - StatInvalid -) - const ( RegionEU = "https://gateway-mg-eu.soimt.com/api.app/v1/" RegionAU = "https://gateway-mg-au.soimt.com/api.app/v1/" ) -type ConcurrentRequest struct { - Status int - Result requests.ChargeStatus -} - // API is an api.Vehicle implementation for SAIC cars type API struct { *request.Helper identity *Identity - request ConcurrentRequest log *util.Logger + + mu sync.Mutex + running bool // background refresh in progress + valid bool // result holds a valid response + result requests.ChargeStatus // last valid response } // NewAPI creates a new vehicle @@ -49,19 +43,34 @@ func NewAPI(log *util.Logger, identity *Identity) *API { Decorator: requests.Decorate, Base: v.Client.Transport, } - v.request.Status = StatInvalid return v } +// store saves a valid response and clears the running flag +func (v *API) store(res requests.ChargeStatus) { + v.mu.Lock() + defer v.mu.Unlock() + v.result, v.valid, v.running = res, true, false +} + +// last returns the last valid response, or ErrMustRetry until one is available +func (v *API) last() (requests.ChargeStatus, error) { + v.mu.Lock() + defer v.mu.Unlock() + if v.valid { + return v.result, nil + } + return v.result, api.ErrMustRetry +} + func (v *API) doRepeatedRequest(path string, event_id string) error { token, err := v.identity.Token() if err != nil { - v.request.Status = StatInvalid return err } - req, err := requests.CreateRequest( + req, _ := requests.CreateRequest( v.identity.baseUrl, path, http.MethodGet, @@ -69,40 +78,29 @@ func (v *API) doRepeatedRequest(path string, event_id string) error { request.JSONContent, token.AccessToken, event_id) - if err != nil { - v.request.Status = StatInvalid - return err - } var res requests.Answer[requests.ChargeStatus] - _, err = doRequest(v, req, &res) - if err == nil { - v.request = ConcurrentRequest{ - Status: StatValid, - Result: res.Data, - } - } else if err != api.ErrMustRetry { - v.request.Status = StatInvalid + if _, err = doRequest(v, req, &res); err == nil { + v.store(res.Data) } return err } -// This is running concurrently +// repeatRequest polls for the deferred answer in the background func (v *API) repeatRequest(path string, event_id string) { - var count int + // always clear running so a future query can start + defer func() { + v.mu.Lock() + v.running = false + v.mu.Unlock() + }() - v.request.Status = StatRunning - for err := api.ErrMustRetry; err == api.ErrMustRetry && count < 20; { + for count := range 20 { time.Sleep(2 * time.Second) - v.log.TRACE.Printf("Starting repeated query. Count: %d", count) - err = v.doRepeatedRequest(path, event_id) - count++ - } - - // Make sure that we don't exit here with status running (probably after 20 tries). - // This would not allow us to ever do a query again. - if v.request.Status == StatRunning { - v.request.Status = StatInvalid + v.log.TRACE.Printf("repeated query %d", count) + if err := v.doRepeatedRequest(path, event_id); err != api.ErrMustRetry { + return + } } } @@ -178,18 +176,14 @@ func (v *API) Wakeup(vin string) error { func (v *API) Status(vin string) (requests.ChargeStatus, error) { var zero requests.ChargeStatus - // Check if we are already running in the background - if v.request.Status == StatValid { - v.request.Status = StatInvalid - v.log.TRACE.Printf("StatValid. Returning stored value") - // v.printAnswer() - return v.request.Result, nil + // refresh in progress, return last known value + v.mu.Lock() + running := v.running + v.mu.Unlock() + if running { + v.log.TRACE.Printf("query running") + return v.last() } - if v.request.Status == StatRunning { - v.log.TRACE.Printf("StatRunning. Exiting") - return zero, api.ErrMustRetry - } - v.log.TRACE.Printf("StatInvalid. Starting query") token, err := v.identity.Token() if err != nil { @@ -198,7 +192,7 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) { path := "vehicle/charging/mgmtData?vin=" + requests.Sha256(vin) // get charging status of vehicle - req, err := requests.CreateRequest( + req, _ := requests.CreateRequest( v.identity.baseUrl, path, http.MethodGet, @@ -206,9 +200,6 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) { request.JSONContent, token.AccessToken, "") - if err != nil { - return zero, err - } var res requests.Answer[requests.ChargeStatus] event_id, err := doRequest(v, req, &res) @@ -217,11 +208,11 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) { } if event_id == "" { - v.log.TRACE.Printf("Answer without event ID") - return zero, api.ErrMustRetry + v.log.TRACE.Printf("answer without event id") + return v.last() } - req, err = requests.CreateRequest( + req, _ = requests.CreateRequest( v.identity.baseUrl, path, http.MethodGet, @@ -229,18 +220,19 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) { request.JSONContent, token.AccessToken, event_id) - if err != nil { + + // answer not yet available, keep polling in the background + if _, err = doRequest(v, req, &res); err == api.ErrMustRetry { + v.mu.Lock() + v.running = true + v.mu.Unlock() + v.log.TRACE.Printf("no answer yet, continuing in background") + go v.repeatRequest(path, event_id) + return v.last() + } else if err != nil { return zero, err } - _, err = doRequest(v, req, &res) - - // Continue checking.... - if err == api.ErrMustRetry { - v.request.Status = StatRunning - v.log.TRACE.Printf(" No answer yet. Continue status query in background") - go v.repeatRequest(path, event_id) - } - - return res.Data, err + v.store(res.Data) + return res.Data, nil }