Merge pull request #867 from rohandesai/DVCSMP-1699

DVCSMP-1699 Na02>Missing/Failed Oauth Tokens - Netatmo
This commit is contained in:
rohandesai
2016-05-16 13:17:57 -07:00

View File

@@ -4,29 +4,33 @@
import java.text.DecimalFormat import java.text.DecimalFormat
import groovy.json.JsonSlurper import groovy.json.JsonSlurper
private apiUrl() { "https://api.netatmo.com" } private getApiUrl() { "https://api.netatmo.com" }
private getVendorName() { "netatmo" } private getVendorName() { "netatmo" }
private getVendorAuthPath() { "https://api.netatmo.com/oauth2/authorize?" } private getVendorAuthPath() { "${apiUrl}/oauth2/authorize?" }
private getVendorTokenPath(){ "https://api.netatmo.com/oauth2/token" } private getVendorTokenPath(){ "${apiUrl}/oauth2/token" }
private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" } private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" }
private getClientId() { appSettings.clientId } private getClientId() { appSettings.clientId }
private getClientSecret() { appSettings.clientSecret } private getClientSecret() { appSettings.clientSecret }
private getServerUrl() { "https://graph.api.smartthings.com" } private getServerUrl() { appSettings.serverUrl }
private getShardUrl() { return getApiServerUrl() }
private getCallbackUrl() { "${serverUrl}/oauth/callback" }
private getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" }
// Automatically generated. Make future change here. // Automatically generated. Make future change here.
definition( definition(
name: "Netatmo (Connect)", name: "Netatmo (Connect)",
namespace: "dianoga", namespace: "dianoga",
author: "Brian Steere", author: "Brian Steere",
description: "Netatmo Integration", description: "Netatmo Integration",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png",
oauth: true, oauth: true,
singleInstance: true singleInstance: true
){ ){
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"
appSetting "serverUrl"
} }
preferences { preferences {
@@ -35,35 +39,52 @@ preferences {
} }
mappings { mappings {
path("/receivedToken"){action: [POST: "receivedToken", GET: "receivedToken"]} path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
path("/receiveToken"){action: [POST: "receiveToken", GET: "receiveToken"]} path("/oauth/callback") {action: [GET: "callback"]}
path("/auth"){action: [GET: "auth"]}
} }
def authPage() { def authPage() {
log.debug "In authPage" log.debug "In authPage"
if(canInstallLabs()) {
def description = null
if (state.vendorAccessToken == null) { def description
log.debug "About to create access token." def uninstallAllowed = false
def oauthTokenProvided = false
createAccessToken() if (!state.accessToken) {
description = "Tap to enter Credentials." log.debug "About to create access token."
state.accessToken = createAccessToken()
}
return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) { if (canInstallLabs()) {
section { href url:buildRedirectUrl("auth"), style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description }
def redirectUrl = getBuildRedirectUrl()
log.debug "Redirect url = ${redirectUrl}"
if (state.authToken) {
description = "Tap 'Next' to proceed"
uninstallAllowed = true
oauthTokenProvided = true
} else {
description = "Click to enter Credentials."
}
if (!oauthTokenProvided) {
log.debug "Show the login page"
return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
section() {
paragraph "Tap below to log in to the netatmo and authorize SmartThings access."
href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description
}
} }
} else { } else {
description = "Tap 'Next' to proceed" log.debug "Show the devices page"
return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: true, install:false) { section() {
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description } input(name:"Devices", style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description)
}
} }
} }
} } else {
else
{
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
@@ -78,229 +99,175 @@ To update your Hub, access Location Settings in the Main Menu (tap the gear next
} }
} }
def auth() {
redirect location: oauthInitUrl()
}
def oauthInitUrl() { def oauthInitUrl() {
log.debug "In oauthInitUrl" log.debug "In oauthInitUrl"
/* OAuth Step 1: Request access code with our client ID */
state.oauthInitState = UUID.randomUUID().toString() state.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [ response_type: "code",
client_id: getClientId(),
state: state.oauthInitState,
redirect_uri: buildRedirectUrl("receiveToken") ,
scope: "read_station"
]
return getVendorAuthPath() + toQueryString(oauthParams)
}
def buildRedirectUrl(endPoint) {
log.debug "In buildRedirectUrl"
return getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}"
}
def receiveToken() {
log.debug "In receiveToken"
def oauthParams = [ def oauthParams = [
client_secret: getClientSecret(), response_type: "code",
client_id: getClientId(), client_id: getClientId(),
grant_type: "authorization_code", client_secret: getClientSecret(),
redirect_uri: buildRedirectUrl('receiveToken'), state: state.oauthInitState,
code: params.code, redirect_uri: getCallbackUrl(),
scope: "read_station" scope: "read_station"
]
def tokenUrl = getVendorTokenPath()
def params = [
uri: tokenUrl,
contentType: 'application/x-www-form-urlencoded',
body: oauthParams,
] ]
log.debug params log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}"
/* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */ redirect (location: getVendorAuthPath() + toQueryString(oauthParams))
try { }
httpPost(params) { response ->
log.debug response.data
def slurper = new JsonSlurper();
response.data.each {key, value -> def callback() {
def data = slurper.parseText(key); log.debug "callback()>> params: $params, params.code ${params.code}"
log.debug "Data: $data"
state.vendorRefreshToken = data.refresh_token def code = params.code
state.vendorAccessToken = data.access_token def oauthState = params.state
state.vendorTokenExpires = now() + (data.expires_in * 1000)
return if (oauthState == state.oauthInitState) {
def tokenParams = [
client_secret: getClientSecret(),
client_id : getClientId(),
grant_type: "authorization_code",
redirect_uri: getCallbackUrl(),
code: code,
scope: "read_station"
]
log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}"
def tokenUrl = getVendorTokenPath()
def params = [
uri: tokenUrl,
contentType: 'application/x-www-form-urlencoded',
body: tokenParams
]
log.debug "PARAMS: ${params}"
httpPost(params) { resp ->
def slurper = new JsonSlurper()
resp.data.each { key, value ->
def data = slurper.parseText(key)
state.refreshToken = data.refresh_token
state.authToken = data.access_token
state.tokenExpires = now() + (data.expires_in * 1000)
log.debug "swapped token: $resp.data"
} }
} }
} catch (Exception e) {
log.debug "Error: $e" // Handle success and failure here, and render stuff accordingly
if (state.authToken) {
success()
} else {
fail()
}
} else {
log.error "callback() failed oauthState != state.oauthInitState"
} }
}
log.debug "State: $state" def success() {
log.debug "in success"
def message = """
<p>We have located your """ + getVendorName() + """ account.</p>
<p>Tap 'Done' to continue to Devices.</p>
"""
connectionStatus(message)
}
if ( !state.vendorAccessToken ) { //We didn't get an access token, bail on install def fail() {
return log.debug "in fail"
def message = """
<p>The connection could not be established!</p>
<p>Click 'Done' to return to the menu.</p>
"""
connectionStatus(message)
}
def connectionStatus(message, redirectUrl = null) {
def redirectHtml = ""
if (redirectUrl) {
redirectHtml = """
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
"""
} }
/* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */
def html = """ def html = """
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>${getVendorName()} Connection</title> <title>${getVendorName()} Connection</title>
<style type="text/css"> <style type="text/css">
* { box-sizing: border-box; } * { box-sizing: border-box; }
@font-face { @font-face {
font-family: 'Swiss 721 W01 Thin'; font-family: 'Swiss 721 W01 Thin';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot'); src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'), src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg'); url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'Swiss 721 W01 Light'; font-family: 'Swiss 721 W01 Light';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot'); src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'), src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg'); url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
.container { .container {
width: 100%; width: 100%;
padding: 40px; padding: 40px;
/*background: #eee;*/ /*background: #eee;*/
text-align: center; text-align: center;
} }
img { img {
vertical-align: middle; vertical-align: middle;
} }
img:nth-child(2) { img:nth-child(2) {
margin: 0 30px; margin: 0 30px;
} }
p { p {
font-size: 2.2em; font-size: 2.2em;
font-family: 'Swiss 721 W01 Thin'; font-family: 'Swiss 721 W01 Thin';
text-align: center; text-align: center;
color: #666666; color: #666666;
margin-bottom: 0; margin-bottom: 0;
} }
/* /*
p:last-child { p:last-child {
margin-top: 0px; margin-top: 0px;
} }
*/ */
span { span {
font-family: 'Swiss 721 W01 Light'; font-family: 'Swiss 721 W01 Light';
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<img src=""" + getVendorIcon() + """ alt="Vendor icon" /> <img src=""" + getVendorIcon() + """ alt="Vendor icon" />
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" /> <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" /> <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
<p>We have located your """ + getVendorName() + """ account.</p> ${message}
<p>Tap 'Done' to process your credentials.</p>
</div> </div>
</body> </body>
</html> </html>
""" """
render contentType: 'text/html', data: html render contentType: 'text/html', data: html
} }
def receivedToken() {
log.debug "In receivedToken"
def html = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Withings Connection</title>
<style type="text/css">
* { box-sizing: border-box; }
@font-face {
font-family: 'Swiss 721 W01 Thin';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Swiss 721 W01 Light';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
font-weight: normal;
font-style: normal;
}
.container {
width: 560px;
padding: 40px;
/*background: #eee;*/
text-align: center;
}
img {
vertical-align: middle;
}
img:nth-child(2) {
margin: 0 30px;
}
p {
font-size: 2.2em;
font-family: 'Swiss 721 W01 Thin';
text-align: center;
color: #666666;
margin-bottom: 0;
}
/*
p:last-child {
margin-top: 0px;
}
*/
span {
font-family: 'Swiss 721 W01 Light';
}
</style>
</head>
<body>
<div class="container">
<img src=""" + getVendorIcon() + """ alt="Vendor icon" />
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
<p>Tap 'Done' to continue to Devices.</p>
</div>
</body>
</html>
"""
render contentType: 'text/html', data: html
}
// "
def refreshToken() { def refreshToken() {
log.debug "In refreshToken" log.debug "In refreshToken"
@@ -308,8 +275,8 @@ def refreshToken() {
client_secret: getClientSecret(), client_secret: getClientSecret(),
client_id: getClientId(), client_id: getClientId(),
grant_type: "refresh_token", grant_type: "refresh_token",
refresh_token: state.vendorRefreshToken refresh_token: state.refreshToken
] ]
def tokenUrl = getVendorTokenPath() def tokenUrl = getVendorTokenPath()
def params = [ def params = [
@@ -318,7 +285,7 @@ def refreshToken() {
body: oauthParams, body: oauthParams,
] ]
/* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */ // OAuth Step 2: Request access token with our client Secret and OAuth "Code"
try { try {
httpPost(params) { response -> httpPost(params) { response ->
def slurper = new JsonSlurper(); def slurper = new JsonSlurper();
@@ -327,9 +294,9 @@ def refreshToken() {
def data = slurper.parseText(key); def data = slurper.parseText(key);
log.debug "Data: $data" log.debug "Data: $data"
state.vendorRefreshToken = data.refresh_token state.refreshToken = data.refresh_token
state.vendorAccessToken = data.access_token state.accessToken = data.access_token
state.vendorTokenExpires = now() + (data.expires_in * 1000) state.tokenExpires = now() + (data.expires_in * 1000)
return true return true
} }
@@ -338,9 +305,8 @@ def refreshToken() {
log.debug "Error: $e" log.debug "Error: $e"
} }
log.debug "State: $state" // We didn't get an access token
if ( !state.accessToken ) {
if ( !state.vendorAccessToken ) { //We didn't get an access token
return false return false
} }
} }
@@ -482,13 +448,13 @@ def listDevices() {
} }
def apiGet(String path, Map query, Closure callback) { def apiGet(String path, Map query, Closure callback) {
if(now() >= state.vendorTokenExpires) { if(now() >= state.tokenExpires) {
refreshToken(); refreshToken();
} }
query['access_token'] = state.vendorAccessToken query['access_token'] = state.accessToken
def params = [ def params = [
uri: apiUrl(), uri: getApiUrl(),
path: path, path: path,
'query': query 'query': query
] ]