From 29ea3c6549b1a7e1237bf6151ecc156f3563ee5a Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 21 Nov 2020 16:49:59 +0100 Subject: [PATCH] Add basic OCPP support (#395) --- assets/index.html | 21 ++++- cmd/config.go | 2 +- cmd/root.go | 2 +- cmd/setup.go | 4 +- core/loadpoint.go | 29 +++++- core/loadpoint_api.go | 19 ++++ core/remote.go | 11 +++ core/site_api.go | 8 ++ go.mod | 5 +- go.sum | 20 ++++ hems/config.go | 5 +- hems/ocpp/configmap.go | 135 ++++++++++++++++++++++++++ hems/ocpp/core_configuration.go | 40 ++++++++ hems/ocpp/core_other.go | 23 +++++ hems/ocpp/core_transaction.go | 30 ++++++ hems/ocpp/ocpp.go | 106 +++++++++++++++++++++ hems/semp/messages.go | 34 +++++-- hems/semp/semp.go | 80 +++++++++------- server/assets.go | 161 ++++++++++++++++---------------- util/cache.go | 13 +++ 20 files changed, 618 insertions(+), 130 deletions(-) create mode 100644 core/remote.go create mode 100644 hems/ocpp/configmap.go create mode 100644 hems/ocpp/core_configuration.go create mode 100644 hems/ocpp/core_other.go create mode 100644 hems/ocpp/core_transaction.go create mode 100644 hems/ocpp/ocpp.go 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: ` -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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 +}