Skip to content

Commit

Permalink
Add Shelly Pro 3 EM (evcc-io#6457)
Browse files Browse the repository at this point in the history
  • Loading branch information
wimaha authored Feb 27, 2023
1 parent 892057b commit 69a3755
Show file tree
Hide file tree
Showing 12 changed files with 358 additions and 174 deletions.
4 changes: 2 additions & 2 deletions charger/shelly.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

// Shelly charger implementation
type Shelly struct {
conn *shelly.Connection
conn *shelly.Switch
*switchSocket
}

Expand Down Expand Up @@ -44,7 +44,7 @@ func NewShelly(embed embed, uri, user, password string, channel int, standbypowe
}

c := &Shelly{
conn: conn,
conn: shelly.NewSwitch(conn),
}

c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
Expand Down
7 changes: 6 additions & 1 deletion meter/shelly.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,10 @@ func NewShellyFromConfig(other map[string]interface{}) (api.Meter, error) {
return nil, err
}

return shelly.NewConnection(cc.URI, cc.User, cc.Password, cc.Channel)
conn, err := shelly.NewConnection(cc.URI, cc.User, cc.Password, cc.Channel)
if err != nil {
return nil, err
}

return shelly.NewSwitch(conn), nil
}
124 changes: 0 additions & 124 deletions meter/shelly/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,80 +78,6 @@ func NewConnection(uri, user, password string, channel int) (*Connection, error)
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
}

switch {
case d.channel < len(res.Meters):
power = res.Meters[d.channel].Power
case d.channel < len(res.EMeters):
power = res.EMeters[d.channel].Power
default:
return 0, errors.New("invalid channel, missing power meter")
}

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
Expand All @@ -172,53 +98,3 @@ func (d *Connection) execGen2Cmd(method string, enable bool, res interface{}) er

return d.DoJSON(req, &res)
}

// TotalEnergy implements the api.Meter interface
func (d *Connection) TotalEnergy() (float64, error) {
var energy 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
}

switch {
case d.channel < len(res.Meters):
energy = res.Meters[d.channel].Total
case d.channel < len(res.EMeters):
energy = res.EMeters[d.channel].Total
default:
return 0, errors.New("invalid channel, missing power meter")
}

energy = GetGen1Energy(d.devicetype, energy)

default:
var res Gen2StatusResponse
if err := d.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
return 0, err
}

switch d.channel {
case 1:
energy = res.Switch1.Aenergy.Total
case 2:
energy = res.Switch2.Aenergy.Total
default:
energy = res.Switch0.Aenergy.Total
}
}

return energy / 1000, nil
}

// GetGen1Energy in kWh
func GetGen1Energy(devicetype string, energy float64) float64 {
// Gen 1 Shelly EM devices are providing Watt hours, Shelly EM devices are providing Watt minutes
if !strings.Contains(devicetype, "EM") {
energy = energy / 60
}
return energy
}
71 changes: 71 additions & 0 deletions meter/shelly/energymeter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package shelly

import "github.com/evcc-io/evcc/api"

type EnergyMeter struct {
*Connection
}

func NewEnergyMeter(conn *Connection) *EnergyMeter {
res := &EnergyMeter{
Connection: conn,
}

return res
}

// CurrentPower implements the api.Meter interface
func (sh *EnergyMeter) CurrentPower() (float64, error) {
var res Gen2EmStatusResponse
if err := sh.Connection.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
return 0, err
}

return res.TotalPower, nil
}

// TotalEnergy implements the api.Meter interface
func (sh *EnergyMeter) TotalEnergy() (float64, error) {
var res Gen2EmDataStatusResponse
if err := sh.Connection.execGen2Cmd("EMData.GetStatus", false, &res); err != nil {
return 0, err
}

return res.TotalEnergy, nil
}

var _ api.PhaseCurrents = (*EnergyMeter)(nil)

// Currents implements the api.PhaseCurrents interface
func (sh *EnergyMeter) Currents() (float64, float64, float64, error) {
var res Gen2EmStatusResponse
if err := sh.Connection.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
return 0, 0, 0, err
}

return res.CurrentA, res.CurrentB, res.CurrentC, nil
}

var _ api.PhaseVoltages = (*EnergyMeter)(nil)

// Voltages implements the api.PhaseVoltages interface
func (sh *EnergyMeter) Voltages() (float64, float64, float64, error) {
var res Gen2EmStatusResponse
if err := sh.Connection.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
return 0, 0, 0, err
}

return res.VoltageA, res.VoltageB, res.VoltageC, nil
}

var _ api.PhasePowers = (*EnergyMeter)(nil)

// Powers implements the api.PhasePowers interface
func (sh *EnergyMeter) Powers() (float64, float64, float64, error) {
var res Gen2EmStatusResponse
if err := sh.Connection.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
return 0, 0, 0, err
}

return res.PowerA, res.PowerB, res.PowerC, nil
}
149 changes: 149 additions & 0 deletions meter/shelly/switch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package shelly

import (
"errors"
"fmt"
"strings"
)

type Switch struct {
*Connection
}

func NewSwitch(conn *Connection) *Switch {
res := &Switch{
Connection: conn,
}

return res
}

// CurrentPower implements the api.Meter interface
func (sh *Switch) CurrentPower() (float64, error) {
var power float64

d := sh.Connection
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
}

switch {
case d.channel < len(res.Meters):
power = res.Meters[d.channel].Power
case d.channel < len(res.EMeters):
power = res.EMeters[d.channel].Power
default:
return 0, errors.New("invalid channel, missing power meter")
}

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 (sh *Switch) Enabled() (bool, error) {
d := sh.Connection
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 (sh *Switch) Enable(enable bool) error {
var err error
onoff := map[bool]string{true: "on", false: "off"}

d := sh.Connection
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
}

// TotalEnergy implements the api.Meter interface
func (sh *Switch) TotalEnergy() (float64, error) {
var energy float64

d := sh.Connection
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
}

switch {
case d.channel < len(res.Meters):
energy = res.Meters[d.channel].Total
case d.channel < len(res.EMeters):
energy = res.EMeters[d.channel].Total
default:
return 0, errors.New("invalid channel, missing power meter")
}

energy = gen1Energy(d.devicetype, energy)

default:
var res Gen2StatusResponse
if err := d.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
return 0, err
}

switch d.channel {
case 1:
energy = res.Switch1.Aenergy.Total
case 2:
energy = res.Switch2.Aenergy.Total
default:
energy = res.Switch0.Aenergy.Total
}
}

return energy / 1000, nil
}

// gen1Energy in kWh
func gen1Energy(devicetype string, energy float64) float64 {
// Gen 1 Shelly EM devices are providing Watt hours, Shelly EM devices are providing Watt minutes
if !strings.Contains(devicetype, "EM") {
energy = energy / 60
}
return energy
}
Loading

0 comments on commit 69a3755

Please sign in to comment.