2 * StriimLight Connect v 0.1
4 * Author: SmartThings - Ulises Mujica - obycode
8 name: "StriimLight (Connect)",
10 author: "SmartThings - Ulises Mujica - obycode",
11 description: "Allows you to control your StriimLight from the SmartThings app. Control the music and the light.",
12 category: "SmartThings Labs",
13 iconUrl: "http://obycode.com/img/icons/AwoxGreen.png",
14 iconX2Url: "http://obycode.com/img/icons/AwoxGreen@2x.png"
18 page(name: "MainPage", title: "Find and config your StriimLights",nextPage:"", install:true, uninstall: true){
20 href(name: "discover",title: "Discovery process",required: false,page: "striimLightDiscovery", description: "tap to start searching")
22 section("Options", hideable: true, hidden: true) {
23 input("refreshSLInterval", "number", title:"Enter refresh interval (min)", defaultValue:"5", required:false)
26 page(name: "striimLightDiscovery", title:"Discovery Started!", nextPage:"")
29 def striimLightDiscovery()
33 int striimLightRefreshCount = !state.striimLightRefreshCount ? 0 : state.striimLightRefreshCount as int
34 state.striimLightRefreshCount = striimLightRefreshCount + 1
35 def refreshInterval = 5
37 def options = striimLightsDiscovered() ?: []
39 def numFound = options.size() ?: 0
41 if(!state.subscribe) {
42 subscribe(location, null, locationHandler, [filterEvents:false])
43 state.subscribe = true
46 //striimLight discovery request every 5 //25 seconds
47 if((striimLightRefreshCount % 8) == 0) {
48 discoverstriimLights()
51 //setup.xml request every 3 seconds except on discoveries
52 if(((striimLightRefreshCount % 1) == 0) && ((striimLightRefreshCount % 8) != 0)) {
53 verifystriimLightPlayer()
56 return dynamicPage(name:"striimLightDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval) {
57 section("Please wait while we discover your Striim Light. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
58 input "selectedstriimLight", "enum", required:false, title:"Select Striim Light(s) (${numFound} found)", multiple:true, options:options
64 def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
66 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"."""
68 return dynamicPage(name:"striimLightDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
70 paragraph "$upgradeNeeded"
76 private discoverstriimLights()
78 sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:DimmableLight:1", physicalgraph.device.Protocol.LAN))
82 private verifystriimLightPlayer() {
83 def devices = getstriimLightPlayer().findAll { it?.value?.verified != true }
86 verifystriimLight((it?.value?.ip + ":" + it?.value?.port), it?.value?.ssdpPath)
90 private verifystriimLight(String deviceNetworkId, String ssdpPath) {
91 String ip = getHostAddress(deviceNetworkId)
96 sendHubCommand(new physicalgraph.device.HubAction("""GET $ssdpPath HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
97 //sendHubCommand(new physicalgraph.device.HubAction("""GET /aw/DimmableLight_SwitchPower/scpd.xml HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
100 Map striimLightsDiscovered() {
101 def vstriimLights = getVerifiedstriimLightPlayer()
104 def value = "${it.value.name}"
105 def key = it.value.ip + ":" + it.value.port
106 map["${key}"] = value
111 def getstriimLightPlayer()
113 state.striimLights = state.striimLights ?: [:]
116 def getVerifiedstriimLightPlayer()
118 getstriimLightPlayer().findAll{ it?.value?.verified == true }
130 def devices = getChildDevices()
132 deleteChildDevice(it.deviceNetworkId)
137 // remove location subscription aftwards
139 state.subscribe = false
143 if (selectedstriimLight) {
147 scheduledRefreshHandler()
150 def scheduledRefreshHandler() {
154 def scheduledActionsHandler() {
156 runIn(60, scheduledRefreshHandler)
160 private scheduleActions() {
161 def minutes = Math.max(settings.refreshSLInterval.toInteger(),3)
162 def cron = "0 0/${minutes} * * * ?"
163 schedule(cron, scheduledActionsHandler)
168 private syncDevices() {
169 log.debug "syncDevices()"
170 if(!state.subscribe) {
171 subscribe(location, null, locationHandler, [filterEvents:false])
172 state.subscribe = true
175 discoverstriimLights()
178 private refreshAll(){
179 log.trace "refresh all"
180 childDevices*.refresh()
183 def addstriimLight() {
184 def players = getVerifiedstriimLightPlayer()
185 def runSubscribe = false
186 selectedstriimLight.each { dni ->
187 def d = getChildDevice(dni)
190 def newLight = players.find { (it.value.ip + ":" + it.value.port) == dni }
193 d = addChildDevice("obycode", "Striim Light", dni, newLight?.value.hub, [label:"${newLight?.value.name} Striim Light","data":["model":newLight?.value.model,"dcurl":newLight?.value.dcurl,"deurl":newLight?.value.deurl,"spcurl":newLight?.value.spcurl,"speurl":newLight?.value.speurl,"xclcurl":newLight?.value.xclcurl,"xcleurl":newLight?.value.xcleurl,"xwlcurl":newLight?.value.xwlcurl,"xwleurl":newLight?.value.xwleurl,"udn":newLight?.value.udn,"dni":dni]])
200 def locationHandler(evt) {
201 def description = evt.description
203 def parsedEvent = parseEventMessage(description)
204 def msg = parseLanMessage(description)
205 parsedEvent << ["hub":hub]
207 if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:DimmableLight:1"))
208 { //SSDP DISCOVERY EVENTS
209 log.debug "Striim Light device found" + parsedEvent
210 def striimLights = getstriimLightPlayer()
213 if (!(striimLights."${parsedEvent.ssdpUSN.toString()}"))
214 { //striimLight does not exist
215 striimLights << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
218 { // update the values
220 def d = striimLights."${parsedEvent.ssdpUSN.toString()}"
221 boolean deviceChangedValues = false
222 if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
223 d.ip = parsedEvent.ip
224 d.port = parsedEvent.port
225 deviceChangedValues = true
227 if (deviceChangedValues) {
228 def children = getChildDevices()
230 if (parsedEvent.ssdpUSN.toString().contains(it.getDataValue("udn"))) {
231 it.setDeviceNetworkId((parsedEvent.ip + ":" + parsedEvent.port)) //could error if device with same dni already exists
232 it.updateDataValue("dni", (parsedEvent.ip + ":" + parsedEvent.port))
233 log.trace "Updated Device IP"
239 if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:MediaRenderer:1"))
240 { //SSDP DISCOVERY EVENTS
241 log.debug "in media renderer section!!!!"
243 else if (parsedEvent.headers && parsedEvent.body)
244 { // MEDIARENDER RESPONSES
245 def headerString = new String(parsedEvent.headers.decodeBase64())
246 def bodyString = new String(parsedEvent.body.decodeBase64())
248 def type = (headerString =~ /Content-Type:.*/) ? (headerString =~ /Content-Type:.*/)[0] : null
250 if (bodyString?.contains("xml"))
251 { // description.xml response (application/xml)
252 body = new XmlSlurper().parseText(bodyString)
253 log.debug "got $body"
256 if ( body?.device?.manufacturer?.text().startsWith("Awox") && body?.device?.deviceType?.text().contains("urn:schemas-upnp-org:device:DimmableLight:1"))
267 body?.device?.serviceList?.service?.each {
268 if (it?.serviceType?.text().contains("Dimming")) {
269 dcurl = it?.controlURL.text()
270 deurl = it?.eventSubURL.text()
272 else if (it?.serviceType?.text().contains("SwitchPower")) {
273 spcurl = it?.controlURL.text()
274 speurl = it?.eventSubURL.text()
276 else if (it?.serviceType?.text().contains("X_ColorLight")) {
277 xclcurl = it?.controlURL.text()
278 xcleurl = it?.eventSubURL.text()
280 else if (it?.serviceType?.text().contains("X_WhiteLight")) {
281 xwlcurl = it?.controlURL.text()
282 xwleurl = it?.eventSubURL.text()
287 def striimLights = getstriimLightPlayer()
288 def player = striimLights.find {it?.key?.contains(body?.device?.UDN?.text())}
291 player.value << [name:body?.device?.friendlyName?.text(),model:body?.device?.modelName?.text(), serialNumber:body?.device?.UDN?.text(), verified: true,dcurl:dcurl,deurl:deurl,spcurl:spcurl,speurl:speurl,xclcurl:xclcurl,xcleurl:xcleurl,xwlcurl:xwlcurl,xwleurl:xwleurl,udn:body?.device?.UDN?.text()]
296 else if(type?.contains("json"))
297 { //(application/json)
298 body = new groovy.json.JsonSlurper().parseText(bodyString)
303 private def parseEventMessage(Map event) {
304 //handles striimLight attribute events
308 private def parseEventMessage(String description) {
310 def parts = description.split(',')
313 if (part.startsWith('devicetype:')) {
314 def valueString = part.split(":")[1].trim()
315 event.devicetype = valueString
317 else if (part.startsWith('mac:')) {
318 def valueString = part.split(":")[1].trim()
320 event.mac = valueString
323 else if (part.startsWith('networkAddress:')) {
324 def valueString = part.split(":")[1].trim()
326 event.ip = valueString
329 else if (part.startsWith('deviceAddress:')) {
330 def valueString = part.split(":")[1].trim()
332 event.port = valueString
335 else if (part.startsWith('ssdpPath:')) {
336 def valueString = part.split(":")[1].trim()
338 event.ssdpPath = valueString
341 else if (part.startsWith('ssdpUSN:')) {
343 def valueString = part.trim()
345 event.ssdpUSN = valueString
348 else if (part.startsWith('ssdpTerm:')) {
350 def valueString = part.trim()
352 event.ssdpTerm = valueString
355 else if (part.startsWith('headers')) {
357 def valueString = part.trim()
359 event.headers = valueString
362 else if (part.startsWith('body')) {
364 def valueString = part.trim()
366 event.body = valueString
375 /////////CHILD DEVICE METHODS
376 def parse(childDevice, description) {
377 def parsedEvent = parseEventMessage(description)
379 if (parsedEvent.headers && parsedEvent.body) {
380 def headerString = new String(parsedEvent.headers.decodeBase64())
381 def bodyString = new String(parsedEvent.body.decodeBase64())
383 def body = new groovy.json.JsonSlurper().parseText(bodyString)
389 private Integer convertHexToInt(hex) {
390 Integer.parseInt(hex,16)
393 private String convertHexToIP(hex) {
394 [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
397 private getHostAddress(d) {
398 def parts = d.split(":")
399 def ip = convertHexToIP(parts[0])
400 def port = convertHexToInt(parts[1])
401 return ip + ":" + port
404 private Boolean canInstallLabs()
406 return hasAllHubsOver("000.011.00603")
409 private Boolean hasAllHubsOver(String desiredFirmware)
411 return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
414 private List getRealHubFirmwareVersions()
416 return location.hubs*.firmwareVersionString.findAll { it }