/** * Harmony (Connect) - https://developer.Harmony.com/documentation * * Copyright 2015 SmartThings * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * Author: SmartThings * * For complete set of capabilities, attributes, and commands see: * * https://graph.api.smartthings.com/ide/doc/capabilities * * ---------------------+-------------------+-----------------------------+------------------------------------ * Device Type | Attribute Name | Commands | Attribute Values * ---------------------+-------------------+-----------------------------+------------------------------------ * switches | switch | on, off | on, off * motionSensors | motion | | active, inactive * contactSensors | contact | | open, closed * thermostat | thermostat | setHeatingSetpoint, | temperature, heatingSetpoint * | | setCoolingSetpoint(number) | coolingSetpoint, thermostatSetpoint * | | off, heat, emergencyHeat | thermostatMode — ["emergency heat", "auto", "cool", "off", "heat"] * | | cool, setThermostatMode | thermostatFanMode — ["auto", "on", "circulate"] * | | fanOn, fanAuto, fanCirculate| thermostatOperatingState — ["cooling", "heating", "pending heat", * | | setThermostatFanMode, auto | "fan only", "vent economizer", "pending cool", "idle"] * presenceSensors | presence | | present, 'not present' * temperatureSensors | temperature | | * accelerationSensors | acceleration | | active, inactive * waterSensors | water | | wet, dry * lightSensors | illuminance | | * humiditySensors | humidity | | * alarms | alarm | strobe, siren, both, off | strobe, siren, both, off * locks | lock | lock, unlock | locked, unlocked * ---------------------+-------------------+-----------------------------+------------------------------------ */ include 'asynchttp_v1' definition( name: "Logitech Harmony (Connect)", namespace: "smartthings", author: "SmartThings", description: "Allows you to integrate your Logitech Harmony account with SmartThings.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png", oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"], singleInstance: true ){ appSetting "clientId" appSetting "clientSecret" } preferences(oauthPage: "deviceAuthorization") { page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization") page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) { section("Allow Logitech Harmony to control these things...") { input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false input "temperatureSensors", "capability.temperatureMeasurement", title: "Which Temperature Sensors?", multiple: true, required: false input "accelerationSensors", "capability.accelerationSensor", title: "Which Vibration Sensors?", multiple: true, required: false input "waterSensors", "capability.waterSensor", title: "Which Water Sensors?", multiple: true, required: false input "lightSensors", "capability.illuminanceMeasurement", title: "Which Light Sensors?", multiple: true, required: false input "humiditySensors", "capability.relativeHumidityMeasurement", title: "Which Relative Humidity Sensors?", multiple: true, required: false input "alarms", "capability.alarm", title: "Which Sirens?", multiple: true, required: false input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false } } } mappings { path("/devices") { action: [ GET: "listDevices"] } path("/devices/:id") { action: [ GET: "getDevice", PUT: "updateDevice"] } path("/subscriptions") { action: [ GET: "listSubscriptions", POST: "addSubscription"] } path("/subscriptions/:id") { action: [ DELETE: "removeSubscription"] } path("/phrases") { action: [ GET: "listPhrases"] } path("/phrases/:id") { action: [ PUT: "executePhrase"] } path("/hubs") { action: [ GET: "listHubs" ] } path("/hubs/:id") { action: [ GET: "getHub" ] } path("/activityCallback/:dni") { action: [ POST: "activityCallback" ] } path("/harmony") { action: [ GET: "getHarmony", POST: "harmony" ] } path("/harmony/:mac") { action: [ DELETE: "deleteHarmony" ] } path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] } path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] } path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] } path("/oauth/callback") { action: [ GET: "callback" ] } path("/oauth/initialize") { action: [ GET: "init"] } } def getServerUrl() { return "https://graph.api.smartthings.com" } def getServercallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" } def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" } def authPage() { def description = null if (!state.HarmonyAccessToken) { if (!state.accessToken) { log.debug "Harmony - About to create access token" createAccessToken() } description = "Click to enter Harmony Credentials" def redirectUrl = buildRedirectUrl return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) { 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." } section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description } } } else { //device discovery request every 5 //25 seconds int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int state.deviceRefreshCount = deviceRefreshCount + 1 def refreshInterval = 5 def huboptions = state.HarmonyHubs ?: [] def actoptions = state.HarmonyActivities ?: [] def numFoundHub = huboptions.size() ?: 0 def numFoundAct = actoptions.size() ?: 0 if((deviceRefreshCount % 5) == 0) { discoverDevices() } return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { 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.") { input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, submitOnChange: true, options:huboptions } // Virtual activity flag if (numFoundHub > 0 && numFoundAct > 0 && true) section("You can also add activities as virtual switches for other convenient integrations") { input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, submitOnChange: true, options:actoptions } if (state.resethub) section("Connection to the hub timed out. Please restart the hub and try again.") {} } } } def callback() { def redirectUrl = null if (params.authQueryString) { redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", "")) log.debug "Harmony - redirectUrl: ${redirectUrl}" } else { log.warn "Harmony - No authQueryString" } if (state.HarmonyAccessToken) { log.debug "Harmony - Access token already exists" discovery() success() } else { def code = params.code if (code) { if (code.size() > 6) { // Harmony code log.debug "Harmony - Exchanging code for access token" receiveToken(redirectUrl) } else { // Initiate the Harmony OAuth flow. init() } } else { log.debug "Harmony - This code should be unreachable" success() } } } def init() { log.debug "Harmony - Requesting Code" def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${servercallbackUrl}" ] redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}") } def receiveToken(redirectUrl = null) { log.debug "Harmony - receiveToken" def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code ] def params = [ uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}", ] try { httpPost(params) { response -> state.HarmonyAccessToken = response.data.access_token } } catch (java.util.concurrent.TimeoutException e) { fail(e) log.warn "Harmony - Connection timed out, please try again later." } discovery() if (state.HarmonyAccessToken) { success() } else { fail("") } } def success() { def message = """

Your Harmony Account is now connected to SmartThings!

Click 'Done' to finish setup.

""" connectionStatus(message) } def fail(msg) { def message = """

The connection could not be established!

$msg

Click 'Done' to return to the menu.

""" connectionStatus(message) } def receivedToken() { def message = """

Your Harmony Account is already connected to SmartThings!

Click 'Done' to finish setup.

""" connectionStatus(message) } def connectionStatus(message, redirectUrl = null) { def redirectHtml = "" if (redirectUrl) { redirectHtml = """ """ } def html = """ SmartThings Connection ${redirectHtml}
Harmony icon connected device icon SmartThings logo ${message}
""" render contentType: 'text/html', data: html } String toQueryString(Map m) { return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") } def buildRedirectUrl(page) { return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}" } def installed() { if (!state.accessToken) { log.debug "Harmony - About to create access token" createAccessToken() } else { initialize() } } def updated() { if (!state.accessToken) { log.debug "Harmony - About to create access token" createAccessToken() } else { initialize() } } def uninstalled() { if (state.HarmonyAccessToken) { try { state.HarmonyAccessToken = "" log.debug "Harmony - Success disconnecting Harmony from SmartThings" } catch (groovyx.net.http.HttpResponseException e) { log.error "Harmony - Error disconnecting Harmony from SmartThings: ${e.statusCode}" } } } def initialize() { state.aux = 0 if (selectedhubs || selectedactivities) { addDevice() runEvery5Minutes("poll") getActivityList() } } def getHarmonydevices() { state.Harmonydevices ?: [] } Map discoverDevices() { log.trace "Harmony - Discovering devices..." discovery() if (getHarmonydevices() != []) { def devices = state.Harmonydevices.hubs log.trace devices.toString() def activities = [:] def hubs = [:] devices.each { def hubkey = it.key def hubname = getHubName(it.key) def hubvalue = "${hubname}" hubs["harmony-${hubkey}"] = hubvalue it.value.response.data.activities.each { def value = "${it.value.name}" def key = "harmony-${hubkey}-${it.key}" activities["${key}"] = value } } state.HarmonyHubs = hubs state.HarmonyActivities = activities } } //CHILD DEVICE METHODS def discovery() { def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" try { httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> if (response.status == 200) { log.debug "Harmony - valid Token" state.Harmonydevices = response.data state.resethub = false } else { log.debug "Harmony - Error: $response.status" } } } catch (groovyx.net.http.HttpResponseException e) { if (e.statusCode == 401) { // token is expired state.remove("HarmonyAccessToken") log.warn "Harmony - Harmony Access token has expired" } } catch (java.net.SocketTimeoutException e) { log.warn "Harmony - Connection to the hub timed out. Please restart the hub and try again." state.resethub = true } catch (e) { log.info "Harmony - Error: $e" } return null } def addDevice() { log.trace "Harmony - Adding Hubs" selectedhubs.each { dni -> def d = getChildDevice(dni) if(!d) { def newAction = state.HarmonyHubs.find { it.key == dni } d = addChildDevice("smartthings", "Logitech Harmony Hub C2C", dni, null, [label:"${newAction.value}"]) log.trace "Harmony - Created ${d.displayName} with id $dni" poll() } else { log.trace "Harmony - Found ${d.displayName} with id $dni already exists" } } log.trace "Harmony - Adding Activities" selectedactivities.each { dni -> def d = getChildDevice(dni) if(!d) { def newAction = state.HarmonyActivities.find { it.key == dni } if (newAction) { d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) log.trace "Harmony - Created ${d.displayName} with id $dni" poll() } } else { log.trace "Harmony - Found ${d.displayName} with id $dni already exists" } } } def activity(dni,mode) { def tokenParam = [auth: state.HarmonyAccessToken] def url if (dni == "all") { url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(tokenParam)}" } else { def aux = dni.split('-') def hubId = aux[1] if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){ url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(tokenParam)}" } else { def activityId = aux[2] url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(tokenParam)}" } } def params = [ uri: url, contentType: 'application/json' ] asynchttp_v1.post('activityResponse', params) return "Command Sent" } def activityResponse(response, data) { if (response.hasError()) { log.error "Harmony - response has error: $response.errorMessage" if (response.status == 401) { // token is expired state.remove("HarmonyAccessToken") log.warn "Harmony - Access token has expired" } } else { if (response.status == 200) { log.trace "Harmony - Command sent succesfully" poll() } else { log.trace "Harmony - Command failed. Error: $response.status" } } } def poll() { // GET THE LIST OF ACTIVITIES if (state.HarmonyAccessToken) { def tokenParam = [auth: state.HarmonyAccessToken] def params = [ uri: "https://home.myharmony.com/cloudapi/state?${toQueryString(tokenParam)}", headers: ["Accept": "application/json"], contentType: 'application/json' ] asynchttp_v1.get('pollResponse', params) } else { log.warn "Harmony - Access token has expired" } } def pollResponse(response, data) { if (response.hasError()) { log.error "Harmony - response has error: $response.errorMessage" if (response.status == 401) { // token is expired state.remove("HarmonyAccessToken") log.warn "Harmony - Access token has expired" } } else { def ResponseValues try { // json response already parsed into JSONElement object ResponseValues = response.json } catch (e) { log.error "Harmony - error parsing json from response: $e" } if (ResponseValues) { def map = [:] ResponseValues.hubs.each { // Device-Watch relies on the Logitech Harmony Cloud to get the Device state. def isAlive = it.value.status def d = getChildDevice("harmony-${it.key}") d?.sendEvent(name: "DeviceWatch-DeviceStatus", value: isAlive!=504? "online":"offline", displayed: false, isStateChange: true) if (it.value.message == "OK") { map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}" def hub = getChildDevice("harmony-${it.key}") if (hub) { if (it.value.response.data.currentAvActivity == "-1") { hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", displayed: false) } else { def currentActivity def activityDTH = getChildDevice("harmony-${it.key}-${it.value.response.data.currentAvActivity}") if (activityDTH) currentActivity = activityDTH.device.displayName else currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key) hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", displayed: false) } } } else { log.trace "Harmony - error response: $it.value.message" } } def activities = getChildDevices() def activitynotrunning = true activities.each { activity -> def act = activity.deviceNetworkId.split('-') if (act.size() > 2) { def aux = map.find { it.key == act[1] } if (aux) { def aux2 = aux.value.split(',') def childDevice = getChildDevice(activity.deviceNetworkId) if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) { childDevice?.sendEvent(name: "switch", value: "on") if (aux2[1] == "1") runIn(5, "poll", [overwrite: true]) } else { childDevice?.sendEvent(name: "switch", value: "off") if (aux2[1] == "3") runIn(5, "poll", [overwrite: true]) } } } } } else { log.debug "Harmony - did not get json results from response body: $response.data" } } } def getActivityList() { if (state.HarmonyAccessToken) { def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" try { httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> response.data.hubs.each { def hub = getChildDevice("harmony-${it.key}") if (hub) { def hubname = getHubName("${it.key}") def activities = [] def aux = it.value.response.data.activities.size() if (aux >= 1) { activities = it.value.response.data.activities.collect { [id: it.key, name: it.value['name'], type: it.value['type']] } activities += [id: "off", name: "Activity OFF", type: "0"] } hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", displayed: false) } } } } catch (groovyx.net.http.HttpResponseException e) { log.trace e } catch (java.net.SocketTimeoutException e) { log.trace e } catch(Exception e) { log.trace e } } } def getActivityName(activity,hubId) { // GET ACTIVITY'S NAME def actname = activity if (state.HarmonyAccessToken) { def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" try { httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> actname = response.data.data.activities[activity].name } } catch(Exception e) { log.trace e } } return actname } def getActivityId(activity,hubId) { // GET ACTIVITY'S NAME def actid = activity if (state.HarmonyAccessToken) { def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" try { httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> response.data.data.activities.each { if (it.value.name == activity) actid = it.key } } } catch(Exception e) { log.trace e } } return actid } def getHubName(hubId) { // GET HUB'S NAME def hubname = hubId if (state.HarmonyAccessToken) { def Params = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}" try { httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> hubname = response.data.data.name } } catch(Exception e) { log.trace e } } return hubname } def sendNotification(msg) { sendNotification(msg) } def hookEventHandler() { // log.debug "In hookEventHandler method." log.debug "Harmony - request = ${request}" def json = request.JSON def html = """{"code":200,"message":"OK"}""" render contentType: 'application/json', data: html } def listDevices() { log.debug "Harmony - getDevices(), params: ${params}" allDevices.collect { deviceItem(it) } } def getDevice() { log.debug "Harmony - getDevice(), params: ${params}" def device = allDevices.find { it.id == params.id } if (!device) { render status: 404, data: '{"msg": "Device not found"}' } else { deviceItem(device) } } def updateDevice() { def data = request.JSON def command = data.command def arguments = data.arguments log.debug "Harmony - updateDevice(), params: ${params}, request: ${data}" if (!command) { render status: 400, data: '{"msg": "command is required"}' } else { def device = allDevices.find { it.id == params.id } if (device) { if (validateCommand(device, command)) { if (arguments) { device."$command"(*arguments) } else { device."$command"() } render status: 204, data: "{}" } else { render status: 403, data: '{"msg": "Access denied. This command is not supported by current capability."}' } } else { render status: 404, data: '{"msg": "Device not found"}' } } } /** * Validating the command passed by the user based on capability. * @return boolean */ def validateCommand(device, command) { def capabilityCommands = getDeviceCapabilityCommands(device.capabilities) def currentDeviceCapability = getCapabilityName(device) if (currentDeviceCapability != "" && capabilityCommands[currentDeviceCapability]) { return (command in capabilityCommands[currentDeviceCapability] || (currentDeviceCapability == "Switch" && command == "setLevel" && device.hasCommand("setLevel"))) ? true : false } else { // Handling other device types here, which don't accept commands httpError(400, "Bad request.") } } /** * Need to get the attribute name to do the lookup. Only * doing it for the device types which accept commands * @return attribute name of the device type */ def getCapabilityName(device) { def capName = "" if (switches.find{it.id == device.id}) capName = "Switch" else if (alarms.find{it.id == device.id}) capName = "Alarm" else if (locks.find{it.id == device.id}) capName = "Lock" log.trace "Device: $device - Capability Name: $capName" return capName } /** * Constructing the map over here of * supported commands by device capability * @return a map of device capability -> supported commands */ def getDeviceCapabilityCommands(deviceCapabilities) { def map = [:] deviceCapabilities.collect { map[it.name] = it.commands.collect{ it.name.toString() } } return map } def listSubscriptions() { log.debug "Harmony - listSubscriptions()" app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect { def deviceInfo = state[it.device.id] def response = [ id: it.id, deviceId: it.device.id, attributeName: it.data, handler: it.handler ] if (!state.harmonyHubs) { response.callbackUrl = deviceInfo?.callbackUrl } response } ?: [] } def addSubscription() { def data = request.JSON def attribute = data.attributeName def callbackUrl = data.callbackUrl log.debug "Harmony - addSubscription, params: ${params}, request: ${data}" if (!attribute) { render status: 400, data: '{"msg": "attributeName is required"}' } else { def device = allDevices.find { it.id == data.deviceId } if (device) { if (!state.harmonyHubs) { log.debug "Harmony - Adding callbackUrl: $callbackUrl" state[device.id] = [callbackUrl: callbackUrl] } log.debug "Harmony - Adding subscription" def subscription = subscribe(device, attribute, deviceHandler) if (!subscription || !subscription.eventSubscription) { subscription = app.subscriptions?.find { it.device?.device && it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' } } def response = [ id: subscription.id, deviceId: subscription.device.id, attributeName: subscription.data, handler: subscription.handler ] if (!state.harmonyHubs) { response.callbackUrl = callbackUrl } response } else { render status: 400, data: '{"msg": "Device not found"}' } } } def removeSubscription() { def subscription = app.subscriptions?.find { it.id == params.id } def device = subscription?.device log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}" if (device) { log.debug "Harmony - Removing subscription for device: ${device.id}" state.remove(device.id) unsubscribe(device) } render status: 204, data: "{}" } def listPhrases() { location.helloHome.getPhrases()?.collect {[ id: it.id, label: it.label ]} } def executePhrase() { log.debug "executedPhrase, params: ${params}" location.helloHome.execute(params.id) render status: 204, data: "{}" } def deviceHandler(evt) { def deviceInfo = state[evt.deviceId] if (state.harmonyHubs) { state.harmonyHubs.each { harmonyHub -> log.trace "Harmony - Sending data to $harmonyHub.name" sendToHarmony(evt, harmonyHub.callbackUrl) } } else if (deviceInfo) { if (deviceInfo.callbackUrl) { sendToHarmony(evt, deviceInfo.callbackUrl) } else { log.warn "Harmony - No callbackUrl set for device: ${evt.deviceId}" } } else { log.warn "Harmony - No subscribed device found for device: ${evt.deviceId}" } } def sendToHarmony(evt, String callbackUrl) { def callback = new URI(callbackUrl) if (callback.port != -1) { def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path sendHubCommand(new physicalgraph.device.HubAction( method: "POST", path: path, headers: [ "Host": host, "Content-Type": "application/json" ], body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] )) } else { def params = [ uri: callbackUrl, body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] ] try { log.debug "Harmony - Sending data to Harmony Cloud: $params" httpPostJson(params) { resp -> log.debug "Harmony - Cloud Response: ${resp.status}" } } catch (e) { log.error "Harmony - Cloud Something went wrong: $e" } } } def listHubs() { location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.collect { hubItem(it) } } def getHub() { def hub = location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.find { it.id == params.id } if (!hub) { render status: 404, data: '{"msg": "Hub not found"}' } else { hubItem(hub) } } def activityCallback() { def data = request.JSON def device = getChildDevice(params.dni) if (device) { if (data.errorCode == "200") { device.setCurrentActivity(data.currentActivityId) } else { log.warn "Harmony - Activity callback error: ${data}" } } else { log.warn "Harmony - Activity callback sent to non-existant dni: ${params.dni}" } render status: 200, data: '{"msg": "Successfully received callbackUrl"}' } def getHarmony() { state.harmonyHubs ?: [] } def harmony() { def data = request.JSON if (data.mac && data.callbackUrl && data.name) { if (!state.harmonyHubs) { state.harmonyHubs = [] } def harmonyHub = state.harmonyHubs.find { it.mac == data.mac } if (harmonyHub) { harmonyHub.mac = data.mac harmonyHub.callbackUrl = data.callbackUrl harmonyHub.name = data.name } else { state.harmonyHubs << [mac: data.mac, callbackUrl: data.callbackUrl, name: data.name] } render status: 200, data: '{"msg": "Successfully received Harmony data"}' } else { if (!data.mac) { render status: 400, data: '{"msg": "mac is required"}' } else if (!data.callbackUrl) { render status: 400, data: '{"msg": "callbackUrl is required"}' } else if (!data.name) { render status: 400, data: '{"msg": "name is required"}' } } } def deleteHarmony() { log.debug "Harmony - Trying to delete Harmony hub with mac: ${params.mac}" def harmonyHub = state.harmonyHubs?.find { it.mac == params.mac } if (harmonyHub) { log.debug "Harmony - Deleting Harmony hub with mac: ${params.mac}" state.harmonyHubs.remove(harmonyHub) } else { log.debug "Harmony - Couldn't find Harmony hub with mac: ${params.mac}" } render status: 204, data: "{}" } private getAllDevices() { ([] + switches + motionSensors + contactSensors + thermostats + presenceSensors + temperatureSensors + accelerationSensors + waterSensors + lightSensors + humiditySensors + alarms + locks)?.findAll()?.unique { it.id } } private deviceItem(device) { [ id: device.id, label: device.displayName, currentStates: device.currentStates, capabilities: device.capabilities?.collect {[ name: it.name ]}, attributes: device.supportedAttributes?.collect {[ name: it.name, dataType: it.dataType, values: it.values ]}, commands: device.supportedCommands?.collect {[ name: it.name, arguments: it.arguments ]}, type: [ name: device.typeName, author: device.typeAuthor ] ] } private hubItem(hub) { [ id: hub.id, name: hub.name, ip: hub.localIP, port: hub.localSrvPortTCP ] }