Update WindowOrDoorOpen.groovy
[smartapps.git] / official / jawbone-up-connect.groovy
1 /**
2  *  Jawbone Service Manager
3  *
4  *  Author: Juan Risso
5  *  Date: 2013-12-19
6  */
7
8 include 'asynchttp_v1'
9
10 definition(
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",
18         oauth: true,
19     usePreferencesForAuthorization: false,
20     singleInstance: true
21 ) {
22         appSetting "clientId"
23         appSetting "clientSecret"
24 }
25
26 preferences {
27     page(name: "Credentials", title: "Jawbone UP", content: "authPage", install: false)
28 }
29
30 mappings {
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" ] }
36 }
37
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) }
41
42 def callback() {
43         def redirectUrl = null
44         if (params.authQueryString) {
45                 redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
46                 log.debug "redirectUrl: ${redirectUrl}"
47         } else {
48                 log.warn "No authQueryString"
49         }
50
51         if (state.JawboneAccessToken) {
52                 log.debug "Access token already exists"
53                 setup()
54                 success()
55         } else {
56                 def code = params.code
57                 if (code) {
58                         if (code.size() > 6) {
59                                 // Jawbone code
60                                 log.debug "Exchanging code for access token"
61                                 receiveToken(redirectUrl)
62                         } else {
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)}")
69                         }
70                 } else {
71                         log.debug "This code should be unreachable"
72                         success()
73                 }
74         }
75 }
76
77 def authPage() {
78     log.debug "authPage"
79     def description = null
80     if (state.JawboneAccessToken == null) {
81                 if (!state.accessToken) {
82                         log.debug "About to create access token"
83                         createAccessToken()
84                 }
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 }
92         }
93     } else {
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 }
97         }
98     }
99 }
100
101 def oauthInitUrl() {
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)}")
106 }
107
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 ]
111     def params = [
112       uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
113     ]
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
119     }
120
121         setup()
122         if (state.JawboneAccessToken) {
123                 success()
124         } else {
125                 def message = """
126                         <p>The connection could not be established!</p>
127                         <p>Click 'Done' to return to the menu.</p>
128                 """
129                 connectionStatus(message)
130         }
131 }
132
133 def success() {
134         def message = """
135                 <p>Your Jawbone Account is now connected to SmartThings!</p>
136                 <p>Click 'Done' to finish setup.</p>
137         """
138         connectionStatus(message)
139 }
140
141 def receivedToken() {
142         def message = """
143                 <p>Your Jawbone Account is already connected to SmartThings!</p>
144                 <p>Click 'Done' to finish setup.</p>
145         """
146         connectionStatus(message)
147 }
148
149 def connectionStatus(message, redirectUrl = null) {
150         def redirectHtml = ""
151         if (redirectUrl) {
152                 redirectHtml = """
153                         <meta http-equiv="refresh" content="3; url=${redirectUrl}" />
154                 """
155         }
156
157     def html = """
158         <!DOCTYPE html>
159         <html>
160         <head>
161         <meta name="viewport" content="width=640">
162         <title>SmartThings Connection</title>
163         <style type="text/css">
164             @font-face {
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');
171                 font-weight: normal;
172                 font-style: normal;
173             }
174             @font-face {
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');
181                 font-weight: normal;
182                 font-style: normal;
183             }
184             .container {
185                 width: 560px;
186                 padding: 40px;
187                 /*background: #eee;*/
188                 text-align: center;
189             }
190             img {
191                 vertical-align: middle;
192             }
193             img:nth-child(2) {
194                 margin: 0 30px;
195             }
196             p {
197                 font-size: 2.2em;
198                 font-family: 'Swiss 721 W01 Thin';
199                 text-align: center;
200                 color: #666666;
201                 padding: 0 40px;
202                 margin-bottom: 0;
203             }
204         /*
205             p:last-child {
206                 margin-top: 0px;
207             }
208         */
209             span {
210                 font-family: 'Swiss 721 W01 Light';
211             }
212         </style>
213                 ${redirectHtml}
214         </head>
215         <body>
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" />
220                 ${message}
221             </div>
222         </body>
223         </html>
224         """
225         render contentType: 'text/html', data: html
226 }
227
228 String toQueryString(Map m) {
229         return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
230 }
231
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}"
236
237         try {
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
243                 }
244             }
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)}"
251                         def params = [
252                                 uri: tokenUrl
253                         ]
254                         httpGet(params) { refreshResponse ->
255                                         def data = refreshResponse.data
256                                         log.debug "Status: ${refreshResponse.status}, data: ${data}"
257                                         if (data.error) {
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")
263                                                 }
264                                         } else {
265                                                 log.debug "Setting access token"
266                                                 state.JawboneAccessToken = data.access_token
267                                                 state.refreshToken = data.refresh_token
268                                         }
269                                 }
270             }
271         }
272         } catch (java.net.SocketTimeoutException e) {
273                 log.warn "Connection timed out, not much we can do here"
274         }
275 }
276
277 def initialize() {
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}" ])
283 }
284
285 def setup() {
286         // make sure this is going to work
287         validateCurrentToken()
288
289         if (state.JawboneAccessToken) {
290                 def urlmember = "https://jawbone.com/nudge/api/users/@me/"
291                 def member = null
292                 httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
293                     member = response.data.data
294                 }
295
296                 if (member) {
297                         state.member = member
298                         def externalId = "${app.id}.${member.xid}"
299
300                         // find the appropriate child device based on my app id and the device network id
301                         def deviceWrapper = getChildDevice("${externalId}")
302
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])
307                             if (childDevice) {
308                                 log.debug "Child Device Successfully Created"
309                                 childDevice?.generateSleepingEvent(false)
310                     pollChild(childDevice)
311                             }
312                         }
313                 }
314
315                 initialize()
316         }
317 }
318
319 def installed() {
320
321         if (!state.accessToken) {
322                 log.debug "About to create access token"
323                 createAccessToken()
324         }
325
326         if (state.JawboneAccessToken) {
327                 setup()
328         }
329 }
330
331 def updated() {
332
333         if (!state.accessToken) {
334                 log.debug "About to create access token"
335                 createAccessToken()
336         }
337
338         if (state.JawboneAccessToken) {
339                 setup()
340         }
341 }
342
343 def uninstalled() {
344         if (state.JawboneAccessToken) {
345                 try {
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"
348                         }
349                 } catch (groovyx.net.http.HttpResponseException e) {
350                         log.error "Error disconnecting Jawbone from SmartThings: ${e.statusCode}"
351                 }
352         }
353 }
354
355 def pollChild(childDevice) {
356                 def childMap = [ value: "$childDevice.device.deviceNetworkId}"]
357
358                 def params = [
359                                 uri: 'https://jawbone.com',
360                                 path: '/nudge/api/users/@me/goals',
361                                 headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
362                                 contentType: 'application/json'
363                 ]
364
365                 asynchttp_v1.get('responseGoals', params, childMap)
366
367                 def params2 = [
368                                 uri: 'https://jawbone.com',
369                                 path: '/nudge/api/users/@me/moves',
370                                 headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
371                                 contentType: 'application/json'
372                 ]
373
374                 asynchttp_v1.get('responseMoves', params2, childMap)
375 }
376
377 def responseGoals(response, dni) {
378         if (response.hasError()) {
379                         log.error "response has error: $response.errorMessage"
380         } else {
381                         def goals
382                         try {
383                                         // json response already parsed into JSONElement object
384                                         goals = response.json.data
385                         } catch (e) {
386                                         log.error "error parsing json from response: $e"
387                         }
388                         if (goals) {
389                                 def childDevice = getChildDevice(dni.value)
390                                 log.debug "Goal = ${goals.move_steps} Steps"
391                                 childDevice?.sendEvent(name:"goal", value: goals.move_steps)
392                         } else {
393                                         log.debug "did not get json results from response body: $response.data"
394                         }
395         }
396 }
397
398 def responseMoves(response, dni) {
399         if (response.hasError()) {
400                         log.error "response has error: $response.errorMessage"
401         } else {
402                         def moves
403                         try {
404                                         // json response already parsed into JSONElement object
405                                         moves = response.json.data.items[0]
406                         } catch (e) {
407                                         log.error "error parsing json from response: $e"
408                         }
409                         if (moves) {
410                                 def childDevice = getChildDevice(dni.value)
411                                 log.debug "Moves = ${moves.details.steps} Steps"
412                                 childDevice?.sendEvent(name:"steps", value: moves.details.steps)
413                         } else {
414                                         log.debug "did not get json results from response body: $response.data"
415                         }
416         }
417 }
418
419 def setColor (steps,goal,childDevice) {
420     def result = steps * 100 / goal
421     if (result < 25)
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)
429 }
430
431 def hookEventHandler() {
432     // log.debug "In hookEventHandler method."
433     log.debug "request = ${request}"
434
435     def json = request.JSON
436
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]
441
442     //log.debug json
443     log.debug "Userid = ${userId}"
444     log.debug "Notification Type: " + json_type
445     log.debug "Notification Action: " + json_action
446
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}")
450
451     if (childDevice) {
452         switch (json_action) {
453                 case "enter_sleep_mode":
454                 childDevice?.generateSleepingEvent(true)
455                 break
456             case "exit_sleep_mode":
457                 childDevice?.generateSleepingEvent(false)
458                 break
459             case "creation":
460                 childDevice?.sendEvent(name:"steps", value: 0)
461                         break
462             case "updation":
463                 def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
464                 def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
465                 def goals = null
466                 def moves = null
467                 httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
468                        goals = response.data.data
469                 }
470                 httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
471                    moves = response.data.data.items[0]
472                 }
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)
478                 break
479                         case "deletion":
480                                 app.delete()
481                                 break
482                 }
483     }
484     else {
485             log.debug "Couldn't find child device associated with Jawbone."
486     }
487
488         def html = """{"code":200,"message":"OK"}"""
489         render contentType: 'application/json', data: html
490 }