diff --git a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy index 905cdde..a127d5b 100644 --- a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy +++ b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy @@ -4,29 +4,33 @@ import java.text.DecimalFormat import groovy.json.JsonSlurper -private apiUrl() { "https://api.netatmo.com" } -private getVendorName() { "netatmo" } -private getVendorAuthPath() { "https://api.netatmo.com/oauth2/authorize?" } -private getVendorTokenPath(){ "https://api.netatmo.com/oauth2/token" } +private getApiUrl() { "https://api.netatmo.com" } +private getVendorName() { "netatmo" } +private getVendorAuthPath() { "${apiUrl}/oauth2/authorize?" } +private getVendorTokenPath(){ "${apiUrl}/oauth2/token" } private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" } -private getClientId() { appSettings.clientId } -private getClientSecret() { appSettings.clientSecret } -private getServerUrl() { "https://graph.api.smartthings.com" } +private getClientId() { appSettings.clientId } +private getClientSecret() { appSettings.clientSecret } +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. definition( - name: "Netatmo (Connect)", - namespace: "dianoga", - author: "Brian Steere", - description: "Netatmo Integration", - category: "SmartThings Labs", - 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", - oauth: true, - singleInstance: true + name: "Netatmo (Connect)", + namespace: "dianoga", + author: "Brian Steere", + description: "Netatmo Integration", + category: "SmartThings Labs", + 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", + oauth: true, + singleInstance: true ){ appSetting "clientId" appSetting "clientSecret" + appSetting "serverUrl" } preferences { @@ -35,35 +39,52 @@ preferences { } mappings { - path("/receivedToken"){action: [POST: "receivedToken", GET: "receivedToken"]} - path("/receiveToken"){action: [POST: "receiveToken", GET: "receiveToken"]} - path("/auth"){action: [GET: "auth"]} + path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} + path("/oauth/callback") {action: [GET: "callback"]} } def authPage() { log.debug "In authPage" - if(canInstallLabs()) { - def description = null - if (state.vendorAccessToken == null) { - log.debug "About to create access token." + def description + def uninstallAllowed = false + def oauthTokenProvided = false - createAccessToken() - description = "Tap to enter Credentials." + if (!state.accessToken) { + log.debug "About to create access token." + state.accessToken = createAccessToken() + } - return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) { - section { href url:buildRedirectUrl("auth"), style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description } + if (canInstallLabs()) { + + 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 { - description = "Tap 'Next' to proceed" - - return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: true, install:false) { - section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description } + log.debug "Show the devices page" + return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) { + section() { + 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. 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() { log.debug "In oauthInitUrl" - /* OAuth Step 1: Request access code with our client ID */ - 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 = [ - client_secret: getClientSecret(), + response_type: "code", client_id: getClientId(), - grant_type: "authorization_code", - redirect_uri: buildRedirectUrl('receiveToken'), - code: params.code, + client_secret: getClientSecret(), + state: state.oauthInitState, + redirect_uri: getCallbackUrl(), 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" */ - try { - httpPost(params) { response -> - log.debug response.data - def slurper = new JsonSlurper(); + redirect (location: getVendorAuthPath() + toQueryString(oauthParams)) +} - response.data.each {key, value -> - def data = slurper.parseText(key); - log.debug "Data: $data" +def callback() { + log.debug "callback()>> params: $params, params.code ${params.code}" - state.vendorRefreshToken = data.refresh_token - state.vendorAccessToken = data.access_token - state.vendorTokenExpires = now() + (data.expires_in * 1000) - return + def code = params.code + def oauthState = params.state + + 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 = """ +
We have located your """ + getVendorName() + """ account.
+Tap 'Done' to continue to Devices.
+ """ + connectionStatus(message) +} - if ( !state.vendorAccessToken ) { //We didn't get an access token, bail on install - return +def fail() { + log.debug "in fail" + def message = """ +The connection could not be established!
+Click 'Done' to return to the menu.
+ """ + connectionStatus(message) +} + +def connectionStatus(message, redirectUrl = null) { + def redirectHtml = "" + if (redirectUrl) { + redirectHtml = """ + + """ } - /* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */ - def html = """ - - - - -We have located your """ + getVendorName() + """ account.
-Tap 'Done' to process your credentials.
+ + + + +Tap 'Done' to continue to Devices.
-