From 69a37555cd257d90943b59496f0d74f1cdabaf2c Mon Sep 17 00:00:00 2001 From: Wilko Date: Mon, 27 Feb 2023 17:48:21 +0100 Subject: [PATCH] Add Shelly Pro 3 EM (#6457) --- charger/shelly.go | 4 +- meter/shelly.go | 7 +- meter/shelly/connection.go | 124 --------------- meter/shelly/energymeter.go | 71 +++++++++ meter/shelly/switch.go | 149 ++++++++++++++++++ meter/shelly/types.go | 17 ++ meter/shelly/types_test.go | 77 ++++----- meter/shelly_energymeter.go | 33 ++++ templates/definition/meter/shelly-1pm.yaml | 2 - .../definition/meter/shelly-pro-3em.yaml | 18 +++ templates/docs/meter/shelly-1pm_0.yaml | 2 +- templates/docs/meter/shelly-pro-3em_0.yaml | 28 ++++ 12 files changed, 358 insertions(+), 174 deletions(-) create mode 100644 meter/shelly/energymeter.go create mode 100644 meter/shelly/switch.go create mode 100644 meter/shelly_energymeter.go create mode 100644 templates/definition/meter/shelly-pro-3em.yaml create mode 100644 templates/docs/meter/shelly-pro-3em_0.yaml diff --git a/charger/shelly.go b/charger/shelly.go index ecc1a7ffc..53b2a904e 100644 --- a/charger/shelly.go +++ b/charger/shelly.go @@ -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) diff --git a/meter/shelly.go b/meter/shelly.go index b5c5622f8..67ac8f8a8 100644 --- a/meter/shelly.go +++ b/meter/shelly.go @@ -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 } diff --git a/meter/shelly/connection.go b/meter/shelly/connection.go index b618da83d..32ebeb1f5 100644 --- a/meter/shelly/connection.go +++ b/meter/shelly/connection.go @@ -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 -} diff --git a/meter/shelly/energymeter.go b/meter/shelly/energymeter.go new file mode 100644 index 000000000..e8d6f9027 --- /dev/null +++ b/meter/shelly/energymeter.go @@ -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 +} diff --git a/meter/shelly/switch.go b/meter/shelly/switch.go new file mode 100644 index 000000000..c7052fc9d --- /dev/null +++ b/meter/shelly/switch.go @@ -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 +} diff --git a/meter/shelly/types.go b/meter/shelly/types.go index 040fed021..229df8851 100644 --- a/meter/shelly/types.go +++ b/meter/shelly/types.go @@ -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 } diff --git a/meter/shelly/types_test.go b/meter/shelly/types_test.go index d2c23b837..007347e14 100644 --- a/meter/shelly/types_test.go +++ b/meter/shelly/types_test.go @@ -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) } diff --git a/meter/shelly_energymeter.go b/meter/shelly_energymeter.go new file mode 100644 index 000000000..090a12bbd --- /dev/null +++ b/meter/shelly_energymeter.go @@ -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 +} diff --git a/templates/definition/meter/shelly-1pm.yaml b/templates/definition/meter/shelly-1pm.yaml index d8b57b9e8..8f6d9101a 100644 --- a/templates/definition/meter/shelly-1pm.yaml +++ b/templates/definition/meter/shelly-1pm.yaml @@ -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) diff --git a/templates/definition/meter/shelly-pro-3em.yaml b/templates/definition/meter/shelly-pro-3em.yaml new file mode 100644 index 000000000..231b3f844 --- /dev/null +++ b/templates/definition/meter/shelly-pro-3em.yaml @@ -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 diff --git a/templates/docs/meter/shelly-1pm_0.yaml b/templates/docs/meter/shelly-1pm_0.yaml index 22d22c44c..f1faa5153 100644 --- a/templates/docs/meter/shelly-1pm_0.yaml +++ b/templates/docs/meter/shelly-1pm_0.yaml @@ -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 diff --git a/templates/docs/meter/shelly-pro-3em_0.yaml b/templates/docs/meter/shelly-pro-3em_0.yaml new file mode 100644 index 000000000..42314407c --- /dev/null +++ b/templates/docs/meter/shelly-pro-3em_0.yaml @@ -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