2 * Jawbone Service Manager
11 name: "Jawbone UP (Connect)",
12 namespace: "juano2310",
13 author: "Juan Pablo Risso",
14 description: "Connect your Jawbone UP to SmartThings",
15 category: "SmartThings Labs",
16 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
17 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png",
19 usePreferencesForAuthorization: false,
23 appSetting "clientSecret"
27 page(name: "Credentials", title: "Jawbone UP", content: "authPage", install: false)
31 path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
32 path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
33 path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
34 path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
35 path("/oauth/callback") { action: [ GET: "callback" ] }
38 def getServerUrl() { return "https://graph.api.smartthings.com" }
39 def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
40 def buildRedirectUrl(page) { return buildActionUrl(page) }
43 def redirectUrl = null
44 if (params.authQueryString) {
45 redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
46 log.debug "redirectUrl: ${redirectUrl}"
48 log.warn "No authQueryString"
51 if (state.JawboneAccessToken) {
52 log.debug "Access token already exists"
56 def code = params.code
58 if (code.size() > 6) {
60 log.debug "Exchanging code for access token"
61 receiveToken(redirectUrl)
63 // SmartThings code, which we ignore, as we don't need to exchange for an access token.
64 // Instead, go initiate the Jawbone OAuth flow.
65 log.debug "Executing callback redirect to auth page"
66 state.oauthInitState = UUID.randomUUID().toString()
67 def oauthParams = [response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"]
68 redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
71 log.debug "This code should be unreachable"
79 def description = null
80 if (state.JawboneAccessToken == null) {
81 if (!state.accessToken) {
82 log.debug "About to create access token"
85 description = "Click to enter Jawbone Credentials"
86 def redirectUrl = buildRedirectUrl
87 log.debug "RedirectURL = ${redirectUrl}"
88 def donebutton= state.JawboneAccessToken != null
89 return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
90 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." }
91 section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description }
94 description = "Jawbone Credentials Already Entered."
95 return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) {
96 section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description }
102 log.debug "oauthInitUrl"
103 state.oauthInitState = UUID.randomUUID().toString()
104 def oauthParams = [ response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ]
105 redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
108 def receiveToken(redirectUrl = null) {
109 log.debug "receiveToken"
110 def oauthParams = [ client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "authorization_code", code: params.code ]
112 uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
114 httpGet(params) { response ->
115 log.debug "${response.data}"
116 log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}"
117 state.JawboneAccessToken = response.data.access_token
118 state.refreshToken = response.data.refresh_token
122 if (state.JawboneAccessToken) {
126 <p>The connection could not be established!</p>
127 <p>Click 'Done' to return to the menu.</p>
129 connectionStatus(message)
135 <p>Your Jawbone Account is now connected to SmartThings!</p>
136 <p>Click 'Done' to finish setup.</p>
138 connectionStatus(message)
141 def receivedToken() {
143 <p>Your Jawbone Account is already connected to SmartThings!</p>
144 <p>Click 'Done' to finish setup.</p>
146 connectionStatus(message)
149 def connectionStatus(message, redirectUrl = null) {
150 def redirectHtml = ""
153 <meta http-equiv="refresh" content="3; url=${redirectUrl}" />
161 <meta name="viewport" content="width=640">
162 <title>SmartThings Connection</title>
163 <style type="text/css">
165 font-family: 'Swiss 721 W01 Thin';
166 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
167 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
168 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
169 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
170 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
175 font-family: 'Swiss 721 W01 Light';
176 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
177 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
178 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
179 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
180 url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
187 /*background: #eee;*/
191 vertical-align: middle;
198 font-family: 'Swiss 721 W01 Thin';
210 font-family: 'Swiss 721 W01 Light';
216 <div class="container">
217 <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSMuoEIQ7gQhFtc02vXkybwmH0o7L1cs5mtbcJye0mgNqop_LOZbg" alt="Jawbone UP icon" />
218 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
219 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
225 render contentType: 'text/html', data: html
228 String toQueryString(Map m) {
229 return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
232 def validateCurrentToken() {
233 log.debug "validateCurrentToken"
234 def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
235 def requestBody = "secret=${appSettings.clientSecret}"
238 httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
239 if (response.status == 200) {
240 log.debug "${response.data}"
241 log.debug "Setting refresh token"
242 state.refreshToken = response.data.data.refresh_token
245 } catch (groovyx.net.http.HttpResponseException e) {
246 if (e.statusCode == 401) { // token is expired
247 log.debug "Access token is expired"
248 if (state.refreshToken) { // if we have this we are okay
249 def oauthParams = [client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
250 def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}"
254 httpGet(params) { refreshResponse ->
255 def data = refreshResponse.data
256 log.debug "Status: ${refreshResponse.status}, data: ${data}"
258 if (data.error == "access_denied") {
259 // User has removed authorization (probably)
260 log.warn "Access denied, because: ${data.error_description}"
261 state.remove("JawboneAccessToken")
262 state.remove("refreshToken")
265 log.debug "Setting access token"
266 state.JawboneAccessToken = data.access_token
267 state.refreshToken = data.refresh_token
272 } catch (java.net.SocketTimeoutException e) {
273 log.warn "Connection timed out, not much we can do here"
278 log.debug "Callback URL - Webhook"
279 def localServerUrl = getApiServerUrl()
280 def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
281 def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
282 httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
286 // make sure this is going to work
287 validateCurrentToken()
289 if (state.JawboneAccessToken) {
290 def urlmember = "https://jawbone.com/nudge/api/users/@me/"
292 httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
293 member = response.data.data
297 state.member = member
298 def externalId = "${app.id}.${member.xid}"
300 // find the appropriate child device based on my app id and the device network id
301 def deviceWrapper = getChildDevice("${externalId}")
303 // invoke the generatePresenceEvent method on the child device
304 log.debug "Device $externalId: $deviceWrapper"
305 if (!deviceWrapper) {
306 def childDevice = addChildDevice('juano2310', "Jawbone User", "${app.id}.${member.xid}",null,[name:"Jawbone UP - " + member.first, completedSetup: true])
308 log.debug "Child Device Successfully Created"
309 childDevice?.generateSleepingEvent(false)
310 pollChild(childDevice)
321 if (!state.accessToken) {
322 log.debug "About to create access token"
326 if (state.JawboneAccessToken) {
333 if (!state.accessToken) {
334 log.debug "About to create access token"
338 if (state.JawboneAccessToken) {
344 if (state.JawboneAccessToken) {
346 httpDelete(uri: "https://jawbone.com/nudge/api/v.1.0/users/@me/PartnerAppMembership", headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) { response ->
347 log.debug "Success disconnecting Jawbone from SmartThings"
349 } catch (groovyx.net.http.HttpResponseException e) {
350 log.error "Error disconnecting Jawbone from SmartThings: ${e.statusCode}"
355 def pollChild(childDevice) {
356 def childMap = [ value: "$childDevice.device.deviceNetworkId}"]
359 uri: 'https://jawbone.com',
360 path: '/nudge/api/users/@me/goals',
361 headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
362 contentType: 'application/json'
365 asynchttp_v1.get('responseGoals', params, childMap)
368 uri: 'https://jawbone.com',
369 path: '/nudge/api/users/@me/moves',
370 headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
371 contentType: 'application/json'
374 asynchttp_v1.get('responseMoves', params2, childMap)
377 def responseGoals(response, dni) {
378 if (response.hasError()) {
379 log.error "response has error: $response.errorMessage"
383 // json response already parsed into JSONElement object
384 goals = response.json.data
386 log.error "error parsing json from response: $e"
389 def childDevice = getChildDevice(dni.value)
390 log.debug "Goal = ${goals.move_steps} Steps"
391 childDevice?.sendEvent(name:"goal", value: goals.move_steps)
393 log.debug "did not get json results from response body: $response.data"
398 def responseMoves(response, dni) {
399 if (response.hasError()) {
400 log.error "response has error: $response.errorMessage"
404 // json response already parsed into JSONElement object
405 moves = response.json.data.items[0]
407 log.error "error parsing json from response: $e"
410 def childDevice = getChildDevice(dni.value)
411 log.debug "Moves = ${moves.details.steps} Steps"
412 childDevice?.sendEvent(name:"steps", value: moves.details.steps)
414 log.debug "did not get json results from response body: $response.data"
419 def setColor (steps,goal,childDevice) {
420 def result = steps * 100 / goal
422 childDevice?.sendEvent(name:"steps", value: "steps", label: steps)
423 else if ((result >= 25) && (result < 50))
424 childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
425 else if ((result >= 50) && (result < 75))
426 childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
427 else if (result >= 75)
428 childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps)
431 def hookEventHandler() {
432 // log.debug "In hookEventHandler method."
433 log.debug "request = ${request}"
435 def json = request.JSON
437 // get some stuff we need
438 def userId = json.events.user_xid[0]
439 def json_type = json.events.type[0]
440 def json_action = json.events.action[0]
443 log.debug "Userid = ${userId}"
444 log.debug "Notification Type: " + json_type
445 log.debug "Notification Action: " + json_action
447 // find the appropriate child device based on my app id and the device network id
448 def externalId = "${app.id}.${userId}"
449 def childDevice = getChildDevice("${externalId}")
452 switch (json_action) {
453 case "enter_sleep_mode":
454 childDevice?.generateSleepingEvent(true)
456 case "exit_sleep_mode":
457 childDevice?.generateSleepingEvent(false)
460 childDevice?.sendEvent(name:"steps", value: 0)
463 def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
464 def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
467 httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
468 goals = response.data.data
470 httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
471 moves = response.data.data.items[0]
473 log.debug "Goal = ${goals.move_steps} Steps"
474 log.debug "Steps = ${moves.details.steps} Steps"
475 childDevice?.sendEvent(name:"steps", value: moves.details.steps)
476 childDevice?.sendEvent(name:"goal", value: goals.move_steps)
477 //setColor(moves.details.steps,goals.move_steps,childDevice)
485 log.debug "Couldn't find child device associated with Jawbone."
488 def html = """{"code":200,"message":"OK"}"""
489 render contentType: 'application/json', data: html