From c18eb34df6e32f47aa59e56389a642a8d2797418 Mon Sep 17 00:00:00 2001 From: Chris591 <39920986+Chris591@users.noreply.github.com> Date: Mon, 31 Jan 2022 19:37:27 +0100 Subject: [PATCH] Wakeup sleeping cars using api or charger (#2265) --- api/api.go | 5 ++++ core/loadpoint.go | 60 ++++++++++++++++++++++++++++++++++++---- core/loadpoint_test.go | 4 +++ core/timer.go | 57 ++++++++++++++++++++++++++++++++++++++ core/timer_test.go | 30 ++++++++++++++++++++ vehicle/ford.go | 7 ----- vehicle/ford/api.go | 9 ++++++ vehicle/ford/provider.go | 10 +++++++ 8 files changed, 169 insertions(+), 13 deletions(-) create mode 100644 core/timer.go create mode 100644 core/timer_test.go diff --git a/api/api.go b/api/api.go index 7e5e5e100..385b11404 100644 --- a/api/api.go +++ b/api/api.go @@ -187,6 +187,11 @@ type VehicleStopCharge interface { StopCharge() error } +// AlarmClock provides wakeup calls to the vehicle with an API call or a CP interrupt from the charger +type AlarmClock interface { + WakeUp() error +} + type Tariff interface { IsCheap() (bool, error) CurrentPrice() (float64, error) // EUR/kWh, CHF/kWh, ... diff --git a/core/loadpoint.go b/core/loadpoint.go index 54cad5b31..d8017782c 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -142,6 +142,7 @@ type LoadPoint struct { connectedTime time.Time // Time when vehicle was connected pvTimer time.Time // PV enabled/disable timer phaseTimer time.Time // 1p3p switch timer + wakeUpTimer *Timer // Vehicle wake-up timeout // charge progress vehicleSoc float64 // Vehicle SoC @@ -335,6 +336,9 @@ func (lp *LoadPoint) configureChargerType(charger api.Charger) { _ = lp.bus.Subscribe(evChargeStop, ct.StopCharge) lp.chargeTimer = ct } + + // add wakeup timer + lp.wakeUpTimer = NewTimer() } // pushEvent sends push messages to clients @@ -354,6 +358,8 @@ func (lp *LoadPoint) evChargeStartHandler() { lp.log.INFO.Println("start charging ->") lp.pushEvent(evChargeStart) + lp.wakeUpTimer.Stop() + // soc update reset lp.socUpdated = time.Time{} } @@ -427,9 +433,7 @@ func (lp *LoadPoint) evVehicleDisconnectHandler() { if len(lp.vehicles) == 1 { // but reset values if poll mode is not always (i.e. connected or charging) if lp.SoC.Poll.Mode != pollAlways { - lp.publish("vehicleSoC", -1) - lp.publish("vehicleRange", -1) - lp.setRemainingDuration(-1) + lp.unpublishVehicle() } } @@ -606,7 +610,7 @@ func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) error { return nil } - // sleep vehicle + // remote stop // TODO https://github.com/evcc-io/evcc/discussions/1929 // if car, ok := lp.vehicle.(api.VehicleStopCharge); !enabled && ok { // // log but don't propagate @@ -625,7 +629,16 @@ func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) error { lp.bus.Publish(evChargeCurrent, chargeCurrent) - // wake up vehicle + // start/stop vehicle wake-up timer + if enabled { + lp.log.DEBUG.Printf("wake-up timer: start") + lp.wakeUpTimer.Start() + } else { + lp.log.DEBUG.Printf("wake-up timer: stop") + lp.wakeUpTimer.Stop() + } + + // remote start // TODO https://github.com/evcc-io/evcc/discussions/1929 // if car, ok := lp.vehicle.(api.VehicleStartCharge); enabled && ok { // // log but don't propagate @@ -807,8 +820,37 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) { lp.publish("vehicleCapacity", int64(0)) } + lp.unpublishVehicle() +} + +func (lp *LoadPoint) wakeUpVehicle() { + // charger + if c, ok := lp.charger.(api.AlarmClock); ok { + if err := c.WakeUp(); err != nil { + lp.log.ERROR.Printf("wake-up charger: %v", err) + } + return + } + + // vehicle + if lp.vehicle != nil { + if vs, ok := lp.vehicle.(api.AlarmClock); ok { + if err := vs.WakeUp(); err != nil { + lp.log.ERROR.Printf("wake-up vehicle: %v", err) + } + } + } +} + +// unpublishVehicle resets published vehicle data +func (lp *LoadPoint) unpublishVehicle() { + lp.vehicleSoc = 0 + + lp.publish("vehicleSoC", 0.0) lp.publish("vehicleRange", int64(0)) lp.publish("vehicleOdometer", 0.0) + + lp.setRemainingDuration(-1) } // startVehicleDetection resets connection timer and starts api refresh timer @@ -1357,7 +1399,7 @@ func (lp *LoadPoint) publishSoCAndRange() { // range if vs, ok := lp.vehicle.(api.VehicleRange); ok { if rng, err := vs.Range(); err == nil { - lp.log.DEBUG.Printf("vehicle range: %vkm", rng) + lp.log.DEBUG.Printf("vehicle range: %dkm", rng) lp.publish("vehicleRange", rng) } } @@ -1511,6 +1553,12 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool, batteryBuffered bool) err = lp.setLimit(targetCurrent, required) } + // Wake-up checks + if lp.enabled && lp.status == api.StatusB && + int(lp.vehicleSoc) < lp.SoC.Target && lp.wakeUpTimer.Expired() { + lp.wakeUpVehicle() + } + // stop an active target charging session if not currently evaluated if !lp.socTimer.DemandValidated() { lp.socTimer.Stop() diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 387e07977..6f5e1be43 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -151,6 +151,7 @@ func TestUpdatePowerZero(t *testing.T) { chargeMeter: &Null{}, // silence nil panics chargeRater: &Null{}, // silence nil panics chargeTimer: &Null{}, // silence nil panics + wakeUpTimer: NewTimer(), MinCurrent: minA, MaxCurrent: maxA, Phases: 1, @@ -383,6 +384,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) { chargeRater: &Null{}, // silence nil panics chargeTimer: &Null{}, // silence nil panics progress: NewProgress(0, 10), // silence nil panics + wakeUpTimer: NewTimer(), // silence nil panics MinCurrent: minA, MaxCurrent: maxA, vehicle: vehicle, // needed for targetSoC check @@ -454,6 +456,7 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) { chargeMeter: &Null{}, // silence nil panics chargeRater: &Null{}, // silence nil panics chargeTimer: &Null{}, // silence nil panics + wakeUpTimer: NewTimer(), MinCurrent: minA, MaxCurrent: maxA, status: api.StatusC, @@ -525,6 +528,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { chargeMeter: &Null{}, // silence nil panics chargeRater: rater, chargeTimer: &Null{}, // silence nil panics + wakeUpTimer: NewTimer(), MinCurrent: minA, MaxCurrent: maxA, status: api.StatusC, diff --git a/core/timer.go b/core/timer.go new file mode 100644 index 000000000..d34889e85 --- /dev/null +++ b/core/timer.go @@ -0,0 +1,57 @@ +package core + +import ( + "sync" + "time" + + "github.com/benbjohnson/clock" +) + +const wakeupTimeout = 30 * time.Second + +// Timer measures active time between start and stop events +type Timer struct { + sync.Mutex + clck clock.Clock + started time.Time +} + +// NewTimer creates timer that can expire +func NewTimer() *Timer { + return &Timer{ + clck: clock.New(), + } +} + +// Start starts the timer if not started already +func (m *Timer) Start() { + m.Lock() + defer m.Unlock() + + if !m.started.IsZero() { + return + } + + m.started = m.clck.Now() +} + +// Reset resets the timer +func (m *Timer) Stop() { + m.Lock() + defer m.Unlock() + + m.started = time.Time{} +} + +// Expired checks if the timer has elapsed and if resets its status +func (m *Timer) Expired() bool { + m.Lock() + defer m.Unlock() + + res := !m.started.IsZero() && (m.clck.Since(m.started) >= wakeupTimeout) + if res { + m.started = time.Time{} + } + + return res +} diff --git a/core/timer_test.go b/core/timer_test.go new file mode 100644 index 000000000..3d953f86f --- /dev/null +++ b/core/timer_test.go @@ -0,0 +1,30 @@ +package core + +import ( + "testing" + "time" + + "github.com/benbjohnson/clock" + "github.com/stretchr/testify/require" +) + +func TestTimer(t *testing.T) { + at := NewTimer() + + clck := clock.NewMock() + at.clck = clck + + // start + at.Start() + clck.Add(20 * time.Second) + require.Equal(t, at.Expired(), false) + + // wait another 20 sec to expire the timer - this will reset the timer as well + clck.Add(20 * time.Second) + require.Equal(t, at.Expired(), true) + + // start + at.Start() + clck.Add(time.Minute) + require.Equal(t, at.Expired(), true) +} diff --git a/vehicle/ford.go b/vehicle/ford.go index f028dab4c..fdef337ab 100644 --- a/vehicle/ford.go +++ b/vehicle/ford.go @@ -14,13 +14,6 @@ import ( // Ford is an api.Vehicle implementation for Ford cars type Ford struct { *embed - // *request.Helper - // log *util.Logger - // vin string - // tokenSource oauth2.TokenSource - // statusG func() (interface{}, error) - // refreshId string - // refreshTime time.Time *ford.Provider } diff --git a/vehicle/ford/api.go b/vehicle/ford/api.go index c43d5a076..19300985b 100644 --- a/vehicle/ford/api.go +++ b/vehicle/ford/api.go @@ -99,3 +99,12 @@ func (v *API) RefreshRequest(vin string) (string, error) { return resp.CommandId, err } + +// WakeUp performs a wakeup request +func (v *API) WakeUp(vin string) error { + uri := fmt.Sprintf("%s/api/dashboard/v1/users/vehicles?wakeupVin=%s", TokenURI, vin) + + _, err := v.GetBody(uri) + + return err +} diff --git a/vehicle/ford/provider.go b/vehicle/ford/provider.go index dccc1fe98..1e965c36f 100644 --- a/vehicle/ford/provider.go +++ b/vehicle/ford/provider.go @@ -14,6 +14,7 @@ type Provider struct { expiry time.Duration refreshTime time.Time refreshId string + wakeup func() error } func NewProvider(api *API, vin string, expiry, cache time.Duration) *Provider { @@ -29,6 +30,8 @@ func NewProvider(api *API, vin string, expiry, cache time.Duration) *Provider { ) }, cache).InterfaceGetter() + impl.wakeup = func() error { return api.WakeUp(vin) } + return impl } @@ -136,3 +139,10 @@ func (v *Provider) Position() (float64, float64, error) { return 0, 0, err } + +var _ api.AlarmClock = (*Provider)(nil) + +// WakeUp implements the api.AlarmClock interface +func (v *Provider) WakeUp() error { + return v.wakeup() +}