evcc-io/core/loadpoint_api.go
Michael Geers 7de484969a
Persist targetSoc/Energy, minSoc, targetTime per vehicle, add minSoc to UI (#8115)
* Plugins: make javascript return values more permissive

* Persist targetSoc/Energy, minSoc and target time, remove experimental from minSoc UI

* go generate

* Fix tests

* Update minSoc description

* Store settings by index. Show plan outside pv mode. Add help texts.

* remove slug

* go mock

* lint

* chore: simplify templates (#8304)

* Add e2e tests with playwright (#8123)

* Revert "1p3p: let charger handle session stop/restart (#7723)"

This reverts commit dd787ce52f.

* mazda2mqtt: document vin required (#8319)

* FoxESS: split H1/H3 devices (#7376)

Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com>

* Translations update from Hosted Weblate (#8124)

* Translated using Weblate (Czech)

Currently translated at 37.3% (99 of 265 strings)

Co-authored-by: Dusan Suja <bc.suja.dusan@googlemail.com>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/cs/
Translation: evcc/evcc

* Translated using Weblate (Finnish)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: Arna Lepikkö <arna.lepikko@telemail.fi>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/fi/
Translation: evcc/evcc

* Translated using Weblate (German)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: ThinkEV <claas@rootdir.de>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/de/
Translation: evcc/evcc

* Translated using Weblate (Croatian)

Currently translated at 100.0% (265 of 265 strings)

Translated using Weblate (Slovenian)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: Žiga Deisinger <ziga@deisinger.si>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/hr/
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/sl/
Translation: evcc/evcc

* Translated using Weblate (Catalan)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: Norbert Poch <npoch@yahoo.com>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/ca/
Translation: evcc/evcc

* Translated using Weblate (Dutch)

Currently translated at 76.6% (203 of 265 strings)

Translated using Weblate (Dutch)

Currently translated at 74.3% (197 of 265 strings)

Co-authored-by: Ruben Van Boxem <vanboxem.ruben@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/nl/
Translation: evcc/evcc

* fix toml

---------

Co-authored-by: Dusan Suja <bc.suja.dusan@googlemail.com>
Co-authored-by: Arna Lepikkö <arna.lepikko@telemail.fi>
Co-authored-by: ThinkEV <claas@rootdir.de>
Co-authored-by: Žiga Deisinger <ziga@deisinger.si>
Co-authored-by: Norbert Poch <npoch@yahoo.com>
Co-authored-by: Ruben Van Boxem <vanboxem.ruben@gmail.com>
Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com>

* Add OBO Betterman Ion (#8321)

Also aligns heartbeat implementations

* Enphase: add token auth (firmware D7.x.xxx) and grid (#8247)

* Mqtt: fix smartCostLimit topic case (#8328)

* Add ISO15118 vehicle template (#8302)

* Mqtt: simplify setters

* Mqtt: disable message ordering to improve performance

* chore: simplify random state generation

* Porsche: remove deprecated mobile api (#8349)

* Porsche: remove remaining mobile api types

* Cupra: add odometer (#8340)

* chore: refactor go-stylish

* chore: fix stale handler

* mazda2mqtt: longer timeout (#8364)

* chore: pre-process toml

* chore: prevent toml double quotes

* Audi: temporarily switch to etron (#8374)

* Fix nightly build (#8384)

* Update SunSpec templates (#8270)

* Revert "Fix nightly build (#8384)"

This reverts commit 536dbc936c.

* Revert "Add e2e tests with playwright (#8123)"

This reverts commit 0df815729b.

* Easee: update Observation IDs (#8391)

* Easee: handle smartCharging errors (#8389)

* TWC: add non-Tesla vehicle warning (#8329)

* TWC: allow loadpoint to wakeup vehicle (#8284)

* chore: use gridx consts

* Fix ISO15118 vehicle (#8402)

Such vehicles would be reported as `(Offline)` previously, as no Soc can be fetched and thus needs to behave as the `offline` template.

The SoC is fetched via the charger, so hardcode it to 0 in here

* Modbus: add coils  (#8385)

* OpenEVSE: fix api (#8415)

* chore: refactor offline vehicles (#8404)

* Easee: wait for api confirmation (#8307)

* Revert "Easee: wait for api confirmation (#8307)"

This reverts commit ce0e4277aa.

* automatically switch session log energy unit (#8371)

Show the charged energy in the session log table and details in Wh for
values below 1 kWh and in kWh for larger values

* Fix nightly/release + integration workflow  (#8450)

* Revert "Add e2e tests with playwright (#8123)"

This reverts commit 0df815729b.

* Revert "Revert "Add e2e tests with playwright (#8123)""

This reverts commit 2299306954.

* fix nightly/release

* fix nightly/release

* fix nightly/release

---------

Co-authored-by: andig <cpuidle@gmx.de>

* chore: no integration on nightly

* chore: no integration on nightly

* chore: no integration on nightly

* Sessions: filter, monthly sums, paging (#8278)

* fmt

* Update i18n/en.toml

Co-authored-by: Simon Riepl <43091717+savus4@users.noreply.github.com>

* Use exp/slices

* Use mux consistently

* add integration tests

---------

Co-authored-by: andig <cpuidle@gmx.de>
Co-authored-by: C64Axel <33828760+C64Axel@users.noreply.github.com>
Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com>
Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: Dusan Suja <bc.suja.dusan@googlemail.com>
Co-authored-by: Arna Lepikkö <arna.lepikko@telemail.fi>
Co-authored-by: ThinkEV <claas@rootdir.de>
Co-authored-by: Žiga Deisinger <ziga@deisinger.si>
Co-authored-by: Norbert Poch <npoch@yahoo.com>
Co-authored-by: Ruben Van Boxem <vanboxem.ruben@gmail.com>
Co-authored-by: salz3n <79696598+salz3n@users.noreply.github.com>
Co-authored-by: Daniel Paschke <paschdan@gmail.com>
Co-authored-by: Andreas Linde <42185+DerAndereAndi@users.noreply.github.com>
Co-authored-by: lex777777 <84906358+lex777777@users.noreply.github.com>
Co-authored-by: Oscar <OAltr@users.noreply.github.com>
Co-authored-by: Michael Heß <GrimmiMeloni@users.noreply.github.com>
Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com>
Co-authored-by: kscholty <47229207+kscholty@users.noreply.github.com>
Co-authored-by: Jan Alexander <jan.alexander@posteo.de>
Co-authored-by: Simon Riepl <43091717+savus4@users.noreply.github.com>
2023-08-02 15:43:00 +02:00

445 lines
10 KiB
Go

package core
import (
"errors"
"fmt"
"math"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/core/wrapper"
)
var _ loadpoint.API = (*Loadpoint)(nil)
// Title returns the human-readable loadpoint title
func (lp *Loadpoint) Title() string {
return lp.Title_
}
// GetStatus returns the charging status
func (lp *Loadpoint) GetStatus() api.ChargeStatus {
lp.Lock()
defer lp.Unlock()
return lp.status
}
// GetMode returns loadpoint charge mode
func (lp *Loadpoint) GetMode() api.ChargeMode {
lp.Lock()
defer lp.Unlock()
return lp.Mode
}
// SetMode sets loadpoint charge mode
func (lp *Loadpoint) SetMode(mode api.ChargeMode) {
lp.Lock()
defer lp.Unlock()
if _, err := api.ChargeModeString(mode.String()); err != nil {
lp.log.ERROR.Printf("invalid charge mode: %s", string(mode))
return
}
lp.log.DEBUG.Printf("set charge mode: %s", string(mode))
// apply immediately
if lp.Mode != mode {
lp.Mode = mode
lp.publish("mode", mode)
// reset timers
switch mode {
case api.ModeNow, api.ModeOff:
lp.resetPhaseTimer()
lp.resetPVTimer()
lp.setPlanActive(false)
case api.ModeMinPV:
lp.resetPVTimer()
}
lp.requestUpdate()
}
}
// getChargedEnergy returns loadpoint charge target energy in Wh
func (lp *Loadpoint) getChargedEnergy() float64 {
lp.Lock()
defer lp.Unlock()
return lp.sessionEnergy.TotalWh()
}
// GetTargetEnergy returns loadpoint charge target energy
func (lp *Loadpoint) GetTargetEnergy() float64 {
lp.Lock()
defer lp.Unlock()
return lp.targetEnergy
}
// setTargetEnergy sets loadpoint charge target energy (no mutex)
func (lp *Loadpoint) setTargetEnergy(energy float64) {
lp.targetEnergy = energy
lp.publish(targetEnergy, energy)
}
// SetTargetEnergy sets loadpoint charge target energy
func (lp *Loadpoint) SetTargetEnergy(energy float64) {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("set target energy:", energy)
// apply immediately
if lp.targetEnergy != energy {
lp.setTargetEnergy(energy)
lp.requestUpdate()
lp.persistVehicleSettings()
}
}
// GetPriority returns the loadpoint priority
func (lp *Loadpoint) GetPriority() int {
lp.Lock()
defer lp.Unlock()
return lp.Priority_
}
// SetPriority sets the loadpoint priority
func (lp *Loadpoint) SetPriority(prio int) {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("set priority:", prio)
if lp.Priority_ != prio {
lp.Priority_ = prio
lp.publish("priority", prio)
}
}
// GetTargetSoc returns loadpoint charge target soc
func (lp *Loadpoint) GetTargetSoc() int {
lp.Lock()
defer lp.Unlock()
return lp.Soc.target
}
// setTargetSoc sets loadpoint charge target soc (no mutex)
func (lp *Loadpoint) setTargetSoc(soc int) {
lp.Soc.target = soc
lp.publish(targetSoc, soc)
}
// SetTargetSoc sets loadpoint charge target soc
func (lp *Loadpoint) SetTargetSoc(soc int) {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("set target soc:", soc)
// apply immediately
if lp.Soc.target != soc {
lp.setTargetSoc(soc)
lp.requestUpdate()
lp.persistVehicleSettings()
}
}
// GetMinSoc returns loadpoint charge minimum soc
func (lp *Loadpoint) GetMinSoc() int {
lp.Lock()
defer lp.Unlock()
return lp.Soc.min
}
// setMinSoc sets loadpoint charge min soc (no mutex)
func (lp *Loadpoint) setMinSoc(soc int) {
lp.Soc.min = soc
lp.publish(minSoc, soc)
}
// SetMinSoc sets loadpoint charge minimum soc
func (lp *Loadpoint) SetMinSoc(soc int) {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("set min soc:", soc)
// apply immediately
if lp.Soc.min != soc {
lp.setMinSoc(soc)
lp.requestUpdate()
lp.persistVehicleSettings()
}
}
// GetPhases returns loadpoint enabled phases
func (lp *Loadpoint) GetPhases() int {
lp.Lock()
defer lp.Unlock()
return lp.phases
}
// SetPhases sets loadpoint enabled phases
func (lp *Loadpoint) SetPhases(phases int) error {
// limit auto mode (phases=0) to scalable charger
if _, ok := lp.charger.(api.PhaseSwitcher); !ok && phases == 0 {
return fmt.Errorf("invalid number of phases: %d", phases)
}
if phases != 0 && phases != 1 && phases != 3 {
return fmt.Errorf("invalid number of phases: %d", phases)
}
// set new default
lp.log.DEBUG.Println("set phases:", phases)
lp.setConfiguredPhases(phases)
// apply immediately if not 1p3p
if _, ok := lp.charger.(api.PhaseSwitcher); !ok {
lp.setPhases(phases)
}
lp.requestUpdate()
return nil
}
// GetTargetTime returns the target time
func (lp *Loadpoint) GetTargetTime() time.Time {
lp.Lock()
defer lp.Unlock()
return lp.targetTime
}
// SetTargetTime sets the charge target time
func (lp *Loadpoint) SetTargetTime(finishAt time.Time) error {
if !finishAt.IsZero() && finishAt.Before(time.Now()) {
return errors.New("timestamp is in the past")
}
lp.Lock()
defer lp.Unlock()
lp.setTargetTime(finishAt)
lp.persistVehicleSettings()
return nil
}
// setTargetTime sets the charge target time
func (lp *Loadpoint) setTargetTime(finishAt time.Time) {
lp.targetTime = finishAt
lp.publish(targetTime, finishAt)
// TODO planActive is not guarded by mutex
if finishAt.IsZero() {
lp.setPlanActive(false)
}
}
// GetEnableThreshold gets the loadpoint enable threshold
func (lp *Loadpoint) GetEnableThreshold() float64 {
lp.Lock()
defer lp.Unlock()
return lp.Enable.Threshold
}
// SetEnableThreshold sets loadpoint enable threshold
func (lp *Loadpoint) SetEnableThreshold(threshold float64) {
lp.Lock()
defer lp.Unlock()
if lp.Enable.Threshold != threshold {
lp.Enable.Threshold = threshold
}
}
// GetDisableThreshold gets the loadpoint enable threshold
func (lp *Loadpoint) GetDisableThreshold() float64 {
lp.Lock()
defer lp.Unlock()
return lp.Disable.Threshold
}
// SetDisableThreshold sets loadpoint disable threshold
func (lp *Loadpoint) SetDisableThreshold(threshold float64) {
lp.Lock()
defer lp.Unlock()
if lp.Disable.Threshold != threshold {
lp.Disable.Threshold = threshold
}
}
// RemoteControl sets remote status demand
func (lp *Loadpoint) RemoteControl(source string, demand loadpoint.RemoteDemand) {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("remote demand:", demand)
// apply immediately
if lp.remoteDemand != demand {
lp.remoteDemand = demand
lp.publish("remoteDisabled", demand)
lp.publish("remoteDisabledSource", source)
lp.requestUpdate()
}
}
// HasChargeMeter determines if a physical charge meter is attached
func (lp *Loadpoint) HasChargeMeter() bool {
_, isWrapped := lp.chargeMeter.(*wrapper.ChargeMeter)
return lp.chargeMeter != nil && !isWrapped
}
// GetChargePower returns the current charge power
func (lp *Loadpoint) GetChargePower() float64 {
lp.Lock()
defer lp.Unlock()
return lp.chargePower
}
// GetChargePowerFlexibility returns the flexible amount of current charging power
func (lp *Loadpoint) GetChargePowerFlexibility() float64 {
// no locking
mode := lp.GetMode()
if mode == api.ModeNow || !lp.charging() || lp.minSocNotReached() {
return 0
}
if mode == api.ModePV {
return lp.GetChargePower()
}
// MinPV mode
return math.Max(0, lp.GetChargePower()-lp.GetMinPower())
}
// GetMinCurrent returns the min loadpoint current
func (lp *Loadpoint) GetMinCurrent() float64 {
lp.Lock()
defer lp.Unlock()
return lp.MinCurrent
}
// SetMinCurrent sets the min loadpoint current
func (lp *Loadpoint) SetMinCurrent(current float64) {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("set min current:", current)
if current != lp.MinCurrent {
lp.MinCurrent = current
lp.publish(minCurrent, lp.MinCurrent)
}
}
// GetMaxCurrent returns the max loadpoint current
func (lp *Loadpoint) GetMaxCurrent() float64 {
lp.Lock()
defer lp.Unlock()
return lp.MaxCurrent
}
// SetMaxCurrent sets the max loadpoint current
func (lp *Loadpoint) SetMaxCurrent(current float64) {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("set max current:", current)
if current != lp.MaxCurrent {
lp.MaxCurrent = current
lp.publish(maxCurrent, lp.MaxCurrent)
}
}
// GetMinPower returns the min loadpoint power for a single phase
func (lp *Loadpoint) GetMinPower() float64 {
return Voltage * lp.GetMinCurrent()
}
// GetMaxPower returns the max loadpoint power taking vehicle capabilities and phase scaling into account
func (lp *Loadpoint) GetMaxPower() float64 {
return Voltage * lp.GetMaxCurrent() * float64(lp.maxActivePhases())
}
// SetRemainingDuration sets the estimated remaining charging duration
func (lp *Loadpoint) SetRemainingDuration(chargeRemainingDuration time.Duration) {
lp.Lock()
defer lp.Unlock()
lp.setRemainingDuration(chargeRemainingDuration)
}
// setRemainingDuration sets the estimated remaining charging duration (no mutex)
func (lp *Loadpoint) setRemainingDuration(remainingDuration time.Duration) {
if lp.chargeRemainingDuration != remainingDuration {
lp.chargeRemainingDuration = remainingDuration
lp.publish(chargeRemainingDuration, remainingDuration)
}
}
// GetRemainingDuration is the estimated remaining charging duration
func (lp *Loadpoint) GetRemainingDuration() time.Duration {
lp.Lock()
defer lp.Unlock()
return lp.chargeRemainingDuration
}
// SetRemainingEnergy sets the remaining charge energy in Wh
func (lp *Loadpoint) SetRemainingEnergy(chargeRemainingEnergy float64) {
lp.Lock()
defer lp.Unlock()
lp.setRemainingEnergy(chargeRemainingEnergy)
}
// setRemainingEnergy sets the remaining charge energy in Wh (no mutex)
func (lp *Loadpoint) setRemainingEnergy(chargeRemainingEnergy float64) {
if lp.chargeRemainingEnergy != chargeRemainingEnergy {
lp.chargeRemainingEnergy = chargeRemainingEnergy
lp.publish("chargeRemainingEnergy", chargeRemainingEnergy)
}
}
// GetRemainingEnergy is the remaining charge energy in Wh
func (lp *Loadpoint) GetRemainingEnergy() float64 {
lp.Lock()
defer lp.Unlock()
return lp.chargeRemainingEnergy
}
// GetVehicle gets the active vehicle
func (lp *Loadpoint) GetVehicle() api.Vehicle {
lp.vehicleMux.Lock()
defer lp.vehicleMux.Unlock()
return lp.vehicle
}
// SetVehicle sets the active vehicle
func (lp *Loadpoint) SetVehicle(vehicle api.Vehicle) {
// set desired vehicle (protected by lock, no locking here)
lp.setActiveVehicle(vehicle)
lp.vehicleMux.Lock()
defer lp.vehicleMux.Unlock()
// disable auto-detect
lp.stopVehicleDetection()
}
// StartVehicleDetection allows triggering vehicle detection for debugging purposes
func (lp *Loadpoint) StartVehicleDetection() {
// reset vehicle
lp.setActiveVehicle(nil)
lp.Lock()
defer lp.Unlock()
// start auto-detect
lp.startVehicleDetection()
}