aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--example.go78
-rw-r--r--example_hk.go125
-rw-r--r--inform/inform.go29
-rw-r--r--inform/server.go206
-rw-r--r--inform/tx_messages.go20
5 files changed, 378 insertions, 80 deletions
diff --git a/example.go b/example.go
index 8e10090..440b4ee 100644
--- a/example.go
+++ b/example.go
@@ -1,49 +1,55 @@
1package main 1package main
2 2
3import ( 3import (
4 "encoding/json" 4 "github.com/brutella/hc"
5 "fmt" 5 "github.com/brutella/hc/accessory"
6 "github.com/mcrute/go-inform/inform" 6
7 "io/ioutil" 7 "log"
8 "os" 8 "time"
9) 9)
10 10
11func main() { 11func main() {
12 fp, err := os.Open("data/test_files/1.bin") 12 switchInfo := accessory.Info{
13 if err != nil { 13 Name: "Lamp",
14 fmt.Println("Error loading file") 14 SerialNumber: "051AC-23AAM1",
15 return 15 Manufacturer: "Foobar",
16 } 16 Model: "AB",
17 defer fp.Close()
18
19 kp, err := os.Open("data/device_keys.json")
20 if err != nil {
21 fmt.Println("Error loading key file")
22 return
23 } 17 }
24 defer kp.Close() 18 acc := accessory.NewSwitch(switchInfo)
25 19
26 var keys map[string]string 20 config := hc.Config{Pin: "12344321", Port: "12345", StoragePath: "./db"}
27 kd, _ := ioutil.ReadAll(kp) 21 t, err := hc.NewIPTransport(config, acc.Accessory)
28 json.Unmarshal(kd, &keys)
29 22
30 codec := &inform.Codec{keys}
31
32 msg, err := codec.Unmarshal(fp)
33 if err != nil { 23 if err != nil {
34 fmt.Println(err.Error()) 24 log.Fatal(err)
35 return
36 } 25 }
37 26
38 fmt.Printf("%s", msg) 27 // Log to console when client (e.g. iOS app) changes the value of the on characteristic
39 28 acc.Switch.On.OnValueRemoteUpdate(func(on bool) {
40 out, _ := os.Create("test.out") 29 if on == true {
41 defer out.Close() 30 log.Println("[INFO] Client changed switch to on")
42 31 } else {
43 pkt, err := codec.Marshal(msg) 32 log.Println("[INFO] Client changed switch to off")
44 if err != nil { 33 }
45 fmt.Println(err.Error()) 34 })
46 return 35
47 } 36 // Periodically toggle the switch's on characteristic
48 out.Write(pkt) 37 go func() {
38 for {
39 on := !acc.Switch.On.GetValue()
40 if on == true {
41 log.Println("[INFO] Switch is on")
42 } else {
43 log.Println("[INFO] Switch is off")
44 }
45 acc.Switch.On.SetValue(on)
46 time.Sleep(5 * time.Second)
47 }
48 }()
49
50 hc.OnTermination(func() {
51 t.Stop()
52 })
53
54 t.Start()
49} 55}
diff --git a/example_hk.go b/example_hk.go
new file mode 100644
index 0000000..ee89281
--- /dev/null
+++ b/example_hk.go
@@ -0,0 +1,125 @@
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "github.com/brutella/hc"
7 "github.com/brutella/hc/accessory"
8 "github.com/mcrute/go-inform/inform"
9 "io/ioutil"
10 "log"
11 "os"
12)
13
14// Load devices into state
15// Gather current initial state from devices
16// Track state transitions
17// Inputs:
18// - Homekit
19// - Devices
20
21type Port struct {
22 Label string `json:"label"`
23 Port int `json:"port"`
24}
25
26type Device struct {
27 Key string `json:"key"`
28 Name string `json:"name"`
29 Model string `json:"model"`
30 Serial string `json:"serial"`
31 Ports []*Port `json:"ports"`
32}
33
34type DeviceMap map[string]*Device
35
36func LoadKeys(file string) (DeviceMap, error) {
37 var keys DeviceMap
38
39 kp, err := os.Open(file)
40 if err != nil {
41 return nil, err
42 }
43 defer kp.Close()
44
45 kd, err := ioutil.ReadAll(kp)
46 if err != nil {
47 return nil, err
48 }
49
50 err = json.Unmarshal(kd, &keys)
51 if err != nil {
52 return nil, err
53 }
54
55 return keys, nil
56}
57
58func main() {
59 devs, err := LoadKeys("data/device_keys.json")
60 if err != nil {
61 log.Println("Error loading key file")
62 log.Println(err.Error())
63 return
64 }
65
66 keys := make(map[string]string, len(devs))
67 for i, d := range devs {
68 keys[i] = d.Key
69 }
70
71 h := inform.NewInformHandler(&inform.Codec{keys})
72 s, _ := inform.NewServer(h)
73 as := make([]*accessory.Accessory, 0, len(devs)*3)
74
75 for i, d := range devs {
76 for _, p := range d.Ports {
77 a := accessory.NewSwitch(accessory.Info{
78 Name: p.Label,
79 SerialNumber: fmt.Sprintf("%s-%d", d.Serial, p.Port),
80 Manufacturer: "Ubiquiti",
81 Model: d.Model,
82 })
83
84 // Capture these for the closure, otherwise they're bound to the
85 // single loop variable and will only see the final value of that
86 // variable
87 dev, port := i, p.Port
88
89 a.Switch.On.OnValueRemoteUpdate(func(on bool) {
90 h.SetState(dev, port, on)
91 })
92
93 h.AddPort(dev, port)
94 as = append(as, a.Accessory)
95 }
96 }
97
98 // The root accessory is what gets used to name the bridge so let's make it
99 // an actual bridge
100 br := accessory.New(accessory.Info{
101 Name: "UnifiBridge",
102 Manufacturer: "Mike Crute",
103 Model: "0.1",
104 }, accessory.TypeBridge)
105
106 config := hc.Config{
107 Pin: "12344321",
108 Port: "12345",
109 StoragePath: "./db",
110 }
111
112 t, err := hc.NewIPTransport(config, br, as...)
113 if err != nil {
114 log.Fatal(err)
115 return
116 }
117
118 hc.OnTermination(func() {
119 t.Stop()
120 os.Exit(0) // Otherwise homekit doesn't actually stop
121 })
122
123 go t.Start()
124 s.ListenAndServe()
125}
diff --git a/inform/inform.go b/inform/inform.go
index 5c3ca1c..ac3b57d 100644
--- a/inform/inform.go
+++ b/inform/inform.go
@@ -27,12 +27,26 @@ type InformWrapper struct {
27 27
28// Create InformWrapper with sane defaults 28// Create InformWrapper with sane defaults
29func NewInformWrapper() *InformWrapper { 29func NewInformWrapper() *InformWrapper {
30 return &InformWrapper{ 30 w := &InformWrapper{
31 Version: INFORM_VERSION, 31 Version: INFORM_VERSION,
32 MacAddr: make([]byte, 6), 32 MacAddr: make([]byte, 6),
33 Flags: 0, 33 Flags: 0,
34 DataVersion: DATA_VERSION, 34 DataVersion: DATA_VERSION,
35 } 35 }
36
37 // Almost all messages are encrypted outside of provisioning so default
38 // this and make users explicitly disable it.
39 w.SetEncrypted(true)
40
41 return w
42}
43
44// Create an InformWrapper that is a response to an incoming wrapper. Copies
45// all necessary data for a response so callers can just set a payload
46func NewInformWrapperResponse(msg *InformWrapper) *InformWrapper {
47 w := NewInformWrapper()
48 copy(w.MacAddr, msg.MacAddr)
49 return w
36} 50}
37 51
38// Update the payload data with JSON value 52// Update the payload data with JSON value
@@ -45,6 +59,19 @@ func (i *InformWrapper) UpdatePayload(v interface{}) error {
45 } 59 }
46} 60}
47 61
62// Unmarshal a payload body that we received from a device. Does not work for
63// user-set messages
64func (i *InformWrapper) UnmarshalPayload() (*DeviceMessage, error) {
65 var m DeviceMessage
66
67 err := json.Unmarshal(i.Payload, &m)
68 if err != nil {
69 return nil, err
70 }
71
72 return &m, nil
73}
74
48// Format Mac address bytes as lowercase string with colons 75// Format Mac address bytes as lowercase string with colons
49func (i *InformWrapper) FormattedMac() string { 76func (i *InformWrapper) FormattedMac() string {
50 return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", 77 return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
diff --git a/inform/server.go b/inform/server.go
index 7e33a86..710b8ac 100644
--- a/inform/server.go
+++ b/inform/server.go
@@ -1,55 +1,155 @@
1package inform 1package inform
2 2
3import ( 3import (
4 "container/list"
5 "errors"
4 "fmt" 6 "fmt"
5 "log" 7 "log"
6 "net/http" 8 "net/http"
9 "sync"
7 "time" 10 "time"
8) 11)
9 12
10type StateTree struct { 13func Log(handler http.Handler) http.Handler {
11 states map[string]map[int]int 14 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15 handler.ServeHTTP(w, r)
16 t := time.Now().Format("02/Jan/2006 15:04:05")
17 log.Printf("%s - - [%s] \"%s %s %s\"", r.RemoteAddr, t, r.Method, r.URL, r.Proto)
18 })
12} 19}
13 20
14func NewStateTree() *StateTree { 21type Device struct {
15 return &StateTree{make(map[string]map[int]int)} 22 Initialized bool
23 CurrentState bool
24 DesiredState bool
25 *sync.Mutex
26}
27
28type InformHandler struct {
29 Codec *Codec
30 ports map[string]map[int]*Device
31 queue map[string]*list.List
32 *sync.RWMutex
33}
34
35func NewInformHandler(c *Codec) *InformHandler {
36 return &InformHandler{
37 Codec: c,
38 ports: make(map[string]map[int]*Device),
39 queue: make(map[string]*list.List),
40 RWMutex: &sync.RWMutex{},
41 }
16} 42}
17 43
18func (t *StateTree) ensureNode(device string, port int) { 44func (h *InformHandler) AddPort(dev string, port int) {
19 _, ok := t.states[device] 45 h.Lock()
46 defer h.Unlock()
47
48 _, ok := h.ports[dev]
20 if !ok { 49 if !ok {
21 t.states[device] = make(map[int]int) 50 h.ports[dev] = make(map[int]*Device)
22 } 51 }
23 52
24 _, ok = t.states[device][port] 53 _, ok = h.queue[dev]
25 if !ok { 54 if !ok {
26 t.states[device][port] = 0 55 log.Printf("Adding queue for %s", dev)
56 h.queue[dev] = list.New()
57 }
58
59 log.Printf("Adding %s port %d", dev, port)
60 h.ports[dev][port] = &Device{
61 Mutex: &sync.Mutex{},
27 } 62 }
28} 63}
29 64
30func (t *StateTree) GetState(device string, port int) int { 65func (h *InformHandler) getPort(dev string, port int) (*Device, error) {
31 t.ensureNode(device, port) 66 h.RLock()
32 return t.states[device][port] 67 defer h.RUnlock()
68
69 _, ok := h.ports[dev]
70 if !ok {
71 return nil, errors.New("No device found")
72 }
73
74 p, ok := h.ports[dev][port]
75 if !ok {
76 return nil, errors.New("No port found")
77 }
78
79 return p, nil
33} 80}
34 81
35func (t *StateTree) SetState(device string, port, value int) { 82func (h *InformHandler) SetState(dev string, port int, state bool) error {
36 t.ensureNode(device, port) 83 p, err := h.getPort(dev, port)
37 t.states[device][port] = value 84 if err != nil {
85 return err
86 }
87
88 p.Lock()
89 defer p.Unlock()
90
91 log.Printf("Set state to %t for %s port %d", state, dev, port)
92 p.DesiredState = state
93 return nil
38} 94}
39 95
40func Log(handler http.Handler) http.Handler { 96func (h *InformHandler) buildCommands(dev string, pl *DeviceMessage) error {
41 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 97 for _, o := range pl.Outputs {
42 handler.ServeHTTP(w, r) 98 ds, err := h.getPort(dev, o.Port)
43 t := time.Now().Format("02/Jan/2006 15:04:05") 99 if err != nil {
44 log.Printf("%s - - [%s] \"%s %s %s\"", r.RemoteAddr, t, r.Method, r.URL, r.Proto) 100 return err
45 // Addr - - [D/M/Y H:M:S] "Method RequestURI Proto" Code Size 101 }
46 // 127.0.0.1 - - [24/Sep/2016 14:30:35] "GET / HTTP/1.1" 200 - 102 ds.Lock()
47 }) 103
104 // Get initial state
105 if !ds.Initialized {
106 ds.CurrentState = o.OutputState
107 ds.Initialized = true
108 return nil
109 }
110
111 // State didn't change at the sensor
112 if ds.CurrentState == o.OutputState {
113 if ds.DesiredState != o.OutputState {
114 log.Printf("Toggle state %t for %s port %d", ds.DesiredState, dev, o.Port)
115 // Generate change command
116 // TODO: Don't lock the whole handler
117 h.Lock()
118 h.queue[dev].PushFront(NewOutputCommand(o.Port, ds.DesiredState, 0))
119 h.Unlock()
120 }
121 } else { // Sensor caused the change, leave it alone
122 log.Printf("Sensor state changed %s port %d", dev, o.Port)
123 ds.DesiredState = o.OutputState
124 }
125
126 ds.CurrentState = o.OutputState
127 ds.Unlock() // Don't hold the lock the entire loop
128 }
129
130 return nil
48} 131}
49 132
50type InformHandler struct { 133func (h *InformHandler) pop(dev string) *CommandMessage {
51 Codec *Codec 134 // TODO: Don't lock the whole handler
52 StateTree *StateTree 135 h.Lock()
136 defer h.Unlock()
137
138 q, ok := h.queue[dev]
139 if !ok {
140 log.Printf("No queue for %s", dev)
141 return nil
142 }
143
144 e := q.Front()
145 if e != nil {
146 h.queue[dev].Remove(e)
147 cmd := e.Value.(*CommandMessage)
148 cmd.Freshen()
149 return cmd
150 }
151
152 return nil
53} 153}
54 154
55func (h *InformHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 155func (h *InformHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -58,35 +158,61 @@ func (h *InformHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
58 return 158 return
59 } 159 }
60 160
61 if r.URL != nil && r.URL.Path != "/inform" { 161 msg, err := h.Codec.Unmarshal(r.Body)
62 http.Error(w, "404 Not Found", http.StatusNotFound) 162 if err != nil {
163 log.Printf("Unmarshal message: %s", err.Error())
164 http.Error(w, "400 Bad Request", http.StatusBadRequest)
63 return 165 return
64 } 166 }
65 167
66 msg, err := h.Codec.Unmarshal(r.Body) 168 pl, err := msg.UnmarshalPayload()
67 if err != nil { 169 if err != nil {
68 http.Error(w, "Bad Request", http.StatusBadRequest) 170 log.Printf("Unmarshal payload: %s", err.Error())
171 http.Error(w, "400 Bad Request", http.StatusBadRequest)
69 return 172 return
70 } 173 }
71 174
72 pl := NewInformWrapper() 175 dev := msg.FormattedMac()
73 copy(pl.MacAddr, msg.MacAddr) 176 ret := NewInformWrapperResponse(msg)
74 pl.SetEncrypted(true) 177 log.Printf("Inform from %s", dev)
75 178
76 // TODO: compare current state to tree and update 179 // Send a command until the queue is empty
180 if cmd := h.pop(dev); cmd != nil {
181 ret.UpdatePayload(cmd)
182 } else {
183 // Update internal state vs reality
184 if err := h.buildCommands(msg.FormattedMac(), pl); err != nil {
185 http.Error(w, "500 Server Error", 500)
186 return
187 }
188
189 // If that generated a command send it
190 if cmd = h.pop(dev); cmd != nil {
191 ret.UpdatePayload(cmd)
192 } else {
193 // Otherwise noop
194 ret.UpdatePayload(NewNoop(10))
195 }
196 }
77 197
78 res, err := h.Codec.Marshal(pl) 198 res, err := h.Codec.Marshal(ret)
79 if err != nil { 199 if err != nil {
80 http.Error(w, "Server Error", 500) 200 http.Error(w, "500 Server Error", 500)
81 return 201 return
82 } 202 }
83 203
84 fmt.Fprintf(w, "%s", res) 204 fmt.Fprintf(w, "%s", res)
85} 205}
86 206
87func NewServer(handler *InformHandler) *http.Server { 207// Create a new server, returns the mux so users can add other methods (for
208// example, if they want to share a process to build a console that also
209// accepts informs)
210func NewServer(handler *InformHandler) (*http.Server, *http.ServeMux) {
211 mux := http.NewServeMux()
212 mux.Handle("/inform", handler)
213
88 return &http.Server{ 214 return &http.Server{
89 Addr: ":6080", 215 Addr: ":6080",
90 Handler: Log(handler), 216 Handler: Log(mux),
91 } 217 }, mux
92} 218}
diff --git a/inform/tx_messages.go b/inform/tx_messages.go
index 0021ef3..8962cad 100644
--- a/inform/tx_messages.go
+++ b/inform/tx_messages.go
@@ -25,8 +25,15 @@ type CommandMessage struct {
25 Voltage int `json:"volt,omitempty"` 25 Voltage int `json:"volt,omitempty"`
26} 26}
27 27
28func NewOutputCommand(port, val, timer int) *CommandMessage { 28// Freshen timestamps
29 return &CommandMessage{ 29func (m *CommandMessage) Freshen() {
30 m.DateTime = time.Now().Format(time.RFC3339)
31 m.ServerTime = unixMicroPSTString()
32 m.Time = unixMicroPST()
33}
34
35func NewOutputCommand(port int, val bool, timer int) *CommandMessage {
36 m := &CommandMessage{
30 Type: "cmd", 37 Type: "cmd",
31 Command: "mfi-output", 38 Command: "mfi-output",
32 DateTime: time.Now().Format(time.RFC3339), 39 DateTime: time.Now().Format(time.RFC3339),
@@ -34,8 +41,15 @@ func NewOutputCommand(port, val, timer int) *CommandMessage {
34 ServerTime: unixMicroPSTString(), 41 ServerTime: unixMicroPSTString(),
35 Time: unixMicroPST(), 42 Time: unixMicroPST(),
36 Timer: timer, 43 Timer: timer,
37 Value: val,
38 } 44 }
45
46 if val {
47 m.Value = 1
48 } else {
49 m.Value = 0
50 }
51
52 return m
39} 53}
40 54
41type NoopMessage struct { 55type NoopMessage struct {