2 * Harmony (Connect) - https://developer.Harmony.com/documentation
4 * Copyright 2015 SmartThings
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:
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 * For complete set of capabilities, attributes, and commands see:
19 * https://graph.api.smartthings.com/ide/doc/capabilities
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 * ---------------------+-------------------+-----------------------------+------------------------------------
43 include 'asynchttp_v1'
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"],
57 appSetting "clientSecret"
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
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"] }
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}" }
104 def description = null
105 if (!state.HarmonyAccessToken) {
106 if (!state.accessToken) {
107 log.debug "Harmony - About to create access token"
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 }
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
122 def huboptions = state.HarmonyHubs ?: []
123 def actoptions = state.HarmonyActivities ?: []
125 def numFoundHub = huboptions.size() ?: 0
126 def numFoundAct = actoptions.size() ?: 0
128 if((deviceRefreshCount % 5) == 0) {
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
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
142 section("Connection to the hub timed out. Please restart the hub and try again.") {}
148 def redirectUrl = null
149 if (params.authQueryString) {
150 redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
151 log.debug "Harmony - redirectUrl: ${redirectUrl}"
153 log.warn "Harmony - No authQueryString"
156 if (state.HarmonyAccessToken) {
157 log.debug "Harmony - Access token already exists"
161 def code = params.code
163 if (code.size() > 6) {
165 log.debug "Harmony - Exchanging code for access token"
166 receiveToken(redirectUrl)
168 // Initiate the Harmony OAuth flow.
172 log.debug "Harmony - This code should be unreachable"
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)}")
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 ]
188 uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
191 httpPost(params) { response ->
192 state.HarmonyAccessToken = response.data.access_token
194 } catch (java.util.concurrent.TimeoutException e) {
196 log.warn "Harmony - Connection timed out, please try again later."
199 if (state.HarmonyAccessToken) {
208 <p>Your Harmony Account is now connected to SmartThings!</p>
209 <p>Click 'Done' to finish setup.</p>
211 connectionStatus(message)
216 <p>The connection could not be established!</p>
218 <p>Click 'Done' to return to the menu.</p>
220 connectionStatus(message)
223 def receivedToken() {
225 <p>Your Harmony Account is already connected to SmartThings!</p>
226 <p>Click 'Done' to finish setup.</p>
228 connectionStatus(message)
231 def connectionStatus(message, redirectUrl = null) {
232 def redirectHtml = ""
235 <meta http-equiv="refresh" content="3; url=${redirectUrl}" />
243 <meta name="viewport" content="width=640">
244 <title>SmartThings Connection</title>
245 <style type="text/css">
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');
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');
269 /*background: #eee;*/
273 vertical-align: middle;
280 font-family: 'Swiss 721 W01 Thin';
292 font-family: 'Swiss 721 W01 Light';
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" />
307 render contentType: 'text/html', data: html
310 String toQueryString(Map m) {
311 return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
314 def buildRedirectUrl(page) {
315 return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
319 if (!state.accessToken) {
320 log.debug "Harmony - About to create access token"
328 if (!state.accessToken) {
329 log.debug "Harmony - About to create access token"
337 if (state.HarmonyAccessToken) {
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}"
349 if (selectedhubs || selectedactivities) {
351 runEvery5Minutes("poll")
356 def getHarmonydevices() {
357 state.Harmonydevices ?: []
360 Map discoverDevices() {
361 log.trace "Harmony - Discovering devices..."
363 if (getHarmonydevices() != []) {
364 def devices = state.Harmonydevices.hubs
365 log.trace devices.toString()
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
379 state.HarmonyHubs = hubs
380 state.HarmonyActivities = activities
384 //CHILD DEVICE METHODS
386 def Params = [auth: state.HarmonyAccessToken]
387 def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
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
395 log.debug "Harmony - Error: $response.status"
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"
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
407 log.info "Harmony - Error: $e"
413 log.trace "Harmony - Adding Hubs"
414 selectedhubs.each { dni ->
415 def d = getChildDevice(dni)
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"
422 log.trace "Harmony - Found ${d.displayName} with id $dni already exists"
425 log.trace "Harmony - Adding Activities"
426 selectedactivities.each { dni ->
427 def d = getChildDevice(dni)
429 def newAction = state.HarmonyActivities.find { it.key == dni }
431 d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"])
432 log.trace "Harmony - Created ${d.displayName} with id $dni"
436 log.trace "Harmony - Found ${d.displayName} with id $dni already exists"
441 def activity(dni,mode) {
442 def tokenParam = [auth: state.HarmonyAccessToken]
445 url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(tokenParam)}"
447 def aux = dni.split('-')
449 if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
450 url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(tokenParam)}"
452 def activityId = aux[2]
453 url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(tokenParam)}"
458 contentType: 'application/json'
460 asynchttp_v1.post('activityResponse', params)
461 return "Command Sent"
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"
472 if (response.status == 200) {
473 log.trace "Harmony - Command sent succesfully"
476 log.trace "Harmony - Command failed. Error: $response.status"
482 // GET THE LIST OF ACTIVITIES
483 if (state.HarmonyAccessToken) {
484 def tokenParam = [auth: state.HarmonyAccessToken]
486 uri: "https://home.myharmony.com/cloudapi/state?${toQueryString(tokenParam)}",
487 headers: ["Accept": "application/json"],
488 contentType: 'application/json'
490 asynchttp_v1.get('pollResponse', params)
492 log.warn "Harmony - Access token has expired"
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"
506 // json response already parsed into JSONElement object
507 ResponseValues = response.json
509 log.error "Harmony - error parsing json from response: $e"
511 if (ResponseValues) {
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}")
522 if (it.value.response.data.currentAvActivity == "-1") {
523 hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", displayed: false)
526 def activityDTH = getChildDevice("harmony-${it.key}-${it.value.response.data.currentAvActivity}")
528 currentActivity = activityDTH.device.displayName
530 currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
531 hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", displayed: false)
535 log.trace "Harmony - error response: $it.value.message"
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] }
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")
550 runIn(5, "poll", [overwrite: true])
552 childDevice?.sendEvent(name: "switch", value: "off")
554 runIn(5, "poll", [overwrite: true])
560 log.debug "Harmony - did not get json results from response body: $response.data"
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)}"
570 httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
571 response.data.hubs.each {
572 def hub = getChildDevice("harmony-${it.key}")
574 def hubname = getHubName("${it.key}")
576 def aux = it.value.response.data.activities.size()
578 activities = it.value.response.data.activities.collect {
579 [id: it.key, name: it.value['name'], type: it.value['type']]
581 activities += [id: "off", name: "Activity OFF", type: "0"]
583 hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", displayed: false)
587 } catch (groovyx.net.http.HttpResponseException e) {
589 } catch (java.net.SocketTimeoutException e) {
591 } catch(Exception e) {
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)}"
604 httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
605 actname = response.data.data.activities[activity].name
607 } catch(Exception e) {
614 def getActivityId(activity,hubId) {
615 // GET ACTIVITY'S NAME
617 if (state.HarmonyAccessToken) {
618 def Params = [auth: state.HarmonyAccessToken]
619 def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
621 httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
622 response.data.data.activities.each {
623 if (it.value.name == activity)
627 } catch(Exception e) {
634 def getHubName(hubId) {
637 if (state.HarmonyAccessToken) {
638 def Params = [auth: state.HarmonyAccessToken]
639 def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}"
641 httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
642 hubname = response.data.data.name
644 } catch(Exception e) {
651 def sendNotification(msg) {
652 sendNotification(msg)
655 def hookEventHandler() {
656 // log.debug "In hookEventHandler method."
657 log.debug "Harmony - request = ${request}"
659 def json = request.JSON
661 def html = """{"code":200,"message":"OK"}"""
662 render contentType: 'application/json', data: html
666 log.debug "Harmony - getDevices(), params: ${params}"
673 log.debug "Harmony - getDevice(), params: ${params}"
674 def device = allDevices.find { it.id == params.id }
676 render status: 404, data: '{"msg": "Device not found"}'
683 def data = request.JSON
684 def command = data.command
685 def arguments = data.arguments
686 log.debug "Harmony - updateDevice(), params: ${params}, request: ${data}"
688 render status: 400, data: '{"msg": "command is required"}'
690 def device = allDevices.find { it.id == params.id }
692 if (validateCommand(device, command)) {
694 device."$command"(*arguments)
698 render status: 204, data: "{}"
700 render status: 403, data: '{"msg": "Access denied. This command is not supported by current capability."}'
703 render status: 404, data: '{"msg": "Device not found"}'
709 * Validating the command passed by the user based on capability.
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
718 // Handling other device types here, which don't accept commands
719 httpError(400, "Bad request.")
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
728 def getCapabilityName(device) {
730 if (switches.find{it.id == device.id})
732 else if (alarms.find{it.id == device.id})
734 else if (locks.find{it.id == device.id})
736 log.trace "Device: $device - Capability Name: $capName"
741 * Constructing the map over here of
742 * supported commands by device capability
743 * @return a map of device capability -> supported commands
745 def getDeviceCapabilityCommands(deviceCapabilities) {
747 deviceCapabilities.collect {
748 map[it.name] = it.commands.collect{ it.name.toString() }
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]
759 deviceId: it.device.id,
760 attributeName: it.data,
763 if (!state.harmonyHubs) {
764 response.callbackUrl = deviceInfo?.callbackUrl
770 def addSubscription() {
771 def data = request.JSON
772 def attribute = data.attributeName
773 def callbackUrl = data.callbackUrl
775 log.debug "Harmony - addSubscription, params: ${params}, request: ${data}"
777 render status: 400, data: '{"msg": "attributeName is required"}'
779 def device = allDevices.find { it.id == data.deviceId }
781 if (!state.harmonyHubs) {
782 log.debug "Harmony - Adding callbackUrl: $callbackUrl"
783 state[device.id] = [callbackUrl: callbackUrl]
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' }
793 deviceId: subscription.device.id,
794 attributeName: subscription.data,
795 handler: subscription.handler
797 if (!state.harmonyHubs) {
798 response.callbackUrl = callbackUrl
802 render status: 400, data: '{"msg": "Device not found"}'
807 def removeSubscription() {
808 def subscription = app.subscriptions?.find { it.id == params.id }
809 def device = subscription?.device
811 log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}"
813 log.debug "Harmony - Removing subscription for device: ${device.id}"
814 state.remove(device.id)
817 render status: 204, data: "{}"
821 location.helloHome.getPhrases()?.collect {[
827 def executePhrase() {
828 log.debug "executedPhrase, params: ${params}"
829 location.helloHome.execute(params.id)
830 render status: 204, data: "{}"
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)
840 } else if (deviceInfo) {
841 if (deviceInfo.callbackUrl) {
842 sendToHarmony(evt, deviceInfo.callbackUrl)
844 log.warn "Harmony - No callbackUrl set for device: ${evt.deviceId}"
847 log.warn "Harmony - No subscribed device found for device: ${evt.deviceId}"
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(
861 "Content-Type": "application/json"
863 body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
868 body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
871 log.debug "Harmony - Sending data to Harmony Cloud: $params"
872 httpPostJson(params) { resp ->
873 log.debug "Harmony - Cloud Response: ${resp.status}"
876 log.error "Harmony - Cloud Something went wrong: $e"
882 location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.collect { hubItem(it) }
886 def hub = location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.find { it.id == params.id }
888 render status: 404, data: '{"msg": "Hub not found"}'
894 def activityCallback() {
895 def data = request.JSON
896 def device = getChildDevice(params.dni)
898 if (data.errorCode == "200") {
899 device.setCurrentActivity(data.currentActivityId)
901 log.warn "Harmony - Activity callback error: ${data}"
904 log.warn "Harmony - Activity callback sent to non-existant dni: ${params.dni}"
906 render status: 200, data: '{"msg": "Successfully received callbackUrl"}'
910 state.harmonyHubs ?: []
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 }
919 harmonyHub.mac = data.mac
920 harmonyHub.callbackUrl = data.callbackUrl
921 harmonyHub.name = data.name
923 state.harmonyHubs << [mac: data.mac, callbackUrl: data.callbackUrl, name: data.name]
925 render status: 200, data: '{"msg": "Successfully received Harmony data"}'
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"}'
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 }
941 log.debug "Harmony - Deleting Harmony hub with mac: ${params.mac}"
942 state.harmonyHubs.remove(harmonyHub)
944 log.debug "Harmony - Couldn't find Harmony hub with mac: ${params.mac}"
946 render status: 204, data: "{}"
949 private getAllDevices() {
950 ([] + switches + motionSensors + contactSensors + thermostats + presenceSensors + temperatureSensors + accelerationSensors + waterSensors + lightSensors + humiditySensors + alarms + locks)?.findAll()?.unique { it.id }
953 private deviceItem(device) {
956 label: device.displayName,
957 currentStates: device.currentStates,
958 capabilities: device.capabilities?.collect {[
961 attributes: device.supportedAttributes?.collect {[
963 dataType: it.dataType,
966 commands: device.supportedCommands?.collect {[
968 arguments: it.arguments
971 name: device.typeName,
972 author: device.typeAuthor
977 private hubItem(hub) {
982 port: hub.localSrvPortTCP