Add Shelly Pro 3 EM (#6457)
This commit is contained in:
parent
892057bb7f
commit
69a37555cd
12 changed files with 358 additions and 174 deletions
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
// Shelly charger implementation
|
||||
type Shelly struct {
|
||||
conn *shelly.Connection
|
||||
conn *shelly.Switch
|
||||
*switchSocket
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
meter/shelly/energymeter.go
Normal file
71
meter/shelly/energymeter.go
Normal file
|
|
@ -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
meter/shelly/switch.go
Normal file
149
meter/shelly/switch.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -37,6 +37,23 @@ type Gen2StatusResponse struct {
|
|||
Switch2 Gen2Switch `json:"switch:2"`
|
||||
}
|
||||
|
||||
type Gen2EmStatusResponse struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
type Gen2EmDataStatusResponse struct {
|
||||
TotalEnergy float64 `json:"total_act"`
|
||||
}
|
||||
|
||||
type Gen1SwitchResponse struct {
|
||||
Ison bool
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,65 +3,54 @@ package shelly
|
|||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test Gen1StatusResponse response
|
||||
func TestUnmarshalGen1StatusResponse(t *testing.T) {
|
||||
var res Gen1StatusResponse
|
||||
{
|
||||
// Shelly 1 PM channel 0 (1)
|
||||
var res Gen1StatusResponse
|
||||
|
||||
// Shelly 1 PM channel 0 (1)
|
||||
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}`
|
||||
if err := json.Unmarshal([]byte(jsonstr), &res); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if GetGen1Energy("SHSW-PM", res.Meters[0].Total) != 107875.21666666666 {
|
||||
t.Error("SHSW-PM res.Meters[0].Total) != 107875.21666666666")
|
||||
}
|
||||
if res.Meters[0].Power != 4711.12 {
|
||||
t.Error("res.Meters[0].Power != 4711.12")
|
||||
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}`
|
||||
assert.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)
|
||||
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}`
|
||||
if err := json.Unmarshal([]byte(jsonstr), &res); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if GetGen1Energy("SHSW-1", res.Meters[0].Total) != 0 {
|
||||
t.Error("SHSW-1 res.Meters[0].Total) != 0")
|
||||
}
|
||||
if res.Meters[0].Power != 81.5 {
|
||||
t.Error("res.Meters[0].Power != -81.5")
|
||||
{
|
||||
// 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}`
|
||||
assert.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)
|
||||
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}`
|
||||
if err := json.Unmarshal([]byte(jsonstr), &res); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if GetGen1Energy("SHEM", res.EMeters[0].Total) != 401472.9 {
|
||||
t.Error("SHEM res.EMeters[0].Total) != 401472.9")
|
||||
}
|
||||
if res.EMeters[0].Power != -620.34 {
|
||||
t.Error("res.EMeters[0].Power != -620.34")
|
||||
}
|
||||
{
|
||||
// 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}`
|
||||
assert.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 Gen2StatusResponse response
|
||||
func TestUnmarshalGen2StatusResponse(t *testing.T) {
|
||||
// Shelly Pro 1PM channel 0 (1)
|
||||
var res Gen2StatusResponse
|
||||
|
||||
// Shelly Pro 1PM channel 0 (1)
|
||||
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}}`
|
||||
if err := json.Unmarshal([]byte(jsonstr), &res); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if res.Switch0.Aenergy.Total != 5.125 {
|
||||
t.Error("res.Switch0.Aenergy.Total != 5.125")
|
||||
}
|
||||
if res.Switch0.Apower != 47.11 {
|
||||
t.Error("res.Switch0.Apower != 47.11")
|
||||
}
|
||||
assert.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
|
||||
|
||||
assert.Equal(t, 5.125, res.Switch0.Aenergy.Total)
|
||||
assert.Equal(t, 47.11, res.Switch0.Apower)
|
||||
}
|
||||
|
|
|
|||
33
meter/shelly_energymeter.go
Normal file
33
meter/shelly_energymeter.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -11,9 +11,7 @@ params:
|
|||
default: 192.0.2.2
|
||||
- name: user
|
||||
- name: password
|
||||
mask: true
|
||||
- name: channel
|
||||
default: 0
|
||||
render: |
|
||||
type: shelly
|
||||
uri: http://{{ .host }} # shelly device ip address (local)
|
||||
|
|
|
|||
18
templates/definition/meter/shelly-pro-3em.yaml
Normal file
18
templates/definition/meter/shelly-pro-3em.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
template: shelly-pro-3em
|
||||
products:
|
||||
- brand: Shelly
|
||||
description:
|
||||
generic: Pro 3 EM
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["grid", "pv", "charge"]
|
||||
- name: host
|
||||
default: 192.0.2.2
|
||||
- name: user
|
||||
- name: password
|
||||
render: |
|
||||
type: shelly-energymeter
|
||||
uri: http://{{ .host }} # shelly device ip address (local)
|
||||
{{ if ne .user "" }}user: {{ .user }}{{ end }}
|
||||
{{ if ne .password "" }}password: {{ .password }}{{ end }}
|
||||
channel: 0 # shelly device relay channel
|
||||
|
|
@ -11,4 +11,4 @@ render:
|
|||
host: 192.0.2.2 # IP-Adresse oder Hostname
|
||||
user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
|
||||
password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional
|
||||
channel: 0 # Optional
|
||||
channel: # Optional
|
||||
|
|
|
|||
28
templates/docs/meter/shelly-pro-3em_0.yaml
Normal file
28
templates/docs/meter/shelly-pro-3em_0.yaml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
product:
|
||||
brand: Shelly
|
||||
description: Pro 3 EM
|
||||
render:
|
||||
- usage: grid
|
||||
default: |
|
||||
type: template
|
||||
template: shelly-pro-3em
|
||||
usage: grid
|
||||
host: 192.0.2.2 # IP-Adresse oder Hostname
|
||||
user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
|
||||
password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional
|
||||
- usage: pv
|
||||
default: |
|
||||
type: template
|
||||
template: shelly-pro-3em
|
||||
usage: pv
|
||||
host: 192.0.2.2 # IP-Adresse oder Hostname
|
||||
user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
|
||||
password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional
|
||||
- usage: charge
|
||||
default: |
|
||||
type: template
|
||||
template: shelly-pro-3em
|
||||
usage: charge
|
||||
host: 192.0.2.2 # IP-Adresse oder Hostname
|
||||
user: # Benutzerkonto (bspw. E-Mail Adresse, User Id, etc.) # Optional
|
||||
password: # Passwort des Benutzerkontos (bei führenden Nullen bitte in einfache Hochkommata setzen) # Optional
|
||||
Loading…
Add table
Add a link
Reference in a new issue