-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdiscover.go
153 lines (132 loc) · 3.37 KB
/
discover.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package gohue
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/hashicorp/mdns"
"net"
"net/http"
"strings"
)
type BridgeDiscoverMethod int
const (
DiscoveryManual BridgeDiscoverMethod = iota
DiscoveryMDNS
DiscoveryEndpoint
)
func DiscoverBridges(ctx context.Context) ([]Bridge, error) {
bridges, err := MDNSDiscover()
if err == nil && len(bridges) > 0 {
return bridges, nil
}
bridges, err = CloudDiscovery(ctx)
if err == nil && len(bridges) > 0 {
return bridges, nil
}
return nil, errors.New("no bridges found")
}
type Bridge struct {
ID string `json:"id"`
Name string `json:"name"`
ModelID string `json:"modelid"`
AddrIPv4 net.IP `json:"addr_ipv4"`
AddrIPv6 net.IP `json:"addr_ipv6"`
// Addr is going to be either ipv4 or ipv6
Addr net.IP `json:"addr"`
Port int `json:"port"`
DiscoveredBy BridgeDiscoverMethod `json:"discovered_by"`
}
// MDNSDiscover will discover bridges using mDNS
func MDNSDiscover() ([]Bridge, error) {
// Make a channel for results and start listening
entriesCh := make(chan *mdns.ServiceEntry, 4)
var dnsErr error
go func() {
dnsErr = mdns.Lookup("_hue._tcp", entriesCh)
close(entriesCh)
}()
var bridges []Bridge
for entry := range entriesCh {
bridge := Bridge{
ID: "",
Name: entry.Name,
ModelID: "",
AddrIPv4: entry.AddrV4,
AddrIPv6: entry.AddrV6,
Addr: entry.Addr,
Port: entry.Port,
DiscoveredBy: DiscoveryMDNS,
}
for _, info := range entry.InfoFields {
parts := strings.Split(info, "=")
if len(parts) != 2 {
continue
}
switch parts[0] {
case "bridgeid":
bridge.ID = parts[1]
case "modelid":
bridge.ModelID = parts[1]
}
}
bridges = append(bridges, bridge)
}
return bridges, dnsErr
}
// CloudDiscovery asks hue cloud for the list of bridges.
// This is rate limited.
func CloudDiscovery(ctx context.Context) ([]Bridge, error) {
cli := http.DefaultClient
req, err := http.NewRequest("GET", "https://discovery.meethue.com", nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
resp, err := cli.Do(req)
if err != nil {
return nil, err
}
if err := debugHttpResponse(ctx, resp); err != nil {
return nil, fmt.Errorf("debug request: %w", err)
}
if resp.StatusCode == http.StatusTooManyRequests {
return nil, errors.New("rate limited from discovery endpoint, try again later in 15minutes")
}
type discoverBridge struct {
ID string `json:"id"`
Name string `json:"name"`
// "id":"001788fffe100491",
// "internalipaddress":"192.168.2.23",
// "macaddress":"00:17:88:10:04:91",
// "name":"Philips Hue"
InternapIPAddress string `json:"internalipaddress"`
MACAddress string `json:"macaddress"`
}
var bridges []discoverBridge
err = json.NewDecoder(resp.Body).Decode(&bridges)
if err != nil {
return nil, err
}
var normalizedBridges []Bridge
for _, bridge := range bridges {
addr := net.ParseIP(bridge.InternapIPAddress)
if addr == nil {
return nil, fmt.Errorf("invalid ip address %q", bridge.InternapIPAddress)
}
nb := Bridge{
ID: bridge.ID,
Name: bridge.Name,
Addr: addr,
Port: 0,
DiscoveredBy: DiscoveryEndpoint,
}
if len(nb.Addr) == net.IPv4len {
nb.AddrIPv4 = nb.Addr
} else {
nb.AddrIPv6 = nb.Addr
}
normalizedBridges = append(normalizedBridges, nb)
}
return normalizedBridges, nil
}