Wakeup sleeping cars using api or charger (#2265)

This commit is contained in:
Chris591 2022-01-31 19:37:27 +01:00 • committed by GitHub
parent 7fbefb08f7
commit c18eb34df6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 169 additions and 13 deletions

View file

@ -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, ...

View file

@ -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()

View file

@ -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,

57
core/timer.go Normal file
View file

@ -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
core/timer_test.go Normal file
View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

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