diff --git a/core/const.go b/core/const.go index 5f07645e9..7fd794ba1 100644 --- a/core/const.go +++ b/core/const.go @@ -1,6 +1,8 @@ package core const ( + title = "title" // loadpoint title + phasesConfigured = "phasesConfigured" // configured phases (1/3, 0 for auto on 1p3p chargers, nil for plain chargers) phasesEnabled = "phasesEnabled" // enabled phases (1/3) phasesActive = "phasesActive" // active phases as used by vehicle (1/2/3) @@ -16,9 +18,13 @@ const ( vehiclePresent = "vehiclePresent" // vehicle detected vehicleTitle = "vehicleTitle" // vehicle title - minSoc = "minSoc" // min soc goal - targetSoc = "targetSoc" // target charging soc goal - targetTime = "targetTime" // target charging finish time goal - planActive = "planActive" // target charging plan has determined current slot to be an active slot - planProjectedStart = "planProjectedStart" // target charging plan start time (earliest slot) + minCurrent = "minCurrent" // charger min current + maxCurrent = "maxCurrent" // charger max current + chargeRemainingDuration = "chargeRemainingDuration" // charge remaining duration + minSoc = "minSoc" // min soc goal + targetEnergy = "targetEnergy" // target charging energy goal + targetSoc = "targetSoc" // target charging soc goal + targetTime = "targetTime" // target charging finish time goal + planActive = "planActive" // target charging plan has determined current slot to be an active slot + planProjectedStart = "planProjectedStart" // target charging plan start time (earliest slot) ) diff --git a/core/loadpoint.go b/core/loadpoint.go index f45c784bb..47dc22941 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -5,7 +5,6 @@ import ( "fmt" "math" "reflect" - "regexp" "strings" "sync" "time" @@ -17,7 +16,6 @@ import ( "github.com/evcc-io/evcc/core/planner" "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" @@ -47,10 +45,8 @@ const ( timerInactive = "inactive" - minActiveCurrent = 1.0 // minimum current at which a phase is treated as active - minActiveVoltage = 208 // minimum voltage at which a phase is treated as active - vehicleDetectInterval = 1 * time.Minute - vehicleDetectDuration = 10 * time.Minute + minActiveCurrent = 1.0 // minimum current at which a phase is treated as active + minActiveVoltage = 208 // minimum voltage at which a phase is treated as active guardGracePeriod = 10 * time.Second // allow out of sync during this timespan ) @@ -553,9 +549,9 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even _ = lp.bus.Subscribe(evVehicleSoc, lp.evVehicleSocProgressHandler) // publish initial values - lp.publish("title", lp.Title()) - lp.publish("minCurrent", lp.MinCurrent) - lp.publish("maxCurrent", lp.MaxCurrent) + lp.publish(title, lp.Title()) + lp.publish(minCurrent, lp.MinCurrent) + lp.publish(maxCurrent, lp.MaxCurrent) lp.setConfiguredPhases(lp.ConfiguredPhases) lp.publish(phasesEnabled, lp.phases) @@ -791,294 +787,6 @@ func (lp *Loadpoint) remoteControlled(demand loadpoint.RemoteDemand) bool { return lp.remoteDemand == demand } -// setVehicleIdentifier updated the vehicle id as read from the charger -func (lp *Loadpoint) setVehicleIdentifier(id string) { - if lp.vehicleIdentifier != id { - lp.vehicleIdentifier = id - lp.publish("vehicleIdentity", id) - } -} - -// 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.vehicleIdentifier == id { - return - } - - // vehicle found or removed - lp.setVehicleIdentifier(id) - - if id != "" { - lp.log.DEBUG.Println("charger vehicle id:", id) - - if vehicle := lp.selectVehicleByID(id); vehicle != nil { - lp.stopVehicleDetection() - lp.setActiveVehicle(vehicle) - } - } -} - -// 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 vehicles { - for _, vid := range vehicle.Identifiers() { - if strings.EqualFold(id, vid) { - return vehicle - } - } - } - - // find placeholder match - for _, vehicle := range vehicles { - for _, vid := range vehicle.Identifiers() { - // case insensitive match - re, err := regexp.Compile("(?i)" + 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, configures soc estimator -// and adds an odometer task -func (lp *Loadpoint) setActiveVehicle(vehicle api.Vehicle) { - lp.Lock() - - if lp.vehicle == vehicle { - lp.Unlock() - return - } - - from := "unknown" - if lp.vehicle != nil { - lp.coordinator.Release(lp.vehicle) - from = lp.vehicle.Title() - } - to := "unknown" - if vehicle != nil { - lp.coordinator.Acquire(vehicle) - to = vehicle.Title() - } - lp.log.INFO.Printf("vehicle updated: %s -> %s", from, to) - - lp.vehicle = vehicle - - // reset minSoc and targetSoc before change - lp.setMinSoc(0) - lp.setTargetSoc(100) - - // reset target energy - lp.setTargetEnergy(0) - - // unblock api - lp.Unlock() - - if vehicle != nil { - lp.socUpdated = time.Time{} - - // resolve optional config - var estimate bool - if lp.Soc.Estimate == nil || *lp.Soc.Estimate { - estimate = true - } - lp.socEstimator = soc.NewEstimator(lp.log, lp.charger, vehicle, estimate) - - lp.publish(vehiclePresent, true) - lp.publish(vehicleTitle, lp.vehicle.Title()) - lp.publish(vehicleIcon, lp.vehicle.Icon()) - lp.publish(vehicleCapacity, lp.vehicle.Capacity()) - - lp.applyAction(vehicle.OnIdentified()) - lp.addTask(lp.vehicleOdometer) - - lp.progress.Reset() - } else { - lp.socEstimator = nil - - lp.publish(vehiclePresent, false) - lp.publish(vehicleTitle, "") - lp.publish(vehicleIcon, "") - lp.publish(vehicleCapacity, int64(0)) - lp.publish(vehicleOdometer, 0.0) - } - - // re-publish vehicle settings - lp.publish(phasesActive, lp.activePhases()) - lp.unpublishVehicle() - - lp.updateSession(func(session *db.Session) { - var title string - if lp.vehicle != nil { - title = lp.vehicle.Title() - } - - lp.session.Vehicle = title - }) -} - -func (lp *Loadpoint) wakeUpVehicle() { - // charger - if c, ok := lp.charger.(api.Resurrector); ok { - if err := c.WakeUp(); err != nil { - lp.log.ERROR.Printf("wake-up charger: %v", err) - } - return - } - - // vehicle - if vs, ok := lp.vehicle.(api.Resurrector); ok { - if err := vs.WakeUp(); err != nil { - lp.log.ERROR.Printf("wake-up vehicle: %v", err) - } - } -} - -// unpublishVehicle resets published vehicle data -func (lp *Loadpoint) unpublishVehicle() { - lp.vehicleSoc = 0 - - lp.publish(vehicleSoc, 0.0) - lp.publish(vehicleRange, int64(0)) - lp.publish(vehicleTargetSoc, 0.0) - - lp.setRemainingEnergy(0) - lp.setRemainingDuration(0) - - lp.vehiclePublishFeature(api.Offline) -} - -// vehicleHasFeature checks availability of vehicle feature -func (lp *Loadpoint) vehicleHasFeature(f api.Feature) bool { - v, ok := lp.vehicle.(api.FeatureDescriber) - if ok { - ok = v.Has(f) - } - return ok -} - -// vehiclePublishFeature availability of vehicle features -func (lp *Loadpoint) vehiclePublishFeature(f api.Feature) { - lp.publish("vehicleFeature"+f.String(), lp.vehicleHasFeature(f)) -} - -// vehicleUnidentified returns true if there are associated vehicles and detection is running. -// It will also reset the api cache at regular intervals. -// Detection is stopped after maximum duration and the "guest vehicle" message dispatched. -func (lp *Loadpoint) vehicleUnidentified() bool { - if lp.vehicle != nil || lp.vehicleDetect.IsZero() || len(lp.coordinatedVehicles()) == 0 { - return false - } - - // stop detection - if lp.clock.Since(lp.vehicleDetect) > vehicleDetectDuration { - lp.stopVehicleDetection() - lp.pushEvent(evVehicleUnidentified) - return false - } - - // request vehicle api refresh while waiting to identify - select { - case <-lp.vehicleDetectTicker.C: - lp.log.DEBUG.Println("vehicle api refresh") - provider.ResetCached() - default: - } - - return true -} - -// vehicleDefaultOrDetect will assign and update default vehicle or start detection -func (lp *Loadpoint) vehicleDefaultOrDetect() { - if lp.defaultVehicle != nil { - if lp.vehicle != lp.defaultVehicle { - lp.setActiveVehicle(lp.defaultVehicle) - } else { - // default vehicle is already active, update odometer anyway - // need to do this here since setActiveVehicle would short-circuit - lp.addTask(lp.vehicleOdometer) - } - } else if len(lp.coordinatedVehicles()) > 0 && lp.connected() { - lp.startVehicleDetection() - } -} - -// startVehicleDetection reset connection timer and starts api refresh timer -func (lp *Loadpoint) startVehicleDetection() { - // flush all vehicles before detection starts - lp.log.DEBUG.Println("vehicle api refresh") - provider.ResetCached() - - lp.vehicleDetect = lp.clock.Now() - lp.vehicleDetectTicker = lp.clock.Ticker(vehicleDetectInterval) - lp.publish(vehicleDetectionActive, true) -} - -// stopVehicleDetection expires the connection timer and ticker -func (lp *Loadpoint) stopVehicleDetection() { - lp.vehicleDetect = time.Time{} - if lp.vehicleDetectTicker != nil { - lp.vehicleDetectTicker.Stop() - } - lp.publish(vehicleDetectionActive, false) -} - -// identifyVehicleByStatus validates if the active vehicle is still connected to the loadpoint -func (lp *Loadpoint) identifyVehicleByStatus() { - if len(lp.coordinatedVehicles()) == 0 { - return - } - - if vehicle := lp.coordinator.IdentifyVehicleByStatus(); vehicle != nil { - lp.stopVehicleDetection() - lp.setActiveVehicle(vehicle) - return - } - - // remove previous vehicle if status was not confirmed - if _, ok := lp.vehicle.(api.ChargeState); ok { - lp.setActiveVehicle(nil) - } -} - -// vehicleOdometer updates odometer -func (lp *Loadpoint) vehicleOdometer() { - if vs, ok := lp.vehicle.(api.VehicleOdometer); ok { - if odo, err := vs.Odometer(); err == nil { - lp.log.DEBUG.Printf("vehicle odometer: %.0fkm", odo) - lp.publish(vehicleOdometer, odo) - - // update session once odometer is read - lp.updateSession(func(session *db.Session) { - session.Odometer = odo - }) - } else { - lp.log.ERROR.Printf("vehicle odometer: %v", err) - } - } -} - // statusEvents converts the observed charger status change into a logical sequence of events func statusEvents(prevStatus, status api.ChargeStatus) []string { res := make([]string, 0, 2) diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index 5eb44c78d..04a15c534 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -86,7 +86,7 @@ func (lp *Loadpoint) GetTargetEnergy() float64 { // setTargetEnergy sets loadpoint charge target energy (no mutex) func (lp *Loadpoint) setTargetEnergy(energy float64) { lp.targetEnergy = energy - lp.publish("targetEnergy", energy) + lp.publish(targetEnergy, energy) } // SetTargetEnergy sets loadpoint charge target energy @@ -301,7 +301,7 @@ func (lp *Loadpoint) SetMinCurrent(current float64) { if current != lp.MinCurrent { lp.MinCurrent = current - lp.publish("minCurrent", lp.MinCurrent) + lp.publish(minCurrent, lp.MinCurrent) } } @@ -321,7 +321,7 @@ func (lp *Loadpoint) SetMaxCurrent(current float64) { if current != lp.MaxCurrent { lp.MaxCurrent = current - lp.publish("maxCurrent", lp.MaxCurrent) + lp.publish(maxCurrent, lp.MaxCurrent) } } @@ -343,10 +343,10 @@ func (lp *Loadpoint) SetRemainingDuration(chargeRemainingDuration time.Duration) } // setRemainingDuration sets the estimated remaining charging duration (no mutex) -func (lp *Loadpoint) setRemainingDuration(chargeRemainingDuration time.Duration) { - if lp.chargeRemainingDuration != chargeRemainingDuration { - lp.chargeRemainingDuration = chargeRemainingDuration - lp.publish("chargeRemainingDuration", chargeRemainingDuration) +func (lp *Loadpoint) setRemainingDuration(remainingDuration time.Duration) { + if lp.chargeRemainingDuration != remainingDuration { + lp.chargeRemainingDuration = remainingDuration + lp.publish(chargeRemainingDuration, remainingDuration) } } diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go new file mode 100644 index 000000000..0b9bc3d60 --- /dev/null +++ b/core/loadpoint_vehicle.go @@ -0,0 +1,305 @@ +package core + +import ( + "regexp" + "strings" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/db" + "github.com/evcc-io/evcc/core/soc" + "github.com/evcc-io/evcc/provider" +) + +const ( + vehicleDetectInterval = 1 * time.Minute + vehicleDetectDuration = 10 * time.Minute +) + +// setVehicleIdentifier updated the vehicle id as read from the charger +func (lp *Loadpoint) setVehicleIdentifier(id string) { + if lp.vehicleIdentifier != id { + lp.vehicleIdentifier = id + lp.publish("vehicleIdentity", id) + } +} + +// 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.vehicleIdentifier == id { + return + } + + // vehicle found or removed + lp.setVehicleIdentifier(id) + + if id != "" { + lp.log.DEBUG.Println("charger vehicle id:", id) + + if vehicle := lp.selectVehicleByID(id); vehicle != nil { + lp.stopVehicleDetection() + lp.setActiveVehicle(vehicle) + } + } +} + +// 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 vehicles { + for _, vid := range vehicle.Identifiers() { + if strings.EqualFold(id, vid) { + return vehicle + } + } + } + + // find placeholder match + for _, vehicle := range vehicles { + for _, vid := range vehicle.Identifiers() { + // case insensitive match + re, err := regexp.Compile("(?i)" + 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, configures soc estimator +// and adds an odometer task +func (lp *Loadpoint) setActiveVehicle(vehicle api.Vehicle) { + lp.Lock() + + if lp.vehicle == vehicle { + lp.Unlock() + return + } + + from := "unknown" + if lp.vehicle != nil { + lp.coordinator.Release(lp.vehicle) + from = lp.vehicle.Title() + } + to := "unknown" + if vehicle != nil { + lp.coordinator.Acquire(vehicle) + to = vehicle.Title() + } + lp.log.INFO.Printf("vehicle updated: %s -> %s", from, to) + + lp.vehicle = vehicle + + // reset minSoc and targetSoc before change + lp.setMinSoc(0) + lp.setTargetSoc(100) + + // reset target energy + lp.setTargetEnergy(0) + + // unblock api + lp.Unlock() + + if vehicle != nil { + lp.socUpdated = time.Time{} + + // resolve optional config + var estimate bool + if lp.Soc.Estimate == nil || *lp.Soc.Estimate { + estimate = true + } + lp.socEstimator = soc.NewEstimator(lp.log, lp.charger, vehicle, estimate) + + lp.publish(vehiclePresent, true) + lp.publish(vehicleTitle, lp.vehicle.Title()) + lp.publish(vehicleIcon, lp.vehicle.Icon()) + lp.publish(vehicleCapacity, lp.vehicle.Capacity()) + + lp.applyAction(vehicle.OnIdentified()) + lp.addTask(lp.vehicleOdometer) + + lp.progress.Reset() + } else { + lp.socEstimator = nil + + lp.publish(vehiclePresent, false) + lp.publish(vehicleTitle, "") + lp.publish(vehicleIcon, "") + lp.publish(vehicleCapacity, int64(0)) + lp.publish(vehicleOdometer, 0.0) + } + + // re-publish vehicle settings + lp.publish(phasesActive, lp.activePhases()) + lp.unpublishVehicle() + + lp.updateSession(func(session *db.Session) { + var title string + if lp.vehicle != nil { + title = lp.vehicle.Title() + } + + lp.session.Vehicle = title + }) +} + +func (lp *Loadpoint) wakeUpVehicle() { + // charger + if c, ok := lp.charger.(api.Resurrector); ok { + if err := c.WakeUp(); err != nil { + lp.log.ERROR.Printf("wake-up charger: %v", err) + } + return + } + + // vehicle + if vs, ok := lp.vehicle.(api.Resurrector); ok { + if err := vs.WakeUp(); err != nil { + lp.log.ERROR.Printf("wake-up vehicle: %v", err) + } + } +} + +// unpublishVehicle resets published vehicle data +func (lp *Loadpoint) unpublishVehicle() { + lp.vehicleSoc = 0 + + lp.publish(vehicleSoc, 0.0) + lp.publish(vehicleRange, int64(0)) + lp.publish(vehicleTargetSoc, 0.0) + + lp.setRemainingEnergy(0) + lp.setRemainingDuration(0) + + lp.vehiclePublishFeature(api.Offline) +} + +// vehicleHasFeature checks availability of vehicle feature +func (lp *Loadpoint) vehicleHasFeature(f api.Feature) bool { + v, ok := lp.vehicle.(api.FeatureDescriber) + if ok { + ok = v.Has(f) + } + return ok +} + +// vehiclePublishFeature availability of vehicle features +func (lp *Loadpoint) vehiclePublishFeature(f api.Feature) { + lp.publish("vehicleFeature"+f.String(), lp.vehicleHasFeature(f)) +} + +// vehicleUnidentified returns true if there are associated vehicles and detection is running. +// It will also reset the api cache at regular intervals. +// Detection is stopped after maximum duration and the "guest vehicle" message dispatched. +func (lp *Loadpoint) vehicleUnidentified() bool { + if lp.vehicle != nil || lp.vehicleDetect.IsZero() || len(lp.coordinatedVehicles()) == 0 { + return false + } + + // stop detection + if lp.clock.Since(lp.vehicleDetect) > vehicleDetectDuration { + lp.stopVehicleDetection() + lp.pushEvent(evVehicleUnidentified) + return false + } + + // request vehicle api refresh while waiting to identify + select { + case <-lp.vehicleDetectTicker.C: + lp.log.DEBUG.Println("vehicle api refresh") + provider.ResetCached() + default: + } + + return true +} + +// vehicleDefaultOrDetect will assign and update default vehicle or start detection +func (lp *Loadpoint) vehicleDefaultOrDetect() { + if lp.defaultVehicle != nil { + if lp.vehicle != lp.defaultVehicle { + lp.setActiveVehicle(lp.defaultVehicle) + } else { + // default vehicle is already active, update odometer anyway + // need to do this here since setActiveVehicle would short-circuit + lp.addTask(lp.vehicleOdometer) + } + } else if len(lp.coordinatedVehicles()) > 0 && lp.connected() { + lp.startVehicleDetection() + } +} + +// startVehicleDetection reset connection timer and starts api refresh timer +func (lp *Loadpoint) startVehicleDetection() { + // flush all vehicles before detection starts + lp.log.DEBUG.Println("vehicle api refresh") + provider.ResetCached() + + lp.vehicleDetect = lp.clock.Now() + lp.vehicleDetectTicker = lp.clock.Ticker(vehicleDetectInterval) + lp.publish(vehicleDetectionActive, true) +} + +// stopVehicleDetection expires the connection timer and ticker +func (lp *Loadpoint) stopVehicleDetection() { + lp.vehicleDetect = time.Time{} + if lp.vehicleDetectTicker != nil { + lp.vehicleDetectTicker.Stop() + } + lp.publish(vehicleDetectionActive, false) +} + +// identifyVehicleByStatus validates if the active vehicle is still connected to the loadpoint +func (lp *Loadpoint) identifyVehicleByStatus() { + if len(lp.coordinatedVehicles()) == 0 { + return + } + + if vehicle := lp.coordinator.IdentifyVehicleByStatus(); vehicle != nil { + lp.stopVehicleDetection() + lp.setActiveVehicle(vehicle) + return + } + + // remove previous vehicle if status was not confirmed + if _, ok := lp.vehicle.(api.ChargeState); ok { + lp.setActiveVehicle(nil) + } +} + +// vehicleOdometer updates odometer +func (lp *Loadpoint) vehicleOdometer() { + if vs, ok := lp.vehicle.(api.VehicleOdometer); ok { + if odo, err := vs.Odometer(); err == nil { + lp.log.DEBUG.Printf("vehicle odometer: %.0fkm", odo) + lp.publish(vehicleOdometer, odo) + + // update session once odometer is read + lp.updateSession(func(session *db.Session) { + session.Odometer = odo + }) + } else { + lp.log.ERROR.Printf("vehicle odometer: %v", err) + } + } +}