Skip to content

Commit 6a9ca73

Browse files
authored
Add soc: min setting to always force-charge to this value (#379)
1 parent 0a1f7ca commit 6a9ca73

File tree

16 files changed

+503
-274
lines changed

16 files changed

+503
-274
lines changed

assets/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,8 @@ <h2 class="value">
335335
<div class="row">
336336
<div class="col-6 col-md-3 mt-3">
337337
<div class="mb-2 value">Leistung
338-
<i class="text-primary fa fa-temperature-low ml-1" v-if="state.climater=='heating'"></i>
338+
<i class="text-warning fas fa-battery-quarter ml-1" v-if="minSoCActive"></i>
339+
<i class="text-primary fa fa-temperature-low ml-1" v-else-if="state.climater=='heating'"></i>
339340
<i class="text-primary fa fa-temperature-high ml-1" v-else-if="state.climater=='cooling'"></i>
340341
<i class="text-primary fa fa-thermometer-half ml-1" v-else-if="state.climater=='on'"></i>
341342
</div>
@@ -413,6 +414,7 @@ <h2 class="value">
413414
'progress-bar-animated': state.charging,
414415
'bg-light': !state.connected,
415416
'text-secondary': !state.connected,
417+
'bg-warning': state.connected && minSoCActive,
416418
}" v-bind:style="{width: socChargeDisplayWidth+'%'}">{{socChargeDisplayValue}}</div>
417419
</div>
418420
</div>

assets/js/app.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,12 @@ Vue.component("loadpoint", {
346346
Vue.component("loadpoint-details", {
347347
template: "#loadpoint-details-template",
348348
props: ["state"],
349-
mixins: [formatter]
349+
mixins: [formatter],
350+
computed: {
351+
minSoCActive: function () {
352+
return this.state.minSoC > 0 && this.state.socCharge < this.state.minSoC;
353+
}
354+
}
350355
});
351356

352357
Vue.component("vehicle", {
@@ -377,6 +382,9 @@ Vue.component("vehicle", {
377382
socCharge += "%";
378383
}
379384
return socCharge;
385+
},
386+
minSoCActive: function () {
387+
return this.state.minSoC > 0 && this.state.socCharge < this.state.minSoC;
380388
}
381389
}
382390
});

cmd/setup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func init() {
2121
}
2222

2323
// setup influx databases
24-
func configureDatabase(conf server.InfluxConfig, loadPoints []*core.LoadPoint, in <-chan util.Param) {
24+
func configureDatabase(conf server.InfluxConfig, loadPoints []core.LoadPointAPI, in <-chan util.Param) {
2525
influx := server.NewInfluxClient(
2626
conf.URL,
2727
conf.Token,

core/loadpoint.go

Lines changed: 50 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ const (
2929
minActiveCurrent = 1.0 // minimum current at which a phase is treated as active
3030
)
3131

32+
// SoCConfig defines soc settings, estimation and update behaviour
33+
type SoCConfig struct {
34+
AlwaysUpdate bool `mapstructure:"alwaysUpdate"`
35+
Levels []int `mapstructure:"levels"`
36+
Estimate bool `mapstructure:"estimate"`
37+
Min int `mapstructure:"min"` // Default minimum SoC, guarded by mutex
38+
Target int `mapstructure:"target"` // Default target SoC, guarded by mutex
39+
}
40+
3241
// ThresholdConfig defines enable/disable hysteresis parameters
3342
type ThresholdConfig struct {
3443
Delay time.Duration
@@ -47,8 +56,7 @@ type LoadPoint struct {
4756

4857
// exposed public configuration
4958
sync.Mutex // guard status
50-
Mode api.ChargeMode `mapstructure:"mode"` // Charge mode, guarded by mutex
51-
TargetSoC int `mapstructure:"targetSoC"` // Target SoC, guarded by mutex
59+
Mode api.ChargeMode `mapstructure:"mode"` // Charge mode, guarded by mutex
5260

5361
Title string `mapstructure:"title"` // UI title
5462
Phases int64 `mapstructure:"phases"` // Phases- required for converting power and current
@@ -57,11 +65,7 @@ type LoadPoint struct {
5765
Meters struct {
5866
ChargeMeterRef string `mapstructure:"charge"` // Charge meter reference
5967
}
60-
SoC struct {
61-
AlwaysUpdate bool `mapstructure:"alwaysUpdate"`
62-
Levels []int `mapstructure:"levels"`
63-
Estimate bool `mapstructure:"estimate"`
64-
}
68+
SoC SoCConfig
6569
OnDisconnect struct {
6670
Mode api.ChargeMode `mapstructure:"mode"` // Charge mode to apply when car disconnected
6771
TargetSoC int `mapstructure:"targetSoC"` // Target SoC to apply when car disconnected
@@ -102,11 +106,14 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
102106
lp.OnDisconnect.Mode = api.ChargeModeString(string(lp.OnDisconnect.Mode))
103107

104108
sort.Ints(lp.SoC.Levels)
105-
if lp.TargetSoC == 0 {
106-
lp.TargetSoC = 100
109+
if lp.SoC.Target == 0 {
110+
lp.SoC.Target = lp.OnDisconnect.TargetSoC // use disconnect value as default soc
111+
if lp.SoC.Target == 0 {
112+
lp.SoC.Target = 100
113+
}
107114

108115
if len(lp.SoC.Levels) > 0 {
109-
lp.TargetSoC = lp.SoC.Levels[len(lp.SoC.Levels)-1]
116+
lp.SoC.Target = lp.SoC.Levels[len(lp.SoC.Levels)-1]
110117
}
111118
}
112119

@@ -162,50 +169,6 @@ func NewLoadPoint(log *util.Logger) *LoadPoint {
162169
return lp
163170
}
164171

165-
// GetMode returns loadpoint charge mode
166-
func (lp *LoadPoint) GetMode() api.ChargeMode {
167-
lp.Lock()
168-
defer lp.Unlock()
169-
return lp.Mode
170-
}
171-
172-
// SetMode sets loadpoint charge mode
173-
func (lp *LoadPoint) SetMode(mode api.ChargeMode) {
174-
lp.Lock()
175-
defer lp.Unlock()
176-
177-
lp.log.INFO.Printf("set charge mode: %s", string(mode))
178-
179-
// apply immediately
180-
if lp.Mode != mode {
181-
lp.Mode = mode
182-
lp.publish("mode", mode)
183-
lp.requestUpdate()
184-
}
185-
}
186-
187-
// GetTargetSoC returns loadpoint charge targetSoC
188-
func (lp *LoadPoint) GetTargetSoC() int {
189-
lp.Lock()
190-
defer lp.Unlock()
191-
return lp.TargetSoC
192-
}
193-
194-
// SetTargetSoC sets loadpoint charge targetSoC
195-
func (lp *LoadPoint) SetTargetSoC(targetSoC int) {
196-
lp.Lock()
197-
defer lp.Unlock()
198-
199-
lp.log.INFO.Println("set target soc:", targetSoC)
200-
201-
// apply immediately
202-
if lp.TargetSoC != targetSoC {
203-
lp.TargetSoC = targetSoC
204-
lp.publish("targetSoC", targetSoC)
205-
lp.requestUpdate()
206-
}
207-
}
208-
209172
// requestUpdate requests site to update this loadpoint
210173
func (lp *LoadPoint) requestUpdate() {
211174
select {
@@ -309,7 +272,7 @@ func (lp *LoadPoint) evVehicleDisconnectHandler() {
309272
lp.SetMode(lp.OnDisconnect.Mode)
310273
}
311274
if lp.OnDisconnect.TargetSoC != 0 {
312-
lp.SetTargetSoC(lp.OnDisconnect.TargetSoC)
275+
_ = lp.SetTargetSoC(lp.OnDisconnect.TargetSoC)
313276
}
314277
}
315278

@@ -364,7 +327,8 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
364327
// publish initial values
365328
lp.Lock()
366329
lp.publish("mode", lp.Mode)
367-
lp.publish("targetSoC", lp.TargetSoC)
330+
lp.publish("targetSoC", lp.SoC.Target)
331+
lp.publish("minSoC", lp.SoC.Min)
368332
lp.Unlock()
369333

370334
// prepare charger status
@@ -376,10 +340,20 @@ func (lp *LoadPoint) connected() bool {
376340
return lp.status == api.StatusB || lp.status == api.StatusC
377341
}
378342

379-
// targetSocReached checks if targetSoC configured and reached
380-
func (lp *LoadPoint) targetSocReached(socCharge, targetSoC float64) bool {
381-
// check for vehicle != nil is not necessary as socCharge would be zero then
382-
return targetSoC > 0 && targetSoC < 100 && socCharge >= targetSoC
343+
// targetSocReached checks if target is configured and reached.
344+
// If vehicle is not configured this will always return false
345+
func (lp *LoadPoint) targetSocReached() bool {
346+
return lp.vehicle != nil &&
347+
lp.SoC.Target > 0 &&
348+
lp.socCharge >= float64(lp.SoC.Target)
349+
}
350+
351+
// minSocNotReached checks if minimum is configured and not reached.
352+
// If vehicle is not configured this will always return true
353+
func (lp *LoadPoint) minSocNotReached() bool {
354+
return lp.vehicle != nil &&
355+
lp.SoC.Min > 0 &&
356+
lp.socCharge < float64(lp.SoC.Min)
383357
}
384358

385359
// climateActive checks if vehicle has active climate request
@@ -481,8 +455,13 @@ func (lp *LoadPoint) detectPhases() {
481455
}
482456
}
483457

484-
// maxCurrent calculates the maximum target current for PV mode
485-
func (lp *LoadPoint) maxCurrent(mode api.ChargeMode, sitePower float64) int64 {
458+
// pvDisableTimer puts the pv enable/disable timer into elapsed state
459+
func (lp *LoadPoint) pvDisableTimer() {
460+
lp.pvTimer = time.Now().Add(-lp.Disable.Delay)
461+
}
462+
463+
// pvMaxCurrent calculates the maximum target current for PV mode
464+
func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) int64 {
486465
// calculate target charge current from delta power and actual current
487466
effectiveCurrent := lp.handler.TargetCurrent()
488467
if lp.status != api.StatusC {
@@ -624,11 +603,11 @@ func (lp *LoadPoint) publishSoC() {
624603

625604
chargeEstimate := time.Duration(-1)
626605
if lp.charging {
627-
chargeEstimate = lp.socEstimator.RemainingChargeDuration(lp.chargePower, lp.TargetSoC)
606+
chargeEstimate = lp.socEstimator.RemainingChargeDuration(lp.chargePower, lp.SoC.Target)
628607
}
629608
lp.publish("chargeEstimate", chargeEstimate)
630609

631-
chargeRemainingEnergy := 1e3 * lp.socEstimator.RemainingChargeEnergy(lp.TargetSoC)
610+
chargeRemainingEnergy := 1e3 * lp.socEstimator.RemainingChargeEnergy(lp.SoC.Target)
632611
lp.publish("chargeRemainingEnergy", chargeRemainingEnergy)
633612

634613
return
@@ -684,8 +663,8 @@ func (lp *LoadPoint) Update(sitePower float64) {
684663
// https://github.com/andig/evcc/issues/105
685664
err = lp.handler.Ramp(0)
686665

687-
case lp.targetSocReached(lp.socCharge, float64(lp.TargetSoC)):
688-
var targetCurrent int64
666+
case lp.targetSocReached():
667+
var targetCurrent int64 // zero disables
689668
if lp.climateActive() {
690669
targetCurrent = lp.MinCurrent
691670
}
@@ -694,11 +673,15 @@ func (lp *LoadPoint) Update(sitePower float64) {
694673
case mode == api.ModeOff:
695674
err = lp.handler.Ramp(0, true)
696675

676+
case lp.minSocNotReached():
677+
err = lp.handler.Ramp(lp.MaxCurrent, true)
678+
lp.pvDisableTimer() // let PV mode disable immediately afterwards
679+
697680
case mode == api.ModeNow:
698681
err = lp.handler.Ramp(lp.MaxCurrent, true)
699682

700683
case mode == api.ModeMinPV || mode == api.ModePV:
701-
targetCurrent := lp.maxCurrent(mode, sitePower)
684+
targetCurrent := lp.pvMaxCurrent(mode, sitePower)
702685
lp.log.DEBUG.Printf("target charge current: %dA", targetCurrent)
703686

704687
var required bool // false

core/loadpoint_api.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package core
2+
3+
import (
4+
"github.com/andig/evcc/api"
5+
"github.com/andig/evcc/core/wrapper"
6+
)
7+
8+
// LoadPointAPI is the external loadpoint API
9+
type LoadPointAPI interface {
10+
Name() string
11+
HasChargeMeter() bool
12+
LoadPointSettingsAPI
13+
LoadPointEnergyAPI
14+
}
15+
16+
// LoadPointSettingsAPI is the getter/setter part of the external loadpoint API
17+
type LoadPointSettingsAPI interface {
18+
GetMode() api.ChargeMode
19+
SetMode(api.ChargeMode)
20+
GetTargetSoC() int
21+
SetTargetSoC(int) error
22+
GetMinSoC() int
23+
SetMinSoC(int) error
24+
}
25+
26+
// LoadPointEnergyAPI is the external loadpoint API
27+
type LoadPointEnergyAPI interface {
28+
GetMinCurrent() int64
29+
GetMaxCurrent() int64
30+
GetMinPower() int64
31+
GetMaxPower() int64
32+
}
33+
34+
// GetMode returns loadpoint charge mode
35+
func (lp *LoadPoint) GetMode() api.ChargeMode {
36+
lp.Lock()
37+
defer lp.Unlock()
38+
return lp.Mode
39+
}
40+
41+
// SetMode sets loadpoint charge mode
42+
func (lp *LoadPoint) SetMode(mode api.ChargeMode) {
43+
lp.Lock()
44+
defer lp.Unlock()
45+
46+
lp.log.INFO.Printf("set charge mode: %s", string(mode))
47+
48+
// apply immediately
49+
if lp.Mode != mode {
50+
lp.Mode = mode
51+
lp.publish("mode", mode)
52+
lp.requestUpdate()
53+
}
54+
}
55+
56+
// GetTargetSoC returns loadpoint charge target soc
57+
func (lp *LoadPoint) GetTargetSoC() int {
58+
lp.Lock()
59+
defer lp.Unlock()
60+
return lp.SoC.Target
61+
}
62+
63+
// SetTargetSoC sets loadpoint charge target soc
64+
func (lp *LoadPoint) SetTargetSoC(soc int) error {
65+
if lp.vehicle == nil {
66+
return api.ErrNotAvailable
67+
}
68+
69+
lp.Lock()
70+
defer lp.Unlock()
71+
72+
lp.log.INFO.Println("set target soc:", soc)
73+
74+
// apply immediately
75+
if lp.SoC.Target != soc {
76+
lp.SoC.Target = soc
77+
lp.publish("targetSoC", soc)
78+
lp.requestUpdate()
79+
}
80+
81+
return nil
82+
}
83+
84+
// GetMinSoC returns loadpoint charge minimum soc
85+
func (lp *LoadPoint) GetMinSoC() int {
86+
lp.Lock()
87+
defer lp.Unlock()
88+
return lp.SoC.Min
89+
}
90+
91+
// SetMinSoC sets loadpoint charge minimum soc
92+
func (lp *LoadPoint) SetMinSoC(soc int) error {
93+
if lp.vehicle == nil {
94+
return api.ErrNotAvailable
95+
}
96+
97+
lp.Lock()
98+
defer lp.Unlock()
99+
100+
lp.log.INFO.Println("set min soc:", soc)
101+
102+
// apply immediately
103+
if lp.SoC.Min != soc {
104+
lp.SoC.Min = soc
105+
lp.publish("minSoC", soc)
106+
lp.requestUpdate()
107+
}
108+
109+
return nil
110+
}
111+
112+
// HasChargeMeter determines if a physical charge meter is attached
113+
func (lp *LoadPoint) HasChargeMeter() bool {
114+
_, isWrapped := lp.chargeMeter.(*wrapper.ChargeMeter)
115+
return lp.chargeMeter != nil && !isWrapped
116+
}
117+
118+
// GetMinCurrent returns the minimal loadpoint current
119+
func (lp *LoadPoint) GetMinCurrent() int64 {
120+
return lp.MinCurrent
121+
}
122+
123+
// GetMaxCurrent returns the minimal loadpoint current
124+
func (lp *LoadPoint) GetMaxCurrent() int64 {
125+
return lp.MaxCurrent
126+
}
127+
128+
// GetMinPower returns the minimal loadpoint power for a single phase
129+
func (lp *LoadPoint) GetMinPower() int64 {
130+
return int64(Voltage) * lp.MinCurrent
131+
}
132+
133+
// GetMaxPower returns the minimal loadpoint power taking active phases into account
134+
func (lp *LoadPoint) GetMaxPower() int64 {
135+
return int64(Voltage) * lp.Phases * lp.MaxCurrent
136+
}

0 commit comments

Comments
 (0)