2 * Hue Lights and Groups and Scenes (OH MY) - new Hue Service Manager
4 * Version 1.4: Added ability to create / modify / delete Hue Hub Scenes directly from SmartApp
5 * Added ability to create / modify / delete Hue Hub Groups directly from SmartApp
6 * Overhauled communications to / from Hue Hub
7 * Revised child app functions
9 * Authors: Anthony Pastor (infofiend) and Clayton (claytonjn)
14 name: "Hue Lights and Groups and Scenes (OH MY)",
15 namespace: "info_fiend",
16 author: "Anthony Pastor",
17 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. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.",
18 category: "SmartThings Labs",
19 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
20 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png",
25 page(name:"mainPage", title:"Hue Device Setup", content:"mainPage", refreshTimeout:5)
26 page(name:"bridgeDiscovery", title:"Hue Bridge Discovery", content:"bridgeDiscovery", refreshTimeout:5)
27 page(name:"bridgeBtnPush", title:"Linking with your Hue", content:"bridgeLinking", refreshTimeout:5)
28 page(name:"bulbDiscovery", title:"Bulb Discovery", content:"bulbDiscovery", refreshTimeout:5)
29 page(name:"groupDiscovery", title:"Group Discovery", content:"groupDiscovery", refreshTimeout:5)
30 page(name:"sceneDiscovery", title:"Scene Discovery", content:"sceneDiscovery", refreshTimeout:5)
31 page(name:"defaultTransition", title:"Default Transition", content:"defaultTransition", refreshTimeout:5)
32 page(name:"createScene", title:"Create A New Scene", content:"createScene")
33 page(name:"changeSceneName", title:"Change Name Of An Existing Scene", content:"changeSceneName")
34 page(name:"modifyScene", title:"Modify An Existing Scene", content:"modifyScene")
35 page(name:"removeScene", title:"Delete An Existing Scene", content:"removeScene")
36 page(name:"createGroup", title:"Create A New Group", content:"createGroup")
37 page(name:"modifyGroup", title:"Modify An Existing Group", content:"modifyGroup")
38 page(name:"removeGroup", title:"Delete An Existing Group", content:"removeGroup")
42 def bridges = bridgesDiscovered()
43 if (state.username && bridges) {
44 return bulbDiscovery()
46 return bridgeDiscovery()
50 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.subscribe) {
61 subscribe(location, null, locationHandler, [filterEvents:false])
62 state.subscribe = true
65 //bridge discovery request every 15 //25 seconds
66 if((bridgeRefreshCount % 5) == 0) {
70 //setup.xml request every 3 seconds except on discoveries
71 if(((bridgeRefreshCount % 3) == 0) && ((bridgeRefreshCount % 5) != 0)) {
75 return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) {
76 section("Please wait while we discover your Hue Bridge. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
77 input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options
84 int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
85 state.linkRefreshcount = linkRefreshcount + 1
86 def refreshInterval = 3
89 def title = "Linking with your Hue"
90 def paragraphText = "Press the button on your Hue Bridge to setup a link."
91 if (state.username) { //if discovery worked
92 nextPage = "bulbDiscovery"
93 title = "Success! - click 'Next'"
94 paragraphText = "Linking to your hub was a success! Please click 'Next'!"
97 if((linkRefreshcount % 2) == 0 && !state.username) {
101 return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
102 section("Button Press") {
103 paragraph """${paragraphText}"""
108 def bulbDiscovery() {
111 def bridge = getChildDevice(selectedHue)
112 subscribe(bridge, "bulbList", bulbListHandler)
115 int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
116 state.bulbRefreshCount = bulbRefreshCount + 1
117 def refreshInterval = 3
119 def optionsBulbs = bulbsDiscovered() ?: []
120 state.optBulbs = optionsBulbs
121 def numFoundBulbs = optionsBulbs.size() ?: 0
123 if((bulbRefreshCount % 3) == 0) {
124 log.debug "START BULB DISCOVERY"
126 log.debug "END BULB DISCOVERY"
129 return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"groupDiscovery", refreshInterval:refreshInterval, uninstall: true) {
130 section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
131 input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFoundBulbs} found)", multiple:true, options:optionsBulbs
134 def title = bridgeDni ? "Hue bridge (${bridgeHostname})" : "Find bridges"
135 href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
141 def groupDiscovery() {
144 def bridge = getChildDevice(selectedHue)
145 subscribe(bridge, "groupList", groupListHandler)
148 int groupRefreshCount = !state.groupRefreshCount ? 0 : state.groupRefreshCount as int
149 state.groupRefreshCount = groupRefreshCount + 1
151 def refreshInterval = 3
154 state.gChange = false
157 def optionsGroups = []
158 def numFoundGroups = []
159 optionsGroups = groupsDiscovered() ?: []
160 numFoundGroups = optionsGroups.size() ?: 0
162 // if (numFoundGroups == 0)
163 // app.updateSetting("selectedGroups", "")
165 if((groupRefreshCount % 3) == 0) {
166 log.debug "START GROUP DISCOVERY"
168 log.debug "END GROUP DISCOVERY"
171 return dynamicPage(name:"groupDiscovery", title:"Group Discovery Started!", nextPage:"sceneDiscovery", refreshInterval:refreshInterval, install: false, uninstall: isInitComplete) {
172 section("Please wait while we discover your Hue Groups. Discovery can take a few minutes, so sit back and relax! Select your device below once discovered.") {
173 input "selectedGroups", "enum", required:false, title:"Select Hue Groups (${numFoundGroups} found)", multiple:true, options:optionsGroups
177 def title = bridgeDni ? "Hue bridge (${bridgeHostname})" : "Find bridges"
178 href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
181 if (state.initialized) {
184 href "createGroup", title: "Create a Group", description: "Create A New Group On Hue Hub", state: selectedHue ? "complete" : "incomplete"
188 href "modifyGroup", title: "Modify a Group", description: "Modify The Lights Contained In Existing Group On Hue Hub", state: selectedHue ? "complete" : "incomplete"
192 href "removeGroup", title: "Delete a Group", description: "Delete An Existing Group From Hue Hub", state: selectedHue ? "complete" : "incomplete"
200 def createGroup(params=[:]) {
202 def theBulbs = state.bulbs
203 def creBulbsNames = []
208 theBulbName = state.bulbs[bulbID].name as String
209 creBulbsNames << [name: theBulbName, id:bulbID]
212 state.creBulbNames = creBulbsNames
213 log.trace "creBulbsNames = ${creBulbsNames}."
215 def creTheLightsID = []
218 creTheLights.each { v ->
219 creBulbsNames.each { m ->
221 creTheLightsID << m.id
227 if (creTheLightsID) {log.debug "The Selected Lights ${creTheLights} have ids of ${creTheLightsID}."}
229 if (creTheLightsID && creGroupName) {
231 def body = [name: creGroupName, lights: creTheLightsID]
233 log.debug "***************The body for createNewGroup() will be ${body}."
235 if ( creGroupConfirmed == "Yes" ) {
237 // create new group on Hue Hub
241 app.updateSetting("creGroupConfirmed", null)
242 app.updateSetting("creGroupName", null)
243 app.updateSetting("creTheLights", null)
244 app.updateSetting("creTheLightsID", null)
245 app.updateSetting("creGroup", null)
255 return dynamicPage(name:"createGroup", title:"Create Group", nextPage:"groupDiscovery", refreshInterval:3, install:false, uninstall: false) {
256 section("Choose Name for New Group") {
257 input "creGroupName", "text", title: "Group Name: (hit enter when done)", required: true, submitOnChange: true
261 input "creTheLights", "enum", title: "Choose The Lights You Want In This Scene", required: true, multiple: true, submitOnChange: true, options:creBulbsNames.name.sort()
265 paragraph "ATTENTION: Clicking Yes below will IMMEDIATELY create a new group on the Hue Hub called ${creGroupName} using the selected lights."
267 input "creGroupConfirmed", "enum", title: "Are you sure?", required: true, options: ["Yes", "No"], defaultValue: "No", submitOnChange: true
275 def modifyGroup(params=[:]) {
279 theGroups = state.groups // scenesDiscovered() ?: []
281 def modGroupOptions = []
286 theGroupName = state.groups[grID].name as String
287 modGroupOptions << [name:theGroupName, id:grID]
290 state.modGroupOptions = modGroupOptions
291 log.trace "modGroupOptions = ${modGroupOptions}."
295 state.modGroupOptions.each { m ->
296 if (modGroup == m.name) {
298 log.debug "The selected group ${modGroup} has an id of ${modGroupID}."
303 def theBulbs = state.bulbs
304 def modBulbsNames = []
309 theBulbName = state.bulbs[bulbID].name as String
310 modBulbsNames << [name: theBulbName, id:bulbID]
313 state.modBulbNames = modBulbsNames
314 log.trace "modBulbsNames = ${modBulbsNames}."
316 def modTheLightsID = []
319 modTheLights.each { v ->
320 modBulbsNames.each { m ->
321 // log.debug "m.name is ${m.name} and v is ${v}."
323 // log.debug "m.id is ${m.id}."
324 modTheLightsID << m.id // myBulbs.find{it.name == v}
330 if (modTheLightsID) {log.debug "The Selected Lights ${modTheLights} have ids of ${modTheLightsID}."}
332 if (modTheLightsID && modGroupID) {
334 def body = [groupID: modGroupID]
335 if (modGroupName) {body.name = modGroupName}
336 if (modTheLightsID) {body.lights = modTheLightsID}
338 log.debug "***************The body for updateGroup() will be ${body}."
340 if ( modGroupConfirmed == "Yes" ) {
342 // modify Group lights (and optionally name) on Hue Hub
345 // modify Group lights within ST
346 def dni = app.id + "/" + modGroupID + "g"
347 if (modTheLightsID) { sendEvent(dni, [name: "lights", value: modTheLights]) }
350 app.updateSetting("modGroupConfirmed", null)
351 app.updateSetting("modGroupName", "")
352 app.updateSetting("modTheLights", null)
353 app.updateSetting("modTheLightsID", null)
354 app.updateSetting("modGroup", null)
355 app.updateSetting("modGroupID", null)
363 return dynamicPage(name:"modifyGroup", title:"Modify Group", nextPage:"groupDiscovery", refreshInterval:3, install:false, uninstall: false) {
364 section("Choose Group to Modify") {
365 input "modGroup", "enum", title: "Modify Group:", required: true, multiple: false, submitOnChange: true, options:modGroupOptions.name.sort() {it.value}
369 input "modTheLights", "enum", title: "Choose The Lights You Want In This Group (reflected in Hue Hub and within ST device)", required: true, multiple: true, submitOnChange: true, options:modBulbsNames.name.sort()
372 input "modGroupName", "text", title: "Change Group Name (Reflected ONLY within Hue Hub - ST only allows name / label change via mobile app or IDE ). ", required: false, submitOnChange: true
374 paragraph "ATTENTION: Clicking Yes below will IMMEDIATELY set the ${modGroup} group to the selected lights."
376 input "modGroupConfirmed", "enum", title: "Are you sure?", required: true, options: ["Yes", "No"], defaultValue: "No", submitOnChange: true
387 def removeGroup(params=[:]) {
390 theGroups = state.groups
392 def remGroupOptions = []
397 theGroupName = state.groups[grID].name as String
398 remGroupOptions << [name:theGroupName, id:grID]
401 state.remGroupOptions = remGroupOptions
402 log.trace "remGroupOptions = ${remGroupOptions}."
406 state.remGroupOptions.each { m ->
407 if (remGroup == m.name) {
409 log.debug "The selected group ${remGroup} has an id of ${remGroupID}."
416 def body = [groupID: remGroupID]
418 log.debug "***************The body for deleteGroup() will be ${body}."
420 if ( remGroupConfirmed == "Yes" ) {
422 // delete group from hub
425 // try deleting group from ST (if exists)
426 def dni = app.id + "/" + remGroupID + "g"
427 log.debug "remGroup: dni = ${dni}."
429 deleteChildDevice(dni)
430 log.trace "${remGroup} found and successfully deleted from ST."
432 log.debug "${remGroup} not found within ST - no action taken."
436 app.updateSetting("remGroupConfirmed", null)
437 app.updateSetting("remGroup", null)
438 app.updateSetting("remGroupID", null)
446 return dynamicPage(name:"removeGroup", title:"Delete Group", nextPage:"groupDiscovery", refreshInterval:3, install:false, uninstall: false) {
447 section("Choose Group to DELETE") {
448 input "remGroup", "enum", title: "Delete Group:", required: true, multiple: false, submitOnChange: true, options:remGroupOptions.name.sort() {it.value}
452 paragraph "ATTENTION: Clicking Yes below will IMMEDIATELY DELETE the ${remGroup} group FOREVER!!!"
454 input "remGroupConfirmed", "enum", title: "Are you sure?", required: true, options: ["Yes", "No"], defaultValue: "No", submitOnChange: true
465 def sceneDiscovery() {
467 def isInitComplete = initComplete() == "complete"
469 state.inItemDiscovery = true
472 def bridge = getChildDevice(selectedHue)
473 subscribe(bridge, "sceneList", sceneListHandler)
478 int sceneRefreshCount = !state.sceneRefreshCount ? 0 : state.sceneRefreshCount as int
479 state.sceneRefreshCount = sceneRefreshCount + 1
481 def refreshInterval = 3
484 state.sChange = false
487 def optionsScenes = scenesDiscovered() ?: []
488 def numFoundScenes = optionsScenes.size() ?: 0
490 // if (numFoundScenes == 0)
491 // app.updateSetting("selectedScenes", "")
493 if((sceneRefreshCount % 3) == 0) {
494 log.debug "START HUE SCENE DISCOVERY"
496 log.debug "END HUE SCENE DISCOVERY"
499 return dynamicPage(name:"sceneDiscovery", title:"Scene Discovery Started!", nextPage:toDo, refreshInterval:refreshInterval, install: true, uninstall: isInitComplete) {
500 section("Please wait while we discover your Hue Scenes. Discovery can take a few minutes, so sit back and relax! Select your device below once discovered.") {
501 input "selectedScenes", "enum", required:false, title:"Select Hue Scenes (${numFoundScenes} found)", multiple:true, options:optionsScenes.sort {it.value}
504 def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
505 href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
508 if (state.initialized) {
511 href "createScene", title: "Create a Scene", description: "Create A New Scene On Hue Hub", state: selectedHue ? "complete" : "incomplete"
515 href "changeSceneName", title: "Change the Name of a Scene", description: "Change Scene Name On Hue Hub", state: selectedHue ? "complete" : "incomplete"
519 href "modifyScene", title: "Modify a Scene", description: "Modify The Lights And / Or The Light Settings For An Existing Scene On Hue Hub", state: selectedHue ? "complete" : "incomplete"
523 href "removeScene", title: "Delete a Scene", description: "Delete An Existing Scene From Hue Hub", state: selectedHue ? "complete" : "incomplete"
530 def createScene(params=[:]) {
532 def theBulbs = state.bulbs
533 def creBulbsNames = []
538 theBulbName = state.bulbs[bulbID].name as String
539 creBulbsNames << [name: theBulbName, id:bulbID]
542 state.creBulbNames = creBulbsNames
543 log.trace "creBulbsNames = ${creBulbsNames}."
545 def creTheLightsID = []
548 creTheLights.each { v ->
549 creBulbsNames.each { m ->
551 creTheLightsID << m.id // myBulbs.find{it.name == v}
557 if (creTheLightsID) {log.debug "The Selected Lights ${creTheLights} have ids of ${creTheLightsID}."}
559 if (creTheLightsID && creSceneName) {
561 def body = [name: creSceneName, lights: creTheLightsID]
563 log.debug "***************The body for createNewScene() will be ${body}."
565 if ( creSceneConfirmed == "Yes" ) {
567 // create new scene on Hue Hub
571 app.updateSetting("creSceneConfirmed", null)
572 app.updateSetting("creSceneName", "")
573 app.updateSetting("creTheLights", null)
574 app.updateSetting("creTheLightsID", null)
575 app.updateSetting("creScene", null)
583 return dynamicPage(name:"createScene", title:"Create Scene", nextPage:"sceneDiscovery", refreshInterval:3, install:false, uninstall: false) {
584 section("Choose Name for New Scene") {
585 input "creSceneName", "text", title: "New Scene Name:", required: true, submitOnChange: true
589 input "creTheLights", "enum", title: "Choose The Lights You Want In This Scene", required: true, multiple: true, submitOnChange: true, options:creBulbsNames.name.sort()
593 paragraph "ATTENTION: Clicking Yes below will IMMEDIATELY create a new scene on the Hue Hub called ${creSceneName} using the selected lights' current configuration."
595 input "creSceneConfirmed", "enum", title: "Are you sure?", required: true, options: ["Yes", "No"], defaultValue: "No", submitOnChange: true
605 def modifyScene(params=[:]) {
608 theScenes = state.scenes // scenesDiscovered() ?: []
610 def modSceneOptions = []
615 theSceneName = state.scenes[scID].name as String
616 modSceneOptions << [name:theSceneName, id:scID]
619 state.modSceneOptions = modSceneOptions
620 log.trace "modSceneOptions = ${modSceneOptions}."
624 state.modSceneOptions.each { m ->
625 if (modScene == m.name) {
627 log.debug "The selected scene ${modScene} has an id of ${modSceneID}."
632 def theBulbs = state.bulbs
633 def modBulbsNames = []
638 theBulbName = state.bulbs[bulbID].name as String
639 modBulbsNames << [name: theBulbName, id:bulbID]
642 state.modBulbNames = modBulbsNames
643 log.trace "modBulbsNames = ${modBulbsNames}."
645 def modTheLightsID = []
648 modTheLights.each { v ->
649 modBulbsNames.each { m ->
651 modTheLightsID << m.id
657 if (modTheLightsID) {log.debug "The Selected Lights ${modTheLights} have ids of ${modTheLightsID}."}
659 if (modTheLightsID && modSceneID) {
661 def body = [sceneID: modSceneID]
662 if (modSceneName) {body.name = modSceneName}
663 if (modTheLightsID) {body.lights = modTheLightsID}
665 log.debug "***************The body for updateScene() will be ${body}."
667 if ( modSceneConfirmed == "Yes" ) {
669 // update Scene lights and settings on Hue Hub
672 // update Scene lights on ST device
673 def dni = app.id + "/" + modSceneID + "s"
674 if (modTheLightsID) { sendEvent(dni, [name: "lights", value: modTheLightsID]) }
678 app.updateSetting("modSceneConfirmed", null)
679 app.updateSetting("modSceneName", "")
680 app.updateSetting("modTheLights", null)
681 app.updateSetting("modScene", null)
683 // app.updateSetting("modSceneID", null)
684 // app.updateSetting("modTheLightsID", null)
692 return dynamicPage(name:"modifyScene", title:"Modify Scene", nextPage:"sceneDiscovery", refreshInterval:3, install:false, uninstall: false) {
693 section("Choose Scene to Modify") {
694 input "modScene", "enum", title: "Modify Scene:", required: true, multiple: false, submitOnChange: true, options:modSceneOptions.name.sort() {it.value}
698 input "modTheLights", "enum", title: "Choose The Lights You Want In This Scene (reflected within Hue Hub and on ST Scene device", required: true, multiple: true, submitOnChange: true, options:modBulbsNames.name.sort()
701 input "modSceneName", "text", title: "Change Scene Name (Reflected ONLY within Hue Hub - ST only allows name / label change via mobile app or IDE ).", required: false, submitOnChange: true
703 paragraph "ATTENTION: Clicking Yes below will IMMEDIATELY set the ${modScene} scene to selected lights' current configuration."
705 input "modSceneConfirmed", "enum", title: "Are you sure?", required: true, options: ["Yes", "No"], defaultValue: "No", submitOnChange: true
713 def changeSceneName(params=[:]) {
717 theScenes = state.scenes
719 def renameSceneOptions = []
724 theSceneName = state.scenes[scID].name as String
725 renameSceneOptions << [name:theSceneName, id:scID]
728 state.renameSceneOptions = renameSceneOptions
729 log.trace "renameSceneOptions = ${renameSceneOptions}."
733 state.renameSceneOptions.each { m ->
734 if (oldSceneName == m.name) {
736 log.debug "The selected scene ${oldSceneName} has an id of ${renameSceneID}."
741 if (renameSceneID && newSceneName) {
743 def body = [sceneID: renameSceneID, name: newSceneName]
745 log.debug "***************The body for renameScene() will be ${body}."
747 if ( renameSceneConfirmed == "Yes" ) {
749 // rename scene on Hue Hub
753 app.updateSetting("renameSceneConfirmed", null)
754 app.updateSetting("newSceneName", "")
755 app.updateSetting("oldSceneName", null)
756 app.updateSetting("renameSceneID", null)
764 return dynamicPage(name:"changeSceneName", title:"Change Scene Name", nextPage:"sceneDiscovery", refreshInterval:3, install:false, uninstall: false) {
765 section("Change NAME of which Scene ") {
766 input "oldSceneName", "enum", title: "Change Scene:", required: true, multiple: false, submitOnChange: true, options:renameSceneOptions.name.sort() {it.value}
770 input "newSceneName", "text", title: "Change Scene Name to (click enter when done): ", required: false, submitOnChange: true
772 paragraph "ATTENTION: Clicking Yes below will IMMEDIATELY CHANGE the name of ${oldSceneName} to ${newSceneName}!!!"
774 input "renameSceneConfirmed", "enum", title: "Are you sure?", required: true, options: ["Yes", "No"], defaultValue: "No", submitOnChange: true
786 def removeScene(params=[:]) {
790 theScenes = state.scenes // scenesDiscovered() ?: []
792 def remSceneOptions = []
797 theSceneName = state.scenes[scID].name as String
798 remSceneOptions << [name:theSceneName, id:scID]
801 state.remSceneOptions = remSceneOptions
802 log.trace "remSceneOptions = ${remSceneOptions}."
806 state.remSceneOptions.each { m ->
807 if (remScene == m.name) {
809 log.debug "The selected scene ${remScene} has an id of ${remSceneID}."
816 def body = [sceneID: remSceneID]
818 log.debug "***************The body for deleteScene() will be ${body}."
820 if ( remSceneConfirmed == "Yes" ) {
822 // delete scene on Hue Buh
824 log.trace "${remScene} found and deleted from Hue Hub."
826 // try deleting child scene from ST
827 def dni = app.id + "/" + renameSceneID + "s"
829 deleteChildDevice(dni)
830 log.trace "${remScene} found and successfully deleted from ST."
832 log.debug "${remScene} not found within ST - no action taken."
836 app.updateSetting("remSceneConfirmed", null)
837 app.updateSetting("remScene", null)
838 app.updateSetting("remSceneID", null)
846 return dynamicPage(name:"removeScene", title:"Delete Scene", nextPage:"sceneDiscovery", refreshInterval:3, install:false, uninstall: false) {
847 section("Choose Scene to DELETE") {
848 input "remScene", "enum", title: "Delete Scene:", required: true, multiple: false, submitOnChange: true, options:remSceneOptions.name.sort() {it.value}
852 paragraph "ATTENTION: Clicking Yes below will IMMEDIATELY DELETE the ${remScene} scene FOREVER!!!"
854 input "remSceneConfirmed", "enum", title: "Are you sure?", required: true, options: ["Yes", "No"], defaultValue: "No", submitOnChange: true
865 if (state.initialized){
873 def defaultTransition()
875 int sceneRefreshCount = !state.sceneRefreshCount ? 0 : state.sceneRefreshCount as int
876 state.sceneRefreshCount = sceneRefreshCount + 1
877 def refreshInterval = 3
879 return dynamicPage(name:"defaultTransition", title:"Default Transition", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
880 section("Choose how long bulbs should take to transition between on/off and color changes. This can be modified per-device.") {
881 input "selectedTransition", "number", required:true, title:"Transition Time (seconds)", value: 1
891 private discoverBridges() {
892 sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:basic:1", physicalgraph.device.Protocol.LAN))
895 private sendDeveloperReq() {
897 sendHubCommand(new physicalgraph.device.HubAction([
901 HOST: bridgeHostnameAndPort
903 body: [devicetype: "$token-0"]], bridgeDni))
906 private discoverHueBulbs() {
907 log.trace "discoverHueBulbs REACHED"
908 def host = getBridgeIP()
909 sendHubCommand(new physicalgraph.device.HubAction([
911 path: "/api/${state.username}/lights",
913 HOST: bridgeHostnameAndPort
917 private discoverHueGroups() {
918 log.trace "discoverHueGroups REACHED"
919 def host = getBridgeIP()
920 sendHubCommand(new physicalgraph.device.HubAction([
922 path: "/api/${state.username}/groups",
924 HOST: bridgeHostnameAndPort
925 ]], "${selectedHue}"))
928 private discoverHueScenes() {
929 log.trace "discoverHueScenes REACHED"
930 def host = getBridgeIP()
931 sendHubCommand(new physicalgraph.device.HubAction([
933 path: "/api/${state.username}/scenes",
935 HOST: bridgeHostnameAndPort
936 ]], "${selectedHue}"))
939 private verifyHueBridge(String deviceNetworkId) {
940 log.trace "verifyHueBridge($deviceNetworkId)"
941 sendHubCommand(new physicalgraph.device.HubAction([
943 path: "/description.xml",
945 HOST: ipAddressFromDni(deviceNetworkId)
946 ]], "${selectedHue}"))
949 private verifyHueBridges() {
950 def devices = getHueBridges().findAll { it?.value?.verified != true }
951 log.debug "UNVERIFIED BRIDGES!: $devices"
953 verifyHueBridge((it?.value?.ip + ":" + it?.value?.port))
957 Map bridgesDiscovered() {
958 def vbridges = getVerifiedHueBridges()
961 def value = "${it.value.name}"
962 def key = it.value.ip + ":" + it.value.port
963 map["${key}"] = value
968 Map bulbsDiscovered() {
969 def bulbs = getHueBulbs()
971 if (bulbs instanceof java.util.Map) {
973 def value = "${it?.value?.name}"
974 def key = app.id +"/"+ it?.value?.id
975 map["${key}"] = value
977 } else { //backwards compatable
979 def value = "${it?.name}"
980 def key = app.id +"/"+ it?.id
981 map["${key}"] = value
987 Map groupsDiscovered() {
988 def groups = getHueGroups()
990 if (groups instanceof java.util.Map) {
992 def value = "${it?.value?.name}"
993 def key = app.id +"/"+ it?.value?.id + "g"
994 map["${key}"] = value
996 } else { //backwards compatable
998 def value = "${it?.name}"
999 def key = app.id +"/"+ it?.id + "g"
1000 map["${key}"] = value
1006 Map scenesDiscovered() {
1007 def scenes = getHueScenes()
1009 if (scenes instanceof java.util.Map) {
1011 def value = "${it?.value?.name}"
1012 def key = app.id +"/"+ it?.value?.id + "s"
1013 map["${key}"] = value
1015 } else { //backwards compatable
1017 def value = "${it?.name}"
1018 def key = app.id +"/"+ it?.id + "s"
1019 map["${key}"] = value
1027 log.debug "HUE BULBS:"
1028 state.bulbs = state.bulbs ?: [:] // discoverHueBulbs() //
1031 def getHueGroups() {
1033 log.debug "HUE GROUPS:"
1034 state.groups = state.groups ?: [:] // discoverHueGroups() //
1037 def getHueScenes() {
1039 log.debug "HUE SCENES:"
1040 state.scenes = state.scenes ?: [:] // discoverHueScenes() //
1043 def getHueBridges() {
1044 state.bridges = state.bridges ?: [:]
1047 def getVerifiedHueBridges() {
1048 getHueBridges().findAll{ it?.value?.verified == true }
1052 log.trace "Installed with settings: ${settings}"
1057 log.trace "Updated with settings: ${settings}"
1064 // remove location subscription aftwards
1065 log.debug "INITIALIZE"
1066 state.initialized = true
1067 state.subscribe = false
1068 state.bridgeSelectedOverride = false
1073 if (selectedBulbs) {
1084 /** if (selectedHue) {
1085 def bridge = getChildDevice(selectedHue)
1086 subscribe(bridge, "bulbList", bulbListHandler)
1087 subscribe(bridge, "groupList", groupListHandler)
1088 subscribe(bridge, "sceneList", sceneListHandler)
1091 runEvery5Minutes("doDeviceSync")
1095 def manualRefresh() {
1099 runEvery5Minutes("doDeviceSync")
1104 state.username = null
1107 // Handles events to add new bulbs
1108 def bulbListHandler(evt) {
1110 log.trace "Adding bulbs to state..."
1111 //state.bridgeProcessedLightList = true
1112 evt.jsonData.each { k,v ->
1114 if (v instanceof Map) {
1115 bulbs[k] = [id: k, name: v.name, type: v.type, hub:evt.value]
1119 log.info "${bulbs.size()} bulbs found"
1122 def groupListHandler(evt) {
1124 log.trace "Adding groups to state..."
1125 state.bridgeProcessedGroupList = true
1126 evt.jsonData.each { k,v ->
1128 if (v instanceof Map) {
1129 groups[k] = [id: k, name: v.name, type: v.type, lights: v.lights, hub:evt.value]
1132 state.groups = groups
1133 log.info "${groups.size()} groups found"
1136 def sceneListHandler(evt) {
1138 log.trace "Adding scenes to state..."
1139 state.bridgeProcessedSceneList = true
1140 evt.jsonData.each { k,v ->
1142 if (v instanceof Map) {
1143 scenes[k] = [id: k, name: v.name, type: "Scene", lights: v.lights, hub:evt.value]
1146 state.scenes = scenes
1147 log.info "${scenes.size()} scenes found"
1151 def bulbs = getHueBulbs()
1152 selectedBulbs.each { dni ->
1153 def d = getChildDevice(dni)
1156 if (bulbs instanceof java.util.Map) {
1157 newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
1158 if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light")) {
1159 d = addChildDevice("info_fiend", "AP Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
1160 d.initialize(newHueBulb?.value.id)
1162 d = addChildDevice("info_fiend", "AP Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
1163 d.initialize(newHueBulb?.value.id)
1167 //backwards compatable
1168 newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
1169 d = addChildDevice("info_fiend", "AP Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
1170 d.initialize(newHueBulb?.id)
1173 log.debug "created ${d.displayName} with id $dni"
1176 log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
1177 if (bulbs instanceof java.util.Map) {
1178 def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
1179 if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") && d.typeName == "Hue Bulb") {
1180 d.setDeviceType("AP Hue Lux Bulb")
1181 d.initialize(newHueBulb?.value.id)
1189 def groups = getHueGroups()
1190 selectedGroups.each { dni ->
1191 def d = getChildDevice(dni)
1195 if (groups instanceof java.util.Map)
1197 newHueGroup = groups.find { (app.id + "/" + it.value.id + "g") == dni }
1198 d = addChildDevice("info_fiend", "AP Hue Group", dni, newHueGroup?.value.hub, ["label":newHueGroup?.value.name, "groupID":newHueGroup?.value.id])
1199 d.initialize(newHueGroup?.value.id)
1202 log.debug "created ${d.displayName} with id $dni"
1207 log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
1213 def scenes = getHueScenes()
1214 selectedScenes.each { dni ->
1215 def d = getChildDevice(dni)
1219 if (scenes instanceof java.util.Map)
1221 newHueScene = scenes.find { (app.id + "/" + it.value.id + "s") == dni }
1222 d = addChildDevice("info_fiend", "AP Hue Scene", dni, newHueScene?.value.hub, ["label":newHueScene?.value.name, "sceneID":newHueScene?.value.id])
1225 log.debug "created ${d.displayName} with id $dni"
1230 log.debug "found ${d.displayName} with id $dni already exists, type: 'Scene'"
1236 def vbridges = getVerifiedHueBridges()
1237 def vbridge = vbridges.find {(it.value.ip + ":" + it.value.port) == selectedHue}
1240 def d = getChildDevice(selectedHue)
1242 d = addChildDevice("info_fiend", "AP Hue Bridge", selectedHue, vbridge.value.hub, ["data":["mac": vbridge.value.mac]]) // ["preferences":["ip": vbridge.value.ip, "port":vbridge.value.port, "path":vbridge.value.ssdpPath, "term":vbridge.value.ssdpTerm]]
1244 log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
1246 sendEvent(d.deviceNetworkId, [name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port)])
1247 sendEvent(d.deviceNetworkId, [name: "serialNumber", value: vbridge.value.serialNumber])
1251 log.debug "found ${d.displayName} with id $dni already exists"
1256 def locationHandler(evt) {
1257 log.info "LOCATION HANDLER: $evt.description"
1258 def description = evt.description
1259 def hub = evt?.hubId
1261 def parsedEvent = parseEventMessage(description)
1262 parsedEvent << ["hub":hub]
1264 if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:basic:1"))
1265 { //SSDP DISCOVERY EVENTS
1266 log.trace "SSDP DISCOVERY EVENTS"
1267 def bridges = getHueBridges()
1269 if (!(bridges."${parsedEvent.ssdpUSN.toString()}"))
1270 { //bridge does not exist
1271 log.trace "Adding bridge ${parsedEvent.ssdpUSN}"
1272 bridges << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
1275 { // update the values
1277 log.debug "Device was already found in state..."
1279 def d = bridges."${parsedEvent.ssdpUSN.toString()}"
1280 def host = parsedEvent.ip + ":" + parsedEvent.port
1281 if(d.ip != parsedEvent.ip || d.port != parsedEvent.port || host != state.hostname) {
1283 log.debug "Device's port or ip changed..."
1284 state.hostname = host
1285 d.ip = parsedEvent.ip
1286 d.port = parsedEvent.port
1287 d.name = "Philips hue ($bridgeHostname)"
1289 app.updateSetting("selectedHue", host)
1292 if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
1293 log.debug "updating dni for device ${it} with mac ${parsedEvent.mac}"
1294 it.setDeviceNetworkId((parsedEvent.ip + ":" + parsedEvent.port)) //could error if device with same dni already exists
1301 else if (parsedEvent.headers && parsedEvent.body)
1302 { // HUE BRIDGE RESPONSES
1303 log.trace "HUE BRIDGE RESPONSES"
1304 def headerString = new String(parsedEvent.headers.decodeBase64())
1305 def bodyString = new String(parsedEvent.body.decodeBase64())
1306 def type = (headerString =~ /Content-type:.*/) ? (headerString =~ /Content-type:.*/)[0] : null
1309 if (type?.contains("xml"))
1310 { // description.xml response (application/xml)
1311 body = new XmlSlurper().parseText(bodyString)
1313 if (body?.device?.modelName?.text().startsWith("Philips hue bridge"))
1315 def bridges = getHueBridges()
1316 def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
1319 bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
1323 log.error "/description.xml returned a bridge that didn't exist"
1327 else if(type?.contains("json") && isValidSource(parsedEvent.mac))
1328 { //(application/json)
1329 body = new groovy.json.JsonSlurper().parseText(bodyString)
1331 if (body?.success != null)
1332 { //POST /api response (application/json)
1333 if (body?.success?.username)
1335 state.username = body.success.username[0]
1336 state.hostname = selectedHue
1339 else if (body.error != null)
1341 //TODO: handle retries...
1342 log.error "ERROR: application/json ${body.error}"
1345 { //GET /api/${state.username}/lights response (application/json)
1348 { //check if first time poll made it here by mistake
1350 if(!body?.type?.equalsIgnoreCase("LightGroup") || !body?.type?.equalsIgnoreCase("Room"))
1352 log.debug "LIGHT GROUP!!!"
1355 def bulbs = getHueBulbs()
1356 def groups = getHueGroups()
1357 def scenes = getHueScenes()
1359 log.debug "Adding bulbs, groups, and scenes to state!"
1362 if(v.type == "LightGroup" || v.type == "Room")
1364 groups[k] = [id: k, name: v.name, type: v.type, hub:parsedEvent.hub]
1366 else if (v.type == "Extended color light" || v.type == "Color light" || v.type == "Dimmable light" )
1368 bulbs[k] = [id: k, name: v.name, type: v.type, hub:parsedEvent.hub]
1372 scenes[k] = [id: k, name: v.name, type: "Scene", hub:parsedEvent.hub]
1380 log.trace "NON-HUE EVENT $evt.description"
1384 private def parseEventMessage(Map event) {
1385 //handles bridge attribute events
1389 private def parseEventMessage(String description) {
1391 def parts = description.split(',')
1392 parts.each { part ->
1394 if (part.startsWith('devicetype:')) {
1395 def valueString = part.split(":")[1].trim()
1396 event.devicetype = valueString
1398 else if (part.startsWith('mac:')) {
1399 def valueString = part.split(":")[1].trim()
1401 event.mac = valueString
1404 else if (part.startsWith('networkAddress:')) {
1405 def valueString = part.split(":")[1].trim()
1407 event.ip = valueString
1410 else if (part.startsWith('deviceAddress:')) {
1411 def valueString = part.split(":")[1].trim()
1413 event.port = valueString
1416 else if (part.startsWith('ssdpPath:')) {
1417 def valueString = part.split(":")[1].trim()
1419 event.ssdpPath = valueString
1422 else if (part.startsWith('ssdpUSN:')) {
1424 def valueString = part.trim()
1426 event.ssdpUSN = valueString
1429 else if (part.startsWith('ssdpTerm:')) {
1431 def valueString = part.trim()
1433 event.ssdpTerm = valueString
1436 else if (part.startsWith('headers')) {
1438 def valueString = part.trim()
1440 event.headers = valueString
1443 else if (part.startsWith('body')) {
1445 def valueString = part.trim()
1447 event.body = valueString
1456 log.trace "Doing Hue Device Sync!"
1458 //shrink the large bulb lists
1459 convertBulbListToMap()
1460 convertGroupListToMap()
1461 convertSceneListToMap()
1465 subscribe(location, null, locationHandler, [filterEvents:false])
1467 log.trace "Subscription already exist"
1472 def isValidSource(macAddress) {
1473 def vbridges = getVerifiedHueBridges()
1474 return (vbridges?.find {"${it.value.mac}" == macAddress}) != null
1477 /////////////////////////////////////
1478 //CHILD DEVICE METHODS
1479 /////////////////////////////////////
1481 def parse(childDevice, description) {
1482 def parsedEvent = parseEventMessage(description)
1484 if (parsedEvent.headers && parsedEvent.body) {
1485 def headerString = new String(parsedEvent.headers.decodeBase64())
1486 def bodyString = new String(parsedEvent.body.decodeBase64())
1487 childDevice?.log "parse() - ${bodyString}"
1488 def body = new groovy.json.JsonSlurper().parseText(bodyString)
1489 // childDevice?.log "BODY - $body"
1491 if (body instanceof java.util.HashMap) { // POLL RESPONSE
1493 def devices = getChildDevices()
1496 for (bulb in body) {
1497 def d = devices.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
1499 if (bulb.value.type == "Extended color light" || bulb.value.type == "Color light" || bulb.value.type == "Dimmable light") {
1500 log.trace "Reading Poll for Lights"
1501 if (bulb.value.state.reachable) {
1502 sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
1503 sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value?.state?.bri * 100 / 255)])
1504 if (bulb.value.state.sat) {
1505 def hue = Math.min(Math.round(bulb.value?.state?.hue * 100 / 65535), 65535) as int
1506 def sat = Math.round(bulb.value?.state?.sat * 100 / 255) as int
1507 def hex = colorUtil.hslToHex(hue, sat)
1508 sendEvent(d.deviceNetworkId, [name: "color", value: hex])
1509 sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
1510 sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
1512 if (bulb.value.state.ct) {
1513 def ct = mireksToKelvin(bulb.value?.state?.ct) as int
1514 sendEvent(d.deviceNetworkId, [name: "colorTemperature", value: ct])
1516 if (bulb.value.state.effect) { sendEvent(d.deviceNetworkId, [name: "effect", value: bulb.value?.state?.effect]) }
1517 if (bulb.value.state.colormode) { sendEvent(d.deviceNetworkId, [name: "colormode", value: bulb.value?.state?.colormode]) }
1519 } else { // Bulb not reachable
1520 sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
1521 sendEvent(d.deviceNetworkId, [name: "level", value: 100])
1524 def hex = colorUtil.hslToHex(23, 56)
1525 sendEvent(d.deviceNetworkId, [name: "color", value: hex])
1526 sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
1527 sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
1529 sendEvent(d.deviceNetworkId, [name: "colorTemperature", value: ct])
1530 sendEvent(d.deviceNetworkId, [name: "effect", value: "none"])
1531 sendEvent(d.deviceNetworkId, [name: "colormode", value: hs] )
1537 // devices = getChildDevices()
1540 for (bulb in body) {
1541 def g = devices.find{it.deviceNetworkId == "${app.id}/${bulb.key}g"}
1543 if(bulb.value.type == "LightGroup" || bulb.value.type == "Room" || bulb.value.type == "Luminaire" || bulb.value.type == "Lightsource" ) {
1544 log.trace "Reading Poll for Groups"
1546 sendEvent(g.deviceNetworkId, [name: "name", value: bulb.value?.name ])
1547 sendEvent(g.deviceNetworkId, [name: "switch", value: bulb.value?.action?.on ? "on" : "off"])
1548 sendEvent(g.deviceNetworkId, [name: "level", value: Math.round(bulb.value?.action?.bri * 100 / 255)])
1549 sendEvent(g.deviceNetworkId, [name: "lights", value: bulb.value?.lights ])
1551 if (bulb.value.action.sat) {
1552 def hue = Math.min(Math.round(bulb.value?.action?.hue * 100 / 65535), 65535) as int
1553 def sat = Math.round(bulb.value?.action?.sat * 100 / 255) as int
1554 def hex = colorUtil.hslToHex(hue, sat)
1555 sendEvent(g.deviceNetworkId, [name: "color", value: hex])
1556 sendEvent(g.deviceNetworkId, [name: "hue", value: hue])
1557 sendEvent(g.deviceNetworkId, [name: "saturation", value: sat])
1560 if (bulb.value.action.ct) {
1561 def ct = mireksToKelvin(bulb.value?.action?.ct) as int
1562 sendEvent(g.deviceNetworkId, [name: "colorTemperature", value: ct])
1565 if (bulb.value.action.effect) { sendEvent(g.deviceNetworkId, [name: "effect", value: bulb.value?.action?.effect]) }
1566 if (bulb.value.action.alert) { sendEvent(g.deviceNetworkId, [name: "alert", value: bulb.value?.action?.alert]) }
1567 if (bulb.value.action.transitiontime) { sendEvent(g.deviceNetworkId, [name: "transitiontime", value: bulb.value?.action?.transitiontime ?: 0]) }
1568 if (bulb.value.action.colormode) { sendEvent(g.deviceNetworkId, [name: "colormode", value: bulb.value?.action?.colormode]) }
1574 for (bulb in body) {
1575 def sc = devices.find{it.deviceNetworkId == "${app.id}/${bulb.key}s"}
1577 if ( !bulb.value.type || bulb.value.type == "Scene" || bulb.value.recycle ) {
1578 log.trace "Reading Poll for Scene"
1579 sendEvent(sc.deviceNetworkId, [ name: "name", value: bulb.value?.name ])
1580 sendEvent(sc.deviceNetworkId, [ name: "lights", value: bulb.value?.lights ])
1585 } else { // PUT RESPONSE
1587 body.each { payload ->
1588 childDevice?.log $payload
1589 if (payload?.success) {
1591 def childDeviceNetworkId = app.id + "/"
1593 body?.success[0].each { k,v ->
1594 log.trace "********************************************************"
1595 log.trace "********************************************************"
1596 if (k.split("/")[1] == "groups") {
1597 childDeviceNetworkId += k.split("/")[2] + "g"
1598 } else if (k.split("/")[1] == "scenes") {
1599 childDeviceNetworkId += k.split("/")[2] + "s"
1601 childDeviceNetworkId += k.split("/")[2]
1604 if (!hsl[childDeviceNetworkId]) hsl[childDeviceNetworkId] = [:]
1606 eventType = k.split("/")[4]
1607 childDevice?.log "eventType: $eventType"
1610 sendEvent(childDeviceNetworkId, [name: "switch", value: (v == true) ? "on" : "off"])
1613 sendEvent(childDeviceNetworkId, [name: "level", value: Math.round(v * 100 / 255)])
1616 hsl[childDeviceNetworkId].saturation = Math.round(v * 100 / 255) as int
1619 hsl[childDeviceNetworkId].hue = Math.min(Math.round(v * 100 / 65535), 65535) as int
1622 sendEvent(childDeviceNetworkId, [name: "colorTemperature", value: mireksToKelvin(v)])
1625 sendEvent(childDeviceNetworkId, [name: "effect", value: v])
1628 sendEvent(childDeviceNetworkId, [name: "colormode", value: v])
1631 sendEvent(childDeviceNetworkId, [name: "lights", value: v])
1633 case "transitiontime":
1634 sendEvent(childDeviceNetworkId, [name: "transitiontime", value: v ]) // ?: getSelectedTransition()
1639 } else if (payload.error) {
1640 childDevice?.error "JSON error - ${body?.error}"
1645 hsl.each { childDeviceNetworkId, hueSat ->
1646 if (hueSat.hue && hueSat.saturation) {
1647 def hex = colorUtil.hslToHex(hueSat.hue, hueSat.saturation)
1648 childDevice?.log "sending ${hueSat} for ${childDeviceNetworkId} as ${hex}"
1649 sendEvent(hsl.childDeviceNetworkId, [name: "color", value: hex])
1653 } else { // SOME OTHER RESPONSE
1654 childDevice?.log "parse - got something other than headers,body..."
1660 def hubVerification(bodytext) {
1661 childDevice?.trace "Bridge sent back description.xml for verification"
1662 def body = new XmlSlurper().parseText(bodytext)
1663 if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) {
1664 def bridges = getHueBridges()
1665 def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
1667 bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
1669 childDevice?.error "/description.xml returned a bridge that didn't exist"
1673 def setTransitionTime ( childDevice, transitionTime, deviceType ) {
1674 childDevice?.log "HLGS: Executing 'setTransitionTime'"
1676 def dType = "lights"
1677 def deviceID = getId(childDevice)
1678 if(deviceType == "groups") {
1681 deviceID = deviceID - "g"
1683 def path = dType + "/" + deviceID + "/" + api
1684 childDevice?.log "HLGS: 'transitionTime' path is $path"
1686 def value = [transitiontime: transitionTime * 10]
1688 childDevice?.log "HLGS: sending ${value}."
1693 def on( childDevice, percent, transitionTime, deviceType ) {
1694 childDevice?.log "HLGS: Executing 'on'"
1696 def dType = "lights"
1697 def deviceID = getId(childDevice)
1698 if(deviceType == "groups") {
1701 deviceID = deviceID - "g"
1703 def level = Math.min(Math.round(percent * 255 / 100), 255)
1705 def path = dType + "/" + deviceID + "/" + api
1706 childDevice?.log "HLGS: 'on' path is $path"
1708 def value = [on: true, bri: level, transitiontime: transitionTime * 10 ]
1710 childDevice?.log "HLGS: sending 'on' using ${value}."
1714 def off( childDevice, transitionTime, deviceType ) {
1715 childDevice?.log "HLGS: Executing 'off'"
1717 def dType = "lights"
1718 def deviceID = getId(childDevice)
1719 if(deviceType == "groups") {
1722 deviceID = deviceID - "g"
1724 def path = dType + "/" + deviceID + "/" + api
1725 childDevice?.log "HLGS: 'off' path is ${path}."
1726 def value = [on: false, transitiontime: transitionTime * 10 ]
1727 childDevice?.log "HLGS: sending 'off' using ${value}."
1732 def setLevel( childDevice, percent, transitionTime, deviceType ) {
1734 def dType = "lights"
1735 def deviceID = getId(childDevice)
1736 if(deviceType == "groups") {
1739 deviceID = deviceID - "g"
1741 def level = Math.min(Math.round(percent * 255 / 100), 255)
1742 def value = [bri: level, on: percent > 0, transitiontime: transitionTime * 10 ]
1743 def path = dType + "/" + deviceID + "/" + api
1744 childDevice?.log "HLGS: 'on' path is $path"
1745 childDevice?.log "HLGS: Executing 'setLevel($percent).'"
1749 def setSaturation(childDevice, percent, transitionTime, deviceType) {
1751 def dType = "lights"
1752 def deviceID = getId(childDevice)
1754 if(deviceType == "groups") {
1757 deviceID = deviceID - "g"
1759 def path = dType + "/" + deviceID + "/" + api
1761 def level = Math.min(Math.round(percent * 255 / 100), 255)
1763 childDevice?.log "HLGS: Executing 'setSaturation($percent).'"
1764 put( path, [sat: level, transitiontime: transitionTime * 10 ])
1767 def setHue(childDevice, percent, transitionTime, deviceType ) {
1769 def dType = "lights"
1770 def deviceID = getId(childDevice)
1771 if(deviceType == "groups") {
1774 deviceID = deviceID - "g"
1777 def path = dType + "/" + deviceID + "/" + api
1779 childDevice?.log "HLGS: Executing 'setHue($percent)'"
1780 def level = Math.min(Math.round(percent * 65535 / 100), 65535)
1781 put( path, [hue: level, transitiontime: transitionTime * 10 ])
1784 def setColorTemperature(childDevice, huesettings, transitionTime, deviceType ) {
1786 def dType = "lights"
1787 def deviceID = getId(childDevice)
1788 if(deviceType == "groups") {
1791 deviceID = deviceID - "g"
1794 def path = dType + "/" + deviceID + "/" + api
1796 childDevice?.log "HLGS: Executing 'setColorTemperature($huesettings)'"
1797 def value = [on: true, ct: kelvinToMireks(huesettings), transitiontime: transitionTime * 10 ]
1802 def setColor(childDevice, huesettings, transitionTime, deviceType ) {
1804 def dType = "lights"
1805 def deviceID = getId(childDevice)
1806 if(deviceType == "groups") {
1809 deviceID = deviceID - "g"
1811 def path = dType + "/" + deviceID + "/" + api
1818 if (huesettings.hex != null) {
1819 value.xy = getHextoXY(huesettings.hex)
1821 if (huesettings.hue != null)
1822 value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
1823 if (huesettings.saturation != null)
1824 value.sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
1827 if (huesettings.level > 0 || huesettings.switch == "on") {
1829 } else if (huesettings.switch == "off") {
1832 value.transitiontime = transitionTime * 10
1833 value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
1834 value.alert = huesettings.alert ? huesettings.alert : "none"
1836 childDevice?.log "HLGS: Executing 'setColor($value).'"
1839 // return "Color set to $value"
1842 def setGroupScene(childDevice, Number inGroupID) {
1843 childDevice?.log "HLGS: Executing setGroupScene with inGroupID of ${inGroupID}."
1844 def sceneID = getId(childDevice) // - "s"
1845 def groupID = inGroupID ?: "0"
1846 childDevice?.log "HLGS: setGroupScene scene is ${sceneID} "
1847 String path = "groups/${groupID}/action/"
1849 childDevice?.log "Path = ${path} "
1851 put( path, [scene: sceneID])
1854 def setToGroup(childDevice, Number inGroupID ) {
1855 childDevice?.log "HLGS: setToGroup with inGroupID of ${inGroupID}."
1856 def sceneID = getId(childDevice) - "s"
1857 def groupID = inGroupID ?: "0"
1859 childDevice?.log "HLGS: setToGroup: sceneID = ${sceneID} "
1860 String gPath = "groups/${groupID}/action/"
1862 childDevice?.log "Group path = ${gPath} "
1864 put( gPath, [scene: sceneID])
1868 def nextLevel(childDevice) {
1869 def level = device.latestValue("level") as Integer ?: 0
1871 level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
1872 } else { level = 25 }
1873 setLevel(childDevice, level)
1877 def getId(childDevice) {
1878 childDevice?.log "HLGS: Executing getId"
1879 if (childDevice.device?.deviceNetworkId?.startsWith("Hue") || childDevice.device?.deviceNetworkId?.startsWith("AP Hue") ) {
1880 childDevice?.log "Device ID returned is ${childDevice.device?.deviceNetworkId[3..-1]}."
1881 return childDevice.device?.deviceNetworkId[3..-1]
1883 childDevice?.log "Device ID returned (based on SPLIT '/') is ${childDevice.device?.deviceNetworkId.split("/")[-1]}."
1884 return childDevice.device?.deviceNetworkId.split("/")[-1]
1889 def updateSceneFromDevice(childDevice) {
1890 childDevice?.log "HLGS: updateSceneFromDevice: Scene ${childDevice} requests scene use current light states."
1891 def sceneID = getId(childDevice) - "s"
1893 childDevice?.log "HLGS: updateScene: sceneID = ${sceneID} "
1894 String path = "scenes/${sceneID}/"
1896 def value = [storelightstate: true]
1897 childDevice?.log "Path = ${path} "
1903 def updateScene(body) {
1904 log.trace "HLGS: updateScene "
1905 def sceneID = body.sceneID
1907 String path = "scenes/${sceneID}/"
1908 def value = [lights: body.lights, storelightstate: true]
1911 value.name = body.name
1914 log.trace "HLGS: updateScene: Path = ${path} & body = ${value}"
1918 app.updateSetting("modSceneConfirmed", null)
1919 app.updateSetting("modSceneName", "")
1920 app.updateSetting("modTheLights", [])
1921 app.updateSetting("modScene", null)
1922 state.updateScene == null
1923 // state.scenes = []
1927 def renameScene(body) {
1928 log.trace "HLGS: renameScene "
1929 def sceneID = body.sceneID
1931 String path = "scenes/${sceneID}/"
1933 def value = body.name
1936 log.trace "HLGS: renameScene: Path = ${path} & body = ${value}"
1940 app.updateSetting("renameSceneConfirmed", null)
1941 app.updateSetting("renameSceneName", "")
1942 app.updateSetting("renameScene", null)
1943 state.renameScene == null
1945 // state.scenes = []
1950 def deleteScene(body) {
1951 log.trace "HLGS: deleteScene "
1952 def host = getBridgeIP()
1955 def sceneID = body.sceneID
1956 String path = "scenes/${sceneID}/"
1957 def uri = "/api/${state.username}/$path"
1959 log.trace "HLGS: deleteScene: uri = $uri"
1962 sendHubCommand(new physicalgraph.device.HubAction([
1968 ],"${selectedHue}"))
1970 app.updateSetting("remSceneConfirmed", null)
1971 app.updateSetting("remScene", null)
1972 state.deleteScene == null
1973 // state.scenes = []
1977 def createNewScene(body) {
1978 log.trace "HLGS: createNewScene "
1979 def host = getBridgeIP()
1981 String path = "scenes/"
1982 def uri = "/api/${state.username}/$path"
1984 def newBody = [name: body.name, lights: body.lights]
1986 def bodyJSON = new groovy.json.JsonBuilder(newBody).toString()
1988 log.trace "HLGS: createNewScene: POST: $uri"
1989 log.trace "HLGS: createNewScene: BODY: $bodyJSON"
1992 sendHubCommand(new physicalgraph.device.HubAction([
1997 ], body: bodyJSON],"${selectedHue}"))
1999 app.updateSetting("creSceneConfirmed", null)
2000 app.updateSetting("creSceneName", "")
2001 app.updateSetting("creTheLights", [])
2002 app.updateSetting("creScene", null)
2003 state.createScene == null
2004 // state.scenes = []
2008 def updateGroup(body) {
2009 log.trace "HLGS: updateGroup "
2010 def groupID = body.groupID
2012 String path = "groups/${groupID}/"
2013 def value = [lights: body.lights]
2016 value.name = body.name
2019 log.trace "HLGS: updateGroup: Path = ${path} & body = ${value}"
2023 app.updateSetting("modGroupConfirmed", null)
2024 app.updateSetting("modGroupName", "")
2025 app.updateSetting("modTheLights", [])
2026 app.updateSetting("modGroup", null)
2027 state.updateGroup == null
2028 // state.groups = []
2031 def renameGroup(body) {
2032 log.trace "HLGS: renameGroup "
2033 def groupID = body.groupID
2035 String path = "groups/${groupID}/"
2036 def value = body.name
2039 log.trace "HLGS: renameGroup: Path = ${path} & body = ${value}"
2043 app.updateSetting("renameGroupConfirmed", null)
2044 app.updateSetting("renameGroupName", "")
2045 app.updateSetting("renameGroup", null)
2046 state.renameGroup == null
2048 // state.groups = []
2052 def deleteGroup(body) {
2053 log.trace "HLGS: deleteGroup "
2054 def host = getBridgeIP()
2057 def groupID = body.groupID
2058 String path = "groups/${groupID}/"
2059 def uri = "/api/${state.username}/$path"
2061 log.trace "HLGS: deleteGroup: uri = $uri"
2064 sendHubCommand(new physicalgraph.device.HubAction([
2070 ],"${selectedHue}"))
2072 app.updateSetting("remGroupConfirmed", null)
2073 app.updateSetting("remGroup", null)
2074 state.deleteGroup == null
2075 // state.groups = []
2078 def createNewGroup(body) {
2079 log.trace "HLGS: createNewGroup "
2080 def host = getBridgeIP()
2082 String path = "groups/"
2083 def uri = "/api/${state.username}/$path"
2085 body.type = "LightGroup"
2086 def bodyJSON = new groovy.json.JsonBuilder(body).toString()
2088 log.trace "HLGS: createNewGroup: POST: $uri"
2089 log.trace "HLGS: createNewGroup: BODY: $bodyJSON"
2092 sendHubCommand(new physicalgraph.device.HubAction([
2097 ], body: bodyJSON],"${selectedHue}"))
2099 app.updateSetting("creGroupConfirmed", null)
2100 app.updateSetting("creGroupName", "")
2101 app.updateSetting("creTheLights", [])
2102 app.updateSetting("creGroup", null)
2103 state.createGroup == null
2104 // state.groups = []
2110 def host = getBridgeIP()
2112 def uris = ["/api/${state.username}/lights/", "/api/${state.username}/groups/", "/api/${state.username}/scenes/"]
2115 sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
2117 """, physicalgraph.device.Protocol.LAN, selectedHue))
2119 log.warn "Parsing Body failed - trying again..."
2127 private put(path, body) {
2128 childDevice?.log "HLGS: put: path = ${path}."
2129 def host = getBridgeIP()
2130 def uri = "/api/${state.username}/$path" // "lights"
2132 if ( path.startsWith("groups") || path.startsWith("scenes")) {
2133 uri = "/api/${state.username}/$path"[0..-1]
2136 // if (path.startsWith("scenes")) {
2137 // uri = "/api/${state.username}/$path"[0..-1]
2140 def bodyJSON = new groovy.json.JsonBuilder(body).toString()
2142 childDevice?.log "PUT: $uri"
2143 childDevice?.log "BODY: $bodyJSON" // ${body} ?
2146 sendHubCommand(new physicalgraph.device.HubAction([
2152 body: bodyJSON], "${selectedHue}"))
2156 def getBridgeDni() {
2160 def getBridgeHostname() {
2161 def dni = state.hostname
2163 def segs = dni.split(":")
2164 convertHexToIP(segs[0])
2170 def getBridgeHostnameAndPort() {
2172 def dni = state.hostname
2174 def segs = dni.split(":")
2175 result = convertHexToIP(segs[0]) + ":" + convertHexToInt(segs[1])
2177 log.trace "result = $result"
2181 private getBridgeIP() {
2184 def d = getChildDevice(selectedHue)
2186 if (d.getDeviceDataByName("networkAddress"))
2187 host = d.getDeviceDataByName("networkAddress")
2189 host = d.latestState('networkAddress').stringValue
2191 if (host == null || host == "") {
2192 def serialNumber = selectedHue
2193 def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
2195 bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
2197 if (bridge?.ip && bridge?.port) {
2198 if (bridge?.ip.contains("."))
2199 host = "${bridge?.ip}:${bridge?.port}"
2201 host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
2202 } else if (bridge?.networkAddress && bridge?.deviceAddress)
2203 host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
2205 log.trace "Bridge: $selectedHue - Host: $host"
2210 private getHextoXY(String colorStr) {
2211 // For the hue bulb the corners of the triangle are:
2212 // -Red: 0.675, 0.322
2213 // -Green: 0.4091, 0.518
2214 // -Blue: 0.167, 0.04
2216 def cred = Integer.valueOf( colorStr.substring( 1, 3 ), 16 )
2217 def cgreen = Integer.valueOf( colorStr.substring( 3, 5 ), 16 )
2218 def cblue = Integer.valueOf( colorStr.substring( 5, 7 ), 16 )
2220 double[] normalizedToOne = new double[3];
2221 normalizedToOne[0] = (cred / 255);
2222 normalizedToOne[1] = (cgreen / 255);
2223 normalizedToOne[2] = (cblue / 255);
2224 float red, green, blue;
2226 // Make red more vivid
2227 if (normalizedToOne[0] > 0.04045) {
2228 red = (float) Math.pow(
2229 (normalizedToOne[0] + 0.055) / (1.0 + 0.055), 2.4);
2231 red = (float) (normalizedToOne[0] / 12.92);
2234 // Make green more vivid
2235 if (normalizedToOne[1] > 0.04045) {
2236 green = (float) Math.pow((normalizedToOne[1] + 0.055) / (1.0 + 0.055), 2.4);
2238 green = (float) (normalizedToOne[1] / 12.92);
2241 // Make blue more vivid
2242 if (normalizedToOne[2] > 0.04045) {
2243 blue = (float) Math.pow((normalizedToOne[2] + 0.055) / (1.0 + 0.055), 2.4);
2245 blue = (float) (normalizedToOne[2] / 12.92);
2248 float X = (float) (red * 0.649926 + green * 0.103455 + blue * 0.197109);
2249 float Y = (float) (red * 0.234327 + green * 0.743075 + blue * 0.022598);
2250 float Z = (float) (red * 0.0000000 + green * 0.053077 + blue * 1.035763);
2252 float x = (X != 0 ? X / (X + Y + Z) : 0);
2253 float y = (Y != 0 ? Y / (X + Y + Z) : 0);
2255 double[] xy = new double[2];
2261 private Integer convertHexToInt(hex) {
2262 Integer.parseInt(hex,16)
2265 def convertBulbListToMap() {
2266 log.debug "CONVERT BULB LIST"
2268 if (state.bulbs instanceof java.util.List) {
2270 state.bulbs.unique {it.id}.each { bulb ->
2271 map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "hub":bulb.hub]]
2275 } catch(Exception e) {
2276 log.error "Caught error attempting to convert bulb list to map: $e"
2280 def convertGroupListToMap() {
2281 log.debug "CONVERT GROUP LIST"
2283 if (state.groups instanceof java.util.List) {
2285 state.groups.unique {it.id}.each { group ->
2286 map << ["${group.id}g":["id":group.id+"g", "name":group.name, "type": group.type, "lights": group.lights, "hub":group.hub]]
2291 catch(Exception e) {
2292 log.error "Caught error attempting to convert group list to map: $e"
2296 def convertSceneListToMap() {
2297 log.debug "CONVERT SCENE LIST"
2299 if (state.scenes instanceof java.util.List) {
2301 state.scenes.unique {it.id}.each { scene ->
2302 map << ["${scene.id}s":["id":scene.id+"s", "name":scene.name, "type": group.type, "lights": group.lights, "hub":scene.hub]]
2307 catch(Exception e) {
2308 log.error "Caught error attempting to convert scene list to map: $e"
2312 private String convertHexToIP(hex) {
2313 [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
2316 private Boolean hasAllHubsOver(String desiredFirmware)
2318 return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
2321 private List getRealHubFirmwareVersions()
2323 return location.hubs*.firmwareVersionString.findAll { it }
2326 def ipAddressFromDni(dni) {
2328 def segs = dni.split(":")
2329 convertHexToIP(segs[0]) + ":" + convertHexToInt(segs[1])
2335 def getSelectedTransition() {
2336 return settings.selectedTransition
2339 def setAlert(childDevice, effect, transitionTime, deviceType ) {
2341 def dType = "lights"
2342 def deviceID = getId(childDevice)
2343 if(deviceType == "groups") {
2346 deviceID = deviceID - "g"
2348 def path = dType + "/" + deviceID + "/" + api
2351 if(effect != "none" && effect != "select" && effect != "lselect") { childDevice?.log "Invalid alert value!" }
2353 def value = [alert: effect, transitiontime: transitionTime * 10 ]
2354 childDevice?.log "setAlert: Alert ${effect}."
2359 def setEffect(childDevice, effect, transitionTime, deviceType) {
2361 def dType = "lights"
2362 def deviceID = getId(childDevice)
2363 if(deviceType == "groups") {
2366 deviceID = deviceID - "g"
2368 def path = dType + "/" + deviceID + "/" + api
2371 def value = [effect: effect, transitiontime: transitionTime * 10 ]
2372 childDevice?.log "setEffect: Effect ${effect}."
2377 int kelvinToMireks(kelvin) {
2378 return 1000000 / kelvin //https://en.wikipedia.org/wiki/Mired
2381 int mireksToKelvin(mireks) {
2382 return 1000000 / mireks //https://en.wikipedia.org/wiki/Mired