From d5262f4b4281b775882eb7fe23634ed684333736 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 12 Aug 2021 21:45:24 +0200 Subject: [PATCH] Add configurable actions to provide custom behaviour when vehicle is identified (#1329) --- core/coordinator.go | 8 +- core/coordinator_test.go | 2 +- core/loadpoint.go | 163 +++++++++++++++++++++++---------------- core/loadpoint_test.go | 19 +---- evcc.dist.yaml | 4 + 5 files changed, 106 insertions(+), 90 deletions(-) diff --git a/core/coordinator.go b/core/coordinator.go index d2ee716eb..8798ea1f4 100644 --- a/core/coordinator.go +++ b/core/coordinator.go @@ -40,14 +40,10 @@ func (lp *vehicleCoordinator) availableVehicles(owner interface{}, vehicles []ap } // find active vehicle by charge state -func (lp *vehicleCoordinator) findActiveVehicleByStatus(log *util.Logger, owner interface{}, vehicles []api.Vehicle) api.Vehicle { - var res api.Vehicle - +func (lp *vehicleCoordinator) identifyVehicleByStatus(log *util.Logger, owner interface{}, vehicles []api.Vehicle) api.Vehicle { available := lp.availableVehicles(owner, vehicles) - // log.DEBUG.Printf("!!available vehicles: %v", funk.Map(available, func(v api.Vehicle) string { - // return v.Title() - // })) + var res api.Vehicle for _, vehicle := range available { if vs, ok := vehicle.(api.ChargeState); ok { status, err := vs.Status() diff --git a/core/coordinator_test.go b/core/coordinator_test.go index f424aee63..8706e5552 100644 --- a/core/coordinator_test.go +++ b/core/coordinator_test.go @@ -48,7 +48,7 @@ func TestVehicleDetectByStatus(t *testing.T) { v1.MockVehicle.EXPECT().Title().Return("v1") v2.MockVehicle.EXPECT().Title().Return("v2") - res := c.findActiveVehicleByStatus(log, lp, vehicles) + res := c.identifyVehicleByStatus(log, lp, vehicles) if tc.res != res { t.Errorf("expected %v, got %v", tc.res, res) } diff --git a/core/loadpoint.go b/core/loadpoint.go index ce2c20b44..3761f15e1 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -64,6 +64,12 @@ type ThresholdConfig struct { Threshold float64 } +// ActionConfig defines an action to take on event +type ActionConfig struct { + Mode api.ChargeMode `mapstructure:"mode"` // Charge mode to apply when car disconnected + TargetSoC int `mapstructure:"targetSoC"` // Target SoC to apply when car disconnected +} + // LoadPoint is responsible for controlling charge depending on // SoC needs and power availability. type LoadPoint struct { @@ -86,11 +92,9 @@ type LoadPoint struct { Meters struct { ChargeMeterRef string `mapstructure:"charge"` // Charge meter reference } - SoC SoCConfig - OnDisconnect struct { - Mode api.ChargeMode `mapstructure:"mode"` // Charge mode to apply when car disconnected - TargetSoC int `mapstructure:"targetSoC"` // Target SoC to apply when car disconnected - } + SoC SoCConfig + OnDisconnect ActionConfig `mapstructure:"onDisconnect"` + OnIdentify map[string]ActionConfig `mapstructure:"onIdentify"` Enable, Disable ThresholdConfig MinCurrent float64 // PV mode: start current Min+PV mode: min current @@ -103,6 +107,7 @@ type LoadPoint struct { socUpdated time.Time // SoC updated timestamp (poll: connected) vehicleConnected time.Time // Vehicle connected timestamp vehicleConnectedTicker *clock.Ticker + vehicleID string charger api.Charger chargeTimer api.ChargeTimer @@ -358,12 +363,7 @@ func (lp *LoadPoint) evVehicleDisconnectHandler() { } // set default mode on disconnect - if lp.OnDisconnect.Mode != "" && lp.GetMode() != api.ModeOff { - lp.SetMode(lp.OnDisconnect.Mode) - } - if lp.OnDisconnect.TargetSoC != 0 { - _ = lp.SetTargetSoC(lp.OnDisconnect.TargetSoC) - } + lp.applyAction(lp.OnDisconnect) // soc update reset lp.socUpdated = time.Time{} @@ -394,6 +394,16 @@ func (lp *LoadPoint) evChargeCurrentWrappedMeterHandler(current float64) { lp.chargeMeter.(*wrapper.ChargeMeter).SetPower(power) } +// applyAction executes the action +func (lp *LoadPoint) applyAction(action ActionConfig) { + if action.Mode != "" && lp.GetMode() != api.ModeOff { + lp.SetMode(action.Mode) + } + if action.TargetSoC != 0 { + _ = lp.SetTargetSoC(action.TargetSoC) + } +} + // Name returns the human-readable loadpoint title func (lp *LoadPoint) Name() string { return lp.Title @@ -600,6 +610,68 @@ func (lp *LoadPoint) remoteControlled(demand RemoteDemand) bool { return lp.remoteDemand == demand } +// identifyVehicle reads vehicle identification from charger +func (lp *LoadPoint) identifyVehicle() { + identifier, ok := lp.charger.(api.Identifier) + if !ok { + return + } + + id, err := identifier.Identify() + if err != nil { + lp.log.ERROR.Println("charger vehicle id:", err) + return + } + + if lp.vehicleID == id { + return + } + + // vehicle found or removed + lp.vehicleID = id + + lp.log.DEBUG.Println("charger vehicle id:", id) + lp.publish("socIdentity", id) + + if id != "" { + if vehicle := lp.selectVehicleByID(id); vehicle != nil { + lp.setActiveVehicle(vehicle) + } + + if action, ok := lp.OnIdentify[id]; ok { + lp.log.DEBUG.Println("running vehicle action:", action) + lp.applyAction(action) + } + } +} + +// selectVehicleByID selects the vehicle with the given ID +func (lp *LoadPoint) selectVehicleByID(id string) api.Vehicle { + // find exact match + for _, vehicle := range lp.vehicles { + if vid, err := vehicle.Identify(); err == nil && vid == id { + return vehicle + } + } + + // find placeholder match + for _, vehicle := range lp.vehicles { + if vid, err := vehicle.Identify(); err == nil && vid != "" { + re, err := regexp.Compile(strings.ReplaceAll(vid, "*", ".*?")) + if err != nil { + lp.log.ERROR.Printf("vehicle id: %v", err) + continue + } + + if re.MatchString(id) { + return vehicle + } + } + } + + return nil +} + // setActiveVehicle assigns currently active vehicle and configures soc estimator func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) { if lp.vehicle == vehicle { @@ -639,9 +711,9 @@ func (lp *LoadPoint) startVehicleDetection() { lp.vehicleConnectedTicker = lp.clock.Ticker(vehicleDetectInterval) } -// vehicleIdentificationAllowed checks if loadpoint has multiple vehicles associated and starts discovery period -func (lp *LoadPoint) vehicleIdentificationAllowed() bool { - res := len(lp.vehicles) > 1 && lp.connected() && lp.clock.Since(lp.vehicleConnected) < vehicleDetectDuration +// vehicleUnidentified checks if loadpoint has multiple vehicles associated and starts discovery period +func (lp *LoadPoint) vehicleUnidentified() bool { + res := len(lp.vehicles) > 1 && lp.clock.Since(lp.vehicleConnected) < vehicleDetectDuration // request vehicle api refresh while waiting to identify if res { @@ -656,58 +728,13 @@ func (lp *LoadPoint) vehicleIdentificationAllowed() bool { return res } -// find active vehicle by id -func (lp *LoadPoint) findActiveVehicleByID() api.Vehicle { - if identifier, ok := lp.charger.(api.Identifier); ok { - id, err := identifier.Identify() - - if err != nil { - lp.log.ERROR.Println("charger vehicle id:", err) - return nil - } - - if id != "" { - lp.log.DEBUG.Println("charger vehicle id:", id) - - // find exact match - for _, vehicle := range lp.vehicles { - if vid, err := vehicle.Identify(); err == nil && vid == id { - return vehicle - } - } - - // find placeholder match - for _, vehicle := range lp.vehicles { - if vid, err := vehicle.Identify(); err == nil && vid != "" { - re, err := regexp.Compile(strings.ReplaceAll(vid, "*", ".*?")) - if err != nil { - lp.log.ERROR.Printf("vehicle identity: %v", err) - continue - } - - if re.MatchString(id) { - return vehicle - } - } - } - } - } - - return nil -} - -// findActiveVehicle validates if the active vehicle is still connected to the loadpoint -func (lp *LoadPoint) findActiveVehicle() { +// identifyVehicleByStatus validates if the active vehicle is still connected to the loadpoint +func (lp *LoadPoint) identifyVehicleByStatus() { if len(lp.vehicles) <= 1 { return } - if vehicle := lp.findActiveVehicleByID(); vehicle != nil { - lp.setActiveVehicle(vehicle) - return - } - - if vehicle := coordinator.findActiveVehicleByStatus(lp.log, lp, lp.vehicles); vehicle != nil { + if vehicle := coordinator.identifyVehicleByStatus(lp.log, lp, lp.vehicles); vehicle != nil { lp.setActiveVehicle(vehicle) return } @@ -1024,9 +1051,15 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool) { lp.publish("charging", lp.charging()) lp.publish("enabled", lp.enabled) - // update active vehicle if not yet done - if lp.vehicleIdentificationAllowed() { - lp.findActiveVehicle() + // identify connected vehicle + if lp.connected() { + // read identity and run associated action + lp.identifyVehicle() + + // find vehicle by status for a couple of minutes after connecting + if lp.vehicleUnidentified() { + lp.identifyVehicleByStatus() + } } // publish soc after updating charger status to make sure diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 01d8dde8e..bf54acc98 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -751,13 +751,6 @@ func TestMinSoC(t *testing.T) { func TestVehicleDetectByID(t *testing.T) { ctrl := gomock.NewController(t) - type charger struct { - *mock.MockCharger - *mock.MockIdentifier - } - - c := &charger{mock.NewMockCharger(ctrl), mock.NewMockIdentifier(ctrl)} - v1 := mock.NewMockVehicle(ctrl) v2 := mock.NewMockVehicle(ctrl) @@ -768,41 +761,32 @@ func TestVehicleDetectByID(t *testing.T) { prepare func(testcase) } tc := []testcase{ - {"_/_/_->0", "", "", "", nil, func(tc testcase) { - c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil) - }}, {"1/_/_->0", "1", "", "", nil, func(tc testcase) { - c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil) v1.EXPECT().Identify().Return(tc.i1, nil) v2.EXPECT().Identify().Return(tc.i2, nil) v1.EXPECT().Identify().Return(tc.i1, nil) v2.EXPECT().Identify().Return(tc.i2, nil) }}, {"1/1/2->1", "1", "1", "2", v1, func(tc testcase) { - c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil) v1.EXPECT().Identify().Return(tc.i1, nil) }}, {"2/1/2->2", "2", "1", "2", v2, func(tc testcase) { - c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil) v1.EXPECT().Identify().Return(tc.i1, nil) v2.EXPECT().Identify().Return(tc.i2, nil) }}, {"11/1*/2->1", "11", "1*", "2", v1, func(tc testcase) { - c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil) v1.EXPECT().Identify().Return(tc.i1, nil) v2.EXPECT().Identify().Return(tc.i2, nil) v1.EXPECT().Identify().Return(tc.i1, nil) // v2.EXPECT().Identify().Return(tc.i2, nil) }}, {"22/1*/2*->2", "22", "1*", "2*", v2, func(tc testcase) { - c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil) v1.EXPECT().Identify().Return(tc.i1, nil) v2.EXPECT().Identify().Return(tc.i2, nil) v1.EXPECT().Identify().Return(tc.i1, nil) v2.EXPECT().Identify().Return(tc.i2, nil) }}, {"2/_/*->2", "2", "", "*", v2, func(tc testcase) { - c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil) v1.EXPECT().Identify().Return(tc.i1, nil) v2.EXPECT().Identify().Return(tc.i2, nil) v1.EXPECT().Identify().Return(tc.i1, nil) @@ -815,7 +799,6 @@ func TestVehicleDetectByID(t *testing.T) { lp := &LoadPoint{ log: util.NewLogger("foo"), - charger: c, vehicles: []api.Vehicle{v1, v2}, } @@ -823,7 +806,7 @@ func TestVehicleDetectByID(t *testing.T) { tc.prepare(tc) } - if res := lp.findActiveVehicleByID(); tc.res != res { + if res := lp.selectVehicleByID(tc.id); tc.res != res { t.Errorf("expected %v, got %v", tc.res, res) } } diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 840dbe589..d80a96abe 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -91,6 +91,10 @@ loadpoints: onDisconnect: # set defaults when vehicle disconnects mode: pv # switch back to pv mode targetSoC: 100 # charge to 100% + onIdentify: # set defaults when vehicle is identified + - e-Up: + mode: pv # switch back to pv mode + targetSoC: 100 # charge to 100% phases: 3 # ev phases (default 3) enable: # pv mode enable behavior delay: 1m # threshold must be exceeded for this long