From 5332148fc11f609f1ce7d6bc9e6217f8e9884c8d Mon Sep 17 00:00:00 2001 From: Markus Thierolf <77847348+thierolm@users.noreply.github.com> Date: Thu, 7 Jul 2022 22:56:18 +0200 Subject: [PATCH] Add Homematic pluggable switch (#3758) --- charger/homematic.go | 115 ++++++++++++++++ meter/homematic.go | 66 ++++++++++ meter/homematic/connection.go | 137 ++++++++++++++++++++ meter/homematic/types.go | 27 ++++ templates/definition/charger/homematic.yaml | 36 +++++ templates/definition/meter/homematic.yaml | 35 +++++ templates/docs/charger/homematic_0.yaml | 12 ++ templates/docs/meter/homematic_0.yaml | 31 +++++ templates/evcc.io/brands.json | 3 + 9 files changed, 462 insertions(+) create mode 100644 charger/homematic.go create mode 100644 meter/homematic.go create mode 100644 meter/homematic/connection.go create mode 100644 meter/homematic/types.go create mode 100644 templates/definition/charger/homematic.yaml create mode 100644 templates/definition/meter/homematic.yaml create mode 100644 templates/docs/charger/homematic_0.yaml create mode 100644 templates/docs/meter/homematic_0.yaml diff --git a/charger/homematic.go b/charger/homematic.go new file mode 100644 index 0000000000..a2390e943a --- /dev/null +++ b/charger/homematic.go @@ -0,0 +1,115 @@ +package charger + +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/homematic" + "github.com/evcc-io/evcc/util" +) + +// Homematic CCU charger implementation +type CCU struct { + conn *homematic.Connection + standbypower float64 +} + +func init() { + registry.Add("homematic", NewCCUFromConfig) +} + +// NewCCUFromConfig creates a Homematic charger from generic config +func NewCCUFromConfig(other map[string]interface{}) (api.Charger, error) { + cc := struct { + URI string + Device string + MeterChannel string + SwitchChannel string + User string + Password string + StandbyPower float64 + }{} + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewCCU(cc.URI, cc.Device, cc.MeterChannel, cc.SwitchChannel, cc.User, cc.Password, cc.StandbyPower) +} + +// NewCCU creates a new connection with standbypower for charger +func NewCCU(uri, deviceid, meterid, switchid, user, password string, standbypower float64) (*CCU, error) { + conn, err := homematic.NewConnection(uri, deviceid, meterid, switchid, user, password) + + wb := &CCU{ + conn: conn, + standbypower: standbypower, + } + + return wb, err +} + +// Enabled implements the api.Charger interface +func (c *CCU) Enabled() (bool, error) { + return c.conn.Enabled() +} + +// Enable implements the api.Charger interface +func (c *CCU) Enable(enable bool) error { + return c.conn.Enable(enable) +} + +// MaxCurrent implements the api.Charger interface +func (c *CCU) MaxCurrent(current int64) error { + return nil +} + +// Status implements the api.Charger interface +func (c *CCU) Status() (api.ChargeStatus, error) { + res := api.StatusB + on, err := c.Enabled() + if err != nil { + return res, err + } + + power, err := c.conn.CurrentPower() + if err != nil { + return res, err + } + + // static mode || standby power mode condition + if on && (c.standbypower < 0 || power > c.standbypower) { + res = api.StatusC + } + + return res, nil +} + +var _ api.Meter = (*CCU)(nil) + +// CurrentPower implements the api.Meter interface +func (c *CCU) CurrentPower() (float64, error) { + var power float64 + + // set fix static power in static mode + if c.standbypower < 0 { + on, err := c.Enabled() + if on { + power = -c.standbypower + } + return power, err + } + + // ignore power in standby mode + power, err := c.conn.CurrentPower() + if power <= c.standbypower { + power = 0 + } + + return power, err +} + +var _ api.MeterEnergy = (*CCU)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (c *CCU) TotalEnergy() (float64, error) { + return c.conn.TotalEnergy() +} diff --git a/meter/homematic.go b/meter/homematic.go new file mode 100644 index 0000000000..19bb849367 --- /dev/null +++ b/meter/homematic.go @@ -0,0 +1,66 @@ +package meter + +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/homematic" + "github.com/evcc-io/evcc/util" +) + +// Homematic CCU meter implementation +type CCU struct { + conn *homematic.Connection + usage string +} + +func init() { + registry.Add("homematic", NewCCUFromConfig) +} + +// NewCCUFromConfig creates a Homematic meter from generic config +func NewCCUFromConfig(other map[string]interface{}) (api.Meter, error) { + cc := struct { + URI string + Device string + MeterChannel string + SwitchChannel string + User string + Password string + Usage string + }{} + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewCCU(cc.URI, cc.Device, cc.MeterChannel, cc.SwitchChannel, cc.User, cc.Password, cc.Usage) +} + +// NewCCU creates a new connection with usage for meter +func NewCCU(uri, deviceid, meterid, switchid, user, password, usage string) (*CCU, error) { + conn, err := homematic.NewConnection(uri, deviceid, meterid, switchid, user, password) + + m := &CCU{ + conn: conn, + usage: usage, + } + + return m, err +} + +// CurrentPower implements the api.Meter interface +func (c *CCU) CurrentPower() (float64, error) { + if c.usage == "grid" { + return c.conn.GridCurrentPower() + } + return c.conn.CurrentPower() +} + +var _ api.MeterEnergy = (*CCU)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (c *CCU) TotalEnergy() (float64, error) { + if c.usage == "grid" { + return c.conn.GridTotalEnergy() + } + return c.conn.TotalEnergy() +} diff --git a/meter/homematic/connection.go b/meter/homematic/connection.go new file mode 100644 index 0000000000..d62a1e0205 --- /dev/null +++ b/meter/homematic/connection.go @@ -0,0 +1,137 @@ +package homematic + +import ( + "encoding/xml" + "fmt" + "net/http" + "strings" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" +) + +// Homematic plugable switchchannel and meterchannel charger based on CCU XML-RPC interface +// https://homematic-ip.com/sites/default/files/downloads/HM_XmlRpc_API.pdf +// https://homematic-ip.com/sites/default/files/downloads/HMIP_XmlRpc_API_Addendum.pdf + +// Homematic CCU settings +type Settings struct { + URI, Device, MeterChannel, SwitchChannel, User, Password string +} + +// Connection is the Homematic CCU connection +type Connection struct { + log *util.Logger + *request.Helper + *Settings +} + +// NewConnection creates a new Homematic device connection. +func NewConnection(uri, device, meterchannel, switchchannel, user, password string) (*Connection, error) { + log := util.NewLogger("homematic") + + settings := &Settings{ + URI: util.DefaultScheme(uri, "http"), + Device: device, + MeterChannel: meterchannel, + SwitchChannel: switchchannel, + } + + conn := &Connection{ + log: log, + Helper: request.NewHelper(log), + Settings: settings, + } + + conn.Client.Transport = request.NewTripper(log, transport.Insecure()) + + if user != "" { + log.Redact(transport.BasicAuthHeader(user, password)) + conn.Client.Transport = transport.BasicAuth(user, password, conn.Client.Transport) + } + + return conn, nil +} + +func (c *Connection) XmlCmd(method, channel string, values ...Param) (MethodResponse, error) { + target := fmt.Sprintf("%s:%s", c.Device, channel) + hmc := MethodCall{ + XMLName: xml.Name{}, + MethodName: method, + Params: append([]Param{{CCUString: target}}, values...), + } + + var hmr MethodResponse + body, err := xml.Marshal(hmc) + if err != nil { + return hmr, err + } + + headers := map[string]string{ + "Content-Type": "text/xml", + } + + if req, err := request.New(http.MethodPost, c.URI, strings.NewReader(xml.Header+string(body)), headers); err == nil { + if res, err := c.DoBody(req); err == nil { + if strings.Contains(string(res), "faultCode") { + return hmr, fmt.Errorf("ccu: %s", string(res)) + } + + // correct Homematic IP Legacy API (CCU port 2010) method response encoding value + res = []byte(strings.Replace(string(res), "ISO-8859-1", "UTF-8", 1)) + + // correct XML-RPC-Schnittstelle (CCU port 2001) method response encoding value + res = []byte(strings.Replace(string(res), "iso-8859-1", "UTF-8", 1)) + + if err := xml.Unmarshal(res, &hmr); err != nil { + return hmr, err + } + } + } + + return hmr, err +} + +// Enabled reads the homematic HMIP-PSM switchchannel state true=on/false=off +func (c *Connection) Enabled() (bool, error) { + res, err := c.XmlCmd("getValue", c.SwitchChannel, Param{CCUString: "STATE"}) + return res.Value.CCUBool == "1", err +} + +// Enable sets the homematic HMIP-PSM switchchannel state to true=on/false=off +func (c *Connection) Enable(enable bool) error { + onoff := map[bool]string{true: "1", false: "0"} + _, err := c.XmlCmd("setValue", c.SwitchChannel, Param{CCUString: "STATE"}, Param{CCUBool: onoff[enable]}) + return err +} + +// CurrentPower reads the homematic HMIP-PSM meterchannel power in W +func (c *Connection) CurrentPower() (float64, error) { + res, err := c.XmlCmd("getValue", c.MeterChannel, Param{CCUString: "POWER"}) + return res.Value.CCUFloat, err +} + +// TotalEnergy reads the homematic HMIP-PSM meterchannel energy in Wh +func (c *Connection) TotalEnergy() (float64, error) { + res, err := c.XmlCmd("getValue", c.MeterChannel, Param{CCUString: "ENERGY_COUNTER"}) + return res.Value.CCUFloat / 1000, err +} + +// Currents reads the homematic HMIP-PSM meterchannel L1 current in A +func (c *Connection) Currents() (float64, float64, float64, error) { + res, err := c.XmlCmd("getValue", c.MeterChannel, Param{CCUString: "CURRENT"}) + return res.Value.CCUFloat / 1000, 0, 0, err +} + +// GridCurrentPower reads the homematic HM-ES-TX-WM grid meterchannel power in W +func (c *Connection) GridCurrentPower() (float64, error) { + res, err := c.XmlCmd("getValue", c.MeterChannel, Param{CCUString: "IEC_POWER"}) + return res.Value.CCUFloat, err +} + +// GridTotalEnergy reads the homematic HM-ES-TX-WM grid meterchannel energy in Wh +func (c *Connection) GridTotalEnergy() (float64, error) { + res, err := c.XmlCmd("getValue", c.MeterChannel, Param{CCUString: "IEC_ENERGY_COUNTER"}) + return res.Value.CCUFloat, err +} diff --git a/meter/homematic/types.go b/meter/homematic/types.go new file mode 100644 index 0000000000..f9ed85b508 --- /dev/null +++ b/meter/homematic/types.go @@ -0,0 +1,27 @@ +package homematic + +import ( + "encoding/xml" +) + +// Homematic CCU XML-RPC types +// https://homematic-ip.com/sites/default/files/downloads/HM_XmlRpc_API.pdf +// https://homematic-ip.com/sites/default/files/downloads/HMIP_XmlRpc_API_Addendum.pdf + +type Param struct { + CCUBool string `xml:"value>boolean,omitempty"` + CCUFloat float64 `xml:"value>double,omitempty"` + CCUInt int64 `xml:"value>i4,omitempty"` + CCUString string `xml:"value>string,omitempty"` +} + +type MethodCall struct { + XMLName xml.Name `xml:"methodCall"` + MethodName string `xml:"methodName"` + Params []Param `xml:"params>param,omitempty"` +} + +type MethodResponse struct { + XMLName xml.Name `xml:"methodResponse"` + Value Param `xml:"params>param,omitempty"` +} diff --git a/templates/definition/charger/homematic.yaml b/templates/definition/charger/homematic.yaml new file mode 100644 index 0000000000..e68392957f --- /dev/null +++ b/templates/definition/charger/homematic.yaml @@ -0,0 +1,36 @@ +template: homematic +products: + - brand: Homematic IP +group: switchsockets +params: + - name: host + - name: device + description: + de: Geräteadresse/Seriennummer + en: Device address/Serial number + required: true + mask: false + example: "0001EE89AAD848" + help: + en: Homematic device id like shown in the CCU web user interface. + de: Homematic Geräte Id, wie im CCU Webfrontend angezeigt. + - name: standbypower + default: 15 + - name: user + required: false + - name: password + required: false + mask: true +render: | + type: homematic + uri: {{ .host }}:2010 + device: {{ .device }} + meterchannel: 6 + switchchannel: 3 + standbypower: {{ .standbypower }} + {{ if ne .user "" }} + user: {{ .user }} + {{ end }} + {{ if ne .password "" }} + password: {{ .password }} + {{ end }} diff --git a/templates/definition/meter/homematic.yaml b/templates/definition/meter/homematic.yaml new file mode 100644 index 0000000000..50647cdd4c --- /dev/null +++ b/templates/definition/meter/homematic.yaml @@ -0,0 +1,35 @@ +template: homematic +products: + - brand: Homematic IP +group: switchsockets +params: + - name: usage + choice: ["grid", "pv", "charge"] + - name: host + - name: device + description: + de: Geräteadresse/Seriennummer + en: Device address/Serial number + required: true + mask: false + example: "0001EE89AAD848" + help: + en: Homematic device id like shown in the CCU web user interface. + de: Homematic Geräte Id, wie im CCU Webfrontend angezeigt. + - name: user + required: false + - name: password + required: false + mask: true +render: | + type: homematic + usage: {{ .usage }} + uri: {{ .host }}:{{- if (eq .usage "grid")}}2001{{ else }}2010{{end}} + device: {{ .device }} + meterchannel: {{ if (eq .usage "grid")}}1{{ else }}6{{end}} + {{ if ne .user "" }} + user: {{ .user }} + {{ end }} + {{ if ne .password "" }} + password: {{ .password }} + {{ end }} diff --git a/templates/docs/charger/homematic_0.yaml b/templates/docs/charger/homematic_0.yaml new file mode 100644 index 0000000000..b90a5cdbb1 --- /dev/null +++ b/templates/docs/charger/homematic_0.yaml @@ -0,0 +1,12 @@ +product: + brand: Homematic IP + group: Schaltbare Steckdosen +render: + - default: | + type: template + template: homematic + host: 192.0.2.2 # IP-Adresse oder Hostname + device: 0001EE89AAD848 # Homematic Geräte Id, wie im CCU Webfrontend angezeigt. + standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional + user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional + password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional diff --git a/templates/docs/meter/homematic_0.yaml b/templates/docs/meter/homematic_0.yaml new file mode 100644 index 0000000000..651dbbc433 --- /dev/null +++ b/templates/docs/meter/homematic_0.yaml @@ -0,0 +1,31 @@ +product: + brand: Homematic IP + group: Schaltbare Steckdosen +render: + - usage: grid + default: | + type: template + template: homematic + usage: grid + host: 192.0.2.2 # IP-Adresse oder Hostname + device: 0001EE89AAD848 # Homematic Geräte Id, wie im CCU Webfrontend angezeigt. + user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional + password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional + - usage: pv + default: | + type: template + template: homematic + usage: pv + host: 192.0.2.2 # IP-Adresse oder Hostname + device: 0001EE89AAD848 # Homematic Geräte Id, wie im CCU Webfrontend angezeigt. + user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional + password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional + - usage: charge + default: | + type: template + template: homematic + usage: charge + host: 192.0.2.2 # IP-Adresse oder Hostname + device: 0001EE89AAD848 # Homematic Geräte Id, wie im CCU Webfrontend angezeigt. + user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional + password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json index 61315365b9..7a09e5664e 100644 --- a/templates/evcc.io/brands.json +++ b/templates/evcc.io/brands.json @@ -43,6 +43,7 @@ ], "SmartPlugs": [ "AVM", + "Homematic IP", "Shelly", "Tasmota", "TP-Link" @@ -56,6 +57,7 @@ "Eastron", "FENECON", "Fronius", + "Homematic IP", "Huawei", "Janitza", "Kostal", @@ -85,6 +87,7 @@ "Eastron", "FENECON", "Fronius", + "Homematic IP", "Huawei", "Janitza", "Kostal",