Improve action handling to consistently apply and reset vehicle settings (#1942)

allow GetTargetSoC() and GetMinSoC() to be used even when no vehicle is connected
use SetMinSoC() and SetTargetSoC() functions in applyAction()
allow to set minSoC to 0 with applyAction()
apply OnIdentify actions only when OnIdentify node is present in vehicle configuration (optional parameter)
This commit is contained in:
rivengh 2021-12-01 13:49:59 +01:00 • committed by GitHub
parent d9c4cc5108
commit a45e240253
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 67 additions and 72 deletions

View file

@ -1,6 +1,10 @@
package api
import "time"
import (
"encoding/json"
"fmt"
"time"
)
//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/evcc-io/evcc/api Charger,ChargeState,ChargePhases,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery
@ -42,11 +46,20 @@ func (c ChargeStatus) String() string {
// ActionConfig defines an action to take on event
type ActionConfig struct {
Mode ChargeMode `mapstructure:"mode"` // Charge mode
MinCurrent float64 `mapstructure:"minCurrent"` // Minimum Current
MaxCurrent float64 `mapstructure:"maxCurrent"` // Maximum Current
MinSoC int `mapstructure:"minSoC"` // Minimum SoC
TargetSoC int `mapstructure:"targetSoC"` // Target SoC
Mode *ChargeMode `mapstructure:"mode,omitempty"` // Charge Mode
MinCurrent *float64 `mapstructure:"minCurrent,omitempty"` // Minimum Current
MaxCurrent *float64 `mapstructure:"maxCurrent,omitempty"` // Maximum Current
MinSoC *int `mapstructure:"minSoC,omitempty"` // Minimum SoC
TargetSoC *int `mapstructure:"targetSoC,omitempty"` // Target SoC
}
// String implements Stringer
func (a ActionConfig) String() string {
if data, err := json.Marshal(a); err != nil {
return fmt.Sprintf("%v\n", err)
} else {
return fmt.Sprintf("%s\n", data)
}
}
// Meter is able to provide current power in W

View file

@ -162,6 +162,12 @@ func (d *dumper) Dump(name string, v interface{}) {
}
}
if v, ok := v.(api.Vehicle); ok {
fmt.Fprintf(w, "Capacity:\t%dkWh\n", v.Capacity())
fmt.Fprintf(w, "Identifiers:\t%v\n", v.Identifiers())
fmt.Fprintf(w, "OnIdentified:\t%v\n", v.OnIdentified())
}
// Identity
if v, ok := v.(api.Identifier); ok {
@ -177,10 +183,5 @@ func (d *dumper) Dump(name string, v interface{}) {
v.Diagnose()
}
if v, ok := v.(api.Vehicle); ok {
fmt.Fprintf(w, "Capacity:\t%dkWh\n", v.Capacity())
fmt.Fprintf(w, "Identifiers:\t%v\n", v.Identifiers())
}
w.Flush()
}

View file

@ -21,6 +21,7 @@ import (
evbus "github.com/asaskevich/EventBus"
"github.com/avast/retry-go/v3"
"github.com/benbjohnson/clock"
"github.com/cjrd/allocate"
)
const (
@ -171,13 +172,6 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
}
}
if lp.SoC.Target == 0 {
lp.SoC.Target = lp.onDisconnect.TargetSoC // use disconnect value as default soc
if lp.SoC.Target == 0 {
lp.SoC.Target = 100
}
}
if lp.MinCurrent == 0 {
lp.log.WARN.Println("minCurrent must not be zero")
}
@ -242,8 +236,9 @@ func NewLoadPoint(log *util.Logger) *LoadPoint {
Mode: api.ModeOff,
Phases: 3,
status: api.StatusNone,
MinCurrent: 6, // A
MaxCurrent: 16, // A
MinCurrent: 6, // A
MaxCurrent: 16, // A
SoC: SoCConfig{Min: 0, Target: 100}, // %
GuardDuration: 5 * time.Minute,
}
@ -252,12 +247,19 @@ func NewLoadPoint(log *util.Logger) *LoadPoint {
// collectDefaults collects default values for use on disconnect
func (lp *LoadPoint) collectDefaults() {
lp.onDisconnect = api.ActionConfig{
Mode: lp.GetMode(),
MinCurrent: lp.GetMinCurrent(),
MaxCurrent: lp.GetMaxCurrent(),
MinSoC: lp.GetMinSoC(),
TargetSoC: lp.GetTargetSoC(),
// get reference to action config
actionCfg := &lp.onDisconnect
// allocate action config such that all pointer fields are fully allocated
if err := allocate.Zero(actionCfg); err == nil {
// initialize with default values
*actionCfg.Mode = lp.GetMode()
*actionCfg.MinCurrent = lp.GetMinCurrent()
*actionCfg.MaxCurrent = lp.GetMaxCurrent()
*actionCfg.MinSoC = lp.GetMinSoC()
*actionCfg.TargetSoC = lp.GetTargetSoC()
} else {
lp.log.ERROR.Printf("error allocating action config: %v", err)
}
}
@ -420,29 +422,21 @@ func (lp *LoadPoint) evChargeCurrentWrappedMeterHandler(current float64) {
}
// applyAction executes the action
func (lp *LoadPoint) applyAction(action api.ActionConfig) {
if action.Mode != api.ModeEmpty {
lp.SetMode(action.Mode)
func (lp *LoadPoint) applyAction(actionCfg api.ActionConfig) {
if actionCfg.Mode != nil {
lp.SetMode(*actionCfg.Mode)
}
if action.MinCurrent > 0 {
lp.SetMinCurrent(action.MinCurrent)
if actionCfg.MinCurrent != nil {
lp.SetMinCurrent(*actionCfg.MinCurrent)
}
if action.MaxCurrent > 0 {
lp.SetMaxCurrent(action.MaxCurrent)
if actionCfg.MaxCurrent != nil {
lp.SetMaxCurrent(*actionCfg.MaxCurrent)
}
if action.MinSoC != 0 {
// TODO deduplicate with SetMinSoC
lp.Lock()
lp.SoC.Min = action.MinSoC
lp.publish("minSoC", action.MinSoC)
lp.Unlock()
if actionCfg.MinSoC != nil {
lp.SetMinSoC(*actionCfg.MinSoC)
}
if action.TargetSoC != 0 {
// TODO deduplicate with SetTargetSoC
lp.Lock()
lp.SoC.Target = action.TargetSoC
lp.publish("targetSoC", action.TargetSoC)
lp.Unlock()
if actionCfg.TargetSoC != nil {
lp.SetTargetSoC(*actionCfg.TargetSoC)
}
}

View file

@ -36,11 +36,11 @@ type API interface {
// GetTargetSoC returns the charge target soc
GetTargetSoC() int
// SetTargetSoC sets the charge target soc
SetTargetSoC(int) error
SetTargetSoC(int)
// GetMinSoC returns the charge minimum soc
GetMinSoC() int
// SetMinSoC sets the charge minimum soc
SetMinSoC(int) error
SetMinSoC(int)
// GetPhases returns the enabled phases
GetPhases() int
// SetPhases sets the enabled phases

View file

@ -56,14 +56,10 @@ func (lp *LoadPoint) GetTargetSoC() int {
}
// SetTargetSoC sets loadpoint charge target soc
func (lp *LoadPoint) SetTargetSoC(soc int) error {
func (lp *LoadPoint) SetTargetSoC(soc int) {
lp.Lock()
defer lp.Unlock()
if lp.vehicle == nil {
return api.ErrNotAvailable
}
lp.log.INFO.Println("set target soc:", soc)
// apply immediately
@ -72,8 +68,6 @@ func (lp *LoadPoint) SetTargetSoC(soc int) error {
lp.publish("targetSoC", soc)
lp.requestUpdate()
}
return nil
}
// GetMinSoC returns loadpoint charge minimum soc
@ -84,14 +78,10 @@ func (lp *LoadPoint) GetMinSoC() int {
}
// SetMinSoC sets loadpoint charge minimum soc
func (lp *LoadPoint) SetMinSoC(soc int) error {
func (lp *LoadPoint) SetMinSoC(soc int) {
lp.Lock()
defer lp.Unlock()
if lp.vehicle == nil {
return api.ErrNotAvailable
}
lp.log.INFO.Println("set min soc:", soc)
// apply immediately
@ -100,8 +90,6 @@ func (lp *LoadPoint) SetMinSoC(soc int) error {
lp.publish("minSoC", soc)
lp.requestUpdate()
}
return nil
}
// GetPhases returns loadpoint enabled phases

1
go.mod
View file

@ -14,6 +14,7 @@ require (
github.com/benbjohnson/clock v1.3.0
github.com/bogosj/tesla v1.0.2
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cjrd/allocate v0.0.0-20191115010018-022b87fe59fc
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/containrrr/shoutrrr v0.5.2
github.com/deepmap/oapi-codegen v1.9.0 // indirect

2
go.sum
View file

@ -120,6 +120,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cjrd/allocate v0.0.0-20191115010018-022b87fe59fc h1:JtJ84VruFjlxE4igdwtW8+hs0Ep34lmdThEWVXkkOYQ=
github.com/cjrd/allocate v0.0.0-20191115010018-022b87fe59fc/go.mod h1:xCdduY82QBtGJbFbch7ShY3ltvP4/a+1kBh3HzGTaEk=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=

View file

@ -114,10 +114,8 @@ func targetSoCHandler(lp loadpoint.API) http.HandlerFunc {
soc, err := strconv.ParseInt(vars["value"], 10, 32)
if err == nil {
err = lp.SetTargetSoC(int(soc))
}
if err != nil {
lp.SetTargetSoC(int(soc))
} else {
jsonError(w, http.StatusBadRequest, err)
return
}
@ -133,10 +131,8 @@ func minSoCHandler(lp loadpoint.API) http.HandlerFunc {
soc, err := strconv.ParseInt(vars["value"], 10, 32)
if err == nil {
err = lp.SetMinSoC(int(soc))
}
if err != nil {
lp.SetMinSoC(int(soc))
} else {
jsonError(w, http.StatusBadRequest, err)
return
}

View file

@ -71,12 +71,12 @@ func (m *MQTT) listenSetters(topic string, apiHandler loadpoint.API) {
})
m.Handler.ListenSetter(topic+"/minSoC/set", func(payload string) {
if soc, err := strconv.Atoi(payload); err == nil {
_ = apiHandler.SetMinSoC(soc)
apiHandler.SetMinSoC(soc)
}
})
m.Handler.ListenSetter(topic+"/targetSoC/set", func(payload string) {
if soc, err := strconv.Atoi(payload); err == nil {
_ = apiHandler.SetTargetSoC(soc)
apiHandler.SetTargetSoC(soc)
}
})
m.Handler.ListenSetter(topic+"/minCurrent/set", func(payload string) {

View file

@ -35,7 +35,7 @@ func (v *Wrapper) Identifiers() []string {
return nil
}
// OnIdentified returns the identify action
// OnIdentified implements the api.Vehicle interface
func (v *Wrapper) OnIdentified() api.ActionConfig {
return api.ActionConfig{}
}