Update vacation-lighting-director.groovy
[smartapps.git] / official / simple-control.groovy
1 /**
2  *  Simple Control
3  *
4  *  Copyright 2015 Roomie Remote, Inc.
5  *
6  *      Date: 2015-09-22
7  */
8
9 definition(
10     name: "Simple Control",
11     namespace: "roomieremote-roomieconnect",
12     author: "Roomie Remote, Inc.",
13     description: "Integrate SmartThings with your Simple Control activities.",
14     category: "My Apps",
15     iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png",
16     iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png",
17     iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png")
18
19 preferences()
20 {
21         section("Allow Simple Control to Monitor and Control These Things...")
22     {
23         input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
24         input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
25         input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
26         input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false
27         input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false
28         input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false
29         input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false
30         }
31         
32         page(name: "mainPage", title: "Simple Control Setup", content: "mainPage", refreshTimeout: 5)
33         page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5)
34         page(name:"manualAgentEntry")
35         page(name:"verifyManualEntry")
36 }
37
38 mappings {
39         path("/devices") {
40         action: [
41                 GET: "getDevices"
42         ]
43         }
44     path("/:deviceType/devices") {
45         action: [
46                 GET: "getDevices",
47             POST: "handleDevicesWithIDs"
48         ]
49     }
50     path("/device/:deviceType/:id") {
51         action: [
52                 GET: "getDevice",
53             POST: "updateDevice"
54         ]
55     }
56         path("/subscriptions") {
57                 action: [
58                         GET: "listSubscriptions",
59                         POST: "addSubscription", // {"deviceId":"xxx", "attributeName":"xxx","callbackUrl":"http://..."}
60             DELETE: "removeAllSubscriptions"
61                 ]
62         }
63         path("/subscriptions/:id") {
64                 action: [
65                         DELETE: "removeSubscription"
66                 ]
67         }
68 }
69
70 private getAllDevices()
71 {
72         //log.debug("getAllDevices()")
73         ([] + switches + locks + thermostats + imageCaptures + relaySwitches + doorControls + colorControls + musicPlayers + speechSynthesizers + switchLevels + indicators + mediaControllers + tones + tvs + alarms + valves + motionSensors + presenceSensors + beacons + pushButtons + smokeDetectors + coDetectors + contactSensors + accelerationSensors + energyMeters + powerMeters + lightSensors + humiditySensors + temperatureSensors + speechRecognizers + stepSensors + touchSensors)?.findAll()?.unique { it.id }
74 }
75
76 def getDevices()
77 {
78         //log.debug("getDevices, params: ${params}")
79     allDevices.collect {
80         //log.debug("device: ${it}")
81                 deviceItem(it)
82         }
83 }
84
85 def getDevice()
86 {
87         //log.debug("getDevice, params: ${params}")
88     def device = allDevices.find { it.id == params.id }
89     if (!device)
90     {
91         render status: 404, data: '{"msg": "Device not found"}'
92     }
93     else
94     {
95         deviceItem(device)
96     }
97 }
98
99 def handleDevicesWithIDs()
100 {
101         //log.debug("handleDevicesWithIDs, params: ${params}")
102         def data = request.JSON
103         def ids = data?.ids?.findAll()?.unique()
104     //log.debug("ids: ${ids}")
105     def command = data?.command
106         def arguments = data?.arguments
107         def type = params?.deviceType
108     //log.debug("device type: ${type}")
109     if (command)
110     {
111         def statusCode = 404
112             //log.debug("command ${command}, arguments ${arguments}")
113         for (devId in ids)
114         {
115                         def device = allDevices.find { it.id == devId }
116             //log.debug("device: ${device}")
117                         // Check if we have a device that responds to the specified command
118                         if (validateCommand(device, type, command)) {
119                 if (arguments) {
120                                         device."$command"(*arguments)
121                 }
122                 else {
123                         device."$command"()
124                 }
125                                 statusCode = 200
126                         } else {
127                 statusCode = 403
128                         }
129                 }
130         def responseData = "{}"
131         switch (statusCode)
132         {
133                 case 403:
134                 responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
135                 break
136                         case 404:
137                 responseData = '{"msg": "Device not found"}'
138                 break
139         }
140         render status: statusCode, data: responseData
141     }
142     else
143     {
144                 ids.collect {
145                 def currentId = it
146                 def device = allDevices.find { it.id == currentId }
147                 if (device)
148                 {
149                         deviceItem(device)
150             }
151                 }
152         }
153 }
154
155 private deviceItem(device) {
156         [
157                 id: device.id,
158                 label: device.displayName,
159                 currentState: device.currentStates,
160                 capabilities: device.capabilities?.collect {[
161                         name: it.name
162                 ]},
163                 attributes: device.supportedAttributes?.collect {[
164                         name: it.name,
165                         dataType: it.dataType,
166                         values: it.values
167                 ]},
168                 commands: device.supportedCommands?.collect {[
169                         name: it.name,
170                         arguments: it.arguments
171                 ]},
172                 type: [
173                         name: device.typeName,
174                         author: device.typeAuthor
175                 ]
176         ]
177 }
178
179 def updateDevice()
180 {
181         //log.debug("updateDevice, params: ${params}")
182         def data = request.JSON
183         def command = data?.command
184         def arguments = data?.arguments
185         def type = params?.deviceType
186     //log.debug("device type: ${type}")
187
188         //log.debug("updateDevice, params: ${params}, request: ${data}")
189         if (!command) {
190                 render status: 400, data: '{"msg": "command is required"}'
191         } else {
192                 def statusCode = 404
193                 def device = allDevices.find { it.id == params.id }
194                 if (device) {
195                         // Check if we have a device that responds to the specified command
196                         if (validateCommand(device, type, command)) {
197                         if (arguments) {
198                                         device."$command"(*arguments)
199                     }
200                     else {
201                         device."$command"()
202                     }
203                                 statusCode = 200
204                         } else {
205                         statusCode = 403
206                         }
207                 }
208                 
209             def responseData = "{}"
210             switch (statusCode)
211             {
212                 case 403:
213                         responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
214                     break
215                         case 404:
216                         responseData = '{"msg": "Device not found"}'
217                     break
218             }
219             render status: statusCode, data: responseData
220         }
221 }
222
223 /**
224  * Validating the command passed by the user based on capability.
225  * @return boolean
226  */
227 def validateCommand(device, deviceType, command) {
228         //log.debug("validateCommand ${command}")
229     def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
230     //log.debug("capabilityCommands: ${capabilityCommands}")
231         def currentDeviceCapability = getCapabilityName(deviceType)
232     //log.debug("currentDeviceCapability: ${currentDeviceCapability}")
233         if (capabilityCommands[currentDeviceCapability]) {
234                 return command in capabilityCommands[currentDeviceCapability] ? true : false
235         } else {
236                 // Handling other device types here, which don't accept commands
237                 httpError(400, "Bad request.")
238         }
239 }
240
241 /**
242  * Need to get the attribute name to do the lookup. Only
243  * doing it for the device types which accept commands
244  * @return attribute name of the device type
245  */
246 def getCapabilityName(type) {
247     switch(type) {
248                 case "switches":
249                         return "Switch"
250                 case "locks":
251                         return "Lock"
252         case "thermostats":
253                 return "Thermostat"
254         case "doorControls":
255                 return "Door Control"
256         case "colorControls":
257                 return "Color Control"
258         case "musicPlayers":
259                 return "Music Player"
260         case "switchLevels":
261                 return "Switch Level"
262                 default:
263                         return type
264         }
265 }
266
267 /**
268  * Constructing the map over here of
269  * supported commands by device capability
270  * @return a map of device capability -> supported commands
271  */
272 def getDeviceCapabilityCommands(deviceCapabilities) {
273         def map = [:]
274         deviceCapabilities.collect {
275                 map[it.name] = it.commands.collect{ it.name.toString() }
276         }
277         return map
278 }
279
280 def listSubscriptions()
281 {
282         //log.debug "listSubscriptions()"
283         app.subscriptions?.findAll { it.deviceId }?.collect {
284                 def deviceInfo = state[it.deviceId]
285                 def response = [
286                         id: it.id,
287                         deviceId: it.deviceId,
288                         attributeName: it.data,
289                         handler: it.handler
290                 ]
291                 //if (!selectedAgent) {
292                         response.callbackUrl = deviceInfo?.callbackUrl
293                 //}
294                 response
295         } ?: []
296 }
297
298 def addSubscription() {
299         def data = request.JSON
300         def attribute = data.attributeName
301         def callbackUrl = data.callbackUrl
302
303         //log.debug "addSubscription, params: ${params}, request: ${data}"
304         if (!attribute) {
305                 render status: 400, data: '{"msg": "attributeName is required"}'
306         } else {
307                 def device = allDevices.find { it.id == data.deviceId }
308                 if (device) {
309                         //if (!selectedAgent) {
310                                 //log.debug "Adding callbackUrl: $callbackUrl"
311                                 state[device.id] = [callbackUrl: callbackUrl]
312                         //}
313                         //log.debug "Adding subscription"
314                         def subscription = subscribe(device, attribute, deviceHandler)
315                         if (!subscription || !subscription.eventSubscription) {
316                 //log.debug("subscriptions: ${app.subscriptions}")
317                 //for (sub in app.subscriptions)
318                 //{
319                         //log.debug("subscription.id ${sub.id} subscription.handler ${sub.handler} subscription.deviceId ${sub.deviceId}")
320                     //log.debug(sub.properties.collect{it}.join('\n'))
321                                 //}
322                                 subscription = app.subscriptions?.find { it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' }
323                         }
324
325                         def response = [
326                                 id: subscription.id,
327                                 deviceId: subscription.device?.id,
328                                 attributeName: subscription.data,
329                                 handler: subscription.handler
330                         ]
331                         //if (!selectedAgent) {
332                                 response.callbackUrl = callbackUrl
333                         //}
334                         response
335                 } else {
336                         render status: 400, data: '{"msg": "Device not found"}'
337                 }
338         }
339 }
340
341 def removeSubscription()
342 {
343         def subscription = app.subscriptions?.find { it.id == params.id }
344         def device = subscription?.device
345
346         //log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}"
347         if (device) {
348                 //log.debug "Removing subscription for device: ${device.id}"
349                 state.remove(device.id)
350                 unsubscribe(device)
351         }
352         render status: 204, data: "{}"
353 }
354
355 def removeAllSubscriptions()
356 {
357         for (sub in app.subscriptions)
358     {
359         //log.debug("Subscription: ${sub}")
360         //log.debug(sub.properties.collect{it}.join('\n'))
361         def handler = sub.handler
362         def device = sub.device
363         
364         if (device && handler == 'deviceHandler')
365         {
366                 //log.debug(device.properties.collect{it}.join('\n'))
367                 //log.debug("Removing subscription for device: ${device}")
368             state.remove(device.id)
369             unsubscribe(device)
370         }
371     }
372 }
373
374 def deviceHandler(evt) {
375         def deviceInfo = state[evt.deviceId]
376         //if (selectedAgent) {
377     //  sendToRoomie(evt, agentCallbackUrl)
378         //} else if (deviceInfo) {
379     if (deviceInfo)
380     {
381                 if (deviceInfo.callbackUrl) {
382                         sendToRoomie(evt, deviceInfo.callbackUrl)
383                 } else {
384                         log.warn "No callbackUrl set for device: ${evt.deviceId}"
385                 }
386         } else {
387                 log.warn "No subscribed device found for device: ${evt.deviceId}"
388         }
389 }
390
391 def sendToRoomie(evt, String callbackUrl) {
392         def callback = new URI(callbackUrl)
393         def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
394         def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
395         /*sendHubCommand(new physicalgraph.device.HubAction(
396                 method: "POST",
397                 path: path,
398                 headers: [
399                         "Host": host,
400                         "Content-Type": "application/json"
401                 ],
402                 body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
403         ))*/
404 }
405
406 def mainPage()
407 {
408         if (canInstallLabs())
409     {
410         return agentDiscovery()
411     }
412     else
413     {
414         def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
415
416 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"."""
417
418         return dynamicPage(name:"mainPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
419             section("Upgrade")
420             {
421                 paragraph "$upgradeNeeded"
422             }
423         }
424     }
425 }
426
427 def agentDiscovery(params=[:])
428 {
429         int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
430     state.refreshCount = refreshCount + 1
431     def refreshInterval = refreshCount == 0 ? 2 : 5
432         
433     if (!state.subscribe)
434     {
435         subscribe(location, null, locationHandler, [filterEvents:false])
436         state.subscribe = true
437     }
438         
439     //ssdp request every fifth refresh
440     if ((refreshCount % 5) == 0)
441     {
442         discoverAgents()
443     }
444         
445     def agentsDiscovered = agentsDiscovered()
446     
447     return dynamicPage(name:"agentDiscovery", title:"Pair with Simple Sync", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
448         section("Pair with Simple Sync")
449         {
450             input "selectedAgent", "enum", required:false, title:"Select Simple Sync\n(${agentsDiscovered.size() ?: 0} found)", multiple:false, options:agentsDiscovered
451                 href(name:"manualAgentEntry",
452                  title:"Manually Configure Simple Sync",
453                  required:false,
454                  page:"manualAgentEntry")
455         }
456         section("Allow Simple Control to Monitor and Control These Things...")
457         {
458                         input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
459                         input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
460                         input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
461                         input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false
462                         input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false
463                         input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false
464                         input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false
465                 }
466     }
467 }
468
469 def manualAgentEntry()
470 {
471         dynamicPage(name:"manualAgentEntry", title:"Manually Configure Simple Sync", nextPage:"verifyManualEntry", install:false, uninstall:true) {
472         section("Manually Configure Simple Sync")
473         {
474                 paragraph "In the event that Simple Sync cannot be automatically discovered by your SmartThings hub, you may enter Simple Sync's IP address here."
475             input(name: "manualIPAddress", type: "text", title: "IP Address", required: true)
476         }
477     }
478 }
479
480 def verifyManualEntry()
481 {
482     def hexIP = convertIPToHexString(manualIPAddress)
483     def hexPort = convertToHexString(47147)
484     def uuid = "593C03D2-1DA9-4CDB-A335-6C6DC98E56C3"
485     def hubId = ""
486     
487     for (hub in location.hubs)
488     {
489         if (hub.localIP != null)
490         {
491                 hubId = hub.id
492             break
493         }
494     }
495     
496     def manualAgent = [deviceType: "04",
497                                         mac: "unknown",
498                                         ip: hexIP,
499                         port: hexPort,
500                         ssdpPath: "/upnp/Roomie.xml",
501                         ssdpUSN: "uuid:$uuid::urn:roomieremote-com:device:roomie:1",
502                         hub: hubId,
503                         verified: true,
504                         name: "Simple Sync $manualIPAddress"]
505         
506     state.agents[uuid] = manualAgent
507     
508     addOrUpdateAgent(state.agents[uuid])
509     
510     dynamicPage(name: "verifyManualEntry", title: "Manual Configuration Complete", nextPage: "", install:true, uninstall:true) {
511         section("")
512         {
513                 paragraph("Tap Done to complete the installation process.")
514         }
515     }
516 }
517
518 def discoverAgents()
519 {
520     def urn = getURN()
521     
522     //sendHubCommand(new physicalgraph.device.HubAction("lan discovery $urn", physicalgraph.device.Protocol.LAN))
523 }
524
525 def agentsDiscovered()
526 {
527     def gAgents = getAgents()
528     def agents = gAgents.findAll { it?.value?.verified == true }
529     def map = [:]
530     agents.each
531     {
532         map["${it.value.uuid}"] = it.value.name
533     }
534     map
535 }
536
537 def getAgents()
538 {
539     if (!state.agents)
540     {
541         state.agents = [:]
542     }
543     
544     state.agents
545 }
546
547 def installed()
548 {
549         initialize()
550 }
551
552 def updated()
553 {
554         initialize()
555 }
556
557 def initialize()
558 {
559         if (state.subscribe)
560         {
561         unsubscribe()
562                 state.subscribe = false
563         }
564     
565     if (selectedAgent)
566     {
567         addOrUpdateAgent(state.agents[selectedAgent])
568     }
569 }
570
571 def addOrUpdateAgent(agent)
572 {
573         def children = getChildDevices()
574         def dni = agent.ip + ":" + agent.port
575     def found = false
576         
577         children.each
578         {
579                 if ((it.getDeviceDataByName("mac") == agent.mac))
580                 {
581                 found = true
582             
583             if (it.getDeviceNetworkId() != dni)
584             {
585                                 it.setDeviceNetworkId(dni)
586                         }
587                 }
588         else if (it.getDeviceNetworkId() == dni)
589         {
590                 found = true
591         }
592         }
593     
594         if (!found)
595         {
596         addChildDevice("roomieremote-agent", "Simple Sync", dni, agent.hub, [label: "Simple Sync"])
597         }
598 }
599
600 def locationHandler(evt)
601 {
602     def description = evt?.description
603     def urn = getURN()
604     def hub = evt?.hubId
605     def parsedEvent = parseEventMessage(description)
606     
607     parsedEvent?.putAt("hub", hub)
608     
609     //SSDP DISCOVERY EVENTS
610         if (parsedEvent?.ssdpTerm?.contains(urn))
611         {
612         def agent = parsedEvent
613         def ip = convertHexToIP(agent.ip)
614         def agents = getAgents()
615         
616         agent.verified = true
617         agent.name = "Simple Sync $ip"
618         
619         if (!agents[agent.uuid])
620         {
621                 state.agents[agent.uuid] = agent
622         }
623     }
624 }
625
626 private def parseEventMessage(String description)
627 {
628         def event = [:]
629         def parts = description.split(',')
630     
631         parts.each
632     { part ->
633                 part = part.trim()
634                 if (part.startsWith('devicetype:'))
635         {
636                         def valueString = part.split(":")[1].trim()
637                         event.devicetype = valueString
638                 }
639                 else if (part.startsWith('mac:'))
640         {
641                         def valueString = part.split(":")[1].trim()
642                         if (valueString)
643             {
644                                 event.mac = valueString
645                         }
646                 }
647                 else if (part.startsWith('networkAddress:'))
648         {
649                         def valueString = part.split(":")[1].trim()
650                         if (valueString)
651             {
652                                 event.ip = valueString
653                         }
654                 }
655                 else if (part.startsWith('deviceAddress:'))
656         {
657                         def valueString = part.split(":")[1].trim()
658                         if (valueString)
659             {
660                                 event.port = valueString
661                         }
662                 }
663                 else if (part.startsWith('ssdpPath:'))
664         {
665                         def valueString = part.split(":")[1].trim()
666                         if (valueString)
667             {
668                                 event.ssdpPath = valueString
669                         }
670                 }
671                 else if (part.startsWith('ssdpUSN:'))
672         {
673                         part -= "ssdpUSN:"
674                         def valueString = part.trim()
675                         if (valueString)
676             {
677                                 event.ssdpUSN = valueString
678                 
679                 def uuid = getUUIDFromUSN(valueString)
680                 
681                 if (uuid)
682                 {
683                         event.uuid = uuid
684                 }
685                         }
686                 }
687                 else if (part.startsWith('ssdpTerm:'))
688         {
689                         part -= "ssdpTerm:"
690                         def valueString = part.trim()
691                         if (valueString)
692             {
693                                 event.ssdpTerm = valueString
694                         }
695                 }
696                 else if (part.startsWith('headers'))
697         {
698                         part -= "headers:"
699                         def valueString = part.trim()
700                         if (valueString)
701             {
702                                 event.headers = valueString
703                         }
704                 }
705                 else if (part.startsWith('body'))
706         {
707                         part -= "body:"
708                         def valueString = part.trim()
709                         if (valueString)
710             {
711                                 event.body = valueString
712                         }
713                 }
714         }
715
716         event
717 }
718
719 def getURN()
720 {
721     return "urn:roomieremote-com:device:roomie:1"
722 }
723
724 def getUUIDFromUSN(usn)
725 {
726         def parts = usn.split(":")
727         
728         for (int i = 0; i < parts.size(); ++i)
729         {
730                 if (parts[i] == "uuid")
731                 {
732                         return parts[i + 1]
733                 }
734         }
735 }
736
737 def String convertHexToIP(hex)
738 {
739         [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
740 }
741
742 def Integer convertHexToInt(hex)
743 {
744         Integer.parseInt(hex,16)
745 }
746
747 def String convertToHexString(n)
748 {
749         String hex = String.format("%X", n.toInteger())
750 }
751
752 def String convertIPToHexString(ipString)
753 {
754         String hex = ipString.tokenize(".").collect {
755         String.format("%02X", it.toInteger())
756     }.join()
757 }
758
759 def Boolean canInstallLabs()
760 {
761     return hasAllHubsOver("000.011.00603")
762 }
763
764 def Boolean hasAllHubsOver(String desiredFirmware)
765 {
766     return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
767 }
768
769 def List getRealHubFirmwareVersions()
770 {
771     return location.hubs*.firmwareVersionString.findAll { it }
772 }
773
774