Skip to content

Commit

Permalink
Victron: add battery control (evcc-io#10753)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Nov 16, 2023
1 parent a08fe82 commit b92cbf8
Show file tree
Hide file tree
Showing 16 changed files with 1,719 additions and 124 deletions.
1 change: 0 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ type CurrentGetter interface {
// BatteryController optionally allows to control home battery (dis)charging behaviour
type BatteryController interface {
SetBatteryMode(BatteryMode) error
GetBatteryMode() BatteryMode
}

// Charger provides current charging status and enable/disable charging
Expand Down
4 changes: 2 additions & 2 deletions api/batterymode.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package api
// BatteryMode is the home battery operation mode. Valid values are normal, locked and charge
type BatteryMode int

//go:generate enumer -type BatteryMode
//go:generate enumer -type BatteryMode -trimprefix Battery -transform=lower
const (
BatteryUnknown BatteryMode = iota
BatteryNormal
BatteryLocked
BatteryHold
BatteryCharge
)
36 changes: 18 additions & 18 deletions api/batterymode_enumer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 0 additions & 14 deletions api/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions core/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ func NewSite() *Site {
log: util.NewLogger("site"),
publishCache: make(map[string]any),
Voltage: 230, // V
batteryMode: api.BatteryNormal,
}

return lp
Expand Down Expand Up @@ -824,7 +823,7 @@ func (site *Site) prepare() {

site.publish("vehicles", vehicleTitles(site.GetVehicles()))
site.publish("batteryDischargeControl", site.BatteryDischargeControl)
site.publish("batteryMode", site.batteryMode)
site.publish("batteryMode", site.batteryMode.String())
}

// Prepare attaches communication channels to site and loadpoints
Expand Down
4 changes: 2 additions & 2 deletions core/site_battery.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ func (site *Site) setBatteryMode(batMode api.BatteryMode) {
site.Lock()
defer site.Unlock()
site.batteryMode = batMode
site.publish("batteryMode", batMode.String())
}

func (site *Site) updateBatteryMode(loadpoints []loadpoint.API) {
// determine expected state
batMode := api.BatteryNormal
for _, lp := range loadpoints {
if lp.GetStatus() == api.StatusC && (lp.GetMode() == api.ModeNow || lp.GetPlanActive()) {
batMode = api.BatteryLocked
batMode = api.BatteryHold
break
}
}
Expand All @@ -45,5 +46,4 @@ func (site *Site) updateBatteryMode(loadpoints []loadpoint.API) {

// update state and publish
site.setBatteryMode(batMode)
site.publish("batteryMode", batMode)
}
6 changes: 3 additions & 3 deletions core/site_battery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ func TestBatteryDischarge(t *testing.T) {
}{
{api.StatusB, false, api.BatteryNormal, api.ModeOff}, // mode off -> bat enabled
{api.StatusB, false, api.BatteryNormal, api.ModeNow}, // mode now, not charging -> bat enabled
{api.StatusC, false, api.BatteryLocked, api.ModeNow}, // mode now, charging -> bat disabled
{api.StatusC, false, api.BatteryHold, api.ModeNow}, // mode now, charging -> bat disabled
{api.StatusB, false, api.BatteryNormal, api.ModeMinPV}, // mode minPV, not charging -> bat enabled
{api.StatusC, false, api.BatteryNormal, api.ModeMinPV}, // mode minPV, charging -> bat enabled
{api.StatusB, false, api.BatteryNormal, api.ModePV}, // mode PV, not charging -> bat enabled
{api.StatusC, false, api.BatteryNormal, api.ModePV}, // mode PV, charging, no planner -> bat enabled
{api.StatusC, true, api.BatteryLocked, api.ModePV}, // mode PV, charging, planner active -> bat disabled
{api.StatusC, true, api.BatteryHold, api.ModePV}, // mode PV, charging, planner active -> bat disabled
}

log := util.NewLogger("foo")
Expand Down Expand Up @@ -69,7 +69,7 @@ func TestBatteryModeNoUpdate(t *testing.T) {
api.NewMockBatteryController(ctrl),
api.NewMockMeter(ctrl),
}
batCtrl.MockBatteryController.EXPECT().SetBatteryMode(api.BatteryLocked).Times(1)
batCtrl.MockBatteryController.EXPECT().SetBatteryMode(api.BatteryHold).Times(1)

lp := loadpoint.NewMockAPI(ctrl)
lp.EXPECT().GetStatus().Return(api.StatusC).Times(2)
Expand Down
32 changes: 32 additions & 0 deletions meter/battery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package meter

import (
"github.com/evcc-io/evcc/api"
)

type battery struct {
MinSoc, MaxSoc float64
}

// Decorator returns an api.BatteryController decorator
func (m *battery) BatteryController(socG func() (float64, error), limitSocS func(float64) error) func(api.BatteryMode) error {
return func(mode api.BatteryMode) error {
switch mode {
case api.BatteryNormal:
return limitSocS(m.MinSoc)

case api.BatteryHold:
soc, err := socG()
if err != nil {
return err
}
return limitSocS(soc)

case api.BatteryCharge:
return limitSocS(m.MaxSoc)

default:
return api.ErrNotAvailable
}
}
}
38 changes: 29 additions & 9 deletions meter/meter.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,27 @@ func init() {
registry.Add(api.Custom, NewConfigurableFromConfig)
}

//go:generate go run ../cmd/tools/decorate.go -f decorateMeter -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.PhasePowers,Powers,func() (float64, float64, float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryCapacity,Capacity,func() float64"
// go:generate go run ../cmd/tools/decorate.go -f decorateMeter -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.PhasePowers,Powers,func() (float64, float64, float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryCapacity,Capacity,func() float64" -t "api.BatteryController,SetBatteryMode,func(api.BatteryMode) error"

// NewConfigurableFromConfig creates api.Meter from config
func NewConfigurableFromConfig(other map[string]interface{}) (api.Meter, error) {
var cc struct {
capacity `mapstructure:",squash"`
cc := struct {
Power provider.Config
Energy *provider.Config // optional
Soc *provider.Config // optional
Currents []provider.Config // optional
Voltages []provider.Config // optional
Powers []provider.Config // optional

// battery
capacity `mapstructure:",squash"`
battery `mapstructure:",squash"`
Soc *provider.Config // optional
LimitSoc *provider.Config // optional
}{
battery: battery{
MinSoc: 20,
MaxSoc: 95,
},
}

if err := util.DecodeOther(other, &cc); err != nil {
Expand Down Expand Up @@ -66,15 +75,25 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Meter, error)
}

// decorate soc
var batterySocG func() (float64, error)
var socG func() (float64, error)
if cc.Soc != nil {
batterySocG, err = provider.NewFloatGetterFromConfig(*cc.Soc)
socG, err = provider.NewFloatGetterFromConfig(*cc.Soc)
if err != nil {
return nil, fmt.Errorf("battery soc: %w", err)
}
}

var batModeS func(api.BatteryMode) error
if cc.Soc != nil && cc.LimitSoc != nil {
limitSocS, err := provider.NewFloatSetterFromConfig("limitSoc", *cc.LimitSoc)
if err != nil {
return nil, fmt.Errorf("battery: %w", err)
return nil, fmt.Errorf("battery limit soc: %w", err)
}

batModeS = cc.battery.BatteryController(socG, limitSocS)
}

res := m.Decorate(totalEnergyG, currentsG, voltagesG, powersG, batterySocG, cc.capacity.Decorator())
res := m.Decorate(totalEnergyG, currentsG, voltagesG, powersG, socG, cc.capacity.Decorator(), batModeS)

return res, nil
}
Expand Down Expand Up @@ -140,8 +159,9 @@ func (m *Meter) Decorate(
powers func() (float64, float64, float64, error),
batterySoc func() (float64, error),
capacity func() float64,
setBatteryMode func(api.BatteryMode) error,
) api.Meter {
return decorateMeter(m, totalEnergy, currents, voltages, powers, batterySoc, capacity)
return decorateMeter(m, totalEnergy, currents, voltages, powers, batterySoc, capacity, setBatteryMode)
}

// CurrentPower implements the api.Meter interface
Expand Down
2 changes: 1 addition & 1 deletion meter/meter_average.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func NewMovingAverageFromConfig(other map[string]interface{}) (api.Meter, error)
powers = m.Powers
}

return meter.Decorate(totalEnergy, currents, voltages, powers, batterySoc, cc.Meter.capacity.Decorator()), nil
return meter.Decorate(totalEnergy, currents, voltages, powers, batterySoc, cc.Meter.capacity.Decorator(), nil), nil
}

type MovingAverage struct {
Expand Down
Loading

0 comments on commit b92cbf8

Please sign in to comment.