diff options
author | Mike Crute <mike@crute.us> | 2018-02-12 17:05:46 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2018-02-12 17:05:46 +0000 |
commit | 15c9c1a3dd304fe4f5b37b5cc6e9f979272799ad (patch) | |
tree | 1801569726b3283cf8276644840101ceddc7bf09 | |
download | mfi_homekit-15c9c1a3dd304fe4f5b37b5cc6e9f979272799ad.tar.bz2 mfi_homekit-15c9c1a3dd304fe4f5b37b5cc6e9f979272799ad.tar.xz mfi_homekit-15c9c1a3dd304fe4f5b37b5cc6e9f979272799ad.zip |
Initial import, working
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | config.example.json | 14 | ||||
-rw-r--r-- | main.go | 257 | ||||
-rwxr-xr-x | power_control.sh | 81 |
4 files changed, 358 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6dbe7bf --- /dev/null +++ b/.gitignore | |||
@@ -0,0 +1,6 @@ | |||
1 | /bin/ | ||
2 | /pkg/ | ||
3 | /src/ | ||
4 | /mFi Bridge/ | ||
5 | /mfi_homekit | ||
6 | /config.json | ||
diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..dc13259 --- /dev/null +++ b/config.example.json | |||
@@ -0,0 +1,14 @@ | |||
1 | { | ||
2 | "DEFAULT" : { | ||
3 | "Username": "mfi", | ||
4 | "Password": "foobar", | ||
5 | "Port": 22 | ||
6 | }, | ||
7 | |||
8 | "Bedroom": { | ||
9 | "Host": "192.168.0.2", | ||
10 | "Devices": [ | ||
11 | "Lamp" | ||
12 | ] | ||
13 | } | ||
14 | } | ||
@@ -0,0 +1,257 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "encoding/json" | ||
5 | "errors" | ||
6 | "fmt" | ||
7 | "github.com/brutella/hc" | ||
8 | "github.com/brutella/hc/accessory" | ||
9 | "github.com/brutella/hc/characteristic" | ||
10 | "golang.org/x/crypto/ssh" | ||
11 | "io/ioutil" | ||
12 | "net" | ||
13 | ) | ||
14 | |||
15 | const CMD_PATH = "/var/etc/persistent/power_control.sh" | ||
16 | |||
17 | type Device struct { | ||
18 | Root *Device | ||
19 | Username string | ||
20 | Password string | ||
21 | Port int | ||
22 | Host string | ||
23 | Devices []string | ||
24 | } | ||
25 | |||
26 | func (d *Device) GetUsername() string { | ||
27 | if d.Username != "" { | ||
28 | return d.Username | ||
29 | } else if d.Root != nil { | ||
30 | return d.Root.Username | ||
31 | } else { | ||
32 | return "" | ||
33 | } | ||
34 | } | ||
35 | |||
36 | func (d *Device) GetPassword() string { | ||
37 | if d.Password != "" { | ||
38 | return d.Password | ||
39 | } else if d.Root != nil { | ||
40 | return d.Root.Password | ||
41 | } else { | ||
42 | return "" | ||
43 | } | ||
44 | } | ||
45 | |||
46 | func (d *Device) GetPort() int { | ||
47 | if d.Port != 0 { | ||
48 | return d.Port | ||
49 | } else if d.Root != nil { | ||
50 | return d.Root.Port | ||
51 | } else { | ||
52 | return 22 | ||
53 | } | ||
54 | } | ||
55 | |||
56 | func (d *Device) newSession() (*ssh.Client, *ssh.Session, error) { | ||
57 | cfg := &ssh.ClientConfig{ | ||
58 | User: d.GetUsername(), | ||
59 | Auth: []ssh.AuthMethod{ssh.Password(d.GetPassword())}, | ||
60 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), | ||
61 | } | ||
62 | |||
63 | // These devices use really old crufty versions of SSH | ||
64 | cfg.Config.KeyExchanges = []string{"diffie-hellman-group1-sha1"} | ||
65 | cfg.Config.Ciphers = append(cfg.Config.Ciphers, "aes128-cbc") | ||
66 | |||
67 | conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.GetPort()), cfg) | ||
68 | if err != nil { | ||
69 | return nil, nil, err | ||
70 | } | ||
71 | |||
72 | s, err := conn.NewSession() | ||
73 | if err != nil { | ||
74 | conn.Close() | ||
75 | return nil, nil, err | ||
76 | } | ||
77 | |||
78 | return conn, s, nil | ||
79 | } | ||
80 | |||
81 | func (d *Device) runCommand(cmd string) ([]DeviceOutput, error) { | ||
82 | conn, sess, err := d.newSession() | ||
83 | if err != nil { | ||
84 | return nil, err | ||
85 | } | ||
86 | defer conn.Close() | ||
87 | defer sess.Close() | ||
88 | |||
89 | out, err := sess.Output(cmd) | ||
90 | if err != nil { | ||
91 | return nil, err | ||
92 | } | ||
93 | |||
94 | if out == nil { | ||
95 | return nil, nil | ||
96 | } else { | ||
97 | var r []DeviceOutput | ||
98 | err = json.Unmarshal(out, &r) | ||
99 | if err != nil { | ||
100 | return nil, err | ||
101 | } | ||
102 | return r, nil | ||
103 | } | ||
104 | } | ||
105 | |||
106 | func (d *Device) findOutput(name string) (int, error) { | ||
107 | for i, e := range d.Devices { | ||
108 | if e == name { | ||
109 | return i + 1, nil | ||
110 | } | ||
111 | } | ||
112 | |||
113 | return 0, errors.New("Unknown device") | ||
114 | } | ||
115 | |||
116 | func (d *Device) TurnOn(n string) ([]DeviceOutput, error) { | ||
117 | o, err := d.findOutput(n) | ||
118 | if err != nil { | ||
119 | return nil, err | ||
120 | } | ||
121 | |||
122 | out, err := d.runCommand(fmt.Sprintf("%s on %d", CMD_PATH, o)) | ||
123 | if err != nil { | ||
124 | return nil, err | ||
125 | } else { | ||
126 | return out, nil | ||
127 | } | ||
128 | } | ||
129 | |||
130 | func (d *Device) TurnOff(n string) ([]DeviceOutput, error) { | ||
131 | o, err := d.findOutput(n) | ||
132 | if err != nil { | ||
133 | return nil, err | ||
134 | } | ||
135 | |||
136 | out, err := d.runCommand(fmt.Sprintf("%s off %d", CMD_PATH, o)) | ||
137 | if err != nil { | ||
138 | return nil, err | ||
139 | } else { | ||
140 | return out, nil | ||
141 | } | ||
142 | } | ||
143 | |||
144 | func (d *Device) Toggle(n string, on bool) ([]DeviceOutput, error) { | ||
145 | if on == true { | ||
146 | return d.TurnOn(n) | ||
147 | } else { | ||
148 | return d.TurnOff(n) | ||
149 | } | ||
150 | } | ||
151 | |||
152 | func (d *Device) GetReport() ([]DeviceOutput, error) { | ||
153 | out, err := d.runCommand(fmt.Sprintf("%s report", CMD_PATH)) | ||
154 | if err != nil { | ||
155 | return nil, err | ||
156 | } else { | ||
157 | return out, nil | ||
158 | } | ||
159 | } | ||
160 | |||
161 | type DeviceOutput struct { | ||
162 | Output int `json:"output"` | ||
163 | Engaged bool `json:"engaged"` | ||
164 | ActivePower float64 `json:"active_power"` | ||
165 | EnergySum float64 `json:"energy_sum"` | ||
166 | CurrentRMS float64 `json:"current_rms"` | ||
167 | VoltageRMS float64 `json:"voltage_rms"` | ||
168 | PowerFactor float64 `json:"power_factor"` | ||
169 | } | ||
170 | |||
171 | func LoadAppConfig(filename string) (map[string]*Device, error) { | ||
172 | raw, err := ioutil.ReadFile(filename) | ||
173 | if err != nil { | ||
174 | return nil, err | ||
175 | } | ||
176 | |||
177 | var cfg map[string]*Device | ||
178 | err = json.Unmarshal(raw, &cfg) | ||
179 | if err != nil { | ||
180 | return nil, err | ||
181 | } | ||
182 | |||
183 | if def, ok := cfg["DEFAULT"]; ok { | ||
184 | delete(cfg, "DEFAULT") | ||
185 | |||
186 | for _, v := range cfg { | ||
187 | v.Root = def | ||
188 | } | ||
189 | } | ||
190 | |||
191 | return cfg, nil | ||
192 | } | ||
193 | |||
194 | type Output struct { | ||
195 | Device *Device | ||
196 | Name string | ||
197 | } | ||
198 | |||
199 | func (o *Output) Toggle(on bool) error { | ||
200 | _, err := o.Device.Toggle(o.Name, on) | ||
201 | if err != nil { | ||
202 | fmt.Println(err) | ||
203 | } | ||
204 | return err | ||
205 | } | ||
206 | |||
207 | func main() { | ||
208 | cfg, err := LoadAppConfig("config.json") | ||
209 | if err != nil { | ||
210 | fmt.Println(err) | ||
211 | return | ||
212 | } | ||
213 | |||
214 | reg := []*accessory.Accessory{} | ||
215 | accs := make(map[*characteristic.Characteristic]*Output, 10) | ||
216 | |||
217 | for _, v := range cfg { | ||
218 | report, err := v.GetReport() | ||
219 | if err != nil { | ||
220 | panic(err) | ||
221 | } | ||
222 | |||
223 | for i, d := range v.Devices { | ||
224 | // Unnamed outputs are unused | ||
225 | if d == "" { | ||
226 | continue | ||
227 | } | ||
228 | |||
229 | sw := accessory.NewSwitch(accessory.Info{Name: d}) | ||
230 | sw.Switch.On.OnValueUpdateFromConn(func(_ net.Conn, c *characteristic.Characteristic, new, _ interface{}) { | ||
231 | on := new.(bool) | ||
232 | output := accs[c] | ||
233 | fmt.Printf("Client changed switch %s to %t\n", output.Name, on) | ||
234 | output.Toggle(on) | ||
235 | }) | ||
236 | accs[sw.Switch.On.Characteristic] = &Output{ | ||
237 | Device: v, | ||
238 | Name: d, | ||
239 | } | ||
240 | sw.Switch.On.UpdateValue(report[i].Engaged) | ||
241 | reg = append(reg, sw.Accessory) | ||
242 | } | ||
243 | } | ||
244 | |||
245 | br := accessory.NewBridge(accessory.Info{Name: "mFi Bridge"}) | ||
246 | |||
247 | t, err := hc.NewIPTransport(hc.Config{Pin: "00102003"}, br.Accessory, reg...) | ||
248 | if err != nil { | ||
249 | fmt.Println(err) | ||
250 | } | ||
251 | |||
252 | hc.OnTermination(func() { | ||
253 | <-t.Stop() | ||
254 | }) | ||
255 | |||
256 | t.Start() | ||
257 | } | ||
diff --git a/power_control.sh b/power_control.sh new file mode 100755 index 0000000..8e995bd --- /dev/null +++ b/power_control.sh | |||
@@ -0,0 +1,81 @@ | |||
1 | #!/bin/sh | ||
2 | |||
3 | POWER_PATH="/proc/power" | ||
4 | NUM_OUTPUTS=$(find $POWER_PATH -name 'output*' | wc -l) | ||
5 | |||
6 | valid_output() { | ||
7 | [ "$1" -gt "$NUM_OUTPUTS" ] && return 1 | ||
8 | [ "$1" -lt "1" ] && return 1 | ||
9 | return 0 | ||
10 | } | ||
11 | |||
12 | check_output() { | ||
13 | ! valid_output $1 && error "Invalid output number" | ||
14 | } | ||
15 | |||
16 | |||
17 | error() { | ||
18 | echo "{\"error\":\"$1\"}" | ||
19 | exit 1 | ||
20 | } | ||
21 | |||
22 | set_output() { | ||
23 | echo $2 > $POWER_PATH/relay$1 | ||
24 | } | ||
25 | |||
26 | clear_all() { | ||
27 | for i in $(seq 1 $NUM_OUTPUTS); do | ||
28 | echo 1 > $POWER_PATH/clear_ae$i | ||
29 | done | ||
30 | } | ||
31 | |||
32 | report() { | ||
33 | echo "[" | ||
34 | for i in $(seq 1 $NUM_OUTPUTS); do | ||
35 | relay_state=$(cat $POWER_PATH/relay$i) | ||
36 | if [ $relay_state -eq 1 ]; then | ||
37 | state="true" | ||
38 | else | ||
39 | state="false" | ||
40 | fi | ||
41 | |||
42 | echo -e "\t{" | ||
43 | echo -e "\t\t\"output\": $i," | ||
44 | echo -e "\t\t\"engaged\": $state," | ||
45 | echo -e "\t\t\"active_power\": $(cat $POWER_PATH/active_pwr$i)," | ||
46 | echo -e "\t\t\"energy_sum\": $(cat $POWER_PATH/energy_sum$i)," | ||
47 | echo -e "\t\t\"current_rms\": $(cat $POWER_PATH/i_rms$i)," | ||
48 | echo -e "\t\t\"voltage_rms\": $(cat $POWER_PATH/v_rms$i)," | ||
49 | echo -e "\t\t\"power_factor\": $(cat $POWER_PATH/pf$i)" | ||
50 | |||
51 | if [ $i -eq $NUM_OUTPUTS ]; then | ||
52 | echo -e "\t}" | ||
53 | else | ||
54 | echo -e "\t}," | ||
55 | fi | ||
56 | done | ||
57 | echo "]" | ||
58 | } | ||
59 | |||
60 | |||
61 | case $1 in | ||
62 | on) | ||
63 | check_output $2 | ||
64 | set_output $2 1 | ||
65 | report | ||
66 | ;; | ||
67 | off) | ||
68 | check_output $2 | ||
69 | set_output $2 0 | ||
70 | report | ||
71 | ;; | ||
72 | report) | ||
73 | report | ||
74 | ;; | ||
75 | clear) | ||
76 | clear_all | ||
77 | ;; | ||
78 | *) | ||
79 | error "Invalid command" | ||
80 | ;; | ||
81 | esac | ||