diff --git a/api/api.go b/api/api.go
index a5a45c26f..c9c578979 100644
--- a/api/api.go
+++ b/api/api.go
@@ -14,6 +14,11 @@ const (
ModePV ChargeMode = "pv"
)
+// String implements Stringer
+func (c ChargeMode) String() string {
+ return string(c)
+}
+
// ChargeStatus is the EV's charging status from A to F
type ChargeStatus string
@@ -27,6 +32,11 @@ const (
StatusF ChargeStatus = "F" // Fzg. angeschlossen: ja Laden aktiv: nein - Fehler (Ausfall Wallbox)
)
+// String implements Stringer
+func (c ChargeStatus) String() string {
+ return string(c)
+}
+
// Meter is able to provide current power in W
type Meter interface {
CurrentPower() (float64, error)
diff --git a/assets/index.html b/assets/index.html
index 4f4cc6c30..64e46a41f 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -41,6 +41,13 @@
.progress-bar-animated {
animation-direction: reverse;
}
+ .btn.caption {
+ opacity: 1;
+ }
+ .btn.first {
+ border-top-left-radius: .2rem;
+ border-bottom-left-radius: .2rem;
+ }
@@ -128,66 +135,52 @@
-
-
+
+
+
+
+
+
+
+
+
@@ -366,10 +448,10 @@
- © 2020
+ <<.Version>> <>(<<.Commit>>)<>
- <<.Version>> <>(<<.Commit>>)<>
+ © 2020
diff --git a/assets/js/app.js b/assets/js/app.js
index 699cd27aa..7e249c0d3 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -186,6 +186,7 @@ Vue.component('version', {
watch: {
"state.availableVersion": function () {
if (this.installed != "<<.Version>>" && // go template parsed?
+ this.installed != "0.0.1-alpha" && // make used?
this.state.availableVersion != this.installed) {
$(this.$refs.bar).collapse("show");
}
@@ -193,27 +194,6 @@ Vue.component('version', {
}
});
-Vue.component('modeswitch', {
- template: '#mode-template',
- data: function() {
- return {
- state: store.state // global state
- };
- },
- computed: {
- mode: {
- get: function() {
- return this.state.mode;
- },
- set: function(mode) {
- axios.post('mode/' + mode).then(function(response) {
- this.state.mode = response.data.mode;
- }.bind(this)).catch(toasts.error);
- }
- }
- },
-});
-
Vue.component("site", {
template: "#site-template",
mixins: [formatter],
@@ -222,6 +202,11 @@ Vue.component("site", {
state: store.state // global state
};
},
+ computed: {
+ multi: function() {
+ return this.state.loadpoints.length > 1
+ }
+ },
methods: {
connect: function() {
const protocol = loc.protocol == "https:" ? "wss:" : "ws:";
@@ -249,15 +234,26 @@ Vue.component("site", {
}
});
+Vue.component("site-details", {
+ template: "#site-details-template",
+ props: ["state"],
+ mixins: [formatter]
+});
+
Vue.component("loadpoint", {
template: "#loadpoint-template",
- props: { state: Object },
+ props: ["state", "id", "pv", "multi"],
mixins: [formatter],
data: function() {
return {
tickerHandle: null,
};
},
+ computed: {
+ hasTargetSoC: function () {
+ return this.state.socLevels != null && this.state.socLevels.length > 0;
+ },
+ },
watch: {
"state.chargeDuration": function() {
window.clearInterval(this.tickerHandle);
@@ -269,11 +265,65 @@ Vue.component("loadpoint", {
}
},
},
+ methods: {
+ api: function (func) {
+ return "lp/" + this.id + "/" + func;
+ },
+ targetMode: function (mode) {
+ axios.post(this.api("mode") + "/" + mode).then(function (response) {
+ this.state.mode = response.data.mode;
+ }.bind(this)).catch(toasts.error);
+ },
+ targetSoC: function (soc) {
+ axios.post(this.api("targetsoc") + "/" + soc).then(function (response) {
+ this.state.targetSoC = response.data.targetSoC;
+ }.bind(this)).catch(toasts.error);
+ },
+ },
destroyed: function() {
window.clearInterval(this.tickerHandle);
}
});
+Vue.component("loadpoint-details", {
+ template: "#loadpoint-details-template",
+ props: ["state"],
+ mixins: [formatter]
+});
+
+Vue.component("vehicle", {
+ template: "#vehicle-template",
+ props: ["state"]
+});
+
+Vue.component("mode", {
+ template: "#mode-template",
+ props: ["mode", "pv", "caption"],
+ methods: {
+ targetMode: function (mode) {
+ this.$emit("updated", mode)
+ }
+ },
+});
+
+Vue.component("soc", {
+ template: "#soc-template",
+ props: ["soc", "caption", "levels"],
+ computed: {
+ levelsOrDefault: function() {
+ if (this.levels == null || this.levels.length == 0) {
+ return []; // disabled, or use 30, 50, 80, 100
+ }
+ return this.levels;
+ }
+ },
+ methods: {
+ targetSoC: function (mode) {
+ this.$emit("updated", mode)
+ }
+ },
+});
+
//
// Routing
//
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 7142f4864..8526fbd91 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -1,6 +1,7 @@
package core
import (
+ "sync"
"time"
"github.com/andig/evcc/api"
@@ -38,9 +39,14 @@ type LoadPoint struct {
bus evbus.Bus // event bus
pushChan chan<- push.Event // notifications
uiChan chan<- util.Param // client push messages
+ lpChan chan<- *LoadPoint // update requests
log *util.Logger
// exposed public configuration
+ sync.Mutex // guard status
+ Mode api.ChargeMode `mapstructure:"mode"` // Charge mode, guarded by mutex
+ targetSoC int `mapstructure:"targetSoC"` // Target SoC, guarded by mutex
+
Title string `mapstructure:"title"` // UI title
Phases int64 `mapstructure:"phases"` // Phases- required for converting power and current
ChargerRef string `mapstructure:"charger"` // Charger reference
@@ -49,7 +55,12 @@ type LoadPoint struct {
ChargeMeterRef string `mapstructure:"charge"` // Charge meter reference
}
SoC struct {
- AlwaysUpdate bool `mapstructure:"alwaysUpdate"`
+ AlwaysUpdate bool `mapstructure:"alwaysUpdate"`
+ Levels []int `mapstructure:"levels"`
+ }
+ OnDisconnect struct {
+ Mode api.ChargeMode `mapstructure:"mode"` // Charge mode to apply when car disconnected
+ TargetSoC int `mapstructure:"targetSoC"` // Target SoC to apply when car disconnected
}
Enable, Disable ThresholdConfig
@@ -69,6 +80,7 @@ type LoadPoint struct {
connectedTime time.Time // Time when vehicle was connected
pvTimer time.Time // PV enabled/disable timer
+ socCharge float64 // Vehicle SoC
chargedEnergy float64 // Charged energy while connected
deltaChargedEnergy float64 // Charged energy for single cycle
chargeDuration time.Duration // Charge duration
@@ -79,6 +91,16 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
lp := NewLoadPoint(log)
util.DecodeOther(log, other, &lp)
+ // workaround mapstructure
+ if lp.Mode == "0" {
+ lp.Mode = api.ModeOff
+ }
+
+ lp.targetSoC = 100
+ if len(lp.SoC.Levels) > 0 {
+ lp.targetSoC = lp.SoC.Levels[len(lp.SoC.Levels)-1]
+ }
+
if lp.Meters.ChargeMeterRef != "" {
lp.chargeMeter = cp.Meter(lp.Meters.ChargeMeterRef)
}
@@ -116,6 +138,7 @@ func NewLoadPoint(log *util.Logger) *LoadPoint {
log: log, // logger
clock: clock, // mockable time
bus: bus, // event bus
+ Mode: api.ModeOff,
Phases: 1,
status: api.StatusNone,
HandlerConfig: HandlerConfig{
@@ -129,6 +152,50 @@ func NewLoadPoint(log *util.Logger) *LoadPoint {
return lp
}
+// GetMode returns loadpoint charge mode
+func (lp *LoadPoint) GetMode() api.ChargeMode {
+ lp.Lock()
+ defer lp.Unlock()
+ return lp.Mode
+}
+
+// SetMode sets loadpoint charge mode
+func (lp *LoadPoint) SetMode(mode api.ChargeMode) {
+ lp.Lock()
+ defer lp.Unlock()
+
+ lp.log.INFO.Printf("set charge mode: %s", string(mode))
+
+ // apply immediately
+ if lp.Mode != mode {
+ lp.Mode = mode
+ lp.publish("mode", mode)
+ lp.lpChan <- lp // request loadpoint update
+ }
+}
+
+// GetTargetSoC returns loadpoint charge targetSoC
+func (lp *LoadPoint) GetTargetSoC() int {
+ lp.Lock()
+ defer lp.Unlock()
+ return lp.targetSoC
+}
+
+// SetTargetSoC sets loadpoint charge targetSoC
+func (lp *LoadPoint) SetTargetSoC(targetSoC int) {
+ lp.Lock()
+ defer lp.Unlock()
+
+ lp.log.INFO.Println("set target soc:", targetSoC)
+
+ // apply immediately
+ if lp.targetSoC != targetSoC {
+ lp.targetSoC = targetSoC
+ lp.publish("targetSoC", targetSoC)
+ lp.lpChan <- lp // request loadpoint update
+ }
+}
+
// configureChargerType ensures that chargeMeter, Rate and Timer can use charger capabilities
func (lp *LoadPoint) configureChargerType(charger api.Charger) {
// ensure charge meter exists
@@ -214,6 +281,14 @@ func (lp *LoadPoint) evVehicleDisconnectHandler() {
lp.publish("connectedDuration", lp.clock.Since(lp.connectedTime))
lp.notify(evVehicleDisconnect)
+
+ // set default mode on disconnect
+ if lp.OnDisconnect.Mode != "" {
+ lp.SetMode(lp.OnDisconnect.Mode)
+ }
+ if lp.OnDisconnect.TargetSoC != 0 {
+ lp.SetTargetSoC(lp.OnDisconnect.TargetSoC)
+ }
}
// evChargeCurrentHandler updates the dummy charge meter's charge power. This simplifies the main flow
@@ -248,14 +323,21 @@ func (lp *LoadPoint) Name() string {
}
// Prepare loadpoint configuration by adding missing helper elements
-func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event) {
- lp.pushChan = pushChan
+func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event, lpChan chan<- *LoadPoint) {
lp.uiChan = uiChan
+ lp.pushChan = pushChan
+ lp.lpChan = lpChan
// event handlers
_ = lp.bus.Subscribe(evChargeStart, lp.evChargeStartHandler)
_ = lp.bus.Subscribe(evChargeStop, lp.evChargeStopHandler)
+ // publish initial values
+ lp.Lock()
+ lp.publish("mode", lp.Mode)
+ lp.publish("targetSoC", lp.targetSoC)
+ lp.Unlock()
+
// prepare charger status
lp.handler.Prepare()
}
@@ -292,7 +374,7 @@ func (lp *LoadPoint) updateChargeStatus() error {
}
}
- // changed to A - disconnected
+ // changed to A - disconnected
if status == api.StatusA {
lp.bus.Publish(evVehicleDisconnect)
}
@@ -474,8 +556,9 @@ func (lp *LoadPoint) publishSoC() {
if lp.SoC.AlwaysUpdate || lp.connected() {
f, err := lp.vehicle.ChargeState()
if err == nil {
- lp.log.DEBUG.Printf("vehicle soc: %.0f%%", f)
- lp.publish("socCharge", f)
+ lp.socCharge = f
+ lp.log.DEBUG.Printf("vehicle soc: %.0f%%", lp.socCharge)
+ lp.publish("socCharge", lp.socCharge)
lp.publish("chargeEstimate", lp.remainingChargeDuration(f))
return
}
@@ -487,7 +570,10 @@ func (lp *LoadPoint) publishSoC() {
}
// Update is the main control function. It reevaluates meters and charger state
-func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) {
+func (lp *LoadPoint) Update(sitePower float64) {
+ mode := lp.GetMode()
+ lp.publish("mode", string(mode))
+
// read and publish meters first
lp.updateChargeMeter()
@@ -522,11 +608,14 @@ func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) {
var err error
// execute loading strategy
- switch mode {
- case api.ModeOff:
+ switch {
+ case lp.targetSoC > 0 && lp.vehicle != nil && lp.socCharge >= float64(lp.targetSoC):
+ err = lp.handler.Ramp(0)
+
+ case mode == api.ModeOff:
err = lp.handler.Ramp(0, true)
- case api.ModeNow:
+ case mode == api.ModeNow:
// ensure that new connections happen at min current
current := lp.MinCurrent
if lp.connected() {
@@ -534,7 +623,7 @@ func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) {
}
err = lp.handler.Ramp(current, true)
- case api.ModeMinPV, api.ModePV:
+ case mode == api.ModeMinPV || mode == api.ModePV:
targetCurrent := lp.maxCurrent(mode, sitePower)
if !lp.connected() {
// ensure minimum current when not connected
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 1de8ca529..e87520e87 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -160,7 +160,8 @@ func TestUpdate(t *testing.T) {
tc.expect(handler)
}
- lp.Update(tc.mode, 0)
+ lp.Mode = tc.mode
+ lp.Update(0)
ctrl.Finish()
}
diff --git a/core/site.go b/core/site.go
index 1b8ba0568..f42f16aa9 100644
--- a/core/site.go
+++ b/core/site.go
@@ -1,7 +1,6 @@
package core
import (
- "sync"
"time"
"github.com/andig/evcc/api"
@@ -16,22 +15,21 @@ import (
// Updater abstracts the LoadPoint implementation for testing
type Updater interface {
- Update(api.ChargeMode, float64)
+ Update(float64)
}
// Site is the main configuration container. A site can host multiple loadpoints.
type Site struct {
- sync.Mutex // guard status
- triggerChan chan struct{} // API updates
- uiChan chan<- util.Param // client push messages
- log *util.Logger
+ uiChan chan<- util.Param // client push messages
+ lpUpdateChan chan *LoadPoint
+
+ log *util.Logger
// configuration
- Title string `mapstructure:"title"` // UI title
- Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
- ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
- Mode api.ChargeMode `mapstructure:"mode"` // Charge mode, guarded by mutex
- Meters MetersConfig // Meter references
+ Title string `mapstructure:"title"` // UI title
+ Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
+ ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
+ Meters MetersConfig // Meter references
// meters
gridMeter api.Meter // Grid usage meter
@@ -66,11 +64,6 @@ func NewSiteFromConfig(
Voltage = site.Voltage
site.loadpoints = loadpoints
- // workaround mapstructure
- if site.Mode == "0" {
- site.Mode = api.ModeOff
- }
-
// configure meter from references
// if site.Meters.PVMeterRef == "" && site.Meters.GridMeterRef == "" {
// site.log.FATAL.Fatal("config: missing either pv or grid meter")
@@ -94,10 +87,8 @@ func NewSiteFromConfig(
// NewSite creates a Site with sane defaults
func NewSite() *Site {
lp := &Site{
- log: util.NewLogger("core"),
- triggerChan: make(chan struct{}, 1),
- Mode: api.ModeOff,
- Voltage: 230, // V
+ log: util.NewLogger("core"),
+ Voltage: 230, // V
}
return lp
@@ -106,7 +97,6 @@ func NewSite() *Site {
// SiteConfiguration contains the global site configuration
type SiteConfiguration struct {
Title string `json:"title"`
- Mode string `json:"mode"`
GridMeter bool `json:"gridMeter"`
PVMeter bool `json:"pvMeter"`
BatteryMeter bool `json:"batteryMeter"`
@@ -115,6 +105,7 @@ type SiteConfiguration struct {
// LoadpointConfiguration is the loadpoint feature structure
type LoadpointConfiguration struct {
+ Mode string `json:"mode"`
Title string `json:"title"`
Phases int64 `json:"phases"`
MinCurrent int64 `json:"minCurrent"`
@@ -123,6 +114,7 @@ type LoadpointConfiguration struct {
SoC bool `json:"soc"`
SoCCapacity int64 `json:"socCapacity"`
SoCTitle string `json:"socTitle"`
+ SoCLevels []int `json:"socLevels"`
}
func (lp *LoadPoint) hasChargeMeter() bool {
@@ -139,7 +131,6 @@ func (site *Site) LoadPoints() []*LoadPoint {
func (site *Site) Configuration() SiteConfiguration {
c := SiteConfiguration{
Title: site.Title,
- Mode: string(site.GetMode()),
GridMeter: site.gridMeter != nil,
PVMeter: site.pvMeter != nil,
BatteryMeter: site.batteryMeter != nil,
@@ -147,6 +138,7 @@ func (site *Site) Configuration() SiteConfiguration {
for _, lp := range site.loadpoints {
lpc := LoadpointConfiguration{
+ Mode: string(lp.GetMode()),
Title: lp.Name(),
Phases: lp.Phases,
MinCurrent: lp.MinCurrent,
@@ -158,6 +150,7 @@ func (site *Site) Configuration() SiteConfiguration {
lpc.SoC = true
lpc.SoCCapacity = lp.vehicle.Capacity()
lpc.SoCTitle = lp.vehicle.Title()
+ lpc.SoCLevels = lp.SoC.Levels
}
c.LoadPoints = append(c.LoadPoints, lpc)
@@ -169,7 +162,6 @@ func (site *Site) Configuration() SiteConfiguration {
// DumpConfig site configuration
func (site *Site) DumpConfig() {
site.log.INFO.Println("site config:")
- site.log.INFO.Printf(" mode: %s", site.GetMode())
site.log.INFO.Printf(" grid %s", presence[site.gridMeter != nil])
site.log.INFO.Printf(" pv %s", presence[site.pvMeter != nil])
site.log.INFO.Printf(" battery %s", presence[site.batteryMeter != nil])
@@ -192,36 +184,6 @@ func (site *Site) DumpConfig() {
}
}
-// Update triggers loadpoint to run main control loop and push messages to UI socket
-func (site *Site) Update() {
- select {
- case site.triggerChan <- struct{}{}: // non-blocking send
- default:
- site.log.WARN.Printf("update blocked")
- }
-}
-
-// GetMode returns loadpoint charge mode
-func (site *Site) GetMode() api.ChargeMode {
- site.Lock()
- defer site.Unlock()
- return site.Mode
-}
-
-// SetMode sets loadpoint charge mode
-func (site *Site) SetMode(mode api.ChargeMode) {
- site.Lock()
- defer site.Unlock()
-
- site.log.INFO.Printf("set charge mode: %s", string(mode))
-
- // apply immediately
- if site.Mode != mode {
- site.Mode = mode
- site.Update()
- }
-}
-
// publish sends values to UI and databases
func (site *Site) publish(key string, val interface{}) {
// test helper
@@ -300,17 +262,16 @@ func (site *Site) sitePower() (float64, error) {
func (site *Site) update(lp Updater) {
site.log.DEBUG.Println("----")
- mode := site.GetMode()
- site.publish("mode", string(mode))
if sitePower, err := site.sitePower(); err == nil {
- lp.Update(mode, sitePower)
+ lp.Update(sitePower)
}
}
// Prepare attaches communication channels to site and loadpoints
func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event) {
site.uiChan = uiChan
+ site.lpUpdateChan = make(chan *LoadPoint)
for id, lp := range site.loadpoints {
lpUIChan := make(chan util.Param)
@@ -330,7 +291,7 @@ func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event)
}
}(id)
- lp.Prepare(lpUIChan, lpPushChan)
+ lp.Prepare(lpUIChan, lpPushChan, site.lpUpdateChan)
}
}
@@ -346,23 +307,18 @@ func (site *Site) loopLoadpoints(next chan<- Updater) {
// Run is the main control loop. It reacts to trigger events by
// updating measurements and executing control logic.
func (site *Site) Run(interval time.Duration) {
- // update ticker
- ticker := time.NewTicker(interval)
- site.triggerChan <- struct{}{} // start immediately
-
loadpointChan := make(chan Updater)
go site.loopLoadpoints(loadpointChan)
+ ticker := time.NewTicker(interval)
+ site.update(<-loadpointChan) // start immediately
+
for {
select {
case <-ticker.C:
site.update(<-loadpointChan)
- case <-site.triggerChan:
- for range site.loadpoints {
- site.update(<-loadpointChan)
- }
- ticker.Stop()
- ticker = time.NewTicker(interval)
+ case lp := <-site.lpUpdateChan:
+ site.update(lp)
}
}
}
diff --git a/evcc.dist.yaml b/evcc.dist.yaml
index 4e8e421fb..678353dea 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -231,8 +231,17 @@ site:
loadpoints:
- title: Ladepunkt # ui display name
vehicle: audi
+ mode: pv
soc:
alwaysUpdate: false # set true to update vehicle soc even when disconnected
+ levels: # target soc levels for UI
+ - 30
+ - 50
+ - 80
+ - 100
+ onDisconnect:
+ mode: pv # switch back to pv mode
+ targetSoC: 100 # charge fully
charger: wallbe # charger
phases: 3 # ev phases (default 3)
sensitivity: 1 # current raise/lower step size (default 10A)
diff --git a/mock/mock_loadpoint.go b/mock/mock_loadpoint.go
index 4b369eebb..75335d3b7 100644
--- a/mock/mock_loadpoint.go
+++ b/mock/mock_loadpoint.go
@@ -5,7 +5,6 @@
package mock
import (
- api "github.com/andig/evcc/api"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
@@ -34,13 +33,13 @@ func (m *MockUpdater) EXPECT() *MockUpdaterMockRecorder {
}
// Update mocks base method
-func (m *MockUpdater) Update(arg0 api.ChargeMode, arg1 float64) {
+func (m *MockUpdater) Update(arg0 float64) {
m.ctrl.T.Helper()
- m.ctrl.Call(m, "Update", arg0, arg1)
+ m.ctrl.Call(m, "Update", arg0)
}
// Update indicates an expected call of Update
-func (mr *MockUpdaterMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockUpdaterMockRecorder) Update(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUpdater)(nil).Update), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUpdater)(nil).Update), arg0)
}
diff --git a/server/assets.go b/server/assets.go
index f89b4b5d0..a7424b1b3 100644
--- a/server/assets.go
+++ b/server/assets.go
@@ -3561,115 +3561,128 @@ oJE6K0E084u3ea7ne0X73qsc2P2KOB+ngKecxITsMAgTVJfqMwAA//+21HeD5gEAAA==
"/index.html": {
name: "index.html",
local: "../assets/index.html",
- size: 14298,
+ size: 17523,
modtime: 1566640112,
compressed: `
-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
+H4sIAAAAAAAC/9w823LbSHbv+oo2dse01mpSki87S4OcmpGd7GbtiStyPFVJ5aEBHII9ArqR7gZFWcOq
+/EPymMf9jLzNn+yXpPqGGwGSkuWZZP0wArvP/Zw+5zS6MeGjhMfqpgC0VHk2Pwr1H5QRls4CYIEeAJLM
+jxAKc1AExUsiJKhZUKoF/jqoJxjJYRasKFwXXKgAxZwpYGoWXNNELWcJrGgM2Pw4QZRRRUmGZUwymJ2d
+ILkUlF1hxfGCqhnjW4QTkLGghaKcNWi/+YgulkSkgC44U4JnGYgtVFKqJRcNLMISmm6DFUUGOOcRzQBf
+Q4RJUeCYFCTKoIF8A/IwVKmIKiWOiMBS3bRoRBmJr4L5kSaTUXaFBGSeiuJlvMQ01npK+gnkLDj7+nR9
+9vVpgJYCFrOAxnzShR0XzGlU07M0tG9nAc1JChMN5Ik+O18/O2+SXJCVxsBm4u70zl6uz1720TMTffRy
+wugCpGoiSapgfA1RNbeFI6+ccRpIZEEExQVlDBKsSDSWq1SbO+NiFvzmRRSR5EWXklxyoeJSoS41J/eY
+xttRmEtteBoTHYf4A83gQjNpuPY359GL3//h93swY84WNG2gacaR4NcShJ0br/Nsi4haQg447rJcmH8u
+nhRVGcxhFcfhxD6b4UcYo4vLS4RxbQercyzlJOJcSSVIMc4pG8dSBs5IOnDlEqDpiBptwZki1yB5DrsQ
+Pf/vPBcvhV3SSIp4FvwoJz/+ewniBj8bPx+fjWVGc0P0RxnMw4kF7cMqeFGAOAi0VjMqWZJBD5KX9ds1
+5XJATqLnduB+LOEfLgdwVyUcJOqqBCx4qXo1M/DawhoTocnvkIAVCAko4yShLEWE0dyEGvrdxMCMC8FT
+AdImJDsNCbo1k6iGxwkVEOunqSf6ysBsLJlIsXFMTB6ukHlBYqpupuhsC3RBhVQVYMRFAgIrXuAMFgoL
+ktBSTtH4XED+qg0UcaV4PgineYQTZ4RwYotUGPHkZn50FCZ0hWhiUqoNXK2IFpkyqUiWQTILwnD80Y7O
+54GBdkDaQtra7qc1NyMrFGdEylnAyCoiAtk/GNYFYQnOUj+Q0XSpUJTah8C6KCRtbBwJwhKfdpZKFXI6
+maRULctoHPN8YmrURC/jYB5Sj6xgrXAhaE7EDVoQiRYEZ0AWKBf4XMtM3conjm1UKsVZh7fiaaoLpcvl
+FiZACVHEzc2CmGcZKST4YV1kda6xJL4nq28z9Y6Iq7IIfAgJSnRi02W44tUCtCDWYNoDC5JpBmY0I5HO
+Gx8Me21Kmpp4dPbTAV+QAUVsQdDroyDMKT6xWrlfOh4cqlfMu6tWVIdAj9SVAA0iDpeRVTWNUGjXKzZJ
+sgbEVEGuudmJIsJnAVJ8FkyC+VuSAAsnDcQGOZ1KwlAQlgL6LU3WJ+i3hsJ0hsZvKbuS87lLMvfkr3/L
+SRhq4vN5MA9Dw2D8wZSN+W65gCWfy99Vwfmfzd9SGIcPcSX7SB6wlCZUylL3b5dlobvUap3ogEnoyseO
+ewwnjKzs8s9BSpLqjotIJefhpDNwVKuvW+DadubXkSN55LO8XXh6NU/WWEFeZERBOwlVo4Z2I/YEv0YF
+fqYzzDURTGf7OoaNDUz+6gt8fHZexev3UAJyCRCtQCx+/p80IuKRm/6TTZQUhJqi29sqb242YwfxscLR
+ALrbhTFZEZrphtkRrqHDaB4StMIRZcnUumq021cCMiAS5ESRdDJ62s8gqAKkmSF1GUCFSYmv+TXTRXE8
+Hmtvh5Oo8rjLje0k6I2VcQlokXGisDB5HJlaoztQkVKma9gU4fNi3c5fFxrvwFTaKjd1oJs8Z2guaZIA
+mwVKlBDMHyuag3zVzHJbea6OXPPQ6BgOiDzII90+D8WdTu2EMhAoX2NSKo6MrWNgCqp4W5558ITKIiM3
++DkqFH4WoBWmi1lgOtJgfntrHjabcLI8c6iFx8yAJL08HA1ZRhUZ/6wpFVWpbYWZThEe1TxXtqN56kFN
+20Xz1APqxxpuIUgObVAzVEHbX94JvvgaT64wZBJaPJ2eNE/xIitpEnwRKXyY9MSCb49s8qo9baKOKJ7T
+2EWdj/qCS2qbQhJJnpUKXiGzBJ7rjgyZJTJFp+MX5mdOGV6CHTs/PS3Wdshs/qfomR7xYupi8t4RR1RV
+BaWdcdEKL/RezuR9qgEhl5XZ9C87F6Cp1mvkkWmCR0/1xFhbeXoFNxZO//R2ajFqGezgnF2RGFo7hrZb
+/wlk5GYWnJ2enp5WKpgZHfBLmoBf8Vsp3JDButcF0WiMlOCsiqpcGDpVUGhtzfudRzM00gVjtNVQJrrF
+EL6fhHWcEbcZUIISluqlpltL9EYILnTXrRnelf9skL8vY/sF+MFCbouQkyxrcrQvX4L5Hz98eK/LVz2m
+M4UBP6gO5Bk+R3mEdVqrM3tCZU4bft2uAA+cz/vCQBc5nQKNbi4CtXKfUwJyQtkBFcCHpqQKdNut/9yX
+pUZusgy9/O2Wp2c1JJhxBijBeYIXGayR7XDMnzzBz1Gu7F+S0ZSZnlFiYEm9cqqSszwzxcS0GKae/PRT
+8EeeQ9CsK/3OaDP92jP18Z+XmaKNtaq1TUARmskqlWu2s8D8CZw5PUx/Xe/pCLeM0dpGa7IDmF7QR01J
+h8xMWUYZ4Cjj8VVl54r4HtN8CSvo1q7glFUV4kk1ckKTY10rrFerYWlbRl86kllgSoNjX4EFaFqsnDjj
+VNDkHZgWZGrM1PRrOKmQqqJxjyXgVO0uhe4S6Nr3pTfvMx15VZ/VlXvbOXmEz7eh3/NrEGiOTitnfQef
+yhTtfgVBhODXOOHXzObpXevF8212RW8okwVQWbLDOOldeS+f5bkneXu7yNWTjmLHm42vFU0eeakg0Qlg
+kat/ZrQP7YdW0QgnllEdkXfxSrHa6ZP5G/EJyrRtC6u+MYEsWdU2uNnbUdNWo6nnY6QPT09GtZqdyfns
+dGNtud+MDuVuRqyR7mDC3sw+YM2IKAXiZrdJvzNAFHZEV72T7Nq2FeDefo6ttzAaNYKzD2bu7XxgzDZx
+72bxDuZOs98rXVXp7pCy3VMGh0vE84bLtaddMX0xXMGbLwWXZw7O17xuRX9LEihKdqVMWW++LPT1ZHdp
+H6yETVlr2Wo77Sl1W4AHiWXE0AL1vqHUPYERq4jsu6Lm2+gBimZx1RB6E8YT8EDX+KyxYdEzfgnqZ1cv
+i5WG4GxaFglRoPeY5nXHOw0yDycatCFDvQx2ieRiaEnkB0Pskl+0xZQ87pdS8tgLqSpUNM1gBZn0M5LH
+b81Av+iGWziRPB4QvP/V4cGNWq7wC1TcWFc9b77wqN22r0fzZmonLhe4L0fTpulO/PjZ+Wj6qDmzqSPJ
+OH6/q6fuKMi/LTjM9TvaRS3ufqcbh9/TxYeI3HX5QW51q1FLf95Zdfvc17f+vuTa292vH2b+X2y97Tf+
+ofZtVJntNfgLF5tdsdTaurWENeVm12Zuv+5W1cj+J09w3emHK1jSOIOhMuWm71Izn++umQOb5K327R1P
+StnOuc3VESmGU8HLAsv8l1ok91AQPX6M+tfTlsI6eD5RyNo6N5Zdr8pfcPUNJevhpqh3Uf4qXdEDcXyo
+pvmBNvpDW523QKUqWbp/ixGbm3P32GG0ET9zazykx99DZg/HD1MjecNApDf3UaRG/WG5S5fPUOY1KUHs
+UOVyyYV67U7AW8L5wVqx3hPPpoItYlvaNgh+prKtbbjk8aD2/wRSfQKq7mWAN1KZu0oPZoAGwQffGuvq
+MbSwq5SN6uRtj4jNUf6SKkBySRJ+Xeev0BwqNCgYXF4qk2f924uEShJlkCB/MWvBmcLX5gAORzxLvKvc
+vHOUqar2BabmcjC/rY0GiRVdga24sxFfLEYn5t7X9JFj6DcWIWVF6Q0oSEJ5gFYkK2EW8MXCFaQ4o/FV
+syw/MSSPg/ml4sXDy8v49egAAZntM/sF1DS0gHzBhXp4EXPKitVo491YrPaLa1CGBbYUj4PmSXX18s2W
+cZRgmbtQfEcZeoref2w1sm0k1wXI3KEbJJqXeQfxgU1zV7vsMspdLHJHW3xfin4r3O/AgMf/b7KMb2Xv
+6Xp3qKM7Vn+gY/vZfxSvYUHKTFUBYsZngfnjLgLQxL7W7QaP7pRnlmg7Uz1+TJOZeSc+HEiWm4snx207
+pC75hRX7WFcl87TZfPXZrnebsH2vXm3h9XtSyeMPblv6d2RpjheC5lFxA9FfE65uflT3OJ4X61fW55J+
+gunZ6elXr1DjNtTZy/pWRw9Bc80JCW5uk7gxM+S2CXUb0V3wVb0fte4wSyVoAcloihrVnbL0ZADe33ne
+hRCl9SGKh+KMQawgaYOZFzujKXr0pAOGfvqpQ/+4gWl6FQkxZ4llchf8Tb3Ts765dTdqRtoZuoh5h3tU
+9A0KTBsdoCna4vQNClYgopI5gCAFJYAxFRxvNrv2wAf4FDIJ7fbQfagzR3/4P+PhRx1rPX485PRB03fU
+ezpq+aEa32y++jxb/g2Z7EnHNuHpMfoGnaIhYz7I/tveNb7D3RZzy3noOmP/FcjOPeY73Gycv3ZvJhIQ
+yKdofIJ+IFkW8TVGJUvQv/z8l2UG4qrJZRxOCnsL+2jgBOh59ybI0dBhS/Nw6E43Y/qOQHrFWb4YZL91
+kWTH/vPcHJc0xP0e1KdP1jrTzsWrntPewTsSf/3v/2zfxtpDxFxheJwnRC63rnHdS4/3H72T76xGdang
+V1fCn7Xf1yHtA/0vos5WjB5+eahqNv1Q+86QuX1av+sbPTUXTYfPZ4dW5571ub1CK54DBxDF7sPLAQkb
+C/agJXuXOHFbg26UHBAnta72jc5wqNw7WPaeTe/S6/2SSGCHqdRyXaER5WZTPKAw7ygbo0sleH4PgXLK
+LkohgKnN5tuHFIqsP0Mosj5QqC8d3OYAZNlZmN1PdW7wM7Q8H1znB6x1/c83BW28tnh9A7t07vfalrPc
+u0LIsmln9ECHNbaff/2P/+qchw6KfrBsfyYF+UTVz39R95fvgthvWDebK3MIsF+8bmjsvhHSfzt6wbkC
+Ue0EdDkoIr1e3El0ETXPbN1g/RHt/ia293S4x7Ct++pDpyfND2ZRGNIFYoD8EBpf8Dynaj5/EobV87H7
+TrB75X53Z2GYiu59pUG5Hse8uHmFzk/PT4f5bG8irPHr3YOeXJTMfPuM7Nd75ivLJ8fu+2UBqhQM/asj
+Ppns+zrTH3BUOpjSPEVB50PLoN5s+e+ZGkCXbqgFR/O0AfKnPG3Pmi+BmgBmoAWjxxsQWuT53F+C3Jw0
+lTQuNAP/9uoIoU3f1qv6Nr4ouh+rhxP7WXY4sf+Lkf8NAAD//xSeKhRzRAAA
`,
},
"/js/app.js": {
name: "app.js",
local: "../assets/js/app.js",
- size: 7225,
+ size: 8507,
modtime: 1566640112,
compressed: `
-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
-fwUAAP//HalhmTkcAAA=
+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
`,
},
diff --git a/server/http.go b/server/http.go
index 1265ae76b..88653c38f 100644
--- a/server/http.go
+++ b/server/http.go
@@ -5,6 +5,7 @@ import (
"fmt"
"html/template"
"net/http"
+ "strconv"
"time"
"github.com/andig/evcc/api"
@@ -33,6 +34,10 @@ type chargeModeJSON struct {
Mode api.ChargeMode `json:"mode"`
}
+type targetSoCJSON struct {
+ TargetSoC int `json:"targetSoC"`
+}
+
type route struct {
Methods []string
Pattern string
@@ -41,9 +46,16 @@ type route struct {
// site is the minimal interface for accessing site methods
type site interface {
+ Configuration() core.SiteConfiguration
+ LoadPoints() []*core.LoadPoint
+}
+
+// mode is the minimal interface for accessing loadpoint methods
+type loadpoint interface {
GetMode() api.ChargeMode
SetMode(api.ChargeMode)
- Configuration() core.SiteConfiguration
+ GetTargetSoC() int
+ SetTargetSoC(targetSoC int)
}
// routeLogger traces matched routes including their executing time
@@ -128,15 +140,15 @@ func StateHandler(cache *util.Cache) http.HandlerFunc {
}
// CurrentChargeModeHandler returns current charge mode
-func CurrentChargeModeHandler(site site) http.HandlerFunc {
+func CurrentChargeModeHandler(loadpoint loadpoint) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
- res := chargeModeJSON{Mode: site.GetMode()}
+ res := chargeModeJSON{Mode: loadpoint.GetMode()}
jsonResponse(w, r, res)
}
}
// ChargeModeHandler updates charge mode
-func ChargeModeHandler(site site) http.HandlerFunc {
+func ChargeModeHandler(loadpoint loadpoint) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
@@ -146,9 +158,37 @@ func ChargeModeHandler(site site) http.HandlerFunc {
return
}
- site.SetMode(api.ChargeMode(mode))
+ loadpoint.SetMode(api.ChargeMode(mode))
- res := chargeModeJSON{Mode: site.GetMode()}
+ res := chargeModeJSON{Mode: loadpoint.GetMode()}
+ jsonResponse(w, r, res)
+ }
+}
+
+// CurrentTargetSoCHandler returns current target soc
+func CurrentTargetSoCHandler(loadpoint loadpoint) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ res := targetSoCJSON{TargetSoC: loadpoint.GetTargetSoC()}
+ jsonResponse(w, r, res)
+ }
+}
+
+// TargetSoCHandler updates target soc
+func TargetSoCHandler(loadpoint loadpoint) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+
+ socS, ok := vars["soc"]
+ soc, err := strconv.ParseInt(socS, 10, 32)
+
+ if !ok || err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ loadpoint.SetTargetSoC(int(soc))
+
+ res := targetSoCJSON{TargetSoC: loadpoint.GetTargetSoC()}
jsonResponse(w, r, res)
}
}
@@ -162,17 +202,15 @@ func SocketHandler(hub *SocketHub) http.HandlerFunc {
// NewHTTPd creates HTTP server with configured routes for loadpoint
func NewHTTPd(url string, links []MenuConfig, site site, hub *SocketHub, cache *util.Cache) *http.Server {
- var routes = []route{{
- []string{"GET"}, "/health", HealthHandler(),
- }, {
- []string{"GET"}, "/config", ConfigHandler(site),
- }, {
- []string{"GET"}, "/state", StateHandler(cache),
- }, {
- []string{"GET"}, "/mode", CurrentChargeModeHandler(site),
- }, {
- []string{"PUT", "POST", "OPTIONS"}, "/mode/{mode:[a-z]+}", ChargeModeHandler(site),
- }}
+ var routes = map[string]route{
+ "health": {[]string{"GET"}, "/health", HealthHandler()},
+ "config": {[]string{"GET"}, "/config", ConfigHandler(site)},
+ "state": {[]string{"GET"}, "/state", StateHandler(cache)},
+ "getmode": {[]string{"GET"}, "/mode", CurrentChargeModeHandler(site.LoadPoints()[0])},
+ "setmode": {[]string{"PUT", "POST", "OPTIONS"}, "/mode/{mode:[a-z]+}", ChargeModeHandler(site.LoadPoints()[0])},
+ "gettargetsoc": {[]string{"GET"}, "/targetsoc", CurrentTargetSoCHandler(site.LoadPoints()[0])},
+ "settargetsoc": {[]string{"PUT", "POST", "OPTIONS"}, "/targetsoc/{soc:[0-9]+}", TargetSoCHandler(site.LoadPoints()[0])},
+ }
router := mux.NewRouter().StrictSlash(true)
@@ -199,11 +237,26 @@ func NewHTTPd(url string, links []MenuConfig, site site, hub *SocketHub, cache *
}),
))
+ // site api
for _, r := range routes {
- api.
- Methods(r.Methods...).
- Path(r.Pattern).
- Handler(r.HandlerFunc) // routeLogger
+ api.Methods(r.Methods...).Path(r.Pattern).Handler(r.HandlerFunc)
+ }
+
+ // loadpoint api
+ for id, lp := range site.LoadPoints() {
+ subAPI := api.PathPrefix(fmt.Sprintf("/lp/%d", id)).Subrouter()
+
+ r := routes["getmode"]
+ subAPI.Methods(r.Methods...).Path(r.Pattern).Handler(CurrentChargeModeHandler(lp))
+
+ r = routes["setmode"]
+ subAPI.Methods(r.Methods...).Path(r.Pattern).Handler(ChargeModeHandler(lp))
+
+ r = routes["gettargetsoc"]
+ subAPI.Methods(r.Methods...).Path(r.Pattern).Handler(CurrentTargetSoCHandler(lp))
+
+ r = routes["settargetsoc"]
+ subAPI.Methods(r.Methods...).Path(r.Pattern).Handler(TargetSoCHandler(lp))
}
srv := &http.Server{