summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2018-02-12 17:05:46 +0000
committerMike Crute <mike@crute.us>2018-02-12 17:05:46 +0000
commit15c9c1a3dd304fe4f5b37b5cc6e9f979272799ad (patch)
tree1801569726b3283cf8276644840101ceddc7bf09
downloadmfi_homekit-15c9c1a3dd304fe4f5b37b5cc6e9f979272799ad.tar.bz2
mfi_homekit-15c9c1a3dd304fe4f5b37b5cc6e9f979272799ad.tar.xz
mfi_homekit-15c9c1a3dd304fe4f5b37b5cc6e9f979272799ad.zip
Initial import, working
-rw-r--r--.gitignore6
-rw-r--r--config.example.json14
-rw-r--r--main.go257
-rwxr-xr-xpower_control.sh81
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}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..4b39e1e
--- /dev/null
+++ b/main.go
@@ -0,0 +1,257 @@
1package main
2
3import (
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
15const CMD_PATH = "/var/etc/persistent/power_control.sh"
16
17type Device struct {
18 Root *Device
19 Username string
20 Password string
21 Port int
22 Host string
23 Devices []string
24}
25
26func (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
36func (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
46func (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
56func (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
81func (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
106func (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
116func (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
130func (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
144func (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
152func (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
161type 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
171func 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
194type Output struct {
195 Device *Device
196 Name string
197}
198
199func (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
207func 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
3POWER_PATH="/proc/power"
4NUM_OUTPUTS=$(find $POWER_PATH -name 'output*' | wc -l)
5
6valid_output() {
7 [ "$1" -gt "$NUM_OUTPUTS" ] && return 1
8 [ "$1" -lt "1" ] && return 1
9 return 0
10}
11
12check_output() {
13 ! valid_output $1 && error "Invalid output number"
14}
15
16
17error() {
18 echo "{\"error\":\"$1\"}"
19 exit 1
20}
21
22set_output() {
23 echo $2 > $POWER_PATH/relay$1
24}
25
26clear_all() {
27 for i in $(seq 1 $NUM_OUTPUTS); do
28 echo 1 > $POWER_PATH/clear_ae$i
29 done
30}
31
32report() {
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
61case $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 ;;
81esac