From 9e7e892f41ac7102217c0b49f2b09c7510624767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Alirio=20Mejia=20Ca=C3=B1as?= Date: Wed, 29 Mar 2017 07:03:46 -0700 Subject: [PATCH] MSA-1864: mediaRenderer connect --- .../mediarenderer-connect.groovy | 445 ++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 smartapps/mujica/mediarenderer-connect.src/mediarenderer-connect.groovy diff --git a/smartapps/mujica/mediarenderer-connect.src/mediarenderer-connect.groovy b/smartapps/mujica/mediarenderer-connect.src/mediarenderer-connect.groovy new file mode 100644 index 0000000..85ab6df --- /dev/null +++ b/smartapps/mujica/mediarenderer-connect.src/mediarenderer-connect.groovy @@ -0,0 +1,445 @@ +/** + * MediaRenderer Service Manager v 2.0.1 + * + * Author: SmartThings - Ulises Mujica + */ + +definition( + name: "MediaRenderer (Connect)", + namespace: "mujica", + author: "SmartThings - Ulises Mujica", + description: "Allows you to control your Media Renderer from the SmartThings app. Perform basic functions like play, pause, stop, change track, and check artist and song name from the Things screen.", + category: "SmartThings Labs", + singleInstance: true, + iconUrl: "https://graph.api.smartthings.com/api/devices/icons/st.secondary.smartapps-tile?displaySize=2x", + iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.secondary.smartapps-tile?displaySize=2x" +) + +preferences { + page(name: "MainPage", title: "Search and config your Media Renderers", install:true, uninstall: true){ + section("") { + href(name: "discover",title: "Discovery process",required: false,page: "mediaRendererDiscovery",description: "tap to start searching") + } + section("Options", hideable: true, hidden: true) { + input("refreshMRInterval", "number", title:"Enter refresh players interval (min)",defaultValue:"15", required:false) + } + } + page(name: "mediaRendererDiscovery", title:"Discovery Started!") +} + +def mediaRendererDiscovery() +{ + log.trace "mediaRendererDiscovery() state.subscribe ${state.subscribe}" + if(canInstallLabs()) + { + + + int mediaRendererRefreshCount = !state.mediaRendererRefreshCount ? 0 : state.mediaRendererRefreshCount as int + state.mediaRendererRefreshCount = mediaRendererRefreshCount + 1 + def refreshInterval = 5 + + def options = mediaRenderersDiscovered() ?: [] + + def numFound = options.size() ?: 0 + + if(!state.subscribe) { + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + //mediaRenderer discovery request every 5 //25 seconds + if((mediaRendererRefreshCount % 8) == 0) { + discoverMediaRenderers() + } + + //setup.xml request every 3 seconds except on discoveries + if(((mediaRendererRefreshCount % 1) == 0) && ((mediaRendererRefreshCount % 8) != 0)) { + verifyMediaRendererPlayer() + } + + return dynamicPage(name:"mediaRendererDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval) { + section("Please wait while we discover your MediaRenderer. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { + input "selectedMediaRenderer", "enum", required:false, title:"Select Media Renderer (${numFound} found)", multiple:true, options:options + } + } + } + else + { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + 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".""" + + return dynamicPage(name:"mediaRendererDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section("Upgrade") { + paragraph "$upgradeNeeded" + } + } + } +} + +private discoverMediaRenderers() +{ + sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:MediaRenderer:1", physicalgraph.device.Protocol.LAN)) +} + + +private verifyMediaRendererPlayer() { + def devices = getMediaRendererPlayer().findAll { it?.value?.verified != true } + + devices.each { + verifyMediaRenderer((it?.value?.ip + ":" + it?.value?.port), it?.value?.ssdpPath) + } +} + +private verifyMediaRenderer(String deviceNetworkId, String ssdpPath) { + String ip = getHostAddress(deviceNetworkId) + if(!ssdpPath){ + ssdpPath = "/" + } + log.trace "verifyMediaRenderer($deviceNetworkId, $ssdpPath, $ip)" + sendHubCommand(new physicalgraph.device.HubAction("""GET $ssdpPath HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}")) +} + +Map mediaRenderersDiscovered() { + def vmediaRenderers = getVerifiedMediaRendererPlayer() + def map = [:] + vmediaRenderers.each { + def value = "${it.value.name}" + def key = it.value.ip + ":" + it.value.port + map["${key}"] = value + } + map +} + +def getMediaRendererPlayer() +{ + state.mediaRenderers = state.mediaRenderers ?: [:] +} + +def getVerifiedMediaRendererPlayer() +{ + getMediaRendererPlayer().findAll{ it?.value?.verified == true } +} + +def installed() { + log.trace "installed()" + //initialize() +} + +def updated() { + log.trace "updated()" + + + if (selectedMediaRenderer) { + addMediaRenderer() + } + + unsubscribe() + state.subscribe = false + unschedule() + scheduleTimer() + scheduleActions() + + refreshAll() + syncDevices() +} + +def uninstalled() { + def devices = getChildDevices() + devices.each { + deleteChildDevice(it.deviceNetworkId) + } +} + +def initialize() { + // remove location subscription aftwards + log.trace "initialize()" + //scheduledRefreshHandler() +} + + +def scheduledRefreshHandler(){ + +} + +def scheduledTimerHandler() { + timerAll() +} + +def scheduledActionsHandler() { + syncDevices() + //runIn(60, scheduledRefreshHandler) +} + +private scheduleTimer() { + def cron = "0 0/3 * * * ?" + schedule(cron, scheduledTimerHandler) +} + +private scheduleActions() { + def minutes = Math.max(settings.refreshMRInterval.toInteger(),1) + def cron = "0 0/${minutes} * * * ?" + schedule(cron, scheduledActionsHandler) +} + +private syncDevices() { + log.debug "syncDevices()" + if(!state.subscribe) { + subscribe(location, null, locationHandler, [filterEvents:false]) + log.trace "subscribe($location, null, locationHandler, [filterEvents:false])" + state.subscribe = true + } + + discoverMediaRenderers() +} + +private timerAll(){ + childDevices*.poll() +} + +private refreshAll(){ + childDevices*.refresh() +} + +def addMediaRenderer() { + def players = getVerifiedMediaRendererPlayer() + def runSubscribe = false + selectedMediaRenderer.each { dni -> + def d = getChildDevice(dni) + if(!d) { + def newPlayer = players.find { (it.value.ip + ":" + it.value.port) == dni } + if (newPlayer){ + d = addChildDevice("mujica", "DLNA Player", dni, newPlayer?.value.hub, [label:"${newPlayer?.value.name} Speaker","data":["model":newPlayer?.value.model,"avtcurl":newPlayer?.value.avtcurl,"avteurl":newPlayer?.value.avteurl,"rccurl":newPlayer?.value.rccurl,"rceurl":newPlayer?.value.rceurl,"udn":newPlayer?.value.udn,"dni":dni]]) + } + runSubscribe = true + } + } +} + +def locationHandler(evt) { + def description = evt.description + def hub = evt?.hubId + def parsedEvent = parseEventMessage(description) + def msg = parseLanMessage(description) + + if (msg?.headers?.sid) + { + childDevices*.each { childDevice -> + if(childDevice.getDataValue('subscriptionId') == ((msg?.headers?.sid ?:"") - "uuid:")|| childDevice.getDataValue('subscriptionId1') == ((msg?.headers?.sid ?:"") - "uuid:")){ + childDevice.parse(description) + } + } + } + + + parsedEvent << ["hub":hub] + + if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:MediaRenderer:1")) + { //SSDP DISCOVERY EVENTS + log.debug "MediaRenderer device found" + parsedEvent + def mediaRenderers = getMediaRendererPlayer() + + + if (!(mediaRenderers."${parsedEvent.ssdpUSN.toString()}")) + { //mediaRenderer does not exist + mediaRenderers << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] + } + else + { // update the values + + def d = mediaRenderers."${parsedEvent.ssdpUSN.toString()}" + boolean deviceChangedValues = false + if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { + d.ip = parsedEvent.ip + d.port = parsedEvent.port + deviceChangedValues = true + } + if (deviceChangedValues) { + def children = getChildDevices() + children.each { + if (parsedEvent.ssdpUSN.toString().contains(it.getDataValue("udn"))) { + it.setDeviceNetworkId((parsedEvent.ip + ":" + parsedEvent.port)) //could error if device with same dni already exists + it.updateDataValue("dni", (parsedEvent.ip + ":" + parsedEvent.port)) + it.refresh() + log.trace "Updated Device IP" + + } + } + } + } + } + else if (parsedEvent.headers && parsedEvent.body) + { // MEDIARENDER RESPONSES + def headerString = new String(parsedEvent?.headers?.decodeBase64()) + def bodyString = new String(parsedEvent.body.decodeBase64()) + + def type = (headerString =~ /Content-Type:.*/) ? (headerString =~ /Content-Type:.*/)[0] : null + def body + def device + if (bodyString?.contains("xml")) + { // description.xml response (application/xml) + body = new XmlSlurper().parseText(bodyString) + log.trace "MEDIARENDER RESPONSES ${body?.device?.modelName?.text()}" + // Avoid add sonos devices + device = body?.device + body?.device?.deviceList?.device?.each{ + if (it?.deviceType?.text().contains("urn:schemas-upnp-org:device:MediaRenderer:1")) { + device = it + } + } + if ( device?.deviceType?.text().contains("urn:schemas-upnp-org:device:MediaRenderer:1")) + { + def avtcurl = "" + def avteurl = "" + def rccurl = "" + def rceurl = "" + + + device?.serviceList?.service?.each{ + if (it?.serviceType?.text().contains("AVTransport")) { + avtcurl = it?.controlURL?.text().startsWith("/")? it?.controlURL.text() : "/" + it?.controlURL.text() + avteurl = it?.eventSubURL?.text().startsWith("/")? it?.eventSubURL.text() : "/" + it?.eventSubURL.text() + } + else if (it?.serviceType?.text().contains("RenderingControl")) { + rccurl = it?.controlURL?.text().startsWith("/")? it?.controlURL?.text() : "/" + it?.controlURL?.text() + rceurl = it?.eventSubURL?.text().startsWith("/")? it?.eventSubURL?.text() : "/" + it?.eventSubURL?.text() + } + } + + + def mediaRenderers = getMediaRendererPlayer() + def player = mediaRenderers.find {it?.key?.contains(device?.UDN?.text())} + if (player) + { + player.value << [name:device?.friendlyName?.text(),model:device?.modelName?.text(), serialNumber:device?.UDN?.text(), verified: true,avtcurl:avtcurl,avteurl:avteurl,rccurl:rccurl,rceurl:rceurl,udn:device?.UDN?.text()] + } + + } + } + else if(type?.contains("json")) + { //(application/json) + body = new groovy.json.JsonSlurper().parseText(bodyString) + } + } +} + +private def parseEventMessage(Map event) { + //handles mediaRenderer attribute events + return event +} + +private def parseEventMessage(String description) { + def event = [:] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('devicetype:')) { + def valueString = part.split(":")[1].trim() + event.devicetype = valueString + } + else if (part.startsWith('mac:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + event.mac = valueString + } + } + else if (part.startsWith('networkAddress:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + event.ip = valueString + } + } + else if (part.startsWith('deviceAddress:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + event.port = valueString + } + } + else if (part.startsWith('ssdpPath:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + event.ssdpPath = valueString + } + } + else if (part.startsWith('ssdpUSN:')) { + part -= "ssdpUSN:" + def valueString = part.trim() + if (valueString) { + event.ssdpUSN = valueString + } + } + else if (part.startsWith('ssdpTerm:')) { + part -= "ssdpTerm:" + def valueString = part.trim() + if (valueString) { + event.ssdpTerm = valueString + } + } + else if (part.startsWith('headers')) { + part -= "headers:" + def valueString = part.trim() + if (valueString) { + event.headers = valueString + } + } + else if (part.startsWith('body')) { + part -= "body:" + def valueString = part.trim() + if (valueString) { + event.body = valueString + } + } + } + + if (event.devicetype == "04" && event.ssdpPath =~ /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/ && !event.ssdpUSN && !event.ssdpTerm){ + def matcher = event.ssdpPath =~ /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/ + def ssdpUSN = matcher[0] + event.ssdpUSN = "uuid:$ssdpUSN::urn:schemas-upnp-org:device:MediaRenderer:1" + event.ssdpTerm = "urn:schemas-upnp-org:device:MediaRenderer:1" + } + event +} + + +/////////CHILD DEVICE METHODS +def parse(childDevice, description) { + def parsedEvent = parseEventMessage(description) + + if (parsedEvent.headers && parsedEvent.body) { + def headerString = new String(parsedEvent.headers.decodeBase64()) + def bodyString = new String(parsedEvent.body.decodeBase64()) + + def body = new groovy.json.JsonSlurper().parseText(bodyString) + } else { + return [] + } +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private getHostAddress(d) { + def parts = d.split(":") + def ip = convertHexToIP(parts[0]) + def port = convertHexToInt(parts[1]) + return ip + ":" + port +} + +private Boolean canInstallLabs() +{ + return hasAllHubsOver("000.011.00603") +} + +private Boolean hasAllHubsOver(String desiredFirmware) +{ + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +private List getRealHubFirmwareVersions() +{ + return location.hubs*.firmwareVersionString.findAll { it } +} \ No newline at end of file