diff --git a/charger/easee.go b/charger/easee.go index b53b03f2d7..b3f1e3103a 100644 --- a/charger/easee.go +++ b/charger/easee.go @@ -25,6 +25,7 @@ import ( "net/http" "os" "strconv" + "strings" "sync" "time" @@ -57,8 +58,9 @@ type Easee struct { phaseMode int currentPower, sessionEnergy, totalEnergy, currentL1, currentL2, currentL3 float64 - rfid string - lp loadpoint.API + rfid string + lp loadpoint.API + respChan chan easee.SignalRCommandResponse } func init() { @@ -96,11 +98,12 @@ func NewEasee(user, password, charger string, timeout time.Duration) (*Easee, er } c := &Easee{ - Helper: request.NewHelper(log), - charger: charger, - log: log, - current: 6, // default current - done: make(chan struct{}), + Helper: request.NewHelper(log), + charger: charger, + log: log, + current: 6, // default current + done: make(chan struct{}), + respChan: make(chan easee.SignalRCommandResponse), } c.Client.Timeout = timeout @@ -320,6 +323,11 @@ func (c *Easee) CommandResponse(i json.RawMessage) { return } c.log.TRACE.Printf("CommandResponse %s: %+v", res.SerialNumber, res) + + select { + case c.respChan <- res: + default: + } } func (c *Easee) chargers() ([]easee.Charger, error) { @@ -379,11 +387,9 @@ func (c *Easee) Enable(enable bool) error { } uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger) - resp, err := c.Post(uri, request.JSONContent, request.MarshalJSON(data)) - if err != nil { + if err := c.postJSONAndWait(uri, data); err != nil { return err } - resp.Body.Close() } // resume/stop charger @@ -391,10 +397,74 @@ func (c *Easee) Enable(enable bool) error { if enable { action = easee.ChargeResume } + uri := fmt.Sprintf("%s/chargers/%s/commands/%s", easee.API, c.charger, action) - _, err := c.Post(uri, request.JSONContent, nil) + if err := c.postJSONAndWait(uri, nil); err != nil { + return err + } - return err + if enable { + // reset currents after enable, as easee automatically resets to maxA + return c.MaxCurrent(int64(c.current)) + } + + return nil +} + +// posts JSON to the Easee API endpoint and waits for the async response +func (c *Easee) postJSONAndWait(uri string, data any) error { + resp, err := c.Post(uri, request.JSONContent, request.MarshalJSON(data)) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { //sync call + return nil + } + + if resp.StatusCode == 202 { //async call, wait for response + var cmd easee.RestCommandResponse + + if strings.Contains(uri, "/commands/") { //command endpoint + if err := json.NewDecoder(resp.Body).Decode(&cmd); err != nil { + return err + } + } else { //settings endpoint + var cmdArr []easee.RestCommandResponse + if err := json.NewDecoder(resp.Body).Decode(&cmdArr); err != nil { + return err + } + + if len(cmdArr) != 0 { + cmd = cmdArr[0] + } + } + + if cmd.Ticks == 0 { //Easee API thinks this was a noop + return nil + } + return c.waitForTickResponse(cmd.Ticks) + } + + // all other response codes lead to an error + return fmt.Errorf("invalid status: %d", resp.StatusCode) +} + +func (c *Easee) waitForTickResponse(expectedTick int64) error { + for { + select { + case cmdResp := <-c.respChan: + if cmdResp.Ticks == expectedTick { + if !cmdResp.WasAccepted { + return fmt.Errorf("command rejected: %d", cmdResp.Ticks) + } + return nil + } + case <-time.After(10 * time.Second): + return api.ErrTimeout + } + } } // MaxCurrent implements the api.Charger interface @@ -405,21 +475,16 @@ func (c *Easee) MaxCurrent(current int64) error { } uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger) - resp, err := c.Post(uri, request.JSONContent, request.MarshalJSON(data)) - if err == nil { - resp.Body.Close() - if resp.StatusCode == 202 && resp.ContentLength <= 2 { - // no tick id, Easee effectively ignored this update - return api.ErrMustRetry - } - - c.mux.Lock() - defer c.mux.Unlock() - c.current = cur - c.currentUpdated = time.Now() + if err := c.postJSONAndWait(uri, data); err != nil { + return err } - return err + c.mux.Lock() + defer c.mux.Unlock() + c.current = cur + c.currentUpdated = time.Now() + + return nil } var _ api.PhaseSwitcher = (*Easee)(nil) @@ -456,10 +521,7 @@ func (c *Easee) Phases1p3p(phases int) error { data.DynamicCircuitCurrentP3 = &max3 } - var resp *http.Response - if resp, err = c.Post(uri, request.JSONContent, request.MarshalJSON(data)); err == nil { - resp.Body.Close() - } + err = c.postJSONAndWait(uri, data) } else { // charger level if phases == 3 { @@ -474,10 +536,7 @@ func (c *Easee) Phases1p3p(phases int) error { uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger) - var resp *http.Response - if resp, err = c.Post(uri, request.JSONContent, request.MarshalJSON(data)); err == nil { - resp.Body.Close() - } + err = c.postJSONAndWait(uri, data) } } @@ -553,10 +612,8 @@ func (c *Easee) updateSmartCharging() { } uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger) - req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) - if err == nil { - _, err = c.DoBody(req) - } + + err := c.postJSONAndWait(uri, data) if err != nil { c.log.WARN.Printf("smart charging: %v", err) }