diff options
-rw-r--r-- | agent/mfi_agent.go | 227 |
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 | |||
26 | package main | ||
27 | |||
28 | import ( | ||
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 | |||
40 | var upgrader = websocket.Upgrader{} | ||
41 | |||
42 | type Command struct { | ||
43 | Cmd string | ||
44 | Args []string | ||
45 | FailOk bool | ||
46 | } | ||
47 | |||
48 | type 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 | |||
59 | type 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. | ||
67 | func 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. | ||
87 | func 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. | ||
123 | func 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 | ||
146 | func 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 | |||
165 | func 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. | ||
181 | func 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 | ||
203 | func isConfigured() bool { | ||
204 | if err := exec.Command("grep", "mfi_agent", "/etc/inittab").Run(); err != nil { | ||
205 | return false | ||
206 | } | ||
207 | return true | ||
208 | } | ||
209 | |||
210 | func 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 | } | ||