Add basic OCPP support (#395)
This commit is contained in:
parent
9bd8754b85
commit
29ea3c6549
20 changed files with 618 additions and 130 deletions
|
|
@ -265,8 +265,25 @@ loadpoints:
|
|||
</div>
|
||||
|
||||
<div class="row" v-if="!multi">
|
||||
<div class="col-12 col-md-4 d-none d-md-flex mt-3 mt-md-5 align-items-end">
|
||||
<span class="h1 align-bottom">{{state.title||"Ladepunkt"}}</span>
|
||||
<div class="col-12 col-md-4 d-none d-md-flex mt-3 mt-md-5 ">
|
||||
<span class="h1 ">{{state.title||"Ladepunkt"}}</span>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 mt-3 mt-md-5 align-items-end"
|
||||
v-if="state.remoteDisabled">
|
||||
<h1 class="d-md-inline d-none">
|
||||
<span class="badge badge-warning font-base" v-if="state.remoteDisabled == 'soft'">
|
||||
Adaptives PV-Laden deaktiviert durch {{state.remoteDisabledSource}}
|
||||
</span>
|
||||
<span class="badge badge-danger font-base" v-if="state.remoteDisabled == 'hard'">
|
||||
Deaktiviert durch {{state.remoteDisabledSource}}
|
||||
</span>
|
||||
</h1>
|
||||
<span class="d-md-none badge badge-warning font-base" v-if="state.remoteDisabled == 'soft'">
|
||||
Adaptives PV-Laden deaktiviert durch {{state.remoteDisabledSource}}
|
||||
</span>
|
||||
<span class="d-md-none badge badge-danger font-base" v-if="state.remoteDisabled == 'hard'">
|
||||
Deaktiviert durch {{state.remoteDisabledSource}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
11
core/remote.go
Normal file
11
core/remote.go
Normal file
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
go.mod
5
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
|
||||
|
|
|
|||
20
go.sum
20
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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
135
hems/ocpp/configmap.go
Normal file
135
hems/ocpp/configmap.go
Normal file
|
|
@ -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
|
||||
}
|
||||
40
hems/ocpp/core_configuration.go
Normal file
40
hems/ocpp/core_configuration.go
Normal file
|
|
@ -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
|
||||
}
|
||||
23
hems/ocpp/core_other.go
Normal file
23
hems/ocpp/core_other.go
Normal file
|
|
@ -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
|
||||
}
|
||||
30
hems/ocpp/core_transaction.go
Normal file
30
hems/ocpp/core_transaction.go
Normal file
|
|
@ -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
|
||||
}
|
||||
106
hems/ocpp/ocpp.go
Normal file
106
hems/ocpp/ocpp.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
161
server/assets.go
161
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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
|
||||
i6v1thHYS4INDBZW5NpUInXoseKEuEoNy1K8SCSv0NTKfihYXF+zK8ZL+dskpjhSM2v1XnWjMUIikzcX
|
||||
Cc5sIBaLwfMFlUpPH5ULVo2QgPIsr8QoSUyFV7Aw88RiUaYBEaPRZTMLempRPvPmF1pkD08vF1eDHgTy
|
||||
oi7uJtDgMASKhZD64UlMKc9Wg/p8JFvtJ9eCbCe4wPisvp/plCh1ucuWpUG+odypFdz5ZdrFliWknQ9f
|
||||
w7sPa6AHlshdxbFLFj0EUSd0TZa2yaFuGHjzt7nslsK9Tq5F9OdHmHvGlqpsuKfmy7sGpjqo7hkUtcO/
|
||||
ynNckJzp2j7s85ln/5S3+WnZ62nbjqlKZgVSNz49eULjmT1v3W5HxWqlOZWrbVrUhTgryH5mNiT76fb2
|
||||
qy/VfFnh7jsYKiq2qvJXInpfttX+pbxJ7DVvfTcAMymWEpWq38VIrH6nMDnOrl8VKlf0E07Hh4dfvYLG
|
||||
SzPj0+y6IyGoENq3YUAK+35H+cw+ajl3va8PmpC+0pJmGA+m0NjFKV8+3zKfcLu97wSomsiDKZQn0JHg
|
||||
HCONcWOWTSwURoLH9ghy19z123PrdatpptJOKb8QZ99aG6zAbtelciHxz/YnB6agRFT8WsB5cUfuo3n8
|
||||
9eArs0V9/twe/WDMsWznbGkP/P9WxVpwWubojpVnu87IVrkVQv4JU0INwk3pOd3fpkqMip52ih0CGB8e
|
||||
1nvDA9TgxXsOd3hx4pHvO93ogjr/GDLbLru2b9A0z7g23q2tuqit9xM6Qc/L5kSMEqqY4T+vrob69kJ4
|
||||
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=
|
||||
`,
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue