diff --git a/api/feature.go b/api/feature.go index 15d7e200f4..44b5240c5a 100644 --- a/api/feature.go +++ b/api/feature.go @@ -10,4 +10,5 @@ const ( IntegratedDevice Heating Retryable + WelcomeCharge ) diff --git a/api/feature_enumer.go b/api/feature_enumer.go index 858b77ac91..736284087b 100644 --- a/api/feature_enumer.go +++ b/api/feature_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryable" +const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryableWelcomeCharge" -var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52} +var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52, 65} -const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryable" +const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryablewelcomecharge" func (i Feature) String() string { i -= 1 @@ -30,9 +30,10 @@ func _FeatureNoOp() { _ = x[IntegratedDevice-(3)] _ = x[Heating-(4)] _ = x[Retryable-(5)] + _ = x[WelcomeCharge-(6)] } -var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable} +var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable, WelcomeCharge} var _FeatureNameToValueMap = map[string]Feature{ _FeatureName[0:7]: Offline, @@ -45,6 +46,8 @@ var _FeatureNameToValueMap = map[string]Feature{ _FeatureLowerName[36:43]: Heating, _FeatureName[43:52]: Retryable, _FeatureLowerName[43:52]: Retryable, + _FeatureName[52:65]: WelcomeCharge, + _FeatureLowerName[52:65]: WelcomeCharge, } var _FeatureNames = []string{ @@ -53,6 +56,7 @@ var _FeatureNames = []string{ _FeatureName[20:36], _FeatureName[36:43], _FeatureName[43:52], + _FeatureName[52:65], } // FeatureString retrieves an enum value from the enum constants string name. diff --git a/core/coordinator/adapter.go b/core/coordinator/adapter.go index 1642e750b9..e700828168 100644 --- a/core/coordinator/adapter.go +++ b/core/coordinator/adapter.go @@ -19,8 +19,8 @@ func NewAdapter(lp loadpoint.API, c *Coordinator) API { } } -func (a *adapter) GetVehicles() []api.Vehicle { - return a.c.GetVehicles() +func (a *adapter) GetVehicles(availableOnly bool) []api.Vehicle { + return a.c.GetVehicles(availableOnly) } func (a *adapter) Owner(v api.Vehicle) loadpoint.API { diff --git a/core/coordinator/api.go b/core/coordinator/api.go index 6bb1963271..5e2cc91332 100644 --- a/core/coordinator/api.go +++ b/core/coordinator/api.go @@ -7,8 +7,8 @@ import ( // API is the coordinator API type API interface { - // GetVehicles returns the list of all vehicles - GetVehicles() []api.Vehicle + // GetVehicles returns the list of all vehicles, filtered by availability + GetVehicles(availableOnly bool) []api.Vehicle // Owner returns the loadpoint that currently owns the vehicle Owner(api.Vehicle) loadpoint.API diff --git a/core/coordinator/coordinator.go b/core/coordinator/coordinator.go index 3eee8dadb1..01b4e4f3c2 100644 --- a/core/coordinator/coordinator.go +++ b/core/coordinator/coordinator.go @@ -1,7 +1,6 @@ package coordinator import ( - "slices" "sync" "github.com/evcc-io/evcc/api" @@ -27,11 +26,18 @@ func New(log *util.Logger, vehicles []api.Vehicle) *Coordinator { } // GetVehicles returns the list of all vehicles -func (c *Coordinator) GetVehicles() []api.Vehicle { +func (c *Coordinator) GetVehicles(availableOnly bool) []api.Vehicle { c.mu.RLock() defer c.mu.RUnlock() - return slices.Clone(c.vehicles) + res := make([]api.Vehicle, 0, len(c.vehicles)) + for _, v := range c.vehicles { + if _, tracked := c.tracked[v]; !availableOnly || availableOnly && !tracked { + res = append(res, v) + } + } + + return res } // Owner returns the loadpoint that currently owns the vehicle diff --git a/core/coordinator/dummy.go b/core/coordinator/dummy.go index 07eaee8099..fddb037ab9 100644 --- a/core/coordinator/dummy.go +++ b/core/coordinator/dummy.go @@ -12,7 +12,7 @@ func NewDummy() API { return new(dummy) } -func (a *dummy) GetVehicles() []api.Vehicle { +func (a *dummy) GetVehicles(_ bool) []api.Vehicle { return nil } diff --git a/core/loadpoint.go b/core/loadpoint.go index 3a2c6508c0..b5c76e86db 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "reflect" + "slices" "strings" "sync" "testing" @@ -470,6 +471,9 @@ func (lp *Loadpoint) evVehicleConnectHandler() { lp.socEstimator.Reset() } + // get pv mode before vehicle defaults are applied + pvMode := lp.GetMode() == api.ModePV || lp.GetMode() == api.ModeMinPV + // set default or start detection if !lp.chargerHasFeature(api.IntegratedDevice) { lp.vehicleDefaultOrDetect() @@ -478,6 +482,18 @@ func (lp *Loadpoint) evVehicleConnectHandler() { // immediately allow pv mode activity lp.elapsePVTimer() + // Enable charging on connect if any available vehicle requires it. We're using the PV timer + // to disable after the welcome, hence this must be placed after elapsePVTimer. + // TODO check is this doesn't conflict with vehicle defaults like mode: off + if pvMode { + for _, v := range lp.availableVehicles() { + if slices.Contains(v.Features(), api.WelcomeCharge) { + lp.setLimit(lp.effectiveMinCurrent()) + break + } + } + } + // create charging session lp.createSession() } diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go index a8b5b502fb..c2afa4caf8 100644 --- a/core/loadpoint_vehicle.go +++ b/core/loadpoint_vehicle.go @@ -20,12 +20,20 @@ const ( vehicleDetectDuration = 10 * time.Minute ) +// availableVehicles is the slice of vehicles from the coordinator that are available +func (lp *Loadpoint) availableVehicles() []api.Vehicle { + if lp.coordinator == nil { + return nil + } + return lp.coordinator.GetVehicles(true) +} + // 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() + return lp.coordinator.GetVehicles(false) } // setVehicleIdentifier updated the vehicle id as read from the charger