Shelly: split api types (#20445)

This commit is contained in:
Markus Thierolf 2025-04-07 14:28:15 +02:00 • committed by GitHub
parent 61e79f39da
commit 92b8fdded5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 297 additions and 250 deletions

View file

@ -15,12 +15,11 @@ import (
// Connection is the Shelly connection
type Connection struct {
*request.Helper
uri string
channel int
gen int // Shelly api generation
devicetype string // Shelly device type
app string // Shelly device app code
profile string // Shelly device profile
uri string
channel int
gen int // Shelly api generation
model string // Shelly device type
profile string // Shelly device profile
}
// NewConnection creates a new Shelly device connection.
@ -42,39 +41,41 @@ func NewConnection(uri, user, password string, channel int) (*Connection, error)
return nil, err
}
if (resp.Auth || resp.AuthEn) && (user == "" || password == "") {
return nil, fmt.Errorf("%s (%s) missing user/password", resp.Model, resp.Mac)
}
conn := &Connection{
Helper: client,
channel: channel,
gen: resp.Gen,
devicetype: resp.Type,
app: resp.App,
profile: resp.Profile,
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())
if (resp.Auth || resp.AuthEn) && (user == "" || password == "") {
return conn, fmt.Errorf("%s (%s) missing user/password", resp.Model, resp.Mac)
// Set default profile to "monophase" if not provided
if resp.Profile == "" {
resp.Profile = "monophase"
}
switch conn.gen {
case 0, 1:
// Shelly GEN 1 API
// https://shelly-api-docs.shelly.cloud/gen1/#shelly-family-overview
conn.uri = util.DefaultScheme(uri, "http")
if user != "" {
log.Redact(transport.BasicAuthHeader(user, password))
conn.Client.Transport = transport.BasicAuth(user, password, conn.Client.Transport)
}
case 2, 3:
// Shelly GEN 2+ API
// https://shelly-api-docs.shelly.cloud/gen2/
conn.uri = fmt.Sprintf("%s/rpc", util.DefaultScheme(uri, "http"))
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)
}

View file

@ -3,12 +3,12 @@ package shelly
import "github.com/evcc-io/evcc/api"
type EnergyMeter struct {
*Connection
conn *Connection
}
func NewEnergyMeter(conn *Connection) *EnergyMeter {
res := &EnergyMeter{
Connection: conn,
conn: conn,
}
return res
@ -17,7 +17,7 @@ func NewEnergyMeter(conn *Connection) *EnergyMeter {
// CurrentPower implements the api.Meter interface
func (sh *EnergyMeter) CurrentPower() (float64, error) {
var res Gen2StatusResponse
if err := sh.Connection.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
if err := sh.conn.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
return 0, err
}
@ -27,7 +27,7 @@ func (sh *EnergyMeter) CurrentPower() (float64, error) {
// 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 {
if err := sh.conn.execGen2Cmd("EMData.GetStatus", false, &res); err != nil {
return 0, err
}
@ -39,7 +39,7 @@ 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.Connection.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
if err := sh.conn.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
return 0, 0, 0, err
}
@ -51,7 +51,7 @@ 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.Connection.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
if err := sh.conn.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
return 0, 0, 0, err
}
@ -63,7 +63,7 @@ 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.Connection.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
if err := sh.conn.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
return 0, 0, 0, err
}

25
meter/shelly/gen1.go Normal file
View file

@ -0,0 +1,25 @@
package shelly
// Gen1API endpoint reference: https://shelly-api-docs.shelly.cloud/gen1/#shelly-family-overview
type Gen1SwitchResponse struct {
Ison bool
}
type Gen1Status struct {
Meters []struct {
Power float64
Current float64
Voltage float64
Total float64
Total_Returned float64
}
// Shelly EM meter JSON response
EMeters []struct {
Power float64
Current float64
Voltage float64
Total float64
Total_Returned float64
}
}

45
meter/shelly/gen1_test.go Normal file
View file

@ -0,0 +1,45 @@
package shelly
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Test Gen1Status response
func TestUnmarshalGen1Status(t *testing.T) {
{
// Shelly 1 PM channel 0 (1)
var res Gen1Status
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))
assert.Equal(t, 4711.12, res.Meters[0].Power)
}
{
// Shelly 1 channel 0 (1)
var res Gen1Status
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))
assert.Equal(t, 81.5, res.Meters[0].Power)
}
{
// Shelly EM channel 0 (1)
var res Gen1Status
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))
assert.Equal(t, -620.34, res.EMeters[0].Power)
}
}

82
meter/shelly/gen2.go Normal file
View file

@ -0,0 +1,82 @@
package shelly
// Gen2API endpoint reference: https://shelly-api-docs.shelly.cloud/gen2/
type Gen2RpcPost struct {
Id int `json:"id"`
On bool `json:"on"`
Src string `json:"src"`
Method string `json:"method"`
}
type Gen2SwitchStatus struct {
Output bool
Apower float64
Voltage float64
Current float64
Aenergy struct {
Total float64
}
Ret_Aenergy struct {
Total float64
}
}
type Gen2EMStatus struct {
TotalActPower float64 `json:"total_act_power"`
ACurrent float64 `json:"a_current"`
BCurrent float64 `json:"b_current"`
CCurrent float64 `json:"c_current"`
AVoltage float64 `json:"a_voltage"`
BVoltage float64 `json:"b_voltage"`
CVoltage float64 `json:"c_voltage"`
AActPower float64 `json:"a_act_power"`
BActPower float64 `json:"b_act_power"`
CActPower float64 `json:"c_act_power"`
}
type Gen2EMData struct {
TotalAct float64 `json:"total_act"`
TotalActRet float64 `json:"total_act_ret"`
}
type Gen2EM1Status struct {
Current float64 `json:"current"`
Voltage float64 `json:"voltage"`
ActPower float64 `json:"act_power"`
}
type Gen2EM1Data struct {
TotalActEnergy float64 `json:"total_act_energy"`
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"`
}
type Gen2EmDataStatusResponse struct {
TotalEnergy float64 `json:"total_act"`
}

80
meter/shelly/gen2_test.go Normal file
View file

@ -0,0 +1,80 @@
package shelly
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// 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}}`
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)
}
{
// 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}}`
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)
}
{
// 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}}`
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)
}
}

View file

@ -8,12 +8,12 @@ import (
)
type Switch struct {
*Connection
conn *Connection
}
func NewSwitch(conn *Connection) *Switch {
res := &Switch{
Connection: conn,
conn: conn,
}
return res
@ -23,31 +23,30 @@ func NewSwitch(conn *Connection) *Switch {
func (sh *Switch) CurrentPower() (float64, error) {
var power float64
d := sh.Connection
switch d.gen {
switch sh.conn.gen {
case 0, 1:
var res Gen1StatusResponse
uri := fmt.Sprintf("%s/status", d.uri)
if err := d.GetJSON(uri, &res); err != nil {
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 d.channel < len(res.Meters):
power = res.Meters[d.channel].Power
case d.channel < len(res.EMeters):
power = res.EMeters[d.channel].Power
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 := d.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
if err := sh.conn.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
return 0, err
}
switch d.channel {
switch sh.conn.channel {
case 1:
power = res.Switch1.Apower + res.Pm1.Apower + res.Em1.ActPower
case 2:
@ -63,17 +62,16 @@ func (sh *Switch) CurrentPower() (float64, error) {
// Enabled implements the api.Charger interface
func (sh *Switch) Enabled() (bool, error) {
d := sh.Connection
switch d.gen {
switch sh.conn.gen {
case 0, 1:
var res Gen1SwitchResponse
uri := fmt.Sprintf("%s/relay/%d", d.uri, d.channel)
err := d.GetJSON(uri, &res)
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 Gen2SwitchResponse
err := d.execGen2Cmd("Switch.GetStatus", false, &res)
var res Gen2SwitchStatus
err := sh.conn.execGen2Cmd("Switch.GetStatus", false, &res)
return res.Output, err
}
}
@ -83,16 +81,15 @@ func (sh *Switch) Enable(enable bool) error {
var err error
onoff := map[bool]string{true: "on", false: "off"}
d := sh.Connection
switch d.gen {
switch sh.conn.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)
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 Gen2SwitchResponse
err = d.execGen2Cmd("Switch.Set", enable, &res)
var res Gen2SwitchStatus
err = sh.conn.execGen2Cmd("Switch.Set", enable, &res)
}
return err
@ -102,33 +99,32 @@ func (sh *Switch) Enable(enable bool) error {
func (sh *Switch) TotalEnergy() (float64, error) {
var energy float64
d := sh.Connection
switch d.gen {
switch sh.conn.gen {
case 0, 1:
var res Gen1StatusResponse
uri := fmt.Sprintf("%s/status", d.uri)
if err := d.GetJSON(uri, &res); err != nil {
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 d.channel < len(res.Meters):
energy = res.Meters[d.channel].Total
case d.channel < len(res.EMeters):
energy = res.EMeters[d.channel].Total
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(d.devicetype, energy)
energy = gen1Energy(sh.conn.model, energy)
default:
var res Gen2StatusResponse
if err := d.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
if err := sh.conn.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
return 0, err
}
switch d.channel {
switch sh.conn.channel {
case 1:
energy = res.Switch1.Aenergy.Total + res.Pm1.Aenergy.Total + res.Em1Data.TotalActEnergy - res.Em1Data.TotalActRetEnergy
case 2:

View file

@ -1,91 +1,15 @@
package shelly
// Shelly api homepage
// https://shelly-api-docs.shelly.cloud/#common-http-api
// DeviceInfo is the common /shelly endpoint response
// https://shelly-api-docs.shelly.cloud/gen1/#shelly
// https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Shelly#http-endpoint-shelly
type DeviceInfo struct {
Mac string `json:"mac"`
Gen int `json:"gen"`
Id string `json:"id"`
Model string `json:"model"`
Type string `json:"type"`
Mac string `json:"mac"`
App string `json:"app"`
Auth bool `json:"auth"`
AuthEn bool `json:"auth_en"`
NumMeters int `json:"num_meters"`
Profile string `json:"profile"`
}
type Gen2RpcPost struct {
Id int `json:"id"`
On bool `json:"on"`
Src string `json:"src"`
Method string `json:"method"`
}
type Gen2SwitchResponse struct {
Output bool `json:"output"`
}
type Gen2Switch struct {
Apower float64
Aenergy struct {
Total float64
}
}
type Gen2Em struct {
Current float64 `json:"current"`
Voltage float64 `json:"voltage"`
ActPower float64 `json:"act_power"`
}
type Gen2EmData struct {
TotalActEnergy float64 `json:"total_act_energy"`
TotalActRetEnergy float64 `json:"total_act_ret_energy"`
}
type Gen2StatusResponse struct {
Switch0 Gen2Switch `json:"switch:0"`
Switch1 Gen2Switch `json:"switch:1"`
Switch2 Gen2Switch `json:"switch:2"`
Pm0 Gen2Switch `json:"pm1:0"`
Pm1 Gen2Switch `json:"pm2:1"`
Pm2 Gen2Switch `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 Gen2Em `json:"em1:0"`
Em1 Gen2Em `json:"em1:1"`
Em2 Gen2Em `json:"em1:2"`
Em0Data Gen2EmData `json:"em1data:0"`
Em1Data Gen2EmData `json:"em1data:1"`
Em2Data Gen2EmData `json:"em1data:2"`
}
type Gen2EmDataStatusResponse struct {
TotalEnergy float64 `json:"total_act"`
}
type Gen1SwitchResponse struct {
Ison bool
}
type Gen1StatusResponse struct {
Meters []struct {
Power float64
Total float64
}
// Shelly EM meter JSON response
EMeters []struct {
Power float64
Total float64
}
}

View file

@ -8,113 +8,6 @@ import (
"github.com/stretchr/testify/require"
)
// Test Gen1StatusResponse response
func TestUnmarshalGen1StatusResponse(t *testing.T) {
{
// Shelly 1 PM channel 0 (1)
var res Gen1StatusResponse
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))
assert.Equal(t, 4711.12, res.Meters[0].Power)
}
{
// Shelly 1 channel 0 (1)
var res Gen1StatusResponse
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))
assert.Equal(t, 81.5, res.Meters[0].Power)
}
{
// Shelly EM channel 0 (1)
var res Gen1StatusResponse
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))
assert.Equal(t, -620.34, res.EMeters[0].Power)
}
}
// 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}}`
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)
}
{
// 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}}`
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)
}
{
// 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}}`
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)
}
}
// Test Shelly device info
func TestUnmarshalDeviceInfoResponse(t *testing.T) {
{
@ -124,7 +17,8 @@ func TestUnmarshalDeviceInfoResponse(t *testing.T) {
jsonstr := `{"name":null,"id":"shellypro3em-fce8c0dba900","mac":"FCE8C0DBA900","slot":1,"model":"SPEM-003CEBEU","gen":2,"fw_id":"20241011-114455/1.4.4-g6d2a586","ver":"1.4.4","app":"Pro3EM","auth_en":false,"auth_domain":null,"profile":"monophase"}`
require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
assert.Equal(t, "Pro3EM", res.App)
assert.Equal(t, 2, res.Gen)
assert.Equal(t, "SPEM-003CEBEU", res.Model)
assert.Equal(t, "monophase", res.Profile)
}
}