diff --git a/discovery/discovery.go b/discovery/discovery.go index bc8087a..e01fecb 100644 --- a/discovery/discovery.go +++ b/discovery/discovery.go @@ -20,6 +20,7 @@ const ( TargetTypeShellyPlug = "shellyplug" TargetTypeShellyPlus = "shellyplus" TargetTypeShellyPro = "shellypro" + TargetTypeShellyEm3 = "shellyem3" ) type ( @@ -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() @@ -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() diff --git a/shellyplug/prober.gen1em.go b/shellyplug/prober.gen1em.go new file mode 100644 index 0000000..23f05d5 --- /dev/null +++ b/shellyplug/prober.gen1em.go @@ -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) + } + } +} diff --git a/shellyplug/prober.go b/shellyplug/prober.go index a55ce43..0c02313 100644 --- a/shellyplug/prober.go +++ b/shellyplug/prober.go @@ -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: diff --git a/shellyprober/gen1em.go b/shellyprober/gen1em.go new file mode 100644 index 0000000..4138c0d --- /dev/null +++ b/shellyprober/gen1em.go @@ -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 +}