Standardise Switchsockets (Tapo+TP-Link+Tasmota+Shelly) (#3311)
This commit is contained in:
parent
0bd7ea9429
commit
7fb3661c85
31 changed files with 586 additions and 356 deletions
|
|
@ -1,27 +1,16 @@
|
|||
package charger
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/charger/shelly"
|
||||
"github.com/evcc-io/evcc/meter/shelly"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
"github.com/jpfielding/go-http-digest/pkg/digest"
|
||||
)
|
||||
|
||||
// Shelly charger implementation
|
||||
type Shelly struct {
|
||||
*request.Helper
|
||||
log *util.Logger
|
||||
uri string
|
||||
gen int // Shelly api generation
|
||||
channel int
|
||||
conn *shelly.Connection
|
||||
standbypower float64
|
||||
}
|
||||
|
||||
|
|
@ -43,107 +32,32 @@ func NewShellyFromConfig(other map[string]interface{}) (api.Charger, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if cc.URI == "" {
|
||||
return nil, errors.New("missing uri")
|
||||
}
|
||||
|
||||
return NewShelly(cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower)
|
||||
}
|
||||
|
||||
// NewShelly creates Shelly charger
|
||||
func NewShelly(uri, user, password string, channel int, standbypower float64) (*Shelly, error) {
|
||||
for _, suffix := range []string{"/", "/rcp", "/shelly"} {
|
||||
uri = strings.TrimSuffix(uri, suffix)
|
||||
}
|
||||
|
||||
log := util.NewLogger("shelly")
|
||||
client := request.NewHelper(log)
|
||||
|
||||
// Shelly Gen1 and Gen2 families expose the /shelly endpoint
|
||||
var resp shelly.DeviceInfo
|
||||
if err := client.GetJSON(fmt.Sprintf("%s/shelly", util.DefaultScheme(uri, "http")), &resp); err != nil {
|
||||
conn, err := shelly.NewConnection(uri, user, password, channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Shelly{
|
||||
Helper: client,
|
||||
log: log,
|
||||
channel: channel,
|
||||
shelly := &Shelly{
|
||||
conn: conn,
|
||||
standbypower: standbypower,
|
||||
gen: resp.Gen,
|
||||
}
|
||||
|
||||
c.Client.Transport = request.NewTripper(log, transport.Insecure())
|
||||
|
||||
if (resp.Auth || resp.AuthEn) && (user == "" || password == "") {
|
||||
return c, fmt.Errorf("%s (%s) missing user/password", resp.Model, resp.Mac)
|
||||
}
|
||||
|
||||
switch c.gen {
|
||||
case 0, 1:
|
||||
// Shelly GEN 1 API
|
||||
// https://shelly-api-docs.shelly.cloud/gen1/#shelly-family-overview
|
||||
c.uri = util.DefaultScheme(uri, "http")
|
||||
if user != "" {
|
||||
log.Redact(transport.BasicAuthHeader(user, password))
|
||||
c.Client.Transport = transport.BasicAuth(user, password, c.Client.Transport)
|
||||
}
|
||||
|
||||
if resp.NumMeters == 0 {
|
||||
// Shelly1 force static mode with fake power http://192.168.178.xxx/settings/power/0?power=standbypower+1
|
||||
uri := fmt.Sprintf("%s/settings/power/%d?power=%d", c.uri, c.channel, int(math.Abs(c.standbypower)))
|
||||
if err := c.GetJSON(uri, &resp); err != nil {
|
||||
return c, err
|
||||
}
|
||||
}
|
||||
|
||||
case 2:
|
||||
// Shelly GEN 2 API
|
||||
// https://shelly-api-docs.shelly.cloud/gen2/
|
||||
c.uri = fmt.Sprintf("%s/rpc", util.DefaultScheme(uri, "http"))
|
||||
if user != "" {
|
||||
c.Client.Transport = digest.NewTransport(user, password, c.Client.Transport)
|
||||
}
|
||||
|
||||
default:
|
||||
return c, fmt.Errorf("%s (%s) unknown api generation (%d)", resp.Type, resp.Model, c.gen)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
return shelly, nil
|
||||
}
|
||||
|
||||
// Enabled implements the api.Charger interface
|
||||
func (c *Shelly) Enabled() (bool, error) {
|
||||
switch c.gen {
|
||||
case 0, 1:
|
||||
var resp shelly.Gen1SwitchResponse
|
||||
uri := fmt.Sprintf("%s/relay/%d", c.uri, c.channel)
|
||||
err := c.GetJSON(uri, &resp)
|
||||
return resp.Ison, err
|
||||
|
||||
default:
|
||||
var resp shelly.Gen2SwitchResponse
|
||||
err := c.execGen2Cmd("Switch.GetStatus", false, &resp)
|
||||
return resp.Output, err
|
||||
}
|
||||
return c.conn.Enabled()
|
||||
}
|
||||
|
||||
// Enable implements the api.Charger interface
|
||||
func (c *Shelly) Enable(enable bool) error {
|
||||
var err error
|
||||
onoff := map[bool]string{true: "on", false: "off"}
|
||||
|
||||
switch c.gen {
|
||||
case 0, 1:
|
||||
var resp shelly.Gen1SwitchResponse
|
||||
uri := fmt.Sprintf("%s/relay/%d?turn=%s", c.uri, c.channel, onoff[enable])
|
||||
err = c.GetJSON(uri, &resp)
|
||||
|
||||
default:
|
||||
var resp shelly.Gen2SwitchResponse
|
||||
err = c.execGen2Cmd("Switch.Set", enable, &resp)
|
||||
}
|
||||
|
||||
err := c.conn.Enable(enable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -153,6 +67,7 @@ func (c *Shelly) Enable(enable bool) error {
|
|||
case err != nil:
|
||||
return err
|
||||
case enable != enabled:
|
||||
onoff := map[bool]string{true: "on", false: "off"}
|
||||
return fmt.Errorf("switch %s failed", onoff[enable])
|
||||
default:
|
||||
return nil
|
||||
|
|
@ -192,61 +107,21 @@ var _ api.Meter = (*Shelly)(nil)
|
|||
// CurrentPower implements the api.Meter interface
|
||||
func (c *Shelly) CurrentPower() (float64, error) {
|
||||
var power float64
|
||||
switch c.gen {
|
||||
case 0, 1:
|
||||
var resp shelly.Gen1StatusResponse
|
||||
uri := fmt.Sprintf("%s/status", c.uri)
|
||||
if err := c.GetJSON(uri, &resp); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if c.channel >= len(resp.Meters) {
|
||||
return 0, errors.New("invalid channel, missing power meter")
|
||||
}
|
||||
|
||||
power = resp.Meters[c.channel].Power
|
||||
|
||||
default:
|
||||
var resp shelly.Gen2StatusResponse
|
||||
if err := c.execGen2Cmd("Shelly.GetStatus", false, &resp); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch c.channel {
|
||||
case 1:
|
||||
power = resp.Switch1.Apower
|
||||
case 2:
|
||||
power = resp.Switch2.Apower
|
||||
default:
|
||||
power = resp.Switch0.Apower
|
||||
// set fix static power in static mode
|
||||
if c.standbypower < 0 {
|
||||
on, err := c.Enabled()
|
||||
if on {
|
||||
power = -c.standbypower
|
||||
}
|
||||
return power, err
|
||||
}
|
||||
|
||||
// ignore standby power
|
||||
if power <= c.standbypower {
|
||||
// ignore power in standby mode
|
||||
power, err := c.conn.CurrentPower()
|
||||
if c.standbypower >= 0 && power <= c.standbypower {
|
||||
power = 0
|
||||
}
|
||||
|
||||
return power, nil
|
||||
}
|
||||
|
||||
// execGen2Cmd executes a shelly api gen1/gen2 command and provides the response
|
||||
func (c *Shelly) execGen2Cmd(method string, enable bool, res interface{}) error {
|
||||
// Shelly gen 2 rfc7616 authentication
|
||||
// https://shelly-api-docs.shelly.cloud/gen2/Overview/CommonDeviceTraits#authentication
|
||||
// https://datatracker.ietf.org/doc/html/rfc7616
|
||||
|
||||
data := &shelly.Gen2RpcPost{
|
||||
Id: c.channel,
|
||||
On: enable,
|
||||
Src: "evcc",
|
||||
Method: method,
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodPost, fmt.Sprintf("%s/%s", c.uri, method), request.MarshalJSON(data), request.JSONEncoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.DoJSON(req, &res)
|
||||
return power, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
package charger
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/meter/tapo"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
|
|
@ -33,19 +29,11 @@ func NewTapoFromConfig(other map[string]interface{}) (api.Charger, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if cc.URI == "" {
|
||||
return nil, errors.New("missing uri")
|
||||
}
|
||||
|
||||
return NewTapo(cc.URI, cc.User, cc.Password, cc.StandbyPower)
|
||||
}
|
||||
|
||||
// NewTapo creates Tapo charger
|
||||
func NewTapo(uri, user, password string, standbypower float64) (*Tapo, error) {
|
||||
for _, suffix := range []string{"/", "/app"} {
|
||||
uri = strings.TrimSuffix(uri, suffix)
|
||||
}
|
||||
|
||||
conn, err := tapo.NewConnection(uri, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -56,10 +44,6 @@ func NewTapo(uri, user, password string, standbypower float64) (*Tapo, error) {
|
|||
standbypower: standbypower,
|
||||
}
|
||||
|
||||
if user == "" || password == "" {
|
||||
return tapo, fmt.Errorf("missing user or password")
|
||||
}
|
||||
|
||||
return tapo, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,15 +2,10 @@ package charger
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/charger/tasmota"
|
||||
"github.com/evcc-io/evcc/meter/tasmota"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
)
|
||||
|
||||
// Tasmota project homepage
|
||||
|
|
@ -20,9 +15,8 @@ import (
|
|||
|
||||
// Tasmota charger implementation
|
||||
type Tasmota struct {
|
||||
*request.Helper
|
||||
uri, user, password string
|
||||
standbypower float64
|
||||
conn *tasmota.Connection
|
||||
standbypower float64
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -41,25 +35,20 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if cc.URI == "" {
|
||||
return nil, errors.New("missing uri")
|
||||
}
|
||||
|
||||
return NewTasmota(cc.URI, cc.User, cc.Password, cc.StandbyPower)
|
||||
}
|
||||
|
||||
// NewTasmota creates Tasmota charger
|
||||
func NewTasmota(uri, user, password string, standbypower float64) (*Tasmota, error) {
|
||||
log := util.NewLogger("tasmota")
|
||||
c := &Tasmota{
|
||||
Helper: request.NewHelper(log),
|
||||
uri: util.DefaultScheme(strings.TrimRight(uri, "/"), "http"),
|
||||
user: user,
|
||||
password: password,
|
||||
standbypower: standbypower,
|
||||
conn, err := tasmota.NewConnection(uri, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Client.Transport = request.NewTripper(log, transport.Insecure())
|
||||
c := &Tasmota{
|
||||
conn: conn,
|
||||
standbypower: standbypower,
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
|
@ -67,7 +56,7 @@ func NewTasmota(uri, user, password string, standbypower float64) (*Tasmota, err
|
|||
// Enabled implements the api.Charger interface
|
||||
func (c *Tasmota) Enabled() (bool, error) {
|
||||
var res tasmota.StatusResponse
|
||||
err := c.GetJSON(c.cmdUri("Status 0"), &res)
|
||||
err := c.conn.ExecCmd("Status 0", &res)
|
||||
|
||||
return res.Status.Power == 1, err
|
||||
}
|
||||
|
|
@ -79,7 +68,7 @@ func (c *Tasmota) Enable(enable bool) error {
|
|||
if enable {
|
||||
cmd = "Power on"
|
||||
}
|
||||
err := c.GetJSON(c.cmdUri(cmd), &res)
|
||||
err := c.conn.ExecCmd(cmd, &res)
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
|
|
@ -127,7 +116,7 @@ var _ api.Meter = (*Tasmota)(nil)
|
|||
func (c *Tasmota) CurrentPower() (float64, error) {
|
||||
var power float64
|
||||
|
||||
// static mode
|
||||
// set fix static power in static mode
|
||||
if c.standbypower < 0 {
|
||||
on, err := c.Enabled()
|
||||
if on {
|
||||
|
|
@ -136,13 +125,9 @@ func (c *Tasmota) CurrentPower() (float64, error) {
|
|||
return power, err
|
||||
}
|
||||
|
||||
// standby power mode
|
||||
var res tasmota.StatusSNSResponse
|
||||
err := c.GetJSON(c.cmdUri("Status 8"), &res)
|
||||
|
||||
// ignore standby power
|
||||
power = float64(res.StatusSNS.Energy.Power)
|
||||
if power < c.standbypower {
|
||||
// ignore power in standby mode
|
||||
power, err := c.conn.CurrentPower()
|
||||
if c.standbypower >= 0 && power <= c.standbypower {
|
||||
power = 0
|
||||
}
|
||||
|
||||
|
|
@ -153,20 +138,5 @@ var _ api.MeterEnergy = (*Tasmota)(nil)
|
|||
|
||||
// TotalEnergy implements the api.MeterEnergy interface
|
||||
func (c *Tasmota) TotalEnergy() (float64, error) {
|
||||
var resp tasmota.StatusSNSResponse
|
||||
err := c.GetJSON(c.cmdUri("Status 8"), &resp)
|
||||
|
||||
return resp.StatusSNS.Energy.Total, err
|
||||
}
|
||||
|
||||
// cmdUri creates the Tasmota command web request
|
||||
// https://tasmota.github.io/docs/Commands/#with-web-requests
|
||||
func (c *Tasmota) cmdUri(cmd string) string {
|
||||
parameters := url.Values{
|
||||
"user": []string{c.user},
|
||||
"password": []string{c.password},
|
||||
"cmnd": []string{cmd},
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode())
|
||||
return c.conn.TotalEnergy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,18 @@
|
|||
package charger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/charger/tplink"
|
||||
"github.com/evcc-io/evcc/meter/tplink"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
// TPLink charger implementation
|
||||
type TPLink struct {
|
||||
log *util.Logger
|
||||
uri string
|
||||
conn *tplink.Connection
|
||||
standbypower float64
|
||||
}
|
||||
|
||||
|
|
@ -46,9 +40,13 @@ func NewTPLinkFromConfig(other map[string]interface{}) (api.Charger, error) {
|
|||
|
||||
// NewTPLink creates TP-Link charger
|
||||
func NewTPLink(uri string, standbypower float64) (*TPLink, error) {
|
||||
conn, err := tplink.NewConnection(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &TPLink{
|
||||
log: util.NewLogger("tplink"),
|
||||
uri: net.JoinHostPort(uri, "9999"),
|
||||
conn: conn,
|
||||
standbypower: standbypower,
|
||||
}
|
||||
return c, nil
|
||||
|
|
@ -56,35 +54,35 @@ func NewTPLink(uri string, standbypower float64) (*TPLink, error) {
|
|||
|
||||
// Enabled implements the api.Charger interface
|
||||
func (c *TPLink) Enabled() (bool, error) {
|
||||
var resp tplink.SystemResponse
|
||||
if err := c.execCmd(`{"system":{"get_sysinfo":null}}`, &resp); err != nil {
|
||||
var res tplink.SystemResponse
|
||||
if err := c.conn.ExecCmd(`{"system":{"get_sysinfo":null}}`, &res); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := resp.System.GetSysinfo.ErrCode; err != 0 {
|
||||
if err := res.System.GetSysinfo.ErrCode; err != 0 {
|
||||
return false, fmt.Errorf("get_sysinfo error %d", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(resp.System.GetSysinfo.Feature, "ENE") {
|
||||
return false, errors.New(resp.System.GetSysinfo.Model + " not supported, energy meter feature missing")
|
||||
if !strings.Contains(res.System.GetSysinfo.Feature, "ENE") {
|
||||
return false, errors.New(res.System.GetSysinfo.Model + " not supported, energy meter feature missing")
|
||||
}
|
||||
|
||||
return resp.System.GetSysinfo.RelayState == 1, nil
|
||||
return res.System.GetSysinfo.RelayState == 1, nil
|
||||
}
|
||||
|
||||
// Enable implements the api.Charger interface
|
||||
func (c *TPLink) Enable(enable bool) error {
|
||||
var res tplink.SystemResponse
|
||||
cmd := `{"system":{"set_relay_state":{"state":0}}}`
|
||||
if enable {
|
||||
cmd = `{"system":{"set_relay_state":{"state":1}}}`
|
||||
}
|
||||
|
||||
var resp tplink.SystemResponse
|
||||
if err := c.execCmd(cmd, &resp); err != nil {
|
||||
if err := c.conn.ExecCmd(cmd, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := resp.System.SetRelayState.ErrCode; err != 0 {
|
||||
if err := res.System.SetRelayState.ErrCode; err != 0 {
|
||||
return fmt.Errorf("set_relay_state error %d", err)
|
||||
}
|
||||
|
||||
|
|
@ -123,89 +121,29 @@ var _ api.Meter = (*TPLink)(nil)
|
|||
|
||||
// CurrentPower implements the api.Meter interface
|
||||
func (c *TPLink) CurrentPower() (float64, error) {
|
||||
var resp tplink.EmeterResponse
|
||||
if err := c.execCmd(`{"emeter":{"get_realtime":null}}`, &resp); err != nil {
|
||||
return 0, err
|
||||
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
|
||||
}
|
||||
|
||||
if err := resp.Emeter.GetRealtime.ErrCode; err != 0 {
|
||||
return 0, fmt.Errorf("get_realtime error %d", err)
|
||||
}
|
||||
|
||||
power := resp.Emeter.GetRealtime.PowerMw / 1000
|
||||
if power == 0 {
|
||||
power = resp.Emeter.GetRealtime.Power
|
||||
}
|
||||
|
||||
// ignore standby power
|
||||
if power < c.standbypower {
|
||||
// ignore power in standby mode
|
||||
power, err := c.conn.CurrentPower()
|
||||
if c.standbypower >= 0 && power <= c.standbypower {
|
||||
power = 0
|
||||
}
|
||||
|
||||
return power, nil
|
||||
return power, err
|
||||
}
|
||||
|
||||
var _ api.MeterEnergy = (*TPLink)(nil)
|
||||
|
||||
// TotalEnergy implements the api.MeterEnergy interface
|
||||
func (c *TPLink) TotalEnergy() (float64, error) {
|
||||
var resp tplink.EmeterResponse
|
||||
if err := c.execCmd(`{"emeter":{"get_realtime":null}}`, &resp); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := resp.Emeter.GetRealtime.ErrCode; err != 0 {
|
||||
return 0, fmt.Errorf("get_realtime error %d", err)
|
||||
}
|
||||
|
||||
energy := resp.Emeter.GetRealtime.TotalWh / 1000
|
||||
if energy == 0 {
|
||||
energy = resp.Emeter.GetRealtime.Total
|
||||
}
|
||||
|
||||
return energy, nil
|
||||
}
|
||||
|
||||
// execCmd executes an TP-Link Smart Home Protocol command and provides the response
|
||||
func (c *TPLink) execCmd(cmd string, res interface{}) error {
|
||||
// encode command message
|
||||
buf := bytes.NewBuffer([]byte{0, 0, 0, 0})
|
||||
var key byte = 171 // initialization vector
|
||||
for i := 0; i < len(cmd); i++ {
|
||||
key = key ^ cmd[i]
|
||||
_ = buf.WriteByte(key)
|
||||
}
|
||||
|
||||
// write 4 bytes command length to start of buffer
|
||||
binary.BigEndian.PutUint32(buf.Bytes(), uint32(buf.Len()-4))
|
||||
|
||||
// open connection via TP-Link Smart Home Protocol
|
||||
conn, err := net.DialTimeout("tcp", c.uri, 5*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// send command
|
||||
if _, err = buf.WriteTo(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read response
|
||||
resp := make([]byte, 8192)
|
||||
len, err := conn.Read(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// decode response message
|
||||
key = 171 // reset initialization vector
|
||||
for i := 4; i < len; i++ {
|
||||
dec := key ^ resp[i]
|
||||
key = resp[i]
|
||||
_ = buf.WriteByte(dec)
|
||||
}
|
||||
c.log.TRACE.Printf("recv: %s", buf.String())
|
||||
|
||||
return json.Unmarshal(buf.Bytes(), res)
|
||||
return c.conn.TotalEnergy()
|
||||
}
|
||||
|
|
|
|||
28
meter/shelly.go
Normal file
28
meter/shelly.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package meter
|
||||
|
||||
import (
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/meter/shelly"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
// Shelly meter implementation
|
||||
func init() {
|
||||
registry.Add("shelly", NewShellyFromConfig)
|
||||
}
|
||||
|
||||
// NewShellyFromConfig creates a Shelly charger from generic config
|
||||
func NewShellyFromConfig(other map[string]interface{}) (api.Meter, error) {
|
||||
cc := struct {
|
||||
URI string
|
||||
User string
|
||||
Password string
|
||||
Channel int
|
||||
}{}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shelly.NewConnection(cc.URI, cc.User, cc.Password, cc.Channel)
|
||||
}
|
||||
169
meter/shelly/connection.go
Normal file
169
meter/shelly/connection.go
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
package shelly
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
"github.com/jpfielding/go-http-digest/pkg/digest"
|
||||
)
|
||||
|
||||
// Connection is the Shelly connection
|
||||
type Connection struct {
|
||||
*request.Helper
|
||||
uri string
|
||||
channel int
|
||||
gen int // Shelly api generation
|
||||
}
|
||||
|
||||
// NewConnection creates a new Shelly device connection.
|
||||
func NewConnection(uri, user, password string, channel int) (*Connection, error) {
|
||||
if uri == "" {
|
||||
return nil, errors.New("missing uri")
|
||||
}
|
||||
|
||||
for _, suffix := range []string{"/", "/rcp", "/shelly"} {
|
||||
uri = strings.TrimSuffix(uri, suffix)
|
||||
}
|
||||
|
||||
log := util.NewLogger("shelly")
|
||||
client := request.NewHelper(log)
|
||||
|
||||
// Shelly Gen1 and Gen2 families expose the /shelly endpoint
|
||||
var resp DeviceInfo
|
||||
if err := client.GetJSON(fmt.Sprintf("%s/shelly", util.DefaultScheme(uri, "http")), &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn := &Connection{
|
||||
Helper: client,
|
||||
channel: channel,
|
||||
gen: resp.Gen,
|
||||
}
|
||||
|
||||
conn.Client.Transport = request.NewTripper(log, transport.Insecure())
|
||||
|
||||
if (resp.Auth || resp.AuthEn) && (user == "" || password == "") {
|
||||
return conn, fmt.Errorf("%s (%s) missing user/password", resp.Model, resp.Mac)
|
||||
}
|
||||
|
||||
switch conn.gen {
|
||||
case 0, 1:
|
||||
// Shelly GEN 1 API
|
||||
// https://shelly-api-docs.shelly.cloud/gen1/#shelly-family-overview
|
||||
conn.uri = util.DefaultScheme(uri, "http")
|
||||
if user != "" {
|
||||
log.Redact(transport.BasicAuthHeader(user, password))
|
||||
conn.Client.Transport = transport.BasicAuth(user, password, conn.Client.Transport)
|
||||
}
|
||||
|
||||
case 2:
|
||||
// Shelly GEN 2 API
|
||||
// https://shelly-api-docs.shelly.cloud/gen2/
|
||||
conn.uri = fmt.Sprintf("%s/rpc", util.DefaultScheme(uri, "http"))
|
||||
if user != "" {
|
||||
conn.Client.Transport = digest.NewTransport(user, password, conn.Client.Transport)
|
||||
}
|
||||
|
||||
default:
|
||||
return conn, fmt.Errorf("%s (%s) unknown api generation (%d)", resp.Type, resp.Model, conn.gen)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// CurrentPower implements the api.Meter interface
|
||||
func (d *Connection) CurrentPower() (float64, error) {
|
||||
var power float64
|
||||
switch d.gen {
|
||||
case 0, 1:
|
||||
var res Gen1StatusResponse
|
||||
uri := fmt.Sprintf("%s/status", d.uri)
|
||||
if err := d.GetJSON(uri, &res); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if d.channel >= len(res.Meters) {
|
||||
return 0, errors.New("invalid channel, missing power meter")
|
||||
}
|
||||
|
||||
power = res.Meters[d.channel].Power
|
||||
|
||||
default:
|
||||
var res Gen2StatusResponse
|
||||
if err := d.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch d.channel {
|
||||
case 1:
|
||||
power = res.Switch1.Apower
|
||||
case 2:
|
||||
power = res.Switch2.Apower
|
||||
default:
|
||||
power = res.Switch0.Apower
|
||||
}
|
||||
}
|
||||
|
||||
return power, nil
|
||||
}
|
||||
|
||||
// Enabled implements the api.Charger interface
|
||||
func (d *Connection) Enabled() (bool, error) {
|
||||
switch d.gen {
|
||||
case 0, 1:
|
||||
var res Gen1SwitchResponse
|
||||
uri := fmt.Sprintf("%s/relay/%d", d.uri, d.channel)
|
||||
err := d.GetJSON(uri, &res)
|
||||
return res.Ison, err
|
||||
|
||||
default:
|
||||
var res Gen2SwitchResponse
|
||||
err := d.execGen2Cmd("Switch.GetStatus", false, &res)
|
||||
return res.Output, err
|
||||
}
|
||||
}
|
||||
|
||||
// Enable implements the api.Charger interface
|
||||
func (d *Connection) Enable(enable bool) error {
|
||||
var err error
|
||||
onoff := map[bool]string{true: "on", false: "off"}
|
||||
|
||||
switch d.gen {
|
||||
case 0, 1:
|
||||
var res Gen1SwitchResponse
|
||||
uri := fmt.Sprintf("%s/relay/%d?turn=%s", d.uri, d.channel, onoff[enable])
|
||||
err = d.GetJSON(uri, &res)
|
||||
|
||||
default:
|
||||
var res Gen2SwitchResponse
|
||||
err = d.execGen2Cmd("Switch.Set", enable, &res)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// execGen2Cmd executes a shelly api gen1/gen2 command and provides the response
|
||||
func (d *Connection) execGen2Cmd(method string, enable bool, res interface{}) error {
|
||||
// Shelly gen 2 rfc7616 authentication
|
||||
// https://shelly-api-docs.shelly.cloud/gen2/Overview/CommonDeviceTraits#authentication
|
||||
// https://datatracker.ietf.org/doc/html/rfc7616
|
||||
|
||||
data := &Gen2RpcPost{
|
||||
Id: d.channel,
|
||||
On: enable,
|
||||
Src: "evcc",
|
||||
Method: method,
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodPost, fmt.Sprintf("%s/%s", d.uri, method), request.MarshalJSON(data), request.JSONEncoding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.DoJSON(req, &res)
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
package meter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/meter/tapo"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
|
|
@ -25,9 +23,5 @@ func NewTapoFromConfig(other map[string]interface{}) (api.Meter, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if cc.URI == "" {
|
||||
return nil, errors.New("missing uri")
|
||||
}
|
||||
|
||||
return tapo.NewConnection(cc.URI, cc.User, cc.Password)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,18 @@ type Connection struct {
|
|||
// User is encoded by using MessageDigest of SHA1 which is afterwards B64 encoded.
|
||||
// Password is directly B64 encoded.
|
||||
func NewConnection(uri, user, password string) (*Connection, error) {
|
||||
if uri == "" {
|
||||
return nil, errors.New("missing uri")
|
||||
}
|
||||
|
||||
if user == "" || password == "" {
|
||||
return nil, fmt.Errorf("missing user or password")
|
||||
}
|
||||
|
||||
for _, suffix := range []string{"/", "/app"} {
|
||||
uri = strings.TrimSuffix(uri, suffix)
|
||||
}
|
||||
|
||||
log := util.NewLogger("tapo")
|
||||
|
||||
// nosemgrep:go.lang.security.audit.crypto.use_of_weak_crypto.use-of-sha1
|
||||
|
|
@ -208,7 +220,7 @@ func (d *Connection) ExecMethod(method string, deviceOn bool) (*DeviceResponse,
|
|||
return res, nil
|
||||
}
|
||||
|
||||
// execCmd executes a Tapo api command and provides the response
|
||||
// ExecCmd executes a Tapo api command and provides the response
|
||||
func (d *Connection) ExecCmd(method string, enable bool) (*DeviceResponse, error) {
|
||||
// refresh session id
|
||||
if time.Since(d.updated) >= 600*time.Minute {
|
||||
|
|
|
|||
27
meter/tasmota.go
Normal file
27
meter/tasmota.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package meter
|
||||
|
||||
import (
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/meter/tasmota"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
// Tasmota meter implementation
|
||||
func init() {
|
||||
registry.Add("tasmota", NewTasmotaFromConfig)
|
||||
}
|
||||
|
||||
// NewTapoFromConfig creates a tapo meter from generic config
|
||||
func NewTasmotaFromConfig(other map[string]interface{}) (api.Meter, error) {
|
||||
cc := struct {
|
||||
URI string
|
||||
User string
|
||||
Password string
|
||||
}{}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tasmota.NewConnection(cc.URI, cc.User, cc.Password)
|
||||
}
|
||||
75
meter/tasmota/connection.go
Normal file
75
meter/tasmota/connection.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package tasmota
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
)
|
||||
|
||||
// Connection is the Tasmota connection
|
||||
type Connection struct {
|
||||
*request.Helper
|
||||
uri, user, password string
|
||||
}
|
||||
|
||||
// NewConnection creates Tasmota charger
|
||||
func NewConnection(uri, user, password string) (*Connection, error) {
|
||||
if uri == "" {
|
||||
return nil, errors.New("missing uri")
|
||||
}
|
||||
|
||||
log := util.NewLogger("tasmota")
|
||||
c := &Connection{
|
||||
Helper: request.NewHelper(log),
|
||||
uri: util.DefaultScheme(strings.TrimRight(uri, "/"), "http"),
|
||||
user: user,
|
||||
password: password,
|
||||
}
|
||||
|
||||
c.Client.Transport = request.NewTripper(log, transport.Insecure())
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ExecCmd executes a Tasmota api command and provides the response
|
||||
func (d *Connection) ExecCmd(cmd string, res interface{}) error {
|
||||
parameters := url.Values{
|
||||
"user": []string{d.user},
|
||||
"password": []string{d.password},
|
||||
"cmnd": []string{cmd},
|
||||
}
|
||||
|
||||
err := d.GetJSON(fmt.Sprintf("%s/cm?%s", d.uri, parameters.Encode()), res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentPower provides current power consumption
|
||||
func (d *Connection) CurrentPower() (float64, error) {
|
||||
var res *StatusSNSResponse
|
||||
err := d.ExecCmd("Status 8", &res)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return float64(res.StatusSNS.Energy.Power), nil
|
||||
}
|
||||
|
||||
// TotalEnergy implements the api.MeterEnergy interface
|
||||
func (d *Connection) TotalEnergy() (float64, error) {
|
||||
var res *StatusSNSResponse
|
||||
err := d.ExecCmd("Status 8", &res)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return float64(res.StatusSNS.Energy.Total), nil
|
||||
}
|
||||
25
meter/tplink.go
Normal file
25
meter/tplink.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package meter
|
||||
|
||||
import (
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/meter/tplink"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
// TP-Link meter implementation
|
||||
func init() {
|
||||
registry.Add("tplink", NewTPLinkFromConfig)
|
||||
}
|
||||
|
||||
// NewTPLinkFromConfig creates a tapo meter from generic config
|
||||
func NewTPLinkFromConfig(other map[string]interface{}) (api.Meter, error) {
|
||||
cc := struct {
|
||||
URI string
|
||||
}{}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tplink.NewConnection(cc.URI)
|
||||
}
|
||||
114
meter/tplink/connection.go
Normal file
114
meter/tplink/connection.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package tplink
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
// Connection is the TP-Link connection
|
||||
type Connection struct {
|
||||
log *util.Logger
|
||||
uri string
|
||||
}
|
||||
|
||||
// NewConnection creates TP-Link charger
|
||||
func NewConnection(uri string) (*Connection, error) {
|
||||
if uri == "" {
|
||||
return nil, errors.New("missing uri")
|
||||
}
|
||||
|
||||
c := &Connection{
|
||||
log: util.NewLogger("tplink"),
|
||||
uri: net.JoinHostPort(uri, "9999"),
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ExecCmd executes an TP-Link Smart Home Protocol command and provides the response
|
||||
func (d *Connection) ExecCmd(cmd string, res interface{}) error {
|
||||
// encode command message
|
||||
buf := bytes.NewBuffer([]byte{0, 0, 0, 0})
|
||||
var key byte = 171 // initialization vector
|
||||
for i := 0; i < len(cmd); i++ {
|
||||
key = key ^ cmd[i]
|
||||
_ = buf.WriteByte(key)
|
||||
}
|
||||
|
||||
// write 4 bytes command length to start of buffer
|
||||
binary.BigEndian.PutUint32(buf.Bytes(), uint32(buf.Len()-4))
|
||||
|
||||
// open connection via TP-Link Smart Home Protocol
|
||||
conn, err := net.DialTimeout("tcp", d.uri, 5*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// send command
|
||||
if _, err = buf.WriteTo(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read response
|
||||
resp := make([]byte, 8192)
|
||||
len, err := conn.Read(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// decode response message
|
||||
key = 171 // reset initialization vector
|
||||
for i := 4; i < len; i++ {
|
||||
dec := key ^ resp[i]
|
||||
key = resp[i]
|
||||
_ = buf.WriteByte(dec)
|
||||
}
|
||||
d.log.TRACE.Printf("recv: %s", buf.String())
|
||||
|
||||
return json.Unmarshal(buf.Bytes(), res)
|
||||
}
|
||||
|
||||
// CurrentPower provides current power consumption
|
||||
func (d *Connection) CurrentPower() (float64, error) {
|
||||
var res EmeterResponse
|
||||
if err := d.ExecCmd(`{"emeter":{"get_realtime":null}}`, &res); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := res.Emeter.GetRealtime.ErrCode; err != 0 {
|
||||
return 0, fmt.Errorf("get_realtime error %d", err)
|
||||
}
|
||||
|
||||
power := res.Emeter.GetRealtime.PowerMw / 1000
|
||||
if power == 0 {
|
||||
power = res.Emeter.GetRealtime.Power
|
||||
}
|
||||
|
||||
return power, nil
|
||||
}
|
||||
|
||||
// TotalEnergy implements the api.MeterEnergy interface
|
||||
func (d *Connection) TotalEnergy() (float64, error) {
|
||||
var res EmeterResponse
|
||||
if err := d.ExecCmd(`{"emeter":{"get_realtime":null}}`, &res); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := res.Emeter.GetRealtime.ErrCode; err != 0 {
|
||||
return 0, fmt.Errorf("get_realtime error %d", err)
|
||||
}
|
||||
|
||||
energy := res.Emeter.GetRealtime.TotalWh / 1000
|
||||
if energy == 0 {
|
||||
energy = res.Emeter.GetRealtime.Total
|
||||
}
|
||||
|
||||
return energy, nil
|
||||
}
|
||||
|
|
@ -27,4 +27,4 @@ render: |
|
|||
user: {{ .user }}
|
||||
password: {{ .password }}
|
||||
ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker)
|
||||
standbypower: {{ .standbypower }} # treat as charging above this power
|
||||
standbypower: {{ .standbypower }}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,17 @@ products:
|
|||
group: switchsockets
|
||||
params:
|
||||
- name: host
|
||||
- name: user
|
||||
- name: password
|
||||
mask: true
|
||||
- name: channel
|
||||
default: 0
|
||||
- name: standbypower
|
||||
default: 15
|
||||
render: |
|
||||
type: shelly
|
||||
uri: http://{{ .host }} # shelly device ip address (local)
|
||||
channel: 0 # shelly device relay channel
|
||||
standbypower: {{ .standbypower }} # treat as charging above this power
|
||||
uri: http://{{ .host }}
|
||||
{{ if ne .user "" }}user: {{ .user }}{{ end }}
|
||||
{{ if ne .password "" }}password: {{ .password }}{{ end }}
|
||||
channel: {{ .channel }} # shelly device relay channel
|
||||
standbypower: {{ .standbypower }}
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@ render: |
|
|||
{{ if ne .password "" }}
|
||||
password: {{ .password }}
|
||||
{{ end }}
|
||||
standbypower: {{ .standbypower }} # treat as charging above this power
|
||||
standbypower: {{ .standbypower }}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ params:
|
|||
render: |
|
||||
type: tplink
|
||||
uri: {{ .host }}
|
||||
standbypower: {{ .standbypower }} # treat as charging above this power
|
||||
standbypower: {{ .standbypower }}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ products:
|
|||
- brand: AVM
|
||||
description:
|
||||
generic: FritzDECT
|
||||
group: sockets
|
||||
group: switchsockets
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["pv", "charge"]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ products:
|
|||
- brand: Shelly
|
||||
description:
|
||||
generic: 3EM
|
||||
group: sockets
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["grid", "pv", "charge"]
|
||||
|
|
|
|||
|
|
@ -3,24 +3,19 @@ products:
|
|||
- brand: Shelly
|
||||
description:
|
||||
generic: 1PM
|
||||
group: sockets
|
||||
group: switchsockets
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["pv"]
|
||||
- name: host
|
||||
- name: user
|
||||
required: false
|
||||
- name: password
|
||||
required: false
|
||||
mask: true
|
||||
- name: channel
|
||||
default: 0
|
||||
render: |
|
||||
type: custom
|
||||
power:
|
||||
source: http
|
||||
uri: http://{{ if ne .user "" }}{{ .user }}:{{ .password }}@{{ end }}{{ .host }}/status
|
||||
jq: .meters | map(.power) | add
|
||||
energy:
|
||||
source: http
|
||||
uri: http://{{ if ne .user "" }}{{ .user }}:{{ .password }}@{{ end }}{{ .host }}/status
|
||||
jq: .meters | map(.total) | add
|
||||
scale: 0.0000166666666666667
|
||||
type: shelly
|
||||
uri: http://{{ .host }} # shelly device ip address (local)
|
||||
{{ if ne .user "" }}user: {{ .user }}{{ end }}
|
||||
{{ if ne .password "" }}password: {{ .password }}{{ end }}
|
||||
channel: {{ .channel }} # shelly device relay channel
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@ template: tasmota
|
|||
products:
|
||||
- description:
|
||||
generic: Tasmota
|
||||
group: sockets
|
||||
group: switchsockets
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["grid", "pv", "charge"]
|
||||
choice: ["pv"]
|
||||
- name: host
|
||||
- name: user
|
||||
required: false
|
||||
default: admin
|
||||
- name: password
|
||||
required: false
|
||||
mask: true
|
||||
render: |
|
||||
type: custom
|
||||
power: # power reading (W)
|
||||
source: http
|
||||
uri: http://{{ .host }}/cm?cmnd=Status%208
|
||||
jq: .StatusSNS.ENERGY.Power
|
||||
energy: # energy reading (kWh)
|
||||
source: http
|
||||
uri: http://{{ .host }}/cm?cmnd=Status%208
|
||||
jq: .StatusSNS.ENERGY.Total
|
||||
type: tasmota
|
||||
uri: http://{{ .host }}
|
||||
user: {{ .user }}
|
||||
password: {{ .password }}
|
||||
|
|
|
|||
13
templates/definition/meter/tplink.yaml
Normal file
13
templates/definition/meter/tplink.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
template: tplink
|
||||
products:
|
||||
- brand: TP-Link
|
||||
description:
|
||||
generic: H-Series Smart Plug
|
||||
group: switchsockets
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["pv"]
|
||||
- name: host
|
||||
render: |
|
||||
type: tplink
|
||||
uri: {{ .host }}
|
||||
|
|
@ -6,4 +6,7 @@ render:
|
|||
type: template
|
||||
template: shelly
|
||||
host: 192.0.2.2 # IP-Adresse oder Hostname
|
||||
user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
|
||||
password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional
|
||||
channel: 0 # Optional
|
||||
standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet # Optional
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
product:
|
||||
brand: AVM
|
||||
description: FritzDECT
|
||||
group: Steckdosen
|
||||
group: Schaltbare Steckdosen
|
||||
render:
|
||||
- usage: pv
|
||||
default: |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
product:
|
||||
brand: Shelly
|
||||
description: 1PM
|
||||
group: Steckdosen
|
||||
group: Schaltbare Steckdosen
|
||||
render:
|
||||
- usage: pv
|
||||
default: |
|
||||
|
|
@ -11,3 +11,4 @@ render:
|
|||
host: 192.0.2.2 # IP-Adresse oder Hostname
|
||||
user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
|
||||
password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional
|
||||
channel: 0 # Optional
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
product:
|
||||
brand: Shelly
|
||||
description: 3EM
|
||||
group: Steckdosen
|
||||
render:
|
||||
- usage: grid
|
||||
default: |
|
||||
|
|
|
|||
|
|
@ -1,22 +1,12 @@
|
|||
product:
|
||||
description: Tasmota
|
||||
group: Steckdosen
|
||||
group: Schaltbare Steckdosen
|
||||
render:
|
||||
- usage: grid
|
||||
default: |
|
||||
type: template
|
||||
template: tasmota
|
||||
usage: grid
|
||||
host: 192.0.2.2 # IP-Adresse oder Hostname
|
||||
- usage: pv
|
||||
default: |
|
||||
type: template
|
||||
template: tasmota
|
||||
usage: pv
|
||||
host: 192.0.2.2 # IP-Adresse oder Hostname
|
||||
- usage: charge
|
||||
default: |
|
||||
type: template
|
||||
template: tasmota
|
||||
usage: charge
|
||||
host: 192.0.2.2 # IP-Adresse oder Hostname
|
||||
user: admin # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
|
||||
password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional
|
||||
|
|
|
|||
11
templates/docs/meter/tplink_0.yaml
Normal file
11
templates/docs/meter/tplink_0.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
product:
|
||||
brand: TP-Link
|
||||
description: H-Series Smart Plug
|
||||
group: Schaltbare Steckdosen
|
||||
render:
|
||||
- usage: pv
|
||||
default: |
|
||||
type: template
|
||||
template: tplink
|
||||
usage: pv
|
||||
host: 192.0.2.2 # IP-Adresse oder Hostname
|
||||
Loading…
Add table
Add a link
Reference in a new issue