Skip to content

Commit 73d370f

Browse files
committed
switch back to resty v2, improve servicediscovery
Signed-off-by: Markus Blaschke <[email protected]>
1 parent 8b0759a commit 73d370f

File tree

12 files changed

+242
-114
lines changed

12 files changed

+242
-114
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
PROJECT_NAME := $(shell basename $(CURDIR))
22
GIT_TAG := $(shell git describe --dirty --tags --always)
33
GIT_COMMIT := $(shell git rev-parse --short HEAD)
4-
LDFLAGS := -X "main.gitTag=$(GIT_TAG)" -X "main.gitCommit=$(GIT_COMMIT)" -extldflags "-static" -s -w
4+
BUILD_DATE := $(shell TZ=UTC date '+%Y-%m-%dT%H:%M:%SZ')
5+
LDFLAGS := -X "main.gitTag=$(GIT_TAG)" -X "main.gitCommit=$(GIT_COMMIT)" -X "main.buildDate=$(BUILD_DATE)" -extldflags "-static" -s -w
56

67
FIRST_GOPATH := $(firstword $(subst :, ,$(shell go env GOPATH)))
78
GOLANGCI_LINT_BIN := $(FIRST_GOPATH)/bin/golangci-lint

config/opts.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ type (
2727
}
2828

2929
Host struct {
30-
ShellyPlug []string `long:"shelly.host.shellyplug" env:"SHELLY_HOST_SHELLYPLUGS" env-delim:"," description:"shellyplug device IP or hostname to scrape. Pass multiple times for multiple hosts" default:""`
30+
ShellyPlug []string `long:"shelly.host.shellyplug" env:"SHELLY_HOST_SHELLYPLUGS" env-delim:"," description:"shellyplug device IP or hostname to scrape. Pass multiple times for multiple hosts" default:""`
3131
ShellyPlus []string `long:"shelly.host.shellyplus" env:"SHELLY_HOST_SHELLYPLUSES" env-delim:"," description:"shellyplus device IP or hostname to scrape. Pass multiple times for multiple hosts" default:""`
32-
ShellyPro []string `long:"shelly.host.shellypro" env:"SHELLY_HOST_SHELLYPROS" env-delim:"," description:"shellypro device IP or hostname to scrape. Pass multiple times for multiple hosts" default:""`
32+
ShellyPro []string `long:"shelly.host.shellypro" env:"SHELLY_HOST_SHELLYPROS" env-delim:"," description:"shellypro device IP or hostname to scrape. Pass multiple times for multiple hosts" default:""`
3333
}
3434

3535
ServiceDiscovery struct {

discovery/discovery.go

Lines changed: 178 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ type (
3131
lock sync.RWMutex
3232
staticHosts []DiscoveryTarget
3333
}
34+
35+
serviceDiscoveryTarget struct {
36+
mdns.ServiceEntry
37+
38+
Name string
39+
Host string
40+
Address string
41+
Port int
42+
43+
InfoFields map[string]string
44+
Generation string
45+
Version string
46+
}
3447
)
3548

3649
var (
@@ -72,98 +85,186 @@ func (d *serviceDiscovery) init(shellyplugs []string, shellyplus []string, shell
7285
}
7386

7487
func (d *serviceDiscovery) Run(timeout time.Duration) {
88+
var targetList []DiscoveryTarget
89+
targetList = append(targetList, d.staticHosts...)
90+
7591
wg := sync.WaitGroup{}
76-
// Make a channel for results and start listening
77-
entriesCh := make(chan *mdns.ServiceEntry, 15)
92+
93+
targetChannel := make(chan *DiscoveryTarget, 1)
7894

7995
wg.Add(1)
8096
go func() {
8197
defer wg.Done()
82-
var targetList []DiscoveryTarget
83-
targetList = append(targetList, d.staticHosts...)
84-
for entry := range entriesCh {
85-
switch {
86-
case strings.HasPrefix(strings.ToLower(entry.Name), "shellyplug-"):
87-
d.logger.With(
88-
slog.Group(
89-
"target",
90-
slog.String("name", entry.Name),
91-
slog.String("address", entry.AddrV4.String()),
92-
),
93-
).Debug(`found target via mDNS servicediscovery`)
94-
targetList = append(targetList, DiscoveryTarget{
95-
Hostname: entry.Name,
96-
Port: entry.Port,
97-
Address: entry.AddrV4.String(),
98-
Type: TargetTypeShellyPlug,
99-
Static: false,
100-
})
101-
case strings.HasPrefix(strings.ToLower(entry.Name), "shellyplus"):
102-
d.logger.With(
103-
slog.Group(
104-
"target",
105-
slog.String("name", entry.Name),
106-
slog.String("address", entry.AddrV4.String()),
107-
),
108-
).Debug(`found target via mDNS servicediscovery`)
109-
targetList = append(targetList, DiscoveryTarget{
110-
Hostname: entry.Name,
111-
Port: entry.Port,
112-
Address: entry.AddrV4.String(),
113-
Type: TargetTypeShellyPlus,
114-
Static: false,
115-
})
116-
case strings.HasPrefix(strings.ToLower(entry.Name), "shellypro"):
117-
d.logger.With(
118-
slog.Group(
119-
"target",
120-
slog.String("name", entry.Name),
121-
slog.String("address", entry.AddrV4.String()),
122-
),
123-
).Debug(`found target via mDNS servicediscovery`)
124-
targetList = append(targetList, DiscoveryTarget{
125-
Hostname: entry.Name,
126-
Port: entry.Port,
127-
Address: entry.AddrV4.String(),
128-
Type: TargetTypeShellyPro,
129-
Static: false,
130-
})
98+
for target := range targetChannel {
99+
targetList = append(targetList, *target)
100+
}
101+
}()
102+
103+
// mDNS discovery via _http._tcp.
104+
d.discover("_http._tcp", timeout, func(logger *slogger.Logger, target *serviceDiscoveryTarget) *DiscoveryTarget {
105+
switch {
106+
case strings.HasPrefix(target.Name, "shellyplug-"):
107+
logger.Debug(`found target via mDNS servicediscovery`)
108+
return &DiscoveryTarget{
109+
Hostname: target.Name,
110+
Port: target.Port,
111+
Address: target.Address,
112+
Type: TargetTypeShellyPlug,
113+
Generation: target.Generation,
114+
Static: false,
115+
}
116+
case strings.HasPrefix(target.Name, "shellyplus"):
117+
logger.Debug(`found target via mDNS servicediscovery`)
118+
return &DiscoveryTarget{
119+
Hostname: target.Name,
120+
Port: target.Port,
121+
Address: target.Address,
122+
Type: TargetTypeShellyPlus,
123+
Generation: target.Generation,
124+
Static: false,
125+
}
126+
case strings.HasPrefix(target.Name, "shellypro"):
127+
logger.Debug(`found target via mDNS servicediscovery`)
128+
return &DiscoveryTarget{
129+
Hostname: target.Name,
130+
Port: target.Port,
131+
Address: target.Address,
132+
Type: TargetTypeShellyPro,
133+
Generation: target.Generation,
134+
Static: false,
131135
}
132136
}
133-
d.logger.Debug(`finished mDNS servicediscovery"`, slog.Int("targets", len(targetList)))
134-
135-
d.lock.Lock()
136-
defer d.lock.Unlock()
137-
138-
// reduce all non-discovered targets health
139-
for target := range d.targetList {
140-
// set to low health if target was healthy before
141-
// reduce health even further if not detected
142-
// some devices seems not to respond to mdns discovery after some time,
143-
// so try to keep them alive here
144-
if d.targetList[target].Health > TargetHealthLow {
145-
d.targetList[target].Health = TargetHealthLow
146-
} else {
147-
// reduce health even more for each failed service discovery
148-
d.targetList[target].Health = d.targetList[target].Health - 1
137+
138+
if gen, ok := target.InfoFields["gen"]; ok {
139+
switch strings.ToLower(gen) {
140+
case "2":
141+
return &DiscoveryTarget{
142+
Hostname: target.Name,
143+
Port: target.Port,
144+
Address: target.Address,
145+
Type: TargetTypeShellyPro,
146+
Generation: target.Generation,
147+
Static: false,
148+
}
149149
}
150150
}
151151

152-
// set all discovered targets to good health
153-
for _, row := range targetList {
154-
target := row
155-
d.targetList[target.Address] = &target
156-
d.targetList[target.Address].Health = TargetHealthGood
152+
return nil
153+
}, targetChannel)
154+
155+
// mDNS discovery via _shelly._tcp
156+
d.discover("_shelly._tcp", timeout, func(logger *slogger.Logger, target *serviceDiscoveryTarget) *DiscoveryTarget {
157+
if gen, ok := target.InfoFields["gen"]; ok {
158+
switch strings.ToLower(gen) {
159+
case "2":
160+
return &DiscoveryTarget{
161+
Hostname: target.Name,
162+
Port: target.Port,
163+
Address: target.Address,
164+
Type: TargetTypeShellyPro,
165+
Generation: target.Generation,
166+
Static: false,
167+
}
168+
}
169+
}
170+
171+
return nil
172+
}, targetChannel)
173+
174+
close(targetChannel)
175+
wg.Wait()
176+
177+
d.lock.Lock()
178+
defer d.lock.Unlock()
179+
180+
// reduce all non-discovered targets health
181+
for target := range d.targetList {
182+
// set to low health if target was healthy before
183+
// reduce health even further if not detected
184+
// some devices seems not to respond to mdns discovery after some time,
185+
// so try to keep them alive here
186+
if d.targetList[target].Health > TargetHealthLow {
187+
d.targetList[target].Health = TargetHealthLow
188+
} else {
189+
// reduce health even more for each failed service discovery
190+
d.targetList[target].Health = d.targetList[target].Health - 1
191+
}
192+
}
193+
194+
// set all discovered targets to good health
195+
for _, row := range targetList {
196+
target := row
197+
d.targetList[target.Address] = &target
198+
d.targetList[target.Address].Health = TargetHealthGood
199+
}
200+
201+
d.logger.Debug(`finished mDNS servicediscovery"`, slog.Int("targets", len(d.targetList)))
202+
203+
d.cleanup()
204+
}
205+
206+
func (d *serviceDiscovery) discover(service string, timeout time.Duration, callback func(logger *slogger.Logger, target *serviceDiscoveryTarget) *DiscoveryTarget, channel chan *DiscoveryTarget) {
207+
wg := sync.WaitGroup{}
208+
// Make a channel for results and start listening
209+
entriesCh := make(chan *mdns.ServiceEntry, 30)
210+
211+
discoveryLogger := d.logger.With(slog.String("service", service))
212+
213+
wg.Add(1)
214+
go func() {
215+
defer wg.Done()
216+
for entry := range entriesCh {
217+
target := serviceDiscoveryTarget{
218+
ServiceEntry: *entry,
219+
Name: strings.ToLower(entry.Name),
220+
Host: strings.ToLower(entry.Host),
221+
Port: entry.Port,
222+
Address: entry.AddrV4.String(),
223+
InfoFields: map[string]string{},
224+
Generation: "",
225+
Version: "",
226+
}
227+
228+
// skip if we dont have a name or address
229+
if target.Name == "" || target.Address == "" {
230+
continue
231+
}
232+
233+
// parse info fields
234+
for _, field := range entry.InfoFields {
235+
fieldParts := strings.SplitN(field, "=", 2)
236+
if len(fieldParts) == 2 {
237+
target.InfoFields[fieldParts[0]] = fieldParts[1]
238+
}
239+
}
240+
241+
if val, ok := target.InfoFields["gen"]; ok {
242+
target.Generation = val
243+
}
244+
245+
if val, ok := target.InfoFields["ver"]; ok {
246+
target.Version = val
247+
}
248+
249+
entryLogger := discoveryLogger.With(
250+
slog.Group(
251+
"target",
252+
slog.String("name", target.Name),
253+
slog.String("address", target.Address),
254+
),
255+
)
256+
if target := callback(entryLogger, &target); target != nil {
257+
channel <- target
258+
}
157259
}
158-
d.cleanup()
159260
}()
160261

161262
// Start the lookup
162-
params := mdns.DefaultParams("_http._tcp")
263+
params := mdns.DefaultParams(service)
163264
params.DisableIPv6 = true
164265
params.Timeout = timeout
165266
params.Entries = entriesCh
166-
params.Logger = slog.NewLogLogger(d.logger.Handler(), slog.LevelInfo)
267+
params.Logger = slog.NewLogLogger(discoveryLogger.Handler(), slog.LevelInfo)
167268
err := mdns.Query(params)
168269
if err != nil {
169270
panic(err)
@@ -228,7 +329,7 @@ func (d *serviceDiscovery) GetTargetList() []DiscoveryTarget {
228329
return targetList
229330
}
230331

231-
func discoveryTargetFromStatic(entry string, typ string) DiscoveryTarget {
332+
func discoveryTargetFromStatic(entry string, deviceType string) DiscoveryTarget {
232333
parts := strings.Split(entry, ":")
233334
var name string
234335
var port int
@@ -248,7 +349,7 @@ func discoveryTargetFromStatic(entry string, typ string) DiscoveryTarget {
248349
Hostname: name,
249350
Port: port,
250351
Address: name,
251-
Type: typ,
352+
Type: deviceType,
252353
Static: true,
253354
Health: TargetHealthGood,
254355
}

discovery/discoverytarget.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type (
1313
Health int `json:"health"`
1414
Type string `json:"type"`
1515
Static bool `json:"isStatic"`
16+
Generation string `json:"generation"`
1617
}
1718
)
1819

main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ var (
2727
// Git version information
2828
gitCommit = "<unknown>"
2929
gitTag = "<unknown>"
30+
buildDate = "<unknown>"
3031
)
3132

3233
func main() {
3334
initArgparser()
3435
initLogger()
3536

36-
logger.Info(fmt.Sprintf("starting shellyplug-plug-exporter v%s (%s; %s; by %v)", gitTag, gitCommit, runtime.Version(), Author))
37+
logger.Info(fmt.Sprintf("starting shellyplug-plug-exporter v%s (%s; %s; by %v at %v)", gitTag, gitCommit, runtime.Version(), Author, buildDate))
3738
logger.Info(string(Opts.GetJson()))
3839
initSystem()
3940

shellyplug/prober.gen1.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import (
1212
)
1313

1414
func (sp *ShellyPlug) collectFromTargetGen1(target discovery.DiscoveryTarget, logger *slogger.Logger, infoLabels, targetLabels prometheus.Labels) {
15-
client := sp.restyClient(sp.ctx, target)
15+
client := sp.restyClient(sp.ctx, target, logger)
16+
if sp.auth.username != "" {
17+
client.SetDisableWarn(true)
18+
client.SetBasicAuth(sp.auth.username, sp.auth.password)
19+
}
1620

1721
shellyProber := shellyprober.ShellyProberGen1{
1822
Target: target,

shellyplug/prober.gen2.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ type (
2323
func (sp *ShellyPlug) collectFromTargetGen2(target discovery.DiscoveryTarget, logger *slogger.Logger, infoLabels, targetLabels prometheus.Labels) {
2424
sp.prometheus.info.With(infoLabels).Set(1)
2525

26-
client := sp.restyClient(sp.ctx, target)
26+
client := sp.restyClient(sp.ctx, target, logger)
27+
if sp.auth.username != "" {
28+
client.SetDisableWarn(true)
29+
client.SetDigestAuth(sp.auth.username, sp.auth.password)
30+
}
2731

2832
shellyProber := shellyprober.ShellyProberGen2{
2933
Target: target,

shellyplug/prober.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ func (sp *ShellyPlug) GetTargets() []discovery.DiscoveryTarget {
7373
func (sp *ShellyPlug) Run() {
7474
wg := sync.WaitGroup{}
7575

76-
for _, target := range sp.GetTargets() {
76+
for _, row := range sp.GetTargets() {
77+
target := row
7778
wg.Add(1)
7879
go func(target discovery.DiscoveryTarget) {
7980
defer wg.Done()
@@ -136,6 +137,7 @@ func (sp *ShellyPlug) collectFromTarget(target discovery.DiscoveryTarget) {
136137
}
137138
}
138139

140+
targetLogger = targetLogger.With(slog.Int("gen", shellyGeneration))
139141
switch shellyGeneration {
140142
case 1:
141143
sp.collectFromTargetGen1(target, targetLogger, infoLabels, targetLabels)

0 commit comments

Comments
 (0)