From e04b1e28bc5e4418cf2bc593fa55c632affb8a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Fri, 30 Jun 2023 16:49:31 +0200 Subject: [PATCH] Easee: wait for async current update on resume (#8588) --- charger/easee.go | 119 +++++++++++++++++++++++++-------------- charger/easee/signalr.go | 20 ++++++- 2 files changed, 95 insertions(+), 44 deletions(-) diff --git a/charger/easee.go b/charger/easee.go index 07c32fac0..e0a766b68 100644 --- a/charger/easee.go +++ b/charger/easee.go @@ -24,7 +24,6 @@ import ( "fmt" "net/http" "os" - "strconv" "strings" "sync" "time" @@ -51,16 +50,16 @@ type Easee struct { done chan struct{} dynamicChargerCurrent float64 current float64 - currentUpdated time.Time chargerEnabled bool smartCharging bool opMode int phaseMode int currentPower, sessionEnergy, totalEnergy, currentL1, currentL2, currentL3 float64 - rfid string - lp loadpoint.API - respChan chan easee.SignalRCommandResponse + rfid string + lp loadpoint.API + cmdC chan easee.SignalRCommandResponse + obsC chan easee.Observation } func init() { @@ -98,12 +97,13 @@ 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{}), - respChan: make(chan easee.SignalRCommandResponse), + Helper: request.NewHelper(log), + charger: charger, + log: log, + current: 6, // default current + done: make(chan struct{}), + cmdC: make(chan easee.SignalRCommandResponse), + obsC: make(chan easee.Observation), } c.Client.Timeout = timeout @@ -250,33 +250,15 @@ func (c *Easee) ProductUpdate(i json.RawMessage) { return } - var ( - value interface{} - err error - ) - - switch res.DataType { - case easee.Boolean: - value = res.Value == "1" - case easee.Double: - value, err = strconv.ParseFloat(res.Value, 64) - if err != nil { - c.log.ERROR.Println(err) - return - } - case easee.Integer: - value, err = strconv.Atoi(res.Value) - if err != nil { - c.log.ERROR.Println(err) - return - } - case easee.String: - value = res.Value + value, err := res.TypedValue() + if err != nil { + c.log.ERROR.Println(err) + return } // https://github.com/evcc-io/evcc/issues/8009 // logging might be slow or block, execute outside lock - c.log.TRACE.Printf("ProductUpdate %s: %s %v", res.Mid, res.ID, value) + c.log.TRACE.Printf("ProductUpdate %s: (%v) %s %v", res.Mid, res.Timestamp, res.ID, value) c.mux.Lock() defer c.mux.Unlock() @@ -311,15 +293,14 @@ func (c *Easee) ProductUpdate(i json.RawMessage) { c.phaseMode = value.(int) case easee.DYNAMIC_CHARGER_CURRENT: c.dynamicChargerCurrent = value.(float64) - - // ensure that charger current matches evcc's expectation - if c.dynamicChargerCurrent > 0 && c.dynamicChargerCurrent != c.current && - time.Since(c.currentUpdated) > 10*time.Second { - c.log.DEBUG.Printf("current mismatch, expected %.1f, got %.1f", c.current, c.dynamicChargerCurrent) - } case easee.CHARGER_OP_MODE: c.opMode = value.(int) } + + select { + case c.obsC <- res: + default: + } } // ChargerUpdate implements the signalr receiver @@ -338,7 +319,7 @@ func (c *Easee) CommandResponse(i json.RawMessage) { c.log.TRACE.Printf("CommandResponse %s: %+v", res.SerialNumber, res) select { - case c.respChan <- res: + case c.cmdC <- res: default: } } @@ -412,16 +393,29 @@ func (c *Easee) Enable(enable bool) error { // resume/stop charger action := easee.ChargePause + targetCurrent := 0.0 if enable { action = easee.ChargeResume + targetCurrent = 32 } + c.log.DEBUG.Printf("send command: %s", action) uri := fmt.Sprintf("%s/chargers/%s/commands/%s", easee.API, c.charger, action) if err := c.postJSONAndWait(uri, nil); err != nil { return err } + if err := c.waitForDynamicChargerCurrent(targetCurrent); err != nil { + return err + } + + c.mux.Lock() + dynamicChargerCurrent := c.dynamicChargerCurrent + c.mux.Unlock() + + c.log.DEBUG.Printf("DCC update received, current: %.3f, dynamicChargerCurrent: %.3f", c.current, dynamicChargerCurrent) if enable { + c.log.DEBUG.Printf("send enable, reset current: %.3f", c.current) // reset currents after enable, as easee automatically resets to maxA return c.MaxCurrent(int64(c.current)) } @@ -471,16 +465,52 @@ func (c *Easee) postJSONAndWait(uri string, data any) error { } func (c *Easee) waitForTickResponse(expectedTick int64) error { + c.log.TRACE.Printf("wait for tick response: %d", expectedTick) for { select { - case cmdResp := <-c.respChan: + case cmdResp := <-c.cmdC: if cmdResp.Ticks == expectedTick { if !cmdResp.WasAccepted { return fmt.Errorf("command rejected: %d", cmdResp.Ticks) } + c.log.TRACE.Printf("received tick response: %d", cmdResp.Ticks) return nil } case <-time.After(10 * time.Second): + c.log.TRACE.Printf("tick response timed out: %d", expectedTick) + return api.ErrTimeout + } + } +} + +// wait for up to 3s for current become targetCurrent +func (c *Easee) waitForDynamicChargerCurrent(targetCurrent float64) error { + c.log.DEBUG.Printf("wait for DCC update: %.3f", targetCurrent) + + // check any updates received meanwhile + c.mux.Lock() + if c.dynamicChargerCurrent == targetCurrent { + c.mux.Unlock() + return nil + } + c.mux.Unlock() + + timer := time.NewTimer(10 * time.Second) + for { + select { + case obs := <-c.obsC: + if obs.ID != easee.DYNAMIC_CHARGER_CURRENT { + continue + } + value, err := obs.TypedValue() + if err != nil { + continue + } + c.log.TRACE.Printf("DCC update received: %.3f (want: %.3f)", value.(float64), targetCurrent) + if value.(float64) == targetCurrent { + return nil + } + case <-timer.C: // time is up, bail return api.ErrTimeout } } @@ -498,10 +528,13 @@ func (c *Easee) MaxCurrent(current int64) error { return err } + if err := c.waitForDynamicChargerCurrent(float64(current)); err != nil { + return err + } + c.mux.Lock() defer c.mux.Unlock() c.current = cur - c.currentUpdated = time.Now() return nil } diff --git a/charger/easee/signalr.go b/charger/easee/signalr.go index cb8d825f4..a326190f8 100644 --- a/charger/easee/signalr.go +++ b/charger/easee/signalr.go @@ -1,6 +1,9 @@ package easee -import "time" +import ( + "strconv" + "time" +) type Observation struct { Mid string @@ -10,6 +13,21 @@ type Observation struct { Value string } +func (o *Observation) TypedValue() (interface{}, error) { + switch o.DataType { + case Boolean: + return o.Value == "1", nil + case Double: + return strconv.ParseFloat(o.Value, 64) + case Integer: + return strconv.Atoi(o.Value) + case String: + fallthrough + default: + return o.Value, nil + } +} + type SignalRCommandResponse struct { SerialNumber string ID int