PageRenderTime 55ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/smartapps/smartthings/hue-connect.groovy

https://github.com/rappleg/SmartThings
Groovy | 671 lines | 551 code | 84 blank | 36 comment | 99 complexity | ca927ae06c790154c0e8989e6854d9cf MD5 | raw file
Possible License(s): Apache-2.0
  1. /**
  2. * Hue Service Manager
  3. *
  4. * Author: SmartThings
  5. */
  6. preferences {
  7. page(name:"bridgeDiscovery", title:"Hue Device Setup", content:"bridgeDiscovery", refreshTimeout:5)
  8. page(name:"bridgeBtnPush", title:"Linking with your Hue", content:"bridgeLinking", refreshTimeout:5)
  9. page(name:"bulbDiscovery", title:"Hue Device Setup", content:"bulbDiscovery", refreshTimeout:5)
  10. }
  11. //PAGES
  12. /////////////////////////////////////
  13. def bridgeDiscovery()
  14. {
  15. if(canInstallLabs())
  16. {
  17. int bridgeRefreshCount = !state.bridgeRefreshCount ? 0 : state.bridgeRefreshCount as int
  18. state.bridgeRefreshCount = bridgeRefreshCount + 1
  19. def refreshInterval = 3
  20. def options = bridgesDiscovered() ?: []
  21. def numFound = options.size() ?: 0
  22. if(!state.subscribe) {
  23. subscribe(location, null, locationHandler, [filterEvents:false])
  24. state.subscribe = true
  25. }
  26. //bridge discovery request every 15 //25 seconds
  27. if((bridgeRefreshCount % 5) == 0) {
  28. discoverBridges()
  29. }
  30. //setup.xml request every 3 seconds except on discoveries
  31. if(((bridgeRefreshCount % 1) == 0) && ((bridgeRefreshCount % 5) != 0)) {
  32. verifyHueBridges()
  33. }
  34. return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) {
  35. 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.") {
  36. input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options
  37. }
  38. }
  39. }
  40. else
  41. {
  42. def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
  43. To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
  44. return dynamicPage(name:"bridgeDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
  45. section("Upgrade") {
  46. paragraph "$upgradeNeeded"
  47. }
  48. }
  49. }
  50. }
  51. /////////////////////////////////////
  52. def bridgeLinking()
  53. {
  54. int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
  55. state.linkRefreshcount = linkRefreshcount + 1
  56. def refreshInterval = 3
  57. def nextPage = ""
  58. def title = "Linking with your Hue"
  59. def paragraphText = "Press the button on your Hue Bridge to setup a link."
  60. if (state.username) { //if discovery worked
  61. nextPage = "bulbDiscovery"
  62. title = "Success! - click 'Next'"
  63. paragraphText = "Linking to your hub was a success! Please click 'Next'!"
  64. }
  65. if((linkRefreshcount % 2) == 0 && !state.username) {
  66. sendDeveloperReq()
  67. }
  68. return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
  69. section("Button Press") {
  70. paragraph """${paragraphText}"""
  71. }
  72. }
  73. }
  74. /////////////////////////////////////
  75. def bulbDiscovery()
  76. {
  77. int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
  78. state.bulbRefreshCount = bulbRefreshCount + 1
  79. def refreshInterval = 3
  80. def options = bulbsDiscovered() ?: []
  81. def numFound = options.size() ?: 0
  82. if((bulbRefreshCount % 3) == 0) {
  83. discoverHueBulbs()
  84. }
  85. return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true) {
  86. 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.") {
  87. input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options
  88. }
  89. }
  90. }
  91. //END PAGES
  92. /////////////////////////////////////
  93. private discoverBridges()
  94. {
  95. sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:basic:1", physicalgraph.device.Protocol.LAN))
  96. }
  97. private sendDeveloperReq()
  98. {
  99. def token = app.id
  100. def body = """{"devicetype":"$token-0","username":"$token-0"}"""
  101. def length = body.getBytes().size().toString()
  102. sendHubCommand(new physicalgraph.device.HubAction("""POST /api HTTP/1.1
  103. HOST: ${selectedHue}
  104. Content-Length: ${length}
  105. ${body}
  106. """, physicalgraph.device.Protocol.LAN, "${selectedHue}"))
  107. }
  108. private discoverHueBulbs()
  109. {
  110. sendHubCommand(new physicalgraph.device.HubAction("""GET /api/${state.username}/lights HTTP/1.1
  111. HOST: ${selectedHue}
  112. """, physicalgraph.device.Protocol.LAN, "${selectedHue}"))
  113. }
  114. private verifyHueBridge(String deviceNetworkId) {
  115. sendHubCommand(new physicalgraph.device.HubAction("""GET /description.xml HTTP/1.1
  116. HOST: ${deviceNetworkId}
  117. """, physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
  118. }
  119. private verifyHueBridges() {
  120. def devices = getHueBridges().findAll { it?.value?.verified != true }
  121. log.debug "UNVERIFIED BRIDGES!: $devices"
  122. devices.each {
  123. verifyHueBridge((it?.value?.ip + ":" + it?.value?.port))
  124. }
  125. }
  126. /////////////////////////////////////
  127. Map bridgesDiscovered() {
  128. def vbridges = getVerifiedHueBridges()
  129. def map = [:]
  130. vbridges.each {
  131. def value = "${it.value.name}"
  132. def key = it.value.ip + ":" + it.value.port
  133. map["${key}"] = value
  134. }
  135. map
  136. }
  137. /////////////////////////////////////
  138. Map bulbsDiscovered() {
  139. def bulbs = getHueBulbs()
  140. def map = [:]
  141. if (bulbs instanceof java.util.Map) {
  142. bulbs.each {
  143. def value = "${it?.value?.name}"
  144. def key = app.id +"/"+ it?.value?.id
  145. map["${key}"] = value
  146. }
  147. } else { //backwards compatable
  148. bulbs.each {
  149. def value = "${it?.name}"
  150. def key = app.id +"/"+ it?.id
  151. map["${key}"] = value
  152. }
  153. }
  154. map
  155. }
  156. /////////////////////////////////////
  157. def getHueBulbs()
  158. {
  159. state.bulbs = state.bulbs ?: [:]
  160. }
  161. /////////////////////////////////////
  162. def getHueBridges()
  163. {
  164. state.bridges = state.bridges ?: [:]
  165. }
  166. /////////////////////////////////////
  167. def getVerifiedHueBridges()
  168. {
  169. getHueBridges().findAll{ it?.value?.verified == true }
  170. }
  171. /////////////////////////////////////
  172. def installed() {
  173. //log.debug "Installed with settings: ${settings}"
  174. initialize()
  175. runIn(300, "doDeviceSync" , [overwrite: false]) //setup ip:port syncing every 5 minutes
  176. }
  177. /////////////////////////////////////
  178. def updated() {
  179. //log.debug "Updated with settings: ${settings}"
  180. unsubscribe()
  181. initialize()
  182. }
  183. /////////////////////////////////////
  184. def initialize() {
  185. // remove location subscription aftwards
  186. unsubscribe()
  187. state.subscribe = false
  188. if (selectedHue) {
  189. addBridge()
  190. }
  191. if (selectedBulbs) {
  192. addBulbs()
  193. }
  194. }
  195. /////////////////////////////////////
  196. def addBulbs() {
  197. def bulbs = getHueBulbs()
  198. selectedBulbs.each { dni ->
  199. def d = getChildDevice(dni)
  200. if(!d) {
  201. def newHueBulb
  202. if (bulbs instanceof java.util.Map) {
  203. newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
  204. d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
  205. } else { //backwards compatable
  206. newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
  207. d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
  208. }
  209. log.debug "created ${d.displayName} with id $dni"
  210. d.refresh()
  211. } else {
  212. log.debug "found ${d.displayName} with id $dni already exists"
  213. }
  214. }
  215. }
  216. def addBridge() {
  217. def vbridges = getVerifiedHueBridges()
  218. def vbridge = vbridges.find {(it.value.ip + ":" + it.value.port) == selectedHue}
  219. if(vbridge) {
  220. def d = getChildDevice(selectedHue)
  221. if(!d) {
  222. d = addChildDevice("smartthings", "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]]
  223. log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
  224. sendEvent(d.deviceNetworkId, [name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port)])
  225. sendEvent(d.deviceNetworkId, [name: "serialNumber", value: vbridge.value.serialNumber])
  226. }
  227. else
  228. {
  229. log.debug "found ${d.displayName} with id $dni already exists"
  230. }
  231. }
  232. }
  233. /////////////////////////////////////
  234. def locationHandler(evt) {
  235. def description = evt.description
  236. def hub = evt?.hubId
  237. def parsedEvent = parseEventMessage(description)
  238. parsedEvent << ["hub":hub]
  239. if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:basic:1"))
  240. { //SSDP DISCOVERY EVENTS
  241. def bridges = getHueBridges()
  242. if (!(bridges."${parsedEvent.ssdpUSN.toString()}"))
  243. { //bridge does not exist
  244. bridges << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
  245. }
  246. else
  247. { // update the values
  248. log.debug "Device was already found in state..."
  249. def d = bridges."${parsedEvent.ssdpUSN.toString()}"
  250. boolean deviceChangedValues = false
  251. if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
  252. d.ip = parsedEvent.ip
  253. d.port = parsedEvent.port
  254. deviceChangedValues = true
  255. log.debug "Device's port or ip changed..."
  256. }
  257. if (deviceChangedValues) {
  258. def children = getChildDevices()
  259. log.debug "Found children ${children}"
  260. children.each {
  261. if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
  262. log.debug "updating dni for device ${it} with mac ${parsedEvent.mac}"
  263. it.setDeviceNetworkId((parsedEvent.ip + ":" + parsedEvent.port)) //could error if device with same dni already exists
  264. }
  265. }
  266. }
  267. }
  268. }
  269. else if (parsedEvent.headers && parsedEvent.body)
  270. { // HUE BRIDGE RESPONSES
  271. def headerString = new String(parsedEvent.headers.decodeBase64())
  272. def bodyString = new String(parsedEvent.body.decodeBase64())
  273. def type = (headerString =~ /Content-type:.*/) ? (headerString =~ /Content-type:.*/)[0] : null
  274. def body
  275. if (type?.contains("xml"))
  276. { // description.xml response (application/xml)
  277. body = new XmlSlurper().parseText(bodyString)
  278. if (body?.device?.modelName?.text().startsWith("Philips hue bridge"))
  279. {
  280. def bridges = getHueBridges()
  281. def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
  282. if (bridge)
  283. {
  284. bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
  285. }
  286. else
  287. {
  288. log.error "/description.xml returned a bridge that didn't exist"
  289. }
  290. }
  291. }
  292. else if(type?.contains("json"))
  293. { //(application/json)
  294. body = new groovy.json.JsonSlurper().parseText(bodyString)
  295. if (body?.success != null)
  296. { //POST /api response (application/json)
  297. if (body?.success?.username)
  298. {
  299. state.username = body.success.username[0]
  300. state.hostname = selectedHue
  301. }
  302. }
  303. else if (body.error != null)
  304. {
  305. //TODO: handle retries...
  306. log.error "ERROR: application/json ${body.error}"
  307. }
  308. else
  309. { //GET /api/${state.username}/lights response (application/json)
  310. if (!body?.state?.on) { //check if first time poll made it here by mistake
  311. def bulbs = getHueBulbs()
  312. log.debug "Adding bulbs to state!"
  313. body.each { k,v ->
  314. bulbs[k] = [id: k, name: v.name, hub:parsedEvent.hub]
  315. }
  316. }
  317. }
  318. }
  319. }
  320. else {
  321. log.debug "GOT EVENT --- ${evt} --- NOT A HUE"
  322. }
  323. }
  324. /////////////////////////////////////
  325. private def parseEventMessage(Map event) {
  326. //handles bridge attribute events
  327. return event
  328. }
  329. private def parseEventMessage(String description) {
  330. def event = [:]
  331. def parts = description.split(',')
  332. parts.each { part ->
  333. part = part.trim()
  334. if (part.startsWith('devicetype:')) {
  335. def valueString = part.split(":")[1].trim()
  336. event.devicetype = valueString
  337. }
  338. else if (part.startsWith('mac:')) {
  339. def valueString = part.split(":")[1].trim()
  340. if (valueString) {
  341. event.mac = valueString
  342. }
  343. }
  344. else if (part.startsWith('networkAddress:')) {
  345. def valueString = part.split(":")[1].trim()
  346. if (valueString) {
  347. event.ip = valueString
  348. }
  349. }
  350. else if (part.startsWith('deviceAddress:')) {
  351. def valueString = part.split(":")[1].trim()
  352. if (valueString) {
  353. event.port = valueString
  354. }
  355. }
  356. else if (part.startsWith('ssdpPath:')) {
  357. def valueString = part.split(":")[1].trim()
  358. if (valueString) {
  359. event.ssdpPath = valueString
  360. }
  361. }
  362. else if (part.startsWith('ssdpUSN:')) {
  363. part -= "ssdpUSN:"
  364. def valueString = part.trim()
  365. if (valueString) {
  366. event.ssdpUSN = valueString
  367. }
  368. }
  369. else if (part.startsWith('ssdpTerm:')) {
  370. part -= "ssdpTerm:"
  371. def valueString = part.trim()
  372. if (valueString) {
  373. event.ssdpTerm = valueString
  374. }
  375. }
  376. else if (part.startsWith('headers')) {
  377. part -= "headers:"
  378. def valueString = part.trim()
  379. if (valueString) {
  380. event.headers = valueString
  381. }
  382. }
  383. else if (part.startsWith('body')) {
  384. part -= "body:"
  385. def valueString = part.trim()
  386. if (valueString) {
  387. event.body = valueString
  388. }
  389. }
  390. }
  391. event
  392. }
  393. /////////////////////////////////////
  394. def doDeviceSync(){
  395. log.debug "Doing Hue Device Sync!"
  396. runIn(300, "doDeviceSync" , [overwrite: false]) //schedule to run again in 5 minutes
  397. //shrink the large bulb lists
  398. convertBulbListToMap()
  399. if(!state.subscribe) {
  400. subscribe(location, null, locationHandler, [filterEvents:false])
  401. state.subscribe = true
  402. }
  403. discoverBridges()
  404. }
  405. ////////////////////////////////////////////
  406. //CHILD DEVICE METHODS
  407. /////////////////////////////////////
  408. def parse(childDevice, description) {
  409. def parsedEvent = parseEventMessage(description)
  410. if (parsedEvent.headers && parsedEvent.body) {
  411. def headerString = new String(parsedEvent.headers.decodeBase64())
  412. def bodyString = new String(parsedEvent.body.decodeBase64())
  413. log.debug "parse() - ${bodyString}"
  414. def body = new groovy.json.JsonSlurper().parseText(bodyString)
  415. if (body instanceof java.util.HashMap)
  416. { //poll response
  417. def bulbs = getChildDevices()
  418. def d = bulbs.find{it.label == body.name}
  419. if (d) {
  420. sendEvent(d.deviceNetworkId, [name: "switch", value: body?.state?.on ? "on" : "off"])
  421. sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(body.state.bri * 100 / 255)])
  422. sendEvent(d.deviceNetworkId, [name: "saturation", value: Math.round(body.state.sat * 100 / 255)])
  423. sendEvent(d.deviceNetworkId, [name: "hue", value: Math.min(Math.round(body.state.hue * 100 / 65535), 65535)])
  424. }
  425. }
  426. else
  427. { //put response
  428. body.each { payload ->
  429. log.debug $payload
  430. if (payload?.success)
  431. {
  432. def childDeviceNetworkId = app.id + "/"
  433. def eventType
  434. body?.success[0].each { k,v ->
  435. childDeviceNetworkId += k.split("/")[2]
  436. eventType = k.split("/")[4]
  437. log.debug "eventType: $eventType"
  438. switch(eventType) {
  439. case "on":
  440. sendEvent(childDeviceNetworkId, [name: "switch", value: (v == true) ? "on" : "off"])
  441. break
  442. case "bri":
  443. sendEvent(childDeviceNetworkId, [name: "level", value: Math.round(v * 100 / 255)])
  444. break
  445. case "sat":
  446. sendEvent(childDeviceNetworkId, [name: "saturation", value: Math.round(v * 100 / 255)])
  447. break
  448. case "hue":
  449. sendEvent(childDeviceNetworkId, [name: "hue", value: Math.min(Math.round(v * 100 / 65535), 65535)])
  450. break
  451. }
  452. }
  453. }
  454. else if (payload.error)
  455. {
  456. log.debug "JSON error - ${body?.error}"
  457. }
  458. }
  459. }
  460. } else {
  461. log.debug "parse - got something other than headers,body..."
  462. return []
  463. }
  464. }
  465. /////////////////////////////////////
  466. def on(childDevice) {
  467. log.debug "Executing 'on'"
  468. put("lights/${getId(childDevice)}/state", [on: true])
  469. }
  470. def off(childDevice) {
  471. log.debug "Executing 'off'"
  472. put("lights/${getId(childDevice)}/state", [on: false])
  473. }
  474. def poll(childDevice) {
  475. log.debug "Executing 'poll'"
  476. get("lights/${getId(childDevice)}")
  477. }
  478. def setLevel(childDevice, percent) {
  479. log.debug "Executing 'setLevel'"
  480. def level = Math.min(Math.round(percent * 255 / 100), 255)
  481. put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0])
  482. }
  483. def setSaturation(childDevice, percent) {
  484. log.debug "Executing 'setSaturation($percent)'"
  485. def level = Math.min(Math.round(percent * 255 / 100), 255)
  486. put("lights/${getId(childDevice)}/state", [sat: level])
  487. }
  488. def setHue(childDevice, percent) {
  489. log.debug "Executing 'setHue($percent)'"
  490. def level = Math.min(Math.round(percent * 65535 / 100), 65535)
  491. put("lights/${getId(childDevice)}/state", [hue: level])
  492. }
  493. def setColor(childDevice, color) {
  494. log.debug "Executing 'setColor($color)'"
  495. def hue = Math.min(Math.round(color.hue * 65535 / 100), 65535)
  496. def sat = Math.min(Math.round(color.saturation * 255 / 100), 255)
  497. def value = [sat: sat, hue: hue]
  498. if (color.level != null) {
  499. value.bri = Math.min(Math.round(color.level * 255 / 100), 255)
  500. value.on = value.bri > 0
  501. }
  502. if (color.switch) {
  503. value.on = color.switch == "on"
  504. }
  505. log.debug "sending command $value"
  506. put("lights/${getId(childDevice)}/state", value)
  507. }
  508. def refresh() {
  509. log.debug "Executing 'refresh'"
  510. poll()
  511. }
  512. def nextLevel() {
  513. def level = device.latestValue("level") as Integer ?: 0
  514. if (level < 100) {
  515. level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
  516. }
  517. else {
  518. level = 25
  519. }
  520. setLevel(level)
  521. }
  522. /////////////////////////////////////
  523. private getId(childDevice) {
  524. if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) {
  525. return childDevice.device?.deviceNetworkId[3..-1]
  526. }
  527. else {
  528. return childDevice.device?.deviceNetworkId.split("/")[-1]
  529. }
  530. }
  531. private get(path) {
  532. log.trace "get($path)"
  533. def uri = "/api/${state.username}/$path"
  534. log.debug "GET: $uri"
  535. sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
  536. HOST: ${selectedHue}
  537. """, physicalgraph.device.Protocol.LAN, "${selectedHue}"))
  538. }
  539. private put(path, body) {
  540. def uri = "/api/${state.username}/$path"
  541. def bodyJSON = new groovy.json.JsonBuilder(body).toString()
  542. def length = bodyJSON.getBytes().size().toString()
  543. log.debug "PUT: $uri"
  544. log.debug "BODY: ${bodyJSON}"
  545. sendHubCommand(new physicalgraph.device.HubAction("""PUT $uri HTTP/1.1
  546. HOST: ${selectedHue}
  547. Content-Length: ${length}
  548. ${bodyJSON}
  549. """, physicalgraph.device.Protocol.LAN, "${selectedHue}"))
  550. }
  551. private Integer convertHexToInt(hex) {
  552. Integer.parseInt(hex,16)
  553. }
  554. private String convertHexToIP(hex) {
  555. [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
  556. }
  557. private Boolean canInstallLabs()
  558. {
  559. return hasAllHubsOver("000.011.00603")
  560. }
  561. private Boolean hasAllHubsOver(String desiredFirmware)
  562. {
  563. return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
  564. }
  565. private List getRealHubFirmwareVersions()
  566. {
  567. return location.hubs*.firmwareVersionString.findAll { it }
  568. }
  569. def convertBulbListToMap() {
  570. try {
  571. if (state.bulbs instanceof java.util.List) {
  572. def map = [:]
  573. state.bulbs.unique {it.id}.each { bulb ->
  574. map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "hub":bulb.hub]]
  575. }
  576. state.bulbs = map
  577. }
  578. }
  579. catch(Exception e) {
  580. log.error "Caught error attempting to convert bulb list to map: $e"
  581. }
  582. }