PageRenderTime 51ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/smartapps/smartthings/sonos-connect.groovy

https://github.com/rappleg/SmartThings
Groovy | 393 lines | 319 code | 60 blank | 14 comment | 64 complexity | cd974e0bb7ac605e6c6faf98f02cdcf4 MD5 | raw file
Possible License(s): Apache-2.0
  1. /**
  2. * Sonos Service Manager
  3. *
  4. * Author: SmartThings
  5. */
  6. preferences {
  7. page(name:"sonosDiscovery", title:"Sonos Device Setup", content:"sonosDiscovery", refreshTimeout:5)
  8. }
  9. //PAGES
  10. def sonosDiscovery()
  11. {
  12. if(canInstallLabs())
  13. {
  14. int sonosRefreshCount = !state.sonosRefreshCount ? 0 : state.sonosRefreshCount as int
  15. state.sonosRefreshCount = sonosRefreshCount + 1
  16. def refreshInterval = 3
  17. def options = sonosesDiscovered() ?: []
  18. def numFound = options.size() ?: 0
  19. if(!state.subscribe) {
  20. log.trace "subscribe to location"
  21. subscribe(location, null, locationHandler, [filterEvents:false])
  22. state.subscribe = true
  23. }
  24. //sonos discovery request every 5 //25 seconds
  25. if((sonosRefreshCount % 8) == 0) {
  26. discoverSonoses()
  27. }
  28. //setup.xml request every 3 seconds except on discoveries
  29. if(((sonosRefreshCount % 1) == 0) && ((sonosRefreshCount % 8) != 0)) {
  30. verifySonosPlayer()
  31. }
  32. return dynamicPage(name:"sonosDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
  33. section("Please wait while we discover your Sonos. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
  34. input "selectedSonos", "enum", required:false, title:"Select Sonos (${numFound} found)", multiple:true, options:options
  35. }
  36. }
  37. }
  38. else
  39. {
  40. def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
  41. 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"."""
  42. return dynamicPage(name:"sonosDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
  43. section("Upgrade") {
  44. paragraph "$upgradeNeeded"
  45. }
  46. }
  47. }
  48. }
  49. private discoverSonoses()
  50. {
  51. //consider using other discovery methods
  52. sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:ZonePlayer:1", physicalgraph.device.Protocol.LAN))
  53. }
  54. private verifySonosPlayer() {
  55. def devices = getSonosPlayer().findAll { it?.value?.verified != true }
  56. if(devices) {
  57. log.warn "UNVERIFIED PLAYERS!: $devices"
  58. }
  59. devices.each {
  60. verifySonos((it?.value?.ip + ":" + it?.value?.port))
  61. }
  62. }
  63. private verifySonos(String deviceNetworkId) {
  64. log.trace "dni: $deviceNetworkId"
  65. String ip = getHostAddress(deviceNetworkId)
  66. log.trace "ip:" + ip
  67. sendHubCommand(new physicalgraph.device.HubAction("""GET /xml/device_description.xml HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
  68. }
  69. Map sonosesDiscovered() {
  70. def vsonoses = getVerifiedSonosPlayer()
  71. def map = [:]
  72. vsonoses.each {
  73. def value = "${it.value.name}"
  74. def key = it.value.ip + ":" + it.value.port
  75. map["${key}"] = value
  76. }
  77. map
  78. }
  79. def getSonosPlayer()
  80. {
  81. state.sonoses = state.sonoses ?: [:]
  82. }
  83. def getVerifiedSonosPlayer()
  84. {
  85. getSonosPlayer().findAll{ it?.value?.verified == true }
  86. }
  87. def installed() {
  88. log.trace "Installed with settings: ${settings}"
  89. initialize()}
  90. def updated() {
  91. log.trace "Updated with settings: ${settings}"
  92. unschedule()
  93. initialize()
  94. }
  95. def uninstalled() {
  96. def devices = getChildDevices()
  97. log.trace "deleting ${devices.size()} Sonos"
  98. devices.each {
  99. deleteChildDevice(it.deviceNetworkId)
  100. }
  101. }
  102. def initialize() {
  103. // remove location subscription aftwards
  104. unsubscribe()
  105. state.subscribe = false
  106. unschedule()
  107. scheduleActions()
  108. if (selectedSonos) {
  109. addSonos()
  110. }
  111. scheduledActionsHandler()
  112. }
  113. def scheduledActionsHandler() {
  114. log.trace "scheduledActionsHandler()"
  115. syncDevices()
  116. refreshAll()
  117. }
  118. private scheduleActions() {
  119. def sec = Math.round(Math.floor(Math.random() * 60))
  120. def min = Math.round(Math.floor(Math.random() * 20))
  121. def cron = "$sec $min/20 * * * ?"
  122. log.trace "schedule('$cron', scheduledActionsHandler)"
  123. schedule(cron, scheduledActionsHandler)
  124. }
  125. private syncDevices() {
  126. log.trace "Doing Sonos Device Sync!"
  127. //runIn(300, "doDeviceSync" , [overwrite: false]) //schedule to run again in 5 minutes
  128. if(!state.subscribe) {
  129. subscribe(location, null, locationHandler, [filterEvents:false])
  130. state.subscribe = true
  131. }
  132. discoverSonoses()
  133. }
  134. private refreshAll(){
  135. log.trace "refreshAll()"
  136. childDevices*.refresh()
  137. log.trace "/refreshAll()"
  138. }
  139. def addSonos() {
  140. def players = getVerifiedSonosPlayer()
  141. def runSubscribe = false
  142. selectedSonos.each { dni ->
  143. def d = getChildDevice(dni)
  144. if(!d) {
  145. def newPlayer = players.find { (it.value.ip + ":" + it.value.port) == dni }
  146. log.trace "newPlayer = $newPlayer"
  147. log.trace "dni = $dni"
  148. d = addChildDevice("smartthings", "Sonos Player", dni, newPlayer?.value.hub, [label:"${newPlayer?.value.name} Sonos"])
  149. log.trace "created ${d.displayName} with id $dni"
  150. d.setModel(newPlayer?.value.model)
  151. log.trace "setModel to ${newPlayer?.value.model}"
  152. runSubscribe = true
  153. } else {
  154. log.trace "found ${d.displayName} with id $dni already exists"
  155. }
  156. }
  157. }
  158. def locationHandler(evt) {
  159. def description = evt.description
  160. def hub = evt?.hubId
  161. def parsedEvent = parseEventMessage(description)
  162. parsedEvent << ["hub":hub]
  163. if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:ZonePlayer:1"))
  164. { //SSDP DISCOVERY EVENTS
  165. log.trace "sonos found"
  166. def sonoses = getSonosPlayer()
  167. if (!(sonoses."${parsedEvent.ssdpUSN.toString()}"))
  168. { //sonos does not exist
  169. sonoses << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
  170. }
  171. else
  172. { // update the values
  173. log.trace "Device was already found in state..."
  174. def d = sonoses."${parsedEvent.ssdpUSN.toString()}"
  175. boolean deviceChangedValues = false
  176. if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
  177. d.ip = parsedEvent.ip
  178. d.port = parsedEvent.port
  179. deviceChangedValues = true
  180. log.trace "Device's port or ip changed..."
  181. }
  182. if (deviceChangedValues) {
  183. def children = getChildDevices()
  184. children.each {
  185. if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
  186. log.trace "updating dni for device ${it} with mac ${parsedEvent.mac}"
  187. it.setDeviceNetworkId((parsedEvent.ip + ":" + parsedEvent.port)) //could error if device with same dni already exists
  188. }
  189. }
  190. }
  191. }
  192. }
  193. else if (parsedEvent.headers && parsedEvent.body)
  194. { // SONOS RESPONSES
  195. def headerString = new String(parsedEvent.headers.decodeBase64())
  196. def bodyString = new String(parsedEvent.body.decodeBase64())
  197. def type = (headerString =~ /Content-Type:.*/) ? (headerString =~ /Content-Type:.*/)[0] : null
  198. def body
  199. log.trace "SONOS REPONSE TYPE: $type"
  200. if (type?.contains("xml"))
  201. { // description.xml response (application/xml)
  202. body = new XmlSlurper().parseText(bodyString)
  203. if (body?.device?.modelName?.text().startsWith("Sonos") && !body?.device?.modelName?.text().contains("Bridge") && !body?.device?.modelName?.text().contains("Sub"))
  204. {
  205. def sonoses = getSonosPlayer()
  206. def player = sonoses.find {it?.key?.contains(body?.device?.UDN?.text())}
  207. if (player)
  208. {
  209. player.value << [name:body?.device?.roomName?.text(),model:body?.device?.modelName?.text(), serialNumber:body?.device?.serialNum?.text(), verified: true]
  210. }
  211. else
  212. {
  213. log.error "/xml/device_description.xml returned a device that didn't exist"
  214. }
  215. }
  216. }
  217. else if(type?.contains("json"))
  218. { //(application/json)
  219. body = new groovy.json.JsonSlurper().parseText(bodyString)
  220. log.trace "GOT JSON $body"
  221. }
  222. }
  223. else {
  224. log.trace "cp desc: " + description
  225. //log.trace description
  226. }
  227. }
  228. private def parseEventMessage(Map event) {
  229. //handles sonos attribute events
  230. return event
  231. }
  232. private def parseEventMessage(String description) {
  233. def event = [:]
  234. def parts = description.split(',')
  235. parts.each { part ->
  236. part = part.trim()
  237. if (part.startsWith('devicetype:')) {
  238. def valueString = part.split(":")[1].trim()
  239. event.devicetype = valueString
  240. }
  241. else if (part.startsWith('mac:')) {
  242. def valueString = part.split(":")[1].trim()
  243. if (valueString) {
  244. event.mac = valueString
  245. }
  246. }
  247. else if (part.startsWith('networkAddress:')) {
  248. def valueString = part.split(":")[1].trim()
  249. if (valueString) {
  250. event.ip = valueString
  251. }
  252. }
  253. else if (part.startsWith('deviceAddress:')) {
  254. def valueString = part.split(":")[1].trim()
  255. if (valueString) {
  256. event.port = valueString
  257. }
  258. }
  259. else if (part.startsWith('ssdpPath:')) {
  260. def valueString = part.split(":")[1].trim()
  261. if (valueString) {
  262. event.ssdpPath = valueString
  263. }
  264. }
  265. else if (part.startsWith('ssdpUSN:')) {
  266. part -= "ssdpUSN:"
  267. def valueString = part.trim()
  268. if (valueString) {
  269. event.ssdpUSN = valueString
  270. }
  271. }
  272. else if (part.startsWith('ssdpTerm:')) {
  273. part -= "ssdpTerm:"
  274. def valueString = part.trim()
  275. if (valueString) {
  276. event.ssdpTerm = valueString
  277. }
  278. }
  279. else if (part.startsWith('headers')) {
  280. part -= "headers:"
  281. def valueString = part.trim()
  282. if (valueString) {
  283. event.headers = valueString
  284. }
  285. }
  286. else if (part.startsWith('body')) {
  287. part -= "body:"
  288. def valueString = part.trim()
  289. if (valueString) {
  290. event.body = valueString
  291. }
  292. }
  293. }
  294. event
  295. }
  296. /////////CHILD DEVICE METHODS
  297. def parse(childDevice, description) {
  298. def parsedEvent = parseEventMessage(description)
  299. if (parsedEvent.headers && parsedEvent.body) {
  300. def headerString = new String(parsedEvent.headers.decodeBase64())
  301. def bodyString = new String(parsedEvent.body.decodeBase64())
  302. log.trace "parse() - ${bodyString}"
  303. def body = new groovy.json.JsonSlurper().parseText(bodyString)
  304. } else {
  305. log.trace "parse - got something other than headers,body..."
  306. return []
  307. }
  308. }
  309. private Integer convertHexToInt(hex) {
  310. Integer.parseInt(hex,16)
  311. }
  312. private String convertHexToIP(hex) {
  313. [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
  314. }
  315. private getHostAddress(d) {
  316. def parts = d.split(":")
  317. def ip = convertHexToIP(parts[0])
  318. def port = convertHexToInt(parts[1])
  319. return ip + ":" + port
  320. }
  321. private Boolean canInstallLabs()
  322. {
  323. return hasAllHubsOver("000.011.00603")
  324. }
  325. private Boolean hasAllHubsOver(String desiredFirmware)
  326. {
  327. return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
  328. }
  329. private List getRealHubFirmwareVersions()
  330. {
  331. return location.hubs*.firmwareVersionString.findAll { it }
  332. }