Checking in all the SmartThings apps; both official and third-party.
[smartapps.git] / official / hue-connect.groovy
diff --git a/official/hue-connect.groovy b/official/hue-connect.groovy
new file mode 100755 (executable)
index 0000000..b343b20
--- /dev/null
@@ -0,0 +1,1801 @@
+/**
+ *  Hue Service Manager
+ *
+ *  Author: Juan Risso (juan@smartthings.com)
+ *
+ *  Copyright 2015 SmartThings
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ *  in compliance with the License. You may obtain a copy of the License at:
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
+ *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
+ *  for the specific language governing permissions and limitations under the License.
+ *
+ */
+include 'localization'
+
+definition(
+               name: "Hue (Connect)",
+               namespace: "smartthings",
+               author: "SmartThings",
+               description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Please update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.",
+               category: "SmartThings Labs",
+               iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
+               iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png",
+               singleInstance: true
+)
+
+preferences {
+       page(name: "mainPage", title: "", content: "mainPage", refreshTimeout: 5)
+       page(name: "bridgeDiscovery", title: "", content: "bridgeDiscovery", refreshTimeout: 5)
+       page(name: "bridgeDiscoveryFailed", title: "", content: "bridgeDiscoveryFailed", refreshTimeout: 0)
+       page(name: "bridgeBtnPush", title: "", content: "bridgeLinking", refreshTimeout: 5)
+       page(name: "bulbDiscovery", title: "", content: "bulbDiscovery", refreshTimeout: 5)
+}
+
+def mainPage() {
+       def bridges = bridgesDiscovered()
+
+       if (state.refreshUsernameNeeded) {
+               return bridgeLinking()
+       } else if (state.username && bridges) {
+               return bulbDiscovery()
+       } else {
+               return bridgeDiscovery()
+       }
+}
+
+def bridgeDiscovery(params = [:]) {
+       def bridges = bridgesDiscovered()
+       int bridgeRefreshCount = !state.bridgeRefreshCount ? 0 : state.bridgeRefreshCount as int
+       state.bridgeRefreshCount = bridgeRefreshCount + 1
+       def refreshInterval = 3
+
+       def options = bridges ?: []
+       def numFound = options.size() ?: "0"
+       if (numFound == 0) {
+               if (state.bridgeRefreshCount == 25) {
+                       log.trace "Cleaning old bridges memory"
+                       state.bridges = [:]
+                       app.updateSetting("selectedHue", "")
+               } else if (state.bridgeRefreshCount > 100) {
+                       // five minutes have passed, give up
+                       // there seems to be a problem going back from discovey failed page in some instances (compared to pressing next)
+                       // however it is probably a SmartThings settings issue
+                       state.bridges = [:]
+                       app.updateSetting("selectedHue", "")
+                       state.bridgeRefreshCount = 0
+                       return bridgeDiscoveryFailed()
+               }
+       }
+
+       ssdpSubscribe()
+       log.trace "bridgeRefreshCount: $bridgeRefreshCount"
+       //bridge discovery request every 15 //25 seconds
+       if ((bridgeRefreshCount % 5) == 0) {
+               discoverBridges()
+       }
+
+       //setup.xml request every 3 seconds except on discoveries
+       if (((bridgeRefreshCount % 3) == 0) && ((bridgeRefreshCount % 5) != 0)) {
+               verifyHueBridges()
+       }
+
+       return dynamicPage(name: "bridgeDiscovery", title: "Discovery Started!", nextPage: "bridgeBtnPush", refreshInterval: refreshInterval, uninstall: true) {
+               section("Please wait while we discover your Hue Bridge. Kindly note that you must first configure your Hue Bridge and Lights using the Philips Hue application. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
+
+
+                       input(name: "selectedHue", type: "enum", required: false, title: "Select Hue Bridge ({{numFound}} found)", messageArgs: [numFound: numFound], multiple: false, options: options, submitOnChange: true)
+               }
+       }
+}
+
+def bridgeDiscoveryFailed() {
+       return dynamicPage(name: "bridgeDiscoveryFailed", title: "Bridge Discovery Failed!", nextPage: "bridgeDiscovery") {
+               section("Failed to discover any Hue Bridges. Please confirm that the Hue Bridge is connected to the same network as your SmartThings Hub, and that it has power.") {
+               }
+       }
+}
+
+def bridgeLinking() {
+       int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
+       state.linkRefreshcount = linkRefreshcount + 1
+       def refreshInterval = 3
+
+       def nextPage = ""
+       def title = "Linking with your Hue"
+       def paragraphText
+       if (selectedHue) {
+               if (state.refreshUsernameNeeded) {
+                       paragraphText = "The current Hue username is invalid. Please press the button on your Hue Bridge to relink."
+               } else {
+                       paragraphText = "Press the button on your Hue Bridge to setup a link."
+               }
+       } else {
+               paragraphText = "You haven't selected a Hue Bridge, please Press 'Done' and select one before clicking next."
+       }
+       if (state.username) { //if discovery worked
+               if (state.refreshUsernameNeeded) {
+                       state.refreshUsernameNeeded = false
+                       // Issue one poll with new username to cancel local polling with old username
+                       poll()
+               }
+               nextPage = "bulbDiscovery"
+               title = "Success!"
+               paragraphText = "Linking to your hub was a success! Please click 'Next'!"
+       }
+
+       if ((linkRefreshcount % 2) == 0 && !state.username) {
+               sendDeveloperReq()
+       }
+
+       return dynamicPage(name: "bridgeBtnPush", title: title, nextPage: nextPage, refreshInterval: refreshInterval) {
+               section("") {
+                       paragraph "$paragraphText"
+               }
+       }
+}
+
+def bulbDiscovery() {
+       int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
+       state.bulbRefreshCount = bulbRefreshCount + 1
+       def refreshInterval = 3
+       state.inBulbDiscovery = true
+       def bridge = null
+
+       state.bridgeRefreshCount = 0
+       def allLightsFound = bulbsDiscovered() ?: [:]
+
+       // List lights currently not added to the user (editable)
+       def newLights = allLightsFound.findAll { getChildDevice(it.key) == null } ?: [:]
+       newLights = newLights.sort { it.value.toLowerCase() }
+
+       // List lights already added to the user (not editable)
+       def existingLights = allLightsFound.findAll { getChildDevice(it.key) != null } ?: [:]
+       existingLights = existingLights.sort { it.value.toLowerCase() }
+
+       def numFound = newLights.size() ?: "0"
+       if (numFound == "0")
+               app.updateSetting("selectedBulbs", "")
+
+       if ((bulbRefreshCount % 5) == 0) {
+               discoverHueBulbs()
+       }
+       def selectedBridge = state.bridges.find { key, value -> value?.serialNumber?.equalsIgnoreCase(selectedHue) }
+       def title = selectedBridge?.value?.name ?: "Find bridges"
+
+       // List of all lights previously added shown to user
+       def existingLightsDescription = ""
+       if (existingLights) {
+               existingLights.each {
+                       if (existingLightsDescription.isEmpty()) {
+                               existingLightsDescription += it.value
+                       } else {
+                               existingLightsDescription += ", ${it.value}"
+                       }
+               }
+       }
+
+       def existingLightsSize = "${existingLights.size()}"
+       if (bulbRefreshCount > 200 && numFound == "0") {
+               // Time out after 10 minutes
+               state.inBulbDiscovery = false
+               bulbRefreshCount = 0
+               return dynamicPage(name: "bulbDiscovery", title: "Light Discovery Failed!", nextPage: "", refreshInterval: 0, install: true, uninstall: true) {
+                       section("Failed to discover any lights, please try again later. Click 'Done' to exit.") {
+                               paragraph title: "Previously added Hue Lights ({{existingLightsSize}} added)", messageArgs: [existingLightsSize: existingLightsSize], existingLightsDescription
+                       }
+                       section {
+                               href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
+                       }
+               }
+
+       } else {
+               return dynamicPage(name: "bulbDiscovery", title: "Light Discovery Started!", nextPage: "", refreshInterval: refreshInterval, install: true, uninstall: true) {
+                       section("Please wait while we discover your Hue Lights. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
+                               input(name: "selectedBulbs", type: "enum", required: false, title: "Select Hue Lights to add ({{numFound}} found)", messageArgs: [numFound: numFound], multiple: true, submitOnChange: true, options: newLights)
+                               paragraph title: "Previously added Hue Lights ({{existingLightsSize}} added)", messageArgs: [existingLightsSize: existingLightsSize], existingLightsDescription
+                       }
+                       section {
+                               href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
+                       }
+               }
+       }
+}
+
+private discoverBridges() {
+       log.trace "Sending Hue Discovery message to the hub"
+       sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:basic:1", physicalgraph.device.Protocol.LAN))
+}
+
+void ssdpSubscribe() {
+       subscribe(location, "ssdpTerm.urn:schemas-upnp-org:device:basic:1", ssdpBridgeHandler)
+}
+
+private sendDeveloperReq() {
+       def token = app.id
+       def host = getBridgeIP()
+       sendHubCommand(new physicalgraph.device.HubAction([
+                       method : "POST",
+                       path   : "/api",
+                       headers: [
+                                       HOST: host
+                       ],
+                       body   : [devicetype: "$token-0"]], "${selectedHue}", [callback: "usernameHandler"]))
+}
+
+private discoverHueBulbs() {
+       def host = getBridgeIP()
+       sendHubCommand(new physicalgraph.device.HubAction([
+                       method : "GET",
+                       path   : "/api/${state.username}/lights",
+                       headers: [
+                                       HOST: host
+                       ]], "${selectedHue}", [callback: "lightsHandler"]))
+}
+
+private verifyHueBridge(String deviceNetworkId, String host) {
+       log.trace "Verify Hue Bridge $deviceNetworkId"
+       sendHubCommand(new physicalgraph.device.HubAction([
+                       method : "GET",
+                       path   : "/description.xml",
+                       headers: [
+                                       HOST: host
+                       ]], deviceNetworkId, [callback: "bridgeDescriptionHandler"]))
+}
+
+private verifyHueBridges() {
+       def devices = getHueBridges().findAll { it?.value?.verified != true }
+       devices.each {
+               def ip = convertHexToIP(it.value.networkAddress)
+               def port = convertHexToInt(it.value.deviceAddress)
+               verifyHueBridge("${it.value.mac}", (ip + ":" + port))
+       }
+}
+
+Map bridgesDiscovered() {
+       def vbridges = getVerifiedHueBridges()
+       def map = [:]
+       vbridges.each {
+               def value = "${it.value.name}"
+               def key = "${it.value.mac}"
+               map["${key}"] = value
+       }
+       map
+}
+
+Map bulbsDiscovered() {
+       def bulbs = getHueBulbs()
+       def bulbmap = [:]
+       if (bulbs instanceof java.util.Map) {
+               bulbs.each {
+                       def value = "${it.value.name}"
+                       def key = app.id + "/" + it.value.id
+                       bulbmap["${key}"] = value
+               }
+       } else { //backwards compatable
+               bulbs.each {
+                       def value = "${it.name}"
+                       def key = app.id + "/" + it.id
+                       logg += "$value - $key, "
+                       bulbmap["${key}"] = value
+               }
+       }
+       return bulbmap
+}
+
+Map getHueBulbs() {
+       state.bulbs = state.bulbs ?: [:]
+}
+
+def getHueBridges() {
+       state.bridges = state.bridges ?: [:]
+}
+
+def getVerifiedHueBridges() {
+       getHueBridges().findAll { it?.value?.verified == true }
+}
+
+def installed() {
+       log.trace "Installed with settings: ${settings}"
+       initialize()
+}
+
+def updated() {
+       log.trace "Updated with settings: ${settings}"
+       unsubscribe()
+       unschedule()
+       initialize()
+}
+
+def initialize() {
+       log.debug "Initializing"
+       unsubscribe(bridge)
+       state.inBulbDiscovery = false
+       state.bridgeRefreshCount = 0
+       state.bulbRefreshCount = 0
+       state.updating = false
+       setupDeviceWatch()
+       if (selectedHue) {
+               addBridge()
+               addBulbs()
+               doDeviceSync()
+               runEvery5Minutes("doDeviceSync")
+       }
+}
+
+def manualRefresh() {
+       checkBridgeStatus()
+       state.updating = false
+       poll()
+}
+
+def uninstalled() {
+       // Remove bridgedevice connection to allow uninstall of smartapp even though bridge is listed
+       // as user of smartapp
+       app.updateSetting("bridgeDevice", null)
+       state.bridges = [:]
+       state.username = null
+}
+
+private setupDeviceWatch() {
+       def hub = location.hubs[0]
+       // Make sure that all child devices are enrolled in device watch
+       getChildDevices().each {
+               it.sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${hub?.hub?.hardwareID}\"}")
+       }
+}
+
+private upgradeDeviceType(device, newHueType) {
+       def deviceType = getDeviceType(newHueType)
+
+       // Automatically change users Hue bulbs to correct device types
+       if (deviceType && !(device?.typeName?.equalsIgnoreCase(deviceType))) {
+               log.debug "Update device type: \"$device.label\" ${device?.typeName}->$deviceType"
+               device.setDeviceType(deviceType)
+       }
+}
+
+private getDeviceType(hueType) {
+       // Determine ST device type based on Hue classification of light
+       if (hueType?.equalsIgnoreCase("Dimmable light"))
+               return "Hue Lux Bulb"
+       else if (hueType?.equalsIgnoreCase("Extended Color Light"))
+               return "Hue Bulb"
+       else if (hueType?.equalsIgnoreCase("Color Light"))
+               return "Hue Bloom"
+       else if (hueType?.equalsIgnoreCase("Color Temperature Light"))
+               return "Hue White Ambiance Bulb"
+       else
+               return null
+}
+
+private addChildBulb(dni, hueType, name, hub, update = false, device = null) {
+       def deviceType = getDeviceType(hueType)
+
+       if (deviceType) {
+               return addChildDevice("smartthings", deviceType, dni, hub, ["label": name])
+       } else {
+               log.warn "Device type $hueType not supported"
+               return null
+       }
+}
+
+def addBulbs() {
+       def bulbs = getHueBulbs()
+       selectedBulbs?.each { dni ->
+               def d = getChildDevice(dni)
+               if (!d) {
+                       def newHueBulb
+                       if (bulbs instanceof java.util.Map) {
+                               newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
+                               if (newHueBulb != null) {
+                                       d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
+                                       if (d) {
+                                               log.debug "created ${d.displayName} with id $dni"
+                                               d.completedSetup = true
+                                       }
+                               } else {
+                                       log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
+                               }
+                       } else {
+                               //backwards compatable
+                               newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
+                               d = addChildBulb(dni, "Extended Color Light", newHueBulb?.value?.name, newHueBulb?.value?.hub)
+                               d?.completedSetup = true
+                               d?.refresh()
+                       }
+               } else {
+                       log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
+                       if (bulbs instanceof java.util.Map) {
+                               // Update device type if incorrect
+                               def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
+                               upgradeDeviceType(d, newHueBulb?.value?.type)
+                       }
+               }
+       }
+}
+
+def addBridge() {
+       def vbridges = getVerifiedHueBridges()
+       def vbridge = vbridges.find { "${it.value.mac}" == selectedHue }
+
+       if (vbridge) {
+               def d = getChildDevice(selectedHue)
+               if (!d) {
+                       // compatibility with old devices
+                       def newbridge = true
+                       childDevices.each {
+                               if (it.getDeviceDataByName("mac")) {
+                                       def newDNI = "${it.getDeviceDataByName("mac")}"
+                                       if (newDNI != it.deviceNetworkId) {
+                                               def oldDNI = it.deviceNetworkId
+                                               log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
+                                               it.setDeviceNetworkId("${newDNI}")
+                                               if (oldDNI == selectedHue) {
+                                                       app.updateSetting("selectedHue", newDNI)
+                                               }
+                                               newbridge = false
+                                       }
+                               }
+                       }
+                       if (newbridge) {
+                               // Hue uses last 6 digits of MAC address as ID number, this number is shown on the bottom of the bridge
+                               def idNumber = getBridgeIdNumber(selectedHue)
+                               d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub, ["label": "Hue Bridge ($idNumber)"])
+                               if (d) {
+                                       // Associate smartapp to bridge so user will be warned if trying to delete bridge
+                                       app.updateSetting("bridgeDevice", [type: "device.hueBridge", value: d.id])
+
+                                       d.completedSetup = true
+                                       log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
+                                       def childDevice = getChildDevice(d.deviceNetworkId)
+                                       childDevice?.sendEvent(name: "status", value: "Online")
+                                       childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
+                                       updateBridgeStatus(childDevice)
+
+                                       childDevice?.sendEvent(name: "idNumber", value: idNumber)
+                                       if (vbridge.value.ip && vbridge.value.port) {
+                                               if (vbridge.value.ip.contains(".")) {
+                                                       childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
+                                                       childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port)
+                                               } else {
+                                                       childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
+                                                       childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
+                                               }
+                                       } else {
+                                               childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
+                                               childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
+                                       }
+                               } else {
+                                       log.error "Failed to create Hue Bridge device"
+                               }
+                       }
+               } else {
+                       log.debug "found ${d.displayName} with id $selectedHue already exists"
+               }
+       }
+}
+
+def ssdpBridgeHandler(evt) {
+       def description = evt.description
+       log.trace "Location: $description"
+
+       def hub = evt?.hubId
+       def parsedEvent = parseLanMessage(description)
+       parsedEvent << ["hub": hub]
+
+       def bridges = getHueBridges()
+       log.trace bridges.toString()
+       if (!(bridges."${parsedEvent.ssdpUSN.toString()}")) {
+               //bridge does not exist
+               log.trace "Adding bridge ${parsedEvent.ssdpUSN}"
+               bridges << ["${parsedEvent.ssdpUSN.toString()}": parsedEvent]
+       } else {
+               // update the values
+               def ip = convertHexToIP(parsedEvent.networkAddress)
+               def port = convertHexToInt(parsedEvent.deviceAddress)
+               def host = ip + ":" + port
+               log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
+               def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
+               def dniReceived = "${parsedEvent.mac}"
+               def currentDni = dstate.mac
+               def d = getChildDevice(dniReceived)
+               def networkAddress = null
+               if (!d) {
+                       // There might be a mismatch between bridge DNI and the actual bridge mac address, correct that
+                       log.debug "Bridge with $dniReceived not found"
+                       def bridge = childDevices.find { it.deviceNetworkId == currentDni }
+                       if (bridge != null) {
+                               log.warn "Bridge is set to ${bridge.deviceNetworkId}, updating to $dniReceived"
+                               bridge.setDeviceNetworkId("${dniReceived}")
+                               dstate.mac = dniReceived
+                               // Check to see if selectedHue is a valid bridge, otherwise update it
+                               def isSelectedValid = bridges?.find { it.value?.mac == selectedHue }
+                               if (isSelectedValid == null) {
+                                       log.warn "Correcting selectedHue in state"
+                                       app.updateSetting("selectedHue", dniReceived)
+                               }
+                               doDeviceSync()
+                       }
+               } else {
+                       updateBridgeStatus(d)
+                       if (d.getDeviceDataByName("networkAddress")) {
+                               networkAddress = d.getDeviceDataByName("networkAddress")
+                       } else {
+                               networkAddress = d.latestState('networkAddress').stringValue
+                       }
+                       log.trace "Host: $host - $networkAddress"
+                       if (host != networkAddress) {
+                               log.debug "Device's port or ip changed for device $d..."
+                               dstate.ip = ip
+                               dstate.port = port
+                               dstate.name = "Philips hue ($ip)"
+                               d.sendEvent(name: "networkAddress", value: host)
+                               d.updateDataValue("networkAddress", host)
+                       }
+                       if (dstate.mac != dniReceived) {
+                               log.warn "Correcting bridge mac address in state"
+                               dstate.mac = dniReceived
+                       }
+                       if (selectedHue != dniReceived) {
+                               // Check to see if selectedHue is a valid bridge, otherwise update it
+                               def isSelectedValid = bridges?.find { it.value?.mac == selectedHue }
+                               if (isSelectedValid == null) {
+                                       log.warn "Correcting selectedHue in state"
+                                       app.updateSetting("selectedHue", dniReceived)
+                               }
+                       }
+               }
+       }
+}
+
+void bridgeDescriptionHandler(physicalgraph.device.HubResponse hubResponse) {
+       log.trace "description.xml response (application/xml)"
+       def body = hubResponse.xml
+       if (body?.device?.modelName?.text()?.startsWith("Philips hue bridge")) {
+               def bridges = getHueBridges()
+               def bridge = bridges.find { it?.key?.contains(body?.device?.UDN?.text()) }
+               if (bridge) {
+                       def idNumber = getBridgeIdNumber(body?.device?.serialNumber?.text())
+
+                       // usually in form of bridge name followed by (ip), i.e. defaults to Philips Hue (192.168.1.2)
+                       // replace IP with id number to make it easier for user to identify
+                       def name = body?.device?.friendlyName?.text()
+                       def index = name?.indexOf('(')
+                       if (index != -1) {
+                               name = name.substring(0, index)
+                               name += " ($idNumber)"
+                       }
+                       bridge.value << [name: name, serialNumber: body?.device?.serialNumber?.text(), idNumber: idNumber, verified: true]
+               } else {
+                       log.error "/description.xml returned a bridge that didn't exist"
+               }
+       }
+}
+
+void lightsHandler(physicalgraph.device.HubResponse hubResponse) {
+       if (isValidSource(hubResponse.mac)) {
+               def body = hubResponse.json
+               if (!body?.state?.on) { //check if first time poll made it here by mistake
+                       log.debug "Adding bulbs to state!"
+                       updateBulbState(body, hubResponse.hubId)
+               }
+       }
+}
+
+void usernameHandler(physicalgraph.device.HubResponse hubResponse) {
+       if (isValidSource(hubResponse.mac)) {
+               def body = hubResponse.json
+               if (body.success != null) {
+                       if (body.success[0] != null) {
+                               if (body.success[0].username)
+                                       state.username = body.success[0].username
+                       }
+               } else if (body.error != null) {
+                       //TODO: handle retries...
+                       log.error "ERROR: application/json ${body.error}"
+               }
+       }
+}
+
+/**
+ * @deprecated This has been replaced by the combination of {@link #ssdpBridgeHandler()}, {@link #bridgeDescriptionHandler()},
+ * {@link #lightsHandler()}, and {@link #usernameHandler()}. After a pending event subscription migration, it can be removed.
+ */
+@Deprecated
+def locationHandler(evt) {
+       def description = evt.description
+       log.trace "Location: $description"
+
+       def hub = evt?.hubId
+       def parsedEvent = parseLanMessage(description)
+       parsedEvent << ["hub": hub]
+
+       if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:basic:1")) {
+               //SSDP DISCOVERY EVENTS
+               log.trace "SSDP DISCOVERY EVENTS"
+               def bridges = getHueBridges()
+               log.trace bridges.toString()
+               if (!(bridges."${parsedEvent.ssdpUSN.toString()}")) {
+                       //bridge does not exist
+                       log.trace "Adding bridge ${parsedEvent.ssdpUSN}"
+                       bridges << ["${parsedEvent.ssdpUSN.toString()}": parsedEvent]
+               } else {
+                       // update the values
+                       def ip = convertHexToIP(parsedEvent.networkAddress)
+                       def port = convertHexToInt(parsedEvent.deviceAddress)
+                       def host = ip + ":" + port
+                       log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
+                       def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
+                       def dni = "${parsedEvent.mac}"
+                       def d = getChildDevice(dni)
+                       def networkAddress = null
+                       if (!d) {
+                               childDevices.each {
+                                       if (it.getDeviceDataByName("mac")) {
+                                               def newDNI = "${it.getDeviceDataByName("mac")}"
+                                               d = it
+                                               if (newDNI != it.deviceNetworkId) {
+                                                       def oldDNI = it.deviceNetworkId
+                                                       log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
+                                                       it.setDeviceNetworkId("${newDNI}")
+                                                       if (oldDNI == selectedHue) {
+                                                               app.updateSetting("selectedHue", newDNI)
+                                                       }
+                                                       doDeviceSync()
+                                               }
+                                       }
+                               }
+                       } else {
+                               updateBridgeStatus(d)
+                               if (d.getDeviceDataByName("networkAddress")) {
+                                       networkAddress = d.getDeviceDataByName("networkAddress")
+                               } else {
+                                       networkAddress = d.latestState('networkAddress').stringValue
+                               }
+                               log.trace "Host: $host - $networkAddress"
+                               if (host != networkAddress) {
+                                       log.debug "Device's port or ip changed for device $d..."
+                                       dstate.ip = ip
+                                       dstate.port = port
+                                       dstate.name = "Philips hue ($ip)"
+                                       d.sendEvent(name: "networkAddress", value: host)
+                                       d.updateDataValue("networkAddress", host)
+                               }
+                       }
+               }
+       } else if (parsedEvent.headers && parsedEvent.body) {
+               log.trace "HUE BRIDGE RESPONSES"
+               def headerString = parsedEvent.headers.toString()
+               if (headerString?.contains("xml")) {
+                       log.trace "description.xml response (application/xml)"
+                       def body = new XmlSlurper().parseText(parsedEvent.body)
+                       if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) {
+                               def bridges = getHueBridges()
+                               def bridge = bridges.find { it?.key?.contains(body?.device?.UDN?.text()) }
+                               if (bridge) {
+                                       bridge.value << [name: body?.device?.friendlyName?.text(), serialNumber: body?.device?.serialNumber?.text(), verified: true]
+                               } else {
+                                       log.error "/description.xml returned a bridge that didn't exist"
+                               }
+                       }
+               } else if (headerString?.contains("json") && isValidSource(parsedEvent.mac)) {
+                       log.trace "description.xml response (application/json)"
+                       def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
+                       if (body.success != null) {
+                               if (body.success[0] != null) {
+                                       if (body.success[0].username) {
+                                               state.username = body.success[0].username
+                                       }
+                               }
+                       } else if (body.error != null) {
+                               //TODO: handle retries...
+                               log.error "ERROR: application/json ${body.error}"
+                       } else {
+                               //GET /api/${state.username}/lights response (application/json)
+                               if (!body?.state?.on) { //check if first time poll made it here by mistake
+                                       log.debug "Adding bulbs to state!"
+                                       updateBulbState(body, parsedEvent.hub)
+                               }
+                       }
+               }
+       } else {
+               log.trace "NON-HUE EVENT $evt.description"
+       }
+}
+
+def doDeviceSync() {
+       log.trace "Doing Hue Device Sync!"
+
+       // Check if state.updating failed to clear
+       if (state.lastUpdateStarted < (now() - 20 * 1000) && state.updating) {
+               state.updating = false
+               log.warn "state.updating failed to clear"
+       }
+
+       convertBulbListToMap()
+       poll()
+       ssdpSubscribe()
+       discoverBridges()
+       checkBridgeStatus()
+}
+
+/**
+ * Called when data is received from the Hue bridge, this will update the lastActivity() that
+ * is used to keep track of online/offline status of the bridge. Bridge is considered offline
+ * if not heard from in 16 minutes
+ *
+ * @param childDevice Hue Bridge child device
+ */
+private void updateBridgeStatus(childDevice) {
+       // Update activity timestamp if child device is a valid bridge
+       def vbridges = getVerifiedHueBridges()
+       def vbridge = vbridges.find {
+               "${it.value.mac}".toUpperCase() == childDevice?.device?.deviceNetworkId?.toUpperCase()
+       }
+       vbridge?.value?.lastActivity = now()
+       if (vbridge && childDevice?.device?.currentValue("status") == "Offline") {
+               log.debug "$childDevice is back Online"
+               childDevice?.sendEvent(name: "status", value: "Online")
+               childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
+       }
+}
+
+/**
+ * Check if all Hue bridges have been heard from in the last 11 minutes, if not an Offline event will be sent
+ * for the bridge and all connected lights. Also, set ID number on bridge if not done previously.
+ */
+private void checkBridgeStatus() {
+       def bridges = getHueBridges()
+       // Check if each bridge has been heard from within the last 11 minutes (2 poll intervals times 5 minutes plus buffer)
+       def time = now() - (1000 * 60 * 11)
+       bridges.each {
+               def d = getChildDevice(it.value.mac)
+               if (d) {
+                       // Set id number on bridge if not done
+                       if (it.value.idNumber == null) {
+                               it.value.idNumber = getBridgeIdNumber(it.value.serialNumber)
+                               d.sendEvent(name: "idNumber", value: it.value.idNumber)
+                       }
+
+                       if (it.value.lastActivity < time) { // it.value.lastActivity != null &&
+                               if (d.currentStatus == "Online") {
+                                       log.warn "$d is Offline"
+                                       d.sendEvent(name: "status", value: "Offline")
+                                       d.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true)
+
+                                       Calendar currentTime = Calendar.getInstance()
+                                       getChildDevices().each {
+                                               def id = getId(it)
+                                               if (state.bulbs[id]?.online == true) {
+                                                       state.bulbs[id]?.online = false
+                                                       state.bulbs[id]?.unreachableSince = currentTime.getTimeInMillis()
+                                                       it.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true)
+                                               }
+                                       }
+                               }
+                       } else if (d.currentStatus == "Offline") {
+                               log.debug "$d is back Online"
+                               d.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
+                               d.sendEvent(name: "status", value: "Online")//setOnline(false)
+                       }
+               }
+       }
+}
+
+def isValidSource(macAddress) {
+       def vbridges = getVerifiedHueBridges()
+       return (vbridges?.find { "${it.value.mac}" == macAddress }) != null
+}
+
+def isInBulbDiscovery() {
+       return state.inBulbDiscovery
+}
+
+private updateBulbState(messageBody, hub) {
+       def bulbs = getHueBulbs()
+
+       // Copy of bulbs used to locate old lights in state that are no longer on bridge
+       def toRemove = [:]
+       toRemove << bulbs
+
+       messageBody.each { k, v ->
+
+               if (v instanceof Map) {
+                       if (bulbs[k] == null) {
+                               bulbs[k] = [:]
+                       }
+                       bulbs[k] << [id: k, name: v.name, type: v.type, modelid: v.modelid, hub: hub, remove: false]
+                       toRemove.remove(k)
+               }
+       }
+
+       // Remove bulbs from state that are no longer discovered
+       toRemove.each { k, v ->
+               log.warn "${bulbs[k].name} no longer exists on bridge, removing"
+               bulbs.remove(k)
+       }
+}
+
+/////////////////////////////////////
+//CHILD DEVICE METHODS
+/////////////////////////////////////
+
+def parse(childDevice, description) {
+       // Update activity timestamp if child device is a valid bridge
+       updateBridgeStatus(childDevice)
+
+       def parsedEvent = parseLanMessage(description)
+       if (parsedEvent.headers && parsedEvent.body) {
+               def headerString = parsedEvent.headers.toString()
+               def bodyString = parsedEvent.body.toString()
+               if (headerString?.contains("json")) {
+                       def body
+                       try {
+                               body = new groovy.json.JsonSlurper().parseText(bodyString)
+                       } catch (all) {
+                               log.warn "Parsing Body failed"
+                       }
+                       if (body instanceof java.util.Map) {
+                               // get (poll) reponse
+                               return handlePoll(body)
+                       } else {
+                               //put response
+                               return handleCommandResponse(body)
+                       }
+               }
+       } else {
+               log.debug "parse - got something other than headers,body..."
+               return []
+       }
+}
+
+// Philips Hue priority for color is  xy > ct > hs
+// For SmartThings, try to always send hue, sat and hex
+private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
+       if (device == null || (xy == null && hue == null && sat == null && ct == null))
+               return
+
+       def events = [:]
+       // For now, only care about changing color temperature if requested by user
+       if (ct != null && ct != 0 && (colormode == "ct" || (xy == null && hue == null && sat == null))) {
+               // for some reason setting Hue to their specified minimum off 153 yields 154, dealt with below
+               // 153 (6500K) to 500 (2000K)
+               def temp = (ct == 154) ? 6500 : Math.round(1000000 / ct)
+               device.sendEvent([name: "colorTemperature", value: temp, descriptionText: "Color temperature has changed"])
+               // Return because color temperature change is not counted as a color change in SmartThings so no hex update necessary
+               return
+       }
+
+       if (hue != null) {
+               // 0-65535
+               def value = Math.min(Math.round(hue * 100 / 65535), 65535) as int
+               events["hue"] = [name: "hue", value: value, descriptionText: "Color has changed", displayed: false]
+       }
+
+       if (sat != null) {
+               // 0-254
+               def value = Math.round(sat * 100 / 254) as int
+               events["saturation"] = [name: "saturation", value: value, descriptionText: "Color has changed", displayed: false]
+       }
+
+       // Following is used to decide what to base hex calculations on since it is preferred to return a colorchange in hex
+       if (xy != null && colormode != "hs") {
+               // If xy is present and color mode is not specified to hs, pick xy because of priority
+
+               // [0.0-1.0, 0.0-1.0]
+               def id = device.deviceNetworkId?.split("/")[1]
+               def model = state.bulbs[id]?.modelid
+               def hex = colorFromXY(xy, model)
+
+               // Create Hue and Saturation events if not previously existing
+               def hsv = hexToHsv(hex)
+               if (events["hue"] == null)
+                       events["hue"] = [name: "hue", value: hsv[0], descriptionText: "Color has changed", displayed: false]
+               if (events["saturation"] == null)
+                       events["saturation"] = [name: "saturation", value: hsv[1], descriptionText: "Color has changed", displayed: false]
+
+               events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
+       } else if (colormode == "hs" || colormode == null) {
+               // colormode is "hs" or "xy" is missing, default to follow hue/sat which is already handled above
+               def hueValue = (hue != null) ? events["hue"].value : Integer.parseInt("$device.currentHue")
+               def satValue = (sat != null) ? events["saturation"].value : Integer.parseInt("$device.currentSaturation")
+
+
+               def hex = hsvToHex(hueValue, satValue)
+               events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
+       }
+
+       boolean sendColorChanged = false
+       events.each {
+               device.sendEvent(it.value)
+       }
+}
+
+private sendBasicEvents(device, param, value) {
+       if (device == null || value == null || param == null)
+               return
+
+       switch (param) {
+               case "on":
+                       device.sendEvent(name: "switch", value: (value == true) ? "on" : "off")
+                       break
+               case "bri":
+                       // 1-254
+                       def level = Math.max(1, Math.round(value * 100 / 254)) as int
+                       device.sendEvent(name: "level", value: level, descriptionText: "Level has changed to ${level}%")
+                       break
+       }
+}
+
+/**
+ * Handles a response to a command (PUT) sent to the Hue Bridge.
+ *
+ * Will send appropriate events depending on values changed.
+ *
+ *     Example payload
+ * [,
+ *{"success":{"/lights/5/state/bri":87}},
+ *{"success":{"/lights/5/state/transitiontime":4}},
+ *{"success":{"/lights/5/state/on":true}},
+ *{"success":{"/lights/5/state/xy":[0.4152,0.5336]}},
+ *{"success":{"/lights/5/state/alert":"none"}}* ]
+ *
+ * @param body a data structure of lists and maps based on a JSON data
+ * @return empty array
+ */
+private handleCommandResponse(body) {
+       // scan entire response before sending events to make sure they are always in the same order
+       def updates = [:]
+
+       body.each { payload ->
+               if (payload?.success) {
+                       def childDeviceNetworkId = app.id + "/"
+                       def eventType
+                       payload.success.each { k, v ->
+                               def data = k.split("/")
+                               if (data.length == 5) {
+                                       childDeviceNetworkId = app.id + "/" + k.split("/")[2]
+                                       if (!updates[childDeviceNetworkId])
+                                               updates[childDeviceNetworkId] = [:]
+                                       eventType = k.split("/")[4]
+                                       updates[childDeviceNetworkId]."$eventType" = v
+                               }
+                       }
+               } else if (payload?.error) {
+                       log.warn "Error returned from Hue bridge, error = ${payload?.error}"
+                       // Check for unauthorized user
+                       if (payload?.error?.type?.value == 1) {
+                               log.error "Hue username is not valid"
+                               state.refreshUsernameNeeded = true
+                               state.username = null
+                       }
+                       return []
+               }
+       }
+
+       // send events for each update found above (order of events should be same as handlePoll())
+       updates.each { childDeviceNetworkId, params ->
+               def device = getChildDevice(childDeviceNetworkId)
+               def id = getId(device)
+               sendBasicEvents(device, "on", params.on)
+               sendBasicEvents(device, "bri", params.bri)
+               sendColorEvents(device, params.xy, params.hue, params.sat, params.ct)
+       }
+       return []
+}
+
+/**
+ * Handles a response to a poll (GET) sent to the Hue Bridge.
+ *
+ * Will send appropriate events depending on values changed.
+ *
+ *     Example payload
+ *
+ *{"5":{"state": {"on":true,"bri":102,"hue":25600,"sat":254,"effect":"none","xy":[0.1700,0.7000],"ct":153,"alert":"none",
+ * "colormode":"xy","reachable":true}, "type": "Extended color light", "name": "Go", "modelid": "LLC020", "manufacturername": "Philips",
+ * "uniqueid":"00:17:88:01:01:13:d5:11-0b", "swversion": "5.38.1.14378"},
+ * "6":{"state": {"on":true,"bri":103,"hue":14910,"sat":144,"effect":"none","xy":[0.4596,0.4105],"ct":370,"alert":"none",
+ * "colormode":"ct","reachable":true}, "type": "Extended color light", "name": "Upstairs Light", "modelid": "LCT007", "manufacturername": "Philips",
+ * "uniqueid":"00:17:88:01:10:56:ba:2c-0b", "swversion": "5.38.1.14919"},
+ *
+ * @param body a data structure of lists and maps based on a JSON data
+ * @return empty array
+ */
+private handlePoll(body) {
+       // Used to track "unreachable" time
+       // Device is considered "offline" if it has been in the "unreachable" state for
+       // 11 minutes (e.g. two poll intervals)
+       // Note, Hue Bridge marks devices as "unreachable" often even when they accept commands
+       Calendar time11 = Calendar.getInstance()
+       time11.add(Calendar.MINUTE, -11)
+       Calendar currentTime = Calendar.getInstance()
+
+       def bulbs = getChildDevices()
+       for (bulb in body) {
+               def device = bulbs.find { it.deviceNetworkId == "${app.id}/${bulb.key}" }
+               if (device) {
+                       if (bulb.value.state?.reachable) {
+                               if (state.bulbs[bulb.key]?.online == false || state.bulbs[bulb.key]?.online == null) {
+                                       // light just came back online, notify device watch
+                                       device.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
+                                       log.debug "$device is Online"
+                               }
+                               // Mark light as "online"
+                               state.bulbs[bulb.key]?.unreachableSince = null
+                               state.bulbs[bulb.key]?.online = true
+                       } else {
+                               if (state.bulbs[bulb.key]?.unreachableSince == null) {
+                                       // Store the first time where device was reported as "unreachable"
+                                       state.bulbs[bulb.key]?.unreachableSince = currentTime.getTimeInMillis()
+                               }
+                               if (state.bulbs[bulb.key]?.online || state.bulbs[bulb.key]?.online == null) {
+                                       // Check if device was "unreachable" for more than 11 minutes and mark "offline" if necessary
+                                       if (state.bulbs[bulb.key]?.unreachableSince < time11.getTimeInMillis() || state.bulbs[bulb.key]?.online == null) {
+                                               log.warn "$device went Offline"
+                                               state.bulbs[bulb.key]?.online = false
+                                               device.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true)
+                                       }
+                               }
+                               log.warn "$device may not reachable by Hue bridge"
+                       }
+                       // If user just executed commands, then do not send events to avoid confusing the turning on/off state
+                       if (!state.updating) {
+                               sendBasicEvents(device, "on", bulb.value?.state?.on)
+                               sendBasicEvents(device, "bri", bulb.value?.state?.bri)
+                               sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode)
+                       }
+               }
+       }
+       return []
+}
+
+private updateInProgress() {
+       state.updating = true
+       state.lastUpdateStarted = now()
+       runIn(20, updateHandler)
+}
+
+def updateHandler() {
+       state.updating = false
+       poll()
+}
+
+def hubVerification(bodytext) {
+       log.trace "Bridge sent back description.xml for verification"
+       def body = new XmlSlurper().parseText(bodytext)
+       if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) {
+               def bridges = getHueBridges()
+               def bridge = bridges.find { it?.key?.contains(body?.device?.UDN?.text()) }
+               if (bridge) {
+                       bridge.value << [name: body?.device?.friendlyName?.text(), serialNumber: body?.device?.serialNumber?.text(), verified: true]
+               } else {
+                       log.error "/description.xml returned a bridge that didn't exist"
+               }
+       }
+}
+
+def on(childDevice) {
+       log.debug "Executing 'on'"
+       def id = getId(childDevice)
+       updateInProgress()
+       createSwitchEvent(childDevice, "on")
+       put("lights/$id/state", [on: true])
+       return "Bulb is turning On"
+}
+
+def off(childDevice) {
+       log.debug "Executing 'off'"
+       def id = getId(childDevice)
+       updateInProgress()
+       createSwitchEvent(childDevice, "off")
+       put("lights/$id/state", [on: false])
+       return "Bulb is turning Off"
+}
+
+def setLevel(childDevice, percent) {
+       log.debug "Executing 'setLevel'"
+       def id = getId(childDevice)
+       updateInProgress()
+       // 1 - 254
+       def level
+       if (percent == 1)
+               level = 1
+       else
+               level = Math.min(Math.round(percent * 254 / 100), 254)
+
+       createSwitchEvent(childDevice, level > 0, percent)
+
+       // For Zigbee lights, if level is set to 0 ST just turns them off without changing level
+       // that means that the light will still be on when on is called next time
+       // Lets emulate that here
+       if (percent > 0) {
+               put("lights/$id/state", [bri: level, on: true])
+       } else {
+               put("lights/$id/state", [on: false])
+       }
+       return "Setting level to $percent"
+}
+
+def setSaturation(childDevice, percent) {
+       log.debug "Executing 'setSaturation($percent)'"
+       def id = getId(childDevice)
+       updateInProgress()
+       // 0 - 254
+       def level = Math.min(Math.round(percent * 254 / 100), 254)
+       // TODO should this be done by app only or should we default to on?
+       createSwitchEvent(childDevice, "on")
+       put("lights/$id/state", [sat: level, on: true])
+       return "Setting saturation to $percent"
+}
+
+def setHue(childDevice, percent) {
+       log.debug "Executing 'setHue($percent)'"
+       def id = getId(childDevice)
+       updateInProgress()
+       // 0 - 65535
+       def level = Math.min(Math.round(percent * 65535 / 100), 65535)
+       // TODO should this be done by app only or should we default to on?
+       createSwitchEvent(childDevice, "on")
+       put("lights/$id/state", [hue: level, on: true])
+       return "Setting hue to $percent"
+}
+
+def setColorTemperature(childDevice, huesettings) {
+       log.debug "Executing 'setColorTemperature($huesettings)'"
+       def id = getId(childDevice)
+       updateInProgress()
+       // 153 (6500K) to 500 (2000K)
+       def ct = hueSettings == 6500 ? 153 : Math.round(1000000 / huesettings)
+       createSwitchEvent(childDevice, "on")
+       put("lights/$id/state", [ct: ct, on: true])
+       return "Setting color temperature to $ct"
+}
+
+def setColor(childDevice, huesettings) {
+       log.debug "Executing 'setColor($huesettings)'"
+       def id = getId(childDevice)
+       updateInProgress()
+
+       def value = [:]
+       def hue = null
+       def sat = null
+       def xy = null
+
+       // Prefer hue/sat over hex to make sure it works with the majority of the smartapps
+       if (huesettings.hue != null || huesettings.sat != null) {
+               // If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to
+               // generate hue/sat events even though bridge will prioritize XY when setting color
+               if (huesettings.hue != null)
+                       value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
+               if (huesettings.saturation != null)
+                       value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
+       } else if (huesettings.hex != null) {
+               // For now ignore model to get a consistent color if same color is set across multiple devices
+               // def model = state.bulbs[getId(childDevice)]?.modelid
+               // value.xy = calculateXY(huesettings.hex, model)
+               // Once groups, or scenes are introduced it might be a good idea to use unique models again
+               value.xy = calculateXY(huesettings.hex)
+       }
+
+/* Disabled for now due to bad behavior via Lightning Wizard
+       if (!value.xy) {
+               // Below will translate values to hex->XY to take into account the color support of the different hue types
+               def hex = colorUtil.hslToHex((int) huesettings.hue, (int) huesettings.saturation)
+               // value.xy = calculateXY(hex, model)
+               // Once groups, or scenes are introduced it might be a good idea to use unique models again
+               value.xy = calculateXY(hex)
+       }
+*/
+
+       // Default behavior is to turn light on
+       value.on = true
+
+       if (huesettings.level != null) {
+               if (huesettings.level <= 0)
+                       value.on = false
+               else if (huesettings.level == 1)
+                       value.bri = 1
+               else
+                       value.bri = Math.min(Math.round(huesettings.level * 254 / 100), 254)
+       }
+       value.alert = huesettings.alert ? huesettings.alert : "none"
+       value.transitiontime = huesettings.transitiontime ? huesettings.transitiontime : 4
+
+       // Make sure to turn off light if requested
+       if (huesettings.switch == "off")
+               value.on = false
+
+       createSwitchEvent(childDevice, value.on ? "on" : "off")
+       put("lights/$id/state", value)
+       return "Setting color to $value"
+}
+
+private getId(childDevice) {
+       if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) {
+               return childDevice.device?.deviceNetworkId[3..-1]
+       } else {
+               return childDevice.device?.deviceNetworkId.split("/")[-1]
+       }
+}
+
+private poll() {
+       def host = getBridgeIP()
+       def uri = "/api/${state.username}/lights/"
+       log.debug "GET: $host$uri"
+       sendHubCommand(new physicalgraph.device.HubAction("GET ${uri} HTTP/1.1\r\n" +
+                       "HOST: ${host}\r\n\r\n", physicalgraph.device.Protocol.LAN, selectedHue))
+}
+
+private isOnline(id) {
+       return (state.bulbs[id]?.online != null && state.bulbs[id]?.online) || state.bulbs[id]?.online == null
+}
+
+private put(path, body) {
+       def host = getBridgeIP()
+       def uri = "/api/${state.username}/$path"
+       def bodyJSON = new groovy.json.JsonBuilder(body).toString()
+       def length = bodyJSON.getBytes().size().toString()
+
+       log.debug "PUT:  $host$uri"
+       log.debug "BODY: ${bodyJSON}"
+
+       sendHubCommand(new physicalgraph.device.HubAction("PUT $uri HTTP/1.1\r\n" +
+                       "HOST: ${host}\r\n" +
+                       "Content-Length: ${length}\r\n" +
+                       "\r\n" +
+                       "${bodyJSON}", physicalgraph.device.Protocol.LAN, "${selectedHue}"))
+}
+
+/*
+ * Bridge serial number from Hue API is in format of 0017882413ad (mac address), however on the actual bridge hardware
+ * the id is printed as only last six characters so using that to identify bridge to users
+ */
+private getBridgeIdNumber(serialNumber) {
+       def idNumber = serialNumber ?: ""
+       if (idNumber?.size() >= 6)
+               idNumber = idNumber[-6..-1].toUpperCase()
+       return idNumber
+}
+
+private getBridgeIP() {
+       def host = null
+       if (selectedHue) {
+               def d = getChildDevice(selectedHue)
+               if (d) {
+                       if (d.getDeviceDataByName("networkAddress"))
+                               host = d.getDeviceDataByName("networkAddress")
+                       else
+                               host = d.latestState('networkAddress').stringValue
+               }
+               if (host == null || host == "") {
+                       def serialNumber = selectedHue
+                       def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
+                       if (!bridge) {
+                               bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
+                       }
+                       if (bridge?.ip && bridge?.port) {
+                               if (bridge?.ip.contains("."))
+                                       host = "${bridge?.ip}:${bridge?.port}"
+                               else
+                                       host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
+                       } else if (bridge?.networkAddress && bridge?.deviceAddress)
+                               host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
+               }
+               log.trace "Bridge: $selectedHue - Host: $host"
+       }
+       return host
+}
+
+private Integer convertHexToInt(hex) {
+       Integer.parseInt(hex, 16)
+}
+
+def convertBulbListToMap() {
+       try {
+               if (state.bulbs instanceof java.util.List) {
+                       def map = [:]
+                       state.bulbs?.unique { it.id }.each { bulb ->
+                               map << ["${bulb.id}": ["id": bulb.id, "name": bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub": bulb.hub, "online": bulb.online]]
+                       }
+                       state.bulbs = map
+               }
+       }
+       catch (Exception e) {
+               log.error "Caught error attempting to convert bulb list to map: $e"
+       }
+}
+
+private String convertHexToIP(hex) {
+       [convertHexToInt(hex[0..1]), convertHexToInt(hex[2..3]), convertHexToInt(hex[4..5]), convertHexToInt(hex[6..7])].join(".")
+}
+
+private Boolean hasAllHubsOver(String desiredFirmware) {
+       return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
+}
+
+private List getRealHubFirmwareVersions() {
+       return location.hubs*.firmwareVersionString.findAll { it }
+}
+
+/**
+ * Sends appropriate turningOn/turningOff state events depending on switch or level changes.
+ *
+ * @param childDevice device to send event for
+ * @param setSwitch The new switch state, "on" or "off"
+ * @param setLevel Optional, switchLevel between 0-100, used if you set level to 0 for example since
+ *                               that should generate "off" instead of level change
+ */
+private void createSwitchEvent(childDevice, setSwitch, setLevel = null) {
+
+       if (setLevel == null) {
+               setLevel = childDevice.device?.currentValue("level")
+       }
+       // Create on, off, turningOn or turningOff event as necessary
+       def currentState = childDevice.device?.currentValue("switch")
+       if ((currentState == "off" || currentState == "turningOff")) {
+               if (setSwitch == "on" || setLevel > 0) {
+                       childDevice.sendEvent(name: "switch", value: "turningOn", displayed: false)
+               }
+       } else if ((currentState == "on" || currentState == "turningOn")) {
+               if (setSwitch == "off" || setLevel == 0) {
+                       childDevice.sendEvent(name: "switch", value: "turningOff", displayed: false)
+               }
+       }
+}
+
+/**
+ * Return the supported color range for different Hue lights. If model is not specified
+ * it defaults to the smallest Gamut (B) to ensure that colors set on a mix of devices
+ * will be consistent.
+ *
+ * @param model Philips model number, e.g. LCT001
+ * @return a map with x,y coordinates for red, green and blue according to CIE color space
+ */
+private colorPointsForModel(model = null) {
+       def result = null
+       switch (model) {
+       // Gamut A
+               case "LLC001": /* Monet, Renoir, Mondriaan (gen II) */
+               case "LLC005": /* Bloom (gen II) */
+               case "LLC006": /* Iris (gen III) */
+               case "LLC007": /* Bloom, Aura (gen III) */
+               case "LLC011": /* Hue Bloom */
+               case "LLC012": /* Hue Bloom */
+               case "LLC013": /* Storylight */
+               case "LST001": /* Light Strips */
+               case "LLC010": /* Hue Living Colors Iris + */
+                       result = [r: [x: 0.704f, y: 0.296f], g: [x: 0.2151f, y: 0.7106f], b: [x: 0.138f, y: 0.08f]];
+                       break
+       // Gamut C
+               case "LLC020": /* Hue Go */
+               case "LST002": /* Hue LightStrips Plus */
+                       result = [r: [x: 0.692f, y: 0.308f], g: [x: 0.17f, y: 0.7f], b: [x: 0.153f, y: 0.048f]];
+                       break
+       // Gamut B
+               case "LCT001": /* Hue A19 */
+               case "LCT002": /* Hue BR30 */
+               case "LCT003": /* Hue GU10 */
+               case "LCT007": /* Hue A19 + */
+               case "LLM001": /* Color Light Module + */
+               default:
+                       result = [r: [x: 0.675f, y: 0.322f], g: [x: 0.4091f, y: 0.518f], b: [x: 0.167f, y: 0.04f]];
+
+       }
+       return result;
+}
+
+/**
+ * Return x, y  value from android color and light model Id.
+ * Please note: This method does not incorporate brightness values. Get/set it from/to your light seperately.
+ *
+ * From Philips Hue SDK modified for Groovy
+ *
+ * @param color the color value in hex like // #ffa013
+ * @param model the model Id of Light (or null to get default Gamut B)
+ * @return the float array of length 2, where index 0 and 1 gives  x and y values respectively.
+ */
+private float[] calculateXY(colorStr, model = null) {
+
+       // #ffa013
+       def cred = Integer.valueOf(colorStr.substring(1, 3), 16)
+       def cgreen = Integer.valueOf(colorStr.substring(3, 5), 16)
+       def cblue = Integer.valueOf(colorStr.substring(5, 7), 16)
+
+       float red = cred / 255.0f;
+       float green = cgreen / 255.0f;
+       float blue = cblue / 255.0f;
+
+       // Wide gamut conversion D65
+       float r = ((red > 0.04045f) ? (float) Math.pow((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f));
+       float g = (green > 0.04045f) ? (float) Math.pow((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f);
+       float b = (blue > 0.04045f) ? (float) Math.pow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f);
+
+       // Why values are different in ios and android , IOS is considered
+       // Modified conversion from RGB -> XYZ with better results on colors for
+       // the lights
+       float x = r * 0.664511f + g * 0.154324f + b * 0.162028f;
+       float y = r * 0.283881f + g * 0.668433f + b * 0.047685f;
+       float z = r * 0.000088f + g * 0.072310f + b * 0.986039f;
+
+       float[] xy = new float[2];
+
+       xy[0] = (x / (x + y + z));
+       xy[1] = (y / (x + y + z));
+       if (Float.isNaN(xy[0])) {
+               xy[0] = 0.0f;
+       }
+       if (Float.isNaN(xy[1])) {
+               xy[1] = 0.0f;
+       }
+       /*if(true)
+               return [0.0f,0.0f]*/
+
+       // Check if the given XY value is within the colourreach of our lamps.
+       def xyPoint = [x: xy[0], y: xy[1]];
+       def colorPoints = colorPointsForModel(model);
+       boolean inReachOfLamps = checkPointInLampsReach(xyPoint, colorPoints);
+       if (!inReachOfLamps) {
+               // It seems the colour is out of reach
+               // let's find the closes colour we can produce with our lamp and
+               // send this XY value out.
+
+               // Find the closest point on each line in the triangle.
+               def pAB = getClosestPointToPoints(colorPoints.r, colorPoints.g, xyPoint);
+               def pAC = getClosestPointToPoints(colorPoints.b, colorPoints.r, xyPoint);
+               def pBC = getClosestPointToPoints(colorPoints.g, colorPoints.b, xyPoint);
+
+               // Get the distances per point and see which point is closer to our
+               // Point.
+               float dAB = getDistanceBetweenTwoPoints(xyPoint, pAB);
+               float dAC = getDistanceBetweenTwoPoints(xyPoint, pAC);
+               float dBC = getDistanceBetweenTwoPoints(xyPoint, pBC);
+
+               float lowest = dAB;
+               def closestPoint = pAB;
+               if (dAC < lowest) {
+                       lowest = dAC;
+                       closestPoint = pAC;
+               }
+               if (dBC < lowest) {
+                       lowest = dBC;
+                       closestPoint = pBC;
+               }
+
+               // Change the xy value to a value which is within the reach of the
+               // lamp.
+               xy[0] = closestPoint.x;
+               xy[1] = closestPoint.y;
+       }
+       //              xy[0] = PHHueHelper.precision(4, xy[0]);
+       //              xy[1] = PHHueHelper.precision(4, xy[1]);
+
+       // TODO needed, assume it just sets number of decimals?
+       //xy[0] = PHHueHelper.precision(xy[0]);
+       //xy[1] = PHHueHelper.precision(xy[1]);
+       return xy;
+}
+
+/**
+ * Generates the color for the given XY values and light model.  Model can be null if it is not known.
+ * Note: When the exact values cannot be represented, it will return the closest match.
+ * Note 2: This method does not incorporate brightness values. Get/Set it from/to your light seperately.
+ *
+ * From Philips Hue SDK modified for Groovy
+ *
+ * @param points the float array contain x and the y value. [x,y]
+ * @param model the model of the lamp, example: "LCT001" for hue bulb. Used to calculate the color gamut.
+ *             If this value is empty the default gamut values are used.
+ * @return the color value in hex (#ff03d3). If xy is null OR xy is not an array of size 2, Color. BLACK will be returned
+ */
+private String colorFromXY(points, model) {
+
+       if (points == null || model == null) {
+               log.warn "Input color missing"
+               return "#000000"
+       }
+
+       def xy = [x: points[0], y: points[1]];
+
+       def colorPoints = colorPointsForModel(model);
+       boolean inReachOfLamps = checkPointInLampsReach(xy, colorPoints);
+
+       if (!inReachOfLamps) {
+               // It seems the colour is out of reach
+               // let's find the closest colour we can produce with our lamp and
+               // send this XY value out.
+               // Find the closest point on each line in the triangle.
+               def pAB = getClosestPointToPoints(colorPoints.r, colorPoints.g, xy);
+               def pAC = getClosestPointToPoints(colorPoints.b, colorPoints.r, xy);
+               def pBC = getClosestPointToPoints(colorPoints.g, colorPoints.b, xy);
+
+               // Get the distances per point and see which point is closer to our
+               // Point.
+               float dAB = getDistanceBetweenTwoPoints(xy, pAB);
+               float dAC = getDistanceBetweenTwoPoints(xy, pAC);
+               float dBC = getDistanceBetweenTwoPoints(xy, pBC);
+               float lowest = dAB;
+               def closestPoint = pAB;
+               if (dAC < lowest) {
+                       lowest = dAC;
+                       closestPoint = pAC;
+               }
+               if (dBC < lowest) {
+                       lowest = dBC;
+                       closestPoint = pBC;
+               }
+               // Change the xy value to a value which is within the reach of the
+               // lamp.
+               xy.x = closestPoint.x;
+               xy.y = closestPoint.y;
+       }
+       float x = xy.x;
+       float y = xy.y;
+       float z = 1.0f - x - y;
+       float y2 = 1.0f;
+       float x2 = (y2 / y) * x;
+       float z2 = (y2 / y) * z;
+       /*
+        * // Wide gamut conversion float r = X * 1.612f - Y * 0.203f - Z *
+        * 0.302f; float g = -X * 0.509f + Y * 1.412f + Z * 0.066f; float b = X
+        * * 0.026f - Y * 0.072f + Z * 0.962f;
+        */
+       // sRGB conversion
+       // float r = X * 3.2410f - Y * 1.5374f - Z * 0.4986f;
+       // float g = -X * 0.9692f + Y * 1.8760f + Z * 0.0416f;
+       // float b = X * 0.0556f - Y * 0.2040f + Z * 1.0570f;
+
+       // sRGB D65 conversion
+       float r = x2 * 1.656492f - y2 * 0.354851f - z2 * 0.255038f;
+       float g = -x2 * 0.707196f + y2 * 1.655397f + z2 * 0.036152f;
+       float b = x2 * 0.051713f - y2 * 0.121364f + z2 * 1.011530f;
+
+       if (r > b && r > g && r > 1.0f) {
+               // red is too big
+               g = g / r;
+               b = b / r;
+               r = 1.0f;
+       } else if (g > b && g > r && g > 1.0f) {
+               // green is too big
+               r = r / g;
+               b = b / g;
+               g = 1.0f;
+       } else if (b > r && b > g && b > 1.0f) {
+               // blue is too big
+               r = r / b;
+               g = g / b;
+               b = 1.0f;
+       }
+       // Apply gamma correction
+       r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * (float) Math.pow(r, (1.0f / 2.4f)) - 0.055f;
+       g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * (float) Math.pow(g, (1.0f / 2.4f)) - 0.055f;
+       b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * (float) Math.pow(b, (1.0f / 2.4f)) - 0.055f;
+
+       if (r > b && r > g) {
+               // red is biggest
+               if (r > 1.0f) {
+                       g = g / r;
+                       b = b / r;
+                       r = 1.0f;
+               }
+       } else if (g > b && g > r) {
+               // green is biggest
+               if (g > 1.0f) {
+                       r = r / g;
+                       b = b / g;
+                       g = 1.0f;
+               }
+       } else if (b > r && b > g && b > 1.0f) {
+               r = r / b;
+               g = g / b;
+               b = 1.0f;
+       }
+
+       // neglecting if the value is negative.
+       if (r < 0.0f) {
+               r = 0.0f;
+       }
+       if (g < 0.0f) {
+               g = 0.0f;
+       }
+       if (b < 0.0f) {
+               b = 0.0f;
+       }
+
+       // Converting float components to int components.
+       def r1 = String.format("%02X", (int) (r * 255.0f));
+       def g1 = String.format("%02X", (int) (g * 255.0f));
+       def b1 = String.format("%02X", (int) (b * 255.0f));
+
+       return "#$r1$g1$b1"
+}
+
+/**
+ * Calculates crossProduct of two 2D vectors / points.
+ *
+ * From Philips Hue SDK modified for Groovy
+ *
+ * @param p1 first point used as vector [x: 0.0f, y: 0.0f]
+ * @param p2 second point used as vector [x: 0.0f, y: 0.0f]
+ * @return crossProduct of vectors
+ */
+private float crossProduct(p1, p2) {
+       return (p1.x * p2.y - p1.y * p2.x);
+}
+
+/**
+ * Find the closest point on a line.
+ * This point will be within reach of the lamp.
+ *
+ * From Philips Hue SDK modified for Groovy
+ *
+ * @param A the point where the line starts [x:..,y:..]
+ * @param B the point where the line ends [x:..,y:..]
+ * @param P the point which is close to a line. [x:..,y:..]
+ * @return the point which is on the line. [x:..,y:..]
+ */
+private getClosestPointToPoints(A, B, P) {
+       def AP = [x: (P.x - A.x), y: (P.y - A.y)];
+       def AB = [x: (B.x - A.x), y: (B.y - A.y)];
+
+       float ab2 = AB.x * AB.x + AB.y * AB.y;
+       float ap_ab = AP.x * AB.x + AP.y * AB.y;
+
+       float t = ap_ab / ab2;
+
+       if (t < 0.0f)
+               t = 0.0f;
+       else if (t > 1.0f)
+               t = 1.0f;
+
+       def newPoint = [x: (A.x + AB.x * t), y: (A.y + AB.y * t)];
+       return newPoint;
+}
+
+/**
+ * Find the distance between two points.
+ *
+ * From Philips Hue SDK modified for Groovy
+ *
+ * @param one [x:..,y:..]
+ * @param two [x:..,y:..]
+ * @return the distance between point one and two
+ */
+private float getDistanceBetweenTwoPoints(one, two) {
+       float dx = one.x - two.x; // horizontal difference
+       float dy = one.y - two.y; // vertical difference
+       float dist = Math.sqrt(dx * dx + dy * dy);
+
+       return dist;
+}
+
+/**
+ * Method to see if the given XY value is within the reach of the lamps.
+ *
+ * From Philips Hue SDK modified for Groovy
+ *
+ * @param p the point containing the X,Y value [x:..,y:..]
+ * @return true if within reach, false otherwise.
+ */
+private boolean checkPointInLampsReach(p, colorPoints) {
+
+       def red = colorPoints.r;
+       def green = colorPoints.g;
+       def blue = colorPoints.b;
+
+       def v1 = [x: (green.x - red.x), y: (green.y - red.y)];
+       def v2 = [x: (blue.x - red.x), y: (blue.y - red.y)];
+
+       def q = [x: (p.x - red.x), y: (p.y - red.y)];
+
+       float s = crossProduct(q, v2) / crossProduct(v1, v2);
+       float t = crossProduct(v1, q) / crossProduct(v1, v2);
+
+       if ((s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) {
+               return true;
+       } else {
+               return false;
+       }
+}
+
+/**
+ * Converts an RGB color in hex to HSV/HSB.
+ * Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
+ *
+ * @param colorStr color value in hex (#ff03d3)
+ *
+ * @return HSV representation in an array (0-100) [hue, sat, value]
+ */
+def hexToHsv(colorStr) {
+       def r = Integer.valueOf(colorStr.substring(1, 3), 16) / 255
+       def g = Integer.valueOf(colorStr.substring(3, 5), 16) / 255
+       def b = Integer.valueOf(colorStr.substring(5, 7), 16) / 255
+
+       def max = Math.max(Math.max(r, g), b)
+       def min = Math.min(Math.min(r, g), b)
+
+       def h, s, v = max
+
+       def d = max - min
+       s = max == 0 ? 0 : d / max
+
+       if (max == min) {
+               h = 0
+       } else {
+               switch (max) {
+                       case r: h = (g - b) / d + (g < b ? 6 : 0); break
+                       case g: h = (b - r) / d + 2; break
+                       case b: h = (r - g) / d + 4; break
+               }
+               h /= 6;
+       }
+
+       return [Math.round(h * 100), Math.round(s * 100), Math.round(v * 100)]
+}
+
+/**
+ * Converts HSV/HSB color to RGB in hex.
+ * Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
+ *
+ * @param hue hue 0-100
+ * @param sat saturation 0-100
+ * @param value value 0-100 (defaults to 100)
+
+ * @return the color in hex (#ff03d3)
+ */
+def hsvToHex(hue, sat, value = 100) {
+       def r, g, b;
+       def h = hue / 100
+       def s = sat / 100
+       def v = value / 100
+
+       def i = Math.floor(h * 6)
+       def f = h * 6 - i
+       def p = v * (1 - s)
+       def q = v * (1 - f * s)
+       def t = v * (1 - (1 - f) * s)
+
+       switch (i % 6) {
+               case 0:
+                       r = v
+                       g = t
+                       b = p
+                       break
+               case 1:
+                       r = q
+                       g = v
+                       b = p
+                       break
+               case 2:
+                       r = p
+                       g = v
+                       b = t
+                       break
+               case 3:
+                       r = p
+                       g = q
+                       b = v
+                       break
+               case 4:
+                       r = t
+                       g = p
+                       b = v
+                       break
+               case 5:
+                       r = v
+                       g = p
+                       b = q
+                       break
+       }
+
+       // Converting float components to int components.
+       def r1 = String.format("%02X", (int) (r * 255.0f))
+       def g1 = String.format("%02X", (int) (g * 255.0f))
+       def b1 = String.format("%02X", (int) (b * 255.0f))
+
+       return "#$r1$g1$b1"
+}