package main import ( "encoding/json" "errors" "fmt" "io/ioutil" "net" "time" "github.com/brutella/hc" "github.com/brutella/hc/accessory" "github.com/brutella/hc/characteristic" "golang.org/x/crypto/ssh" ) const ( CMD_PATH = "/var/etc/persistent/power_control.sh" ) type Device struct { Root *Device Name string Username string Password string Port int Host string Devices []string sshClient *ssh.Client } func (d *Device) GetUsername() string { if d.Username != "" { return d.Username } else if d.Root != nil { return d.Root.Username } else { return "" } } func (d *Device) GetPassword() string { if d.Password != "" { return d.Password } else if d.Root != nil { return d.Root.Password } else { return "" } } func (d *Device) GetPort() int { if d.Port != 0 { return d.Port } else if d.Root != nil { return d.Root.Port } else { return 22 } } func (d *Device) Connect() error { cfg := &ssh.ClientConfig{ User: d.GetUsername(), Auth: []ssh.AuthMethod{ssh.Password(d.GetPassword())}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 0, } // These devices use really old crufty versions of SSH cfg.Config.KeyExchanges = []string{"diffie-hellman-group1-sha1"} cfg.Config.Ciphers = append(cfg.Config.Ciphers, "aes128-cbc") conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.GetPort()), cfg) if err != nil { return err } d.sshClient = conn return nil } func (d *Device) Disconnect() { d.sshClient.Close() } func (d *Device) newSession() (*ssh.Session, error) { for i := 3; i > 0; i-- { if s, err := d.sshClient.NewSession(); err == nil { return s, nil } else if err != nil && i == 0 { fmt.Printf("Session error, failing: %s\n", err) return nil, err } fmt.Println("Session error, reconnecting") d.Connect() } return nil, errors.New("Unable to connect via SSH") } func (d *Device) runCommand(cmd string) ([]DeviceOutput, error) { sess, err := d.newSession() if err != nil { fmt.Printf("Session error: %s\n", err) return nil, err } defer sess.Close() out, err := sess.Output(cmd) if err != nil { fmt.Printf("Command error: %s\n", err) return nil, err } if out == nil { fmt.Printf("Nil return\n") return nil, nil } else { var r []DeviceOutput err = json.Unmarshal(out, &r) if err != nil { return nil, err } return r, nil } } func (d *Device) findOutput(name string) (int, error) { for i, e := range d.Devices { if e == name { return i + 1, nil } } return 0, errors.New("Unknown device") } func (d *Device) TurnOn(n string) ([]DeviceOutput, error) { o, err := d.findOutput(n) if err != nil { return nil, err } out, err := d.runCommand(fmt.Sprintf("%s on %d", CMD_PATH, o)) if err != nil { return nil, err } else { return out, nil } } func (d *Device) TurnOff(n string) ([]DeviceOutput, error) { o, err := d.findOutput(n) if err != nil { return nil, err } out, err := d.runCommand(fmt.Sprintf("%s off %d", CMD_PATH, o)) if err != nil { return nil, err } else { return out, nil } } func (d *Device) Toggle(n string, on bool) ([]DeviceOutput, error) { if on == true { return d.TurnOn(n) } else { return d.TurnOff(n) } } func (d *Device) GetReport() ([]DeviceOutput, error) { out, err := d.runCommand(fmt.Sprintf("%s report", CMD_PATH)) if err != nil { return nil, err } else { return out, nil } } type DeviceOutput struct { Output int `json:"output"` Engaged bool `json:"engaged"` ActivePower float64 `json:"active_power"` EnergySum float64 `json:"energy_sum"` CurrentRMS float64 `json:"current_rms"` VoltageRMS float64 `json:"voltage_rms"` PowerFactor float64 `json:"power_factor"` } func LoadAppConfig(filename string) ([]*Device, error) { raw, err := ioutil.ReadFile(filename) if err != nil { return nil, err } var cfg []*Device err = json.Unmarshal(raw, &cfg) if err != nil { return nil, err } if def := cfg[0]; def.Name == "DEFAULT" { cfg = cfg[1:] for _, v := range cfg { v.Root = def } } return cfg, nil } type Output struct { Device *Device Name string } func (o *Output) Toggle(on bool) error { _, err := o.Device.Toggle(o.Name, on) if err != nil { fmt.Println(err) } return err } func GatherReports(devs []*Device) { t := time.NewTicker(10 * time.Second) defer t.Stop() for { <-t.C for _, d := range devs { d.GetReport() } } } func main() { devs, err := LoadAppConfig("config.json") if err != nil { fmt.Println(err) return } reg := []*accessory.Accessory{} accs := make(map[*characteristic.Characteristic]*Output, 10) // TODO: Do this in a goroutine and add retries for devices so one device // doesn't block booting the whole controller for _, k := range devs { fmt.Printf("Connecting to %s\n", k.Name) // TODO: Upgrade or install controller script when first connecting if err = k.Connect(); err != nil { panic(err) } report, err := k.GetReport() if err != nil { panic(err) } // TODO: Allow the homekit app to provide device and output names for i, d := range k.Devices { // Unnamed outputs are unused if d == "" { continue } sw := accessory.NewSwitch(accessory.Info{Name: d}) sw.Switch.On.OnValueUpdateFromConn(func(_ net.Conn, c *characteristic.Characteristic, new, _ interface{}) { on := new.(bool) output := accs[c] fmt.Printf("Client changed switch %s to %t\n", output.Name, on) output.Toggle(on) }) accs[sw.Switch.On.Characteristic] = &Output{ Device: k, Name: d, } sw.Switch.On.UpdateValue(report[i].Engaged) reg = append(reg, sw.Accessory) } } br := accessory.NewBridge(accessory.Info{Name: "mFi Bridge"}) t, err := hc.NewIPTransport(hc.Config{Pin: "00102003", Port: "6969"}, br.Accessory, reg...) if err != nil { fmt.Println(err) } hc.OnTermination(func() { <-t.Stop() }) // Gathering reports also keeps the SSH connections alive go GatherReports(devs) t.Start() }