summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2019-11-26 14:40:18 -0800
committerMike Crute <mike@crute.us>2019-11-26 14:40:18 -0800
commit3009b54d04c8b5bfcfa05c6373d67b92db7c8d27 (patch)
tree54c3e4db46b8094dc5bf993106bb08fbef07d702
parent15573b8deb2bdd582b4e039680abb5cb56f66c08 (diff)
downloadmfi_homekit-3009b54d04c8b5bfcfa05c6373d67b92db7c8d27.tar.bz2
mfi_homekit-3009b54d04c8b5bfcfa05c6373d67b92db7c8d27.tar.xz
mfi_homekit-3009b54d04c8b5bfcfa05c6373d67b92db7c8d27.zip
Remove old Go agent
-rw-r--r--agent/mfi_agent.go227
1 files changed, 0 insertions, 227 deletions
diff --git a/agent/mfi_agent.go b/agent/mfi_agent.go
deleted file mode 100644
index 901d0d1..0000000
--- a/agent/mfi_agent.go
+++ /dev/null
@@ -1,227 +0,0 @@
1//
2// An agent to control/report on mFi devices
3//
4// This agent runs on mFi devices and provides a websocket server that can be
5// connected to by remote agents to control the device. The protocol is simple
6// JSON over websockets. All commands will also return a report of the current
7// state of the device. Reports are collected asynchronously to improve speed
8// of the control channel.
9//
10// This daemon will also bootstrap itself into init since each reboot rewrites
11// the core init files. On first run it will add itself to /etc/inittab, clean
12// up some half-completed UBNT garbage that just wastes memory, and then
13// return. init will start the real version of the process and keep it running.
14//
15// Devices only have about 5MB of free disk space and about as much free RAM so
16// keeping the size of this process small is important. Go will over-allocate
17// virtual memory by a factor of about 22 but the RSS of the process is
18// currently about 1MB so it doesn't get OOM killed.
19//
20// To compile, strip and compress:
21//
22// GOOS=linux GOARCH=mips go build -ldflags="-s -w" mfi_agent.go
23// upx mfi_agent
24//
25
26package main
27
28import (
29 "fmt"
30 "github.com/gorilla/websocket"
31 "io/ioutil"
32 "log"
33 "net/http"
34 "os/exec"
35 "path/filepath"
36 "strconv"
37 "time"
38)
39
40var upgrader = websocket.Upgrader{}
41
42type Command struct {
43 Cmd string
44 Args []string
45 FailOk bool
46}
47
48type PowerInfo struct {
49 Output int
50 Engaged bool
51 ActivePower float64
52 CurrentRMS float64
53 VoltageRMS float64
54 PowerFactor float64
55 EnergySum float64
56 status string
57}
58
59type CommandMessage struct {
60 Type string
61 Output int `json:",omitempty"`
62 Engage bool `json:",omitempty"`
63}
64
65// Collect and deliver reports asynchronously. Doing these in-line casues about
66// 1 second delay in the control channel.
67func reporter(rchan chan chan []*PowerInfo, done <-chan bool) {
68 var report []*PowerInfo
69
70 ticker := time.NewTicker(time.Second)
71 defer ticker.Stop()
72
73 for {
74 select {
75 case <-ticker.C:
76 report, _ = makeReport()
77 case rc := <-rchan:
78 rc <- report
79 case <-done:
80 return
81 }
82 }
83}
84
85// Websocket connection handler. Sends receives JSON over a websocket
86// connection. Will return an error if the connection can't be upgraded.
87func mfiController(rchan chan chan []*PowerInfo, w http.ResponseWriter, r *http.Request) {
88 c, err := upgrader.Upgrade(w, r, nil)
89 if err != nil {
90 log.Println("ERROR: upgrade:", err)
91 return
92 }
93 defer c.Close()
94
95 res := make(chan []*PowerInfo)
96
97 for {
98 var message CommandMessage
99 err = c.ReadJSON(&message)
100 if err != nil {
101 log.Println("ERROR: read:", err)
102 break
103 }
104
105 if message.Type == "set" {
106 setRelay(message.Output, message.Engage)
107 }
108
109 rchan <- res
110 report := <-res
111
112 err = c.WriteJSON(report)
113 if err != nil {
114 log.Println("ERROR: write:", err)
115 break
116 }
117 }
118}
119
120// /dev/power* devices contain a report of the current sensor state of the
121// PL7223 chips. This function reads them and converts one device and converts
122// the output into a PowerInfo struct.
123func parseOutput(path string) (*PowerInfo, error) {
124 c, err := ioutil.ReadFile(path)
125 if err != nil {
126 return nil, err
127 }
128
129 idx, err := strconv.Atoi(string(path[len(path)-1]))
130 if err != nil {
131 return nil, err
132 }
133
134 out := &PowerInfo{Output: idx}
135 fmt.Sscanf(
136 string(c), "%s %f\n %f\n %f\n %f\n %f",
137 &out.status, &out.ActivePower, &out.CurrentRMS,
138 &out.VoltageRMS, &out.PowerFactor, &out.EnergySum)
139
140 out.Engaged = (out.status == "on")
141
142 return out, nil
143}
144
145// Gather reports for all PL7223 devices in the system
146func makeReport() ([]*PowerInfo, error) {
147 files, err := filepath.Glob("/dev/power?")
148 if err != nil {
149 return nil, err
150 }
151
152 reports := make([]*PowerInfo, 0, len(files))
153
154 for _, fn := range files {
155 o, err := parseOutput(fn)
156 if err != nil {
157 return nil, err
158 }
159 reports = append(reports, o)
160 }
161
162 return reports, nil
163}
164
165func setRelay(n int, on bool) error {
166 value := []byte{'0'}
167 if on {
168 value[0] = '1'
169 }
170
171 return ioutil.WriteFile(
172 fmt.Sprintf("/proc/power/relay%d", n), value, 0)
173}
174
175// Kill off some junk processes that don't really do anything on the current
176// system to save RAM. Add ourselves to /etc/inittab and remove some more junk
177// processes that inittab will continually respawn otherwise. Then HUP init to
178// inform it of the config file changes. Finally kill off the remaining
179// processes that we just commented out. /etc is transient and overwritten on
180// every boot so we have to do this every time.
181func bootstrap() error {
182 cmds := []Command{
183 Command{"pkill", []string{"-9", "upnpd"}, true},
184 Command{"pkill", []string{"-9", "avahi"}, true},
185 Command{"sh", []string{"-c", "echo null::respawn:/var/etc/persistent/mfi_agent >> /etc/inittab"}, false},
186 Command{"sed", []string{"-i", "-e", "/ubnt-websockets/s/^/#/", "-e", "/telnetd/s/^/#/", "/etc/inittab"}, false},
187 Command{"kill", []string{"-HUP", "1"}, false},
188 Command{"pkill", []string{"-9", "ubnt-websockets"}, true},
189 Command{"pkill", []string{"-9", "telnetd"}, true},
190 }
191
192 for _, cmd := range cmds {
193 if err := exec.Command(cmd.Cmd, cmd.Args...).Run(); err != nil && !cmd.FailOk {
194 log.Fatalf("ERROR: cmd: %s %s", cmd, err)
195 return err
196 }
197 }
198
199 return nil
200}
201
202// Check if we're configured in init
203func isConfigured() bool {
204 if err := exec.Command("grep", "mfi_agent", "/etc/inittab").Run(); err != nil {
205 return false
206 }
207 return true
208}
209
210func main() {
211 if !isConfigured() {
212 bootstrap()
213 return
214 }
215
216 done := make(chan bool)
217 rc := make(chan chan []*PowerInfo)
218
219 log.Printf("Running server")
220 go reporter(rc, done)
221
222 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
223 mfiController(rc, w, r)
224 })
225 log.Fatal(http.ListenAndServe("0.0.0.0:9090", nil))
226 close(done)
227}