Add Homematic pluggable switch (#3758)
This commit is contained in:
parent
fe732d806f
commit
5332148fc1
9 changed files with 462 additions and 0 deletions
115
charger/homematic.go
Normal file
115
charger/homematic.go
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
66
meter/homematic.go
Normal file
66
meter/homematic.go
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
137
meter/homematic/connection.go
Normal file
137
meter/homematic/connection.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
27
meter/homematic/types.go
Normal file
27
meter/homematic/types.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
36
templates/definition/charger/homematic.yaml
Normal file
36
templates/definition/charger/homematic.yaml
Normal file
|
|
@ -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 }}
|
||||||
35
templates/definition/meter/homematic.yaml
Normal file
35
templates/definition/meter/homematic.yaml
Normal file
|
|
@ -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 }}
|
||||||
12
templates/docs/charger/homematic_0.yaml
Normal file
12
templates/docs/charger/homematic_0.yaml
Normal file
|
|
@ -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
|
||||||
31
templates/docs/meter/homematic_0.yaml
Normal file
31
templates/docs/meter/homematic_0.yaml
Normal file
|
|
@ -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
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
],
|
],
|
||||||
"SmartPlugs": [
|
"SmartPlugs": [
|
||||||
"AVM",
|
"AVM",
|
||||||
|
"Homematic IP",
|
||||||
"Shelly",
|
"Shelly",
|
||||||
"Tasmota",
|
"Tasmota",
|
||||||
"TP-Link"
|
"TP-Link"
|
||||||
|
|
@ -56,6 +57,7 @@
|
||||||
"Eastron",
|
"Eastron",
|
||||||
"FENECON",
|
"FENECON",
|
||||||
"Fronius",
|
"Fronius",
|
||||||
|
"Homematic IP",
|
||||||
"Huawei",
|
"Huawei",
|
||||||
"Janitza",
|
"Janitza",
|
||||||
"Kostal",
|
"Kostal",
|
||||||
|
|
@ -85,6 +87,7 @@
|
||||||
"Eastron",
|
"Eastron",
|
||||||
"FENECON",
|
"FENECON",
|
||||||
"Fronius",
|
"Fronius",
|
||||||
|
"Homematic IP",
|
||||||
"Huawei",
|
"Huawei",
|
||||||
"Janitza",
|
"Janitza",
|
||||||
"Kostal",
|
"Kostal",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue