func main() { flag.Float64Var(&temp, "temp", 20.0, "Target temperature") flag.StringVar(&tempPath, "tempPath", "", "Target temperature path") flag.StringVar(&device, "device", "", "Thermometer device") flag.BoolVar(&verbose, "verbose", false, "Verbose logging") flag.Parse() log.Verbose = verbose if device == "" { log.Fatal("No device specified") } if tempPath != "" { _, err := os.Stat(tempPath) if err != nil { log.Fatal(err) } log.Println("[INFO] Starting thermostat with temperature from path:", tempPath) } else { log.Println("[INFO] Starting thermostat with target temperature:", temp) } log.Println("[VERB] Using thermometer at", device) boiler := NewBoiler() thermometer := NewThermometer(device) go thermometer.RunLoop() thermostat := NewThermostat(boiler, thermometer, temp) go thermostat.RunLoop() if tempPath != "" { tempPoller := NewTempPoller(thermostat, tempPath) temp = tempPoller.PollTemp() go tempPoller.RunLoop() } api := NewAPI(thermostat) go api.RunLoop() homekitService := NewHomeKitService(thermostat) go homekitService.RunLoop() // Handle SIGINT and SIGTERM. ch := make(chan os.Signal) signal.Notify(ch, os.Interrupt, os.Kill, syscall.SIGINT, syscall.SIGTERM) signal := <-ch log.Println("[INFO] Received signal", signal, "terminating") homekitService.Stop() thermostat.Stop() }
func setupHive() { // Connect to Hive var err error hiveHome, err = hive.Connect(hive.Config{ Username: username, Password: password, RefreshInterval: 30 * time.Second, }) if err != nil { log.Fatal(err) return } hiveHome.HandleStateChange(func(state *hive.State) { accessoryUpdate.Lock() defer accessoryUpdate.Unlock() log.Printf("[VERB] Syncing status with HomeKit\n") hotWaterSwitch.Switch.On.SetValue(state.HotWater) heatingBoostSwitch.Switch.On.SetValue(state.HeatingBoosted) thermostat.Thermostat.CurrentTemperature.SetValue(state.CurrentTemp) thermostat.Thermostat.TargetTemperature.SetValue(state.TargetTemp) thermostat.Thermostat.CurrentHeatingCoolingState.SetValue(modeForHiveMode(state.CurrentHeatingMode)) thermostat.Thermostat.TargetHeatingCoolingState.SetValue(modeForHiveMode(state.TargetHeatingMode)) }) }
func (tp *TempPoller) PollTemp() float64 { file, err := os.Open(tp.path) if err != nil { log.Fatal(err) } defer file.Close() var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } f, err := strconv.ParseFloat(lines[0], 64) if err != nil { log.Fatal(err) } return f }
// transportUUIDInStorage returns the uuid stored in storage or // creates a new random uuid and stores it. func transportUUIDInStorage(storage util.Storage) string { uuid, err := storage.Get("uuid") if len(uuid) == 0 || err != nil { str := util.RandomHexString() uuid = []byte(netio.MAC48Address(str)) err := storage.Set("uuid", uuid) if err != nil { log.Fatal(err) } } return string(uuid) }
// Server -> Client // - B: server public key // - signature: from server session public key, server name, client session public key func (verify *VerifyServerController) handlePairVerifyStart(in util.Container) (util.Container, error) { verify.step = VerifyStepStartResponse clientPublicKey := in.GetBytes(TagPublicKey) log.Println("[VERB] -> A:", hex.EncodeToString(clientPublicKey)) if len(clientPublicKey) != 32 { return nil, errInvalidClientKeyLength } var otherPublicKey [32]byte copy(otherPublicKey[:], clientPublicKey) verify.session.GenerateSharedKeyWithOtherPublicKey(otherPublicKey) verify.session.SetupEncryptionKey([]byte("Pair-Verify-Encrypt-Salt"), []byte("Pair-Verify-Encrypt-Info")) device := verify.context.GetSecuredDevice() var material []byte material = append(material, verify.session.PublicKey[:]...) material = append(material, device.Name()...) material = append(material, clientPublicKey...) signature, err := crypto.ED25519Signature(device.PrivateKey(), material) if err != nil { log.Fatal(err) return nil, err } // Encrypt encryptedOut := util.NewTLV8Container() encryptedOut.SetString(TagUsername, device.Name()) encryptedOut.SetBytes(TagSignature, signature) encryptedBytes, mac, _ := chacha20poly1305.EncryptAndSeal(verify.session.EncryptionKey[:], []byte("PV-Msg02"), encryptedOut.BytesBuffer().Bytes(), nil) out := util.NewTLV8Container() out.SetByte(TagSequence, verify.step.Byte()) out.SetBytes(TagPublicKey, verify.session.PublicKey[:]) out.SetBytes(TagEncryptedData, append(encryptedBytes, mac[:]...)) log.Println("[VERB] K:", hex.EncodeToString(verify.session.EncryptionKey[:])) log.Println("[VERB] B:", hex.EncodeToString(verify.session.PublicKey[:])) log.Println("[VERB] S:", hex.EncodeToString(verify.session.PrivateKey[:])) log.Println("[VERB] Shared:", hex.EncodeToString(verify.session.SharedKey[:])) log.Println("[VERB] <- B:", hex.EncodeToString(out.GetBytes(TagPublicKey))) return out, nil }
func GetHKSwitch(sensor *MPSensor) *HKSwitch { hkSwitch, found := switches[sensor.port] if found { return hkSwitch } label := fmt.Sprintf("mPower Port %d", sensor.port) log.Printf("[INFO] Creating New HKSwitch for %s", label) info := accessory.Info{ Name: label, Manufacturer: "Ubiquiti Networks", } acc := accessory.NewSwitch(info) acc.Switch.On.SetValue(sensor.output) config := hc.Config{Pin: pin} transport, err := hc.NewIPTransport(config, acc.Accessory) if err != nil { log.Fatal(err) } go func() { transport.Start() }() hkSwitch = &HKSwitch{acc, sensor, nil, transport} switches[sensor.port] = hkSwitch acc.OnIdentify(func() { timeout := 1 * time.Second for i := 0; i < 4; i++ { ToggleSensor(sensor) time.Sleep(timeout) } }) acc.Switch.On.OnValueRemoteUpdate(func(on bool) { SetOutput(sensor, on) }) return hkSwitch }
// Publish announces the service for the machine's ip address on a random port using mDNS. func (s *MDNSService) Publish() error { ip, err := GetFirstLocalIPAddress() if err != nil { return err } log.Println("[INFO] Accessory IP is", ip) // Host should end with '.' hostname, _ := os.Hostname() host := fmt.Sprintf("%s.", strings.Trim(hostname, ".")) text := s.txtRecords() server, err := bonjour.RegisterProxy(s.name, "_hap._tcp.", "", s.port, host, ip.String(), text, nil) if err != nil { log.Fatal(err) } s.server = server return err }
func NewHomeKitService(thermostat *Thermostat) *HomeKitService { thermostatInfo := model.Info{ Name: "Thermostat", } hkThermostat := accessory.NewThermostat(thermostatInfo, temp, 17, 25, 0.5) hkThermostat.SetTargetMode(model.HeatCoolModeHeat) hkThermostat.OnTargetTempChange(func(temp float64) { log.Println("[INFO] HomeKit requested thermostat to change to", temp) thermostat.targetTemp = temp }) hkThermostat.OnTargetModeChange(func(mode model.HeatCoolModeType) { log.Println("[INFO] HomeKit requested thermostat to change to", mode) switch mode { case model.HeatCoolModeHeat: log.Println("[INFO] HomeKit setting thermostat to default on temp of", defaultOnTemp) thermostat.targetTemp = defaultOnTemp case model.HeatCoolModeOff: log.Println("[INFO] HomeKit setting thermostat to default off temp of", defaultOffTemp) thermostat.targetTemp = defaultOffTemp case model.HeatCoolModeAuto, model.HeatCoolModeCool: hkThermostat.SetTargetMode(model.HeatCoolModeHeat) } }) transport, err := hap.NewIPTransport("24282428", hkThermostat.Accessory) if err != nil { log.Fatal(err) } t := HomeKitService{ thermostat: thermostat, done: make(chan bool), hkThermostat: hkThermostat, transport: transport, } return &t }
// NewIPTransport creates a transport to provide accessories over IP. // The pairing is secured using a 8-numbers pin. // If more than one accessory is provided, the first becomes a bridge in HomeKit. // It's fine when the bridge has no explicit services. // // All accessory specific data (crypto keys, ids) is stored in a folder named after the first accessory. // So changing the order of the accessories or renaming the first accessory makes the stored // data inaccessible to the tranport. In this case new crypto keys are created and the accessory // appears as a new one to clients. func NewIPTransport(pin string, pth string, a *accessory.Accessory, as ...*accessory.Accessory) (Transport, error) { // Find transport name which is visible in mDNS name := a.Name() if len(name) == 0 { log.Fatal("Invalid empty name for first accessory") } hapPin, err := NewPin(pin) if err != nil { return nil, err } storage, err := util.NewFileStorage(path.Join(pth, name)) if err != nil { return nil, err } // Find transport uuid which appears as "id" txt record in mDNS and // must be unique and stay the same over time uuid := transportUUIDInStorage(storage) database := db.NewDatabaseWithStorage(storage) device, err := netio.NewSecuredDevice(uuid, hapPin, database) t := &ipTransport{ database: database, name: name, device: device, container: container.NewContainer(), mutex: &sync.Mutex{}, context: netio.NewContextForSecuredDevice(device), emitter: event.NewEmitter(), } t.addAccessory(a) for _, a := range as { t.addAccessory(a) } t.emitter.AddListener(t) return t, err }
func setupHomeKit() { aInfo := accessory.Info{ Name: "Hive Bridge", Manufacturer: "British Gas PLC", } a := accessory.New(aInfo, accessory.TypeBridge) tInfo := accessory.Info{ Name: "Heating", Manufacturer: "British Gas PLC", } t := accessory.NewThermostat(tInfo, 20.0, hive.MinTemp, hive.MaxTemp, 0.5) t.Thermostat.TargetTemperature.OnValueRemoteUpdate(targetTempChangeRequest) thermostat = t bInfo := accessory.Info{ Name: "Heating Boost", Manufacturer: "British Gas PLC", } b := accessory.NewSwitch(bInfo) b.Switch.On.OnValueRemoteUpdate(heatingBoostStateChangeRequest) heatingBoostSwitch = b sInfo := accessory.Info{ Name: "Hot Water", Manufacturer: "British Gas PLC", } h := accessory.NewSwitch(sInfo) h.Switch.On.OnValueRemoteUpdate(hotWaterStateChangeRequest) hotWaterSwitch = h config := hap.Config{ Pin: pin, } var err error transport, err = hap.NewIPTransport(config, a, t.Accessory, b.Accessory, h.Accessory) if err != nil { log.Fatal(err) } }
func (t *ipTransport) notifyListener(a *accessory.Accessory, c *characteristic.Characteristic, except net.Conn) { conns := t.context.ActiveConnections() for _, conn := range conns { if conn == except { continue } resp, err := event.New(a, c) if err != nil { log.Fatal(err) } // Write response into buffer to replace HTTP protocol // specifier with EVENT as required by HAP var buffer = new(bytes.Buffer) resp.Write(buffer) bytes, err := ioutil.ReadAll(buffer) bytes = event.FixProtocolSpecifier(bytes) log.Printf("[VERB] %s <- %s", conn.RemoteAddr(), string(bytes)) conn.Write(bytes) } }
func GetHKLight(light common.Light) *HKLight { hkLight, found := lights[light.ID()] if found { return hkLight } label, _ := light.GetLabel() log.Printf("[INFO] Creating New HKLight for %s", label) info := model.Info{ Name: label, Manufacturer: "LIFX", } lightBulb := accessory.NewLightBulb(info) power, _ := light.GetPower() lightBulb.SetOn(power) color, _ := light.GetColor() hue, saturation, brightness := ConvertLIFXColor(color) lightBulb.SetBrightness(int(brightness)) lightBulb.SetSaturation(saturation) lightBulb.SetHue(hue) transport, err := hap.NewIPTransport(pin, lightBulb.Accessory) if err != nil { log.Fatal(err) } go func() { transport.Start() }() hkLight = &HKLight{lightBulb.Accessory, nil, transport, lightBulb} lights[light.ID()] = hkLight lightBulb.OnIdentify(func() { timeout := 1 * time.Second for i := 0; i < 4; i++ { ToggleLight(light) time.Sleep(timeout) } }) lightBulb.OnStateChanged(func(power bool) { log.Printf("[INFO] Changed State for %s", label) light.SetPower(power) }) updateColor := func(light common.Light) { // HAP: [0...360] // LIFX: [0...MAX_UINT16] hue := lightBulb.GetHue() // HAP: [0...100] // LIFX: [0...MAX_UINT16] saturation := lightBulb.GetSaturation() // HAP: [0...100] // LIFX: [0...MAX_UINT16] brightness := lightBulb.GetBrightness() // [HSBKKelvinMin..HSBKKelvinMax] kelvin := HSBKKelvinDefault lifxHue := math.MaxUint16 * float64(hue) / float64(characteristic.MaxHue) lifxSaturation := math.MaxUint16 * float64(saturation) / float64(characteristic.MaxSaturation) lifxBrightness := math.MaxUint16 * float64(brightness) / float64(characteristic.MaxBrightness) color := common.Color{ uint16(lifxHue), uint16(lifxSaturation), uint16(lifxBrightness), kelvin, } light.SetColor(color, 500*time.Millisecond) } lightBulb.OnHueChanged(func(value float64) { log.Printf("[INFO] Changed Hue for %s to %d", label, value) updateColor(light) }) lightBulb.OnSaturationChanged(func(value float64) { log.Printf("[INFO] Changed Saturation for %s to %d", label, value) updateColor(light) }) lightBulb.OnBrightnessChanged(func(value int) { log.Printf("[INFO] Changed Brightness for %s to %d", label, value) updateColor(light) }) return hkLight }
// Client -> Server // - encrypted tlv8: entity ltpk, entity name and signature (of H, entity name, ltpk) // - auth tag (mac) // // Server // - Validate signature of encrpyted tlv8 // - Read and store entity ltpk and name // // Server -> Client // - encrpyted tlv8: bridge ltpk, bridge name, signature (of hash, bridge name, ltpk) func (setup *SetupServerController) handleKeyExchange(in util.Container) (util.Container, error) { out := util.NewTLV8Container() setup.step = PairStepKeyExchangeResponse out.SetByte(TagSequence, setup.step.Byte()) data := in.GetBytes(TagEncryptedData) message := data[:(len(data) - 16)] var mac [16]byte copy(mac[:], data[len(message):]) // 16 byte (MAC) log.Println("[VERB] -> Message:", hex.EncodeToString(message)) log.Println("[VERB] -> MAC:", hex.EncodeToString(mac[:])) decrypted, err := chacha20poly1305.DecryptAndVerify(setup.session.EncryptionKey[:], []byte("PS-Msg05"), message, mac, nil) if err != nil { setup.reset() log.Println("[ERRO]", err) out.SetByte(TagErrCode, ErrCodeUnknown.Byte()) // return error 1 } else { decryptedBuf := bytes.NewBuffer(decrypted) in, err := util.NewTLV8ContainerFromReader(decryptedBuf) if err != nil { return nil, err } username := in.GetString(TagUsername) clientltpk := in.GetBytes(TagPublicKey) signature := in.GetBytes(TagSignature) log.Println("[VERB] -> Username:"******"[VERB] -> ltpk:", hex.EncodeToString(clientltpk)) log.Println("[VERB] -> Signature:", hex.EncodeToString(signature)) // Calculate hash `H` hash, _ := hkdf.Sha512(setup.session.PrivateKey, []byte("Pair-Setup-Controller-Sign-Salt"), []byte("Pair-Setup-Controller-Sign-Info")) var material []byte material = append(material, hash[:]...) material = append(material, []byte(username)...) material = append(material, clientltpk...) if crypto.ValidateED25519Signature(clientltpk, material, signature) == false { log.Println("[WARN] ed25519 signature is invalid") setup.reset() out.SetByte(TagErrCode, ErrCodeAuthenticationFailed.Byte()) // return error 2 } else { log.Println("[VERB] ed25519 signature is valid") // Store entity ltpk and name entity := db.NewEntity(username, clientltpk, nil) setup.database.SaveEntity(entity) log.Printf("[INFO] Stored ltpk '%s' for entity '%s'\n", hex.EncodeToString(clientltpk), username) ltpk := setup.device.PublicKey() ltsk := setup.device.PrivateKey() // Send username, ltpk, signature as encrypted message hash, err := hkdf.Sha512(setup.session.PrivateKey, []byte("Pair-Setup-Accessory-Sign-Salt"), []byte("Pair-Setup-Accessory-Sign-Info")) material = make([]byte, 0) material = append(material, hash[:]...) material = append(material, []byte(setup.session.Username)...) material = append(material, ltpk...) signature, err := crypto.ED25519Signature(ltsk, material) if err != nil { log.Fatal(err) return nil, err } tlvPairKeyExchange := util.NewTLV8Container() tlvPairKeyExchange.SetBytes(TagUsername, setup.session.Username) tlvPairKeyExchange.SetBytes(TagPublicKey, ltpk) tlvPairKeyExchange.SetBytes(TagSignature, []byte(signature)) log.Println("[VERB] <- Username:"******"[VERB] <- ltpk:", hex.EncodeToString(tlvPairKeyExchange.GetBytes(TagPublicKey))) log.Println("[VERB] <- Signature:", hex.EncodeToString(tlvPairKeyExchange.GetBytes(TagSignature))) encrypted, mac, _ := chacha20poly1305.EncryptAndSeal(setup.session.EncryptionKey[:], []byte("PS-Msg06"), tlvPairKeyExchange.BytesBuffer().Bytes(), nil) out.SetByte(TagSequence, PairStepKeyExchangeRequest.Byte()) out.SetBytes(TagEncryptedData, append(encrypted, mac[:]...)) } } return out, nil }
// This app can connect to the UVR1611 data bus and provide the sensor values to HomeKit clients // // Optimizations: To improve the performance on a Raspberry Pi B+, the interrupt handler of the // gpio pin is removed every time after successfully decoding a packet. This allows other goroutines // (e.g. HAP server) to do their job more quickly. func main() { var ( pin = flag.String("pin", "", "Accessory pin required for pairing") mode = flag.String("conn", "mock", "Connection type; mock, gpio, replay") file = flag.String("file", "", "Log file from which to replay packets") port = flag.String("port", "P8_07", "GPIO port; default P8_07") timeout = flag.Int("timeout", 120, "Timeout in seconds until accessories are not reachable") ) flag.Parse() sensors = map[string]*hkuvr1611.Sensor{} info := InfoForAccessoryName("UVR1611") uvrAccessory = accessory.New(info, accessory.TypeBridge) timer_duration := time.Duration(*timeout) * time.Second timer = time.AfterFunc(timer_duration, func() { log.Println("[INFO] Not Reachable") if transport != nil { sensors = map[string]*hkuvr1611.Sensor{} transport.Stop() transport = nil } }) var conn Connection callback := func(packet uvr1611.Packet) { sensors := HandlePacket(packet) if transport == nil { config := hap.Config{Pin: *pin} if t, err := hap.NewIPTransport(config, uvrAccessory, sensors...); err != nil { log.Fatal(err) } else { go func() { t.Start() }() transport = t } } timer.Reset(timer_duration) } switch *mode { case "mock": conn = mock.NewConnection(callback) case "replay": conn = mock.NewReplayConnection(*file, callback) case "gpio": conn = gpio.NewConnection(*port, callback) default: log.Fatal("Incorrect -conn flag") } hap.OnTermination(func() { if transport != nil { transport.Stop() } conn.Close() timer.Stop() os.Exit(1) }) select {} }
// NewIPTransport creates a transport to provide accessories over IP. // // The IP transports stores the crypto keys inside a database, which // is by default inside a folder at the current working directory. // The folder is named exactly as the accessory name. // // The transports can contain more than one accessory. If this is the // case, the first accessory acts as the HomeKit bridge. // // *Important:* Changing the name of the accessory, or letting multiple // transports store the data inside the same database lead to // unexpected behavior – don't do that. // // The transport is secured with an 8-digit pin, which must be entered // by an iOS client to successfully pair with the accessory. If the // provided transport config does not specify any pin, 00102003 is used. func NewIPTransport(config Config, a *accessory.Accessory, as ...*accessory.Accessory) (Transport, error) { // Find transport name which is visible in mDNS name := a.Name() if len(name) == 0 { log.Fatal("Invalid empty name for first accessory") } default_config := Config{ StoragePath: name, Pin: "00102003", Port: "", } if dir := config.StoragePath; len(dir) > 0 { default_config.StoragePath = dir } if pin := config.Pin; len(pin) > 0 { default_config.Pin = pin } if port := config.Port; len(port) > 0 { default_config.Port = ":" + port } storage, err := util.NewFileStorage(default_config.StoragePath) if err != nil { return nil, err } // Find transport uuid which appears as "id" txt record in mDNS and // must be unique and stay the same over time uuid := transportUUIDInStorage(storage) database := db.NewDatabaseWithStorage(storage) hap_pin, err := NewPin(default_config.Pin) if err != nil { return nil, err } device, err := netio.NewSecuredDevice(uuid, hap_pin, database) t := &ipTransport{ database: database, name: name, device: device, config: default_config, container: container.NewContainer(), mutex: &sync.Mutex{}, context: netio.NewContextForSecuredDevice(device), emitter: event.NewEmitter(), } t.addAccessory(a) for _, a := range as { t.addAccessory(a) } t.emitter.AddListener(t) return t, err }
func GetHKLight(light common.Light) *HKLight { hkLight, found := lights[light.ID()] if found { return hkLight } label, _ := light.GetLabel() log.Printf("[INFO] Creating New HKLight for %s", label) info := accessory.Info{ Name: label, Manufacturer: "LIFX", } acc := accessory.NewLightbulb(info) power, _ := light.GetPower() acc.Lightbulb.On.SetValue(power) color, _ := light.GetColor() hue, saturation, brightness := ConvertLIFXColor(color) acc.Lightbulb.Brightness.SetValue(int(brightness)) acc.Lightbulb.Saturation.SetValue(saturation) acc.Lightbulb.Hue.SetValue(hue) config := hc.Config{Pin: pin} transport, err := hc.NewIPTransport(config, acc.Accessory) if err != nil { log.Fatal(err) } go func() { transport.Start() }() hkLight = &HKLight{acc, transport, nil} lights[light.ID()] = hkLight acc.OnIdentify(func() { timeout := 1 * time.Second for i := 0; i < 4; i++ { ToggleLight(light) time.Sleep(timeout) } }) acc.Lightbulb.On.OnValueRemoteUpdate(func(power bool) { log.Printf("[INFO] Changed State for %s", label) light.SetPowerDuration(power, transitionDuration) }) updateColor := func(light common.Light) { currentPower, _ := light.GetPower() // HAP: [0...360] // LIFX: [0...MAX_UINT16] hue := acc.Lightbulb.Hue.GetValue() // HAP: [0...100] // LIFX: [0...MAX_UINT16] saturation := acc.Lightbulb.Saturation.GetValue() // HAP: [0...100] // LIFX: [0...MAX_UINT16] brightness := acc.Lightbulb.Brightness.GetValue() // [HSBKKelvinMin..HSBKKelvinMax] kelvin := HSBKKelvinDefault lifxHue := math.MaxUint16 * float64(hue) / float64(HueMax) lifxSaturation := math.MaxUint16 * float64(saturation) / float64(SaturationMax) lifxBrightness := math.MaxUint16 * float64(brightness) / float64(BrightnessMax) color := common.Color{ uint16(lifxHue), uint16(lifxSaturation), uint16(lifxBrightness), kelvin, } light.SetColor(color, transitionDuration) if brightness > 0 && !currentPower { log.Printf("[INFO] Color changed for %s, turning on power.", label) light.SetPowerDuration(true, transitionDuration) } else if brightness == 0 && currentPower { log.Printf("[INFO] Color changed for %s, but brightness = 0 turning off power.", label) light.SetPower(false) } } acc.Lightbulb.Hue.OnValueRemoteUpdate(func(value float64) { log.Printf("[INFO] Changed Hue for %s to %f", label, value) updateColor(light) }) acc.Lightbulb.Saturation.OnValueRemoteUpdate(func(value float64) { log.Printf("[INFO] Changed Saturation for %s to %f", label, value) updateColor(light) }) acc.Lightbulb.Brightness.OnValueRemoteUpdate(func(value int) { log.Printf("[INFO] Changed Brightness for %s to %d", label, value) updateColor(light) }) return hkLight }