evcc-io/charger/shelly.go
2021-10-03 18:21:54 +02:00

233 lines
5.5 KiB
Go

package charger
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger/shelly"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/basicauth"
"github.com/evcc-io/evcc/util/request"
"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
standbypower float64
}
func init() {
registry.Add("shelly", NewShellyFromConfig)
}
// NewShellyFromConfig creates a Shelly charger from generic config
func NewShellyFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
URI string
User string
Password string
Channel int
StandbyPower float64
}{}
if err := util.DecodeOther(other, &cc); err != nil {
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 {
return nil, err
}
c := &Shelly{
Helper: client,
log: log,
channel: channel,
standbypower: standbypower,
gen: resp.Gen,
}
c.Client.Transport = request.NewTripper(log, request.InsecureTransport())
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 != "" {
c.Client.Transport = basicauth.NewTransport(user, password, c.Client.Transport)
}
if resp.NumMeters == 0 {
return c, fmt.Errorf("%s (%s) gen1 missing power meter ", resp.Model, resp.Mac)
}
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
}
// 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
}
}
// 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)
}
if err != nil {
return err
}
enabled, err := c.Enabled()
switch {
case err != nil:
return err
case enable != enabled:
return fmt.Errorf("switch %s failed", onoff[enable])
default:
return nil
}
}
// MaxCurrent implements the api.Charger interface
func (c *Shelly) MaxCurrent(current int64) error {
return nil
}
// Status implements the api.Charger interface
func (c *Shelly) Status() (api.ChargeStatus, error) {
power, err := c.CurrentPower()
if power > c.standbypower {
return api.StatusC, err
}
return api.StatusB, err
}
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
}
}
// ignore standby power
if 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)
}