Add configurable actions to provide custom behaviour when vehicle is identified (#1329)

This commit is contained in:
andig 2021-08-12 21:45:24 +02:00 • committed by GitHub
parent 69d02191f1
commit d5262f4b42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 90 deletions

View file

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

View file

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

View file

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

View file

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

View file

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