summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2022-01-02 13:28:25 -0800
committerMike Crute <mike@crute.us>2022-01-02 13:28:25 -0800
commit6833878bde3b04931ebbfbc7a2c72882fe7c7f23 (patch)
tree54627e82fddaea84bbf7bb54be950117cde13d30
parent5eb13e82544d6e4c1b07dd57f41fd8d228bc0960 (diff)
downloadmfi_homekit-master.tar.bz2
mfi_homekit-master.tar.xz
mfi_homekit-master.zip
Add old/new mqtt controllerHEADmaster
-rw-r--r--mqtt-controller/Makefile2
-rw-r--r--mqtt-controller/db/bridgeConfig5
-rw-r--r--mqtt-controller/devices.go280
-rw-r--r--mqtt-controller/go.mod8
-rw-r--r--mqtt-controller/go.sum38
-rw-r--r--mqtt-controller/main.go146
-rw-r--r--mqtt-controller/mqtt.go93
-rw-r--r--mqtt-controller/status.go80
-rw-r--r--mqtt-controller/unified_switch.go124
9 files changed, 776 insertions, 0 deletions
diff --git a/mqtt-controller/Makefile b/mqtt-controller/Makefile
new file mode 100644
index 0000000..c3efc35
--- /dev/null
+++ b/mqtt-controller/Makefile
@@ -0,0 +1,2 @@
1mqtt-controller: devices.go main.go mqtt.go status.go unified_switch.go
2 go build -o $@ $^
diff --git a/mqtt-controller/db/bridgeConfig b/mqtt-controller/db/bridgeConfig
new file mode 100644
index 0000000..db3afce
--- /dev/null
+++ b/mqtt-controller/db/bridgeConfig
@@ -0,0 +1,5 @@
1{
2 "Broker": "172.16.0.186:1883",
3 "BridgeName": "MQTT Bridge",
4 "Pin": "00102003"
5}
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}
diff --git a/mqtt-controller/go.mod b/mqtt-controller/go.mod
new file mode 100644
index 0000000..128b792
--- /dev/null
+++ b/mqtt-controller/go.mod
@@ -0,0 +1,8 @@
1module golang.crute.me/mfi_homekit
2
3go 1.15
4
5require (
6 github.com/brutella/hc v1.2.3
7 github.com/eclipse/paho.mqtt.golang v1.3.0
8)
diff --git a/mqtt-controller/go.sum b/mqtt-controller/go.sum
new file mode 100644
index 0000000..88cfc14
--- /dev/null
+++ b/mqtt-controller/go.sum
@@ -0,0 +1,38 @@
1github.com/brutella/dnssd v1.1.1 h1:Ar5ytE2Z9x5DTmuNnASlMTBpcQWQLm9ceHb326s0ykg=
2github.com/brutella/dnssd v1.1.1/go.mod h1:9gIcMKQSJvYlO2x+HR50cqqjghb9IWK9hvykmyveVVs=
3github.com/brutella/hc v1.2.3 h1:9a3h61apXx+63b1T+W1vscs+G3xZkLS131gypnh1FIE=
4github.com/brutella/hc v1.2.3/go.mod h1:zknCv+aeiYM27tBXr3WFL49C8UPHMxP2IVY9c5TpMOY=
5github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6github.com/eclipse/paho.mqtt.golang v1.3.0 h1:MU79lqr3FKNKbSrGN7d7bNYqh8MwWW7Zcx0iG+VIw9I=
7github.com/eclipse/paho.mqtt.golang v1.3.0/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
8github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
9github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
10github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
11github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0=
12github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
13github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
15github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
16github.com/tadglines/go-pkgs v0.0.0-20140924210655-1f86682992f1 h1:ms/IQpkxq+t7hWpgKqCE5KjAUQWC24mqBrnL566SWgE=
17github.com/tadglines/go-pkgs v0.0.0-20140924210655-1f86682992f1/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
18github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed h1:Gjnw8buhv4V8qXaHtAWPnKXNpCNx62heQpjO8lOY0/M=
19github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
20golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
21golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
22golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
23golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
24golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
25golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
26golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
27golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
28golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
29golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
30golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
31golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
32golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
33golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
34golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
35golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
36golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
37gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
38gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/mqtt-controller/main.go b/mqtt-controller/main.go
new file mode 100644
index 0000000..29d5d28
--- /dev/null
+++ b/mqtt-controller/main.go
@@ -0,0 +1,146 @@
1package main
2
3import (
4 "encoding/json"
5 "log"
6 "os"
7 "os/signal"
8 "sync"
9 "syscall"
10 "time"
11
12 "github.com/brutella/hc"
13 "github.com/brutella/hc/accessory"
14 "github.com/brutella/hc/util"
15)
16
17type AppConfig struct {
18 Broker string
19 BridgeName string
20 Pin string
21}
22
23func loadAppConfig(s util.Storage) (*AppConfig, error) {
24 d, err := s.Get("bridgeConfig")
25 if err != nil {
26 return nil, err
27 }
28
29 c := &AppConfig{}
30 if err = json.Unmarshal(d, c); err != nil {
31 return nil, err
32 }
33
34 return c, nil
35}
36
37func main() {
38 wg := &sync.WaitGroup{}
39 done := make(chan interface{})
40
41 c := make(chan os.Signal)
42 signal.Notify(c, os.Interrupt)
43 signal.Notify(c, os.Kill)
44 signal.Notify(c, syscall.SIGTERM)
45
46 storage, err := util.NewFileStorage("db")
47 if err != nil {
48 panic(err)
49 }
50
51 cfg, err := loadAppConfig(storage)
52 if err != nil {
53 panic(err)
54 }
55
56 broker, err := NewMQTTBroker(cfg.Broker)
57 if err != nil {
58 panic(err)
59 }
60
61 if err := broker.Subscribe("shellies/announce", nil); err != nil {
62 panic(err)
63 }
64
65 if err := broker.Publish("shellies/command", "announce"); err != nil {
66 panic(err)
67 }
68
69 ac := make(chan *UnifiedSwitch, 10)
70
71 go func() {
72 wg.Add(1)
73 defer wg.Done()
74
75 log.Println("Starting homekit thread")
76
77 reg := []*accessory.Accessory{}
78
79 // Collect devices until the discovery process quiesces, then start the
80 // transport
81 quiescence := 10 * time.Second
82 t := time.NewTimer(quiescence)
83
84 C:
85 for {
86 select {
87 case a := <-ac:
88 log.Println("Homekit: new device")
89 reg = append(reg, a.HomeKitSwitch.Accessory)
90 // TODO: Extend timer deadline
91 case <-t.C:
92 log.Println("Homekit: timeout over")
93 t.Stop()
94 break C
95 case <-done:
96 log.Println("Homekit: cancelled")
97 return
98 }
99 }
100
101 log.Println("MQTT quiesces, starting HomeKit")
102
103 br := accessory.NewBridge(accessory.Info{Name: cfg.BridgeName})
104
105 transport, err := hc.NewIPTransport(hc.Config{Pin: cfg.Pin}, br.Accessory, reg...)
106 if err != nil {
107 log.Fatalf("%s", err)
108 }
109 go transport.Start()
110 log.Printf("HomeKit transport started")
111
112 select {
113 case <-done:
114 log.Printf("Shutting down transport")
115 if transport != nil {
116 <-transport.Stop()
117 }
118 }
119 log.Printf("Transport shut down")
120 }()
121
122 for {
123 select {
124 case d := <-broker.Devices:
125 if d == nil {
126 continue
127 }
128
129 us, err := NewUnifiedSwitch(broker, d)
130 if err != nil {
131 log.Printf("Error connecting device: %e", err)
132 continue
133 }
134
135 go us.MessageLoop(done, wg)
136
137 ac <- us
138 log.Printf("New Device: %s\n", d.Identity())
139 case <-c:
140 close(done)
141 broker.Shutdown()
142 wg.Wait()
143 return
144 }
145 }
146}
diff --git a/mqtt-controller/mqtt.go b/mqtt-controller/mqtt.go
new file mode 100644
index 0000000..0b2687c
--- /dev/null
+++ b/mqtt-controller/mqtt.go
@@ -0,0 +1,93 @@
1package main
2
3import (
4 "encoding/json"
5 "log"
6
7 "github.com/eclipse/paho.mqtt.golang"
8)
9
10type MessageHandler func(topic string, payload []byte)
11
12func mqttSync(t mqtt.Token) error {
13 if t.Wait() && t.Error() != nil {
14 return t.Error()
15 }
16 return nil
17}
18
19type MQTTBroker struct {
20 Devices chan Device
21 messages chan mqtt.Message
22 done chan bool
23
24 client mqtt.Client
25}
26
27func NewMQTTBroker(broker string) (*MQTTBroker, error) {
28 bufferSize := 100
29
30 b := &MQTTBroker{
31 Devices: make(chan Device, bufferSize),
32 messages: make(chan mqtt.Message, bufferSize),
33 done: make(chan bool),
34 }
35
36 b.client = mqtt.NewClient(
37 mqtt.NewClientOptions().
38 AddBroker(broker).
39 SetDefaultPublishHandler(b.handleMessage).
40 SetClientID("homekit-controller"))
41
42 if err := mqttSync(b.client.Connect()); err != nil {
43 return nil, err
44 }
45
46 return b, nil
47}
48
49func (b *MQTTBroker) Subscribe(topic string, h MessageHandler) error {
50 var handler mqtt.MessageHandler
51 if h != nil {
52 handler = func(c mqtt.Client, m mqtt.Message) {
53 h(m.Topic(), m.Payload())
54 }
55 }
56
57 if err := mqttSync(b.client.Subscribe(topic, 0, handler)); err != nil {
58 return err
59 }
60
61 return nil
62}
63
64func (b *MQTTBroker) Publish(topic string, msg interface{}) error {
65 if err := mqttSync(b.client.Publish(topic, 0, false, msg)); err != nil {
66 return err
67 }
68 return nil
69}
70
71// Runs synchronously in the mqtt client comms handler goroutine
72func (b *MQTTBroker) handleMessage(c mqtt.Client, m mqtt.Message) {
73 switch m.Topic() {
74 case "shellies/announce", "ubiquiti/announce":
75 a := DeviceAnnounce{}
76 if err := json.Unmarshal(m.Payload(), &a); err != nil {
77 log.Printf("Unable to unmarshal device announce JSON: %e", err)
78 return
79 }
80
81 d, err := NewDevice(b, a)
82 if err != nil {
83 log.Printf("Error creating device driver: %e", err)
84 return
85 }
86
87 b.Devices <- d
88 }
89}
90
91func (b *MQTTBroker) Shutdown() {
92 b.client.Disconnect(250)
93}
diff --git a/mqtt-controller/status.go b/mqtt-controller/status.go
new file mode 100644
index 0000000..f469256
--- /dev/null
+++ b/mqtt-controller/status.go
@@ -0,0 +1,80 @@
1package main
2
3/*
4shellies/announce <- coming online announcements
5shellies/<deviceid>/info <- status endpoint
6
7shellies/command -> publish to address all (announce, update, update_fw)
8shellies/<deviceid>/command -> publish to address one
9
10<i> is zero based
11
12 shellies/<deviceid>/relay/<i>/command (input: on, off, toggle)
13
14 shellies/<deviceid>/input/<i> (output: 0, 1)
15 shellies/<deviceid>/longpush/<i> (output: 0 short, 1 long)
16 shellies/<deviceid>/temperature (float, device temp in C)
17 shellies/<deviceid>/overtemperature (int, 1 if overtemp, 0 otherwise)
18 shellies/<deviceid>/relay/<i> (output: on, off, overpower)
19 shellies/<deviceid>/relay/<i>/power (float, instantaneous power in watts)
20 shellies/<deviceid>/relay/<i>/energy (int, watt-minute counter)
21 shellies/<deviceid>/relay/<i>/overpower_value (power in watts where overpower occurred)
22*/
23
24type ShellyInput struct {
25 Event string `json:"event"` // L=Long Press, S=Short Press, =None/Invalid
26 EventCount int `json:"event_cnt"`
27 CurrentState int `json:"input"`
28}
29
30type ShellyMeter struct {
31 EnergyCounters []float32 `json:"counters"` // Last 3 round minutes in watt-minute
32 Valid bool `json:"is_valid"`
33 Overpower float32 `json:"overpower"` // Value in Watts, on which an overpower condition is detected
34 Timestamp int `json:"timestamp"` // Last counter value reading time
35 Total int `json:"total"` // Total watt-minutes consumed
36}
37
38type ShellyRelay struct {
39 HasTimer bool `json:"has_timer"`
40 IsValid bool `json:"is_valid"`
41 IsOn bool `json:"ison"`
42 Overpower bool `json:"overpower"`
43 OverTemperature bool `json:"overtemperature"`
44 Source string `json:"source"`
45 TimerDuration int `json:"timer_duration"`
46 TimerRemaining int `json:"timer_remaining"`
47 TimerStarted int `json:"timer_started"` // Unix timestamp of start
48}
49
50type ShellyStatus struct {
51 Device Device
52 ConfigChangeCount int `json:"cfg_changed_cnt"`
53 Cloud struct {
54 Connected bool `json:"connected"`
55 Enabled bool `json:"enabled"`
56 } `json:"cloud"`
57 MQTT struct {
58 Connected bool `json:"connected"`
59 } `json:"mqtt"`
60 FilesystemFreeBytes int `json:"fs_free"`
61 FilesystemSizeBytes int `json:"fs_size"`
62 RAMFreeBytes int `json:"ram_free"`
63 RAMSizeBytes int `json:"ram_total"`
64 HasUpdate bool `json:"has_update"`
65 MacAddress string `json:"mac"`
66 OverTemperature bool `json:"overtemperature"`
67 TemperatureC float32 `json:"temperature"`
68 TemperatureStatus string `json:"temperature_status"`
69 Uptime int `json:"uptime"`
70 Voltage float32 `json:"voltage"`
71 WifiClient struct {
72 Connected bool `json:"connected"`
73 IP string `json:"ip"`
74 RSSI int `json:"rssi"`
75 SSID string `json:"ssid"`
76 } `json:"wifi_sta"`
77 Inputs []ShellyInput `json:"inputs"`
78 Meters []ShellyMeter `json:"meters"`
79 Relays []ShellyRelay `json:"relays"`
80}
diff --git a/mqtt-controller/unified_switch.go b/mqtt-controller/unified_switch.go
new file mode 100644
index 0000000..fb62c7a
--- /dev/null
+++ b/mqtt-controller/unified_switch.go
@@ -0,0 +1,124 @@
1package main
2
3import (
4 "hash/crc64"
5 "log"
6 "sync"
7
8 "github.com/brutella/hc/accessory"
9)
10
11type UnifiedSwitch struct {
12 HomeKitSwitch *accessory.Switch
13 MqttDevice Device
14 Commands <-chan Command
15 metrics chan Metric
16 events chan Event
17 infos chan interface{}
18}
19
20func NewUnifiedSwitch(b *MQTTBroker, d Device) (*UnifiedSwitch, error) {
21 us := &UnifiedSwitch{
22 MqttDevice: d,
23 metrics: make(chan Metric, 100),
24 events: make(chan Event, 100),
25 infos: make(chan interface{}, 100),
26 }
27
28 cmd, err := d.Connect(b, us.events, us.metrics, us.infos)
29 if err != nil {
30 log.Printf("Error connecting device: %e", err)
31 return nil, err
32 }
33
34 us.Commands = cmd
35 us.makeHomekitSwitch()
36
37 return us, nil
38}
39
40func (s *UnifiedSwitch) MessageLoop(done <-chan interface{}, wg *sync.WaitGroup) {
41 wg.Add(1)
42 defer wg.Done()
43
44 log.Printf("Starting switch %s", s.MqttDevice.Identity())
45
46C:
47 for {
48 select {
49 case e := <-s.events:
50 direction := "output"
51 if e.Direction == DirectionInput {
52 direction = "input"
53 }
54
55 s.HomeKitSwitch.Switch.On.SetValue(e.Active)
56
57 log.Printf("New event for '%s'.%d %s: %t", e.Device.Identity(), e.Index, direction, e.Active)
58 case m := <-s.metrics:
59 /*
60 switch t := m.Value.(type) {
61 case float64:
62 log.Printf("New metric '%s' for '%s': %#v", m.Name, m.Device.Identity(), t)
63 case int64:
64 log.Printf("New metric '%s' for '%s': %#v", m.Name, m.Device.Identity(), t)
65 case bool:
66 log.Printf("New metric '%s' for '%s': %#v", m.Name, m.Device.Identity(), t)
67 default:
68 log.Printf("Unknown metric: %#v", m.Value)
69 }
70 */
71 _ = m
72 case i := <-s.infos:
73 switch it := i.(type) {
74 case ShellyStatus:
75 // TODO: Handle multiple relays
76 log.Printf("Info relays: %#v", it.Relays)
77 s.HomeKitSwitch.Switch.On.SetValue(it.Relays[0].IsOn)
78 default:
79 // Just discard them for now
80 }
81 case <-done:
82 log.Printf("Shutting down switch %s", s.MqttDevice.Identity())
83 break C
84 }
85 }
86
87 log.Printf("Switch %s shut down", s.MqttDevice.Identity())
88}
89
90func (s *UnifiedSwitch) HomeKitID() uint64 {
91 h64 := crc64.New(crc64.MakeTable(crc64.ISO))
92 h64.Write([]byte(s.MqttDevice.Identity()))
93 return h64.Sum64()
94}
95
96func (s *UnifiedSwitch) makeHomekitSwitch() {
97 // TODO: Get name from local DB?
98 switch dt := s.MqttDevice.(type) {
99 case *ShellyDevice:
100 s.HomeKitSwitch = accessory.NewSwitch(accessory.Info{
101 ID: s.HomeKitID(),
102 Name: s.MqttDevice.Identity(),
103 Manufacturer: dt.Manufacturer,
104 Model: dt.Model,
105 FirmwareRevision: dt.FirmwareVersion,
106 })
107 default:
108 s.HomeKitSwitch = accessory.NewSwitch(accessory.Info{
109 ID: s.HomeKitID(),
110 Name: s.MqttDevice.Identity(),
111 })
112 }
113 s.HomeKitSwitch.Switch.On.SetValue(false)
114 s.HomeKitSwitch.Switch.On.OnValueRemoteUpdate(s.HomeKitSetValue)
115}
116
117func (s *UnifiedSwitch) HomeKitSetValue(state bool) {
118 log.Printf("Client updated state: %t", state)
119 if state {
120 s.MqttDevice.OutputOn(0)
121 } else {
122 s.MqttDevice.OutputOff(0)
123 }
124}