Update SmartPresence.groovy
[smartapps.git] / third-party / StriimLightConnect.groovy
1 /**
2  *  StriimLight Connect v 0.1
3  *
4  *  Author: SmartThings - Ulises Mujica - obycode
5  */
6
7 definition(
8         name: "StriimLight (Connect)",
9         namespace: "obycode",
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"
15 )
16
17 preferences {
18     page(name: "MainPage", title: "Find and config your StriimLights",nextPage:"", install:true, uninstall: true){
19         section("") {
20             href(name: "discover",title: "Discovery process",required: false,page: "striimLightDiscovery", description: "tap to start searching")
21         }
22         section("Options", hideable: true, hidden: true) {
23             input("refreshSLInterval", "number", title:"Enter refresh interval (min)", defaultValue:"5", required:false)
24         }
25     }
26     page(name: "striimLightDiscovery", title:"Discovery Started!", nextPage:"")
27 }
28
29 def striimLightDiscovery()
30 {
31         if(canInstallLabs())
32         {
33                 int striimLightRefreshCount = !state.striimLightRefreshCount ? 0 : state.striimLightRefreshCount as int
34                 state.striimLightRefreshCount = striimLightRefreshCount + 1
35                 def refreshInterval = 5
36
37                 def options = striimLightsDiscovered() ?: []
38
39                 def numFound = options.size() ?: 0
40
41                 if(!state.subscribe) {
42                         subscribe(location, null, locationHandler, [filterEvents:false])
43                         state.subscribe = true
44                 }
45
46                 //striimLight discovery request every 5 //25 seconds
47                 if((striimLightRefreshCount % 8) == 0) {
48                         discoverstriimLights()
49                 }
50
51                 //setup.xml request every 3 seconds except on discoveries
52                 if(((striimLightRefreshCount % 1) == 0) && ((striimLightRefreshCount % 8) != 0)) {
53                         verifystriimLightPlayer()
54                 }
55
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
59                         }
60                 }
61         }
62         else
63         {
64                 def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
65
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"."""
67
68                 return dynamicPage(name:"striimLightDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
69                         section("Upgrade") {
70                                 paragraph "$upgradeNeeded"
71                         }
72                 }
73         }
74 }
75
76 private discoverstriimLights()
77 {
78         sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:DimmableLight:1", physicalgraph.device.Protocol.LAN))
79 }
80
81
82 private verifystriimLightPlayer() {
83         def devices = getstriimLightPlayer().findAll { it?.value?.verified != true }
84
85         devices.each {
86                 verifystriimLight((it?.value?.ip + ":" + it?.value?.port), it?.value?.ssdpPath)
87         }
88 }
89
90 private verifystriimLight(String deviceNetworkId, String ssdpPath) {
91         String ip = getHostAddress(deviceNetworkId)
92         if(!ssdpPath){
93                 ssdpPath = "/"
94         }
95
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}"))
98 }
99
100 Map striimLightsDiscovered() {
101         def vstriimLights = getVerifiedstriimLightPlayer()
102         def map = [:]
103         vstriimLights.each {
104                 def value = "${it.value.name}"
105                 def key = it.value.ip + ":" + it.value.port
106                 map["${key}"] = value
107         }
108         map
109 }
110
111 def getstriimLightPlayer()
112 {
113         state.striimLights = state.striimLights ?: [:]
114 }
115
116 def getVerifiedstriimLightPlayer()
117 {
118         getstriimLightPlayer().findAll{ it?.value?.verified == true }
119 }
120
121 def installed() {
122         initialize()}
123
124 def updated() {
125         unschedule()
126         initialize()
127 }
128
129 def uninstalled() {
130         def devices = getChildDevices()
131         devices.each {
132                 deleteChildDevice(it.deviceNetworkId)
133         }
134 }
135
136 def initialize() {
137         // remove location subscription aftwards
138         unsubscribe()
139         state.subscribe = false
140
141         unschedule()
142
143         if (selectedstriimLight) {
144                 addstriimLight()
145         }
146     scheduleActions()
147     scheduledRefreshHandler()
148 }
149
150 def scheduledRefreshHandler() {
151         refreshAll()
152 }
153
154 def scheduledActionsHandler() {
155     syncDevices()
156         runIn(60, scheduledRefreshHandler)
157
158 }
159
160 private scheduleActions() {
161         def minutes = Math.max(settings.refreshSLInterval.toInteger(),3)
162     def cron = "0 0/${minutes} * * * ?"
163         schedule(cron, scheduledActionsHandler)
164 }
165
166
167
168 private syncDevices() {
169         log.debug "syncDevices()"
170         if(!state.subscribe) {
171                 subscribe(location, null, locationHandler, [filterEvents:false])
172                 state.subscribe = true
173         }
174
175         discoverstriimLights()
176 }
177
178 private refreshAll(){
179         log.trace "refresh all"
180         childDevices*.refresh()
181 }
182
183 def addstriimLight() {
184         def players = getVerifiedstriimLightPlayer()
185         def runSubscribe = false
186         selectedstriimLight.each { dni ->
187                 def d = getChildDevice(dni)
188                 log.trace "dni $dni"
189                 if(!d) {
190                         def newLight = players.find { (it.value.ip + ":" + it.value.port) == dni }
191                         if (newLight){
192                                 //striimLight
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]])
194                         }
195                         runSubscribe = true
196                 }
197         }
198 }
199
200 def locationHandler(evt) {
201         def description = evt.description
202         def hub = evt?.hubId
203         def parsedEvent = parseEventMessage(description)
204         def msg = parseLanMessage(description)
205         parsedEvent << ["hub":hub]
206
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()
211
212
213                 if (!(striimLights."${parsedEvent.ssdpUSN.toString()}"))
214                 { //striimLight does not exist
215                         striimLights << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
216                 }
217                 else
218                 { // update the values
219
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
226                         }
227                         if (deviceChangedValues) {
228                 def children = getChildDevices()
229                                 children.each {
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"
234                                         }
235                                 }
236                         }
237                 }
238         }
239         if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:MediaRenderer:1"))
240         { //SSDP DISCOVERY EVENTS
241                 log.debug "in media renderer section!!!!"
242         }
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())
247
248                 def type = (headerString =~ /Content-Type:.*/) ? (headerString =~ /Content-Type:.*/)[0] : null
249                 def body
250                 if (bodyString?.contains("xml"))
251                 { // description.xml response (application/xml)
252                         body = new XmlSlurper().parseText(bodyString)
253                         log.debug "got $body"
254
255                         // Find Awox devices
256                         if ( body?.device?.manufacturer?.text().startsWith("Awox") && body?.device?.deviceType?.text().contains("urn:schemas-upnp-org:device:DimmableLight:1"))
257                         {
258                                 def dcurl = ""
259                                 def deurl = ""
260                                 def spcurl = ""
261                                 def speurl = ""
262                                 def xclcurl = ""
263                                 def xcleurl = ""
264                                 def xwlcurl = ""
265                                 def xwleurl = ""
266
267                                 body?.device?.serviceList?.service?.each {
268                                         if (it?.serviceType?.text().contains("Dimming")) {
269                                                 dcurl = it?.controlURL.text()
270                                                 deurl = it?.eventSubURL.text()
271                                         }
272                                         else if (it?.serviceType?.text().contains("SwitchPower")) {
273                                                 spcurl = it?.controlURL.text()
274                                                 speurl = it?.eventSubURL.text()
275                                         }
276                                         else if (it?.serviceType?.text().contains("X_ColorLight")) {
277                                                 xclcurl = it?.controlURL.text()
278                                                 xcleurl = it?.eventSubURL.text()
279                                         }
280                                         else if (it?.serviceType?.text().contains("X_WhiteLight")) {
281                                                 xwlcurl = it?.controlURL.text()
282                                                 xwleurl = it?.eventSubURL.text()
283                                         }
284                                 }
285
286
287                                 def striimLights = getstriimLightPlayer()
288                                 def player = striimLights.find {it?.key?.contains(body?.device?.UDN?.text())}
289                                 if (player)
290                                 {
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()]
292                                 }
293
294                         }
295                 }
296                 else if(type?.contains("json"))
297                 { //(application/json)
298                         body = new groovy.json.JsonSlurper().parseText(bodyString)
299                 }
300         }
301 }
302
303 private def parseEventMessage(Map event) {
304         //handles striimLight attribute events
305         return event
306 }
307
308 private def parseEventMessage(String description) {
309         def event = [:]
310         def parts = description.split(',')
311         parts.each { part ->
312                 part = part.trim()
313                 if (part.startsWith('devicetype:')) {
314                         def valueString = part.split(":")[1].trim()
315                         event.devicetype = valueString
316                 }
317                 else if (part.startsWith('mac:')) {
318                         def valueString = part.split(":")[1].trim()
319                         if (valueString) {
320                                 event.mac = valueString
321                         }
322                 }
323                 else if (part.startsWith('networkAddress:')) {
324                         def valueString = part.split(":")[1].trim()
325                         if (valueString) {
326                                 event.ip = valueString
327                         }
328                 }
329                 else if (part.startsWith('deviceAddress:')) {
330                         def valueString = part.split(":")[1].trim()
331                         if (valueString) {
332                                 event.port = valueString
333                         }
334                 }
335                 else if (part.startsWith('ssdpPath:')) {
336                         def valueString = part.split(":")[1].trim()
337                         if (valueString) {
338                                 event.ssdpPath = valueString
339                         }
340                 }
341                 else if (part.startsWith('ssdpUSN:')) {
342                         part -= "ssdpUSN:"
343                         def valueString = part.trim()
344                         if (valueString) {
345                                 event.ssdpUSN = valueString
346                         }
347                 }
348                 else if (part.startsWith('ssdpTerm:')) {
349                         part -= "ssdpTerm:"
350                         def valueString = part.trim()
351                         if (valueString) {
352                                 event.ssdpTerm = valueString
353                         }
354                 }
355                 else if (part.startsWith('headers')) {
356                         part -= "headers:"
357                         def valueString = part.trim()
358                         if (valueString) {
359                                 event.headers = valueString
360                         }
361                 }
362                 else if (part.startsWith('body')) {
363                         part -= "body:"
364                         def valueString = part.trim()
365                         if (valueString) {
366                                 event.body = valueString
367                         }
368                 }
369         }
370
371         event
372 }
373
374
375 /////////CHILD DEVICE METHODS
376 def parse(childDevice, description) {
377         def parsedEvent = parseEventMessage(description)
378
379         if (parsedEvent.headers && parsedEvent.body) {
380                 def headerString = new String(parsedEvent.headers.decodeBase64())
381                 def bodyString = new String(parsedEvent.body.decodeBase64())
382
383                 def body = new groovy.json.JsonSlurper().parseText(bodyString)
384         } else {
385                 return []
386         }
387 }
388
389 private Integer convertHexToInt(hex) {
390         Integer.parseInt(hex,16)
391 }
392
393 private String convertHexToIP(hex) {
394         [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
395 }
396
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
402 }
403
404 private Boolean canInstallLabs()
405 {
406         return hasAllHubsOver("000.011.00603")
407 }
408
409 private Boolean hasAllHubsOver(String desiredFirmware)
410 {
411         return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
412 }
413
414 private List getRealHubFirmwareVersions()
415 {
416         return location.hubs*.firmwareVersionString.findAll { it }
417 }