Wakeup sleeping cars using api or charger (#2265)
This commit is contained in:
parent
7fbefb08f7
commit
c18eb34df6
8 changed files with 169 additions and 13 deletions
|
|
@ -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, ...
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
57
core/timer.go
Normal 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
30
core/timer_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue