forked from evcc-io/evcc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Homematic pluggable switch (evcc-io#3758)
- Loading branch information
Showing
9 changed files
with
462 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }} |
Oops, something went wrong.