Merge pull request #261 from juano2310/Harmony_Global

Harmony Global Oauth
This commit is contained in:
Juan Pablo Risso
2015-11-05 16:03:09 -05:00
2 changed files with 164 additions and 79 deletions

View File

@@ -0,0 +1,81 @@
/**
* Logitech Harmony Activity
*
* Copyright 2015 Juan Risso
*
* 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.
*
*/
metadata {
definition (name: "Logitech Harmony Activity", namespace: "smartthings", author: "Juan Risso") {
capability "Switch"
capability "Actuator"
capability "Refresh"
command "huboff"
command "alloff"
command "refresh"
}
// simulator metadata
simulator {
}
// UI tile definitions
tiles {
standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "off", label: 'Off', action: "switch.on", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#ffffff", nextState: "on"
state "on", label: 'On', action: "switch.off", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#79b821", nextState: "off"
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("forceoff", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'Force End', action:"switch.off", icon:"st.secondary.off"
}
standardTile("huboff", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'End Hub Action', action:"huboff", icon:"st.harmony.harmony-hub-icon"
}
standardTile("alloff", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'All Actions', action:"alloff", icon:"st.secondary.off"
}
main "button"
details(["button", "refresh", "forceoff", "huboff", "alloff"])
}
}
def parse(String description) {
}
def on() {
sendEvent(name: "switch", value: "on")
log.trace parent.activity(device.deviceNetworkId,"start")
}
def off() {
sendEvent(name: "switch", value: "off")
log.trace parent.activity(device.deviceNetworkId,"end")
}
def huboff() {
sendEvent(name: "switch", value: "off")
log.trace parent.activity(device.deviceNetworkId,"hub")
}
def alloff() {
sendEvent(name: "switch", value: "off")
log.trace parent.activity("all","end")
}
def refresh() {
log.debug "Executing 'refresh'"
log.trace parent.poll()
}

View File

@@ -34,7 +34,7 @@
* locks | lock | lock, unlock | locked, unlocked * locks | lock | lock, unlock | locked, unlocked
* ---------------------+-------------------+-----------------------------+------------------------------------ * ---------------------+-------------------+-----------------------------+------------------------------------
*/ */
definition( definition(
name: "Logitech Harmony (Connect)", name: "Logitech Harmony (Connect)",
namespace: "smartthings", namespace: "smartthings",
@@ -48,7 +48,6 @@ definition(
){ ){
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"
appSetting "callbackUrl"
} }
preferences(oauthPage: "deviceAuthorization") { preferences(oauthPage: "deviceAuthorization") {
@@ -90,16 +89,18 @@ mappings {
} }
def getServerUrl() { return "https://graph.api.smartthings.com" } def getServerUrl() { return "https://graph.api.smartthings.com" }
def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" }
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${apiServerUrl}" }
def authPage() { def authPage() {
def description = null def description = null
if (!state.HarmonyAccessToken) { if (!state.HarmonyAccessToken) {
if (!state.accessToken) { if (!state.accessToken) {
log.debug "About to create access token" log.debug "About to create access token"
createAccessToken() createAccessToken()
} }
description = "Click to enter Harmony Credentials" description = "Click to enter Harmony Credentials"
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" def redirectUrl = buildRedirectUrl
return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) { return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) {
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description } section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
} }
@@ -111,7 +112,7 @@ def authPage() {
def huboptions = state.HarmonyHubs ?: [] def huboptions = state.HarmonyHubs ?: []
def actoptions = state.HarmonyActivities ?: [] def actoptions = state.HarmonyActivities ?: []
def numFoundHub = huboptions.size() ?: 0 def numFoundHub = huboptions.size() ?: 0
def numFoundAct = actoptions.size() ?: 0 def numFoundAct = actoptions.size() ?: 0
if((deviceRefreshCount % 5) == 0) { if((deviceRefreshCount % 5) == 0) {
@@ -121,13 +122,14 @@ def authPage() {
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.") { 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, options:huboptions input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions
} }
if (numFoundHub > 0 && numFoundAct > 0 && false) // Virtual activity flag
if (numFoundHub > 0 && numFoundAct > 0 && true)
section("You can also add activities as virtual switches for other convenient integrations") { 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, options:actoptions input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions
} }
if (state.resethub) if (state.resethub)
section("Connection to the hub timed out. Please restart the hub and try again.") {} section("Connection to the hub timed out. Please restart the hub and try again.") {}
} }
} }
} }
@@ -139,7 +141,7 @@ def callback() {
} else { } else {
log.warn "No authQueryString" log.warn "No authQueryString"
} }
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
log.debug "Access token already exists" log.debug "Access token already exists"
discovery() discovery()
@@ -164,8 +166,8 @@ def callback() {
def init() { def init() {
log.debug "Requesting Code" log.debug "Requesting Code"
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${appSettings.callbackUrl}" ] def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${callbackUrl}" ]
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}") redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
} }
def receiveToken(redirectUrl = null) { def receiveToken(redirectUrl = null) {
@@ -175,7 +177,7 @@ def receiveToken(redirectUrl = null) {
uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}", uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
] ]
try { try {
httpPost(params) { response -> httpPost(params) { response ->
state.HarmonyAccessToken = response.data.access_token state.HarmonyAccessToken = response.data.access_token
} }
} catch (java.util.concurrent.TimeoutException e) { } catch (java.util.concurrent.TimeoutException e) {
@@ -222,7 +224,7 @@ def connectionStatus(message, redirectUrl = null) {
<meta http-equiv="refresh" content="3; url=${redirectUrl}" /> <meta http-equiv="refresh" content="3; url=${redirectUrl}" />
""" """
} }
def html = """ def html = """
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@@ -303,30 +305,28 @@ def buildRedirectUrl(page) {
} }
def installed() { def installed() {
enableCallback()
if (!state.accessToken) { if (!state.accessToken) {
log.debug "About to create access token" log.debug "About to create access token"
createAccessToken() createAccessToken()
} else { } else {
initialize() initialize()
} }
} }
def updated() { def updated() {
unsubscribe() unsubscribe()
unschedule() unschedule()
enableCallback()
if (!state.accessToken) { if (!state.accessToken) {
log.debug "About to create access token" log.debug "About to create access token"
createAccessToken() createAccessToken()
} else { } else {
initialize() initialize()
} }
} }
def uninstalled() { def uninstalled() {
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
try { try {
state.HarmonyAccessToken = "" state.HarmonyAccessToken = ""
log.debug "Success disconnecting Harmony from SmartThings" log.debug "Success disconnecting Harmony from SmartThings"
} catch (groovyx.net.http.HttpResponseException e) { } catch (groovyx.net.http.HttpResponseException e) {
@@ -340,7 +340,7 @@ def initialize() {
if (selectedhubs || selectedactivities) { if (selectedhubs || selectedactivities) {
addDevice() addDevice()
runEvery5Minutes("discovery") runEvery5Minutes("discovery")
} }
} }
def getHarmonydevices() { def getHarmonydevices() {
@@ -360,20 +360,20 @@ Map discoverDevices() {
def hubname = getHubName(it.key) def hubname = getHubName(it.key)
def hubvalue = "${hubname}" def hubvalue = "${hubname}"
hubs["harmony-${hubkey}"] = hubvalue hubs["harmony-${hubkey}"] = hubvalue
it.value.response.data.activities.each { it.value.response.data.activities.each {
def value = "${it.value.name}" def value = "${it.value.name}"
def key = "harmony-${hubkey}-${it.key}" def key = "harmony-${hubkey}-${it.key}"
activities["${key}"] = value activities["${key}"] = value
} }
} }
state.HarmonyHubs = hubs state.HarmonyHubs = hubs
state.HarmonyActivities = activities state.HarmonyActivities = activities
} }
} }
//CHILD DEVICE METHODS //CHILD DEVICE METHODS
def discovery() { def discovery() {
def Params = [auth: state.HarmonyAccessToken] def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
try { try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
@@ -385,11 +385,11 @@ def discovery() {
poll() poll()
} else { } else {
log.debug "Error: $response.status" log.debug "Error: $response.status"
} }
} }
} catch (groovyx.net.http.HttpResponseException e) { } catch (groovyx.net.http.HttpResponseException e) {
if (e.statusCode == 401) { // token is expired if (e.statusCode == 401) { // token is expired
state.remove("HarmonyAccessToken") state.remove("HarmonyAccessToken")
log.warn "Harmony Access token has expired" log.warn "Harmony Access token has expired"
} }
} catch (java.net.SocketTimeoutException e) { } catch (java.net.SocketTimeoutException e) {
@@ -397,12 +397,12 @@ def discovery() {
state.resethub = true state.resethub = true
} catch (e) { } catch (e) {
log.warn "Hostname in certificate didn't match. Please try again later." log.warn "Hostname in certificate didn't match. Please try again later."
} }
return null return null
} }
def addDevice() { def addDevice() {
log.trace "Adding Hubs" log.trace "Adding Hubs"
selectedhubs.each { dni -> selectedhubs.each { dni ->
def d = getChildDevice(dni) def d = getChildDevice(dni)
if(!d) { if(!d) {
@@ -413,8 +413,8 @@ def addDevice() {
} else { } else {
log.trace "found ${d.displayName} with id $dni already exists" log.trace "found ${d.displayName} with id $dni already exists"
} }
} }
log.trace "Adding Activities" log.trace "Adding Activities"
selectedactivities.each { dni -> selectedactivities.each { dni ->
def d = getChildDevice(dni) def d = getChildDevice(dni)
if(!d) { if(!d) {
@@ -425,7 +425,7 @@ def addDevice() {
} else { } else {
log.trace "found ${d.displayName} with id $dni already exists" log.trace "found ${d.displayName} with id $dni already exists"
} }
} }
} }
def activity(dni,mode) { def activity(dni,mode) {
@@ -433,26 +433,26 @@ def activity(dni,mode) {
def msg = "Command failed" def msg = "Command failed"
def url = '' def url = ''
if (dni == "all") { if (dni == "all") {
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}" url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}"
} else { } else {
def aux = dni.split('-') def aux = dni.split('-')
def hubId = aux[1] def hubId = aux[1]
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){ if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}" url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}"
} else { } else {
def activityId = aux[2] def activityId = aux[2]
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}" url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}"
} }
} }
try { try {
httpPostJson(uri: url) { response -> httpPostJson(uri: url) { response ->
if (response.data.code == 200 || dni == "all") { if (response.data.code == 200 || dni == "all") {
msg = "Command sent succesfully" msg = "Command sent succesfully"
state.aux = 0 state.aux = 0
} else { } else {
msg = "Command failed. Error: $response.data.code" msg = "Command failed. Error: $response.data.code"
} }
} }
} catch (groovyx.net.http.HttpResponseException ex) { } catch (groovyx.net.http.HttpResponseException ex) {
log.error ex log.error ex
if (state.aux == 0) { if (state.aux == 0) {
@@ -460,7 +460,7 @@ def activity(dni,mode) {
activity(dni,mode) activity(dni,mode)
} else { } else {
msg = ex msg = ex
state.aux = 0 state.aux = 0
} }
} catch(Exception ex) { } catch(Exception ex) {
msg = ex msg = ex
@@ -473,10 +473,10 @@ def poll() {
// GET THE LIST OF ACTIVITIES // GET THE LIST OF ACTIVITIES
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
getActivityList() getActivityList()
def Params = [auth: state.HarmonyAccessToken] def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}" def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
try { try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
def map = [:] def map = [:]
response.data.hubs.each { response.data.hubs.each {
if (it.value.message == "OK") { if (it.value.message == "OK") {
@@ -489,20 +489,20 @@ def poll() {
def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key) def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false) hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
} }
} }
} else { } else {
log.trace it.value.message log.trace it.value.message
} }
} }
def activities = getChildDevices() def activities = getChildDevices()
def activitynotrunning = true def activitynotrunning = true
activities.each { activity -> activities.each { activity ->
def act = activity.deviceNetworkId.split('-') def act = activity.deviceNetworkId.split('-')
if (act.size() > 2) { if (act.size() > 2) {
def aux = map.find { it.key == act[1] } def aux = map.find { it.key == act[1] }
if (aux) { if (aux) {
def aux2 = aux.value.split(',') def aux2 = aux.value.split(',')
def childDevice = getChildDevice(activity.deviceNetworkId) def childDevice = getChildDevice(activity.deviceNetworkId)
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) { if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
childDevice?.sendEvent(name: "switch", value: "on") childDevice?.sendEvent(name: "switch", value: "on")
if (aux2[1] == "1") if (aux2[1] == "1")
@@ -512,30 +512,30 @@ def poll() {
if (aux2[1] == "3") if (aux2[1] == "3")
runIn(5, "poll", [overwrite: true]) runIn(5, "poll", [overwrite: true])
} }
} }
} }
} }
return "Poll completed $map - $state.hubs" return "Poll completed $map - $state.hubs"
} }
} catch (groovyx.net.http.HttpResponseException e) { } catch (groovyx.net.http.HttpResponseException e) {
if (e.statusCode == 401) { // token is expired if (e.statusCode == 401) { // token is expired
state.remove("HarmonyAccessToken") state.remove("HarmonyAccessToken")
return "Harmony Access token has expired" return "Harmony Access token has expired"
} }
} catch(Exception e) { } catch(Exception e) {
log.trace e log.trace e
} }
} }
} }
def getActivityList() { def getActivityList() {
// GET ACTIVITY'S NAME // GET ACTIVITY'S NAME
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
def Params = [auth: state.HarmonyAccessToken] def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
try { try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
response.data.hubs.each { response.data.hubs.each {
def hub = getChildDevice("harmony-${it.key}") def hub = getChildDevice("harmony-${it.key}")
if (hub) { if (hub) {
@@ -548,10 +548,10 @@ def getActivityList() {
} }
activities += [id: "off", name: "Activity OFF", type: "0"] activities += [id: "off", name: "Activity OFF", type: "0"]
log.trace activities log.trace activities
} }
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false) hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
} }
} }
} }
} catch (groovyx.net.http.HttpResponseException e) { } catch (groovyx.net.http.HttpResponseException e) {
log.trace e log.trace e
@@ -560,7 +560,7 @@ def getActivityList() {
} catch(Exception e) { } catch(Exception e) {
log.trace e log.trace e
} }
} }
return activity return activity
} }
@@ -568,16 +568,16 @@ def getActivityName(activity,hubId) {
// GET ACTIVITY'S NAME // GET ACTIVITY'S NAME
def actname = activity def actname = activity
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
def Params = [auth: state.HarmonyAccessToken] def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
try { try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
actname = response.data.data.activities[activity].name actname = response.data.data.activities[activity].name
} }
} catch(Exception e) { } catch(Exception e) {
log.trace e log.trace e
} }
} }
return actname return actname
} }
@@ -585,19 +585,19 @@ def getActivityId(activity,hubId) {
// GET ACTIVITY'S NAME // GET ACTIVITY'S NAME
def actid = activity def actid = activity
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
def Params = [auth: state.HarmonyAccessToken] def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
try { try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
response.data.data.activities.each { response.data.data.activities.each {
if (it.value.name == activity) if (it.value.name == activity)
actid = it.key actid = it.key
} }
} }
} catch(Exception e) { } catch(Exception e) {
log.trace e log.trace e
} }
} }
return actid return actid
} }
@@ -605,16 +605,16 @@ def getHubName(hubId) {
// GET HUB'S NAME // GET HUB'S NAME
def hubname = hubId def hubname = hubId
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
def Params = [auth: state.HarmonyAccessToken] def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}" def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}"
try { try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
hubname = response.data.data.name hubname = response.data.data.name
} }
} catch(Exception e) { } catch(Exception e) {
log.trace e log.trace e
} }
} }
return hubname return hubname
} }
@@ -625,8 +625,8 @@ def sendNotification(msg) {
def hookEventHandler() { def hookEventHandler() {
// log.debug "In hookEventHandler method." // log.debug "In hookEventHandler method."
log.debug "request = ${request}" log.debug "request = ${request}"
def json = request.JSON def json = request.JSON
def html = """{"code":200,"message":"OK"}""" def html = """{"code":200,"message":"OK"}"""
render contentType: 'application/json', data: html render contentType: 'application/json', data: html
@@ -660,12 +660,16 @@ def updateDevice() {
} else { } else {
def device = allDevices.find { it.id == params.id } def device = allDevices.find { it.id == params.id }
if (device) { if (device) {
if (arguments) { if (device.hasCommand("$command")) {
device."$command"(*arguments) if (arguments) {
} else { device."$command"(*arguments)
device."$command"() } else {
device."$command"()
}
render status: 204, data: "{}"
} else {
render status: 404, data: '{"msg": "Command not supported by this Device"}'
} }
render status: 204, data: "{}"
} else { } else {
render status: 404, data: '{"msg": "Device not found"}' render status: 404, data: '{"msg": "Device not found"}'
} }