diff options
Diffstat (limited to 'mqtt-controller/devices.go')
-rw-r--r-- | mqtt-controller/devices.go | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/mqtt-controller/devices.go b/mqtt-controller/devices.go new file mode 100644 index 0000000..9f11333 --- /dev/null +++ b/mqtt-controller/devices.go | |||
@@ -0,0 +1,280 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "encoding/json" | ||
5 | "fmt" | ||
6 | "log" | ||
7 | "strconv" | ||
8 | "strings" | ||
9 | ) | ||
10 | |||
11 | type Direction int | ||
12 | |||
13 | const ( | ||
14 | DirectionInput Direction = iota | ||
15 | DirectionOutput | ||
16 | ) | ||
17 | |||
18 | type Device interface { | ||
19 | Identity() string | ||
20 | Connect(*MQTTBroker, chan<- Event, chan<- Metric, chan<- interface{}) (<-chan Command, error) | ||
21 | OutputOn(int) error | ||
22 | OutputOff(int) error | ||
23 | Disconnect() error | ||
24 | } | ||
25 | |||
26 | type Command struct { | ||
27 | Index int | ||
28 | Active bool | ||
29 | } | ||
30 | |||
31 | type Event struct { | ||
32 | Device Device | ||
33 | Index int | ||
34 | Active bool | ||
35 | Direction Direction | ||
36 | } | ||
37 | |||
38 | type Metric struct { | ||
39 | Device Device | ||
40 | Name string | ||
41 | Value interface{} | ||
42 | } | ||
43 | |||
44 | type DeviceAnnounce struct { | ||
45 | ID string `json:"id"` | ||
46 | IP string `json:"ip"` | ||
47 | MacAddress string `json:"mac"` | ||
48 | Model string `json:"model"` | ||
49 | FirmwareVersion string `json:"fw_ver"` | ||
50 | NewFirmwareAvailable bool `json:"new_fw"` | ||
51 | } | ||
52 | |||
53 | func NewDevice(b *MQTTBroker, da DeviceAnnounce) (Device, error) { | ||
54 | switch da.Model { | ||
55 | case "SHSW-PM": | ||
56 | return NewShellyDevice(da, 1, 1), nil | ||
57 | case "SHSW-25": | ||
58 | return NewShellyDevice(da, 2, 2), nil | ||
59 | } | ||
60 | return nil, fmt.Errorf("Unknown device model %s", da.Model) | ||
61 | } | ||
62 | |||
63 | type ShellyDevice struct { | ||
64 | ID string | ||
65 | Manufacturer string | ||
66 | IP string // TODO: Fix type | ||
67 | MacAddress string // TODO: Fix type | ||
68 | Model string // TODO: Fix type | ||
69 | FirmwareVersion string // TODO: Fix type | ||
70 | NewFirmwareAvailable bool | ||
71 | broker *MQTTBroker | ||
72 | inputCount int | ||
73 | outputCount int | ||
74 | connected bool | ||
75 | inputState []bool | ||
76 | outputState []bool | ||
77 | subscribed []string | ||
78 | events chan<- Event | ||
79 | metrics chan<- Metric | ||
80 | info chan<- interface{} | ||
81 | commands chan Command | ||
82 | } | ||
83 | |||
84 | func NewShellyDevice(da DeviceAnnounce, inputCount, outputCount int) *ShellyDevice { | ||
85 | return &ShellyDevice{ | ||
86 | Manufacturer: "Shelly", | ||
87 | ID: da.ID, | ||
88 | IP: da.IP, | ||
89 | MacAddress: da.MacAddress, | ||
90 | Model: da.Model, | ||
91 | FirmwareVersion: da.FirmwareVersion, | ||
92 | NewFirmwareAvailable: da.NewFirmwareAvailable, | ||
93 | inputCount: inputCount, | ||
94 | outputCount: outputCount, | ||
95 | inputState: make([]bool, inputCount), | ||
96 | outputState: make([]bool, outputCount), | ||
97 | connected: false, | ||
98 | subscribed: []string{}, | ||
99 | commands: make(chan Command, 100), | ||
100 | } | ||
101 | } | ||
102 | |||
103 | var _ Device = (*ShellyDevice)(nil) | ||
104 | |||
105 | func (d *ShellyDevice) Identity() string { | ||
106 | return d.ID | ||
107 | } | ||
108 | |||
109 | func (d *ShellyDevice) handleOutputMessages(topic string, payload []byte) { | ||
110 | path := strings.Split(topic, "/")[2:] | ||
111 | |||
112 | switch path[0] { | ||
113 | case "info": | ||
114 | p := ShellyStatus{Device: d} | ||
115 | if err := json.Unmarshal(payload, &p); err != nil { | ||
116 | log.Printf("Error parsing info packet: %s", err) | ||
117 | return | ||
118 | } | ||
119 | d.info <- p | ||
120 | case "input": | ||
121 | idx, err := strconv.Atoi(path[1]) | ||
122 | if err != nil { | ||
123 | log.Println("Error parsing index") | ||
124 | return | ||
125 | } | ||
126 | state := payload[0] == '0' | ||
127 | if d.inputState[idx] != state { | ||
128 | d.events <- Event{ | ||
129 | Device: d, | ||
130 | Index: idx, | ||
131 | Active: state, | ||
132 | Direction: DirectionInput, | ||
133 | } | ||
134 | d.inputState[idx] = state | ||
135 | } | ||
136 | case "relay": | ||
137 | idx, err := strconv.Atoi(path[1]) | ||
138 | if err != nil { | ||
139 | log.Println("Error parsing index") | ||
140 | return | ||
141 | } | ||
142 | if len(path) == 2 { | ||
143 | state := string(payload) == "on" | ||
144 | if d.outputState[idx] != state { | ||
145 | d.events <- Event{ | ||
146 | Device: d, | ||
147 | Index: idx, | ||
148 | Active: state, | ||
149 | Direction: DirectionOutput, | ||
150 | } | ||
151 | d.outputState[idx] = state | ||
152 | } | ||
153 | } else { | ||
154 | switch path[2] { | ||
155 | case "power": | ||
156 | m, err := strconv.ParseFloat(string(payload), 16) | ||
157 | if err != nil { | ||
158 | log.Printf("Failed to parse metric") | ||
159 | return | ||
160 | } | ||
161 | d.metrics <- Metric{ | ||
162 | Device: d, | ||
163 | Name: fmt.Sprintf("relay_%d_power", idx), | ||
164 | Value: m, | ||
165 | } | ||
166 | case "energy": | ||
167 | m, err := strconv.ParseInt(string(payload), 10, 32) | ||
168 | if err != nil { | ||
169 | log.Printf("Failed to parse metric") | ||
170 | return | ||
171 | } | ||
172 | d.metrics <- Metric{ | ||
173 | Device: d, | ||
174 | Name: fmt.Sprintf("relay_%d_energy", idx), | ||
175 | Value: m, | ||
176 | } | ||
177 | case "overpower_value": | ||
178 | m, err := strconv.ParseInt(string(payload), 10, 32) | ||
179 | if err != nil { | ||
180 | log.Printf("Failed to parse metric") | ||
181 | return | ||
182 | } | ||
183 | d.metrics <- Metric{ | ||
184 | Device: d, | ||
185 | Name: fmt.Sprintf("relay_%d_overpower_value", idx), | ||
186 | Value: m, | ||
187 | } | ||
188 | } | ||
189 | } | ||
190 | case "temperature": | ||
191 | m, err := strconv.ParseFloat(string(payload), 16) | ||
192 | if err != nil { | ||
193 | log.Printf("Failed to parse metric") | ||
194 | return | ||
195 | } | ||
196 | d.metrics <- Metric{ | ||
197 | Device: d, | ||
198 | Name: "temperature_c", | ||
199 | Value: m, | ||
200 | } | ||
201 | case "overtemperature": | ||
202 | d.metrics <- Metric{ | ||
203 | Device: d, | ||
204 | Name: "is_overtemperature", | ||
205 | Value: payload[0] == '1', | ||
206 | } | ||
207 | } | ||
208 | } | ||
209 | |||
210 | func (d *ShellyDevice) OutputOn(idx int) error { | ||
211 | if idx < 0 || idx >= d.outputCount { | ||
212 | return fmt.Errorf("Index is out of range for device") | ||
213 | } | ||
214 | if d.broker == nil { | ||
215 | return fmt.Errorf("Device is not connected to broker") | ||
216 | } | ||
217 | |||
218 | return d.broker.Publish(fmt.Sprintf("shellies/%s/relay/%d/command", d.Identity(), idx), "on") | ||
219 | } | ||
220 | |||
221 | func (d *ShellyDevice) OutputOff(idx int) error { | ||
222 | if idx < 0 || idx >= d.outputCount { | ||
223 | return fmt.Errorf("Index is out of range for device") | ||
224 | } | ||
225 | if d.broker == nil { | ||
226 | return fmt.Errorf("Device is not connected to broker") | ||
227 | } | ||
228 | |||
229 | return d.broker.Publish(fmt.Sprintf("shellies/%s/relay/%d/command", d.Identity(), idx), "off") | ||
230 | } | ||
231 | |||
232 | func (d *ShellyDevice) Connect(b *MQTTBroker, events chan<- Event, metrics chan<- Metric, info chan<- interface{}) (<-chan Command, error) { | ||
233 | d.broker = b | ||
234 | d.events = events | ||
235 | d.metrics = metrics | ||
236 | d.info = info | ||
237 | |||
238 | outputs := []string{ | ||
239 | "shellies/{ID}/info", | ||
240 | "shellies/{ID}/temperature", | ||
241 | "shellies/{ID}/overtemperature", | ||
242 | } | ||
243 | |||
244 | for i := d.inputCount - 1; i >= 0; i-- { | ||
245 | outputs = append(outputs, fmt.Sprintf("shellies/{ID}/input/%d", i)) | ||
246 | outputs = append(outputs, fmt.Sprintf("shellies/{ID}/longpush/%d", i)) | ||
247 | } | ||
248 | |||
249 | for i := d.outputCount - 1; i >= 0; i-- { | ||
250 | outputs = append(outputs, fmt.Sprintf("shellies/{ID}/relay/%d", i)) | ||
251 | outputs = append(outputs, fmt.Sprintf("shellies/{ID}/relay/%d/power", i)) | ||
252 | outputs = append(outputs, fmt.Sprintf("shellies/{ID}/relay/%d/energy", i)) | ||
253 | outputs = append(outputs, fmt.Sprintf("shellies/{ID}/relay/%d/overpower_value", i)) | ||
254 | } | ||
255 | |||
256 | // TODO: Handle these | ||
257 | inputs := []string{ | ||
258 | "", | ||
259 | } | ||
260 | _ = inputs | ||
261 | |||
262 | for _, topic := range outputs { | ||
263 | tn := strings.Replace(topic, "{ID}", d.ID, 1) | ||
264 | if err := b.Subscribe(tn, d.handleOutputMessages); err != nil { | ||
265 | return nil, err | ||
266 | } | ||
267 | d.subscribed = append(d.subscribed, tn) | ||
268 | } | ||
269 | |||
270 | d.connected = true | ||
271 | |||
272 | return d.commands, nil | ||
273 | } | ||
274 | |||
275 | func (d *ShellyDevice) Disconnect() error { | ||
276 | if !d.connected { | ||
277 | return fmt.Errorf("Device not connected, can not disconnect") | ||
278 | } | ||
279 | return nil | ||
280 | } | ||