4 * Author: Juan Risso (juan@smartthings.com)
6 * Copyright 2015 SmartThings
8 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
9 * in compliance with the License. You may obtain a copy of the License at:
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
14 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
15 * for the specific language governing permissions and limitations under the License.
18 include 'localization'
21 name: "Hue (Connect)",
22 namespace: "smartthings",
23 author: "SmartThings",
24 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.",
25 category: "SmartThings Labs",
26 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
27 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png",
32 page(name: "mainPage", title: "", content: "mainPage", refreshTimeout: 5)
33 page(name: "bridgeDiscovery", title: "", content: "bridgeDiscovery", refreshTimeout: 5)
34 page(name: "bridgeDiscoveryFailed", title: "", content: "bridgeDiscoveryFailed", refreshTimeout: 0)
35 page(name: "bridgeBtnPush", title: "", content: "bridgeLinking", refreshTimeout: 5)
36 page(name: "bulbDiscovery", title: "", content: "bulbDiscovery", refreshTimeout: 5)
40 def bridges = bridgesDiscovered()
42 if (state.refreshUsernameNeeded) {
43 return bridgeLinking()
44 } else if (state.username && bridges) {
45 return bulbDiscovery()
47 return bridgeDiscovery()
51 def bridgeDiscovery(params = [:]) {
52 def bridges = bridgesDiscovered()
53 int bridgeRefreshCount = !state.bridgeRefreshCount ? 0 : state.bridgeRefreshCount as int
54 state.bridgeRefreshCount = bridgeRefreshCount + 1
55 def refreshInterval = 3
57 def options = bridges ?: []
58 def numFound = options.size() ?: "0"
60 if (state.bridgeRefreshCount == 25) {
61 log.trace "Cleaning old bridges memory"
63 app.updateSetting("selectedHue", "")
64 } else if (state.bridgeRefreshCount > 100) {
65 // five minutes have passed, give up
66 // there seems to be a problem going back from discovey failed page in some instances (compared to pressing next)
67 // however it is probably a SmartThings settings issue
69 app.updateSetting("selectedHue", "")
70 state.bridgeRefreshCount = 0
71 return bridgeDiscoveryFailed()
76 log.trace "bridgeRefreshCount: $bridgeRefreshCount"
77 //bridge discovery request every 15 //25 seconds
78 if ((bridgeRefreshCount % 5) == 0) {
82 //setup.xml request every 3 seconds except on discoveries
83 if (((bridgeRefreshCount % 3) == 0) && ((bridgeRefreshCount % 5) != 0)) {
87 return dynamicPage(name: "bridgeDiscovery", title: "Discovery Started!", nextPage: "bridgeBtnPush", refreshInterval: refreshInterval, uninstall: true) {
88 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.") {
91 input(name: "selectedHue", type: "enum", required: false, title: "Select Hue Bridge ({{numFound}} found)", messageArgs: [numFound: numFound], multiple: false, options: options, submitOnChange: true)
96 def bridgeDiscoveryFailed() {
97 return dynamicPage(name: "bridgeDiscoveryFailed", title: "Bridge Discovery Failed!", nextPage: "bridgeDiscovery") {
98 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.") {
103 def bridgeLinking() {
104 int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
105 state.linkRefreshcount = linkRefreshcount + 1
106 def refreshInterval = 3
109 def title = "Linking with your Hue"
112 if (state.refreshUsernameNeeded) {
113 paragraphText = "The current Hue username is invalid. Please press the button on your Hue Bridge to relink."
115 paragraphText = "Press the button on your Hue Bridge to setup a link."
118 paragraphText = "You haven't selected a Hue Bridge, please Press 'Done' and select one before clicking next."
120 if (state.username) { //if discovery worked
121 if (state.refreshUsernameNeeded) {
122 state.refreshUsernameNeeded = false
123 // Issue one poll with new username to cancel local polling with old username
126 nextPage = "bulbDiscovery"
128 paragraphText = "Linking to your hub was a success! Please click 'Next'!"
131 if ((linkRefreshcount % 2) == 0 && !state.username) {
135 return dynamicPage(name: "bridgeBtnPush", title: title, nextPage: nextPage, refreshInterval: refreshInterval) {
137 paragraph "$paragraphText"
142 def bulbDiscovery() {
143 int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
144 state.bulbRefreshCount = bulbRefreshCount + 1
145 def refreshInterval = 3
146 state.inBulbDiscovery = true
149 state.bridgeRefreshCount = 0
150 def allLightsFound = bulbsDiscovered() ?: [:]
152 // List lights currently not added to the user (editable)
153 def newLights = allLightsFound.findAll { getChildDevice(it.key) == null } ?: [:]
154 newLights = newLights.sort { it.value.toLowerCase() }
156 // List lights already added to the user (not editable)
157 def existingLights = allLightsFound.findAll { getChildDevice(it.key) != null } ?: [:]
158 existingLights = existingLights.sort { it.value.toLowerCase() }
160 def numFound = newLights.size() ?: "0"
162 app.updateSetting("selectedBulbs", "")
164 if ((bulbRefreshCount % 5) == 0) {
167 def selectedBridge = state.bridges.find { key, value -> value?.serialNumber?.equalsIgnoreCase(selectedHue) }
168 def title = selectedBridge?.value?.name ?: "Find bridges"
170 // List of all lights previously added shown to user
171 def existingLightsDescription = ""
172 if (existingLights) {
173 existingLights.each {
174 if (existingLightsDescription.isEmpty()) {
175 existingLightsDescription += it.value
177 existingLightsDescription += ", ${it.value}"
182 def existingLightsSize = "${existingLights.size()}"
183 if (bulbRefreshCount > 200 && numFound == "0") {
184 // Time out after 10 minutes
185 state.inBulbDiscovery = false
187 return dynamicPage(name: "bulbDiscovery", title: "Light Discovery Failed!", nextPage: "", refreshInterval: 0, install: true, uninstall: true) {
188 section("Failed to discover any lights, please try again later. Click 'Done' to exit.") {
189 paragraph title: "Previously added Hue Lights ({{existingLightsSize}} added)", messageArgs: [existingLightsSize: existingLightsSize], existingLightsDescription
192 href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
197 return dynamicPage(name: "bulbDiscovery", title: "Light Discovery Started!", nextPage: "", refreshInterval: refreshInterval, install: true, uninstall: true) {
198 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.") {
199 input(name: "selectedBulbs", type: "enum", required: false, title: "Select Hue Lights to add ({{numFound}} found)", messageArgs: [numFound: numFound], multiple: true, submitOnChange: true, options: newLights)
200 paragraph title: "Previously added Hue Lights ({{existingLightsSize}} added)", messageArgs: [existingLightsSize: existingLightsSize], existingLightsDescription
203 href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
209 private discoverBridges() {
210 log.trace "Sending Hue Discovery message to the hub"
211 sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:basic:1", physicalgraph.device.Protocol.LAN))
214 void ssdpSubscribe() {
215 subscribe(location, "ssdpTerm.urn:schemas-upnp-org:device:basic:1", ssdpBridgeHandler)
218 private sendDeveloperReq() {
220 def host = getBridgeIP()
221 sendHubCommand(new physicalgraph.device.HubAction([
227 body : [devicetype: "$token-0"]], "${selectedHue}", [callback: "usernameHandler"]))
230 private discoverHueBulbs() {
231 def host = getBridgeIP()
232 sendHubCommand(new physicalgraph.device.HubAction([
234 path : "/api/${state.username}/lights",
237 ]], "${selectedHue}", [callback: "lightsHandler"]))
240 private verifyHueBridge(String deviceNetworkId, String host) {
241 log.trace "Verify Hue Bridge $deviceNetworkId"
242 sendHubCommand(new physicalgraph.device.HubAction([
244 path : "/description.xml",
247 ]], deviceNetworkId, [callback: "bridgeDescriptionHandler"]))
250 private verifyHueBridges() {
251 def devices = getHueBridges().findAll { it?.value?.verified != true }
253 def ip = convertHexToIP(it.value.networkAddress)
254 def port = convertHexToInt(it.value.deviceAddress)
255 verifyHueBridge("${it.value.mac}", (ip + ":" + port))
259 Map bridgesDiscovered() {
260 def vbridges = getVerifiedHueBridges()
263 def value = "${it.value.name}"
264 def key = "${it.value.mac}"
265 map["${key}"] = value
270 Map bulbsDiscovered() {
271 def bulbs = getHueBulbs()
273 if (bulbs instanceof java.util.Map) {
275 def value = "${it.value.name}"
276 def key = app.id + "/" + it.value.id
277 bulbmap["${key}"] = value
279 } else { //backwards compatable
281 def value = "${it.name}"
282 def key = app.id + "/" + it.id
283 logg += "$value - $key, "
284 bulbmap["${key}"] = value
291 state.bulbs = state.bulbs ?: [:]
294 def getHueBridges() {
295 state.bridges = state.bridges ?: [:]
298 def getVerifiedHueBridges() {
299 getHueBridges().findAll { it?.value?.verified == true }
303 log.trace "Installed with settings: ${settings}"
308 log.trace "Updated with settings: ${settings}"
315 log.debug "Initializing"
317 state.inBulbDiscovery = false
318 state.bridgeRefreshCount = 0
319 state.bulbRefreshCount = 0
320 state.updating = false
326 runEvery5Minutes("doDeviceSync")
330 def manualRefresh() {
332 state.updating = false
337 // Remove bridgedevice connection to allow uninstall of smartapp even though bridge is listed
338 // as user of smartapp
339 app.updateSetting("bridgeDevice", null)
341 state.username = null
344 private setupDeviceWatch() {
345 def hub = location.hubs[0]
346 // Make sure that all child devices are enrolled in device watch
347 getChildDevices().each {
348 it.sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${hub?.hub?.hardwareID}\"}")
352 private upgradeDeviceType(device, newHueType) {
353 def deviceType = getDeviceType(newHueType)
355 // Automatically change users Hue bulbs to correct device types
356 if (deviceType && !(device?.typeName?.equalsIgnoreCase(deviceType))) {
357 log.debug "Update device type: \"$device.label\" ${device?.typeName}->$deviceType"
358 device.setDeviceType(deviceType)
362 private getDeviceType(hueType) {
363 // Determine ST device type based on Hue classification of light
364 if (hueType?.equalsIgnoreCase("Dimmable light"))
365 return "Hue Lux Bulb"
366 else if (hueType?.equalsIgnoreCase("Extended Color Light"))
368 else if (hueType?.equalsIgnoreCase("Color Light"))
370 else if (hueType?.equalsIgnoreCase("Color Temperature Light"))
371 return "Hue White Ambiance Bulb"
376 private addChildBulb(dni, hueType, name, hub, update = false, device = null) {
377 def deviceType = getDeviceType(hueType)
380 return addChildDevice("smartthings", deviceType, dni, hub, ["label": name])
382 log.warn "Device type $hueType not supported"
388 def bulbs = getHueBulbs()
389 selectedBulbs?.each { dni ->
390 def d = getChildDevice(dni)
393 if (bulbs instanceof java.util.Map) {
394 newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
395 if (newHueBulb != null) {
396 d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
398 log.debug "created ${d.displayName} with id $dni"
399 d.completedSetup = true
402 log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
405 //backwards compatable
406 newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
407 d = addChildBulb(dni, "Extended Color Light", newHueBulb?.value?.name, newHueBulb?.value?.hub)
408 d?.completedSetup = true
412 log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
413 if (bulbs instanceof java.util.Map) {
414 // Update device type if incorrect
415 def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
416 upgradeDeviceType(d, newHueBulb?.value?.type)
423 def vbridges = getVerifiedHueBridges()
424 def vbridge = vbridges.find { "${it.value.mac}" == selectedHue }
427 def d = getChildDevice(selectedHue)
429 // compatibility with old devices
432 if (it.getDeviceDataByName("mac")) {
433 def newDNI = "${it.getDeviceDataByName("mac")}"
434 if (newDNI != it.deviceNetworkId) {
435 def oldDNI = it.deviceNetworkId
436 log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
437 it.setDeviceNetworkId("${newDNI}")
438 if (oldDNI == selectedHue) {
439 app.updateSetting("selectedHue", newDNI)
446 // Hue uses last 6 digits of MAC address as ID number, this number is shown on the bottom of the bridge
447 def idNumber = getBridgeIdNumber(selectedHue)
448 d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub, ["label": "Hue Bridge ($idNumber)"])
450 // Associate smartapp to bridge so user will be warned if trying to delete bridge
451 app.updateSetting("bridgeDevice", [type: "device.hueBridge", value: d.id])
453 d.completedSetup = true
454 log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
455 def childDevice = getChildDevice(d.deviceNetworkId)
456 childDevice?.sendEvent(name: "status", value: "Online")
457 childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
458 updateBridgeStatus(childDevice)
460 childDevice?.sendEvent(name: "idNumber", value: idNumber)
461 if (vbridge.value.ip && vbridge.value.port) {
462 if (vbridge.value.ip.contains(".")) {
463 childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
464 childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port)
466 childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
467 childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
470 childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
471 childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
474 log.error "Failed to create Hue Bridge device"
478 log.debug "found ${d.displayName} with id $selectedHue already exists"
483 def ssdpBridgeHandler(evt) {
484 def description = evt.description
485 log.trace "Location: $description"
488 def parsedEvent = parseLanMessage(description)
489 parsedEvent << ["hub": hub]
491 def bridges = getHueBridges()
492 log.trace bridges.toString()
493 if (!(bridges."${parsedEvent.ssdpUSN.toString()}")) {
494 //bridge does not exist
495 log.trace "Adding bridge ${parsedEvent.ssdpUSN}"
496 bridges << ["${parsedEvent.ssdpUSN.toString()}": parsedEvent]
499 def ip = convertHexToIP(parsedEvent.networkAddress)
500 def port = convertHexToInt(parsedEvent.deviceAddress)
501 def host = ip + ":" + port
502 log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
503 def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
504 def dniReceived = "${parsedEvent.mac}"
505 def currentDni = dstate.mac
506 def d = getChildDevice(dniReceived)
507 def networkAddress = null
509 // There might be a mismatch between bridge DNI and the actual bridge mac address, correct that
510 log.debug "Bridge with $dniReceived not found"
511 def bridge = childDevices.find { it.deviceNetworkId == currentDni }
512 if (bridge != null) {
513 log.warn "Bridge is set to ${bridge.deviceNetworkId}, updating to $dniReceived"
514 bridge.setDeviceNetworkId("${dniReceived}")
515 dstate.mac = dniReceived
516 // Check to see if selectedHue is a valid bridge, otherwise update it
517 def isSelectedValid = bridges?.find { it.value?.mac == selectedHue }
518 if (isSelectedValid == null) {
519 log.warn "Correcting selectedHue in state"
520 app.updateSetting("selectedHue", dniReceived)
525 updateBridgeStatus(d)
526 if (d.getDeviceDataByName("networkAddress")) {
527 networkAddress = d.getDeviceDataByName("networkAddress")
529 networkAddress = d.latestState('networkAddress').stringValue
531 log.trace "Host: $host - $networkAddress"
532 if (host != networkAddress) {
533 log.debug "Device's port or ip changed for device $d..."
536 dstate.name = "Philips hue ($ip)"
537 d.sendEvent(name: "networkAddress", value: host)
538 d.updateDataValue("networkAddress", host)
540 if (dstate.mac != dniReceived) {
541 log.warn "Correcting bridge mac address in state"
542 dstate.mac = dniReceived
544 if (selectedHue != dniReceived) {
545 // Check to see if selectedHue is a valid bridge, otherwise update it
546 def isSelectedValid = bridges?.find { it.value?.mac == selectedHue }
547 if (isSelectedValid == null) {
548 log.warn "Correcting selectedHue in state"
549 app.updateSetting("selectedHue", dniReceived)
556 void bridgeDescriptionHandler(physicalgraph.device.HubResponse hubResponse) {
557 log.trace "description.xml response (application/xml)"
558 def body = hubResponse.xml
559 if (body?.device?.modelName?.text()?.startsWith("Philips hue bridge")) {
560 def bridges = getHueBridges()
561 def bridge = bridges.find { it?.key?.contains(body?.device?.UDN?.text()) }
563 def idNumber = getBridgeIdNumber(body?.device?.serialNumber?.text())
565 // usually in form of bridge name followed by (ip), i.e. defaults to Philips Hue (192.168.1.2)
566 // replace IP with id number to make it easier for user to identify
567 def name = body?.device?.friendlyName?.text()
568 def index = name?.indexOf('(')
570 name = name.substring(0, index)
571 name += " ($idNumber)"
573 bridge.value << [name: name, serialNumber: body?.device?.serialNumber?.text(), idNumber: idNumber, verified: true]
575 log.error "/description.xml returned a bridge that didn't exist"
580 void lightsHandler(physicalgraph.device.HubResponse hubResponse) {
581 if (isValidSource(hubResponse.mac)) {
582 def body = hubResponse.json
583 if (!body?.state?.on) { //check if first time poll made it here by mistake
584 log.debug "Adding bulbs to state!"
585 updateBulbState(body, hubResponse.hubId)
590 void usernameHandler(physicalgraph.device.HubResponse hubResponse) {
591 if (isValidSource(hubResponse.mac)) {
592 def body = hubResponse.json
593 if (body.success != null) {
594 if (body.success[0] != null) {
595 if (body.success[0].username)
596 state.username = body.success[0].username
598 } else if (body.error != null) {
599 //TODO: handle retries...
600 log.error "ERROR: application/json ${body.error}"
606 * @deprecated This has been replaced by the combination of {@link #ssdpBridgeHandler()}, {@link #bridgeDescriptionHandler()},
607 * {@link #lightsHandler()}, and {@link #usernameHandler()}. After a pending event subscription migration, it can be removed.
610 def locationHandler(evt) {
611 def description = evt.description
612 log.trace "Location: $description"
615 def parsedEvent = parseLanMessage(description)
616 parsedEvent << ["hub": hub]
618 if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:basic:1")) {
619 //SSDP DISCOVERY EVENTS
620 log.trace "SSDP DISCOVERY EVENTS"
621 def bridges = getHueBridges()
622 log.trace bridges.toString()
623 if (!(bridges."${parsedEvent.ssdpUSN.toString()}")) {
624 //bridge does not exist
625 log.trace "Adding bridge ${parsedEvent.ssdpUSN}"
626 bridges << ["${parsedEvent.ssdpUSN.toString()}": parsedEvent]
629 def ip = convertHexToIP(parsedEvent.networkAddress)
630 def port = convertHexToInt(parsedEvent.deviceAddress)
631 def host = ip + ":" + port
632 log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
633 def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
634 def dni = "${parsedEvent.mac}"
635 def d = getChildDevice(dni)
636 def networkAddress = null
639 if (it.getDeviceDataByName("mac")) {
640 def newDNI = "${it.getDeviceDataByName("mac")}"
642 if (newDNI != it.deviceNetworkId) {
643 def oldDNI = it.deviceNetworkId
644 log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
645 it.setDeviceNetworkId("${newDNI}")
646 if (oldDNI == selectedHue) {
647 app.updateSetting("selectedHue", newDNI)
654 updateBridgeStatus(d)
655 if (d.getDeviceDataByName("networkAddress")) {
656 networkAddress = d.getDeviceDataByName("networkAddress")
658 networkAddress = d.latestState('networkAddress').stringValue
660 log.trace "Host: $host - $networkAddress"
661 if (host != networkAddress) {
662 log.debug "Device's port or ip changed for device $d..."
665 dstate.name = "Philips hue ($ip)"
666 d.sendEvent(name: "networkAddress", value: host)
667 d.updateDataValue("networkAddress", host)
671 } else if (parsedEvent.headers && parsedEvent.body) {
672 log.trace "HUE BRIDGE RESPONSES"
673 def headerString = parsedEvent.headers.toString()
674 if (headerString?.contains("xml")) {
675 log.trace "description.xml response (application/xml)"
676 def body = new XmlSlurper().parseText(parsedEvent.body)
677 if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) {
678 def bridges = getHueBridges()
679 def bridge = bridges.find { it?.key?.contains(body?.device?.UDN?.text()) }
681 bridge.value << [name: body?.device?.friendlyName?.text(), serialNumber: body?.device?.serialNumber?.text(), verified: true]
683 log.error "/description.xml returned a bridge that didn't exist"
686 } else if (headerString?.contains("json") && isValidSource(parsedEvent.mac)) {
687 log.trace "description.xml response (application/json)"
688 def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
689 if (body.success != null) {
690 if (body.success[0] != null) {
691 if (body.success[0].username) {
692 state.username = body.success[0].username
695 } else if (body.error != null) {
696 //TODO: handle retries...
697 log.error "ERROR: application/json ${body.error}"
699 //GET /api/${state.username}/lights response (application/json)
700 if (!body?.state?.on) { //check if first time poll made it here by mistake
701 log.debug "Adding bulbs to state!"
702 updateBulbState(body, parsedEvent.hub)
707 log.trace "NON-HUE EVENT $evt.description"
712 log.trace "Doing Hue Device Sync!"
714 // Check if state.updating failed to clear
715 if (state.lastUpdateStarted < (now() - 20 * 1000) && state.updating) {
716 state.updating = false
717 log.warn "state.updating failed to clear"
720 convertBulbListToMap()
728 * Called when data is received from the Hue bridge, this will update the lastActivity() that
729 * is used to keep track of online/offline status of the bridge. Bridge is considered offline
730 * if not heard from in 16 minutes
732 * @param childDevice Hue Bridge child device
734 private void updateBridgeStatus(childDevice) {
735 // Update activity timestamp if child device is a valid bridge
736 def vbridges = getVerifiedHueBridges()
737 def vbridge = vbridges.find {
738 "${it.value.mac}".toUpperCase() == childDevice?.device?.deviceNetworkId?.toUpperCase()
740 vbridge?.value?.lastActivity = now()
741 if (vbridge && childDevice?.device?.currentValue("status") == "Offline") {
742 log.debug "$childDevice is back Online"
743 childDevice?.sendEvent(name: "status", value: "Online")
744 childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
749 * Check if all Hue bridges have been heard from in the last 11 minutes, if not an Offline event will be sent
750 * for the bridge and all connected lights. Also, set ID number on bridge if not done previously.
752 private void checkBridgeStatus() {
753 def bridges = getHueBridges()
754 // Check if each bridge has been heard from within the last 11 minutes (2 poll intervals times 5 minutes plus buffer)
755 def time = now() - (1000 * 60 * 11)
757 def d = getChildDevice(it.value.mac)
759 // Set id number on bridge if not done
760 if (it.value.idNumber == null) {
761 it.value.idNumber = getBridgeIdNumber(it.value.serialNumber)
762 d.sendEvent(name: "idNumber", value: it.value.idNumber)
765 if (it.value.lastActivity < time) { // it.value.lastActivity != null &&
766 if (d.currentStatus == "Online") {
767 log.warn "$d is Offline"
768 d.sendEvent(name: "status", value: "Offline")
769 d.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true)
771 Calendar currentTime = Calendar.getInstance()
772 getChildDevices().each {
774 if (state.bulbs[id]?.online == true) {
775 state.bulbs[id]?.online = false
776 state.bulbs[id]?.unreachableSince = currentTime.getTimeInMillis()
777 it.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true)
781 } else if (d.currentStatus == "Offline") {
782 log.debug "$d is back Online"
783 d.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
784 d.sendEvent(name: "status", value: "Online")//setOnline(false)
790 def isValidSource(macAddress) {
791 def vbridges = getVerifiedHueBridges()
792 return (vbridges?.find { "${it.value.mac}" == macAddress }) != null
795 def isInBulbDiscovery() {
796 return state.inBulbDiscovery
799 private updateBulbState(messageBody, hub) {
800 def bulbs = getHueBulbs()
802 // Copy of bulbs used to locate old lights in state that are no longer on bridge
806 messageBody.each { k, v ->
808 if (v instanceof Map) {
809 if (bulbs[k] == null) {
812 bulbs[k] << [id: k, name: v.name, type: v.type, modelid: v.modelid, hub: hub, remove: false]
817 // Remove bulbs from state that are no longer discovered
818 toRemove.each { k, v ->
819 log.warn "${bulbs[k].name} no longer exists on bridge, removing"
824 /////////////////////////////////////
825 //CHILD DEVICE METHODS
826 /////////////////////////////////////
828 def parse(childDevice, description) {
829 // Update activity timestamp if child device is a valid bridge
830 updateBridgeStatus(childDevice)
832 def parsedEvent = parseLanMessage(description)
833 if (parsedEvent.headers && parsedEvent.body) {
834 def headerString = parsedEvent.headers.toString()
835 def bodyString = parsedEvent.body.toString()
836 if (headerString?.contains("json")) {
839 body = new groovy.json.JsonSlurper().parseText(bodyString)
841 log.warn "Parsing Body failed"
843 if (body instanceof java.util.Map) {
844 // get (poll) reponse
845 return handlePoll(body)
848 return handleCommandResponse(body)
852 log.debug "parse - got something other than headers,body..."
857 // Philips Hue priority for color is xy > ct > hs
858 // For SmartThings, try to always send hue, sat and hex
859 private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
860 if (device == null || (xy == null && hue == null && sat == null && ct == null))
864 // For now, only care about changing color temperature if requested by user
865 if (ct != null && ct != 0 && (colormode == "ct" || (xy == null && hue == null && sat == null))) {
866 // for some reason setting Hue to their specified minimum off 153 yields 154, dealt with below
867 // 153 (6500K) to 500 (2000K)
868 def temp = (ct == 154) ? 6500 : Math.round(1000000 / ct)
869 device.sendEvent([name: "colorTemperature", value: temp, descriptionText: "Color temperature has changed"])
870 // Return because color temperature change is not counted as a color change in SmartThings so no hex update necessary
876 def value = Math.min(Math.round(hue * 100 / 65535), 65535) as int
877 events["hue"] = [name: "hue", value: value, descriptionText: "Color has changed", displayed: false]
882 def value = Math.round(sat * 100 / 254) as int
883 events["saturation"] = [name: "saturation", value: value, descriptionText: "Color has changed", displayed: false]
886 // Following is used to decide what to base hex calculations on since it is preferred to return a colorchange in hex
887 if (xy != null && colormode != "hs") {
888 // If xy is present and color mode is not specified to hs, pick xy because of priority
890 // [0.0-1.0, 0.0-1.0]
891 def id = device.deviceNetworkId?.split("/")[1]
892 def model = state.bulbs[id]?.modelid
893 def hex = colorFromXY(xy, model)
895 // Create Hue and Saturation events if not previously existing
896 def hsv = hexToHsv(hex)
897 if (events["hue"] == null)
898 events["hue"] = [name: "hue", value: hsv[0], descriptionText: "Color has changed", displayed: false]
899 if (events["saturation"] == null)
900 events["saturation"] = [name: "saturation", value: hsv[1], descriptionText: "Color has changed", displayed: false]
902 events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
903 } else if (colormode == "hs" || colormode == null) {
904 // colormode is "hs" or "xy" is missing, default to follow hue/sat which is already handled above
905 def hueValue = (hue != null) ? events["hue"].value : Integer.parseInt("$device.currentHue")
906 def satValue = (sat != null) ? events["saturation"].value : Integer.parseInt("$device.currentSaturation")
909 def hex = hsvToHex(hueValue, satValue)
910 events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
913 boolean sendColorChanged = false
915 device.sendEvent(it.value)
919 private sendBasicEvents(device, param, value) {
920 if (device == null || value == null || param == null)
925 device.sendEvent(name: "switch", value: (value == true) ? "on" : "off")
929 def level = Math.max(1, Math.round(value * 100 / 254)) as int
930 device.sendEvent(name: "level", value: level, descriptionText: "Level has changed to ${level}%")
936 * Handles a response to a command (PUT) sent to the Hue Bridge.
938 * Will send appropriate events depending on values changed.
942 *{"success":{"/lights/5/state/bri":87}},
943 *{"success":{"/lights/5/state/transitiontime":4}},
944 *{"success":{"/lights/5/state/on":true}},
945 *{"success":{"/lights/5/state/xy":[0.4152,0.5336]}},
946 *{"success":{"/lights/5/state/alert":"none"}}* ]
948 * @param body a data structure of lists and maps based on a JSON data
949 * @return empty array
951 private handleCommandResponse(body) {
952 // scan entire response before sending events to make sure they are always in the same order
955 body.each { payload ->
956 if (payload?.success) {
957 def childDeviceNetworkId = app.id + "/"
959 payload.success.each { k, v ->
960 def data = k.split("/")
961 if (data.length == 5) {
962 childDeviceNetworkId = app.id + "/" + k.split("/")[2]
963 if (!updates[childDeviceNetworkId])
964 updates[childDeviceNetworkId] = [:]
965 eventType = k.split("/")[4]
966 updates[childDeviceNetworkId]."$eventType" = v
969 } else if (payload?.error) {
970 log.warn "Error returned from Hue bridge, error = ${payload?.error}"
971 // Check for unauthorized user
972 if (payload?.error?.type?.value == 1) {
973 log.error "Hue username is not valid"
974 state.refreshUsernameNeeded = true
975 state.username = null
981 // send events for each update found above (order of events should be same as handlePoll())
982 updates.each { childDeviceNetworkId, params ->
983 def device = getChildDevice(childDeviceNetworkId)
984 def id = getId(device)
985 sendBasicEvents(device, "on", params.on)
986 sendBasicEvents(device, "bri", params.bri)
987 sendColorEvents(device, params.xy, params.hue, params.sat, params.ct)
993 * Handles a response to a poll (GET) sent to the Hue Bridge.
995 * Will send appropriate events depending on values changed.
999 *{"5":{"state": {"on":true,"bri":102,"hue":25600,"sat":254,"effect":"none","xy":[0.1700,0.7000],"ct":153,"alert":"none",
1000 * "colormode":"xy","reachable":true}, "type": "Extended color light", "name": "Go", "modelid": "LLC020", "manufacturername": "Philips",
1001 * "uniqueid":"00:17:88:01:01:13:d5:11-0b", "swversion": "5.38.1.14378"},
1002 * "6":{"state": {"on":true,"bri":103,"hue":14910,"sat":144,"effect":"none","xy":[0.4596,0.4105],"ct":370,"alert":"none",
1003 * "colormode":"ct","reachable":true}, "type": "Extended color light", "name": "Upstairs Light", "modelid": "LCT007", "manufacturername": "Philips",
1004 * "uniqueid":"00:17:88:01:10:56:ba:2c-0b", "swversion": "5.38.1.14919"},
1006 * @param body a data structure of lists and maps based on a JSON data
1007 * @return empty array
1009 private handlePoll(body) {
1010 // Used to track "unreachable" time
1011 // Device is considered "offline" if it has been in the "unreachable" state for
1012 // 11 minutes (e.g. two poll intervals)
1013 // Note, Hue Bridge marks devices as "unreachable" often even when they accept commands
1014 Calendar time11 = Calendar.getInstance()
1015 time11.add(Calendar.MINUTE, -11)
1016 Calendar currentTime = Calendar.getInstance()
1018 def bulbs = getChildDevices()
1019 for (bulb in body) {
1020 def device = bulbs.find { it.deviceNetworkId == "${app.id}/${bulb.key}" }
1022 if (bulb.value.state?.reachable) {
1023 if (state.bulbs[bulb.key]?.online == false || state.bulbs[bulb.key]?.online == null) {
1024 // light just came back online, notify device watch
1025 device.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
1026 log.debug "$device is Online"
1028 // Mark light as "online"
1029 state.bulbs[bulb.key]?.unreachableSince = null
1030 state.bulbs[bulb.key]?.online = true
1032 if (state.bulbs[bulb.key]?.unreachableSince == null) {
1033 // Store the first time where device was reported as "unreachable"
1034 state.bulbs[bulb.key]?.unreachableSince = currentTime.getTimeInMillis()
1036 if (state.bulbs[bulb.key]?.online || state.bulbs[bulb.key]?.online == null) {
1037 // Check if device was "unreachable" for more than 11 minutes and mark "offline" if necessary
1038 if (state.bulbs[bulb.key]?.unreachableSince < time11.getTimeInMillis() || state.bulbs[bulb.key]?.online == null) {
1039 log.warn "$device went Offline"
1040 state.bulbs[bulb.key]?.online = false
1041 device.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true)
1044 log.warn "$device may not reachable by Hue bridge"
1046 // If user just executed commands, then do not send events to avoid confusing the turning on/off state
1047 if (!state.updating) {
1048 sendBasicEvents(device, "on", bulb.value?.state?.on)
1049 sendBasicEvents(device, "bri", bulb.value?.state?.bri)
1050 sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode)
1057 private updateInProgress() {
1058 state.updating = true
1059 state.lastUpdateStarted = now()
1060 runIn(20, updateHandler)
1063 def updateHandler() {
1064 state.updating = false
1068 def hubVerification(bodytext) {
1069 log.trace "Bridge sent back description.xml for verification"
1070 def body = new XmlSlurper().parseText(bodytext)
1071 if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) {
1072 def bridges = getHueBridges()
1073 def bridge = bridges.find { it?.key?.contains(body?.device?.UDN?.text()) }
1075 bridge.value << [name: body?.device?.friendlyName?.text(), serialNumber: body?.device?.serialNumber?.text(), verified: true]
1077 log.error "/description.xml returned a bridge that didn't exist"
1082 def on(childDevice) {
1083 log.debug "Executing 'on'"
1084 def id = getId(childDevice)
1086 createSwitchEvent(childDevice, "on")
1087 put("lights/$id/state", [on: true])
1088 return "Bulb is turning On"
1091 def off(childDevice) {
1092 log.debug "Executing 'off'"
1093 def id = getId(childDevice)
1095 createSwitchEvent(childDevice, "off")
1096 put("lights/$id/state", [on: false])
1097 return "Bulb is turning Off"
1100 def setLevel(childDevice, percent) {
1101 log.debug "Executing 'setLevel'"
1102 def id = getId(childDevice)
1109 level = Math.min(Math.round(percent * 254 / 100), 254)
1111 createSwitchEvent(childDevice, level > 0, percent)
1113 // For Zigbee lights, if level is set to 0 ST just turns them off without changing level
1114 // that means that the light will still be on when on is called next time
1115 // Lets emulate that here
1117 put("lights/$id/state", [bri: level, on: true])
1119 put("lights/$id/state", [on: false])
1121 return "Setting level to $percent"
1124 def setSaturation(childDevice, percent) {
1125 log.debug "Executing 'setSaturation($percent)'"
1126 def id = getId(childDevice)
1129 def level = Math.min(Math.round(percent * 254 / 100), 254)
1130 // TODO should this be done by app only or should we default to on?
1131 createSwitchEvent(childDevice, "on")
1132 put("lights/$id/state", [sat: level, on: true])
1133 return "Setting saturation to $percent"
1136 def setHue(childDevice, percent) {
1137 log.debug "Executing 'setHue($percent)'"
1138 def id = getId(childDevice)
1141 def level = Math.min(Math.round(percent * 65535 / 100), 65535)
1142 // TODO should this be done by app only or should we default to on?
1143 createSwitchEvent(childDevice, "on")
1144 put("lights/$id/state", [hue: level, on: true])
1145 return "Setting hue to $percent"
1148 def setColorTemperature(childDevice, huesettings) {
1149 log.debug "Executing 'setColorTemperature($huesettings)'"
1150 def id = getId(childDevice)
1152 // 153 (6500K) to 500 (2000K)
1153 def ct = hueSettings == 6500 ? 153 : Math.round(1000000 / huesettings)
1154 createSwitchEvent(childDevice, "on")
1155 put("lights/$id/state", [ct: ct, on: true])
1156 return "Setting color temperature to $ct"
1159 def setColor(childDevice, huesettings) {
1160 log.debug "Executing 'setColor($huesettings)'"
1161 def id = getId(childDevice)
1169 // Prefer hue/sat over hex to make sure it works with the majority of the smartapps
1170 if (huesettings.hue != null || huesettings.sat != null) {
1171 // If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to
1172 // generate hue/sat events even though bridge will prioritize XY when setting color
1173 if (huesettings.hue != null)
1174 value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
1175 if (huesettings.saturation != null)
1176 value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
1177 } else if (huesettings.hex != null) {
1178 // For now ignore model to get a consistent color if same color is set across multiple devices
1179 // def model = state.bulbs[getId(childDevice)]?.modelid
1180 // value.xy = calculateXY(huesettings.hex, model)
1181 // Once groups, or scenes are introduced it might be a good idea to use unique models again
1182 value.xy = calculateXY(huesettings.hex)
1185 /* Disabled for now due to bad behavior via Lightning Wizard
1187 // Below will translate values to hex->XY to take into account the color support of the different hue types
1188 def hex = colorUtil.hslToHex((int) huesettings.hue, (int) huesettings.saturation)
1189 // value.xy = calculateXY(hex, model)
1190 // Once groups, or scenes are introduced it might be a good idea to use unique models again
1191 value.xy = calculateXY(hex)
1195 // Default behavior is to turn light on
1198 if (huesettings.level != null) {
1199 if (huesettings.level <= 0)
1201 else if (huesettings.level == 1)
1204 value.bri = Math.min(Math.round(huesettings.level * 254 / 100), 254)
1206 value.alert = huesettings.alert ? huesettings.alert : "none"
1207 value.transitiontime = huesettings.transitiontime ? huesettings.transitiontime : 4
1209 // Make sure to turn off light if requested
1210 if (huesettings.switch == "off")
1213 createSwitchEvent(childDevice, value.on ? "on" : "off")
1214 put("lights/$id/state", value)
1215 return "Setting color to $value"
1218 private getId(childDevice) {
1219 if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) {
1220 return childDevice.device?.deviceNetworkId[3..-1]
1222 return childDevice.device?.deviceNetworkId.split("/")[-1]
1227 def host = getBridgeIP()
1228 def uri = "/api/${state.username}/lights/"
1229 log.debug "GET: $host$uri"
1230 sendHubCommand(new physicalgraph.device.HubAction("GET ${uri} HTTP/1.1\r\n" +
1231 "HOST: ${host}\r\n\r\n", physicalgraph.device.Protocol.LAN, selectedHue))
1234 private isOnline(id) {
1235 return (state.bulbs[id]?.online != null && state.bulbs[id]?.online) || state.bulbs[id]?.online == null
1238 private put(path, body) {
1239 def host = getBridgeIP()
1240 def uri = "/api/${state.username}/$path"
1241 def bodyJSON = new groovy.json.JsonBuilder(body).toString()
1242 def length = bodyJSON.getBytes().size().toString()
1244 log.debug "PUT: $host$uri"
1245 log.debug "BODY: ${bodyJSON}"
1247 sendHubCommand(new physicalgraph.device.HubAction("PUT $uri HTTP/1.1\r\n" +
1248 "HOST: ${host}\r\n" +
1249 "Content-Length: ${length}\r\n" +
1251 "${bodyJSON}", physicalgraph.device.Protocol.LAN, "${selectedHue}"))
1255 * Bridge serial number from Hue API is in format of 0017882413ad (mac address), however on the actual bridge hardware
1256 * the id is printed as only last six characters so using that to identify bridge to users
1258 private getBridgeIdNumber(serialNumber) {
1259 def idNumber = serialNumber ?: ""
1260 if (idNumber?.size() >= 6)
1261 idNumber = idNumber[-6..-1].toUpperCase()
1265 private getBridgeIP() {
1268 def d = getChildDevice(selectedHue)
1270 if (d.getDeviceDataByName("networkAddress"))
1271 host = d.getDeviceDataByName("networkAddress")
1273 host = d.latestState('networkAddress').stringValue
1275 if (host == null || host == "") {
1276 def serialNumber = selectedHue
1277 def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
1279 bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
1281 if (bridge?.ip && bridge?.port) {
1282 if (bridge?.ip.contains("."))
1283 host = "${bridge?.ip}:${bridge?.port}"
1285 host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
1286 } else if (bridge?.networkAddress && bridge?.deviceAddress)
1287 host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
1289 log.trace "Bridge: $selectedHue - Host: $host"
1294 private Integer convertHexToInt(hex) {
1295 Integer.parseInt(hex, 16)
1298 def convertBulbListToMap() {
1300 if (state.bulbs instanceof java.util.List) {
1302 state.bulbs?.unique { it.id }.each { bulb ->
1303 map << ["${bulb.id}": ["id": bulb.id, "name": bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub": bulb.hub, "online": bulb.online]]
1308 catch (Exception e) {
1309 log.error "Caught error attempting to convert bulb list to map: $e"
1313 private String convertHexToIP(hex) {
1314 [convertHexToInt(hex[0..1]), convertHexToInt(hex[2..3]), convertHexToInt(hex[4..5]), convertHexToInt(hex[6..7])].join(".")
1317 private Boolean hasAllHubsOver(String desiredFirmware) {
1318 return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
1321 private List getRealHubFirmwareVersions() {
1322 return location.hubs*.firmwareVersionString.findAll { it }
1326 * Sends appropriate turningOn/turningOff state events depending on switch or level changes.
1328 * @param childDevice device to send event for
1329 * @param setSwitch The new switch state, "on" or "off"
1330 * @param setLevel Optional, switchLevel between 0-100, used if you set level to 0 for example since
1331 * that should generate "off" instead of level change
1333 private void createSwitchEvent(childDevice, setSwitch, setLevel = null) {
1335 if (setLevel == null) {
1336 setLevel = childDevice.device?.currentValue("level")
1338 // Create on, off, turningOn or turningOff event as necessary
1339 def currentState = childDevice.device?.currentValue("switch")
1340 if ((currentState == "off" || currentState == "turningOff")) {
1341 if (setSwitch == "on" || setLevel > 0) {
1342 childDevice.sendEvent(name: "switch", value: "turningOn", displayed: false)
1344 } else if ((currentState == "on" || currentState == "turningOn")) {
1345 if (setSwitch == "off" || setLevel == 0) {
1346 childDevice.sendEvent(name: "switch", value: "turningOff", displayed: false)
1352 * Return the supported color range for different Hue lights. If model is not specified
1353 * it defaults to the smallest Gamut (B) to ensure that colors set on a mix of devices
1354 * will be consistent.
1356 * @param model Philips model number, e.g. LCT001
1357 * @return a map with x,y coordinates for red, green and blue according to CIE color space
1359 private colorPointsForModel(model = null) {
1363 case "LLC001": /* Monet, Renoir, Mondriaan (gen II) */
1364 case "LLC005": /* Bloom (gen II) */
1365 case "LLC006": /* Iris (gen III) */
1366 case "LLC007": /* Bloom, Aura (gen III) */
1367 case "LLC011": /* Hue Bloom */
1368 case "LLC012": /* Hue Bloom */
1369 case "LLC013": /* Storylight */
1370 case "LST001": /* Light Strips */
1371 case "LLC010": /* Hue Living Colors Iris + */
1372 result = [r: [x: 0.704f, y: 0.296f], g: [x: 0.2151f, y: 0.7106f], b: [x: 0.138f, y: 0.08f]];
1375 case "LLC020": /* Hue Go */
1376 case "LST002": /* Hue LightStrips Plus */
1377 result = [r: [x: 0.692f, y: 0.308f], g: [x: 0.17f, y: 0.7f], b: [x: 0.153f, y: 0.048f]];
1380 case "LCT001": /* Hue A19 */
1381 case "LCT002": /* Hue BR30 */
1382 case "LCT003": /* Hue GU10 */
1383 case "LCT007": /* Hue A19 + */
1384 case "LLM001": /* Color Light Module + */
1386 result = [r: [x: 0.675f, y: 0.322f], g: [x: 0.4091f, y: 0.518f], b: [x: 0.167f, y: 0.04f]];
1393 * Return x, y value from android color and light model Id.
1394 * Please note: This method does not incorporate brightness values. Get/set it from/to your light seperately.
1396 * From Philips Hue SDK modified for Groovy
1398 * @param color the color value in hex like // #ffa013
1399 * @param model the model Id of Light (or null to get default Gamut B)
1400 * @return the float array of length 2, where index 0 and 1 gives x and y values respectively.
1402 private float[] calculateXY(colorStr, model = null) {
1405 def cred = Integer.valueOf(colorStr.substring(1, 3), 16)
1406 def cgreen = Integer.valueOf(colorStr.substring(3, 5), 16)
1407 def cblue = Integer.valueOf(colorStr.substring(5, 7), 16)
1409 float red = cred / 255.0f;
1410 float green = cgreen / 255.0f;
1411 float blue = cblue / 255.0f;
1413 // Wide gamut conversion D65
1414 float r = ((red > 0.04045f) ? (float) Math.pow((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f));
1415 float g = (green > 0.04045f) ? (float) Math.pow((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f);
1416 float b = (blue > 0.04045f) ? (float) Math.pow((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f);
1418 // Why values are different in ios and android , IOS is considered
1419 // Modified conversion from RGB -> XYZ with better results on colors for
1421 float x = r * 0.664511f + g * 0.154324f + b * 0.162028f;
1422 float y = r * 0.283881f + g * 0.668433f + b * 0.047685f;
1423 float z = r * 0.000088f + g * 0.072310f + b * 0.986039f;
1425 float[] xy = new float[2];
1427 xy[0] = (x / (x + y + z));
1428 xy[1] = (y / (x + y + z));
1429 if (Float.isNaN(xy[0])) {
1432 if (Float.isNaN(xy[1])) {
1436 return [0.0f,0.0f]*/
1438 // Check if the given XY value is within the colourreach of our lamps.
1439 def xyPoint = [x: xy[0], y: xy[1]];
1440 def colorPoints = colorPointsForModel(model);
1441 boolean inReachOfLamps = checkPointInLampsReach(xyPoint, colorPoints);
1442 if (!inReachOfLamps) {
1443 // It seems the colour is out of reach
1444 // let's find the closes colour we can produce with our lamp and
1445 // send this XY value out.
1447 // Find the closest point on each line in the triangle.
1448 def pAB = getClosestPointToPoints(colorPoints.r, colorPoints.g, xyPoint);
1449 def pAC = getClosestPointToPoints(colorPoints.b, colorPoints.r, xyPoint);
1450 def pBC = getClosestPointToPoints(colorPoints.g, colorPoints.b, xyPoint);
1452 // Get the distances per point and see which point is closer to our
1454 float dAB = getDistanceBetweenTwoPoints(xyPoint, pAB);
1455 float dAC = getDistanceBetweenTwoPoints(xyPoint, pAC);
1456 float dBC = getDistanceBetweenTwoPoints(xyPoint, pBC);
1459 def closestPoint = pAB;
1469 // Change the xy value to a value which is within the reach of the
1471 xy[0] = closestPoint.x;
1472 xy[1] = closestPoint.y;
1474 // xy[0] = PHHueHelper.precision(4, xy[0]);
1475 // xy[1] = PHHueHelper.precision(4, xy[1]);
1477 // TODO needed, assume it just sets number of decimals?
1478 //xy[0] = PHHueHelper.precision(xy[0]);
1479 //xy[1] = PHHueHelper.precision(xy[1]);
1484 * Generates the color for the given XY values and light model. Model can be null if it is not known.
1485 * Note: When the exact values cannot be represented, it will return the closest match.
1486 * Note 2: This method does not incorporate brightness values. Get/Set it from/to your light seperately.
1488 * From Philips Hue SDK modified for Groovy
1490 * @param points the float array contain x and the y value. [x,y]
1491 * @param model the model of the lamp, example: "LCT001" for hue bulb. Used to calculate the color gamut.
1492 * If this value is empty the default gamut values are used.
1493 * @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
1495 private String colorFromXY(points, model) {
1497 if (points == null || model == null) {
1498 log.warn "Input color missing"
1502 def xy = [x: points[0], y: points[1]];
1504 def colorPoints = colorPointsForModel(model);
1505 boolean inReachOfLamps = checkPointInLampsReach(xy, colorPoints);
1507 if (!inReachOfLamps) {
1508 // It seems the colour is out of reach
1509 // let's find the closest colour we can produce with our lamp and
1510 // send this XY value out.
1511 // Find the closest point on each line in the triangle.
1512 def pAB = getClosestPointToPoints(colorPoints.r, colorPoints.g, xy);
1513 def pAC = getClosestPointToPoints(colorPoints.b, colorPoints.r, xy);
1514 def pBC = getClosestPointToPoints(colorPoints.g, colorPoints.b, xy);
1516 // Get the distances per point and see which point is closer to our
1518 float dAB = getDistanceBetweenTwoPoints(xy, pAB);
1519 float dAC = getDistanceBetweenTwoPoints(xy, pAC);
1520 float dBC = getDistanceBetweenTwoPoints(xy, pBC);
1522 def closestPoint = pAB;
1531 // Change the xy value to a value which is within the reach of the
1533 xy.x = closestPoint.x;
1534 xy.y = closestPoint.y;
1538 float z = 1.0f - x - y;
1540 float x2 = (y2 / y) * x;
1541 float z2 = (y2 / y) * z;
1543 * // Wide gamut conversion float r = X * 1.612f - Y * 0.203f - Z *
1544 * 0.302f; float g = -X * 0.509f + Y * 1.412f + Z * 0.066f; float b = X
1545 * * 0.026f - Y * 0.072f + Z * 0.962f;
1548 // float r = X * 3.2410f - Y * 1.5374f - Z * 0.4986f;
1549 // float g = -X * 0.9692f + Y * 1.8760f + Z * 0.0416f;
1550 // float b = X * 0.0556f - Y * 0.2040f + Z * 1.0570f;
1552 // sRGB D65 conversion
1553 float r = x2 * 1.656492f - y2 * 0.354851f - z2 * 0.255038f;
1554 float g = -x2 * 0.707196f + y2 * 1.655397f + z2 * 0.036152f;
1555 float b = x2 * 0.051713f - y2 * 0.121364f + z2 * 1.011530f;
1557 if (r > b && r > g && r > 1.0f) {
1562 } else if (g > b && g > r && g > 1.0f) {
1567 } else if (b > r && b > g && b > 1.0f) {
1573 // Apply gamma correction
1574 r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * (float) Math.pow(r, (1.0f / 2.4f)) - 0.055f;
1575 g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * (float) Math.pow(g, (1.0f / 2.4f)) - 0.055f;
1576 b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * (float) Math.pow(b, (1.0f / 2.4f)) - 0.055f;
1578 if (r > b && r > g) {
1585 } else if (g > b && g > r) {
1592 } else if (b > r && b > g && b > 1.0f) {
1598 // neglecting if the value is negative.
1609 // Converting float components to int components.
1610 def r1 = String.format("%02X", (int) (r * 255.0f));
1611 def g1 = String.format("%02X", (int) (g * 255.0f));
1612 def b1 = String.format("%02X", (int) (b * 255.0f));
1618 * Calculates crossProduct of two 2D vectors / points.
1620 * From Philips Hue SDK modified for Groovy
1622 * @param p1 first point used as vector [x: 0.0f, y: 0.0f]
1623 * @param p2 second point used as vector [x: 0.0f, y: 0.0f]
1624 * @return crossProduct of vectors
1626 private float crossProduct(p1, p2) {
1627 return (p1.x * p2.y - p1.y * p2.x);
1631 * Find the closest point on a line.
1632 * This point will be within reach of the lamp.
1634 * From Philips Hue SDK modified for Groovy
1636 * @param A the point where the line starts [x:..,y:..]
1637 * @param B the point where the line ends [x:..,y:..]
1638 * @param P the point which is close to a line. [x:..,y:..]
1639 * @return the point which is on the line. [x:..,y:..]
1641 private getClosestPointToPoints(A, B, P) {
1642 def AP = [x: (P.x - A.x), y: (P.y - A.y)];
1643 def AB = [x: (B.x - A.x), y: (B.y - A.y)];
1645 float ab2 = AB.x * AB.x + AB.y * AB.y;
1646 float ap_ab = AP.x * AB.x + AP.y * AB.y;
1648 float t = ap_ab / ab2;
1655 def newPoint = [x: (A.x + AB.x * t), y: (A.y + AB.y * t)];
1660 * Find the distance between two points.
1662 * From Philips Hue SDK modified for Groovy
1664 * @param one [x:..,y:..]
1665 * @param two [x:..,y:..]
1666 * @return the distance between point one and two
1668 private float getDistanceBetweenTwoPoints(one, two) {
1669 float dx = one.x - two.x; // horizontal difference
1670 float dy = one.y - two.y; // vertical difference
1671 float dist = Math.sqrt(dx * dx + dy * dy);
1677 * Method to see if the given XY value is within the reach of the lamps.
1679 * From Philips Hue SDK modified for Groovy
1681 * @param p the point containing the X,Y value [x:..,y:..]
1682 * @return true if within reach, false otherwise.
1684 private boolean checkPointInLampsReach(p, colorPoints) {
1686 def red = colorPoints.r;
1687 def green = colorPoints.g;
1688 def blue = colorPoints.b;
1690 def v1 = [x: (green.x - red.x), y: (green.y - red.y)];
1691 def v2 = [x: (blue.x - red.x), y: (blue.y - red.y)];
1693 def q = [x: (p.x - red.x), y: (p.y - red.y)];
1695 float s = crossProduct(q, v2) / crossProduct(v1, v2);
1696 float t = crossProduct(v1, q) / crossProduct(v1, v2);
1698 if ((s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) {
1706 * Converts an RGB color in hex to HSV/HSB.
1707 * Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
1709 * @param colorStr color value in hex (#ff03d3)
1711 * @return HSV representation in an array (0-100) [hue, sat, value]
1713 def hexToHsv(colorStr) {
1714 def r = Integer.valueOf(colorStr.substring(1, 3), 16) / 255
1715 def g = Integer.valueOf(colorStr.substring(3, 5), 16) / 255
1716 def b = Integer.valueOf(colorStr.substring(5, 7), 16) / 255
1718 def max = Math.max(Math.max(r, g), b)
1719 def min = Math.min(Math.min(r, g), b)
1724 s = max == 0 ? 0 : d / max
1730 case r: h = (g - b) / d + (g < b ? 6 : 0); break
1731 case g: h = (b - r) / d + 2; break
1732 case b: h = (r - g) / d + 4; break
1737 return [Math.round(h * 100), Math.round(s * 100), Math.round(v * 100)]
1741 * Converts HSV/HSB color to RGB in hex.
1742 * Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
1744 * @param hue hue 0-100
1745 * @param sat saturation 0-100
1746 * @param value value 0-100 (defaults to 100)
1748 * @return the color in hex (#ff03d3)
1750 def hsvToHex(hue, sat, value = 100) {
1756 def i = Math.floor(h * 6)
1759 def q = v * (1 - f * s)
1760 def t = v * (1 - (1 - f) * s)
1795 // Converting float components to int components.
1796 def r1 = String.format("%02X", (int) (r * 255.0f))
1797 def g1 = String.format("%02X", (int) (g * 255.0f))
1798 def b1 = String.format("%02X", (int) (b * 255.0f))