Skip to content

Commit

Permalink
add suport for shelly3em (V1.0)
Browse files Browse the repository at this point in the history
  • Loading branch information
Marc Ritter committed Dec 4, 2023
1 parent c1dc637 commit 7025ed4
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 23 deletions.
38 changes: 16 additions & 22 deletions discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
TargetTypeShellyPlug = "shellyplug"
TargetTypeShellyPlus = "shellyplus"
TargetTypeShellyPro = "shellypro"
TargetTypeShellyEm3 = "shellyem3"
)

type (
Expand Down Expand Up @@ -63,31 +64,14 @@ func (d *serviceDiscovery) Run(timeout time.Duration) {
for entry := range entriesCh {
switch {
case strings.HasPrefix(strings.ToLower(entry.Name), "shellyplug-"):
d.logger.Debugf(`found %v [%v] via mDNS servicediscovery`, entry.Name, entry.AddrV4.String())
targetList = append(targetList, DiscoveryTarget{
Hostname: entry.Name,
Port: entry.Port,
Address: entry.AddrV4.String(),
Type: TargetTypeShellyPlug,
})
targetList = append(targetList, createDiscoveryTarget(d, TargetTypeShellyPlug, entry))
case strings.HasPrefix(strings.ToLower(entry.Name), "shellyplus"):
d.logger.Debugf(`found %v [%v] via mDNS servicediscovery`, entry.Name, entry.AddrV4.String())
targetList = append(targetList, DiscoveryTarget{
Hostname: entry.Name,
Port: entry.Port,
Address: entry.AddrV4.String(),
Type: TargetTypeShellyPlus,
})
targetList = append(targetList, createDiscoveryTarget(d, TargetTypeShellyPlus, entry))
case strings.HasPrefix(strings.ToLower(entry.Name), "shellypro"):
d.logger.Debugf(`found %v [%v] via mDNS servicediscovery`, entry.Name, entry.AddrV4.String())
targetList = append(targetList, DiscoveryTarget{
Hostname: entry.Name,
Port: entry.Port,
Address: entry.AddrV4.String(),
Type: TargetTypeShellyPro,
})
targetList = append(targetList, createDiscoveryTarget(d, TargetTypeShellyPro, entry))
case strings.HasPrefix(strings.ToLower(entry.Name), "shellyem3"):
targetList = append(targetList, createDiscoveryTarget(d, TargetTypeShellyEm3, entry))
}

}

d.lock.Lock()
Expand Down Expand Up @@ -121,6 +105,16 @@ func (d *serviceDiscovery) Run(timeout time.Duration) {
wg.Wait()
}

func createDiscoveryTarget(d *serviceDiscovery, TargetType string, entry *mdns.ServiceEntry) DiscoveryTarget {
d.logger.Debugf(`found %v [%v] via mDNS servicediscovery`, entry.Name, entry.AddrV4.String())
return DiscoveryTarget{
Hostname: entry.Name,
Port: entry.Port,
Address: entry.AddrV4.String(),
Type: TargetType,
}
}

func (d *serviceDiscovery) MarkTarget(address string, healthy bool) {
d.lock.Lock()
defer d.lock.Unlock()
Expand Down
89 changes: 89 additions & 0 deletions shellyplug/prober.gen1em.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package shellyplug

import (
"fmt"

"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"

"github.com/webdevops/shelly-plug-exporter/discovery"
"github.com/webdevops/shelly-plug-exporter/shellyprober"
)

func (sp *ShellyPlug) collectFromTargetGen1em(target discovery.DiscoveryTarget, logger *log.Entry, infoLabels, targetLabels prometheus.Labels) {
shellyProber := shellyprober.ShellyProberGen1Em{
Target: target,
Client: sp.client,
Ctx: sp.ctx,
Cache: globalCache,
}

if result, err := shellyProber.GetSettings(); err == nil {
if discovery.ServiceDiscovery != nil {
discovery.ServiceDiscovery.MarkTarget(target.Address, discovery.TargetHealthy)
}

targetLabels["plugName"] = result.Name

infoLabels["plugName"] = result.Name
infoLabels["plugModel"] = result.Device.Type

powerLimitLabels := copyLabelMap(targetLabels)
powerLimitLabels["id"] = "emeter:0"
powerLimitLabels["name"] = ""
sp.prometheus.powerLoadLimit.With(powerLimitLabels).Set(result.MaxPower)
} else {
logger.Errorf(`failed to fetch settings: %v`, err)
if discovery.ServiceDiscovery != nil {
discovery.ServiceDiscovery.MarkTarget(target.Address, discovery.TargetUnhealthy)
}
}

sp.prometheus.info.With(infoLabels).Set(1)

if result, err := shellyProber.GetStatus(); err == nil {
sp.prometheus.sysUnixtime.With(targetLabels).Set(float64(result.Unixtime))
sp.prometheus.sysUptime.With(targetLabels).Set(float64(result.Uptime))
sp.prometheus.sysMemTotal.With(targetLabels).Set(float64(result.RAMTotal))
sp.prometheus.sysMemFree.With(targetLabels).Set(float64(result.RAMFree))
sp.prometheus.sysFsSize.With(targetLabels).Set(float64(result.FsSize))
sp.prometheus.sysFsFree.With(targetLabels).Set(float64(result.FsFree))

wifiLabels := copyLabelMap(targetLabels)
wifiLabels["ssid"] = result.WifiSta.Ssid
sp.prometheus.wifiRssi.With(wifiLabels).Set(float64(result.WifiSta.Rssi))

sp.prometheus.updateNeeded.With(targetLabels).Set(boolToFloat64(result.HasUpdate))
sp.prometheus.cloudEnabled.With(targetLabels).Set(boolToFloat64(result.Cloud.Enabled))
sp.prometheus.cloudConnected.With(targetLabels).Set(boolToFloat64(result.Cloud.Connected))

for relayID, powerUsage := range result.Emeters {
powerUsageLabels := copyLabelMap(targetLabels)
powerUsageLabels["id"] = fmt.Sprintf("emeter:%d", relayID)
powerUsageLabels["name"] = targetLabels["plugName"]

sp.prometheus.powerLoadCurrent.With(powerUsageLabels).Set(powerUsage.Power * -1)
// total is provided as watt/minutes, we want watt/hours
powerUsageLabels["direction"] = "in"
sp.prometheus.powerLoadTotal.With(powerUsageLabels).Set(powerUsage.Total / 60)
}

for relayID, relay := range result.Relays {
switchLabels := copyLabelMap(targetLabels)
switchLabels["id"] = fmt.Sprintf("relay:%d", relayID)
switchLabels["name"] = targetLabels["plugName"]

switchOnLabels := copyLabelMap(switchLabels)
switchOnLabels["source"] = relay.Source

sp.prometheus.switchOn.With(switchOnLabels).Set(boolToFloat64(relay.Ison))
sp.prometheus.switchOverpower.With(switchLabels).Set(boolToFloat64(relay.Overpower))
sp.prometheus.switchTimer.With(switchLabels).Set(boolToFloat64(relay.HasTimer))
}
} else {
logger.Errorf(`failed to fetch status: %v`, err)
if discovery.ServiceDiscovery != nil {
discovery.ServiceDiscovery.MarkTarget(target.Address, discovery.TargetUnhealthy)
}
}
}
6 changes: 5 additions & 1 deletion shellyplug/prober.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ func (sp *ShellyPlug) collectFromTarget(target discovery.DiscoveryTarget) {

switch shellyGeneration {
case 1:
sp.collectFromTargetGen1(target, targetLogger, infoLabels, targetLabels)
if target.Type == "shellyem3" {
sp.collectFromTargetGen1em(target, targetLogger, infoLabels, targetLabels)
} else {
sp.collectFromTargetGen1(target, targetLogger, infoLabels, targetLabels)
}
case 2:
sp.collectFromTargetGen2(target, targetLogger, infoLabels, targetLabels)
default:
Expand Down
114 changes: 114 additions & 0 deletions shellyprober/gen1em.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package shellyprober

import (
"context"

"github.com/go-resty/resty/v2"
"github.com/patrickmn/go-cache"
"github.com/webdevops/shelly-plug-exporter/discovery"
)

type (
ShellyProberGen1Em struct {
Target discovery.DiscoveryTarget
Client *resty.Client
Ctx context.Context
Cache *cache.Cache
}

ShellyProberGen1EmResultSettings struct {
Name string `json:"name"`
MaxPower float64 `json:"max_power"`
Fw string `json:"fw"`

Device struct {
Hostname string `json:"hostname"`
Mac string `json:"mac"`
Type string `json:"type"`
} `json:"device"`
}

ShellyProberGen1EmResultStatus struct {
WifiSta struct {
Connected bool `yaml:"connected"`
Ssid string `yaml:"ssid"`
IP string `yaml:"ip"`
Rssi int `yaml:"rssi"`
} `yaml:"wifi_sta"`
Cloud struct {
Enabled bool `yaml:"enabled"`
Connected bool `yaml:"connected"`
} `yaml:"cloud"`
Mqtt struct {
Connected bool `yaml:"connected"`
} `yaml:"mqtt"`
Time string `yaml:"time"`
Unixtime int `yaml:"unixtime"`
Serial int `yaml:"serial"`
HasUpdate bool `yaml:"has_update"`
Mac string `yaml:"mac"`
CfgChangedCnt int `yaml:"cfg_changed_cnt"`
ActionsStats struct {
Skipped int `yaml:"skipped"`
} `yaml:"actions_stats"`
Relays []struct {
Ison bool `yaml:"ison"`
HasTimer bool `yaml:"has_timer"`
TimerStarted int `yaml:"timer_started"`
TimerDuration int `yaml:"timer_duration"`
TimerRemaining int `yaml:"timer_remaining"`
Overpower bool `yaml:"overpower"`
IsValid bool `yaml:"is_valid"`
Source string `yaml:"source"`
} `yaml:"relays"`
Emeters []struct {
Power float64 `yaml:"power"`
Pf float64 `yaml:"pf"`
Current float64 `yaml:"current"`
Voltage float64 `yaml:"voltage"`
IsValid bool `yaml:"is_valid"`
Total float64 `yaml:"total"`
TotalReturned float64 `yaml:"total_returned"`
} `yaml:"emeters"`
TotalPower float64 `yaml:"total_power"`
EmeterN struct {
Current int `yaml:"current"`
Ixsum float64 `yaml:"ixsum"`
Mismatch bool `yaml:"mismatch"`
IsValid bool `yaml:"is_valid"`
} `yaml:"emeter_n"`
FsMounted bool `yaml:"fs_mounted"`
VData int `yaml:"v_data"`
CtCalst int `yaml:"ct_calst"`
Update struct {
Status string `yaml:"status"`
HasUpdate bool `yaml:"has_update"`
NewVersion string `yaml:"new_version"`
OldVersion string `yaml:"old_version"`
BetaVersion string `yaml:"beta_version"`
} `yaml:"update"`
RAMTotal int `yaml:"ram_total"`
RAMFree int `yaml:"ram_free"`
FsSize int `yaml:"fs_size"`
FsFree int `yaml:"fs_free"`
Uptime int `yaml:"uptime"`
}
)

func (sp *ShellyProberGen1Em) fetch(url string, target interface{}) error {
r := sp.Client.R().SetContext(sp.Ctx).SetResult(&target).ForceContentType("application/json")
_, err := r.Get(sp.Target.Url(url))
return err
}

func (sp *ShellyProberGen1Em) GetSettings() (ShellyProberGen1EmResultSettings, error) {
result := ShellyProberGen1EmResultSettings{}
err := sp.fetch("/settings", &result)
return result, err
}

func (sp *ShellyProberGen1Em) GetStatus() (ShellyProberGen1EmResultStatus, error) {
result := ShellyProberGen1EmResultStatus{}
err := sp.fetch("/status", &result)
return result, err
}

0 comments on commit 7025ed4

Please sign in to comment.