Skip to content

Commit

Permalink
Wakeup sleeping cars using api or charger (evcc-io#2265)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris591 authored Jan 31, 2022
1 parent 7fbefb0 commit c18eb34
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 13 deletions.
5 changes: 5 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...
Expand Down
60 changes: 54 additions & 6 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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{}
}
Expand Down Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions core/loadpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
57 changes: 57 additions & 0 deletions core/timer.go
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 30 additions & 0 deletions core/timer_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
7 changes: 0 additions & 7 deletions vehicle/ford.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
9 changes: 9 additions & 0 deletions vehicle/ford/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions vehicle/ford/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand Down Expand Up @@ -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()
}

0 comments on commit c18eb34

Please sign in to comment.