Update notify-me-with-hue.groovy
[smartapps.git] / official / logitech-harmony-connect.groovy
1 /**
2  *  Harmony (Connect) - https://developer.Harmony.com/documentation
3  *
4  *  Copyright 2015 SmartThings
5  *
6  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7  *  in compliance with the License. You may obtain a copy of the License at:
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13  *  for the specific language governing permissions and limitations under the License.
14  *
15  *  Author: SmartThings
16  *
17  *  For complete set of capabilities, attributes, and commands see:
18  *
19  *  https://graph.api.smartthings.com/ide/doc/capabilities
20  *
21  *  ---------------------+-------------------+-----------------------------+------------------------------------
22  *  Device Type          | Attribute Name    | Commands                    | Attribute Values
23  *  ---------------------+-------------------+-----------------------------+------------------------------------
24  *  switches             | switch            | on, off                     | on, off
25  *  motionSensors        | motion            |                             | active, inactive
26  *  contactSensors       | contact           |                             | open, closed
27  *  thermostat           | thermostat        | setHeatingSetpoint,         | temperature, heatingSetpoint
28  *                       |                   | setCoolingSetpoint(number)  | coolingSetpoint, thermostatSetpoint
29  *                       |                   | off, heat, emergencyHeat    | thermostatMode — ["emergency heat", "auto", "cool", "off", "heat"]
30  *                       |                   | cool, setThermostatMode     | thermostatFanMode — ["auto", "on", "circulate"]
31  *                       |                   | fanOn, fanAuto, fanCirculate| thermostatOperatingState — ["cooling", "heating", "pending heat",
32  *                       |                   | setThermostatFanMode, auto  | "fan only", "vent economizer", "pending cool", "idle"]
33  *  presenceSensors      | presence          |                             | present, 'not present'
34  *  temperatureSensors   | temperature       |                             | <numeric, F or C according to unit>
35  *  accelerationSensors  | acceleration      |                             | active, inactive
36  *  waterSensors         | water             |                             | wet, dry
37  *  lightSensors         | illuminance       |                             | <numeric, lux>
38  *  humiditySensors      | humidity          |                             | <numeric, percent>
39  *  alarms               | alarm             | strobe, siren, both, off    | strobe, siren, both, off
40  *  locks                | lock              | lock, unlock                | locked, unlocked
41  *  ---------------------+-------------------+-----------------------------+------------------------------------
42  */
43 include 'asynchttp_v1'
44
45 definition(
46     name: "Logitech Harmony (Connect)",
47     namespace: "smartthings",
48     author: "SmartThings",
49     description: "Allows you to integrate your Logitech Harmony account with SmartThings.",
50     category: "SmartThings Labs",
51     iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
52     iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png",
53     oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"],
54     singleInstance: true
55 ){
56         appSetting "clientId"
57         appSetting "clientSecret"
58 }
59
60 preferences(oauthPage: "deviceAuthorization") {
61   page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization")
62         page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) {
63                 section("Allow Logitech Harmony to control these things...") {
64                         input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
65                         input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false
66                         input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false
67       input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
68                         input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false
69                         input "temperatureSensors", "capability.temperatureMeasurement", title: "Which Temperature Sensors?", multiple: true, required: false
70                         input "accelerationSensors", "capability.accelerationSensor", title: "Which Vibration Sensors?", multiple: true, required: false
71                         input "waterSensors", "capability.waterSensor", title: "Which Water Sensors?", multiple: true, required: false
72                         input "lightSensors", "capability.illuminanceMeasurement", title: "Which Light Sensors?", multiple: true, required: false
73                         input "humiditySensors", "capability.relativeHumidityMeasurement", title: "Which Relative Humidity Sensors?", multiple: true, required: false
74                         input "alarms", "capability.alarm", title: "Which Sirens?", multiple: true, required: false
75                         input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
76                 }
77         }
78 }
79
80 mappings {
81         path("/devices") { action: [ GET: "listDevices"] }
82         path("/devices/:id") { action: [ GET: "getDevice", PUT: "updateDevice"] }
83         path("/subscriptions") { action: [ GET: "listSubscriptions", POST: "addSubscription"] }
84         path("/subscriptions/:id") { action: [ DELETE: "removeSubscription"] }
85         path("/phrases") { action: [ GET: "listPhrases"] }
86         path("/phrases/:id") { action: [ PUT: "executePhrase"] }
87         path("/hubs") { action: [ GET: "listHubs" ] }
88         path("/hubs/:id") { action: [ GET: "getHub" ] }
89         path("/activityCallback/:dni") { action: [ POST: "activityCallback" ] }
90         path("/harmony") { action: [ GET: "getHarmony", POST: "harmony" ] }
91         path("/harmony/:mac") { action: [ DELETE: "deleteHarmony" ] }
92         path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
93         path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
94         path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
95         path("/oauth/callback") { action: [ GET: "callback" ] }
96         path("/oauth/initialize") { action: [ GET: "init"] }
97 }
98
99 def getServerUrl() { return "https://graph.api.smartthings.com" }
100 def getServercallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" }
101 def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
102
103 def authPage() {
104     def description = null
105     if (!state.HarmonyAccessToken) {
106                 if (!state.accessToken) {
107                         log.debug "Harmony - About to create access token"
108                         createAccessToken()
109                 }
110         description = "Click to enter Harmony Credentials"
111         def redirectUrl = buildRedirectUrl
112         return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) {
113             section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
114             section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
115         }
116     } else {
117                 //device discovery request every 5 //25 seconds
118                 int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
119                 state.deviceRefreshCount = deviceRefreshCount + 1
120                 def refreshInterval = 5
121
122                 def huboptions = state.HarmonyHubs ?: []
123                 def actoptions = state.HarmonyActivities ?: []
124
125                 def numFoundHub = huboptions.size() ?: 0
126     def numFoundAct = actoptions.size() ?: 0
127
128                 if((deviceRefreshCount % 5) == 0) {
129                         discoverDevices()
130                 }
131
132                 return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
133                         section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
134                                 input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, submitOnChange: true, options:huboptions
135                         }
136       // Virtual activity flag
137       if (numFoundHub > 0 && numFoundAct > 0 && true)
138                         section("You can also add activities as virtual switches for other convenient integrations") {
139                                 input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, submitOnChange: true, options:actoptions
140                         }
141     if (state.resethub)
142                         section("Connection to the hub timed out. Please restart the hub and try again.") {}
143                 }
144     }
145 }
146
147 def callback() {
148         def redirectUrl = null
149         if (params.authQueryString) {
150                 redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
151                 log.debug "Harmony - redirectUrl: ${redirectUrl}"
152         } else {
153                 log.warn "Harmony - No authQueryString"
154         }
155
156         if (state.HarmonyAccessToken) {
157                 log.debug "Harmony - Access token already exists"
158                 discovery()
159                 success()
160         } else {
161                 def code = params.code
162                 if (code) {
163                         if (code.size() > 6) {
164                                 // Harmony code
165                                 log.debug "Harmony - Exchanging code for access token"
166                                 receiveToken(redirectUrl)
167                         } else {
168                                 // Initiate the Harmony OAuth flow.
169                                 init()
170                         }
171                 } else {
172                         log.debug "Harmony - This code should be unreachable"
173                         success()
174                 }
175         }
176 }
177
178 def init() {
179         log.debug "Harmony - Requesting Code"
180         def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${servercallbackUrl}" ]
181         redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
182 }
183
184 def receiveToken(redirectUrl = null) {
185         log.debug "Harmony - receiveToken"
186     def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code ]
187     def params = [
188       uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
189     ]
190         try {
191         httpPost(params) { response ->
192             state.HarmonyAccessToken = response.data.access_token
193         }
194         } catch (java.util.concurrent.TimeoutException e) {
195         fail(e)
196                 log.warn "Harmony - Connection timed out, please try again later."
197         }
198     discovery()
199         if (state.HarmonyAccessToken) {
200                 success()
201         } else {
202                 fail("")
203         }
204 }
205
206 def success() {
207         def message = """
208                 <p>Your Harmony Account is now connected to SmartThings!</p>
209                 <p>Click 'Done' to finish setup.</p>
210         """
211         connectionStatus(message)
212 }
213
214 def fail(msg) {
215     def message = """
216         <p>The connection could not be established!</p>
217         <p>$msg</p>
218         <p>Click 'Done' to return to the menu.</p>
219     """
220     connectionStatus(message)
221 }
222
223 def receivedToken() {
224         def message = """
225                 <p>Your Harmony Account is already connected to SmartThings!</p>
226                 <p>Click 'Done' to finish setup.</p>
227         """
228         connectionStatus(message)
229 }
230
231 def connectionStatus(message, redirectUrl = null) {
232         def redirectHtml = ""
233         if (redirectUrl) {
234                 redirectHtml = """
235                         <meta http-equiv="refresh" content="3; url=${redirectUrl}" />
236                 """
237         }
238
239     def html = """
240         <!DOCTYPE html>
241         <html>
242         <head>
243         <meta name="viewport" content="width=640">
244         <title>SmartThings Connection</title>
245         <style type="text/css">
246             @font-face {
247                 font-family: 'Swiss 721 W01 Thin';
248                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
249                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
250                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
251                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
252                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
253                 font-weight: normal;
254                 font-style: normal;
255             }
256             @font-face {
257                 font-family: 'Swiss 721 W01 Light';
258                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
259                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
260                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
261                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
262                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
263                 font-weight: normal;
264                 font-style: normal;
265             }
266             .container {
267                 width: 560px;
268                 padding: 40px;
269                 /*background: #eee;*/
270                 text-align: center;
271             }
272             img {
273                 vertical-align: middle;
274             }
275             img:nth-child(2) {
276                 margin: 0 30px;
277             }
278             p {
279                 font-size: 2.2em;
280                 font-family: 'Swiss 721 W01 Thin';
281                 text-align: center;
282                 color: #666666;
283                 padding: 0 40px;
284                 margin-bottom: 0;
285             }
286         /*
287             p:last-child {
288                 margin-top: 0px;
289             }
290         */
291             span {
292                 font-family: 'Swiss 721 W01 Light';
293             }
294         </style>
295                 ${redirectHtml}
296         </head>
297         <body>
298             <div class="container">
299                 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/harmony@2x.png" alt="Harmony icon" />
300                 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
301                 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
302                 ${message}
303             </div>
304         </body>
305         </html>
306         """
307         render contentType: 'text/html', data: html
308 }
309
310 String toQueryString(Map m) {
311         return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
312 }
313
314 def buildRedirectUrl(page) {
315     return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
316 }
317
318 def installed() {
319         if (!state.accessToken) {
320                 log.debug "Harmony - About to create access token"
321                 createAccessToken()
322         } else {
323                 initialize()
324         }
325 }
326
327 def updated() {
328         if (!state.accessToken) {
329                 log.debug "Harmony - About to create access token"
330                 createAccessToken()
331         } else {
332                 initialize()
333         }
334 }
335
336 def uninstalled() {
337         if (state.HarmonyAccessToken) {
338                 try {
339                 state.HarmonyAccessToken = ""
340                 log.debug "Harmony - Success disconnecting Harmony from SmartThings"
341                 } catch (groovyx.net.http.HttpResponseException e) {
342                         log.error "Harmony - Error disconnecting Harmony from SmartThings: ${e.statusCode}"
343                 }
344         }
345 }
346
347 def initialize() {
348         state.aux = 0
349         if (selectedhubs || selectedactivities) {
350                 addDevice()
351     runEvery5Minutes("poll")
352     getActivityList()
353         }
354 }
355
356 def getHarmonydevices() {
357         state.Harmonydevices ?: []
358 }
359
360 Map discoverDevices() {
361     log.trace "Harmony - Discovering devices..."
362     discovery()
363     if (getHarmonydevices() != []) {
364         def devices = state.Harmonydevices.hubs
365         log.trace devices.toString()
366         def activities = [:]
367         def hubs = [:]
368         devices.each {
369                 def hubkey = it.key
370             def hubname = getHubName(it.key)
371             def hubvalue = "${hubname}"
372             hubs["harmony-${hubkey}"] = hubvalue
373                   it.value.response.data.activities.each {
374                 def value = "${it.value.name}"
375                 def key = "harmony-${hubkey}-${it.key}"
376                 activities["${key}"] = value
377            }
378         }
379         state.HarmonyHubs = hubs
380         state.HarmonyActivities = activities
381     }
382 }
383
384 //CHILD DEVICE METHODS
385 def discovery() {
386     def Params = [auth: state.HarmonyAccessToken]
387     def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
388         try {
389                 httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
390                 if (response.status == 200) {
391                 log.debug "Harmony - valid Token"
392                 state.Harmonydevices = response.data
393                 state.resethub = false
394                 } else {
395                 log.debug "Harmony - Error: $response.status"
396             }
397             }
398         } catch (groovyx.net.http.HttpResponseException e) {
399         if (e.statusCode == 401) { // token is expired
400             state.remove("HarmonyAccessToken")
401             log.warn "Harmony - Harmony Access token has expired"
402         }
403         } catch (java.net.SocketTimeoutException e) {
404                 log.warn "Harmony - Connection to the hub timed out. Please restart the hub and try again."
405     state.resethub = true
406         } catch (e) {
407     log.info "Harmony - Error: $e"
408         }
409     return null
410 }
411
412 def addDevice() {
413     log.trace "Harmony - Adding Hubs"
414     selectedhubs.each { dni ->
415         def d = getChildDevice(dni)
416         if(!d) {
417             def newAction = state.HarmonyHubs.find { it.key == dni }
418             d = addChildDevice("smartthings", "Logitech Harmony Hub C2C", dni, null, [label:"${newAction.value}"])
419             log.trace "Harmony - Created ${d.displayName} with id $dni"
420             poll()
421         } else {
422             log.trace "Harmony - Found ${d.displayName} with id $dni already exists"
423         }
424     }
425     log.trace "Harmony - Adding Activities"
426     selectedactivities.each { dni ->
427         def d = getChildDevice(dni)
428         if(!d) {
429             def newAction = state.HarmonyActivities.find { it.key == dni }
430             if (newAction) {
431                     d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"])
432                     log.trace "Harmony - Created ${d.displayName} with id $dni"
433                     poll()
434             }
435         } else {
436             log.trace "Harmony - Found ${d.displayName} with id $dni already exists"
437         }
438     }
439 }
440
441 def activity(dni,mode) {
442     def tokenParam = [auth: state.HarmonyAccessToken]
443     def url
444     if (dni == "all") {
445         url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(tokenParam)}"
446     } else {
447         def aux = dni.split('-')
448         def hubId = aux[1]
449         if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
450                 url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(tokenParam)}"
451         } else {
452           def activityId = aux[2]
453                 url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(tokenParam)}"
454         }
455         }
456   def params = [
457       uri: url,
458       contentType: 'application/json'
459   ]
460   asynchttp_v1.post('activityResponse', params)
461   return "Command Sent"
462 }
463
464 def activityResponse(response, data) {
465   if (response.hasError()) {
466     log.error "Harmony - response has error: $response.errorMessage"
467     if (response.status == 401) { // token is expired
468       state.remove("HarmonyAccessToken")
469       log.warn "Harmony - Access token has expired"
470     }
471   } else {
472     if (response.status == 200) {
473       log.trace "Harmony - Command sent succesfully"
474       poll()
475     } else {
476       log.trace "Harmony - Command failed. Error: $response.status"
477     }
478   }
479 }
480
481 def poll() {
482         // GET THE LIST OF ACTIVITIES
483     if (state.HarmonyAccessToken) {
484         def tokenParam = [auth: state.HarmonyAccessToken]
485         def params = [
486             uri: "https://home.myharmony.com/cloudapi/state?${toQueryString(tokenParam)}",
487             headers: ["Accept": "application/json"],
488             contentType: 'application/json'
489         ]
490         asynchttp_v1.get('pollResponse', params)
491       } else {
492         log.warn "Harmony - Access token has expired"
493       }
494 }
495
496 def pollResponse(response, data) {
497         if (response.hasError()) {
498     log.error "Harmony - response has error: $response.errorMessage"
499     if (response.status == 401) { // token is expired
500       state.remove("HarmonyAccessToken")
501       log.warn "Harmony - Access token has expired"
502     }
503         } else {
504                         def ResponseValues
505                         try {
506                                         // json response already parsed into JSONElement object
507                                         ResponseValues = response.json
508                         } catch (e) {
509                                         log.error "Harmony - error parsing json from response: $e"
510                         }
511                         if (ResponseValues) {
512         def map = [:]
513         ResponseValues.hubs.each {
514         // Device-Watch relies on the Logitech Harmony Cloud to get the Device state.
515         def isAlive = it.value.status
516         def d = getChildDevice("harmony-${it.key}")
517         d?.sendEvent(name: "DeviceWatch-DeviceStatus", value: isAlive!=504? "online":"offline", displayed: false, isStateChange: true)
518         if (it.value.message == "OK") {
519               map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}"
520               def hub = getChildDevice("harmony-${it.key}")
521               if (hub) {
522                   if (it.value.response.data.currentAvActivity == "-1") {
523                       hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", displayed: false)
524                   } else {
525                       def currentActivity
526                       def activityDTH = getChildDevice("harmony-${it.key}-${it.value.response.data.currentAvActivity}")
527                       if (activityDTH)
528                         currentActivity = activityDTH.device.displayName
529                       else
530                         currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
531                       hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", displayed: false)
532                   }
533               }
534           } else {
535             log.trace "Harmony - error response: $it.value.message"
536           }
537         }
538         def activities = getChildDevices()
539         def activitynotrunning = true
540         activities.each { activity ->
541             def act = activity.deviceNetworkId.split('-')
542             if (act.size() > 2) {
543                 def aux = map.find { it.key == act[1] }
544                 if (aux) {
545                     def aux2 = aux.value.split(',')
546                     def childDevice = getChildDevice(activity.deviceNetworkId)
547                     if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
548                         childDevice?.sendEvent(name: "switch", value: "on")
549                         if (aux2[1] == "1")
550                             runIn(5, "poll", [overwrite: true])
551                     } else {
552                         childDevice?.sendEvent(name: "switch", value: "off")
553                         if (aux2[1] == "3")
554                             runIn(5, "poll", [overwrite: true])
555                     }
556                 }
557             }
558         }
559                         } else {
560                                         log.debug "Harmony - did not get json results from response body: $response.data"
561                         }
562         }
563 }
564
565 def getActivityList() {
566     if (state.HarmonyAccessToken) {
567         def Params = [auth: state.HarmonyAccessToken]
568         def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
569         try {
570             httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
571                 response.data.hubs.each {
572                     def hub = getChildDevice("harmony-${it.key}")
573                     if (hub) {
574                         def hubname = getHubName("${it.key}")
575                         def activities = []
576                         def aux = it.value.response.data.activities.size()
577                         if (aux >= 1) {
578                             activities = it.value.response.data.activities.collect {
579                                 [id: it.key, name: it.value['name'], type: it.value['type']]
580                             }
581                             activities += [id: "off", name: "Activity OFF", type: "0"]
582                         }
583                         hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", displayed: false)
584                                                   }
585                 }
586             }
587         } catch (groovyx.net.http.HttpResponseException e) {
588                 log.trace e
589         } catch (java.net.SocketTimeoutException e) {
590                 log.trace e
591                     } catch(Exception e) {
592                 log.trace e
593                     }
594     }
595 }
596
597 def getActivityName(activity,hubId) {
598         // GET ACTIVITY'S NAME
599     def actname = activity
600     if (state.HarmonyAccessToken) {
601         def Params = [auth: state.HarmonyAccessToken]
602         def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
603         try {
604             httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
605                 actname = response.data.data.activities[activity].name
606             }
607                 } catch(Exception e) {
608                 log.trace e
609                 }
610     }
611         return actname
612 }
613
614 def getActivityId(activity,hubId) {
615         // GET ACTIVITY'S NAME
616     def actid = activity
617     if (state.HarmonyAccessToken) {
618         def Params = [auth: state.HarmonyAccessToken]
619         def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
620         try {
621             httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
622                 response.data.data.activities.each {
623                         if (it.value.name == activity)
624                                 actid = it.key
625                 }
626             }
627                 } catch(Exception e) {
628                 log.trace e
629                 }
630     }
631         return actid
632 }
633
634 def getHubName(hubId) {
635         // GET HUB'S NAME
636     def hubname = hubId
637     if (state.HarmonyAccessToken) {
638         def Params = [auth: state.HarmonyAccessToken]
639         def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}"
640         try {
641             httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
642                 hubname = response.data.data.name
643             }
644                 } catch(Exception e) {
645                 log.trace e
646                 }
647     }
648         return hubname
649 }
650
651 def sendNotification(msg) {
652         sendNotification(msg)
653 }
654
655 def hookEventHandler() {
656     // log.debug "In hookEventHandler method."
657     log.debug "Harmony - request = ${request}"
658
659     def json = request.JSON
660
661         def html = """{"code":200,"message":"OK"}"""
662         render contentType: 'application/json', data: html
663 }
664
665 def listDevices() {
666         log.debug "Harmony - getDevices(), params: ${params}"
667         allDevices.collect {
668                 deviceItem(it)
669         }
670 }
671
672 def getDevice() {
673         log.debug "Harmony - getDevice(), params: ${params}"
674         def device = allDevices.find { it.id == params.id }
675         if (!device) {
676                 render status: 404, data: '{"msg": "Device not found"}'
677         } else {
678                 deviceItem(device)
679         }
680 }
681
682 def updateDevice() {
683         def data = request.JSON
684         def command = data.command
685         def arguments = data.arguments
686         log.debug "Harmony - updateDevice(), params: ${params}, request: ${data}"
687         if (!command) {
688                 render status: 400, data: '{"msg": "command is required"}'
689         } else {
690                 def device = allDevices.find { it.id == params.id }
691     if (device) {
692         if (validateCommand(device, command)) {
693             if (arguments) {
694                 device."$command"(*arguments)
695             } else {
696                 device."$command"()
697             }
698             render status: 204, data: "{}"
699         } else {
700           render status: 403, data: '{"msg": "Access denied. This command is not supported by current capability."}'
701         }
702     } else {
703       render status: 404, data: '{"msg": "Device not found"}'
704     }
705   }
706 }
707
708 /**
709  * Validating the command passed by the user based on capability.
710  * @return boolean
711  */
712 def validateCommand(device, command) {
713         def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
714         def currentDeviceCapability = getCapabilityName(device)
715         if (currentDeviceCapability != "" && capabilityCommands[currentDeviceCapability]) {
716     return (command in capabilityCommands[currentDeviceCapability] || (currentDeviceCapability == "Switch" && command == "setLevel" && device.hasCommand("setLevel"))) ? true : false
717         } else {
718                 // Handling other device types here, which don't accept commands
719                 httpError(400, "Bad request.")
720         }
721 }
722
723 /**
724  * Need to get the attribute name to do the lookup. Only
725  * doing it for the device types which accept commands
726  * @return attribute name of the device type
727  */
728 def getCapabilityName(device) {
729     def capName = ""
730     if (switches.find{it.id == device.id})
731                         capName = "Switch"
732                 else if (alarms.find{it.id == device.id})
733                         capName = "Alarm"
734                 else if (locks.find{it.id == device.id})
735                         capName = "Lock"
736     log.trace "Device: $device - Capability Name: $capName"
737                 return capName
738 }
739
740 /**
741  * Constructing the map over here of
742  * supported commands by device capability
743  * @return a map of device capability -> supported commands
744  */
745 def getDeviceCapabilityCommands(deviceCapabilities) {
746         def map = [:]
747         deviceCapabilities.collect {
748                 map[it.name] = it.commands.collect{ it.name.toString() }
749         }
750         return map
751 }
752
753 def listSubscriptions() {
754         log.debug "Harmony - listSubscriptions()"
755         app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect {
756                 def deviceInfo = state[it.device.id]
757                 def response = [
758                         id: it.id,
759                         deviceId: it.device.id,
760                         attributeName: it.data,
761                         handler: it.handler
762                 ]
763                 if (!state.harmonyHubs) {
764                         response.callbackUrl = deviceInfo?.callbackUrl
765                 }
766                 response
767         } ?: []
768 }
769
770 def addSubscription() {
771         def data = request.JSON
772         def attribute = data.attributeName
773         def callbackUrl = data.callbackUrl
774
775         log.debug "Harmony - addSubscription, params: ${params}, request: ${data}"
776         if (!attribute) {
777                 render status: 400, data: '{"msg": "attributeName is required"}'
778         } else {
779                 def device = allDevices.find { it.id == data.deviceId }
780                 if (device) {
781                         if (!state.harmonyHubs) {
782                                 log.debug "Harmony - Adding callbackUrl: $callbackUrl"
783                                 state[device.id] = [callbackUrl: callbackUrl]
784                         }
785                         log.debug "Harmony - Adding subscription"
786                         def subscription = subscribe(device, attribute, deviceHandler)
787                         if (!subscription || !subscription.eventSubscription) {
788                                 subscription = app.subscriptions?.find { it.device?.device && it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' }
789                         }
790
791                         def response = [
792                                 id: subscription.id,
793                                 deviceId: subscription.device.id,
794                                 attributeName: subscription.data,
795                                 handler: subscription.handler
796                         ]
797                         if (!state.harmonyHubs) {
798                                 response.callbackUrl = callbackUrl
799                         }
800                         response
801                 } else {
802                         render status: 400, data: '{"msg": "Device not found"}'
803                 }
804         }
805 }
806
807 def removeSubscription() {
808         def subscription = app.subscriptions?.find { it.id == params.id }
809         def device = subscription?.device
810
811         log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}"
812         if (device) {
813                 log.debug "Harmony - Removing subscription for device: ${device.id}"
814                 state.remove(device.id)
815                 unsubscribe(device)
816         }
817         render status: 204, data: "{}"
818 }
819
820 def listPhrases() {
821         location.helloHome.getPhrases()?.collect {[
822                 id: it.id,
823                 label: it.label
824         ]}
825 }
826
827 def executePhrase() {
828         log.debug "executedPhrase, params: ${params}"
829         location.helloHome.execute(params.id)
830         render status: 204, data: "{}"
831 }
832
833 def deviceHandler(evt) {
834         def deviceInfo = state[evt.deviceId]
835         if (state.harmonyHubs) {
836                 state.harmonyHubs.each { harmonyHub ->
837       log.trace "Harmony - Sending data to $harmonyHub.name"
838                         sendToHarmony(evt, harmonyHub.callbackUrl)
839                 }
840         } else if (deviceInfo) {
841                 if (deviceInfo.callbackUrl) {
842                         sendToHarmony(evt, deviceInfo.callbackUrl)
843                 } else {
844                         log.warn "Harmony - No callbackUrl set for device: ${evt.deviceId}"
845                 }
846         } else {
847                 log.warn "Harmony - No subscribed device found for device: ${evt.deviceId}"
848         }
849 }
850
851 def sendToHarmony(evt, String callbackUrl) {
852   def callback = new URI(callbackUrl)
853   if (callback.port != -1) {
854         def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
855         def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
856         sendHubCommand(new physicalgraph.device.HubAction(
857                 method: "POST",
858                 path: path,
859                 headers: [
860                         "Host": host,
861                         "Content-Type": "application/json"
862                 ],
863                 body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
864         ))
865   } else {
866     def params = [
867       uri: callbackUrl,
868       body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
869     ]
870     try {
871         log.debug "Harmony - Sending data to Harmony Cloud: $params"
872         httpPostJson(params) { resp ->
873             log.debug "Harmony - Cloud Response: ${resp.status}"
874         }
875     } catch (e) {
876         log.error "Harmony - Cloud Something went wrong: $e"
877     }
878   }
879 }
880
881 def listHubs() {
882         location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.collect { hubItem(it) }
883 }
884
885 def getHub() {
886         def hub = location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.find { it.id == params.id }
887         if (!hub) {
888                 render status: 404, data: '{"msg": "Hub not found"}'
889         } else {
890                 hubItem(hub)
891         }
892 }
893
894 def activityCallback() {
895         def data = request.JSON
896         def device = getChildDevice(params.dni)
897         if (device) {
898                 if (data.errorCode == "200") {
899                         device.setCurrentActivity(data.currentActivityId)
900                 } else {
901                         log.warn "Harmony - Activity callback error: ${data}"
902                 }
903         } else {
904                 log.warn "Harmony - Activity callback sent to non-existant dni: ${params.dni}"
905         }
906         render status: 200, data: '{"msg": "Successfully received callbackUrl"}'
907 }
908
909 def getHarmony() {
910         state.harmonyHubs ?: []
911 }
912
913 def harmony() {
914         def data = request.JSON
915         if (data.mac && data.callbackUrl && data.name) {
916                 if (!state.harmonyHubs) { state.harmonyHubs = [] }
917                 def harmonyHub = state.harmonyHubs.find { it.mac == data.mac }
918                 if (harmonyHub) {
919                         harmonyHub.mac = data.mac
920                         harmonyHub.callbackUrl = data.callbackUrl
921                         harmonyHub.name = data.name
922                 } else {
923                         state.harmonyHubs << [mac: data.mac, callbackUrl: data.callbackUrl, name: data.name]
924                 }
925                 render status: 200, data: '{"msg": "Successfully received Harmony data"}'
926         } else {
927                 if (!data.mac) {
928                         render status: 400, data: '{"msg": "mac is required"}'
929                 } else if (!data.callbackUrl) {
930                         render status: 400, data: '{"msg": "callbackUrl is required"}'
931                 } else if (!data.name) {
932                         render status: 400, data: '{"msg": "name is required"}'
933                 }
934         }
935 }
936
937 def deleteHarmony() {
938         log.debug "Harmony - Trying to delete Harmony hub with mac: ${params.mac}"
939         def harmonyHub = state.harmonyHubs?.find { it.mac == params.mac }
940         if (harmonyHub) {
941                 log.debug "Harmony - Deleting Harmony hub with mac: ${params.mac}"
942                 state.harmonyHubs.remove(harmonyHub)
943         } else {
944                 log.debug "Harmony - Couldn't find Harmony hub with mac: ${params.mac}"
945         }
946         render status: 204, data: "{}"
947 }
948
949 private getAllDevices() {
950         ([] + switches + motionSensors + contactSensors + thermostats + presenceSensors + temperatureSensors + accelerationSensors + waterSensors + lightSensors + humiditySensors + alarms + locks)?.findAll()?.unique { it.id }
951 }
952
953 private deviceItem(device) {
954         [
955                 id: device.id,
956                 label: device.displayName,
957                 currentStates: device.currentStates,
958                 capabilities: device.capabilities?.collect {[
959                         name: it.name
960                 ]},
961                 attributes: device.supportedAttributes?.collect {[
962                         name: it.name,
963                         dataType: it.dataType,
964                         values: it.values
965                 ]},
966                 commands: device.supportedCommands?.collect {[
967                         name: it.name,
968                         arguments: it.arguments
969                 ]},
970                 type: [
971                         name: device.typeName,
972                         author: device.typeAuthor
973                 ]
974         ]
975 }
976
977 private hubItem(hub) {
978         [
979                 id: hub.id,
980                 name: hub.name,
981                 ip: hub.localIP,
982                 port: hub.localSrvPortTCP
983         ]
984 }