Add battery mode api (#20455)
This commit is contained in:
parent
2d31df8e12
commit
7e9bccf0b8
9 changed files with 226 additions and 15 deletions
|
|
@ -53,4 +53,7 @@ const (
|
|||
BatteryMode = "batteryMode"
|
||||
BatteryPower = "batteryPower"
|
||||
BatterySoc = "batterySoc"
|
||||
|
||||
// external battery control
|
||||
BatteryModeExternal = "batteryModeExternal"
|
||||
)
|
||||
|
|
|
|||
18
core/site.go
18
core/site.go
|
|
@ -103,14 +103,16 @@ type Site struct {
|
|||
pvEnergy map[string]*meterEnergy
|
||||
|
||||
// cached state
|
||||
gridPower float64 // Grid power
|
||||
pvPower float64 // PV power
|
||||
excessDCPower float64 // PV excess DC charge power (hybrid only)
|
||||
auxPower float64 // Aux power
|
||||
batteryPower float64 // Battery power (charge negative, discharge positive)
|
||||
batterySoc float64 // Battery soc
|
||||
batteryCapacity float64 // Battery capacity
|
||||
batteryMode api.BatteryMode // Battery mode (runtime only, not persisted)
|
||||
gridPower float64 // Grid power
|
||||
pvPower float64 // PV power
|
||||
excessDCPower float64 // PV excess DC charge power (hybrid only)
|
||||
auxPower float64 // Aux power
|
||||
batteryPower float64 // Battery power (charge negative, discharge positive)
|
||||
batterySoc float64 // Battery soc
|
||||
batteryCapacity float64 // Battery capacity
|
||||
batteryMode api.BatteryMode // Battery mode (runtime only, not persisted)
|
||||
batteryModeExternal api.BatteryMode // Battery mode (external, runtime only, not persisted)
|
||||
batteryModeExternalTimer time.Time // Battery mode timer for external control
|
||||
}
|
||||
|
||||
// MetersConfig contains the site's meter configuration
|
||||
|
|
|
|||
|
|
@ -67,4 +67,13 @@ type API interface {
|
|||
|
||||
GetBatteryDischargeControl() bool
|
||||
SetBatteryDischargeControl(bool) error
|
||||
|
||||
//
|
||||
// battery control external
|
||||
//
|
||||
|
||||
// GetBatteryModeExternal returns the external battery mode
|
||||
GetBatteryModeExternal() api.BatteryMode
|
||||
// SetBatteryModeExternal sets the external battery mode
|
||||
SetBatteryModeExternal(api.BatteryMode)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package core
|
|||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/keys"
|
||||
|
|
@ -335,3 +336,65 @@ func (site *Site) SetBatteryGridChargeLimit(val *float64) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetBatteryMode returns the battery mode
|
||||
func (site *Site) GetBatteryMode() api.BatteryMode {
|
||||
site.RLock()
|
||||
defer site.RUnlock()
|
||||
return site.batteryMode
|
||||
}
|
||||
|
||||
// GetBatteryModeExternal returns the external battery mode
|
||||
func (site *Site) GetBatteryModeExternal() api.BatteryMode {
|
||||
site.RLock()
|
||||
defer site.RUnlock()
|
||||
return site.batteryModeExternal
|
||||
}
|
||||
|
||||
// SetBatteryModeExternal sets the external battery mode
|
||||
func (site *Site) SetBatteryModeExternal(mode api.BatteryMode) {
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
|
||||
site.log.DEBUG.Printf("set external battery mode: %s", mode.String())
|
||||
|
||||
disable := mode == api.BatteryUnknown
|
||||
|
||||
if mode != site.batteryModeExternal {
|
||||
site.batteryModeExternal = mode
|
||||
site.publish(keys.BatteryModeExternal, mode)
|
||||
|
||||
if !disable {
|
||||
site.setBatteryMode(mode)
|
||||
|
||||
// start watchdog if not running
|
||||
if site.batteryModeExternalTimer.IsZero() {
|
||||
go func() {
|
||||
for range time.Tick(time.Second) {
|
||||
if site.batteryModeWatchdogExpired() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reset timer
|
||||
if !disable {
|
||||
site.batteryModeExternalTimer = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (site *Site) batteryModeWatchdogExpired() bool {
|
||||
site.RLock()
|
||||
elapsed := time.Since(site.batteryModeExternalTimer)
|
||||
site.RUnlock()
|
||||
|
||||
if elapsed > time.Minute && !site.batteryModeExternalTimer.IsZero() {
|
||||
site.SetBatteryModeExternal(api.BatteryUnknown)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package core
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/keys"
|
||||
|
|
@ -16,13 +17,6 @@ func (site *Site) batteryConfigured() bool {
|
|||
return len(site.batteryMeters) > 0
|
||||
}
|
||||
|
||||
// GetBatteryMode returns the battery mode
|
||||
func (site *Site) GetBatteryMode() api.BatteryMode {
|
||||
site.RLock()
|
||||
defer site.RUnlock()
|
||||
return site.batteryMode
|
||||
}
|
||||
|
||||
// setBatteryMode sets the battery mode
|
||||
func (site *Site) setBatteryMode(batMode api.BatteryMode) {
|
||||
site.batteryMode = batMode
|
||||
|
|
@ -39,12 +33,24 @@ func (site *Site) SetBatteryMode(batMode api.BatteryMode) {
|
|||
if site.batteryMode != batMode {
|
||||
site.setBatteryMode(batMode)
|
||||
}
|
||||
|
||||
if site.batteryModeExternal == api.BatteryUnknown {
|
||||
site.batteryModeExternalTimer = time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
// requiredBatteryMode determines required battery mode based on grid charge and rate
|
||||
func (site *Site) requiredBatteryMode(batteryGridChargeActive bool, rate api.Rate) api.BatteryMode {
|
||||
var res api.BatteryMode
|
||||
batMode := site.GetBatteryMode()
|
||||
extMode := site.GetBatteryModeExternal()
|
||||
|
||||
var extModeReset bool
|
||||
if extMode == api.BatteryUnknown {
|
||||
site.Lock()
|
||||
extModeReset = !site.batteryModeExternalTimer.IsZero()
|
||||
site.Unlock()
|
||||
}
|
||||
|
||||
mapper := func(s api.BatteryMode) api.BatteryMode {
|
||||
return map[bool]api.BatteryMode{false: s, true: api.BatteryUnknown}[batMode == s]
|
||||
|
|
@ -53,6 +59,11 @@ func (site *Site) requiredBatteryMode(batteryGridChargeActive bool, rate api.Rat
|
|||
switch {
|
||||
case !site.batteryConfigured():
|
||||
res = api.BatteryUnknown
|
||||
case extModeReset:
|
||||
// require normal mode to leave external control
|
||||
res = api.BatteryNormal
|
||||
case extMode != api.BatteryUnknown:
|
||||
res = extMode
|
||||
case batteryGridChargeActive:
|
||||
res = mapper(api.BatteryCharge)
|
||||
case site.dischargeControlActive(rate):
|
||||
|
|
|
|||
92
core/site_battery_test.go
Normal file
92
core/site_battery_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExternalBatteryMode(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
internal, ext, new api.BatteryMode
|
||||
}{
|
||||
{api.BatteryUnknown, api.BatteryUnknown, api.BatteryUnknown},
|
||||
{api.BatteryUnknown, api.BatteryNormal, api.BatteryNormal},
|
||||
{api.BatteryUnknown, api.BatteryCharge, api.BatteryCharge},
|
||||
|
||||
{api.BatteryNormal, api.BatteryUnknown, api.BatteryUnknown},
|
||||
{api.BatteryNormal, api.BatteryNormal, api.BatteryNormal},
|
||||
{api.BatteryNormal, api.BatteryCharge, api.BatteryCharge},
|
||||
|
||||
{api.BatteryCharge, api.BatteryUnknown, api.BatteryNormal},
|
||||
{api.BatteryCharge, api.BatteryNormal, api.BatteryNormal},
|
||||
{api.BatteryCharge, api.BatteryCharge, api.BatteryCharge},
|
||||
} {
|
||||
t.Logf("%+v", tc)
|
||||
|
||||
site := &Site{
|
||||
log: util.NewLogger("foo"),
|
||||
batteryMeters: []config.Device[api.Meter]{nil},
|
||||
}
|
||||
|
||||
site.batteryMode = tc.internal
|
||||
site.batteryModeExternal = tc.ext
|
||||
|
||||
mode := site.requiredBatteryMode(false, api.Rate{})
|
||||
assert.Equal(t, tc.new.String(), mode.String(), "internal mode expected %s got %s", tc.new, mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalBatteryModeChange(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
internal, ext, expired api.BatteryMode
|
||||
}{
|
||||
{api.BatteryUnknown, api.BatteryUnknown, api.BatteryNormal},
|
||||
{api.BatteryUnknown, api.BatteryNormal, api.BatteryNormal},
|
||||
{api.BatteryUnknown, api.BatteryCharge, api.BatteryNormal},
|
||||
|
||||
{api.BatteryNormal, api.BatteryUnknown, api.BatteryNormal},
|
||||
{api.BatteryNormal, api.BatteryNormal, api.BatteryNormal},
|
||||
{api.BatteryNormal, api.BatteryCharge, api.BatteryNormal},
|
||||
|
||||
{api.BatteryCharge, api.BatteryUnknown, api.BatteryNormal},
|
||||
{api.BatteryCharge, api.BatteryNormal, api.BatteryNormal},
|
||||
{api.BatteryCharge, api.BatteryCharge, api.BatteryNormal},
|
||||
} {
|
||||
t.Logf("%+v", tc)
|
||||
|
||||
site := &Site{
|
||||
log: util.NewLogger("foo"),
|
||||
batteryMeters: []config.Device[api.Meter]{nil},
|
||||
}
|
||||
|
||||
site.batteryMode = tc.internal
|
||||
|
||||
assert.True(t, site.batteryModeExternalTimer.IsZero())
|
||||
site.SetBatteryModeExternal(tc.ext)
|
||||
|
||||
// timer started
|
||||
if tc.ext != api.BatteryUnknown {
|
||||
assert.False(t, site.batteryModeExternalTimer.IsZero())
|
||||
}
|
||||
|
||||
// expire timer
|
||||
site.batteryModeExternalTimer = site.batteryModeExternalTimer.Add(-time.Hour)
|
||||
site.batteryModeWatchdogExpired()
|
||||
|
||||
// mode reverted to unknown, timer still active
|
||||
assert.Equal(t, site.batteryModeExternal, api.BatteryUnknown)
|
||||
assert.False(t, site.batteryModeExternalTimer.IsZero())
|
||||
|
||||
mode := site.requiredBatteryMode(false, api.Rate{})
|
||||
assert.Equal(t, tc.expired.String(), mode.String(), "external mode expected %s got %s", tc.expired, mode)
|
||||
|
||||
// timer disabled
|
||||
site.SetBatteryMode(mode)
|
||||
assert.True(t, site.batteryModeExternalTimer.IsZero())
|
||||
}
|
||||
}
|
||||
|
|
@ -138,6 +138,8 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
|
|||
"batterydischargecontrol": {"POST", "/batterydischargecontrol/{value:[01truefalse]+}", boolHandler(site.SetBatteryDischargeControl, site.GetBatteryDischargeControl)},
|
||||
"batterygridcharge": {"POST", "/batterygridchargelimit/{value:-?[0-9.]+}", floatPtrHandler(pass(site.SetBatteryGridChargeLimit), site.GetBatteryGridChargeLimit)},
|
||||
"batterygridchargedelete": {"DELETE", "/batterygridchargelimit", floatPtrHandler(pass(site.SetBatteryGridChargeLimit), site.GetBatteryGridChargeLimit)},
|
||||
"batterymode": {"POST", "/batterymode/{value:[a-z]+}", updateBatteryMode(site)},
|
||||
"batterymodedelete": {"DELETE", "/batterymode", updateBatteryMode(site)},
|
||||
"prioritysoc": {"POST", "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoc, site.GetPrioritySoc)},
|
||||
"residualpower": {"POST", "/residualpower/{value:-?[0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
|
||||
"smartcost": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", updateSmartCostLimit(site)},
|
||||
|
|
|
|||
|
|
@ -196,6 +196,28 @@ func updateSmartCostLimit(site site.API) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// updateBatteryMode sets the external battery mode
|
||||
func updateBatteryMode(site site.API) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
var val api.BatteryMode
|
||||
|
||||
if r.Method != http.MethodDelete {
|
||||
s, err := api.BatteryModeString(vars["value"])
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
val = s
|
||||
}
|
||||
|
||||
site.SetBatteryModeExternal(val)
|
||||
|
||||
jsonResult(w, site.GetBatteryModeExternal())
|
||||
}
|
||||
}
|
||||
|
||||
// stateHandler returns the combined state
|
||||
func stateHandler(cache *util.ParamCache) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/evcc-io/evcc/core/vehicle"
|
||||
"github.com/evcc-io/evcc/plugin/mqtt"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// MQTTMarshaler is the interface implemented by types that
|
||||
|
|
@ -211,6 +212,12 @@ func (m *MQTT) listenSiteSetters(topic string, site site.API) error {
|
|||
}
|
||||
}))},
|
||||
{"batteryGridChargeLimit", floatPtrSetter(pass(site.SetBatteryGridChargeLimit))},
|
||||
{"batteryMode", ptrSetter(api.BatteryModeString, pass(func(m *api.BatteryMode) {
|
||||
if m == nil {
|
||||
m = lo.ToPtr(api.BatteryUnknown)
|
||||
}
|
||||
site.SetBatteryModeExternal(*m)
|
||||
}))},
|
||||
} {
|
||||
if err := m.Handler.ListenSetter(topic+"/"+s.topic, s.fun); err != nil {
|
||||
return err
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue