Skip to content

Commit b92cbf8

Browse files
authored
Victron: add battery control (evcc-io#10753)
1 parent a08fe82 commit b92cbf8

File tree

16 files changed

+1719
-124
lines changed

16 files changed

+1719
-124
lines changed

api/api.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ type CurrentGetter interface {
137137
// BatteryController optionally allows to control home battery (dis)charging behaviour
138138
type BatteryController interface {
139139
SetBatteryMode(BatteryMode) error
140-
GetBatteryMode() BatteryMode
141140
}
142141

143142
// Charger provides current charging status and enable/disable charging

api/batterymode.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package api
33
// BatteryMode is the home battery operation mode. Valid values are normal, locked and charge
44
type BatteryMode int
55

6-
//go:generate enumer -type BatteryMode
6+
//go:generate enumer -type BatteryMode -trimprefix Battery -transform=lower
77
const (
88
BatteryUnknown BatteryMode = iota
99
BatteryNormal
10-
BatteryLocked
10+
BatteryHold
1111
BatteryCharge
1212
)

api/batterymode_enumer.go

Lines changed: 18 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/mock.go

Lines changed: 0 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/site.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,6 @@ func NewSite() *Site {
226226
log: util.NewLogger("site"),
227227
publishCache: make(map[string]any),
228228
Voltage: 230, // V
229-
batteryMode: api.BatteryNormal,
230229
}
231230

232231
return lp
@@ -824,7 +823,7 @@ func (site *Site) prepare() {
824823

825824
site.publish("vehicles", vehicleTitles(site.GetVehicles()))
826825
site.publish("batteryDischargeControl", site.BatteryDischargeControl)
827-
site.publish("batteryMode", site.batteryMode)
826+
site.publish("batteryMode", site.batteryMode.String())
828827
}
829828

830829
// Prepare attaches communication channels to site and loadpoints

core/site_battery.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ func (site *Site) setBatteryMode(batMode api.BatteryMode) {
1717
site.Lock()
1818
defer site.Unlock()
1919
site.batteryMode = batMode
20+
site.publish("batteryMode", batMode.String())
2021
}
2122

2223
func (site *Site) updateBatteryMode(loadpoints []loadpoint.API) {
2324
// determine expected state
2425
batMode := api.BatteryNormal
2526
for _, lp := range loadpoints {
2627
if lp.GetStatus() == api.StatusC && (lp.GetMode() == api.ModeNow || lp.GetPlanActive()) {
27-
batMode = api.BatteryLocked
28+
batMode = api.BatteryHold
2829
break
2930
}
3031
}
@@ -45,5 +46,4 @@ func (site *Site) updateBatteryMode(loadpoints []loadpoint.API) {
4546

4647
// update state and publish
4748
site.setBatteryMode(batMode)
48-
site.publish("batteryMode", batMode)
4949
}

core/site_battery_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ func TestBatteryDischarge(t *testing.T) {
2121
}{
2222
{api.StatusB, false, api.BatteryNormal, api.ModeOff}, // mode off -> bat enabled
2323
{api.StatusB, false, api.BatteryNormal, api.ModeNow}, // mode now, not charging -> bat enabled
24-
{api.StatusC, false, api.BatteryLocked, api.ModeNow}, // mode now, charging -> bat disabled
24+
{api.StatusC, false, api.BatteryHold, api.ModeNow}, // mode now, charging -> bat disabled
2525
{api.StatusB, false, api.BatteryNormal, api.ModeMinPV}, // mode minPV, not charging -> bat enabled
2626
{api.StatusC, false, api.BatteryNormal, api.ModeMinPV}, // mode minPV, charging -> bat enabled
2727
{api.StatusB, false, api.BatteryNormal, api.ModePV}, // mode PV, not charging -> bat enabled
2828
{api.StatusC, false, api.BatteryNormal, api.ModePV}, // mode PV, charging, no planner -> bat enabled
29-
{api.StatusC, true, api.BatteryLocked, api.ModePV}, // mode PV, charging, planner active -> bat disabled
29+
{api.StatusC, true, api.BatteryHold, api.ModePV}, // mode PV, charging, planner active -> bat disabled
3030
}
3131

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

7474
lp := loadpoint.NewMockAPI(ctrl)
7575
lp.EXPECT().GetStatus().Return(api.StatusC).Times(2)

meter/battery.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package meter
2+
3+
import (
4+
"github.com/evcc-io/evcc/api"
5+
)
6+
7+
type battery struct {
8+
MinSoc, MaxSoc float64
9+
}
10+
11+
// Decorator returns an api.BatteryController decorator
12+
func (m *battery) BatteryController(socG func() (float64, error), limitSocS func(float64) error) func(api.BatteryMode) error {
13+
return func(mode api.BatteryMode) error {
14+
switch mode {
15+
case api.BatteryNormal:
16+
return limitSocS(m.MinSoc)
17+
18+
case api.BatteryHold:
19+
soc, err := socG()
20+
if err != nil {
21+
return err
22+
}
23+
return limitSocS(soc)
24+
25+
case api.BatteryCharge:
26+
return limitSocS(m.MaxSoc)
27+
28+
default:
29+
return api.ErrNotAvailable
30+
}
31+
}
32+
}

meter/meter.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,27 @@ func init() {
1313
registry.Add(api.Custom, NewConfigurableFromConfig)
1414
}
1515

16-
//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"
16+
// 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"
1717

1818
// NewConfigurableFromConfig creates api.Meter from config
1919
func NewConfigurableFromConfig(other map[string]interface{}) (api.Meter, error) {
20-
var cc struct {
21-
capacity `mapstructure:",squash"`
20+
cc := struct {
2221
Power provider.Config
2322
Energy *provider.Config // optional
24-
Soc *provider.Config // optional
2523
Currents []provider.Config // optional
2624
Voltages []provider.Config // optional
2725
Powers []provider.Config // optional
26+
27+
// battery
28+
capacity `mapstructure:",squash"`
29+
battery `mapstructure:",squash"`
30+
Soc *provider.Config // optional
31+
LimitSoc *provider.Config // optional
32+
}{
33+
battery: battery{
34+
MinSoc: 20,
35+
MaxSoc: 95,
36+
},
2837
}
2938

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

6877
// decorate soc
69-
var batterySocG func() (float64, error)
78+
var socG func() (float64, error)
7079
if cc.Soc != nil {
71-
batterySocG, err = provider.NewFloatGetterFromConfig(*cc.Soc)
80+
socG, err = provider.NewFloatGetterFromConfig(*cc.Soc)
81+
if err != nil {
82+
return nil, fmt.Errorf("battery soc: %w", err)
83+
}
84+
}
85+
86+
var batModeS func(api.BatteryMode) error
87+
if cc.Soc != nil && cc.LimitSoc != nil {
88+
limitSocS, err := provider.NewFloatSetterFromConfig("limitSoc", *cc.LimitSoc)
7289
if err != nil {
73-
return nil, fmt.Errorf("battery: %w", err)
90+
return nil, fmt.Errorf("battery limit soc: %w", err)
7491
}
92+
93+
batModeS = cc.battery.BatteryController(socG, limitSocS)
7594
}
7695

77-
res := m.Decorate(totalEnergyG, currentsG, voltagesG, powersG, batterySocG, cc.capacity.Decorator())
96+
res := m.Decorate(totalEnergyG, currentsG, voltagesG, powersG, socG, cc.capacity.Decorator(), batModeS)
7897

7998
return res, nil
8099
}
@@ -140,8 +159,9 @@ func (m *Meter) Decorate(
140159
powers func() (float64, float64, float64, error),
141160
batterySoc func() (float64, error),
142161
capacity func() float64,
162+
setBatteryMode func(api.BatteryMode) error,
143163
) api.Meter {
144-
return decorateMeter(m, totalEnergy, currents, voltages, powers, batterySoc, capacity)
164+
return decorateMeter(m, totalEnergy, currents, voltages, powers, batterySoc, capacity, setBatteryMode)
145165
}
146166

147167
// CurrentPower implements the api.Meter interface

meter/meter_average.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func NewMovingAverageFromConfig(other map[string]interface{}) (api.Meter, error)
6868
powers = m.Powers
6969
}
7070

71-
return meter.Decorate(totalEnergy, currents, voltages, powers, batterySoc, cc.Meter.capacity.Decorator()), nil
71+
return meter.Decorate(totalEnergy, currents, voltages, powers, batterySoc, cc.Meter.capacity.Decorator(), nil), nil
7272
}
7373

7474
type MovingAverage struct {

0 commit comments

Comments
 (0)