Add target charging (#370)

This commit is contained in:
andig 2021-01-02 21:35:40 +01:00 • committed by GitHub
parent 398738cb36
commit ccdb1cba48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 15496 additions and 7353 deletions

View file

@ -47,6 +47,19 @@
:caption="true"
v-on:updated="setTargetSoC"
></Soc>
<!-- <div class="btn-group btn-group-toggle bg-white shadow-none">
<label class="btn btn-outline-primary">
<input
type="checkbox"
class="disabled"
v-on:click="alert('not implemented - use api')"
/>
<fa-icon
icon="clock"
v-bind:class="{ fas: socTimerActive, far: !socTimerActive }"
></fa-icon>
</label>
</div> -->
</div>
</div>
@ -160,6 +173,8 @@ export default {
socTitle: String,
socCharge: Number,
minSoC: Number,
socTimerSet: Boolean,
socTimerActive: Boolean,
// details
chargePower: Number,

View file

@ -53,3 +53,15 @@ VehicleClimater.args = {
chargeEstimate: 5 * 3600,
climater: "on",
};
export const VehicleTimer = Template.bind({});
VehicleTimer.args = {
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
soc: true,
range: 240.123,
chargeEstimate: 5 * 3600,
socTimerSet: true,
socTimerActive: true,
};

View file

@ -3,6 +3,9 @@
<div class="col-6 col-md-3 mt-3">
<div class="mb-2 value">
Leistung
<fa-icon class="text-primary ml-1" icon="clock" v-if="socTimerActive"></fa-icon>
<fa-icon class="text-secondary ml-1" icon="clock" v-else-if="socTimerSet"></fa-icon>
<fa-icon
class="text-primary ml-1"
icon="temperature-low"
@ -69,6 +72,8 @@ export default {
chargedEnergy: Number,
chargeDuration: Number,
soc: Boolean,
socTimerActive: Boolean,
socTimerSet: Boolean,
climater: String,
range: Number,
chargeEstimate: Number,

View file

@ -1,30 +1,32 @@
import Vue from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
import { faArrowDown } from "@fortawesome/free-solid-svg-icons/faArrowDown";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
import { faBatteryThreeQuarters } from "@fortawesome/free-solid-svg-icons/faBatteryThreeQuarters";
import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import { faClock } from "@fortawesome/free-solid-svg-icons";
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle";
import { faLeaf } from "@fortawesome/free-solid-svg-icons/faLeaf";
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
import { faTemperatureLow } from "@fortawesome/free-solid-svg-icons/faTemperatureLow";
import { faTemperatureHigh } from "@fortawesome/free-solid-svg-icons/faTemperatureHigh";
import { faThermometerHalf } from "@fortawesome/free-solid-svg-icons/faThermometerHalf";
import { faLeaf } from "@fortawesome/free-solid-svg-icons/faLeaf";
import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle";
library.add(
faSun,
faArrowUp,
faArrowDown,
faArrowUp,
faBatteryThreeQuarters,
faChevronDown,
faChevronUp,
faClock,
faExclamationTriangle,
faLeaf,
faSun,
faTemperatureLow,
faTemperatureHigh,
faThermometerHalf,
faLeaf,
faChevronUp,
faChevronDown,
faExclamationTriangle
faThermometerHalf
);
Vue.component("fa-icon", FontAwesomeIcon);

View file

@ -10,6 +10,7 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/core/soc"
"github.com/andig/evcc/core/wrapper"
"github.com/andig/evcc/push"
"github.com/andig/evcc/util"
@ -94,10 +95,10 @@ type LoadPoint struct {
MaxCurrent int64 // Max allowed current. Physically ensured by the charger
GuardDuration time.Duration // charger enable/disable minimum holding time
enabled bool // Charger enabled state
maxCurrent float64 // Charger current limit
guardUpdated time.Time // Charger enabled/disabled timestamp
socUpdated time.Time // SoC updated timestamp (poll: connected)
enabled bool // Charger enabled state
chargeCurrent float64 // Charger current limit
guardUpdated time.Time // Charger enabled/disabled timestamp
socUpdated time.Time // SoC updated timestamp (poll: connected)
charger api.Charger
chargeTimer api.ChargeTimer
@ -106,7 +107,8 @@ type LoadPoint struct {
chargeMeter api.Meter // Charger usage meter
vehicle api.Vehicle // Currently active vehicle
vehicles []api.Vehicle // Assigned vehicles
socEstimator *wrapper.SocEstimator
socEstimator *soc.Estimator
socTimer *soc.Timer
// cached state
status api.ChargeStatus // Charger status
@ -191,6 +193,8 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
lp.charger = cp.Charger(lp.ChargerRef)
lp.configureChargerType(lp.charger)
// allow target charge handler to access loadpoint
lp.socTimer = soc.NewTimer(lp.log, lp.adapter(), lp.MaxCurrent)
if lp.Enable.Threshold > lp.Disable.Threshold {
log.WARN.Printf("PV mode enable threshold (%.0fW) is larger than disable threshold (%.0fW)", lp.Enable.Threshold, lp.Disable.Threshold)
}
@ -435,23 +439,23 @@ func (lp *LoadPoint) syncCharger() {
}
}
func (lp *LoadPoint) setLimit(maxCurrent float64, force bool) (err error) {
func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) (err error) {
// set current
if maxCurrent != lp.maxCurrent && maxCurrent >= float64(lp.MinCurrent) {
if chargeCurrent != lp.chargeCurrent && chargeCurrent >= float64(lp.MinCurrent) {
if charger, ok := lp.charger.(api.ChargerEx); ok {
err = charger.MaxCurrentMillis(maxCurrent)
err = charger.MaxCurrentMillis(chargeCurrent)
} else {
err = lp.charger.MaxCurrent(int64(maxCurrent))
err = lp.charger.MaxCurrent(int64(chargeCurrent))
}
if err == nil {
lp.maxCurrent = maxCurrent
lp.bus.Publish(evChargeCurrent, maxCurrent)
lp.chargeCurrent = chargeCurrent
lp.bus.Publish(evChargeCurrent, chargeCurrent)
}
}
// set enabled
if enabled := maxCurrent != 0; enabled != lp.enabled && err == nil {
if enabled := chargeCurrent != 0; enabled != lp.enabled && err == nil {
if remaining := (lp.GuardDuration - lp.clock.Since(lp.guardUpdated)).Truncate(time.Second); remaining > 0 && !force {
lp.log.DEBUG.Printf("charger %s - contactor delay %v", status[enabled], remaining)
return nil
@ -461,7 +465,7 @@ func (lp *LoadPoint) setLimit(maxCurrent float64, force bool) (err error) {
lp.enabled = enabled
lp.guardUpdated = lp.clock.Now()
lp.log.DEBUG.Printf("charger %s", status[enabled])
lp.bus.Publish(evChargeCurrent, maxCurrent)
lp.bus.Publish(evChargeCurrent, chargeCurrent)
}
}
@ -539,7 +543,7 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) {
}
lp.vehicle = vehicle
lp.socEstimator = wrapper.NewSocEstimator(lp.log, vehicle, lp.SoC.Estimate)
lp.socEstimator = soc.NewEstimator(lp.log, vehicle, lp.SoC.Estimate)
lp.publish("socTitle", lp.vehicle.Title())
lp.publish("socCapacity", lp.vehicle.Capacity())
@ -615,7 +619,7 @@ func (lp *LoadPoint) updateChargerStatus() error {
}
// update whenever there is a state change
lp.bus.Publish(evChargeCurrent, lp.maxCurrent)
lp.bus.Publish(evChargeCurrent, lp.chargeCurrent)
}
return nil
@ -655,6 +659,15 @@ func (lp *LoadPoint) detectPhases() {
}
}
// effectiveCurrent returns the currently effective charging current
// it does not take measured currents into account
func (lp *LoadPoint) effectiveCurrent() float64 {
if lp.status != api.StatusC {
return 0
}
return lp.chargeCurrent
}
// pvDisableTimer puts the pv enable/disable timer into elapsed state
func (lp *LoadPoint) pvDisableTimer() {
lp.pvTimer = time.Now().Add(-lp.Disable.Delay)
@ -663,10 +676,7 @@ func (lp *LoadPoint) pvDisableTimer() {
// pvMaxCurrent calculates the maximum target current for PV mode
func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) float64 {
// calculate target charge current from delta power and actual current
effectiveCurrent := lp.maxCurrent
if lp.status != api.StatusC {
effectiveCurrent = 0
}
effectiveCurrent := lp.effectiveCurrent()
deltaCurrent := powerToCurrent(-sitePower, lp.Phases)
targetCurrent := math.Max(math.Min(effectiveCurrent+deltaCurrent, float64(lp.MaxCurrent)), 0)
@ -852,7 +862,7 @@ func (lp *LoadPoint) Update(sitePower float64) {
lp.updateChargeMeter()
// update ChargeRater here to make sure initial meter update is caught
lp.bus.Publish(evChargeCurrent, lp.maxCurrent)
lp.bus.Publish(evChargeCurrent, lp.chargeCurrent)
lp.bus.Publish(evChargePower, lp.chargePower)
// update progress and soc before status is updated
@ -901,8 +911,9 @@ func (lp *LoadPoint) Update(sitePower float64) {
targetCurrent = float64(lp.MinCurrent)
}
err = lp.setLimit(targetCurrent, true)
lp.socTimer.Reset() // once SoC is reached, the target charge request is removed
// OCPP
// OCPP has priority over target charging
case lp.remoteControlled(RemoteHardDisable):
remoteDisabled = RemoteHardDisable
fallthrough
@ -917,6 +928,11 @@ func (lp *LoadPoint) Update(sitePower float64) {
case mode == api.ModeNow:
err = lp.setLimit(float64(lp.MaxCurrent), true)
// target charging
case lp.socTimer.StartRequired():
targetCurrent := lp.socTimer.Handle()
err = lp.setLimit(targetCurrent, false)
case mode == api.ModeMinPV || mode == api.ModePV:
targetCurrent := lp.pvMaxCurrent(mode, sitePower)
lp.log.DEBUG.Printf("target charge current: %.2gA", targetCurrent)

27
core/loadpoint_adapter.go Normal file
View file

@ -0,0 +1,27 @@
package core
import "github.com/andig/evcc/core/soc"
type adapter struct {
lp *LoadPoint
}
func (lp *LoadPoint) adapter() soc.Adapter {
return &adapter{lp: lp}
}
func (a *adapter) Publish(key string, val interface{}) {
a.lp.publish(key, val)
}
func (a *adapter) SocEstimator() *soc.Estimator {
return a.lp.socEstimator
}
func (a *adapter) ActivePhases() int64 {
return a.lp.Phases
}
func (a *adapter) Voltage() float64 {
return Voltage
}

View file

@ -1,6 +1,8 @@
package core
import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/core/wrapper"
)
@ -17,6 +19,7 @@ type LoadPointAPI interface {
SetTargetSoC(int) error
GetMinSoC() int
SetMinSoC(int) error
SetTargetCharge(time.Time, int)
RemoteControl(string, RemoteDemand)
// energy
@ -104,6 +107,21 @@ func (lp *LoadPoint) SetMinSoC(soc int) error {
return nil
}
// SetTargetCharge sets loadpoint charge targetSoC
func (lp *LoadPoint) SetTargetCharge(finishAt time.Time, targetSoC int) {
lp.Lock()
defer lp.Unlock()
lp.log.INFO.Printf("set target charge: %d @ %v", targetSoC, finishAt)
// apply immediately
// TODO check reset of targetSoC
lp.publish("targetTime", finishAt)
lp.publish("targetSoC", targetSoC)
lp.requestUpdate()
}
// RemoteControl sets remote status demand
func (lp *LoadPoint) RemoteControl(source string, demand RemoteDemand) {
lp.Lock()

View file

@ -5,7 +5,7 @@ import (
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/core/wrapper"
"github.com/andig/evcc/core/soc"
"github.com/andig/evcc/mock"
"github.com/andig/evcc/push"
"github.com/andig/evcc/util"
@ -365,7 +365,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
// wrap vehicle with estimator
vehicle.EXPECT().Capacity().Return(int64(10))
socEstimator := wrapper.NewSocEstimator(util.NewLogger("foo"), vehicle, false)
socEstimator := soc.NewEstimator(util.NewLogger("foo"), vehicle, false)
lp := &LoadPoint{
log: util.NewLogger("foo"),
@ -392,7 +392,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
attachListeners(t, lp)
lp.enabled = true
lp.maxCurrent = float64(minA)
lp.chargeCurrent = float64(minA)
t.Log("charging below soc target")
vehicle.EXPECT().ChargeState().Return(85.0, nil)
@ -461,7 +461,7 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
attachListeners(t, lp)
lp.enabled = true
lp.maxCurrent = float64(minA)
lp.chargeCurrent = float64(minA)
lp.Mode = api.ModeNow
t.Log("charging at min")
@ -526,7 +526,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
attachListeners(t, lp)
lp.enabled = true
lp.maxCurrent = float64(maxA)
lp.chargeCurrent = float64(maxA)
lp.Mode = api.ModeNow
// attach cache for verifying values

View file

@ -1,4 +1,4 @@
package wrapper
package soc
import (
"errors"
@ -11,9 +11,9 @@ import (
const chargeEfficiency = 0.9 // assume charge 90% efficiency
// SocEstimator provides vehicle soc and charge duration
// Estimator provides vehicle soc and charge duration
// Vehicle SoC can be estimated to provide more granularity
type SocEstimator struct {
type Estimator struct {
log *util.Logger
vehicle api.Vehicle
estimate bool
@ -26,9 +26,9 @@ type SocEstimator struct {
energyPerSocStep float64 // Energy per SoC percent in Wh
}
// NewSocEstimator creates new estimator
func NewSocEstimator(log *util.Logger, vehicle api.Vehicle, estimate bool) *SocEstimator {
s := &SocEstimator{
// NewEstimator creates new estimator
func NewEstimator(log *util.Logger, vehicle api.Vehicle, estimate bool) *Estimator {
s := &Estimator{
log: log,
vehicle: vehicle,
estimate: estimate,
@ -40,7 +40,7 @@ func NewSocEstimator(log *util.Logger, vehicle api.Vehicle, estimate bool) *SocE
}
// Reset resets the estimation process to default values
func (s *SocEstimator) Reset() {
func (s *Estimator) Reset() {
s.prevSoC = 0
s.prevChargedEnergy = 0
s.capacity = float64(s.vehicle.Capacity()) * 1e3 // cache to simplify debugging
@ -49,7 +49,7 @@ func (s *SocEstimator) Reset() {
}
// RemainingChargeDuration returns the remaining duration estimate based on SoC, target and charge power
func (s *SocEstimator) RemainingChargeDuration(chargePower float64, targetSoC int) time.Duration {
func (s *Estimator) RemainingChargeDuration(chargePower float64, targetSoC int) time.Duration {
if chargePower > 0 {
percentRemaining := float64(targetSoC) - s.socCharge
if percentRemaining <= 0 {
@ -78,7 +78,7 @@ func (s *SocEstimator) RemainingChargeDuration(chargePower float64, targetSoC in
}
// RemainingChargeEnergy returns the remaining charge energy in kWh
func (s *SocEstimator) RemainingChargeEnergy(targetSoC int) float64 {
func (s *Estimator) RemainingChargeEnergy(targetSoC int) float64 {
percentRemaining := float64(targetSoC) - s.socCharge
if percentRemaining <= 0 {
return 0
@ -90,7 +90,7 @@ func (s *SocEstimator) RemainingChargeEnergy(targetSoC int) float64 {
}
// SoC implements Vehicle.ChargeState with addition of given charged energy
func (s *SocEstimator) SoC(chargedEnergy float64) (float64, error) {
func (s *Estimator) SoC(chargedEnergy float64) (float64, error) {
f, err := s.vehicle.ChargeState()
if err != nil {
s.log.WARN.Printf("updating soc failed: %v", err)

View file

@ -1,4 +1,4 @@
package wrapper
package soc
import (
"testing"
@ -15,7 +15,7 @@ func TestRemainingChargeDuration(t *testing.T) {
//9 kWh userBatCap => 10 kWh virtualBatCap
vehicle.EXPECT().Capacity().Return(int64(9))
ce := NewSocEstimator(util.NewLogger("foo"), vehicle, false)
ce := NewEstimator(util.NewLogger("foo"), vehicle, false)
ce.socCharge = 20.0
chargePower := 1000.0
@ -34,7 +34,7 @@ func TestSoCEstimation(t *testing.T) {
var capacity int64 = 9
vehicle.EXPECT().Capacity().Return(capacity)
ce := NewSocEstimator(util.NewLogger("foo"), vehicle, true)
ce := NewEstimator(util.NewLogger("foo"), vehicle, true)
ce.socCharge = 20.0
tc := []struct {

109
core/soc/soctimer.go Normal file
View file

@ -0,0 +1,109 @@
package soc
import (
"math"
"time"
"github.com/andig/evcc/util"
)
const (
deviation = 30 * time.Minute
)
// Adapter provides the required methods for interacting with the loadpoint
type Adapter interface {
Publish(key string, val interface{})
SocEstimator() *Estimator
ActivePhases() int64
Voltage() float64
}
// Timer is the target charging handler
type Timer struct {
Adapter
log *util.Logger
maxCurrent int64
current float64
SoC int
Time time.Time
finishAt time.Time
chargeRequired bool
}
// NewTimer creates a Timer
func NewTimer(log *util.Logger, adapter Adapter, maxCurrent int64) *Timer {
lp := &Timer{
log: log,
Adapter: adapter,
maxCurrent: maxCurrent,
}
return lp
}
// Reset resets the target charging request
func (lp *Timer) Reset() {
if lp == nil {
return
}
lp.current = float64(lp.maxCurrent)
lp.Time = time.Time{}
lp.SoC = 0
}
// StartRequired calculates remaining charge duration and returns true if charge start is required to achieve target soc in time
func (lp *Timer) StartRequired() bool {
if lp == nil {
return false
}
se := lp.SocEstimator()
if !lp.active() || se == nil {
return false
}
power := float64(lp.maxCurrent*lp.ActivePhases()) * lp.Voltage()
// time
remainingDuration := se.RemainingChargeDuration(power, lp.SoC)
lp.finishAt = time.Now().Add(remainingDuration).Round(time.Minute)
lp.log.DEBUG.Printf("target charging active for %v: projected %v (%v remaining)", lp.Time, lp.finishAt, remainingDuration.Round(time.Minute))
lp.chargeRequired = lp.finishAt.After(lp.Time)
lp.Publish("timerActive", lp.chargeRequired)
return lp.chargeRequired
}
// active returns true if there is an active target charging request
func (lp *Timer) active() bool {
inactive := lp.Time.IsZero() || lp.Time.Before(time.Now())
lp.Publish("timerSet", !inactive)
// reset active
if inactive && lp.chargeRequired {
lp.chargeRequired = false
lp.Publish("timerActive", lp.chargeRequired)
}
return !inactive
}
// Handle adjusts current up/down to achieve desired target time taking.
func (lp *Timer) Handle() float64 {
switch {
case lp.finishAt.Before(lp.Time.Add(-deviation)):
lp.current--
lp.log.DEBUG.Printf("target charging: slowdown")
case lp.finishAt.After(lp.Time):
lp.current++
lp.log.DEBUG.Printf("target charging: speedup")
}
lp.current = math.Max(math.Min(lp.current, float64(lp.maxCurrent)), 0)
return lp.current
}

View file

@ -7,7 +7,7 @@
"build": "rimraf dist; parcel build assets/* --public-url ./dist/",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "prettier assets --write && eslint assets/**/*.js assets/**/*.vue --fix",
"storybook": "start-storybook -p 6006 -s assets,node_modules/bootstrap/dist",
"storybook": "start-storybook -p 6006 -s assets,node_modules/jquery/dist,node_modules/popper.js/dist,node_modules/bootstrap/dist",
"build-storybook": "build-storybook",
"fix-audit": "npm-force-resolutions"
},

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@ import (
"fmt"
"html/template"
"net/http"
"os"
"strconv"
"time"
@ -219,6 +220,7 @@ func MinSoCHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
}
if !ok || err != nil {
log.DEBUG.Printf("parse soc: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
@ -256,6 +258,53 @@ func RemoteDemandHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
Source: source,
Demand: demand,
}
jsonResponse(w, r, res)
}
}
func timezone() *time.Location {
tz := os.Getenv("TZ")
if tz == "" {
tz = "Local"
}
loc, _ := time.LoadLocation(tz)
return loc
}
// TargetChargeHandler updates target soc
func TargetChargeHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
socS, ok := vars["soc"]
socV, err := strconv.ParseInt(socS, 10, 32)
if !ok || err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
timeS, ok := vars["time"]
timeV, err := time.ParseInLocation("2006-01-02T15:04:05", timeS, timezone())
if !ok || err != nil || timeV.Before(time.Now()) {
log.DEBUG.Printf("parse time: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
loadpoint.SetTargetCharge(timeV, int(socV))
res := struct {
SoC int64 `json:"soc"`
Time time.Time `json:"time"`
}{
SoC: socV,
Time: timeV,
}
jsonResponse(w, r, res)
}
}
@ -317,13 +366,14 @@ func NewHTTPd(url string, site core.SiteAPI, hub *SocketHub, cache *util.Cache)
lpAPI := api.PathPrefix(fmt.Sprintf("/loadpoints/%d", id)).Subrouter()
routes := map[string]route{
"getmode": {[]string{"GET"}, "/mode", CurrentChargeModeHandler(lp)},
"setmode": {[]string{"POST", "OPTIONS"}, "/mode/{mode:[a-z]+}", ChargeModeHandler(lp)},
"gettargetsoc": {[]string{"GET"}, "/targetsoc", CurrentTargetSoCHandler(lp)},
"settargetsoc": {[]string{"POST", "OPTIONS"}, "/targetsoc/{soc:[0-9]+}", TargetSoCHandler(lp)},
"getminsoc": {[]string{"GET"}, "/minsoc", CurrentMinSoCHandler(lp)},
"setminsoc": {[]string{"POST", "OPTIONS"}, "/minsoc/{soc:[0-9]+}", MinSoCHandler(lp)},
"remotedemand": {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source}", RemoteDemandHandler(lp)},
"getmode": {[]string{"GET"}, "/mode", CurrentChargeModeHandler(lp)},
"setmode": {[]string{"POST", "OPTIONS"}, "/mode/{mode:[a-z]+}", ChargeModeHandler(lp)},
"gettargetsoc": {[]string{"GET"}, "/targetsoc", CurrentTargetSoCHandler(lp)},
"settargetsoc": {[]string{"POST", "OPTIONS"}, "/targetsoc/{soc:[0-9]+}", TargetSoCHandler(lp)},
"getminsoc": {[]string{"GET"}, "/minsoc", CurrentMinSoCHandler(lp)},
"setminsoc": {[]string{"POST", "OPTIONS"}, "/minsoc/{soc:[0-9]+}", MinSoCHandler(lp)},
"settargetcharge": {[]string{"POST", "OPTIONS"}, "/targetcharge/{soc:[0-9]+}/{time:[0-9TZ:-]+}", TargetChargeHandler(lp)},
"remotedemand": {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source}", RemoteDemandHandler(lp)},
}
for _, r := range routes {