summaryrefslogtreecommitdiff
path: root/mqtt-controller/devices.go
diff options
context:
space:
mode:
Diffstat (limited to 'mqtt-controller/devices.go')
-rw-r--r--mqtt-controller/devices.go280
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 @@
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7 "strconv"
8 "strings"
9)
10
11type Direction int
12
13const (
14 DirectionInput Direction = iota
15 DirectionOutput
16)
17
18type 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
26type Command struct {
27 Index int
28 Active bool
29}
30
31type Event struct {
32 Device Device
33 Index int
34 Active bool
35 Direction Direction
36}
37
38type Metric struct {
39 Device Device
40 Name string
41 Value interface{}
42}
43
44type 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
53func 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
63type 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
84func 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
103var _ Device = (*ShellyDevice)(nil)
104
105func (d *ShellyDevice) Identity() string {
106 return d.ID
107}
108
109func (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
210func (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
221func (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
232func (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
275func (d *ShellyDevice) Disconnect() error {
276 if !d.connected {
277 return fmt.Errorf("Device not connected, can not disconnect")
278 }
279 return nil
280}