Easee: fix stale power/current readings when charger goes offline (#28362)
This commit is contained in:
parent
708b1c88e2
commit
3261ad2075
2 changed files with 81 additions and 0 deletions
|
|
@ -40,6 +40,13 @@ import (
|
|||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// observationTimeout is the maximum age of the most recently received charger
|
||||
// observation before power and current readings are considered stale and zeroed.
|
||||
// The Easee charger sends a thermal heartbeat every ~15 minutes when idle, so
|
||||
// 20 minutes gives a comfortable margin above that while staying well below the
|
||||
// time a user would expect stale data to linger.
|
||||
const observationTimeout = 20 * time.Minute
|
||||
|
||||
// Easee charger implementation
|
||||
type Easee struct {
|
||||
*request.Helper
|
||||
|
|
@ -69,6 +76,7 @@ type Easee struct {
|
|||
expectedOrphans map[easee.ObservationID]int
|
||||
obsC chan easee.Observation
|
||||
obsTime map[easee.ObservationID]time.Time
|
||||
lastObsReceived time.Time
|
||||
startDone func()
|
||||
}
|
||||
|
||||
|
|
@ -350,6 +358,12 @@ func (c *Easee) ProductUpdate(i json.RawMessage) {
|
|||
|
||||
c.obsTime[res.ID] = res.Timestamp
|
||||
|
||||
// Update liveness timestamp for observations with a fresh charger-side timestamp.
|
||||
// Stale cloud replay on restart has old timestamps and must not refresh this.
|
||||
if time.Since(res.Timestamp) < observationTimeout {
|
||||
c.lastObsReceived = time.Now()
|
||||
}
|
||||
|
||||
switch res.ID {
|
||||
case easee.USER_IDTOKEN:
|
||||
c.rfid = res.Value
|
||||
|
|
@ -749,6 +763,10 @@ func (c *Easee) CurrentPower() (float64, error) {
|
|||
c.mux.RLock()
|
||||
defer c.mux.RUnlock()
|
||||
|
||||
if time.Since(c.lastObsReceived) > observationTimeout {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return c.currentPower, nil
|
||||
}
|
||||
|
||||
|
|
@ -767,6 +785,11 @@ var _ api.PhaseCurrents = (*Easee)(nil)
|
|||
func (c *Easee) Currents() (float64, float64, float64, error) {
|
||||
c.mux.RLock()
|
||||
defer c.mux.RUnlock()
|
||||
|
||||
if time.Since(c.lastObsReceived) > observationTimeout {
|
||||
return 0, 0, 0, nil
|
||||
}
|
||||
|
||||
return c.currentL1, c.currentL2, c.currentL3, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -560,6 +560,28 @@ func TestEasee_registerExpectedOrphan_multipleRegistrations(t *testing.T) {
|
|||
assert.False(t, e.consumeExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1))
|
||||
}
|
||||
|
||||
func TestProductUpdate_updatesLastObsReceived_freshTimestamp(t *testing.T) {
|
||||
e := newEasee()
|
||||
assert.True(t, e.lastObsReceived.IsZero())
|
||||
|
||||
// Observation with a fresh charger-side timestamp (seconds ago)
|
||||
now := time.Now().UTC().Truncate(0)
|
||||
e.ProductUpdate(createPayload(easee.TOTAL_POWER, now, easee.Double, "3.5"))
|
||||
|
||||
assert.False(t, e.lastObsReceived.IsZero())
|
||||
assert.WithinDuration(t, time.Now(), e.lastObsReceived, 5*time.Second)
|
||||
}
|
||||
|
||||
func TestProductUpdate_doesNotUpdateLastObsReceived_staleTimestamp(t *testing.T) {
|
||||
e := newEasee()
|
||||
|
||||
// Observation with a charger-side timestamp older than observationTimeout
|
||||
stale := time.Now().UTC().Add(-(observationTimeout + time.Minute))
|
||||
e.ProductUpdate(createPayload(easee.TOTAL_POWER, stale, easee.Double, "3.5"))
|
||||
|
||||
assert.True(t, e.lastObsReceived.IsZero(), "stale replay must not update lastObsReceived")
|
||||
}
|
||||
|
||||
func TestEasee_Phases1p3p_registersExpectedOrphan(t *testing.T) {
|
||||
const siteID = 12345
|
||||
const circuitID = 67890
|
||||
|
|
@ -600,3 +622,39 @@ func TestEasee_Phases1p3p_registersExpectedOrphan(t *testing.T) {
|
|||
e.cmdMu.Unlock()
|
||||
assert.Equal(t, 1, count, "expected orphan should be registered before the POST")
|
||||
}
|
||||
|
||||
func TestLivenessCheck_staleObservations(t *testing.T) {
|
||||
e := newEasee()
|
||||
e.opMode = easee.ModeCharging
|
||||
e.currentPower = 7280
|
||||
e.currentL1, e.currentL2, e.currentL3 = 16, 16, 16
|
||||
e.lastObsReceived = time.Now().Add(-(observationTimeout + time.Minute))
|
||||
|
||||
power, err := e.CurrentPower()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, float64(0), power, "expired observations: CurrentPower must return 0W")
|
||||
|
||||
l1, l2, l3, err := e.Currents()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, float64(0), l1)
|
||||
assert.Equal(t, float64(0), l2)
|
||||
assert.Equal(t, float64(0), l3)
|
||||
}
|
||||
|
||||
func TestLivenessCheck_freshObservations(t *testing.T) {
|
||||
e := newEasee()
|
||||
e.opMode = easee.ModeCharging
|
||||
e.currentPower = 7280
|
||||
e.currentL1, e.currentL2, e.currentL3 = 16, 16, 16
|
||||
e.lastObsReceived = time.Now()
|
||||
|
||||
power, err := e.CurrentPower()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, float64(7280), power)
|
||||
|
||||
l1, l2, l3, err := e.Currents()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, float64(16), l1)
|
||||
assert.Equal(t, float64(16), l2)
|
||||
assert.Equal(t, float64(16), l3)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue