From bacf49fd12473fe56bbf86c70cfbb3e9eb978f5c Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 23 Apr 2025 14:58:01 +0200 Subject: [PATCH] Shelly: use methods instead of models (#20802) --- meter/shelly/connection.go | 12 ++-- meter/shelly/gen2.go | 109 +++++++++++++++---------------------- 2 files changed, 50 insertions(+), 71 deletions(-) diff --git a/meter/shelly/connection.go b/meter/shelly/connection.go index 01dc0a3cf..180614023 100644 --- a/meter/shelly/connection.go +++ b/meter/shelly/connection.go @@ -53,11 +53,6 @@ func NewConnection(uri, user, password string, channel int, cache time.Duration) return nil, 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" - } - model := strings.Split(resp.Type+resp.Model, "-")[0] client.Transport = request.NewTripper(log, transport.Insecure()) @@ -73,7 +68,12 @@ func NewConnection(uri, user, password string, channel int, cache time.Duration) } else { // Shelly GEN 2+ API // https://shelly-api-docs.shelly.cloud/gen2/ - gen = newGen2(client, uri, model, resp.Profile, channel, user, password, cache) + + var err error + gen, err = newGen2(client, uri, model, channel, user, password, cache) + if err != nil { + return nil, err + } } conn := &Connection{gen} diff --git a/meter/shelly/gen2.go b/meter/shelly/gen2.go index 5264880b4..dc6d9d726 100644 --- a/meter/shelly/gen2.go +++ b/meter/shelly/gen2.go @@ -21,6 +21,10 @@ type Gen2RpcPost struct { Method string `json:"method"` } +type Gen2Methods struct { + Methods []string +} + type Gen2SwitchStatus struct { Output bool Apower float64 @@ -70,7 +74,7 @@ type gen2 struct { uri string channel int model string - profile string + methods []string switchstatus util.Cacheable[Gen2SwitchStatus] em1status func() (Gen2EM1Status, error) em1data func() (Gen2EM1Data, error) @@ -89,18 +93,18 @@ func apiCall[T any](c *gen2, api string) func() (T, error) { } // 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, - } - +func newGen2(helper *request.Helper, uri, model string, channel int, user, password string, cache time.Duration) (*gen2, error) { // Shelly GEN 2+ API // https://shelly-api-docs.shelly.cloud/gen2/ - c.uri = fmt.Sprintf("%s/rpc", util.DefaultScheme(uri, "http")) + c := &gen2{ + Helper: helper, + uri: fmt.Sprintf("%s/rpc", util.DefaultScheme(uri, "http")), + channel: channel, + model: model, + } + + // Shelly gen 2 rfc7616 authentication + // https://shelly-api-docs.shelly.cloud/gen2/General/Authentication if user != "" { c.Client.Transport = digest.NewTransport(user, password, c.Client.Transport) } @@ -111,15 +115,18 @@ func newGen2(helper *request.Helper, uri, model, profile string, channel int, us c.emstatus = util.Cached(apiCall[Gen2EMStatus](c, "EM.GetStatus"), cache) c.emdata = util.Cached(apiCall[Gen2EMData](c, "EMData.GetStatus"), cache) - return c + var res Gen2Methods + if err := c.execCmd("Shelly.ListMethods", false, &res); err != nil { + return nil, err + } + + c.methods = res.Methods + + return c, nil } // 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, @@ -138,18 +145,15 @@ func (c *gen2) execCmd(method string, enable bool, res any) error { // 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": + case c.hasEM1Endpoint(): res, err := c.em1status() return res.ActPower, err - // Endpoint EM.GetStatus - case c.hasEMEndpoint() && c.profile == "triphase": + case c.hasEMEndpoint(): res, err := c.emstatus() return res.TotalActPower, err @@ -182,11 +186,11 @@ func (c *gen2) TotalEnergy() (float64, error) { res, err := c.switchstatus.Get() return res.Aenergy.Total / 1000, err - case c.hasEMEndpoint() && c.profile == "monophase": + case c.hasEM1Endpoint(): res, err := c.em1data() return res.TotalActEnergy / 1000, err - case c.hasEMEndpoint() && c.profile == "triphase": + case c.hasEMEndpoint(): res, err := c.emdata() return res.TotalAct / 1000, err @@ -202,11 +206,11 @@ func (c *gen2) Currents() (float64, float64, float64, error) { res, err := c.switchstatus.Get() return res.Current, 0, 0, err - case c.hasEMEndpoint() && c.profile == "monophase": + case c.hasEM1Endpoint(): res, err := c.em1status() return res.Current, 0, 0, err - case c.hasEMEndpoint() && c.profile == "triphase": + case c.hasEMEndpoint(): res, err := c.emstatus() return res.ACurrent, res.BCurrent, res.CCurrent, err @@ -222,11 +226,11 @@ func (c *gen2) Voltages() (float64, float64, float64, error) { res, err := c.switchstatus.Get() return res.Voltage, 0, 0, err - case c.hasEMEndpoint() && c.profile == "monophase": + case c.hasEM1Endpoint(): res, err := c.em1status() return res.Voltage, 0, 0, err - case c.hasEMEndpoint() && c.profile == "triphase": + case c.hasEMEndpoint(): res, err := c.emstatus() return res.AVoltage, res.BVoltage, res.CVoltage, err @@ -242,11 +246,11 @@ func (c *gen2) Powers() (float64, float64, float64, error) { res, err := c.switchstatus.Get() return res.Apower, 0, 0, err - case c.hasEMEndpoint() && c.profile == "monophase": + case c.hasEM1Endpoint(): res, err := c.em1status() return res.ActPower, 0, 0, err - case c.hasEMEndpoint() && c.profile == "triphase": + case c.hasEMEndpoint(): res, err := c.emstatus() return res.AActPower, res.BActPower, res.CActPower, err @@ -257,45 +261,20 @@ func (c *gen2) Powers() (float64, float64, float64, error) { // 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) + return c.hasMethod("Switch.GetStatus") +} + +func (c *gen2) hasEM1Endpoint() bool { + return c.hasMethod("EM1.GetStatus") +} + +func (c *gen2) hasEMEndpoint() bool { + return c.hasMethod("EM.GetStatus") } // 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) +func (c *gen2) hasMethod(method string) bool { + return slices.Contains(c.methods, method) }