From dd668c30073b2c6299399b5c62cf3c8a8c593537 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 17 Sep 2023 11:37:17 +0200 Subject: [PATCH] Dynamically add/remove vehicles (#9903) --- cmd/setup.go | 6 ++-- core/coordinator/coordinator.go | 49 +++++++++++++++++++++++++++++++++ core/helper.go | 9 ------ core/loadpoint.go | 2 +- core/site.go | 6 ++-- core/site_vehicles.go | 29 +++++++++++++++++++ util/config/handler.go | 16 +++++++++++ util/config/instance.go | 10 +++++-- 8 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 core/site_vehicles.go diff --git a/cmd/setup.go b/cmd/setup.go index c32772ab6..77462ba9d 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -633,11 +633,11 @@ func configureSiteAndLoadpoints(conf globalConfig) (*core.Site, error) { return nil, err } - return configureSite(conf.Site, loadpoints, config.Instances(config.Vehicles().Devices()), tariffs) + return configureSite(conf.Site, loadpoints, tariffs) } -func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, vehicles []api.Vehicle, tariffs tariff.Tariffs) (*core.Site, error) { - site, err := core.NewSiteFromConfig(log, conf, loadpoints, vehicles, tariffs) +func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, tariffs tariff.Tariffs) (*core.Site, error) { + site, err := core.NewSiteFromConfig(log, conf, loadpoints, tariffs) if err != nil { return nil, fmt.Errorf("failed configuring site: %w", err) } diff --git a/core/coordinator/coordinator.go b/core/coordinator/coordinator.go index 2ff59d706..d2742faa5 100644 --- a/core/coordinator/coordinator.go +++ b/core/coordinator/coordinator.go @@ -1,6 +1,8 @@ package coordinator import ( + "sync" + "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/util" @@ -8,6 +10,7 @@ import ( // Coordinator coordinates vehicle access between loadpoints type Coordinator struct { + mu sync.Mutex log *util.Logger vehicles []api.Vehicle tracked map[api.Vehicle]loadpoint.API @@ -24,10 +27,46 @@ func New(log *util.Logger, vehicles []api.Vehicle) *Coordinator { // GetVehicles returns the list of all vehicles func (c *Coordinator) GetVehicles() []api.Vehicle { + c.mu.Lock() + defer c.mu.Unlock() + return c.vehicles } +func (c *Coordinator) Add(vehicle api.Vehicle) { + c.mu.Lock() + defer c.mu.Unlock() + + c.vehicles = append(c.vehicles, vehicle) +} + +func (c *Coordinator) Delete(vehicle api.Vehicle) { + c.mu.Lock() + + for i, v := range c.vehicles { + if v == vehicle { + c.vehicles = append(c.vehicles[:i], c.vehicles[i+1:]...) + + if o, ok := c.tracked[vehicle]; ok { + // defer call to SetVehicle to avoid deadlock on c.mu + defer func(o loadpoint.API) { + o.SetVehicle(nil) + }(o) + } + delete(c.tracked, vehicle) + + break + } + } + + // unlock before deferred SetVehicle executes a this will round-trip back here + c.mu.Unlock() +} + func (c *Coordinator) acquire(owner loadpoint.API, vehicle api.Vehicle) { + c.mu.Lock() + defer c.mu.Unlock() + if o, ok := c.tracked[vehicle]; ok && o != owner { o.SetVehicle(nil) } @@ -35,6 +74,9 @@ func (c *Coordinator) acquire(owner loadpoint.API, vehicle api.Vehicle) { } func (c *Coordinator) release(vehicle api.Vehicle) { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.tracked, vehicle) } @@ -43,6 +85,9 @@ func (c *Coordinator) release(vehicle api.Vehicle) { func (c *Coordinator) availableDetectibleVehicles(owner loadpoint.API) []api.Vehicle { var res []api.Vehicle + c.mu.Lock() + defer c.mu.Unlock() + for _, vv := range c.vehicles { // status api available if _, ok := vv.(api.ChargeState); ok { @@ -59,6 +104,10 @@ func (c *Coordinator) availableDetectibleVehicles(owner loadpoint.API) []api.Veh // identifyVehicleByStatus finds active vehicle by charge state func (c *Coordinator) identifyVehicleByStatus(available []api.Vehicle) api.Vehicle { var res api.Vehicle + + c.mu.Lock() + defer c.mu.Unlock() + for _, vehicle := range available { if vs, ok := vehicle.(api.ChargeState); ok { status, err := vs.Status() diff --git a/core/helper.go b/core/helper.go index 8d9bd31be..a4ed2976f 100644 --- a/core/helper.go +++ b/core/helper.go @@ -2,9 +2,7 @@ package core import ( "github.com/avast/retry-go/v4" - "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" - "github.com/samber/lo" ) var ( @@ -39,10 +37,3 @@ func sitePower(log *util.Logger, maxGrid, grid, battery, residual float64) float return grid + battery + residual } - -// vehicleTitles returns a list of vehicle titles -func vehicleTitles(vehicles []api.Vehicle) []string { - return lo.Map(vehicles, func(v api.Vehicle, _ int) string { - return v.Title() - }) -} diff --git a/core/loadpoint.go b/core/loadpoint.go index 62dd8aedb..ca44001b1 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -788,7 +788,7 @@ func (lp *Loadpoint) remainingChargeEnergy() (float64, bool) { } func (lp *Loadpoint) vehicleHasSoc() bool { - return lp.vehicle != nil && !lp.vehicleHasFeature(api.Offline) + return lp.GetVehicle() != nil && !lp.vehicleHasFeature(api.Offline) } // targetEnergyReached checks if target is configured and reached diff --git a/core/site.go b/core/site.go index 704a62198..b13789337 100644 --- a/core/site.go +++ b/core/site.go @@ -104,7 +104,6 @@ func NewSiteFromConfig( log *util.Logger, other map[string]interface{}, loadpoints []*Loadpoint, - vehicles []api.Vehicle, tariffs tariff.Tariffs, ) (*Site, error) { site := NewSite() @@ -115,7 +114,10 @@ func NewSiteFromConfig( Voltage = site.Voltage site.loadpoints = loadpoints site.tariffs = tariffs - site.coordinator = coordinator.New(log, vehicles) + + site.coordinator = coordinator.New(log, config.Instances(config.Vehicles().Devices())) + config.Vehicles().Subscribe(site.updateVehicles) + site.prioritizer = prioritizer.New(log) site.savings = NewSavings(tariffs) diff --git a/core/site_vehicles.go b/core/site_vehicles.go new file mode 100644 index 000000000..628159285 --- /dev/null +++ b/core/site_vehicles.go @@ -0,0 +1,29 @@ +package core + +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util/config" + "github.com/samber/lo" +) + +// vehicleTitles returns a list of vehicle titles +func vehicleTitles(vehicles []api.Vehicle) []string { + return lo.Map(vehicles, func(v api.Vehicle, _ int) string { + return v.Title() + }) +} + +// updateVehicles adds or removes a vehicle asynchronously +func (site *Site) updateVehicles(op config.Operation, dev config.Device[api.Vehicle]) { + vehicle := dev.Instance() + + switch op { + case config.OpAdd: + site.coordinator.Add(vehicle) + + case config.OpDelete: + site.coordinator.Delete(vehicle) + } + + site.publish("vehicles", vehicleTitles(site.GetVehicles())) +} diff --git a/util/config/handler.go b/util/config/handler.go index 65ddc8268..d25a6e817 100644 --- a/util/config/handler.go +++ b/util/config/handler.go @@ -8,9 +8,23 @@ import ( type handler[T any] struct { mu sync.Mutex + topic string devices []Device[T] } +type Operation string + +const ( + OpAdd Operation = "add" + OpDelete Operation = "del" +) + +func (cp *handler[T]) Subscribe(fn func(Operation, Device[T])) { + if err := bus.Subscribe(cp.topic, fn); err != nil { + panic(err) + } +} + // Devices returns the handlers devices func (cp *handler[T]) Devices() []Device[T] { cp.mu.Lock() @@ -35,6 +49,7 @@ func (cp *handler[T]) Add(dev Device[T]) error { defer cp.mu.Unlock() cp.devices = append(cp.devices, dev) + bus.Publish(cp.topic, OpAdd, dev) return nil } @@ -47,6 +62,7 @@ func (cp *handler[T]) Delete(name string) error { for i, dev := range cp.devices { if name == dev.Config().Name { cp.devices = append(cp.devices[:i], cp.devices[i+1:]...) + bus.Publish(cp.topic, OpDelete, dev) return nil } } diff --git a/util/config/instance.go b/util/config/instance.go index 5a1efe1e3..2fd2dcfac 100644 --- a/util/config/instance.go +++ b/util/config/instance.go @@ -1,20 +1,24 @@ package config import ( + evbus "github.com/asaskevich/EventBus" "github.com/evcc-io/evcc/api" ) +var bus = evbus.New() + var instance = struct { meters *handler[api.Meter] chargers *handler[api.Charger] vehicles *handler[api.Vehicle] }{ - meters: new(handler[api.Meter]), - chargers: new(handler[api.Charger]), - vehicles: new(handler[api.Vehicle]), + meters: &handler[api.Meter]{topic: "meter"}, + chargers: &handler[api.Charger]{topic: "charger"}, + vehicles: &handler[api.Vehicle]{topic: "vehicle"}, } type Handler[T any] interface { + Subscribe(fn func(Operation, Device[T])) Devices() []Device[T] Add(dev Device[T]) error Delete(name string) error