From 1464b755cd898e4b8d34f94ff47b2ba7b4d02d55 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 13 Aug 2022 14:03:53 +0200 Subject: [PATCH] Make vehicles config global instead of per loadpoint (BC) (#3827) --- cmd/dump.go | 8 +-- cmd/setup.go | 12 +++- core/coordinator.go | 77 -------------------- core/coordinator/adapter.go | 36 ++++++++++ core/coordinator/api.go | 11 +++ core/coordinator/coordinator.go | 84 ++++++++++++++++++++++ core/{ => coordinator}/coordinator_test.go | 8 +-- core/coordinator/dummy.go | 24 +++++++ core/loadpoint.go | 75 +++++++++---------- core/loadpoint/api.go | 2 - core/loadpoint_api.go | 7 -- core/loadpoint_test.go | 19 ++--- core/loadpoint_vehicle_test.go | 11 +-- core/site.go | 53 +++++++++----- core/site/api.go | 22 +++++- core/site_api.go | 8 +++ go.mod | 2 +- go.sum | 4 +- server/http.go | 2 +- server/http_handler.go | 4 +- server/mqtt.go | 6 +- 21 files changed, 297 insertions(+), 178 deletions(-) delete mode 100644 core/coordinator.go create mode 100644 core/coordinator/adapter.go create mode 100644 core/coordinator/api.go create mode 100644 core/coordinator/coordinator.go rename core/{ => coordinator}/coordinator_test.go (90%) create mode 100644 core/coordinator/dummy.go diff --git a/cmd/dump.go b/cmd/dump.go index b40652e3ce..1e52b22f61 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -73,6 +73,10 @@ func runDump(cmd *cobra.Command, args []string) { } } + for _, v := range site.GetVehicles() { + d.DumpWithHeader(fmt.Sprintf("vehicle: %s", v.Title()), v) + } + for id, lpI := range site.LoadPoints() { lp := lpI.(*core.LoadPoint) @@ -86,9 +90,5 @@ func runDump(cmd *cobra.Command, args []string) { if name := lp.ChargerRef; name != "" { d.DumpWithHeader(fmt.Sprintf("charger: %s", name), cp.Charger(name)) } - - for id, v := range lp.VehiclesRef { - d.DumpWithHeader(fmt.Sprintf("vehicle %d", id), cp.Vehicle(v)) - } } } diff --git a/cmd/setup.go b/cmd/setup.go index 4ecdfa71d3..4e9000aedd 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -21,6 +21,7 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/pipe" "github.com/evcc-io/evcc/util/sponsor" + "github.com/samber/lo" "github.com/spf13/viper" "golang.org/x/text/currency" ) @@ -196,15 +197,20 @@ func configureSiteAndLoadpoints(conf config) (site *core.Site, err error) { } if err == nil { - site, err = configureSite(conf.Site, cp, loadPoints, tariffs) + // list of vehicles + vehicles := lo.MapToSlice(cp.vehicles, func(_ string, v api.Vehicle) api.Vehicle { + return v + }) + + site, err = configureSite(conf.Site, cp, loadPoints, vehicles, tariffs) } } return site, err } -func configureSite(conf map[string]interface{}, cp *ConfigProvider, loadPoints []*core.LoadPoint, tariffs tariff.Tariffs) (*core.Site, error) { - site, err := core.NewSiteFromConfig(log, cp, conf, loadPoints, tariffs) +func configureSite(conf map[string]interface{}, cp *ConfigProvider, loadPoints []*core.LoadPoint, vehicles []api.Vehicle, tariffs tariff.Tariffs) (*core.Site, error) { + site, err := core.NewSiteFromConfig(log, cp, conf, loadPoints, vehicles, tariffs) if err != nil { return nil, fmt.Errorf("failed configuring site: %w", err) } diff --git a/core/coordinator.go b/core/coordinator.go deleted file mode 100644 index 8b753c08e6..0000000000 --- a/core/coordinator.go +++ /dev/null @@ -1,77 +0,0 @@ -package core - -import ( - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/core/loadpoint" - "github.com/evcc-io/evcc/util" -) - -type vehicleCoordinator struct { - tracked map[api.Vehicle]loadpoint.API -} - -var coordinator *vehicleCoordinator - -func init() { - coordinator = &vehicleCoordinator{ - tracked: make(map[api.Vehicle]loadpoint.API), - } -} - -func (lp *vehicleCoordinator) acquire(owner loadpoint.API, vehicle api.Vehicle) { - if o, ok := lp.tracked[vehicle]; ok && o != owner { - o.SetVehicle(nil) - } - lp.tracked[vehicle] = owner -} - -func (lp *vehicleCoordinator) release(vehicle api.Vehicle) { - delete(lp.tracked, vehicle) -} - -// availableDetectibleVehicles is the list of vehicles that are currently not -// associated to another loadpoint and have a status api that allows for detection -func (lp *vehicleCoordinator) availableDetectibleVehicles(owner loadpoint.API, vehicles []api.Vehicle) []api.Vehicle { - var res []api.Vehicle - - for _, vv := range vehicles { - if _, ok := vv.(api.ChargeState); ok { - if o, ok := lp.tracked[vv]; o == owner || !ok { - res = append(res, vv) - } - } - } - - return res -} - -// find active vehicle by charge state -func (lp *vehicleCoordinator) identifyVehicleByStatus(log *util.Logger, owner loadpoint.API, vehicles []api.Vehicle) api.Vehicle { - available := lp.availableDetectibleVehicles(owner, vehicles) - - var res api.Vehicle - for _, vehicle := range available { - if vs, ok := vehicle.(api.ChargeState); ok { - status, err := vs.Status() - - if err != nil { - log.ERROR.Println("vehicle status:", err) - continue - } - - log.DEBUG.Printf("vehicle status: %s (%s)", status, vehicle.Title()) - - // vehicle is plugged or charging, so it should be the right one - if status == api.StatusB || status == api.StatusC { - if res != nil { - log.WARN.Println("vehicle status: >1 matches, giving up") - return nil - } - - res = vehicle - } - } - } - - return res -} diff --git a/core/coordinator/adapter.go b/core/coordinator/adapter.go new file mode 100644 index 0000000000..74765cde27 --- /dev/null +++ b/core/coordinator/adapter.go @@ -0,0 +1,36 @@ +package coordinator + +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/loadpoint" +) + +type adapter struct { + lp loadpoint.API + c *Coordinator +} + +// NewAdapter exposes the coordinator for a given loadpoint. +// Using an adapter simplifies the method signatures seen from the loadpoint. +func NewAdapter(lp loadpoint.API, c *Coordinator) API { + return &adapter{ + lp: lp, + c: c, + } +} + +func (a *adapter) GetVehicles() []api.Vehicle { + return a.c.GetVehicles() +} + +func (a *adapter) Acquire(v api.Vehicle) { + a.c.acquire(a.lp, v) +} + +func (a *adapter) Release(v api.Vehicle) { + a.c.release(v) +} + +func (a *adapter) IdentifyVehicleByStatus() api.Vehicle { + return a.c.identifyVehicleByStatus(a.lp) +} diff --git a/core/coordinator/api.go b/core/coordinator/api.go new file mode 100644 index 0000000000..717ace62c3 --- /dev/null +++ b/core/coordinator/api.go @@ -0,0 +1,11 @@ +package coordinator + +import "github.com/evcc-io/evcc/api" + +// API is the coordinator API +type API interface { + GetVehicles() []api.Vehicle + Acquire(api.Vehicle) + Release(api.Vehicle) + IdentifyVehicleByStatus() api.Vehicle +} diff --git a/core/coordinator/coordinator.go b/core/coordinator/coordinator.go new file mode 100644 index 0000000000..32cdbd4cb5 --- /dev/null +++ b/core/coordinator/coordinator.go @@ -0,0 +1,84 @@ +package coordinator + +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/loadpoint" + "github.com/evcc-io/evcc/util" +) + +// Coordinator coordinates vehicle access between loadpoints +type Coordinator struct { + log *util.Logger + vehicles []api.Vehicle + tracked map[api.Vehicle]loadpoint.API +} + +// New creates a coordinator for a set of vehicles +func New(log *util.Logger, vehicles []api.Vehicle) *Coordinator { + return &Coordinator{ + log: log, + vehicles: vehicles, + tracked: make(map[api.Vehicle]loadpoint.API), + } +} + +func (c *Coordinator) GetVehicles() []api.Vehicle { + return c.vehicles +} + +func (c *Coordinator) acquire(owner loadpoint.API, vehicle api.Vehicle) { + if o, ok := c.tracked[vehicle]; ok && o != owner { + o.SetVehicle(nil) + } + c.tracked[vehicle] = owner +} + +func (c *Coordinator) release(vehicle api.Vehicle) { + delete(c.tracked, vehicle) +} + +// availableDetectibleVehicles is the list of vehicles that are currently not +// associated to another loadpoint and have a status api that allows for detection +func (c *Coordinator) availableDetectibleVehicles(owner loadpoint.API) []api.Vehicle { + var res []api.Vehicle + + for _, vv := range c.vehicles { + if _, ok := vv.(api.ChargeState); ok { + if o, ok := c.tracked[vv]; o == owner || !ok { + res = append(res, vv) + } + } + } + + return res +} + +// identifyVehicleByStatus finds active vehicle by charge state +func (c *Coordinator) identifyVehicleByStatus(owner loadpoint.API) api.Vehicle { + available := c.availableDetectibleVehicles(owner) + + var res api.Vehicle + for _, vehicle := range available { + if vs, ok := vehicle.(api.ChargeState); ok { + status, err := vs.Status() + if err != nil { + c.log.ERROR.Println("vehicle status:", err) + continue + } + + c.log.DEBUG.Printf("vehicle status: %s (%s)", status, vehicle.Title()) + + // vehicle is plugged or charging, so it should be the right one + if status == api.StatusB || status == api.StatusC { + if res != nil { + c.log.WARN.Println("vehicle status: >1 matches, giving up") + return nil + } + + res = vehicle + } + } + } + + return res +} diff --git a/core/coordinator_test.go b/core/coordinator/coordinator_test.go similarity index 90% rename from core/coordinator_test.go rename to core/coordinator/coordinator_test.go index 4fe7def036..4dfc80f396 100644 --- a/core/coordinator_test.go +++ b/core/coordinator/coordinator_test.go @@ -1,4 +1,4 @@ -package core +package coordinator import ( "testing" @@ -38,8 +38,8 @@ func TestVehicleDetectByStatus(t *testing.T) { log := util.NewLogger("foo") vehicles := []api.Vehicle{v1, v2} - lp := &LoadPoint{} - c := &vehicleCoordinator{make(map[api.Vehicle]loadpoint.API)} + var lp loadpoint.API + c := New(log, vehicles) for _, tc := range tc { t.Logf("%+v", tc) @@ -49,7 +49,7 @@ func TestVehicleDetectByStatus(t *testing.T) { v1.MockVehicle.EXPECT().Title().Return("v1").AnyTimes() v2.MockVehicle.EXPECT().Title().Return("v2").AnyTimes() - res := c.identifyVehicleByStatus(log, lp, vehicles) + res := c.identifyVehicleByStatus(lp) if tc.res != res { t.Errorf("expected %v, got %v", tc.res, res) } diff --git a/core/coordinator/dummy.go b/core/coordinator/dummy.go new file mode 100644 index 0000000000..6b75c05b94 --- /dev/null +++ b/core/coordinator/dummy.go @@ -0,0 +1,24 @@ +package coordinator + +import ( + "github.com/evcc-io/evcc/api" +) + +type dummy struct{} + +// NewDummy creates a dummy coordinator without vehicles +func NewDummy() API { + return &dummy{} +} + +func (a *dummy) GetVehicles() []api.Vehicle { + return nil +} + +func (a *dummy) Acquire(v api.Vehicle) {} + +func (a *dummy) Release(v api.Vehicle) {} + +func (a *dummy) IdentifyVehicleByStatus() api.Vehicle { + return nil +} diff --git a/core/loadpoint.go b/core/loadpoint.go index 0ec5019fc9..f72f844e7e 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -11,13 +11,13 @@ import ( "time" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/coordinator" "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/core/soc" "github.com/evcc-io/evcc/core/wrapper" "github.com/evcc-io/evcc/provider" "github.com/evcc-io/evcc/push" "github.com/evcc-io/evcc/util" - "github.com/samber/lo" "golang.org/x/exp/slices" evbus "github.com/asaskevich/EventBus" @@ -104,7 +104,7 @@ type LoadPoint struct { DefaultPhases int `mapstructure:"phases"` // Charger enabled phases ChargerRef string `mapstructure:"charger"` // Charger reference VehicleRef string `mapstructure:"vehicle"` // Vehicle reference - VehiclesRef []string `mapstructure:"vehicles"` // Vehicles reference + VehiclesRef_ []string `mapstructure:"vehicles"` // TODO deprecated MeterRef string `mapstructure:"meter"` // Charge meter reference SoC SoCConfig Enable, Disable ThresholdConfig @@ -129,10 +129,10 @@ type LoadPoint struct { chargeTimer api.ChargeTimer chargeRater api.ChargeRater - chargeMeter api.Meter // Charger usage meter - vehicle api.Vehicle // Currently active vehicle - vehicles []api.Vehicle // Assigned vehicles - defaultVehicle api.Vehicle // Default vehicle (disables detection) + chargeMeter api.Meter // Charger usage meter + vehicle api.Vehicle // Currently active vehicle + defaultVehicle api.Vehicle // Default vehicle (disables detection) + coordinator coordinator.API socEstimator *soc.Estimator socTimer *soc.Timer @@ -200,32 +200,14 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin lp.chargeMeter = cp.Meter(lp.MeterRef) } - // multiple vehicles - for _, ref := range lp.VehiclesRef { - vehicle := cp.Vehicle(ref) - lp.vehicles = append(lp.vehicles, vehicle) - } - // default vehicle if lp.VehicleRef != "" { lp.defaultVehicle = cp.Vehicle(lp.VehicleRef) - - // append default vehicle if not contained in list - if len(lo.Filter(lp.vehicles, func(v api.Vehicle, _ int) bool { - return v == lp.defaultVehicle - })) == 0 { - lp.vehicles = append(lp.vehicles, lp.defaultVehicle) - } } - // verify vehicle detection - if len(lp.vehicles) > 1 { - for _, v := range lp.vehicles { - if _, ok := v.(api.ChargeState); !ok { - lp.log.WARN.Printf("vehicle '%s' does not support automatic detection", v.Title()) - break - } - } + // TODO deprecated + if len(lp.VehiclesRef_) > 0 { + lp.log.WARN.Println("vehicles option is deprecated") } if lp.ChargerRef == "" { @@ -274,8 +256,9 @@ func NewLoadPoint(log *util.Logger) *LoadPoint { Enable: ThresholdConfig{Delay: time.Minute, Threshold: 0}, // t, W Disable: ThresholdConfig{Delay: 3 * time.Minute, Threshold: 0}, // t, W GuardDuration: 5 * time.Minute, - progress: NewProgress(0, 10), // soc progress indicator - tasks: aq.New(), + progress: NewProgress(0, 10), // soc progress indicator + coordinator: coordinator.NewDummy(), // dummy vehicle coordinator + tasks: aq.New(), // task queue } // allow target charge handler to access loadpoint @@ -543,10 +526,8 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even lp.publish("minSoC", lp.SoC.Min) lp.Unlock() - // activate default vehicle (allows poll mode: always) - if lp.defaultVehicle != nil { - lp.setActiveVehicle(lp.defaultVehicle) - } + // set default or start detection + lp.vehicleDefaultOrDetect() // read initial charger state to prevent immediately disabling charger if enabled, err := lp.charger.Enabled(); err == nil { @@ -768,15 +749,17 @@ func (lp *LoadPoint) identifyVehicle() { // selectVehicleByID selects the vehicle with the given ID func (lp *LoadPoint) selectVehicleByID(id string) api.Vehicle { + vehicles := lp.coordinatedVehicles() + // find exact match - for _, vehicle := range lp.vehicles { + for _, vehicle := range vehicles { if slices.Contains(vehicle.Identifiers(), id) { return vehicle } } // find placeholder match - for _, vehicle := range lp.vehicles { + for _, vehicle := range vehicles { for _, vid := range vehicle.Identifiers() { re, err := regexp.Compile(strings.ReplaceAll(vid, "*", ".*?")) if err != nil { @@ -805,12 +788,12 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) { from := "unknown" if lp.vehicle != nil { - coordinator.release(lp.vehicle) + lp.coordinator.Release(lp.vehicle) from = lp.vehicle.Title() } to := "unknown" if vehicle != nil { - coordinator.acquire(lp, vehicle) + lp.coordinator.Acquire(vehicle) to = vehicle.Title() } lp.log.INFO.Printf("vehicle updated: %s -> %s", from, to) @@ -875,7 +858,7 @@ func (lp *LoadPoint) unpublishVehicle() { // vehicleUnidentified checks if there are associated vehicles and starts discovery period func (lp *LoadPoint) vehicleUnidentified() bool { - res := len(lp.vehicles) > 0 && lp.vehicle == nil && + res := len(lp.coordinatedVehicles()) > 0 && lp.vehicle == nil && lp.clock.Since(lp.vehicleDetect) < vehicleDetectDuration // request vehicle api refresh while waiting to identify @@ -901,7 +884,7 @@ func (lp *LoadPoint) vehicleDefaultOrDetect() { // need to do this here since setActiveVehicle would short-circuit lp.addTask(lp.vehicleOdometer) } - } else if len(lp.vehicles) > 0 { + } else if len(lp.coordinatedVehicles()) > 0 { // flush all vehicles before detection starts lp.log.DEBUG.Println("vehicle api refresh") provider.ResetCached() @@ -922,11 +905,11 @@ func (lp *LoadPoint) stopVehicleDetection() { // identifyVehicleByStatus validates if the active vehicle is still connected to the loadpoint func (lp *LoadPoint) identifyVehicleByStatus() { - if len(lp.vehicles) == 0 { + if len(lp.coordinatedVehicles()) == 0 { return } - if vehicle := coordinator.identifyVehicleByStatus(lp.log, lp, lp.vehicles); vehicle != nil { + if vehicle := lp.coordinator.IdentifyVehicleByStatus(); vehicle != nil { lp.setActiveVehicle(vehicle) return } @@ -1185,9 +1168,17 @@ func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6 return false } +// coordinatedVehicles is the slice of vehicles from the coordinator +func (lp *LoadPoint) coordinatedVehicles() []api.Vehicle { + if lp.coordinator == nil { + return nil + } + return lp.coordinator.GetVehicles() +} + // publishVehicles publishes a slice of vehicle titles func (lp *LoadPoint) publishVehicles() { - lp.publish("vehicles", vehicleTitles(lp.vehicles)) + lp.publish("vehicles", vehicleTitles(lp.coordinatedVehicles())) } // TODO move up to timer functions diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go index 64017fa339..55020f4868 100644 --- a/core/loadpoint/api.go +++ b/core/loadpoint/api.go @@ -83,8 +83,6 @@ type API interface { // vehicles // - // GetVehicle is the list of vehicles - GetVehicles() []api.Vehicle // SetVehicle sets the active vehicle SetVehicle(vehicle api.Vehicle) } diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index c404fa6e5e..dae5fbd22c 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -260,13 +260,6 @@ func (lp *LoadPoint) GetRemainingEnergy() float64 { return lp.chargeRemainingEnergy } -// GetVehicles is the list of vehicles -func (lp *LoadPoint) GetVehicles() []api.Vehicle { - lp.Lock() - defer lp.Unlock() - return lp.vehicles -} - // SetVehicle sets the active vehicle func (lp *LoadPoint) SetVehicle(vehicle api.Vehicle) { // TODO develop universal locking approach diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 0fa07680a0..7a48ee04d8 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -390,15 +390,16 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) { socEstimator := soc.NewEstimator(util.NewLogger("foo"), charger, vehicle, false) lp := &LoadPoint{ - log: util.NewLogger("foo"), - bus: evbus.New(), - clock: clock, - charger: charger, - chargeMeter: &Null{}, // silence nil panics - chargeRater: &Null{}, // silence nil panics - chargeTimer: &Null{}, // silence nil panics - progress: NewProgress(0, 10), // silence nil panics - wakeUpTimer: NewTimer(), // silence nil panics + log: util.NewLogger("foo"), + bus: evbus.New(), + clock: clock, + charger: charger, + chargeMeter: &Null{}, // silence nil panics + chargeRater: &Null{}, // silence nil panics + chargeTimer: &Null{}, // silence nil panics + progress: NewProgress(0, 10), // silence nil panics + wakeUpTimer: NewTimer(), // silence nil panics + // coordinator: coordinator.NewDummy(), // silence nil panics MinCurrent: minA, MaxCurrent: maxA, vehicle: vehicle, // needed for targetSoC check diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go index b6884d33eb..de53ad3705 100644 --- a/core/loadpoint_vehicle_test.go +++ b/core/loadpoint_vehicle_test.go @@ -6,6 +6,7 @@ import ( evbus "github.com/asaskevich/EventBus" "github.com/benbjohnson/clock" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/coordinator" "github.com/evcc-io/evcc/mock" "github.com/evcc-io/evcc/util" "github.com/golang/mock/gomock" @@ -61,10 +62,11 @@ func TestVehicleDetectByID(t *testing.T) { t.Logf("%+v", tc) lp := &LoadPoint{ - log: util.NewLogger("foo"), - vehicles: []api.Vehicle{v1, v2}, + log: util.NewLogger("foo"), } + lp.coordinator = coordinator.NewAdapter(lp, coordinator.New(util.NewLogger("foo"), []api.Vehicle{v1, v2})) + if tc.prepare != nil { tc.prepare(tc) } @@ -206,7 +208,7 @@ func TestApplyVehicleDefaults(t *testing.T) { } lp.charger = charger - lp.vehicles = []api.Vehicle{vehicle} + lp.coordinator = coordinator.NewAdapter(lp, coordinator.New(util.NewLogger("foo"), []api.Vehicle{vehicle})) const id = "don't call me stacey" charger.MockIdentifier.EXPECT().Identify().Return(id, nil) @@ -248,9 +250,10 @@ func TestReconnectVehicle(t *testing.T) { MaxCurrent: maxA, phases: 1, Mode: api.ModeNow, - vehicles: []api.Vehicle{vehicle}, } + lp.coordinator = coordinator.NewAdapter(lp, coordinator.New(util.NewLogger("foo"), []api.Vehicle{vehicle})) + attachListeners(t, lp) // mode now diff --git a/core/site.go b/core/site.go index a63afde97c..b665993b2d 100644 --- a/core/site.go +++ b/core/site.go @@ -9,6 +9,7 @@ import ( "github.com/avast/retry-go/v3" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/coordinator" "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/push" "github.com/evcc-io/evcc/tariff" @@ -44,9 +45,10 @@ type Site struct { pvMeters []api.Meter // PV generation meters batteryMeters []api.Meter // Battery charging meters - tariffs tariff.Tariffs // Tariff - loadpoints []*LoadPoint // Loadpoints - savings *Savings // Savings + tariffs tariff.Tariffs // Tariff + loadpoints []*LoadPoint // Loadpoints + coordinator *coordinator.Coordinator // Savings + savings *Savings // Savings // cached state gridPower float64 // Grid power @@ -70,6 +72,7 @@ func NewSiteFromConfig( cp configProvider, other map[string]interface{}, loadpoints []*LoadPoint, + vehicles []api.Vehicle, tariffs tariff.Tariffs, ) (*Site, error) { site := NewSite() @@ -80,8 +83,14 @@ func NewSiteFromConfig( Voltage = site.Voltage site.loadpoints = loadpoints site.tariffs = tariffs + site.coordinator = coordinator.New(log, vehicles) site.savings = NewSavings(tariffs) + // give loadpoints access to vehicles + for _, lp := range loadpoints { + lp.coordinator = coordinator.NewAdapter(lp, site.coordinator) + } + if site.Meters.GridMeterRef != "" { site.gridMeter = cp.Meter(site.Meters.GridMeterRef) } @@ -159,6 +168,16 @@ func meterCapabilities(name string, meter interface{}) string { // DumpConfig site configuration func (site *Site) DumpConfig() { + // verify vehicle detection + if vehicles := site.GetVehicles(); len(vehicles) > 1 { + for _, v := range vehicles { + if _, ok := v.(api.ChargeState); !ok { + site.log.WARN.Printf("vehicle '%s' does not support automatic detection", v.Title()) + break + } + } + } + site.log.INFO.Println("site config:") site.log.INFO.Printf(" meters: grid %s pv %s battery %s", presence[site.gridMeter != nil], @@ -186,6 +205,21 @@ func (site *Site) DumpConfig() { } } + if vehicles := site.GetVehicles(); len(vehicles) > 0 { + site.log.INFO.Println(" vehicles:") + + for i, v := range vehicles { + _, rng := v.(api.VehicleRange) + _, finish := v.(api.VehicleFinishTimer) + _, status := v.(api.ChargeState) + _, climate := v.(api.VehicleClimater) + _, wakeup := v.(api.Resurrector) + site.log.INFO.Printf(" vehicle %d: range %s finish %s status %s climate %s wakeup %s", + i+1, presence[rng], presence[finish], presence[status], presence[climate], presence[wakeup], + ) + } + } + for i, lp := range site.loadpoints { lp.log.INFO.Printf("loadpoint %d:", i+1) lp.log.INFO.Printf(" mode: %s", lp.GetMode()) @@ -210,19 +244,6 @@ func (site *Site) DumpConfig() { if lp.HasChargeMeter() { lp.log.INFO.Printf(meterCapabilities("charge", lp.chargeMeter)) } - - lp.log.INFO.Printf(" vehicles: %s", presence[len(lp.vehicles) > 0]) - - for i, v := range lp.vehicles { - _, rng := v.(api.VehicleRange) - _, finish := v.(api.VehicleFinishTimer) - _, status := v.(api.ChargeState) - _, climate := v.(api.VehicleClimater) - _, wakeup := v.(api.Resurrector) - lp.log.INFO.Printf(" vehicle %d: range %s finish %s status %s climate %s wakeup %s", - i+1, presence[rng], presence[finish], presence[status], presence[climate], presence[wakeup], - ) - } } } diff --git a/core/site/api.go b/core/site/api.go index bb15686a87..3f99db3fcf 100644 --- a/core/site/api.go +++ b/core/site/api.go @@ -1,15 +1,35 @@ package site -import "github.com/evcc-io/evcc/core/loadpoint" +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/loadpoint" +) // API is the external site API type API interface { Healthy() bool LoadPoints() []loadpoint.API + + // + // battery + // + GetBufferSoC() float64 SetBufferSoC(float64) error GetPrioritySoC() float64 SetPrioritySoC(float64) error + + // + // power and energy + // + GetResidualPower() float64 SetResidualPower(float64) error + + // + // vehicles + // + + // GetVehicles is the list of vehicles + GetVehicles() []api.Vehicle } diff --git a/core/site_api.go b/core/site_api.go index b15969fa6b..902b930fb7 100644 --- a/core/site_api.go +++ b/core/site_api.go @@ -3,6 +3,7 @@ package core import ( "errors" + "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/site" ) @@ -69,3 +70,10 @@ func (site *Site) SetResidualPower(power float64) error { return nil } + +// GetVehicles is the list of vehicles +func (site *Site) GetVehicles() []api.Vehicle { + site.Lock() + defer site.Unlock() + return site.coordinator.GetVehicles() +} diff --git a/go.mod b/go.mod index 04f6847796..9c4216a5c1 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,7 @@ require ( github.com/prometheus/client_golang v1.12.2 github.com/prometheus/common v0.34.0 github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f - github.com/samber/lo v1.21.0 + github.com/samber/lo v1.27.0 github.com/shurcooL/graphql v0.0.0-20220520033453-bdb1221e171e github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/spf13/cobra v1.4.0 diff --git a/go.sum b/go.sum index 135941a974..d7968e8a21 100644 --- a/go.sum +++ b/go.sum @@ -819,8 +819,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= -github.com/samber/lo v1.21.0 h1:FSby8pJQtX4KmyddTCCGhc3JvnnIVrDA+NW37rG+7G8= -github.com/samber/lo v1.21.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= +github.com/samber/lo v1.27.0 h1:GOyDWxsblvqYobqsmUuMddPa2/mMzkKyojlXol4+LaQ= +github.com/samber/lo v1.27.0/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= diff --git a/server/http.go b/server/http.go index 80efbaed81..46252df15a 100644 --- a/server/http.go +++ b/server/http.go @@ -92,7 +92,7 @@ func NewHTTPd(addr string, site site.API, hub *SocketHub, cache *util.Cache) *HT "phases": {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", phasesHandler(lp)}, "targetcharge": {[]string{"POST", "OPTIONS"}, "/targetcharge/{soc:[0-9]+}/{time:[0-9TZ:.-]+}", targetChargeHandler(lp)}, "targetcharge2": {[]string{"DELETE", "OPTIONS"}, "/targetcharge", targetChargeRemoveHandler(lp)}, - "vehicle": {[]string{"POST", "OPTIONS"}, "/vehicle/{vehicle:[0-9]+}", vehicleHandler(lp)}, + "vehicle": {[]string{"POST", "OPTIONS"}, "/vehicle/{vehicle:[0-9]+}", vehicleHandler(site, lp)}, "vehicle2": {[]string{"DELETE", "OPTIONS"}, "/vehicle", vehicleRemoveHandler(lp)}, "remotedemand": {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source::[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)}, } diff --git a/server/http_handler.go b/server/http_handler.go index 747b6fca1a..c37093d89a 100644 --- a/server/http_handler.go +++ b/server/http_handler.go @@ -244,14 +244,14 @@ func targetChargeRemoveHandler(loadpoint loadpoint.API) http.HandlerFunc { } // vehicleHandler sets active vehicle -func vehicleHandler(loadpoint loadpoint.API) http.HandlerFunc { +func vehicleHandler(site site.API, loadpoint loadpoint.API) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) valS, ok := vars["vehicle"] val, err := strconv.Atoi(valS) - vehicles := loadpoint.GetVehicles() + vehicles := site.GetVehicles() if !ok || val >= len(vehicles) || err != nil { w.WriteHeader(http.StatusBadRequest) return diff --git a/server/mqtt.go b/server/mqtt.go index b230ecaf63..49cf02a5ee 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -85,7 +85,7 @@ func (m *MQTT) publish(topic string, retained bool, payload interface{}) { m.publishSingleValue(topic, retained, payload) } -func (m *MQTT) listenSetters(topic string, lp loadpoint.API) { +func (m *MQTT) listenSetters(topic string, site site.API, lp loadpoint.API) { m.Handler.ListenSetter(topic+"/mode/set", func(payload string) { lp.SetMode(api.ChargeMode(payload)) }) @@ -116,7 +116,7 @@ func (m *MQTT) listenSetters(topic string, lp loadpoint.API) { }) m.Handler.ListenSetter(topic+"/vehicle/set", func(payload string) { if vehicle, err := strconv.Atoi(payload); err == nil { - vehicles := lp.GetVehicles() + vehicles := site.GetVehicles() if vehicle < len(vehicles) { lp.SetVehicle(vehicles[vehicle]) } @@ -156,7 +156,7 @@ func (m *MQTT) Run(site site.API, in <-chan util.Param) { // loadpoint setters for id, lp := range site.LoadPoints() { topic := fmt.Sprintf("%s/loadpoints/%d", m.root, id+1) - m.listenSetters(topic, lp) + m.listenSetters(topic, site, lp) } // remove deprecated topics