Support SMA Sunny Home Manager as Energy Management System (#353)

This commit is contained in:
andig 2020-09-20 17:03:39 +02:00 • committed by GitHub
parent 1387ec1a58
commit 7eb85ad8b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 751 additions and 13 deletions

1
.gitignore vendored
View file

@ -8,3 +8,4 @@ linux-*.Dockerfile
!evcc.dist.yaml
!modules/**
dist
*.py

View file

@ -34,6 +34,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
- [Charger](#charger)
- [Meter](#meter)
- [Vehicle](#vehicle)
- [Home Energy Management System](#home-energy-management-system)
- [Plugins](#plugins)
- [Modbus](#modbus-read-only)
- [MQTT](#mqtt-readwrite)
@ -216,6 +217,16 @@ Available vehicle implementations are:
Configuration examples are documented at [andig/evcc-config#vehicles](https://github.com/andig/evcc-config#vehicles)
### Home Energy Management System
EVCC can integrate itself with Home Energy Management Systems. At this time, the SMA Home Manager (SHM) is the only supported system. To enable add
```yaml
hems: sma
```
to the configuration. The EVCC loadpoints can then be added to the SHM configuration.
## Plugins
Plugins are used to integrate various devices and external data sources with EVCC. Plugins can be used in combination with a `default` type meter, charger or vehicle.

View file

@ -19,6 +19,7 @@ type config struct {
Interval time.Duration
Mqtt provider.MqttConfig
Influx server.InfluxConfig
HEMS string
Menu []server.MenuConfig
Messaging messagingConfig
Meters []qualifiedConfig

View file

@ -156,6 +156,12 @@ func run(cmd *cobra.Command, args []string) {
socketHub := server.NewSocketHub()
httpd := server.NewHTTPd(uri, conf.Menu, site, socketHub, cache)
// start HEMS server
if conf.HEMS != "" {
hems := configureHEMS(conf.HEMS, site, cache, httpd)
go hems.Run()
}
// publish to UI
go socketHub.Run(tee.Attach(), cache)

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/andig/evcc/core"
"github.com/andig/evcc/hems"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/push"
"github.com/andig/evcc/server"
@ -51,6 +52,16 @@ func configureMQTT(conf provider.MqttConfig) {
provider.MQTT = provider.NewMqttClient(conf.Broker, conf.User, conf.Password, mqttClientID(), 1)
}
// 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)
if err != nil {
log.FATAL.Fatal(err)
}
return hems
}
// setup messaging
func configureMessengers(conf messagingConfig, cache *util.Cache) chan push.Event {
notificationChan := make(chan push.Event, 1)
notificationHub := push.NewHub(conf.Events, cache)

View file

@ -603,7 +603,7 @@ func (lp *LoadPoint) publishSoC() {
}
lp.publish("socCharge", -1)
lp.publish("chargeEstimate", -1)
lp.publish("chargeEstimate", time.Duration(-1))
}
// Update is the main control function. It reevaluates meters and charger state

View file

@ -146,7 +146,8 @@ func (site *Site) SetTargetSoC(targetSoC int) {
}
}
func (lp *LoadPoint) hasChargeMeter() bool {
// HasChargeMeter determines if a physical charge meter is attached
func (lp *LoadPoint) HasChargeMeter() bool {
_, isWrapped := lp.chargeMeter.(*wrapper.ChargeMeter)
return lp.chargeMeter != nil && !isWrapped
}
@ -172,7 +173,7 @@ func (site *Site) Configuration() SiteConfiguration {
Phases: lp.Phases,
MinCurrent: lp.MinCurrent,
MaxCurrent: lp.MaxCurrent,
ChargeMeter: lp.hasChargeMeter(),
ChargeMeter: lp.HasChargeMeter(),
}
if lp.vehicle != nil {
@ -215,8 +216,8 @@ func (site *Site) DumpConfig() {
lp.log.INFO.Printf("loadpoint %d config:", i+1)
lp.log.INFO.Printf(" vehicle %s", presence[lp.vehicle != nil])
lp.log.INFO.Printf(" charge %s", presence[lp.hasChargeMeter()])
if lp.hasChargeMeter() {
lp.log.INFO.Printf(" charge %s", presence[lp.HasChargeMeter()])
if lp.HasChargeMeter() {
lp.log.INFO.Println(" charge meter config:")
logMeter(site.log, lp.chargeMeter)
}

3
go.mod
View file

@ -27,6 +27,8 @@ require (
github.com/joeshaw/carwings v0.0.0-20191118152321-61b46581307a
github.com/jsgoecke/tesla v0.0.0-20200530171421-e02ebd220e5a
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/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/mitchellh/mapstructure v1.3.2
github.com/mjibson/esc v0.2.0
@ -41,6 +43,7 @@ require (
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
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
)

6
go.sum
View file

@ -225,10 +225,14 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d h1:68u9r4wEvL3gYg2jvAOgROwZ3H+Y3hIDk4tbbmIjcYQ=
github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
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=
@ -569,6 +573,8 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=

26
hems/config.go Normal file
View file

@ -0,0 +1,26 @@
package hems
import (
"errors"
"strings"
"github.com/andig/evcc/core"
"github.com/andig/evcc/hems/semp"
"github.com/andig/evcc/server"
"github.com/andig/evcc/util"
)
// HEMS describes the HEMS system interface
type HEMS interface {
Run()
}
// NewFromConfig creates new HEMS from config
func NewFromConfig(typ string, 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)
default:
return nil, errors.New("unknown hems: " + typ)
}
}

30
hems/semp/helper.go Normal file
View file

@ -0,0 +1,30 @@
package semp
import "net"
// LocalIPs returns a slice of local IPv4 addresses
func LocalIPs() []net.IP {
ips := make([]net.IP, 0)
ifaces, err := net.Interfaces()
if err != nil {
panic(err)
}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
panic(err)
}
for _, addr := range addrs {
if ip, ok := addr.(*net.IPNet); ok {
if !ip.IP.IsLoopback() && ip.IP.To4() != nil {
ips = append(ips, ip.IP)
}
}
}
}
return ips
}

163
hems/semp/messages.go Normal file
View file

@ -0,0 +1,163 @@
package semp
import "encoding/xml"
const (
urnUPNPDevice = "urn:schemas-upnp-org:device-1-0"
urnSEMPService = "urn:schemas-simple-energy-management-protocol:service-1-0"
)
type DeviceDescription struct {
XMLName xml.Name `xml:"root"`
Xmlns string `xml:"xmlns,attr"`
SpecVersion SpecVersion `xml:"specVersion"`
Device Device `xml:"device"`
}
type SpecVersion struct {
Major int `xml:"major"`
Minor int `xml:"minor"`
}
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
}
type Service struct {
ServiceType string `xml:"serviceType"`
ServiceID string `xml:"serviceId"`
SCPDURL string `xml:"SCPDURL"`
ControlURL string `xml:"controlURL"`
EventSubURL string `xml:"eventSubURL"`
}
type SEMPService struct {
Xmlns string `xml:"xmlns:semp,attr"`
Server string `xml:"semp:server"`
BasePath string `xml:"semp:basePath"`
Transport string `xml:"semp:transport"`
ExchangeFormat string `xml:"semp:exchangeFormat"`
WsVersion string `xml:"semp:wsVersion"`
}
// Device2EM is the device to EM message
type Device2EM struct {
Xmlns string `xml:"xmlns,attr"`
DeviceInfo []DeviceInfo `xml:",omitempty"`
DeviceStatus []DeviceStatus `xml:",omitempty"`
PlanningRequest []PlanningRequest `xml:",omitempty"`
}
type DeviceInfo struct {
Identification Identification
Characteristics Characteristics
Capabilities Capabilities
}
type Identification struct {
DeviceID string `xml:"DeviceId"`
DeviceName string
DeviceType string
DeviceSerial string
DeviceVendor string
}
type Characteristics struct {
MinPowerConsumption int
MaxPowerConsumption int
MinOnTime int `xml:",omitempty"`
MinOffTime int `xml:",omitempty"`
}
type Capabilities struct {
CurrentPower CurrentPower
Timestamps Timestamps
Interruptions Interruptions
Requests Requests
}
const (
MethodMeasurement = "Measurement"
MethodEstimation = "Estimation"
)
type CurrentPower struct {
Method string
}
type Timestamps struct {
AbsoluteTimestamps bool
}
type Interruptions struct {
InterruptionsAllowed bool
}
type Requests struct {
OptionalEnergy bool
}
const (
StatusOn = "On"
StatusOff = "Off"
)
type DeviceStatus struct {
DeviceID string `xml:"DeviceId"`
EMSignalsAccepted bool
Status string
PowerConsumption PowerConsumption
}
type PowerConsumption struct {
PowerInfo PowerInfo
}
type PowerInfo struct {
AveragePower int
Timestamp int
AveragingInterval int
}
type PlanningRequest struct {
Timeframe Timeframe
}
type Timeframe struct {
DeviceID string `xml:"DeviceId"`
EarliestStart int
LatestEnd int
MinRunningTime int
MaxRunningTime int `xml:",omitempty"`
}
// EM2Device is the EM to device message
type EM2Device struct {
Xmlns string `xml:"xmlns,attr"`
DeviceControl []DeviceControl `xml:",omitempty"`
PlanningRequest []PlanningRequest `xml:",omitempty"`
}
type DeviceControl struct {
DeviceID string `xml:"DeviceId"`
On bool
Timestamp int
}
// Device2EMMsg is the XML message container
func Device2EMMsg() Device2EM {
msg := Device2EM{
Xmlns: "http://www.sma.de/communication/schema/SEMP/v1",
DeviceInfo: make([]DeviceInfo, 0),
DeviceStatus: make([]DeviceStatus, 0),
}
return msg
}

470
hems/semp/semp.go Normal file
View file

@ -0,0 +1,470 @@
package semp
import (
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/core"
"github.com/andig/evcc/server"
"github.com/andig/evcc/util"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/koron/go-ssdp"
)
const (
sempBaseUrlEnv = "SEMP_BASE_URL"
sempGateway = "urn:schemas-simple-energy-management-protocol:device:Gateway:1"
sempLocalDevice = "F-28081973-%s-%.02d"
sempSerialNumber = "%s-%d"
sempCharger = "EVCharger"
basePath = "/semp"
maxAge = 1800
)
var (
serverName = "EVCC SEMP Server " + server.Version
)
// SEMP is the SMA SEMP server
type SEMP struct {
log *util.Logger
cache *util.Cache
closeC chan struct{}
doneC chan struct{}
uid string
hostURI string
port int
site site
}
// site is the minimal interface for accessing site methods
type site interface {
Configuration() core.SiteConfiguration
LoadPoints() []*core.LoadPoint
}
// New generates SEMP Gateway listening at /semp endpoint
func New(site site, cache *util.Cache, httpd *server.HTTPd) (*SEMP, error) {
uid, err := uuid.NewUUID()
if err != nil {
return nil, err
}
s := &SEMP{
doneC: make(chan struct{}),
log: util.NewLogger("semp"),
cache: cache,
site: site,
uid: uid.String(),
}
// find external port
_, port, err := net.SplitHostPort(httpd.Addr)
if err == nil {
s.port, err = strconv.Atoi(port)
}
s.hostURI = s.callbackURI()
s.handlers(httpd.Router)
return s, err
}
func (s *SEMP) advertise(st, usn string) *ssdp.Advertiser {
descriptor := s.hostURI + basePath + "/description.xml"
ad, err := ssdp.Advertise(st, usn, descriptor, serverName, maxAge)
if err != nil {
s.log.ERROR.Println(err)
}
return ad
}
// Run executes the SEMP runtime
func (s *SEMP) Run() {
if s.closeC != nil {
panic("already running")
}
s.closeC = make(chan struct{})
uid := "uuid:" + s.uid
ads := []*ssdp.Advertiser{
s.advertise(ssdp.RootDevice, uid+"::"+ssdp.RootDevice),
s.advertise(uid, uid),
s.advertise(sempGateway, uid+"::"+sempGateway),
}
ticker := time.NewTicker(maxAge * time.Second / 2)
ANNOUNCE:
for {
select {
case <-ticker.C:
for _, ad := range ads {
if err := ad.Alive(); err != nil {
s.log.ERROR.Println(err)
}
}
case <-s.closeC:
break ANNOUNCE
}
}
for _, ad := range ads {
if err := ad.Bye(); err != nil {
s.log.ERROR.Println(err)
}
}
close(s.doneC)
}
// Stop stops the SEMP runtime
func (s *SEMP) Stop() {
if s.closeC == nil {
panic("not running")
}
close(s.closeC)
}
// Done returns the done channel. The channel is closed after byebye has been sent.
func (s *SEMP) Done() chan struct{} {
return s.doneC
}
func (s *SEMP) callbackURI() string {
if uri := os.Getenv(sempBaseUrlEnv); uri != "" {
return strings.TrimSuffix(uri, "/")
}
ip := "localhost"
ips := LocalIPs()
if len(ips) > 0 {
ip = ips[0].String()
} else {
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)
return uri
}
func (s *SEMP) handlers(router *mux.Router) {
sempRouter := router.PathPrefix(basePath).Subrouter()
getRouter := sempRouter.Methods(http.MethodGet).Subrouter()
// get description / root / info / status
getRouter.HandleFunc("/description.xml", s.gatewayDescription)
getRouter.HandleFunc("/", s.deviceRootHandler)
getRouter.HandleFunc("/DeviceInfo", s.deviceInfoQuery)
getRouter.HandleFunc("/DeviceStatus", s.deviceStatusQuery)
getRouter.HandleFunc("/PlanningRequest", s.devicePlanningQuery)
// post control messages
postRouter := sempRouter.Methods(http.MethodPost).Subrouter()
postRouter.HandleFunc("/", s.deviceControlHandler)
}
func (s *SEMP) writeXML(w http.ResponseWriter, msg interface{}) {
b, err := xml.MarshalIndent(msg, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(xml.Header))
_, _ = w.Write(b)
}
func (s *SEMP) gatewayDescription(w http.ResponseWriter, r *http.Request) {
uid := "uuid:" + s.uid
msg := DeviceDescription{
Xmlns: urnUPNPDevice,
SpecVersion: SpecVersion{Major: 1},
Device: Device{
DeviceType: sempGateway,
FriendlyName: "evcc",
Manufacturer: "github.com/andig/evcc",
ModelName: serverName,
PresentationURL: s.hostURI,
UDN: uid,
SEMPService: SEMPService{
Xmlns: urnSEMPService,
Server: s.hostURI,
BasePath: basePath,
Transport: "HTTP/Pull",
ExchangeFormat: "XML",
WsVersion: "1.1.0",
},
},
}
s.writeXML(w, msg)
}
func (s *SEMP) deviceRootHandler(w http.ResponseWriter, r *http.Request) {
msg := Device2EMMsg()
msg.DeviceInfo = append(msg.DeviceInfo, s.allDeviceInfo()...)
msg.DeviceStatus = append(msg.DeviceStatus, s.allDeviceStatus()...)
s.writeXML(w, msg)
}
// deviceInfoQuery answers /semp/DeviceInfo
func (s *SEMP) deviceInfoQuery(w http.ResponseWriter, r *http.Request) {
msg := Device2EMMsg()
did := r.URL.Query().Get("DeviceId")
if did == "" {
msg.DeviceInfo = append(msg.DeviceInfo, s.allDeviceInfo()...)
} else {
for id, lp := range s.site.LoadPoints() {
if did != s.deviceID(id) {
continue
}
msg.DeviceInfo = append(msg.DeviceInfo, s.deviceInfo(id, lp))
}
if len(msg.DeviceInfo) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
}
s.writeXML(w, msg)
}
// deviceStatusQuery answers /semp/DeviceStatus
func (s *SEMP) deviceStatusQuery(w http.ResponseWriter, r *http.Request) {
msg := Device2EMMsg()
did := r.URL.Query().Get("DeviceId")
if did == "" {
msg.DeviceStatus = append(msg.DeviceStatus, s.allDeviceStatus()...)
} else {
for id, lp := range s.site.LoadPoints() {
if did != s.deviceID(id) {
continue
}
msg.DeviceStatus = append(msg.DeviceStatus, s.deviceStatus(id, lp))
}
if len(msg.DeviceStatus) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
}
s.writeXML(w, msg)
}
// devicePlanningQuery answers /semp/PlanningRequest
func (s *SEMP) devicePlanningQuery(w http.ResponseWriter, r *http.Request) {
msg := Device2EMMsg()
did := r.URL.Query().Get("DeviceId")
if did == "" {
msg.PlanningRequest = append(msg.PlanningRequest, s.allPlanningRequest()...)
} else {
for id, lp := range s.site.LoadPoints() {
if did != s.deviceID(id) {
continue
}
if pr := s.planningRequest(id, lp); pr.Timeframe.DeviceID != "" {
msg.PlanningRequest = append(msg.PlanningRequest, pr)
}
}
}
s.writeXML(w, msg)
}
func (s *SEMP) serialNumber() string {
uidParts := strings.SplitN(s.uid, "-", 5)
return uidParts[len(uidParts)-1]
}
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.LoadPoint) DeviceInfo {
method := MethodEstimation
if lp.HasChargeMeter() {
method = MethodMeasurement
}
res := DeviceInfo{
Identification: Identification{
DeviceID: s.deviceID(id),
DeviceName: lp.Name(),
DeviceType: sempCharger,
DeviceSerial: fmt.Sprintf(sempSerialNumber, s.serialNumber(), id),
DeviceVendor: "github.com/andig/evcc",
},
Capabilities: Capabilities{
CurrentPower: CurrentPower{
Method: method,
},
Interruptions: Interruptions{
InterruptionsAllowed: true,
},
Requests: Requests{
OptionalEnergy: true,
},
},
Characteristics: Characteristics{
MinPowerConsumption: 230 * int(lp.MinCurrent),
MaxPowerConsumption: 230 * int(lp.Phases*lp.MaxCurrent),
},
}
return res
}
func (s *SEMP) allDeviceInfo() (res []DeviceInfo) {
for id, lp := range s.site.LoadPoints() {
res = append(res, s.deviceInfo(id, lp))
}
return res
}
func (s *SEMP) deviceStatus(id int, lp *core.LoadPoint) DeviceStatus {
var chargePower float64
if chargePowerP, err := s.cacheGet(id, "chargePower"); err == nil {
chargePower = chargePowerP.Val.(float64)
}
status := StatusOff
if statusP, err := s.cacheGet(id, "charging"); err == nil {
if statusP.Val.(bool) {
status = StatusOn
}
}
res := DeviceStatus{
DeviceID: s.deviceID(id),
EMSignalsAccepted: true,
PowerConsumption: PowerConsumption{
PowerInfo: PowerInfo{
AveragePower: int(chargePower),
AveragingInterval: 60,
},
},
Status: status,
}
return res
}
func (s *SEMP) allDeviceStatus() (res []DeviceStatus) {
for id, lp := range s.site.LoadPoints() {
res = append(res, s.deviceStatus(id, lp))
}
return res
}
func (s *SEMP) planningRequest(id int, lp *core.LoadPoint) (res PlanningRequest) {
mode := api.ModeOff
if modeP, err := s.cacheGet(id, "mode"); err == nil {
mode = api.ChargeMode(modeP.Val.(string))
}
var charging bool
if chargingP, err := s.cacheGet(id, "charging"); err == nil {
charging = chargingP.Val.(bool)
}
chargeEstimate := time.Duration(-1)
if chargeEstimateP, err := s.cacheGet(id, "chargeEstimate"); err == nil {
chargeEstimate = chargeEstimateP.Val.(time.Duration)
}
maxDuration := int(chargeEstimate / time.Second)
if chargeEstimate <= 0 {
maxDuration = 10 * 60 // 10min
}
minDuration := maxDuration
if mode == api.ModePV {
minDuration = 0
}
latestEnd := maxDuration
if mode == api.ModePV {
latestEnd = 2 * maxDuration
}
if charging {
res = PlanningRequest{
Timeframe: Timeframe{
DeviceID: s.deviceID(id),
EarliestStart: 0,
LatestEnd: latestEnd,
MinRunningTime: minDuration,
MaxRunningTime: maxDuration,
},
}
}
return res
}
func (s *SEMP) allPlanningRequest() (res []PlanningRequest) {
for id, lp := range s.site.LoadPoints() {
if pr := s.planningRequest(id, lp); pr.Timeframe.DeviceID != "" {
res = append(res, pr)
}
}
return res
}
func (s *SEMP) deviceControlHandler(w http.ResponseWriter, r *http.Request) {
var msg EM2Device
body, err := ioutil.ReadAll(r.Body)
if err == nil {
defer r.Body.Close()
err = xml.Unmarshal(body, &msg)
}
s.log.TRACE.Printf("recv: %+v", msg)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}

View file

@ -232,8 +232,14 @@ func applyRouteHandler(router *mux.Router, r route, handler http.HandlerFunc) {
router.Methods(r.Methods...).Path(r.Pattern).Handler(handler)
}
// HTTPd wraps an http.Server and adds the root router
type HTTPd struct {
*http.Server
*mux.Router
}
// NewHTTPd creates HTTP server with configured routes for loadpoint
func NewHTTPd(url string, links []MenuConfig, site site, hub *SocketHub, cache *util.Cache) *http.Server {
func NewHTTPd(url string, links []MenuConfig, site site, hub *SocketHub, cache *util.Cache) *HTTPd {
var routes = map[string]route{
"health": {[]string{"GET"}, "/health", HealthHandler()},
"config": {[]string{"GET"}, "/config", ConfigHandler(site)},
@ -284,13 +290,16 @@ func NewHTTPd(url string, links []MenuConfig, site site, hub *SocketHub, cache *
applyRouteHandler(subAPI, routes["settargetsoc"], TargetSoCHandler(lp))
}
srv := &http.Server{
Addr: url,
Handler: router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ErrorLog: log.ERROR,
srv := &HTTPd{
Server: &http.Server{
Addr: url,
Handler: router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ErrorLog: log.ERROR,
},
Router: router,
}
srv.SetKeepAlivesEnabled(true)