Shelly: add usage and caching (BC) (#20570)

This commit is contained in:
Markus Thierolf 2025-04-18 17:01:46 +02:00 • committed by GitHub
parent e08016f6e9
commit 21b76d5627
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 714 additions and 407 deletions

View file

@ -2,6 +2,7 @@ package charger
import (
"fmt"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/meter/shelly"
@ -10,7 +11,7 @@ import (
// Shelly charger implementation
type Shelly struct {
conn *shelly.Switch
conn *shelly.Connection
*switchSocket
}
@ -18,33 +19,50 @@ func init() {
registry.Add("shelly", NewShellyFromConfig)
}
//go:generate go tool decorate -f decorateShelly -b *Shelly -r api.Charger -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhasePowers,Powers,func() (float64, float64, float64, error)"
// NewShellyFromConfig creates a Shelly charger from generic config
func NewShellyFromConfig(other map[string]interface{}) (api.Charger, error) {
var cc struct {
func NewShellyFromConfig(other map[string]any) (api.Charger, error) {
cc := struct {
embed `mapstructure:",squash"`
URI string
User string
Password string
Channel int
StandbyPower float64
Cache time.Duration
}{
Cache: time.Second,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
return NewShelly(cc.embed, cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower)
c, err := NewShelly(cc.embed, cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower, cc.Cache)
if err != nil {
return nil, err
}
var vol, cur, pow func() (float64, float64, float64, error)
if phases, ok := c.conn.Generation.(shelly.Phases); ok {
vol = phases.Voltages
cur = phases.Currents
pow = phases.Powers
}
return decorateShelly(c, vol, cur, pow), nil
}
// NewShelly creates Shelly charger
func NewShelly(embed embed, uri, user, password string, channel int, standbypower float64) (*Shelly, error) {
conn, err := shelly.NewConnection(uri, user, password, channel)
func NewShelly(embed embed, uri, user, password string, channel int, standbypower float64, cache time.Duration) (*Shelly, error) {
conn, err := shelly.NewConnection(uri, user, password, channel, cache)
if err != nil {
return nil, err
}
c := &Shelly{
conn: shelly.NewSwitch(conn),
conn: conn,
}
c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)

View file

@ -0,0 +1,111 @@
package charger
// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
import (
"github.com/evcc-io/evcc/api"
)
func decorateShelly(base *Shelly, phaseVoltages func() (float64, float64, float64, error), phaseCurrents func() (float64, float64, float64, error), phasePowers func() (float64, float64, float64, error)) api.Charger {
switch {
case phaseCurrents == nil && phaseVoltages == nil:
return base
case phaseCurrents == nil && phaseVoltages != nil:
return &struct {
*Shelly
api.PhaseVoltages
}{
Shelly: base,
PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
case phaseCurrents != nil && phasePowers == nil && phaseVoltages == nil:
return &struct {
*Shelly
api.PhaseCurrents
}{
Shelly: base,
PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
}
case phaseCurrents != nil && phasePowers == nil && phaseVoltages != nil:
return &struct {
*Shelly
api.PhaseCurrents
api.PhaseVoltages
}{
Shelly: base,
PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
case phaseCurrents != nil && phasePowers != nil && phaseVoltages == nil:
return &struct {
*Shelly
api.PhaseCurrents
api.PhasePowers
}{
Shelly: base,
PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
PhasePowers: &decorateShellyPhasePowersImpl{
phasePowers: phasePowers,
},
}
case phaseCurrents != nil && phasePowers != nil && phaseVoltages != nil:
return &struct {
*Shelly
api.PhaseCurrents
api.PhasePowers
api.PhaseVoltages
}{
Shelly: base,
PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
PhasePowers: &decorateShellyPhasePowersImpl{
phasePowers: phasePowers,
},
PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
}
return nil
}
type decorateShellyPhaseCurrentsImpl struct {
phaseCurrents func() (float64, float64, float64, error)
}
func (impl *decorateShellyPhaseCurrentsImpl) Currents() (float64, float64, float64, error) {
return impl.phaseCurrents()
}
type decorateShellyPhasePowersImpl struct {
phasePowers func() (float64, float64, float64, error)
}
func (impl *decorateShellyPhasePowersImpl) Powers() (float64, float64, float64, error) {
return impl.phasePowers()
}
type decorateShellyPhaseVoltagesImpl struct {
phaseVoltages func() (float64, float64, float64, error)
}
func (impl *decorateShellyPhaseVoltagesImpl) Voltages() (float64, float64, float64, error) {
return impl.phaseVoltages()
}

View file

@ -1,33 +1,83 @@
package meter
import (
"math"
"strings"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/meter/shelly"
"github.com/evcc-io/evcc/util"
)
// Shelly meter considering usage
type Shelly struct {
shelly.Connection
usage string
}
// Shelly meter implementation
func init() {
registry.Add("shelly", NewShellyFromConfig)
}
//go:generate go tool decorate -f decorateShelly -b *Shelly -r api.Meter -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhasePowers,Powers,func() (float64, float64, float64, error)"
// NewShellyFromConfig creates a Shelly charger from generic config
func NewShellyFromConfig(other map[string]interface{}) (api.Meter, error) {
var cc struct {
func NewShellyFromConfig(other map[string]any) (api.Meter, error) {
cc := struct {
URI string
User string
Password string
Channel int
Usage string
Cache time.Duration
}{
Cache: time.Second,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
conn, err := shelly.NewConnection(cc.URI, cc.User, cc.Password, cc.Channel)
c, err := NewShelly(cc.URI, cc.User, cc.Password, strings.ToLower(cc.Usage), cc.Channel, cc.Cache)
if err != nil {
return nil, err
}
return shelly.NewSwitch(conn), nil
var vol, cur, pow func() (float64, float64, float64, error)
if phases, ok := c.Connection.Generation.(shelly.Phases); ok {
vol = phases.Voltages
cur = phases.Currents
pow = phases.Powers
}
return decorateShelly(c, vol, cur, pow), nil
}
// NewShelly creates Shelly meter
func NewShelly(uri, user, password, usage string, channel int, cache time.Duration) (*Shelly, error) {
conn, err := shelly.NewConnection(uri, user, password, channel, cache)
if err != nil {
return nil, err
}
c := &Shelly{
Connection: *conn,
usage: usage,
}
return c, nil
}
var _ api.Meter = (*Shelly)(nil)
// CurrentPower implements the api.Meter interface
func (c *Shelly) CurrentPower() (float64, error) {
power, err := c.Connection.CurrentPower()
if err != nil {
return 0, err
}
if c.usage == "pv" {
power = math.Abs(power)
}
return power, nil
}

View file

@ -3,27 +3,34 @@ package shelly
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"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"
)
type Generation interface {
CurrentPower() (float64, error)
Enabled() (bool, error)
Enable(bool) error
TotalEnergy() (float64, error)
}
type Phases interface {
Currents() (float64, float64, float64, error)
Voltages() (float64, float64, float64, error)
Powers() (float64, float64, float64, error)
}
// Connection is the Shelly connection
type Connection struct {
*request.Helper
uri string
channel int
gen int // Shelly api generation
model string // Shelly device type
profile string // Shelly device profile
Generation
}
// NewConnection creates a new Shelly device connection.
func NewConnection(uri, user, password string, channel int) (*Connection, error) {
func NewConnection(uri, user, password string, channel int, cache time.Duration) (*Connection, error) {
if uri == "" {
return nil, errors.New("missing uri")
}
@ -31,13 +38,14 @@ func NewConnection(uri, user, password string, channel int) (*Connection, error)
for _, suffix := range []string{"/", "/rcp", "/shelly"} {
uri = strings.TrimSuffix(uri, suffix)
}
uri = util.DefaultScheme(uri, "http")
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 {
if err := client.GetJSON(fmt.Sprintf("%s/shelly", uri), &resp); err != nil {
return nil, err
}
@ -45,61 +53,30 @@ func NewConnection(uri, user, password string, channel int) (*Connection, error)
return nil, fmt.Errorf("%s (%s) missing user/password", resp.Model, resp.Mac)
}
conn := &Connection{
Helper: client,
uri: util.DefaultScheme(uri, "http"),
channel: channel,
gen: resp.Gen,
model: strings.Split(resp.Type+resp.Model, "-")[0],
profile: resp.Profile,
}
conn.Client.Transport = request.NewTripper(log, transport.Insecure())
// Set default profile to "monophase" if not provided
if resp.Profile == "" {
resp.Profile = "monophase"
}
switch conn.gen {
case 0, 1:
model := strings.Split(resp.Type+resp.Model, "-")[0]
client.Transport = request.NewTripper(log, transport.Insecure())
var gen Generation
if resp.Gen < 2 {
// Shelly GEN 1 API
// https://shelly-api-docs.shelly.cloud/gen1/#shelly-family-overview
if user != "" {
log.Redact(transport.BasicAuthHeader(user, password))
conn.Client.Transport = transport.BasicAuth(user, password, conn.Client.Transport)
}
case 2, 3:
gen = newGen1(client, uri, model, channel, user, password, cache)
} else {
// Shelly GEN 2+ API
// https://shelly-api-docs.shelly.cloud/gen2/
conn.uri += "/rpc"
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)
gen = newGen2(client, uri, model, resp.Profile, channel, user, password, cache)
}
conn := &Connection{gen}
return conn, nil
}
// 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)
}

View file

@ -1,71 +0,0 @@
package shelly
import "github.com/evcc-io/evcc/api"
type EnergyMeter struct {
conn *Connection
}
func NewEnergyMeter(conn *Connection) *EnergyMeter {
res := &EnergyMeter{
conn: conn,
}
return res
}
// CurrentPower implements the api.Meter interface
func (sh *EnergyMeter) CurrentPower() (float64, error) {
var res Gen2StatusResponse
if err := sh.conn.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.conn.execGen2Cmd("EMData.GetStatus", false, &res); err != nil {
return 0, err
}
return res.TotalEnergy / 1000, nil
}
var _ api.PhaseCurrents = (*EnergyMeter)(nil)
// Currents implements the api.PhaseCurrents interface
func (sh *EnergyMeter) Currents() (float64, float64, float64, error) {
var res Gen2StatusResponse
if err := sh.conn.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 Gen2StatusResponse
if err := sh.conn.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 Gen2StatusResponse
if err := sh.conn.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
return 0, 0, 0, err
}
return res.PowerA, res.PowerB, res.PowerC, nil
}

View file

@ -1,5 +1,16 @@
package shelly
import (
"errors"
"fmt"
"strings"
"time"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
)
// Gen1API endpoint reference: https://shelly-api-docs.shelly.cloud/gen1/#shelly-family-overview
type Gen1SwitchResponse struct {
@ -23,3 +34,102 @@ type Gen1Status struct {
Total_Returned float64
}
}
var _ Generation = (*gen1)(nil)
type gen1 struct {
*request.Helper
uri string
channel int
model string
status util.Cacheable[Gen1Status]
}
// newGen1 initializes the connection to the shelly gen1 api and sets up the cached gen1Status
func newGen1(client *request.Helper, uri, model string, channel int, user, password string, cache time.Duration) *gen1 {
if user != "" {
client.Transport = transport.BasicAuth(user, password, client.Transport)
}
c := &gen1{
Helper: client,
uri: uri,
model: model,
channel: channel,
}
// Cached gen1Status
c.status = util.ResettableCached(func() (Gen1Status, error) {
var res Gen1Status
if err := c.GetJSON(fmt.Sprintf("%s/status", uri), &res); err != nil {
return Gen1Status{}, err
}
return res, nil
}, cache)
return c
}
func (c *gen1) CurrentPower() (float64, error) {
var power float64
res, err := c.status.Get()
if err != nil {
return 0, err
}
switch {
case c.channel < len(res.Meters):
power = res.Meters[c.channel].Power
case c.channel < len(res.EMeters):
power = res.EMeters[c.channel].Power
default:
return 0, errors.New("invalid channel, missing power meter")
}
return power, nil
}
func (c *gen1) Enabled() (bool, error) {
var res Gen1SwitchResponse
uri := fmt.Sprintf("%s/relay/%d", c.uri, c.channel)
err := c.GetJSON(uri, &res)
return res.Ison, err
}
func (c *gen1) Enable(enable bool) error {
onoff := map[bool]string{true: "on", false: "off"}
c.status.Reset()
var res Gen1SwitchResponse
uri := fmt.Sprintf("%s/relay/%d?turn=%s", c.uri, c.channel, onoff[enable])
return c.GetJSON(uri, &res)
}
func (c *gen1) TotalEnergy() (float64, error) {
var energy float64
res, err := c.status.Get()
if err != nil {
return 0, err
}
switch {
case c.channel < len(res.Meters):
energy = res.Meters[c.channel].Total - res.Meters[c.channel].Total_Returned
case c.channel < len(res.EMeters):
energy = res.EMeters[c.channel].Total - res.EMeters[c.channel].Total_Returned
default:
return 0, errors.New("invalid channel, missing power meter")
}
return c.energy(energy) / 1000, nil
}
// gen1Energy in kWh
func (c *gen1) energy(energy float64) float64 {
// Gen 1 Shelly EM devices are providing Watt hours, Gen 1 Shelly PM devices are providing Watt minutes
if !strings.Contains(c.model, "EM") {
energy /= 60
}
return energy
}

View file

@ -17,7 +17,8 @@ func TestUnmarshalGen1Status(t *testing.T) {
jsonstr := `{"wifi_sta":{"connected":true,"ssid":"XXXX-WLAN","ip":"192.168.178.XXX","rssi":-54},"cloud":{"enabled":false,"connected":false},"mqtt":{"connected":false},"time":"17:59","unixtime":1676134770,"serial":2437,"has_update":true,"mac":"84CCA8XXXXXXX","cfg_changed_cnt":1,"actions_stats":{"skipped":0},"relays":[{"ison":false,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"overpower":false,"source":"http"}],"meters":[{"power":4711.12,"overpower":0.00,"is_valid":true,"timestamp":1676138370,"counters":[0.000, 0.000, 0.000],"total":6472513}],"inputs":[{"input":0,"event":"","event_cnt":0}],"temperature":16.79,"overtemperature":false,"tmp":{"tC":16.79,"tF":62.22, "is_valid":true},"temperature_status":"Normal","ext_sensors":{},"ext_temperature":{},"ext_humidity":{},"update":{"status":"pending","has_update":true,"new_version":"20221108-153925/v1.12.1-1PM-fix-g2821131","old_version":"20220209-094317/v1.11.8-g8c7bb8d"},"ram_total":50456,"ram_free":37056,"fs_size":233681,"fs_free":149094,"uptime":17284290}`
require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
assert.Equal(t, 107875.21666666666, gen1Energy("SHSW-PM", res.Meters[0].Total))
g := &gen1{model: "SHSW-PM"}
assert.Equal(t, 107875.21666666666, g.energy(res.Meters[0].Total))
assert.Equal(t, 4711.12, res.Meters[0].Power)
}
@ -28,7 +29,8 @@ func TestUnmarshalGen1Status(t *testing.T) {
jsonstr := `{"wifi_sta":{"connected":true,"ssid":"XXXX-WLAN","ip":"192.168.178.XXX","rssi":-57},"cloud":{"enabled":false,"connected":false},"mqtt":{"connected":false},"time":"19:25","unixtime":1676139913,"serial":959,"has_update":true,"mac":"E8DB8XXXXXX","cfg_changed_cnt":1,"actions_stats":{"skipped":0},"relays":[{"ison":false,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"source":"timer"}],"meters":[{"power":81.5,"is_valid":true}],"inputs":[{"input":0,"event":"","event_cnt":0}],"ext_sensors":{},"ext_temperature":{},"ext_humidity":{},"update":{"status":"pending","has_update":true,"new_version":"20221027-091427/v1.12.1-ga9117d3","old_version":"20211109-124958/v1.11.7-g682a0db"},"ram_total":50880,"ram_free":38796,"fs_size":233681,"fs_free":151102,"uptime":20319391}`
require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
assert.Equal(t, 0.0, gen1Energy("SHSW-1", res.Meters[0].Total))
g := &gen1{model: "SHSW-1"}
assert.Equal(t, 0.0, g.energy(res.Meters[0].Total))
assert.Equal(t, 81.5, res.Meters[0].Power)
}
@ -39,7 +41,8 @@ func TestUnmarshalGen1Status(t *testing.T) {
jsonstr := `{"wifi_sta":{"connected":true,"ssid":"XXXX","ip":"192.168.178.XXX","rssi":-55},"cloud":{"enabled":false,"connected":false},"mqtt":{"connected":false},"time":"11:16","unixtime":1676110566,"serial":21580,"has_update":false,"mac":"C45BXXXXX","cfg_changed_cnt":0,"actions_stats":{"skipped":0},"relays":[{"ison":false,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"overpower":false,"is_valid":true,"source":"input"}],"emeters":[{"power":-620.34,"reactive":714.48,"pf":-0.66,"voltage":235.68,"is_valid":true,"total":401472.9,"total_returned":653673.7},{"power":0.00,"reactive":0.00,"pf":0.00,"voltage":235.68,"is_valid":true,"total":173411.3,"total_returned":294.2}],"update":{"status":"idle","has_update":false,"new_version":"20221027-105518/v1.12.1-ga9117d3","old_version":"20221027-105518/v1.12.1-ga9117d3"},"ram_total":51072,"ram_free":35660,"fs_size":233681,"fs_free":156373,"uptime":2226140}`
require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
assert.Equal(t, 401472.9, gen1Energy("SHEM", res.EMeters[0].Total))
g := &gen1{model: "SHEM"}
assert.Equal(t, 401472.9, g.energy(res.EMeters[0].Total))
assert.Equal(t, -620.34, res.EMeters[0].Power)
}
}

View file

@ -1,5 +1,17 @@
package shelly
import (
"fmt"
"net/http"
"slices"
"strconv"
"time"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/jpfielding/go-http-digest/pkg/digest"
)
// Gen2API endpoint reference: https://shelly-api-docs.shelly.cloud/gen2/
type Gen2RpcPost struct {
@ -51,32 +63,239 @@ type Gen2EM1Data struct {
TotalActRetEnergy float64 `json:"total_act_ret_energy"`
}
type Gen2StatusResponse struct {
Switch0 Gen2SwitchStatus `json:"switch:0"`
Switch1 Gen2SwitchStatus `json:"switch:1"`
Switch2 Gen2SwitchStatus `json:"switch:2"`
Pm0 Gen2SwitchStatus `json:"pm1:0"`
Pm1 Gen2SwitchStatus `json:"pm2:1"`
Pm2 Gen2SwitchStatus `json:"pm3:2"`
// additional shelly Pro EM meter JSON response
TotalPower float64 `json:"total_act_power"`
CurrentA float64 `json:"a_current"`
CurrentB float64 `json:"b_current"`
CurrentC float64 `json:"c_current"`
VoltageA float64 `json:"a_voltage"`
VoltageB float64 `json:"b_voltage"`
VoltageC float64 `json:"c_voltage"`
PowerA float64 `json:"a_act_power"`
PowerB float64 `json:"b_act_power"`
PowerC float64 `json:"c_act_power"`
Em0 Gen2EM1Status `json:"em1:0"`
Em1 Gen2EM1Status `json:"em1:1"`
Em2 Gen2EM1Status `json:"em1:2"`
Em0Data Gen2EM1Data `json:"em1data:0"`
Em1Data Gen2EM1Data `json:"em1data:1"`
Em2Data Gen2EM1Data `json:"em1data:2"`
var _ Generation = (*gen2)(nil)
type gen2 struct {
*request.Helper
uri string
channel int
model string
profile string
switchstatus util.Cacheable[Gen2SwitchStatus]
em1status func() (Gen2EM1Status, error)
em1data func() (Gen2EM1Data, error)
emstatus func() (Gen2EMStatus, error)
emdata func() (Gen2EMData, error)
}
type Gen2EmDataStatusResponse struct {
TotalEnergy float64 `json:"total_act"`
func apiCall[T any](c *gen2, api string) func() (T, error) {
return func() (T, error) {
var res T
if err := c.execCmd(fmt.Sprintf("%s?id=%d", api, c.channel), false, &res); err != nil {
return res, err
}
return res, nil
}
}
// gen2InitApi initializes the connection to the shelly gen2+ api and sets up the cached gen2SwitchStatus, gen2EM1Status and gen2EMStatus
func newGen2(helper *request.Helper, uri, model, profile string, channel int, user, password string, cache time.Duration) *gen2 {
c := &gen2{
Helper: helper,
uri: uri,
channel: channel,
model: model,
profile: profile,
}
// 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)
}
c.switchstatus = util.ResettableCached(apiCall[Gen2SwitchStatus](c, "Switch.GetStatus"), cache)
c.em1status = util.Cached(apiCall[Gen2EM1Status](c, "EM1.GetStatus"), cache)
c.em1data = util.Cached(apiCall[Gen2EM1Data](c, "EM1Data.GetStatus"), cache)
c.emstatus = util.Cached(apiCall[Gen2EMStatus](c, "EM.GetStatus"), cache)
c.emdata = util.Cached(apiCall[Gen2EMData](c, "EMData.GetStatus"), cache)
return c
}
// execCmd executes a shelly api gen2+ command and provides the response
func (c *gen2) execCmd(method string, enable bool, res any) 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: 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)
}
// CurrentPower implements the api.Meter interface
func (c *gen2) CurrentPower() (float64, error) {
switch {
// Endpoint Switch.GetStatus
case c.hasSwitchEndpoint():
res, err := c.switchstatus.Get()
return res.Apower, err
// Endpoint EM1.GetStatus
case c.hasEMEndpoint() && c.profile == "monophase":
res, err := c.em1status()
return res.ActPower, err
// Endpoint EM.GetStatus
case c.hasEMEndpoint() && c.profile == "triphase":
res, err := c.emstatus()
return res.TotalActPower, err
default:
return 0, fmt.Errorf("unknown shelly model: %s", c.model)
}
}
// Gen2Enabled implements the Gen2 api.Charger interface
func (c *gen2) Enabled() (bool, error) {
if c.hasSwitchEndpoint() {
res, err := c.switchstatus.Get()
return res.Output, err
}
return false, fmt.Errorf("unknown shelly model: %s", c.model)
}
// Gen2Enable implements the api.Charger interface
func (c *gen2) Enable(enable bool) error {
var res Gen2SwitchStatus
c.switchstatus.Reset()
return c.execCmd("Switch.Set?id="+strconv.Itoa(c.channel), enable, &res)
}
// TotalEnergy implements the api.Meter interface
func (c *gen2) TotalEnergy() (float64, error) {
switch {
case c.hasSwitchEndpoint():
res, err := c.switchstatus.Get()
return res.Aenergy.Total / 1000, err
case c.hasEMEndpoint() && c.profile == "monophase":
res, err := c.em1data()
return res.TotalActEnergy / 1000, err
case c.hasEMEndpoint() && c.profile == "triphase":
res, err := c.emdata()
return res.TotalAct / 1000, err
default:
return 0, fmt.Errorf("unknown shelly model: %s", c.model)
}
}
// Currents implements the api.PhaseCurrents interface
func (c *gen2) Currents() (float64, float64, float64, error) {
switch {
case c.hasSwitchEndpoint():
res, err := c.switchstatus.Get()
return res.Current, 0, 0, err
case c.hasEMEndpoint() && c.profile == "monophase":
res, err := c.em1status()
return res.Current, 0, 0, err
case c.hasEMEndpoint() && c.profile == "triphase":
res, err := c.emstatus()
return res.ACurrent, res.BCurrent, res.CCurrent, err
default:
return 0, 0, 0, fmt.Errorf("unknown shelly model: %s", c.model)
}
}
// Voltages implements the api.PhaseVoltages interface
func (c *gen2) Voltages() (float64, float64, float64, error) {
switch {
case c.hasSwitchEndpoint():
res, err := c.switchstatus.Get()
return res.Voltage, 0, 0, err
case c.hasEMEndpoint() && c.profile == "monophase":
res, err := c.em1status()
return res.Voltage, 0, 0, err
case c.hasEMEndpoint() && c.profile == "triphase":
res, err := c.emstatus()
return res.AVoltage, res.BVoltage, res.CVoltage, err
default:
return 0, 0, 0, fmt.Errorf("unknown shelly model: %s", c.model)
}
}
// Powers implements the api.PhasePowers interface
func (c *gen2) Powers() (float64, float64, float64, error) {
switch {
case c.hasSwitchEndpoint():
res, err := c.switchstatus.Get()
return res.Apower, 0, 0, err
case c.hasEMEndpoint() && c.profile == "monophase":
res, err := c.em1status()
return res.ActPower, 0, 0, err
case c.hasEMEndpoint() && c.profile == "triphase":
res, err := c.emstatus()
return res.AActPower, res.BActPower, res.CActPower, err
default:
return 0, 0, 0, fmt.Errorf("unknown shelly model: %s", c.model)
}
}
// Gen2+ models using Switch.GetStatus endpoint https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch#switchgetstatus-example
func (c *gen2) hasSwitchEndpoint() bool {
// Generation 2 Devices (Plus Series):
// - SNSW-001X16EU: Shelly Plus 1 with 1x relay
// - SNSW-001P16EU: Shelly Plus 1PM with 1x relay + power meter
// - SNSW-002P16EU: Shelly Plus 2PM with 2x relay + power meter
// - SNPL-00112EU: Shelly Plus Plug S (EU)
// - SNPL-00110IT: Shelly Plus Plug S (Italy)
// - SNPL-00112UK: Shelly Plus Plug S (UK)
// - SNPL-00116US: Shelly Plus Plug S (US)
// Generation 2 Devices (Pro Series - Hutschiene):
// - SPSW-001XE16EU: Shelly Pro 1 with 1x relay
// - SPSW-001PE16EU: Shelly Pro 1 PM with 1x relay + power meter
// - SPSW-002XE16EU: Shelly Pro 2 with 2x relay
// - SPSW-002PE16EU: Shelly Pro 2 PM with 2x relay + power meter
// - SPSW-003XE16EU: Shelly Pro 3 with 3x relay
// - SPSW-004PE16EU: Shelly Pro 4 PM with 4x relay + power meter
// Generation 3 Devices:
// - S3SW-001P16EU: Shelly 1PM Gen3 with 1x relay + power meter
// Generation 4 Devices:
// - S4SW-001X8EU: Shelly 1 Mini Gen4 with 1x relay
// - S4SW-001P8EU: Shelly 1PM Mini Gen4 with 1x relay + power meter
// - S4SW-001P16EU: Shelly 1PM Gen4 with 1x relay + power meter
switchModels := []string{"SNSW", "SNPL", "SPSW", "S3SW", "S4SW"}
return slices.Contains(switchModels, c.model)
}
// Gen2+ models using EM1.GetStatus endpoint for power and EM1Data.GetStatus for energy
// https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM1#em1getstatus-example
// https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM1Data#em1datagetstatus-example
func (c *gen2) hasEMEndpoint() bool {
// Generation 2 Devices (Pro Series - Hutschiene):
// - SPEM-002CEBEU50: Shelly Pro EM 50 with 1x relay + 2x energy meter
// - SPEM-003CEBEU120: Shelly Pro 3EM with 3x energy meter
// - SPEM-003CEBEU63: Shelly Pro 3EM-3CT63 with 3x energy meter
// Generation 3 Devices:
// - S3EM-002CXCEU: Shelly EM Gen 3 with 1x relay + 2x energy meter
// - S3EM-001XCEU: Shelly 3EM Gen 3 with 3x energy meter
// - S3EM-003CXCEU63: Shelly 3EM Gen 3 with 3x energy meter
// Generation 4 Devices:
// - S4EM-001PXCEU16: Shelly EM Mini Gen4 with power meter
em1Models := []string{"SPEM", "S3EM", "S4EM"}
return slices.Contains(em1Models, c.model)
}

View file

@ -11,70 +11,30 @@ import (
// Test Gen2+ status responses
func TestUnmarshalGen2StatusResponse(t *testing.T) {
{
// Shelly Pro 1PM channel 0 (1)
var res Gen2StatusResponse
jsonstr := `{"ble":{},"cloud":{"connected":true},"eth":{"ip":null},"input:0":{"id":0,"state":false},"input:1":{"id":1,"state":false},"mqtt":{"connected":false},"switch:0":{"id":0, "source":"HTTP", "output":false, "apower":47.11, "voltage":232.0, "current":0.000, "pf":0.00, "aenergy":{"total":5.125,"by_minute":[0.000,0.000,0.000],"minute_ts":1675718520},"temperature":{"tC":25.3, "tF":77.5}},"sys":{"mac":"30C6F78BB4D8","restart_required":false,"time":"22:22","unixtime":1675718522,"uptime":45070,"ram_size":234204,"ram_free":137716,"fs_size":524288,"fs_free":172032,"cfg_rev":13,"kvs_rev":1,"schedule_rev":0,"webhook_rev":0,"available_updates":{"beta":{"version":"0.13.0-beta3"}}},"wifi":{"sta_ip":"192.168.178.64","status":"got ip","ssid":"***","rssi":-62},"ws":{"connected":false}}`
// Switch.GetStatus Endpoint
var res Gen2SwitchStatus
jsonstr := `{"id":0, "source":"HTTP", "output":false, "apower":47.11, "voltage":232.0, "current":0.000, "pf":0.00, "aenergy":{"total":5.125,"by_minute":[0.000,0.000,0.000],"minute_ts":1675718520},"temperature":{"tC":25.3, "tF":77.5}}`
require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
assert.Equal(t, 5.125, res.Switch0.Aenergy.Total)
assert.Equal(t, 47.11, res.Switch0.Apower)
assert.Equal(t, 5.125, res.Aenergy.Total)
assert.Equal(t, 47.11, res.Apower)
}
{
// Shelly PM Mini Gen3 channel 0 (1)
var res Gen2StatusResponse
jsonstr := `{"ble":{},"cloud":{"connected":true},"mqtt":{"connected":false},"pm1:0":{"id":0, "voltage":239.9, "current":7.434, "apower":1780.1 ,"freq":50.1,"aenergy":{"total":3551.682,"by_minute":[15234.772,29611.247,29825.821],"minute_ts":1719917850},"ret_aenergy":{"total":0.000,"by_minute":[0.000,0.000,0.000],"minute_ts":1719917850}},"sys":{"mac":"84FCE638D818","restart_required":false,"time":"12:57","unixtime":1719917851,"uptime":62328,"ram_size":261744,"ram_free":151436,"fs_size":1048576,"fs_free":712704,"cfg_rev":10,"kvs_rev":1,"schedule_rev":0,"webhook_rev":0,"available_updates":{"stable":{"version":"1.3.3"}},"reset_reason":1},"wifi":{"sta_ip":"192.168.178.89","status":"got ip","ssid":"FritzBox 8 2.4","rssi":-62},"ws":{"connected":false}}`
// EM1.GetStatus Endpoint
var res Gen2EM1Status
jsonstr := `{"id":"0","current":1.473,"voltage":226.9,"act_power":-332.2,"aprt_power":335,"pf":0.99,"freq":50,"calibration":"factory"}`
require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
assert.Equal(t, 3551.682, res.Pm0.Aenergy.Total)
assert.Equal(t, 1780.1, res.Pm0.Apower)
assert.Equal(t, -332.2, res.ActPower)
assert.Equal(t, 1.473, res.Current)
assert.Equal(t, 226.9, res.Voltage)
}
{
// Shelly Pro 3EM
var res Gen2StatusResponse
jsonstr := `{"ble":{},"bthome":{"errors":["bluetooth_disabled"]},"cloud":{"connected":true},"em1:0":{"id":0,"current":3.705,"voltage":242.8,"act_power":598.9,"aprt_power":900.6,"pf":0.66,"freq":50.0,"calibration":"factory"},"em1:1":{"id":1,"current":0.194,"voltage":242.8,"act_power":0.0,"aprt_power":47.2,"pf":0.00,"freq":50.0,"calibration":"factory"},"em1:2":{"id":2,"current":0.027,"voltage":242.8,"act_power":0.0,"aprt_power":6.6,"pf":0.00,"freq":50.0,"calibration":"factory"},"em1data:0":{"id":0,"total_act_energy":3458.24,"total_act_ret_energy":1605.24},"em1data:1":{"id":1,"total_act_energy":2768.67,"total_act_ret_energy":25.49},"em1data:2":{"id":2,"total_act_energy":3.09,"total_act_ret_energy":0.71},"eth":{"ip":null},"modbus":{},"mqtt":{"connected":false},"sys":{"mac":"FCE8C0DBA850","restart_required":false,"time":"19:46","unixtime":1731404780,"uptime":563,"ram_size":247148,"ram_free":110596,"fs_size":524288,"fs_free":176128,"cfg_rev":21,"kvs_rev":0,"schedule_rev":3,"webhook_rev":1,"available_updates":{},"reset_reason":3},"temperature:0":{"id": 0,"tC":39.0, "tF":102.2},"wifi":{"sta_ip":"192.168.40.174","status":"got ip","ssid":"IoT","rssi":-67},"ws":{"connected":false}}`
// EM1Data.GetStatus Endpoint
var res Gen2EM1Data
jsonstr := `{"id":"0","total_act_energy":1264.15,"total_act_ret_energy":144792.28}`
require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
// Channel 0 (1)
assert.Equal(t, 598.9, res.Em0.ActPower)
assert.Equal(t, 3.705, res.Em0.Current)
assert.Equal(t, 242.8, res.Em0.Voltage)
assert.Equal(t, 3458.24, res.Em0Data.TotalActEnergy)
assert.Equal(t, 1605.24, res.Em0Data.TotalActRetEnergy)
// Channel 1 (2)
assert.Equal(t, 0.0, res.Em1.ActPower)
assert.Equal(t, 0.194, res.Em1.Current)
assert.Equal(t, 242.8, res.Em1.Voltage)
assert.Equal(t, 2768.67, res.Em1Data.TotalActEnergy)
assert.Equal(t, 25.49, res.Em1Data.TotalActRetEnergy)
// Channel 2 (3)
assert.Equal(t, 0.0, res.Em2.ActPower)
assert.Equal(t, 0.027, res.Em2.Current)
assert.Equal(t, 242.8, res.Em2.Voltage)
assert.Equal(t, 3.09, res.Em2Data.TotalActEnergy)
assert.Equal(t, 0.71, res.Em2Data.TotalActRetEnergy)
}
{
// Shelly Pro EM-50 channel 0 + 1
var res Gen2StatusResponse
jsonstr := `{"ble":{},"bthome":{"errors":["bluetooth_disabled"]},"cloud":{"connected":true},"em1:0":{"id":0,"current":1.473,"voltage":226.9,"act_power":-332.2,"aprt_power":335.0,"pf":0.99,"freq":50.0,"calibration":"factory"},"em1:1":{"id":1,"current":0.428,"voltage":227.0,"act_power":-38.5,"aprt_power":97.4,"pf":0.38,"freq":50.0,"calibration":"factory"},"em1data:0":{"id":0,"total_act_energy":1264.15,"total_act_ret_energy":144792.28},"em1data:1":{"id":1,"total_act_energy":48002.83,"total_act_ret_energy":33241.59},"eth":{"ip":null},"modbus":{},"mqtt":{"connected":false},"switch:0":{"id":0, "source":"HTTP_in", "output":false,"temperature":{"tC":46.4, "tF":115.5}},"sys":{"mac":"08F9E0E8AF2C","restart_required":false,"time":"10:42","unixtime":1742809323,"uptime":3671372,"ram_size":249680,"ram_free":107492,"fs_size":524288,"fs_free":188416,"cfg_rev":12,"kvs_rev":0,"schedule_rev":1,"webhook_rev":0,"available_updates":{"beta":{"version":"1.5.1-beta2"}},"reset_reason":3},"wifi":{"sta_ip":"192.168.1.120","status":"got ip","ssid":"Spaetzlewerk","rssi":-61},"ws":{"connected":false}}`
require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
// Channel 0 (1)
assert.Equal(t, -332.2, res.Em0.ActPower)
assert.Equal(t, 1.473, res.Em0.Current)
assert.Equal(t, 226.9, res.Em0.Voltage)
assert.Equal(t, 1264.15, res.Em0Data.TotalActEnergy)
assert.Equal(t, 144792.28, res.Em0Data.TotalActRetEnergy)
// Channel 1 (2)
assert.Equal(t, -38.5, res.Em1.ActPower)
assert.Equal(t, 0.428, res.Em1.Current)
assert.Equal(t, 227.0, res.Em1.Voltage)
assert.Equal(t, 48002.83, res.Em1Data.TotalActEnergy)
assert.Equal(t, 33241.59, res.Em1Data.TotalActRetEnergy)
assert.Equal(t, 1264.15, res.TotalActEnergy)
assert.Equal(t, 144792.28, res.TotalActRetEnergy)
}
}

View file

@ -1,147 +0,0 @@
package shelly
import (
"errors"
"fmt"
"math"
"strings"
)
type Switch struct {
conn *Connection
}
func NewSwitch(conn *Connection) *Switch {
res := &Switch{
conn: conn,
}
return res
}
// CurrentPower implements the api.Meter interface
func (sh *Switch) CurrentPower() (float64, error) {
var power float64
switch sh.conn.gen {
case 0, 1:
var res Gen1Status
uri := fmt.Sprintf("%s/status", sh.conn.uri)
if err := sh.conn.GetJSON(uri, &res); err != nil {
return 0, err
}
switch {
case sh.conn.channel < len(res.Meters):
power = res.Meters[sh.conn.channel].Power
case sh.conn.channel < len(res.EMeters):
power = res.EMeters[sh.conn.channel].Power
default:
return 0, errors.New("invalid channel, missing power meter")
}
default:
var res Gen2StatusResponse
if err := sh.conn.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
return 0, err
}
switch sh.conn.channel {
case 1:
power = res.Switch1.Apower + res.Pm1.Apower + res.Em1.ActPower
case 2:
power = res.Switch2.Apower + res.Pm2.Apower + res.Em2.ActPower
default:
power = res.Switch0.Apower + res.Pm0.Apower + res.Em0.ActPower
}
}
// Assure positive power response (Gen 1 EM devices can provide negative values)
return math.Abs(power), nil
}
// Enabled implements the api.Charger interface
func (sh *Switch) Enabled() (bool, error) {
switch sh.conn.gen {
case 0, 1:
var res Gen1SwitchResponse
uri := fmt.Sprintf("%s/relay/%d", sh.conn.uri, sh.conn.channel)
err := sh.conn.GetJSON(uri, &res)
return res.Ison, err
default:
var res Gen2SwitchStatus
err := sh.conn.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"}
switch sh.conn.gen {
case 0, 1:
var res Gen1SwitchResponse
uri := fmt.Sprintf("%s/relay/%d?turn=%s", sh.conn.uri, sh.conn.channel, onoff[enable])
err = sh.conn.GetJSON(uri, &res)
default:
var res Gen2SwitchStatus
err = sh.conn.execGen2Cmd("Switch.Set", enable, &res)
}
return err
}
// TotalEnergy implements the api.Meter interface
func (sh *Switch) TotalEnergy() (float64, error) {
var energy float64
switch sh.conn.gen {
case 0, 1:
var res Gen1Status
uri := fmt.Sprintf("%s/status", sh.conn.uri)
if err := sh.conn.GetJSON(uri, &res); err != nil {
return 0, err
}
switch {
case sh.conn.channel < len(res.Meters):
energy = res.Meters[sh.conn.channel].Total
case sh.conn.channel < len(res.EMeters):
energy = res.EMeters[sh.conn.channel].Total
default:
return 0, errors.New("invalid channel, missing power meter")
}
energy = gen1Energy(sh.conn.model, energy)
default:
var res Gen2StatusResponse
if err := sh.conn.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
return 0, err
}
switch sh.conn.channel {
case 1:
energy = res.Switch1.Aenergy.Total + res.Pm1.Aenergy.Total + res.Em1Data.TotalActEnergy - res.Em1Data.TotalActRetEnergy
case 2:
energy = res.Switch2.Aenergy.Total + res.Pm2.Aenergy.Total + res.Em2Data.TotalActEnergy - res.Em2Data.TotalActRetEnergy
default:
energy = res.Switch0.Aenergy.Total + res.Pm0.Aenergy.Total + res.Em0Data.TotalActEnergy - res.Em0Data.TotalActRetEnergy
}
}
return energy / 1000, nil
}
// gen1Energy in kWh
func gen1Energy(devicetype string, energy float64) float64 {
// Gen 1 Shelly EM devices are providing Watt hours, Gen 1 Shelly PM devices are providing Watt minutes
if !strings.Contains(devicetype, "EM") {
energy /= 60
}
return energy
}

111
meter/shelly_decorators.go Normal file
View file

@ -0,0 +1,111 @@
package meter
// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
import (
"github.com/evcc-io/evcc/api"
)
func decorateShelly(base *Shelly, phaseVoltages func() (float64, float64, float64, error), phaseCurrents func() (float64, float64, float64, error), phasePowers func() (float64, float64, float64, error)) api.Meter {
switch {
case phaseCurrents == nil && phaseVoltages == nil:
return base
case phaseCurrents == nil && phaseVoltages != nil:
return &struct {
*Shelly
api.PhaseVoltages
}{
Shelly: base,
PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
case phaseCurrents != nil && phasePowers == nil && phaseVoltages == nil:
return &struct {
*Shelly
api.PhaseCurrents
}{
Shelly: base,
PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
}
case phaseCurrents != nil && phasePowers == nil && phaseVoltages != nil:
return &struct {
*Shelly
api.PhaseCurrents
api.PhaseVoltages
}{
Shelly: base,
PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
case phaseCurrents != nil && phasePowers != nil && phaseVoltages == nil:
return &struct {
*Shelly
api.PhaseCurrents
api.PhasePowers
}{
Shelly: base,
PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
PhasePowers: &decorateShellyPhasePowersImpl{
phasePowers: phasePowers,
},
}
case phaseCurrents != nil && phasePowers != nil && phaseVoltages != nil:
return &struct {
*Shelly
api.PhaseCurrents
api.PhasePowers
api.PhaseVoltages
}{
Shelly: base,
PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
PhasePowers: &decorateShellyPhasePowersImpl{
phasePowers: phasePowers,
},
PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
}
return nil
}
type decorateShellyPhaseCurrentsImpl struct {
phaseCurrents func() (float64, float64, float64, error)
}
func (impl *decorateShellyPhaseCurrentsImpl) Currents() (float64, float64, float64, error) {
return impl.phaseCurrents()
}
type decorateShellyPhasePowersImpl struct {
phasePowers func() (float64, float64, float64, error)
}
func (impl *decorateShellyPhasePowersImpl) Powers() (float64, float64, float64, error) {
return impl.phasePowers()
}
type decorateShellyPhaseVoltagesImpl struct {
phaseVoltages func() (float64, float64, float64, error)
}
func (impl *decorateShellyPhaseVoltagesImpl) Voltages() (float64, float64, float64, error) {
return impl.phaseVoltages()
}

View file

@ -1,33 +0,0 @@
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-energymeter", NewShellyEnergyMeterFromConfig)
}
// NewShellyFromConfig creates a Shelly charger from generic config
func NewShellyEnergyMeterFromConfig(other map[string]interface{}) (api.Meter, error) {
var cc struct {
URI string
User string
Password string
Channel int
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
conn, err := shelly.NewConnection(cc.URI, cc.User, cc.Password, cc.Channel)
if err != nil {
return nil, err
}
return shelly.NewEnergyMeter(conn), nil
}

View file

@ -21,4 +21,5 @@ render: |
{{- if .password }}
password: {{ .password }}
{{- end }}
usage: {{ .usage }}
channel: {{ .channel }} # shelly device relay channel

View file

@ -10,7 +10,7 @@ params:
- name: user
- name: password
render: |
type: shelly-energymeter
type: shelly
uri: http://{{ .host }} # shelly device ip address (local)
{{- if .user }}
user: {{ .user }}
@ -18,4 +18,5 @@ render: |
{{- if .password }}
password: {{ .password }}
{{- end }}
usage: {{ .usage }}
channel: 0 # shelly device relay channel

View file

@ -72,15 +72,12 @@ const teslaloggerMiddleware = (req, res, next) => {
};
const shellyMiddleware = (req, res, next) => {
// simulate a shelly gen1 device api. implement power and energy
// simulate a shelly gen2 device api. implement power and energy
if (req.method === "GET" && req.originalUrl === "/shelly") {
res.end(JSON.stringify({ gen: 2 }));
} else if (req.originalUrl === "/rpc/Shelly.GetStatus") {
res.end(JSON.stringify({ model: "SNSW-001P16EU", gen: 2 }));
} else if (req.originalUrl === "/rpc/Switch.GetStatus?id=0") {
res.end(
JSON.stringify({
"switch:0": { apower: state.site.pv.power, aenergy: { total: state.site.pv.energy } },
})
);
JSON.stringify({ apower: state.site.pv.power, aenergy: { total: state.site.pv.energy } }));
} else {
next();
}