diff --git a/assets/index.html b/assets/index.html
index ef29a2022..4b2e0d254 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -265,8 +265,25 @@ loadpoints:
-
-
{{state.title||"Ladepunkt"}}
+
+ {{state.title||"Ladepunkt"}}
+
+
+
+
+ Adaptives PV-Laden deaktiviert durch {{state.remoteDisabledSource}}
+
+
+ Deaktiviert durch {{state.remoteDisabledSource}}
+
+
+
+ Adaptives PV-Laden deaktiviert durch {{state.remoteDisabledSource}}
+
+
+ Deaktiviert durch {{state.remoteDisabledSource}}
+
diff --git a/cmd/config.go b/cmd/config.go
index c0ec00429..1d7bd58c7 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -20,7 +20,7 @@ type config struct {
Interval time.Duration
Mqtt provider.MqttConfig
Influx server.InfluxConfig
- HEMS string
+ HEMS typedConfig
Messaging messagingConfig
Meters []qualifiedConfig
Chargers []qualifiedConfig
diff --git a/cmd/root.go b/cmd/root.go
index 55b4bd962..51a1a68c2 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -163,7 +163,7 @@ func run(cmd *cobra.Command, args []string) {
httpd := server.NewHTTPd(uri, site, socketHub, cache)
// start HEMS server
- if conf.HEMS != "" {
+ if conf.HEMS.Type != "" {
hems := configureHEMS(conf.HEMS, site, cache, httpd)
go hems.Run()
}
diff --git a/cmd/setup.go b/cmd/setup.go
index d67ac2130..02e00e655 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -52,8 +52,8 @@ func configureMQTT(conf provider.MqttConfig) {
}
// setup HEMS
-func configureHEMS(conf string, site *core.Site, cache *util.Cache, httpd *server.HTTPd) hems.HEMS {
- hems, err := hems.NewFromConfig(conf, site, cache, httpd)
+func configureHEMS(conf typedConfig, site *core.Site, cache *util.Cache, httpd *server.HTTPd) hems.HEMS {
+ hems, err := hems.NewFromConfig(conf.Type, conf.Other, site, cache, httpd)
if err != nil {
log.FATAL.Fatalf("failed configuring hems: %v", err)
}
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 4c74940e9..2e2cb8fb1 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -86,6 +86,7 @@ type LoadPoint struct {
// cached state
status api.ChargeStatus // Charger status
+ remoteDemand RemoteDemand // External status demand
charging bool // Charging cycle
chargePower float64 // Charging power
connectedTime time.Time // Time when vehicle was connected
@@ -412,6 +413,14 @@ func (lp *LoadPoint) climateActive() bool {
return false
}
+// remoteControlled returns true if remote control status is active
+func (lp *LoadPoint) remoteControlled(demand RemoteDemand) bool {
+ lp.Lock()
+ defer lp.Unlock()
+
+ return lp.remoteDemand == demand
+}
+
// setActiveVehicle assigns currently active vehicle and configures soc estimator
func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) {
if lp.vehicle != nil {
@@ -703,7 +712,7 @@ func (lp *LoadPoint) publishSoC() {
// Update is the main control function. It reevaluates meters and charger state
func (lp *LoadPoint) Update(sitePower float64) {
mode := lp.GetMode()
- lp.publish("mode", string(mode))
+ lp.publish("mode", mode)
// read and publish meters first
lp.updateChargeMeter()
@@ -741,6 +750,9 @@ func (lp *LoadPoint) Update(sitePower float64) {
// check if car connected and ready for charging
var err error
+ // track if remote disabled is actually active
+ remoteDisabled := RemoteEnable
+
// execute loading strategy
switch {
case !lp.connected():
@@ -755,6 +767,11 @@ func (lp *LoadPoint) Update(sitePower float64) {
}
err = lp.handler.Ramp(targetCurrent, true)
+ // OCPP
+ case lp.remoteControlled(RemoteHardDisable):
+ remoteDisabled = RemoteHardDisable
+ fallthrough
+
case mode == api.ModeOff:
err = lp.handler.Ramp(0, true)
@@ -775,9 +792,19 @@ func (lp *LoadPoint) Update(sitePower float64) {
required = true
}
+ // Sunny Home Manager
+ if mode == api.ModePV && lp.remoteControlled(RemoteSoftDisable) {
+ remoteDisabled = RemoteSoftDisable
+ targetCurrent = 0
+ required = true
+ }
+
err = lp.handler.Ramp(targetCurrent, required)
}
+ // effective disabled status
+ lp.publish("remoteDisabled", remoteDisabled)
+
if err != nil {
lp.log.ERROR.Println(err)
}
diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go
index 835f51858..160150781 100644
--- a/core/loadpoint_api.go
+++ b/core/loadpoint_api.go
@@ -21,6 +21,7 @@ type LoadPointSettingsAPI interface {
SetTargetSoC(int) error
GetMinSoC() int
SetMinSoC(int) error
+ RemoteControl(string, RemoteDemand)
}
// LoadPointEnergyAPI is the external loadpoint API
@@ -109,6 +110,24 @@ func (lp *LoadPoint) SetMinSoC(soc int) error {
return nil
}
+// RemoteControl sets remote status demand
+func (lp *LoadPoint) RemoteControl(source string, demand RemoteDemand) {
+ lp.Lock()
+ defer lp.Unlock()
+
+ lp.log.INFO.Println("remote demand:", demand)
+
+ // apply immediately
+ if lp.remoteDemand != demand {
+ lp.remoteDemand = demand
+
+ lp.publish("remoteDisabled", demand)
+ lp.publish("remoteDisabledSource", source)
+
+ lp.requestUpdate()
+ }
+}
+
// HasChargeMeter determines if a physical charge meter is attached
func (lp *LoadPoint) HasChargeMeter() bool {
_, isWrapped := lp.chargeMeter.(*wrapper.ChargeMeter)
diff --git a/core/remote.go b/core/remote.go
new file mode 100644
index 000000000..6fa8ae3dc
--- /dev/null
+++ b/core/remote.go
@@ -0,0 +1,11 @@
+package core
+
+// RemoteDemand defines external status demand
+type RemoteDemand string
+
+// remote status demand definition
+const (
+ RemoteEnable RemoteDemand = ""
+ RemoteHardDisable RemoteDemand = "hard"
+ RemoteSoftDisable RemoteDemand = "soft"
+)
diff --git a/core/site_api.go b/core/site_api.go
index 46db1eb32..de39e5ccf 100644
--- a/core/site_api.go
+++ b/core/site_api.go
@@ -62,3 +62,11 @@ func (site *Site) SetMinSoC(soc int) error {
}
return nil
}
+
+// RemoteControl sets remote status demand
+func (site *Site) RemoteControl(source string, demand RemoteDemand) {
+ site.log.INFO.Println("remote demand:", demand)
+ for _, lp := range site.loadpoints {
+ lp.RemoteControl(source, demand)
+ }
+}
diff --git a/go.mod b/go.mod
index 40683dbc7..d19041504 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/avast/retry-go v2.6.0+incompatible
github.com/benbjohnson/clock v1.0.3
github.com/containrrr/shoutrrr v0.0.0-20200721140131-bafc331a1968
+ github.com/denisbrodbeck/machineid v1.0.1
github.com/eclipse/paho.mqtt.golang v1.2.0
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/godbus/dbus/v5 v5.0.3
@@ -29,6 +30,7 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d
github.com/kr/pretty v0.2.0 // indirect
+ github.com/lorenzodonini/ocpp-go v0.12.0
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/mitchellh/mapstructure v1.3.2
github.com/mjibson/esc v0.2.0
@@ -44,7 +46,8 @@ require (
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
github.com/volkszaehler/mbmd v0.0.0-20200831092453-b235d6a65b21
golang.org/x/net v0.0.0-20200707034311-ab3426394381
- gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/ini.v1 v1.57.0
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c
)
+
+replace github.com/lorenzodonini/ocpp-go => github.com/andig/ocpp-go v0.12.1-0.20201110113243-43b1af9c1480
diff --git a/go.sum b/go.sum
index 630896ebc..ee02a9be0 100644
--- a/go.sum
+++ b/go.sum
@@ -25,6 +25,10 @@ github.com/andig/evcc-config v0.0.0-20201030211256-8664d1cdae95 h1:GsXyGiwc3gf1X
github.com/andig/evcc-config v0.0.0-20201030211256-8664d1cdae95/go.mod h1:N0hIjIy+5E2AR1fF7Tg2IzBlblBrnFvCCaDGAaHzbWk=
github.com/andig/gosunspec v0.0.0-20200429133549-3cf6a82fed9c h1:AMtX56iHlNYVxMID7fe9efuVtaxgtdjyMeolg7q87IE=
github.com/andig/gosunspec v0.0.0-20200429133549-3cf6a82fed9c/go.mod h1:YkshK8WMzYn1iXAZzHUO75gIqhMSan2ctgBVtBkRIyA=
+github.com/andig/ocpp-go v0.12.1-0.20201110090118-4fdb491db96c h1:2pVN47ILzwG8KkDMw+inI2VVLi+q2o4j1DEOhHIvGtA=
+github.com/andig/ocpp-go v0.12.1-0.20201110090118-4fdb491db96c/go.mod h1:mN2Tv8bNqQlFMECj3IqcjZL7fOi3PmNnyVuEjICdxgU=
+github.com/andig/ocpp-go v0.12.1-0.20201110113243-43b1af9c1480 h1:5yKucDmOcJ6vRNeAn7PCRqYwU3/UrPY4dhwBUoNR+ug=
+github.com/andig/ocpp-go v0.12.1-0.20201110113243-43b1af9c1480/go.mod h1:mN2Tv8bNqQlFMECj3IqcjZL7fOi3PmNnyVuEjICdxgU=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@@ -60,6 +64,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/deepmap/oapi-codegen v1.3.6/go.mod h1:aBozjEveG+33xPiP55Iw/XbVkhtZHEGLq3nxlX0+hfU=
github.com/deepmap/oapi-codegen v1.3.9 h1:8e3ZfNFzwMDJ5kojta1GGS2+RjzWIThwDWgwe9GqQrE=
github.com/deepmap/oapi-codegen v1.3.9/go.mod h1:suMvK7+rKlx3+tpa8ByptmvoXbAV70wERKTOGH3hLp0=
+github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
+github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0=
@@ -85,6 +91,10 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
+github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
+github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
+github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
@@ -133,10 +143,12 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
+github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregdel/pushover v0.0.0-20200416074932-c8ad547caed4 h1:QZVozMeLCqyMOOhA+OuqQdNXkyu4uUQEu4+mPBB5pPQ=
@@ -228,6 +240,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
+github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
+github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
github.com/lestrrat-go/strftime v1.0.1 h1:o7qz5pmLzPDLyGW4lG6JvTKPUfTFXwe+vOamIYWtnVU=
@@ -317,6 +331,7 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
@@ -349,6 +364,7 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -550,6 +566,10 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v9 v9.30.0 h1:Wk0Z37oBmKj9/n+tPyBHZmeL19LaCoK3Qq48VwYENss=
+gopkg.in/go-playground/validator.v9 v9.30.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
diff --git a/hems/config.go b/hems/config.go
index bf9834f72..12062edc0 100644
--- a/hems/config.go
+++ b/hems/config.go
@@ -5,6 +5,7 @@ import (
"strings"
"github.com/andig/evcc/core"
+ "github.com/andig/evcc/hems/ocpp"
"github.com/andig/evcc/hems/semp"
"github.com/andig/evcc/server"
"github.com/andig/evcc/util"
@@ -16,10 +17,12 @@ type HEMS interface {
}
// NewFromConfig creates new HEMS from config
-func NewFromConfig(typ string, site *core.Site, cache *util.Cache, httpd *server.HTTPd) (HEMS, error) {
+func NewFromConfig(typ string, other map[string]interface{}, site *core.Site, cache *util.Cache, httpd *server.HTTPd) (HEMS, error) {
switch strings.ToLower(typ) {
case "sma", "shm", "semp":
return semp.New(site, cache, httpd)
+ case "ocpp":
+ return ocpp.New(other, site, cache)
default:
return nil, errors.New("unknown hems: " + typ)
}
diff --git a/hems/ocpp/configmap.go b/hems/ocpp/configmap.go
new file mode 100644
index 000000000..b27f12001
--- /dev/null
+++ b/hems/ocpp/configmap.go
@@ -0,0 +1,135 @@
+package ocpp
+
+import (
+ "strconv"
+
+ "github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
+ "github.com/lorenzodonini/ocpp-go/ocpp1.6/types"
+)
+
+// modified from https://github.com/lorenzodonini/ocpp-go/tree/master/example/1.6/cp
+
+// configuration options
+const (
+ AuthorizeRemoteTxRequests string = "AuthorizeRemoteTxRequests"
+ ClockAlignedDataInterval string = "ClockAlignedDataInterval"
+ ConnectionTimeOut string = "ConnectionTimeOut"
+ ConnectorPhaseRotation string = "ConnectorPhaseRotation"
+ GetConfigurationMaxKeys string = "GetConfigurationMaxKeys"
+ HeartbeatInterval string = "HeartbeatInterval"
+ LocalAuthorizeOffline string = "LocalAuthorizeOffline"
+ LocalPreAuthorize string = "LocalPreAuthorize"
+ MeterValuesAlignedData string = "MeterValuesAlignedData"
+ MeterValuesSampledData string = "MeterValuesSampledData"
+ MeterValueSampleInterval string = "MeterValueSampleInterval"
+ NumberOfConnectors string = "NumberOfConnectors"
+ ResetRetries string = "ResetRetries"
+ StopTransactionOnEVSideDisconnect string = "StopTransactionOnEVSideDisconnect"
+ StopTransactionOnInvalidID string = "StopTransactionOnInvalidId"
+ StopTxnAlignedData string = "StopTxnAlignedData"
+ StopTxnSampledData string = "StopTxnSampledData"
+ SupportedFeatureProfiles string = "SupportedFeatureProfiles"
+ TransactionMessageAttempts string = "TransactionMessageAttempts"
+ TransactionMessageRetryInterval string = "TransactionMessageRetryInterval"
+ UnlockConnectorOnEVSideDisconnect string = "UnlockConnectorOnEVSideDisconnect"
+ WebSocketPingInterval string = "WebSocketPingInterval"
+ LocalAuthListEnabled string = "LocalAuthListEnabled"
+ LocalAuthListMaxLength string = "LocalAuthListMaxLength"
+ SendLocalListMaxLength string = "SendLocalListMaxLength"
+ ChargeProfileMaxStackLevel string = "ChargeProfileMaxStackLevel"
+ ChargingScheduleAllowedChargingRateUnit string = "ChargingScheduleAllowedChargingRateUnit"
+ ChargingScheduleMaxPeriods string = "ChargingScheduleMaxPeriods"
+ MaxChargingProfilesInstalled string = "MaxChargingProfilesInstalled"
+)
+
+// ConfigMap defines the active configuration settings
+type ConfigMap map[string]core.ConfigurationKey
+
+// func (c ConfigMap) updateInt(key string, i int64) {
+// configKey, ok := c[key]
+// if ok {
+// configKey.Value = strconv.FormatInt(i, 10)
+// }
+// c[key] = configKey
+// }
+
+// func (c ConfigMap) updateBool(key string, b bool) {
+// configKey, ok := c[key]
+// if ok {
+// configKey.Value = strconv.FormatBool(b)
+// }
+// c[key] = configKey
+// }
+
+// func (c ConfigMap) getInt(key string) (int, bool) {
+// configKey, ok := c[key]
+// if !ok {
+// return 0, ok
+// }
+// result, err := strconv.ParseInt(configKey.Value, 10, 32)
+// if err != nil {
+// return 0, false
+// }
+// return int(result), true
+// }
+
+// func (c ConfigMap) getBool(key string) (bool, bool) {
+// configKey, ok := c[key]
+// if !ok {
+// return false, ok
+// }
+// result, err := strconv.ParseBool(configKey.Value)
+// if err != nil {
+// return false, false
+// }
+// return result, true
+// }
+
+func (c ConfigMap) set(key string, readonly bool, value string) {
+ c[key] = core.ConfigurationKey{
+ Key: key,
+ Readonly: readonly,
+ Value: value,
+ }
+}
+
+func getDefaultConfig() ConfigMap {
+ intBase := 10
+
+ var cfg ConfigMap = make(map[string]core.ConfigurationKey)
+
+ // readonly
+ cfg.set(SupportedFeatureProfiles, true, core.ProfileName)
+ cfg.set(AuthorizeRemoteTxRequests, true, strconv.FormatBool(false))
+ cfg.set(GetConfigurationMaxKeys, true, strconv.FormatInt(50, intBase))
+ cfg.set(NumberOfConnectors, true, strconv.FormatInt(1, intBase))
+ cfg.set(LocalAuthListMaxLength, true, strconv.FormatInt(100, intBase))
+ cfg.set(SendLocalListMaxLength, true, strconv.FormatInt(20, intBase))
+ cfg.set(ChargeProfileMaxStackLevel, true, strconv.FormatInt(10, intBase))
+ cfg.set(ChargingScheduleAllowedChargingRateUnit, true, "Power")
+ cfg.set(ChargingScheduleMaxPeriods, true, strconv.FormatInt(5, intBase))
+ cfg.set(MaxChargingProfilesInstalled, true, strconv.FormatInt(10, intBase))
+
+ // read/write
+ cfg.set(ClockAlignedDataInterval, false, strconv.FormatInt(0, intBase))
+ cfg.set(ConnectionTimeOut, false, strconv.FormatInt(60, intBase))
+ cfg.set(ConnectorPhaseRotation, false, "Unknown")
+ cfg.set(HeartbeatInterval, false, strconv.FormatInt(86400, intBase))
+ cfg.set(LocalAuthorizeOffline, false, strconv.FormatBool(true))
+ cfg.set(LocalAuthListEnabled, false, strconv.FormatBool(true))
+ cfg.set(LocalPreAuthorize, false, strconv.FormatBool(false))
+ cfg.set(MeterValuesAlignedData, false, string(types.MeasurandEnergyActiveExportRegister))
+ cfg.set(MeterValuesSampledData, false, string(types.MeasurandEnergyActiveExportRegister))
+ cfg.set(MeterValueSampleInterval, false, strconv.FormatInt(5, intBase))
+ cfg.set(ResetRetries, false, strconv.FormatInt(10, intBase))
+ cfg.set(StopTransactionOnEVSideDisconnect, false, strconv.FormatBool(true))
+ cfg.set(StopTransactionOnInvalidID, false, strconv.FormatBool(true))
+ cfg.set(StopTxnAlignedData, false, strconv.FormatBool(true))
+ cfg.set(StopTxnSampledData, false, string(types.MeasurandEnergyActiveExportRegister))
+ cfg.set(TransactionMessageAttempts, false, strconv.FormatInt(5, intBase))
+ cfg.set(TransactionMessageRetryInterval, false, strconv.FormatInt(60, intBase))
+ cfg.set(UnlockConnectorOnEVSideDisconnect, false, strconv.FormatBool(true))
+ cfg.set(WebSocketPingInterval, false, strconv.FormatInt(54, intBase))
+
+ return cfg
+}
diff --git a/hems/ocpp/core_configuration.go b/hems/ocpp/core_configuration.go
new file mode 100644
index 000000000..03b89653b
--- /dev/null
+++ b/hems/ocpp/core_configuration.go
@@ -0,0 +1,40 @@
+package ocpp
+
+import "github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
+
+// OnGetConfiguration handles the CS message
+func (s *OCPP) OnGetConfiguration(request *core.GetConfigurationRequest) (confirmation *core.GetConfigurationConfirmation, err error) {
+ s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
+
+ var resultKeys []core.ConfigurationKey
+ var unknownKeys []string
+
+ for _, key := range request.Key {
+ configKey, ok := s.configuration[key]
+ if !ok {
+ unknownKeys = append(unknownKeys, configKey.Value)
+ } else {
+ resultKeys = append(resultKeys, configKey)
+ }
+ }
+
+ // return config for all keys
+ if len(request.Key) == 0 {
+ for _, v := range s.configuration {
+ resultKeys = append(resultKeys, v)
+ }
+ }
+
+ s.log.TRACE.Printf("%s: configuration for requested keys: %v", request.GetFeatureName(), request.Key)
+
+ conf := core.NewGetConfigurationConfirmation(resultKeys)
+ conf.UnknownKey = unknownKeys
+
+ return conf, nil
+}
+
+// OnChangeConfiguration handles the CS message
+func (s *OCPP) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) {
+ s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
+ return core.NewChangeConfigurationConfirmation(core.ConfigurationStatusAccepted), nil
+}
diff --git a/hems/ocpp/core_other.go b/hems/ocpp/core_other.go
new file mode 100644
index 000000000..8674a2384
--- /dev/null
+++ b/hems/ocpp/core_other.go
@@ -0,0 +1,23 @@
+package ocpp
+
+import (
+ "github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
+)
+
+// OnClearCache handles the CS message
+func (s *OCPP) OnClearCache(request *core.ClearCacheRequest) (confirmation *core.ClearCacheConfirmation, err error) {
+ s.log.TRACE.Printf("recv: %s", request.GetFeatureName())
+ return core.NewClearCacheConfirmation(core.ClearCacheStatusAccepted), nil
+}
+
+// OnDataTransfer handles the CS message
+func (s *OCPP) OnDataTransfer(request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) {
+ s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
+ return core.NewDataTransferConfirmation(core.DataTransferStatusAccepted), nil
+}
+
+// OnReset handles the CS message
+func (s *OCPP) OnReset(request *core.ResetRequest) (confirmation *core.ResetConfirmation, err error) {
+ s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
+ return core.NewResetConfirmation(core.ResetStatusAccepted), nil
+}
diff --git a/hems/ocpp/core_transaction.go b/hems/ocpp/core_transaction.go
new file mode 100644
index 000000000..e5fcba0f2
--- /dev/null
+++ b/hems/ocpp/core_transaction.go
@@ -0,0 +1,30 @@
+package ocpp
+
+import (
+ "github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
+ "github.com/lorenzodonini/ocpp-go/ocpp1.6/types"
+)
+
+// OnChangeAvailability handles the CS message
+func (s *OCPP) OnChangeAvailability(request *core.ChangeAvailabilityRequest) (confirmation *core.ChangeAvailabilityConfirmation, err error) {
+ s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
+ return core.NewChangeAvailabilityConfirmation(core.AvailabilityStatusRejected), nil
+}
+
+// OnUnlockConnector handles the CS message
+func (s *OCPP) OnUnlockConnector(request *core.UnlockConnectorRequest) (confirmation *core.UnlockConnectorConfirmation, err error) {
+ s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
+ return core.NewUnlockConnectorConfirmation(core.UnlockStatusUnlocked), nil
+}
+
+// OnRemoteStartTransaction handles the CS message
+func (s *OCPP) OnRemoteStartTransaction(request *core.RemoteStartTransactionRequest) (confirmation *core.RemoteStartTransactionConfirmation, err error) {
+ s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
+ return core.NewRemoteStartTransactionConfirmation(types.RemoteStartStopStatusRejected), nil
+}
+
+// OnRemoteStopTransaction handles the CS message
+func (s *OCPP) OnRemoteStopTransaction(request *core.RemoteStopTransactionRequest) (confirmation *core.RemoteStopTransactionConfirmation, err error) {
+ s.log.TRACE.Printf("recv: %s %+v", request.GetFeatureName(), request)
+ return core.NewRemoteStopTransactionConfirmation(types.RemoteStartStopStatusRejected), nil
+}
diff --git a/hems/ocpp/ocpp.go b/hems/ocpp/ocpp.go
new file mode 100644
index 000000000..df54620f8
--- /dev/null
+++ b/hems/ocpp/ocpp.go
@@ -0,0 +1,106 @@
+package ocpp
+
+import (
+ "fmt"
+ "math/rand"
+ "strings"
+ "time"
+
+ "github.com/andig/evcc/core"
+ "github.com/andig/evcc/util"
+ "github.com/denisbrodbeck/machineid"
+
+ ocpp16 "github.com/lorenzodonini/ocpp-go/ocpp1.6"
+ ocppcore "github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
+ "github.com/lorenzodonini/ocpp-go/ws"
+)
+
+// OCPP is an OCPP client
+type OCPP struct {
+ log *util.Logger
+ cache *util.Cache
+ site site
+ cp ocpp16.ChargePoint
+ configuration ConfigMap
+}
+
+// site is the minimal interface for accessing site methods
+type site interface {
+ LoadPoints() []core.LoadPointAPI
+}
+
+const retryTimeout = 5 * time.Second
+
+// New generates OCPP chargepoint client
+func New(conf map[string]interface{}, site site, cache *util.Cache) (*OCPP, error) {
+ cc := struct {
+ URI string
+ StationID string
+ }{}
+
+ if err := util.DecodeOther(conf, &cc); err != nil {
+ return nil, err
+ }
+
+ log := util.NewLogger("ocpp")
+
+ if cc.StationID == "" {
+ id, err := machineid.ID()
+ if err == nil {
+ cc.StationID = fmt.Sprintf("evcc-%s", strings.ToLower(id))
+ } else {
+ cc.StationID = fmt.Sprintf("evcc-%d", rand.Int31())
+ }
+ log.DEBUG.Println("station id:", cc.StationID)
+ }
+
+ ws := ws.NewClient()
+ cp := ocpp16.NewChargePoint(cc.StationID, nil, ws)
+
+ s := &OCPP{
+ log: log,
+ cache: cache,
+ site: site,
+ cp: cp,
+ configuration: getDefaultConfig(),
+ }
+
+ err := cp.Start(cc.URI)
+ if err == nil {
+ cp.SetCoreHandler(s)
+ go s.errorHandler(ws.Errors())
+ go s.errorHandler(cp.Errors())
+ }
+
+ return s, err
+}
+
+// errorHandler logs error channel
+func (s *OCPP) errorHandler(errC <-chan error) {
+ for err := range errC {
+ s.log.ERROR.Println(err)
+ }
+}
+
+// Run executes the OCPP chargepoint client
+func (s *OCPP) Run() {
+ for {
+ for id := range s.site.LoadPoints() {
+ connector := id + 1
+
+ status := ocppcore.ChargePointStatusAvailable
+ if statusP, err := s.cache.GetChecked(id, "charging"); err == nil {
+ if statusP.Val.(bool) {
+ status = ocppcore.ChargePointStatusCharging
+ }
+ }
+
+ s.log.TRACE.Printf("send: lp-%d status: %+v", connector, status)
+ if _, err := s.cp.StatusNotification(connector, ocppcore.NoError, status); err != nil {
+ s.log.ERROR.Printf("lp-%d: %v", connector, err)
+ }
+ }
+
+ time.Sleep(retryTimeout)
+ }
+}
diff --git a/hems/semp/messages.go b/hems/semp/messages.go
index be019a4ed..231eda644 100644
--- a/hems/semp/messages.go
+++ b/hems/semp/messages.go
@@ -7,6 +7,7 @@ const (
urnSEMPService = "urn:schemas-simple-energy-management-protocol:service-1-0"
)
+// DeviceDescription message definition
type DeviceDescription struct {
XMLName xml.Name `xml:"root"`
Xmlns string `xml:"xmlns,attr"`
@@ -14,22 +15,25 @@ type DeviceDescription struct {
Device Device `xml:"device"`
}
+// SpecVersion message definition
type SpecVersion struct {
Major int `xml:"major"`
Minor int `xml:"minor"`
}
+// Device message definition
type Device struct {
- DeviceType string `xml:"deviceType"`
- FriendlyName string `xml:"friendlyName"`
- Manufacturer string `xml:"manufacturer"`
- ModelName string `xml:"modelName"`
- UDN string `xml:"UDN"`
- PresentationURL string `xml:"presentationURL"`
- SEMPService SEMPService `xml:"semp:X_SEMPSERVICE"`
- ServiceList []Service `xml:"serviceList"` // optional
+ DeviceType string `xml:"deviceType"`
+ FriendlyName string `xml:"friendlyName"`
+ Manufacturer string `xml:"manufacturer"`
+ ModelName string `xml:"modelName"`
+ UDN string `xml:"UDN"`
+ PresentationURL string `xml:"presentationURL"`
+ ServiceDefinition ServiceDefinition `xml:"semp:X_SEMPSERVICE"`
+ ServiceList []Service `xml:"serviceList"` // optional
}
+// Service message definition
type Service struct {
ServiceType string `xml:"serviceType"`
ServiceID string `xml:"serviceId"`
@@ -38,7 +42,8 @@ type Service struct {
EventSubURL string `xml:"eventSubURL"`
}
-type SEMPService struct {
+// ServiceDefinition message definition
+type ServiceDefinition struct {
Xmlns string `xml:"xmlns:semp,attr"`
Server string `xml:"semp:server"`
BasePath string `xml:"semp:basePath"`
@@ -55,12 +60,14 @@ type Device2EM struct {
PlanningRequest []PlanningRequest `xml:",omitempty"`
}
+// DeviceInfo message definition
type DeviceInfo struct {
Identification Identification
Characteristics Characteristics
Capabilities Capabilities
}
+// Identification message definition
type Identification struct {
DeviceID string `xml:"DeviceId"`
DeviceName string
@@ -69,6 +76,7 @@ type Identification struct {
DeviceVendor string
}
+// Characteristics message definition
type Characteristics struct {
MinPowerConsumption int
MaxPowerConsumption int
@@ -76,11 +84,13 @@ type Characteristics struct {
MinOffTime int `xml:",omitempty"`
}
+// method definitions
const (
MethodMeasurement = "Measurement"
MethodEstimation = "Estimation"
)
+// Capabilities message definition
type Capabilities struct {
CurrentPowerMethod string `xml:"CurrentPower>Method"`
AbsoluteTimestamps bool `xml:"Timestamps>AbsoluteTimestamps"`
@@ -88,11 +98,13 @@ type Capabilities struct {
OptionalEnergy bool `xml:"Requests>OptionalEnergy"`
}
+// status definitions
const (
StatusOn = "On"
StatusOff = "Off"
)
+// DeviceStatus message definition
type DeviceStatus struct {
DeviceID string `xml:"DeviceId"`
EMSignalsAccepted bool
@@ -100,16 +112,19 @@ type DeviceStatus struct {
PowerInfo PowerInfo `xml:"PowerConsumption>PowerInfo"`
}
+// PowerInfo message definition
type PowerInfo struct {
AveragePower int
Timestamp int
AveragingInterval int
}
+// PlanningRequest message definition
type PlanningRequest struct {
Timeframe []Timeframe
}
+// Timeframe message definition
type Timeframe struct {
DeviceID string `xml:"DeviceId"`
EarliestStart int
@@ -126,6 +141,7 @@ type EM2Device struct {
DeviceControl []DeviceControl `xml:",omitempty"`
}
+// DeviceControl message definition
type DeviceControl struct {
DeviceID string `xml:"DeviceId"`
On bool
diff --git a/hems/semp/semp.go b/hems/semp/semp.go
index 24613f4f1..387363aa3 100644
--- a/hems/semp/semp.go
+++ b/hems/semp/semp.go
@@ -2,7 +2,6 @@ package semp
import (
"encoding/xml"
- "errors"
"fmt"
"io/ioutil"
"net"
@@ -22,7 +21,8 @@ import (
)
const (
- sempBaseUrlEnv = "SEMP_BASE_URL"
+ sempController = "Sunny Home Manager"
+ sempBaseURLEnv = "SEMP_BASE_URL"
sempGateway = "urn:schemas-simple-energy-management-protocol:device:Gateway:1"
sempLocalDevice = "F-28081973-%s-%.02d"
sempSerialNumber = "%s-%d"
@@ -44,16 +44,11 @@ type SEMP struct {
uid string
hostURI string
port int
- site site
-}
-
-// site is the minimal interface for accessing site methods
-type site interface {
- LoadPoints() []core.LoadPointAPI
+ site core.SiteAPI
}
// New generates SEMP Gateway listening at /semp endpoint
-func New(site site, cache *util.Cache, httpd *server.HTTPd) (*SEMP, error) {
+func New(site core.SiteAPI, cache *util.Cache, httpd *server.HTTPd) (*SEMP, error) {
uid, err := uuid.NewUUID()
if err != nil {
return nil, err
@@ -142,7 +137,7 @@ func (s *SEMP) Done() chan struct{} {
}
func (s *SEMP) callbackURI() string {
- if uri := os.Getenv(sempBaseUrlEnv); uri != "" {
+ if uri := os.Getenv(sempBaseURLEnv); uri != "" {
return strings.TrimSuffix(uri, "/")
}
@@ -151,11 +146,11 @@ func (s *SEMP) callbackURI() string {
if len(ips) > 0 {
ip = ips[0].String()
} else {
- s.log.ERROR.Printf("couldn't determine ip address- specify %s to override", sempBaseUrlEnv)
+ s.log.ERROR.Printf("couldn't determine ip address- specify %s to override", sempBaseURLEnv)
}
uri := fmt.Sprintf("http://%s:%d", ip, s.port)
- s.log.WARN.Printf("%s unspecified, using %s instead", sempBaseUrlEnv, uri)
+ s.log.WARN.Printf("%s unspecified, using %s instead", sempBaseURLEnv, uri)
return uri
}
@@ -177,6 +172,8 @@ func (s *SEMP) handlers(router *mux.Router) {
}
func (s *SEMP) writeXML(w http.ResponseWriter, msg interface{}) {
+ s.log.TRACE.Printf("send: %+v", msg)
+
b, err := xml.MarshalIndent(msg, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
@@ -201,7 +198,7 @@ func (s *SEMP) gatewayDescription(w http.ResponseWriter, r *http.Request) {
ModelName: serverName,
PresentationURL: s.hostURI,
UDN: uid,
- SEMPService: SEMPService{
+ ServiceDefinition: ServiceDefinition{
Xmlns: urnSEMPService,
Server: s.hostURI,
BasePath: basePath,
@@ -303,18 +300,6 @@ func (s *SEMP) deviceID(id int) string {
return fmt.Sprintf(sempLocalDevice, s.serialNumber(), id)
}
-// cacheGet returns loadpoint value from cache
-func (s *SEMP) cacheGet(id int, key string) (res util.Param, err error) {
- pid := util.Param{LoadPoint: &id, Key: key}
-
- res = s.cache.Get(pid.UniqueID())
- if res.Key == "" {
- err = errors.New("not found")
- }
-
- return res, err
-}
-
func (s *SEMP) deviceInfo(id int, lp core.LoadPointAPI) DeviceInfo {
method := MethodEstimation
if lp.HasChargeMeter() {
@@ -353,12 +338,19 @@ func (s *SEMP) allDeviceInfo() (res []DeviceInfo) {
func (s *SEMP) deviceStatus(id int, lp core.LoadPointAPI) DeviceStatus {
var chargePower float64
- if chargePowerP, err := s.cacheGet(id, "chargePower"); err == nil {
+ if chargePowerP, err := s.cache.GetChecked(id, "chargePower"); err == nil {
chargePower = chargePowerP.Val.(float64)
}
+ isPV := false
+ if modeP, err := s.cache.GetChecked(id, "mode"); err == nil {
+ if mode, ok := modeP.Val.(api.ChargeMode); ok && mode == api.ModePV {
+ isPV = true
+ }
+ }
+
status := StatusOff
- if statusP, err := s.cacheGet(id, "charging"); err == nil {
+ if statusP, err := s.cache.GetChecked(id, "charging"); err == nil {
if statusP.Val.(bool) {
status = StatusOn
}
@@ -366,7 +358,7 @@ func (s *SEMP) deviceStatus(id int, lp core.LoadPointAPI) DeviceStatus {
res := DeviceStatus{
DeviceID: s.deviceID(id),
- EMSignalsAccepted: true,
+ EMSignalsAccepted: isPV,
PowerInfo: PowerInfo{
AveragePower: int(chargePower),
AveragingInterval: 60,
@@ -387,17 +379,17 @@ func (s *SEMP) allDeviceStatus() (res []DeviceStatus) {
func (s *SEMP) planningRequest(id int, lp core.LoadPointAPI) (res PlanningRequest) {
mode := api.ModeOff
- if modeP, err := s.cacheGet(id, "mode"); err == nil {
+ if modeP, err := s.cache.GetChecked(id, "mode"); err == nil {
mode = api.ChargeMode(modeP.Val.(string))
}
var charging bool
- if chargingP, err := s.cacheGet(id, "charging"); err == nil {
+ if chargingP, err := s.cache.GetChecked(id, "charging"); err == nil {
charging = chargingP.Val.(bool)
}
chargeEstimate := time.Duration(-1)
- if chargeEstimateP, err := s.cacheGet(id, "chargeEstimate"); err == nil {
+ if chargeEstimateP, err := s.cache.GetChecked(id, "chargeEstimate"); err == nil {
chargeEstimate = chargeEstimateP.Val.(time.Duration)
}
@@ -407,7 +399,7 @@ func (s *SEMP) planningRequest(id int, lp core.LoadPointAPI) (res PlanningReques
}
var maxEnergy int
- if chargeRemainingEnergyP, err := s.cacheGet(id, "chargeRemainingEnergy"); err == nil {
+ if chargeRemainingEnergyP, err := s.cache.GetChecked(id, "chargeRemainingEnergy"); err == nil {
maxEnergy = int(chargeRemainingEnergyP.Val.(float64))
}
@@ -423,7 +415,7 @@ func (s *SEMP) planningRequest(id int, lp core.LoadPointAPI) (res PlanningReques
if charging {
res = PlanningRequest{
- Timeframe: []Timeframe{Timeframe{
+ Timeframe: []Timeframe{{
DeviceID: s.deviceID(id),
EarliestStart: 0,
LatestEnd: latestEnd,
@@ -462,5 +454,27 @@ func (s *SEMP) deviceControlHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ for _, dev := range msg.DeviceControl {
+ did := dev.DeviceID
+
+ for id, lp := range s.site.LoadPoints() {
+ if did != s.deviceID(id) {
+ continue
+ }
+
+ if mode := lp.GetMode(); mode != api.ModePV {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ demand := core.RemoteSoftDisable
+ if dev.On {
+ demand = core.RemoteEnable
+ }
+
+ lp.RemoteControl(sempController, demand)
+ }
+ }
+
w.WriteHeader(http.StatusOK)
}
diff --git a/server/assets.go b/server/assets.go
index 9e6e1258a..5ad92d8e0 100644
--- a/server/assets.go
+++ b/server/assets.go
@@ -24277,87 +24277,90 @@ Pp3/GoRPOTyAIZY5uQvssmEOLnEm3H4CAAD//9PqMyRSAQAA
"/dist/index.html": {
name: "index.html",
local: "dist/index.html",
- size: 19473,
+ size: 20313,
modtime: 1566640112,
compressed: `
-H4sIAAAAAAAC/8w825LbNpbv/RXHdMWyK6bUUl8cy5KqkrZ3kprY60o7dtWmUlsgcSRiGwS4AKi+eLpq
-/2H3cR/zGfOWP5kv2QJAUiRFXbrdyawfLInAuV9wDgD25BGVsbnOEBKT8tnE/g+ciMU0QBHMJgkSOpuk
-aAjECVEazTTIzTz8JiieCpLiNFgyvMykMgHEUhgUZhpcMmqSKcUlizF0P54DE8wwwkMdE47T4XPQiWLi
-IjQynDMzFbKJlaKOFcsMk6KG+M1HOEuIWiCcSWGU5BxVE47kJpGqBkIEZYvWnCzjGKYyYhzDS4xCkmVh
-TDIScaxBXqPeA04bYnIdRkSF2lw3EEScxBfBbMKZuACFvMRgZB4nIYutaJrdoJ4Gw28Or4bfHAaQKJxP
-gwFl2gzas/s4Oo6OT8hpPxOLBl6Py9pyGrCULHBgZ5TIj0ZXR6Mm6jlZWpjQDfVPoyOCL1/SO+Idnl4N
-T7vxuqE+YjQa0Xid35QINkdtmsAslgPNDPYvMapmNMH0RaG3Opwmc6JYmDEhkIaGRP3RPD6kc/qir5cL
-axAu1TR4fBJFhJ6sCQg6kcrEuekUpX9EyYv56HjUZ3HLR1NtLcRiYr00/MA4nllKNQ94PIpOXrx8sQ0s
-lmLOFjUYTz1S8lKj8qN9HOHhyTGS/lXKm8hMgimGcZvu3P0LZhPDDMcZLuN4MvDfJ4/CEM7OzyEMC03U
-pY6kNNookvVTJvp0fnR6cvgS+7HWgdeYc3KdIFamaShNCkMuUcsUHQJyFEUvjuLR3ggULnJOlAM+Hb2k
-IyQv9wbWkjPqQI9PyelJTE/2BiVZ1j8l5JshefFiE9DAZ8RI0usZTChbAqMupoMZTARZQsyJ1tMgWoSc
-LRIDgiwjooqPEK8yImjIF+UDN8nCkhKyGIgUEbT0xsSYTI8HgwUzSR71Y5kOXE4bWKMGswkrgeck5Ejm
-MCcaUhWOwOCVCTPFUqKuLfus8AMyg0mUGyNFi66Ri4XNqEXE+zkBUGJIMTYNYsk5yTSWj202th7nUbwj
-y2+5eUvURZ4FQBQj1sFtpq6odEzxmkFqZeAWtXvKSWRN8MERtjpjCxcyVmM6IxuY9/lhNhnYKTOYDLwU
-hcEKkFKI0hIroaxFO/hsghdQgiztgJK5QWtNcVGbEDKDqcXvB7IoHAZg5DQYBLMfCUUxGdQA74GmSByz
-v7rPXDndtJGSXaj28LEB0zpHHUBp63+POBEXwew8z+zC7x1qQNmy9iGI/Vii0kwKYEIbwrm18C+/9D/6
-p7/+6tVdTLJLqLVb8XMGkxS1Jgu7AhJt9GwyaD2odGZLkFkluvtVceLLiMKjbUQMrkKDacaJwSb96uns
-ADq9xeaE+TRwfB4ANCcpeQlZeATRIrwkSjC74rlJa7jC4agaAniHOUKhEViimv/+90VE1KNqwg9edwyV
-GcPnz5Uqb2/71ZyPFZydYksS7JMlYdyWNAXy+vxJVPKTXYVHe0a4Qo5EYyiksd6wDNncJkhLqxh650ZW
-soF1QO9hj4OSostKNoc2JgL85HGAQ98Yqac4DXMSxgkulRShTSCeDQd0nshL4RPdfvDUzodliFxjG8x6
-9Upfg2i2SXktcZdhxAQde6l72wOrUJseGLIY9L7utttOxb2Wl4JLQnfIeRcB/eLQXAVKB+ZSI8y5JCZU
-bgEDt0ja6kwtmAiNzMYQjrKrZho/s3B7elojI9RZdmnfYU0YpSimgVE5BrMnhqWoXxVJvy6VT/5lILqc
-cND+2h3FfglvrgytEHA2Fg2n74h1WIa2teqOlS6Wii+Tgc9dtVrDJ75yNXKKIEamLC4UUZoik5rZ5WBM
-Ii15bvCVtcqxwvSVs9m4f2K/W0S2GnxfTAdmbFXYyrywDOe2fnbLB7OTMHXx7xzd/vJjAYwtj70SmNGw
-97Ud6DMawPgCr/08+9Pm5waRtRVkd96u4DflbYe48C2KnFxPg+Hh4eFhxbsbIbmRCaNY+tKaTzg0oa39
-cOWOE22UFItyTqocnjIdOTFda/1oCj27HvTqlZqLZErEAhUUoYpXMSepbwmMYkQsOPqQhTdKSTUZeIJ3
-pT/dSL9YpfZg4JOfuc5CSjivU/S9cDD7/sOH93apWj27vZ0M3PQKeFuOSXk4gjQKhxDXsgZlOmU1u65n
-l1rk3ylTNPPEhtTg3cDn30K2wgOtcBtDdw8/Jmxr8SEMYaJyvYntkEsHdhmlSCyl7eOiHkT6tPfLL/2z
-6uevv/aeucKYGayJVyx/HfnLJcPr8KSu12Z6a1QyAJOsHEuGkJnwxBaZI4ikoqjCSBoj02D2iXF+IdMU
-BUTIwPckWTcejoRaTKNgNomK9iWaAdMGKENh4CZXMOd4xSKOAmxhrQ3mqHKxgKUU8IbjhVFyThJ1g/kC
-RX8jLU8G3mjQDBMDJNfPgXANl7//phBeIxNQZ0LIOAHB4sTARaFkW6T14ee0Me8mbyzXtbkoQDNBrSgw
-l3yBgqKA8zhRzBhXCV2irRbGzXJG8ibLjVGACWezN4W0gNZ1oNEjaEoMMkiZgXfEWmESS+r3CPrXJOWT
-gfvdh9cMW6gBzg0RlCh6UcdYx2C76ToauCBCACXz3/+unDY/SsXJAp39UMDTCVkjAnu1JRGX0SAl2qAa
-NEkHs7ImskXOs/5kwFmXlhpqAYoKPhHOI3nlGPVCxW6/UelSK/vi0vDm48/wb7//lnBUGnJBAZeG9+ES
-mUGFqhyDXBhUBbUUzb1ovUNzQ4SOE55rjbqB08Z7ZdMfSpc4t2nkkinq5G7AW9dYswnFtOHmKOoCwtMa
-/wWxZ7adiXLr03tKYjnTLoizXFyYlhzWnplkwui2NOkKphCpw3MbzFeGftqwcsW5DQ7awLtblnNDlHFJ
-px79HpMCgTmg0gY5t7prCD6uBRCEMbRDcZ3iZCB5s5hvJbXZWyZYSjjCd8h0xpA3I9aFY43P8TqGA8/U
-JFNYLcy+4ckVGwOXMeGJ1Gb84vDFITyGb6lCbXsDi/rnHw6Ytd2S8DEMDzU8hp9wgbx8yA+8q48PQreT
-OQZc5uGN96YDu1qOod/vw2N4jYawFvft8KqQcELxLkh+XM3X8FS68wbCnx2UUb9i79I7zB5YqyxyYCPP
-5m638TqG72Vq3bIUHGChGG0IDo/rYh2sPN7yUWBZueRjyBlQpjNOrh2XBwAF4xW/8Lh81KBcTmxoDB5v
-1MdkkCnrh84jtvrdR4YczjPi00jdxR5ZV4T3SkYc7bJjFwZt4HUOudBA8vlqt2Cv/ajZX5j5Po/c9hMq
-hSxO2st7rZTr7v7uU69Zq9brtUmJq1k6dXQSNBRSINAwpaEtWsB3iO4jpeExpMZ/Es4Wwu3X6RAFXXUd
-tQLLVqG+o3SO8be/Bda/AluMZlsL2SbRb0qiZf2Y5tywWp9jpaXO0XVn2VlUlOWcbgWvqwfWlNEqEzdC
-low+qnO6Sc0Rl/FFpeBmv71ZJ3+E+FUwl/300+rJc0af2c7am3MV9YFDVjbadBrUGmn7rWCkAghgnC3L
-fQabXFbFfwBjp666fSeDCtKGwD0joRC8HRHtSGhr+7RU9pF1wKPmfmKL93V7pbavWBKe4zrge3mJCmZw
-WFnxO7zJF9Dqf4uDkbL/JUrJy/ZO2QZvaVKvt09vmNAZMm3bj33o5dkGasmoBPZ0SgKfP89T87Ql7LPb
-27Ibr1NMc2OV50B+FqwL7FOjLZ8MklHNa+9otGy5r8kqbbkupamrQj06F9U2TTHyuVfXY29cUnWyTA6f
-91ZCtwZn08PbL9BzgeVuWl4B3UHHrZTVoe6mQnym640feZoRMQbV9bk8u22aphjY2z7fuflVEb3Zk9dZ
-aoRSaYeCfmkp6NUCoGvO7IvsVcd0N6O1IB8yOla22aH7c3m2U+02QgqEoUkUYvifOVG2uNsvaJq67oqc
-psXubo01h9xmhq+26fk+C1O1ru1Tp3XUPZtLg+NawWKNXFRPJ5tLtvpRdTIs5pVFTruEq4p7V8et9ihX
-dcT2Wm699KkzuWJqpaAdtc3axL34cfQtJ8GmfUXHVhbVjlo27zJajC6gGq1GKmnVl16Gw9q2vh0pw85+
-L8qibGlnSDHOM0oM0mngD5ze2imzySBt9DaN1mEzS4XzJER/cMhW8V2aX8bdXGoZl0yaChTGHJfIdTmi
-Zfyje9DNuqM2GWgZ373n2ackT90e7rU31bG/UBKj25OpzLarGu9euQqPPe2N66p7Xj4fjnrjR/WR25Un
-OcPvNvU4Jq59LY/G9jP9lv7Asrvb6M7g9zTxPiy3Tb6XWYtotNyPWlG3y3xd8fdHxt72Bm0/9f9p8bZb
-+fvqt7a8rMfgn7zKbPOlRpO+vtxsa9t3y+5Fjfx/KQ1XHdxkiQmLOW5aporhuyyWxxsWyw37IK1qLZi9
-lTTXzWRbD4vIiHChZJ6FOv2zouMuksGTJ9AdQWuSuk1BhrwpbC3QOmX9A+NtU3ruqH864++fUgA9EMUH
-qosfaNdmeyvzIzJtcrHY1c/YdsYygoqYXGHIbYHIw2Gzg4o5S4lBNZ32EiSGiUWvcalpb/QJWyQVfuQa
-u4nEUvI7EklQpdLtsocJ4fM9iEjR+4JO12/i36PRbQJ+YZ+73Qn+gtxfd72PaPSNQLW4vo9wK9BPyTb5
-vljA1yRHtbd454lU5nVxWtRguHy4Eraq6TcJ3UC2poEawi9UQCMOtYx3aOQn1OYGmXkApbzRxsXKgyml
-hvChdx3sMr0poVZLJKwWS38N0t0YTphB0Amh8nK1bEzc5aYaBgcrc8OZwCr1OHUCZZpEHCkULQTMpTDh
-JdoaP4wkp9X1ID9e6N8VMf4owNLam+paQ0diw5boC5xpT87nvedzprQZPyoIlg3chIksL9WoCGUy8CJM
-AzmfF2VAzFl8Ua+CnjqUz4LZuZHZw/Mr5GVvDwaFr+e7GbQ4LINyLpV5eBZTJrJlr9rXzZa72XUgmxn2
-GJ9V98pqHQQNmbC8AQ35onDIt0w0eoXm/KLs4osC0s2Hr+H9xxXQA2vkrurYpos9FFEVdHWRNukhpZUe
-3uWqWwv3OnGT8Z+fYe6ZW8q24Z6WL85IbXdQno/63uFf1Wuck5ybyj/c82ngPopbyIz6w9O279iuZOqR
-NvPTkyeMTt050WY/8tQKdyqorXvUuTzzbD+zC5L7dnv71Zdavuhwd21o+46tbPi1jD8UPf+/FDcgg/pt
-1RpgpuRCodbVHfLE2XcMo+Ps6pU3uWY3OB4eHn71CmqX/Yen2VVHQVAidLf4QUl3L7145h61grta13t1
-yFAbxTKkvTHUVnEmFs83zCfCLe9bAcrNr94YipOzWAqBsUFam+UKC42xFNQdnWybu3rrZ0W3nGY77ZSJ
-c3n2rfPBEux21Sp7jX92r0qPQcvYv+X82t/t+WQff937yi5Rnz+3Rz9ad7y93bY98P/bFCvFGZVjc6w4
-k2qMbNSbV/JPmBJmEa5rr0BQ7ILUTGJN9LRT7TCB4eFhtTY8QA/u72ff4cL3ozCESTKs1hjPXXgMmdsu
-u3I3/+t782vvBCbDAlXzXnUn6Otic4KigjJnhM/LK22hu8ha3E9rXHxzF68gDDv2RIrznuP2DZ99Xo+7
-242nrgOPTnaSk43k1+4JbWmPRu5wpPE2n7kp7s+NWy8jdGwbbL/08o///e+1F5q2YXL3UJ6klOhk7f2G
-ewnz/mNp6TvL0rwL8k+XpLzPcF/TdNyf+ENkWnPZ/a+KVbVQ+ah5L8y9mbXa/ut9zei2s9lNsbojWtfj
-tSK54fBhx2XNDRwmW95J6Qrgu/gK1C/BjtfePtziKytZ/VbDDne5t8PsPJzeJtz7hGgU+8nVsF9mAfXt
-bfaAzLxlog/nRsn0HgylTJzlSqEwt7ffPiRT5OoLmCJXezL1R3u4Ow9JWtG5/nbXESSjjcG+R8Dbf2Wd
-0HpFocFe14NtMndbbc1YxSYWcj5uv5Kxn8FqLdI//ut/WgeiG1nfm7e/kozcMPP7b+b+/J2RjMTMXN/e
-Xrit7N3stV1j+5WQznvwcykNqqpP9w5iZNY8j3bv92WRP3XMjP/PXzXaVNSunwuv69L9iY9N2/37vibQ
-eLu+8WcnZvVf7v2Bp3vijGWaMjNw71XabwWy8od73QzKfeXqXeY1V3HiqPLPv2yU9Ekss+tXMDocHa7h
-bH14c60aEK3i+p+2weEIkQzn/f9w75xXVh64P2YzGbi/APZ/AQAA//9oN3clEUwAAA==
+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
+ec/TuUBqLzCC73f0RMpz0+P2Tbk+r5ne7eZg18FhJznJydblN+7b7SiPJvaQ0XkrVn8q76FOuzvtzsvt
+Oy+P/eN//3tnz76Nyd7nepLGRCUb7wndi5l3HypN35kX907VP52T6l7QfVXTcQ/pD+Fpw2T7X7msc6Hq
+kXu/0r7huG7/Db6m8a47Dtt8dY+3bvprveSW8709l563UJjseLery4HvYivQvEw+3XiLd4etrHktWg17
+zOXeBrP3kscu5t4lRCHvx5ejv8wAqtvb7AGJeUP5EC60FOk9CEopP8ulRK5vb799SKLI9RcQRa57EvVH
+W7g9D0la3rn5luQRJJOtzt7D4c2/Kk9overjkNf1YBfP3VrbUFbZxELGpu1Xm/oprFEi/eO//qd152Ar
+6b1p+yvJyCeqf/9N35++M5KRiOqb29tL28reT17bNHZfrep8n2QhhEZZ1+mFgWiRufcr7HuyWVicOma6
++K+4srctqd08F96Upf2pnG3t/r6v2zi/UuH8fMu8+c2+h/O0J85IpCnVI/t+svlUIqu+2Nc2oeor178J
+sGEqlh1Z/YzSVk6fRCK7eQWTw8nhBs7Wn0Jd6wJEyaj5E1E4niCS8WL4H/a3G2otj+yPQgUj+0t6/xcA
+AP//9GuoBVlPAAA=
`,
},
diff --git a/util/cache.go b/util/cache.go
index 23dfbafac..e18cf84f6 100644
--- a/util/cache.go
+++ b/util/cache.go
@@ -1,6 +1,7 @@
package util
import (
+ "errors"
"sync"
)
@@ -91,3 +92,15 @@ func (c *Cache) Get(key string) Param {
return Param{}
}
+
+// GetChecked returns checked value from cache
+func (c *Cache) GetChecked(id int, key string) (res Param, err error) {
+ pid := Param{LoadPoint: &id, Key: key}
+
+ res = c.Get(pid.UniqueID())
+ if res.Key == "" {
+ err = errors.New("not found")
+ }
+
+ return res, err
+}