Skip to content

Commit

Permalink
Standardise Switchsockets (Tapo+TP-Link+Tasmota+Shelly) (evcc-io#3311)
Browse files Browse the repository at this point in the history
  • Loading branch information
thierolm authored May 5, 2022
1 parent 0bd7ea9 commit 7fb3661
Show file tree
Hide file tree
Showing 31 changed files with 584 additions and 354 deletions.
165 changes: 20 additions & 145 deletions charger/shelly.go
Original file line number Diff line number Diff line change
@@ -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
}

Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
}
16 changes: 0 additions & 16 deletions charger/tapo.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down
62 changes: 16 additions & 46 deletions charger/tasmota.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -41,33 +35,28 @@ 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")
conn, err := tasmota.NewConnection(uri, user, password)
if err != nil {
return nil, err
}

c := &Tasmota{
Helper: request.NewHelper(log),
uri: util.DefaultScheme(strings.TrimRight(uri, "/"), "http"),
user: user,
password: password,
conn: conn,
standbypower: standbypower,
}

c.Client.Transport = request.NewTripper(log, transport.Insecure())

return c, nil
}

// 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
}
Expand All @@ -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:
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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()
}
Loading

0 comments on commit 7fb3661

Please sign in to comment.