Esempio n. 1
0
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()
}
Esempio n. 2
0
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))
	})
}
Esempio n. 3
0
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
}
Esempio n. 4
0
// 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)
}
Esempio n. 5
0
// 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
}
Esempio n. 6
0
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
}
Esempio n. 7
0
// 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
}
Esempio n. 8
0
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
}
Esempio n. 9
0
// 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
}
Esempio n. 10
0
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)
	}
}
Esempio n. 11
0
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)
	}
}
Esempio n. 12
0
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
}
Esempio n. 13
0
// 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
}
Esempio n. 14
0
// 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 {}
}
Esempio n. 15
0
// 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
}
Esempio n. 16
0
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
}