Add Shelly pseudo charger (#1028)

This commit is contained in:
Markus Thierolf 2021-05-16 10:31:47 +02:00 • committed by GitHub
parent f43c204cc9
commit 3bb4990312
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 143 additions and 2 deletions

View file

@ -13,7 +13,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
- simple and clean user interface
- multiple [chargers](#charger):
- Wallbe, Phoenix (includes ESL Walli), go-eCharger, NRGkick (direct Bluetooth or via Connect device), SimpleEVSE, EVSEWifi, KEBA/BMW, openWB, Mobile Charger Connect and any other charger using scripting
- Smart-Home outlets: FritzDECT, Tasmota, TP-Link
- Smart-Home outlets: FritzDECT, Shelly, Tasmota, TP-Link
- multiple [meters](#meter): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Sunny Home Manager and Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC, ...), Tesla PowerWall
- wide support of vendor-specific [vehicles](#vehicle) interfaces (remote charge, battery and preconditioning status): Audi, BMW, Ford, Hyundai, Kia, Nissan, Niu, Porsche, Renault, Seat, Skoda, Tesla, Volkswagen, Volvo and any other connected vehicle using scripting
- [plugins](#plugins) for integrating with hardware devices and home automation: Modbus (meters and grid inverters), HTTP, MQTT, Javascript, WebSockets and shell scripts
@ -188,6 +188,7 @@ Available charger implementations are:
Smart-Home outlet charger implementations:
- `fritzdect`: Fritz!DECT 200/210 outlets
- `shelly`: Shelly outlets
- `tasmota`: Tasmota outlets
- `tplink`: TP-Link HSXXX series outlets

View file

@ -38,6 +38,7 @@ const (
taskMeter = "meter"
taskFronius = "fronius"
taskTasmota = "tasmota"
taskShelly = "shelly"
// taskTPLink = "tplink"
)
@ -252,7 +253,7 @@ func init() {
Type: tasks.Http,
Depends: TaskHttp,
Config: map[string]interface{}{
"path": "//cm?cmnd=Module",
"path": "/cm?cmnd=Module",
"jq": ".Module",
},
})
@ -277,4 +278,14 @@ func init() {
"jq": ".version",
},
})
taskList.Add(tasks.Task{
ID: taskShelly,
Type: tasks.Http,
Depends: TaskHttp,
Config: map[string]interface{}{
"path": "/shelly",
"jq": ".type",
},
})
}

129
internal/charger/shelly.go Normal file
View file

@ -0,0 +1,129 @@
package charger
import (
"errors"
"fmt"
"strings"
"github.com/andig/evcc/api"
"github.com/andig/evcc/util"
"github.com/andig/evcc/util/request"
)
// Shelly api homepage
// https://shelly-api-docs.shelly.cloud/#common-http-api
// shellyRelayResponse provides the evcc shelly charger enabled information
type shellyRelayResponse struct {
Ison bool `json:"ison,omitempty"`
}
// shellyStatusResponse provides the evcc shelly charger current power information
type shellyStatusResponse struct {
Meters []struct {
Power float64 `json:"power,omitempty"`
} `json:"meters,omitempty"`
}
// Shelly charger implementation
type Shelly struct {
*request.Helper
uri string
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
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.Channel, cc.StandbyPower)
}
// NewShelly creates Shelly charger
func NewShelly(uri string, channel int, standbypower float64) (*Shelly, error) {
c := &Shelly{
Helper: request.NewHelper(util.NewLogger("shelly")),
uri: strings.TrimRight(uri, "/"),
channel: channel,
standbypower: standbypower,
}
return c, nil
}
// Enabled implements the api.Charger interface
func (c *Shelly) Enabled() (bool, error) {
var resp shellyRelayResponse
err := c.GetJSON(fmt.Sprintf("%s/relay/%d", c.uri, c.channel), &resp)
return resp.Ison, err
}
// Enable implements the api.Charger interface
func (c *Shelly) Enable(enable bool) error {
var resp shellyRelayResponse
onoff := map[bool]string{true: "on", false: "off"}
err := c.GetJSON(fmt.Sprintf("%s/relay/%d?turn=%s", c.uri, c.channel, onoff[enable]), &resp)
switch {
case err != nil:
return err
case enable && !resp.Ison:
return errors.New("switchOn failed")
case !enable && resp.Ison:
return errors.New("switchOff failed")
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 > 0 {
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 resp shellyStatusResponse
err := c.GetJSON(fmt.Sprintf("%s/%s", c.uri, "status"), &resp)
if c.channel >= len(resp.Meters) {
return 0, errors.New("invalid channel, power meter missing")
}
power := resp.Meters[c.channel].Power
// ignore standby power
if power < c.standbypower {
power = 0
}
return power, err
}