Update hue-minimote.groovy
[smartapps.git] / official / quirky-connect.groovy
1 /**
2  *  Quirky (Connect)
3  *
4  *  Author: todd@wackford.net
5  *  Date: 2014-02-15
6  *
7  *  Update: 2014-02-22
8  *                      Added eggtray
9  *                      Added device specific methods called from poll (versus in poll)
10  *
11  *  Update2:2014-02-22
12  *                      Added nimbus
13  *
14  *  Update3:2014-02-26
15  *                      Improved eggtray integration
16  *                      Added notifications to hello home
17  *                      Introduced Quirky Eggtray specific icons (Thanks to Dane)
18  *                      Added an Egg Report that outputs to hello home.
19  *                      Switched to Dan Lieberman's client and secret
20  *                      Still not browser flow (next update?)
21  *
22  *  Update4:2014-03-08
23  *                      Added Browser Flow OAuth
24  *
25  *
26  *  Update5:2014-03-14
27  *                      Added dynamic icon/tile updating to the nimbus. Changes the device icon from app.
28  *
29  *  Update6:2014-03-31
30  *                      Stubbed out creation and choice of nimbus, eggtray and porkfolio per request.
31  *
32  *  Update7:2014-04-01
33  *          Renamed to 'Quirky (Connect)' and updated device names
34  *
35  *  Update8:2014-04-08 (dlieberman)
36  *         Stubbed out Spotter
37  *
38  *  Update9:2014-04-08 (twackford)
39  *         resubscribe to events on each poll
40  */
41
42 import java.text.DecimalFormat
43
44 // Wink API
45 private apiUrl()                        { "https://winkapi.quirky.com/" }
46 private getVendorName()         { "Quirky Wink" }
47 private getVendorAuthPath()     { "https://winkapi.quirky.com/oauth2/authorize?" }
48 private getVendorTokenPath(){ "https://winkapi.quirky.com/oauth2/token?" }
49 private getVendorIcon()         { "https://s3.amazonaws.com/smartthings-device-icons/custom/quirky/quirky-device@2x.png" }
50 private getClientId()           { appSettings.clientId }
51 private getClientSecret()       { appSettings.clientSecret }
52 private getServerUrl()          { appSettings.serverUrl }
53
54 definition(
55     name: "Quirky (Connect)",
56     namespace: "wackford",
57     author: "SmartThings",
58     description: "Connect your Quirky to SmartThings.",
59     category: "SmartThings Labs",
60     iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png",
61     iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png",
62     singleInstance: true
63 ) {
64         appSetting "clientId"
65         appSetting "clientSecret"
66     appSetting "serverUrl"
67 }
68
69 preferences {
70         page(name: "Credentials", title: "Fetch OAuth2 Credentials", content: "authPage", install: false)
71         page(name: "listDevices", title: "Quirky Devices", content: "listDevices", install: false)
72 }
73
74 mappings {
75         path("/receivedToken")          { action:[ POST: "receivedToken",                       GET: "receivedToken"] }
76         path("/receiveToken")           { action:[ POST: "receiveToken",                        GET: "receiveToken"] }
77         path("/powerstripCallback")     { action:[ POST: "powerstripEventHandler",      GET: "subscriberIdentifyVerification"]}
78         path("/sensor_podCallback") { action:[ POST: "sensor_podEventHandler",  GET: "subscriberIdentifyVerification"]}
79         path("/piggy_bankCallback") { action:[ POST: "piggy_bankEventHandler",  GET: "subscriberIdentifyVerification"]}
80         path("/eggtrayCallback")        { action:[ POST: "eggtrayEventHandler",         GET: "subscriberIdentifyVerification"]}
81         path("/cloud_clockCallback"){ action:[ POST: "cloud_clockEventHandler", GET: "subscriberIdentifyVerification"]}
82 }
83
84 def authPage() {
85         log.debug "In authPage"
86         if(canInstallLabs()) {
87                 def description = null
88
89                 if (state.vendorAccessToken == null) {
90                         log.debug "About to create access token."
91
92                         createAccessToken()
93                         description = "Tap to enter Credentials."
94
95                         def redirectUrl = oauthInitUrl()
96
97
98                         return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) {
99                                 section { href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description }
100                         }
101                 } else {
102                         description = "Tap 'Next' to proceed"
103
104                         return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: true, install:false) {
105                                 section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description }
106                         }
107                 }
108         }
109         else
110         {
111                 def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
112
113 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"."""
114
115
116                 return dynamicPage(name:"Credentials", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
117                         section {
118                                 paragraph "$upgradeNeeded"
119                         }
120                 }
121
122         }
123 }
124
125 def oauthInitUrl() {
126         log.debug "In oauthInitUrl"
127
128         /* OAuth Step 1: Request access code with our client ID */
129
130         state.oauthInitState = UUID.randomUUID().toString()
131
132         def oauthParams = [ response_type: "code",
133                 client_id: getClientId(),
134                 state: state.oauthInitState,
135                 redirect_uri: buildRedirectUrl("receiveToken") ]
136
137         return getVendorAuthPath() + toQueryString(oauthParams)
138 }
139
140 def buildRedirectUrl(endPoint) {
141         log.debug "In buildRedirectUrl"
142
143         return getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}"
144 }
145
146 def receiveToken() {
147         log.debug "In receiveToken"
148
149         def oauthParams = [ client_secret: getClientSecret(),
150                 grant_type: "authorization_code",
151                 code: params.code ]
152
153         def tokenUrl = getVendorTokenPath() + toQueryString(oauthParams)
154         def params = [
155                 uri: tokenUrl,
156         ]
157
158         /* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */
159         httpPost(params) { response ->
160
161                 def data = response.data.data
162
163                 state.vendorRefreshToken = data.refresh_token //these may need to be adjusted depending on depth of returned data
164                 state.vendorAccessToken = data.access_token
165         }
166
167         if ( !state.vendorAccessToken ) {  //We didn't get an access token, bail on install
168                 return
169         }
170
171         /* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */
172
173         def html = """
174         <!DOCTYPE html>
175         <html>
176         <head>
177         <meta name="viewport" content="width=50%,height=50%,  user-scalable = yes">
178         <title>${getVendorName()} Connection</title>
179         <style type="text/css">
180             @font-face {
181                 font-family: 'Swiss 721 W01 Thin';
182                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
183                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
184                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
185                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
186                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
187                 font-weight: normal;
188                 font-style: normal;
189             }
190             @font-face {
191                 font-family: 'Swiss 721 W01 Light';
192                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
193                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
194                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
195                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
196                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
197                 font-weight: normal;
198                 font-style: normal;
199             }
200             .container {
201                 width: 560px;
202                 padding: 40px;
203                 /*background: #eee;*/
204                 text-align: center;
205             }
206             img {
207                 vertical-align: middle;
208             }
209             img:nth-child(2) {
210                 margin: 0 30px;
211             }
212             p {
213                 font-size: 2.2em;
214                 font-family: 'Swiss 721 W01 Thin';
215                 text-align: center;
216                 color: #666666;
217                 padding: 0 40px;
218                 margin-bottom: 0;
219             }
220         /*
221             p:last-child {
222                 margin-top: 0px;
223             }
224         */
225             span {
226                 font-family: 'Swiss 721 W01 Light';
227             }
228         </style>
229         </head>
230         <body>
231             <div class="container">
232                 <img src=""" + getVendorIcon() + """ alt="Vendor icon" />
233                 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
234                 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
235                 <p>We have located your """ + getVendorName() + """ account.</p>
236                 <p>Tap 'Done' to process your credentials.</p>
237                         </div>
238         </body>
239         </html>
240         """
241         render contentType: 'text/html', data: html
242 }
243
244 def receivedToken() {
245         log.debug "In receivedToken"
246
247         def html = """
248         <!DOCTYPE html>
249         <html>
250         <head>
251         <meta name="viewport" content="width=50%,height=50%,  user-scalable = yes">
252         <title>Withings Connection</title>
253         <style type="text/css">
254             @font-face {
255                 font-family: 'Swiss 721 W01 Thin';
256                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
257                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
258                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
259                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
260                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
261                 font-weight: normal;
262                 font-style: normal;
263             }
264             @font-face {
265                 font-family: 'Swiss 721 W01 Light';
266                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
267                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
268                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
269                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
270                      url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
271                 font-weight: normal;
272                 font-style: normal;
273             }
274             .container {
275                 width: 560px;
276                 padding: 40px;
277                 /*background: #eee;*/
278                 text-align: center;
279             }
280             img {
281                 vertical-align: middle;
282             }
283             img:nth-child(2) {
284                 margin: 0 30px;
285             }
286             p {
287                 font-size: 2.2em;
288                 font-family: 'Swiss 721 W01 Thin';
289                 text-align: center;
290                 color: #666666;
291                 padding: 0 40px;
292                 margin-bottom: 0;
293             }
294         /*
295             p:last-child {
296                 margin-top: 0px;
297             }
298         */
299             span {
300                 font-family: 'Swiss 721 W01 Light';
301             }
302         </style>
303         </head>
304         <body>
305             <div class="container">
306                 <img src=""" + getVendorIcon() + """ alt="Vendor icon" />
307                 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
308                 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
309                 <p>Tap 'Done' to continue to Devices.</p>
310                         </div>
311         </body>
312         </html>
313         """
314         render contentType: 'text/html', data: html
315 }
316
317 String toQueryString(Map m) {
318         return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
319 }
320
321
322 def subscriberIdentifyVerification()
323 {
324         log.debug "In subscriberIdentifyVerification"
325
326         def challengeToken = params.hub.challenge
327
328         render contentType: 'text/plain', data: challengeToken
329 }
330
331 def initialize()
332 {
333         log.debug "Initialized with settings: ${settings}"
334
335         //createAccessToken()
336
337         //state.oauthInitState = UUID.randomUUID().toString()
338
339         settings.devices.each {
340                 def deviceId = it
341
342                 state.deviceDataArr.each {
343                         if ( it.id == deviceId ) {
344                                 switch(it.type) {
345
346                                         case "powerstrip":
347                                                 log.debug "we have a Pivot Power Genius"
348                                                 createPowerstripChildren(it.data) //has sub-devices, so we call out to create kids
349                                                 createWinkSubscription( it.subsPath, it.subsSuff )
350                                                 break
351
352                                         case "sensor_pod":
353                                                 log.debug "we have a Spotter"
354                                                 addChildDevice("wackford", "Quirky Wink Spotter", deviceId, null, [name: it.name, label: it.label, completedSetup: true])
355                                                 createWinkSubscription( it.subsPath, it.subsSuff )
356                                                 break
357
358                                         case "piggy_bank":
359                                                 log.debug "we have a Piggy Bank"
360                                                 addChildDevice("wackford", "Quirky Wink Porkfolio", deviceId, null, [name: it.name, label: it.label, completedSetup: true])
361                                                 createWinkSubscription( it.subsPath, it.subsSuff )
362                                                 break
363
364                                         case "eggtray":
365                                                 log.debug "we have a Egg Minder"
366                                                 addChildDevice("wackford", "Quirky Wink Eggtray", deviceId, null, [name: it.name, label: it.label, completedSetup: true])
367                                                 createWinkSubscription( it.subsPath, it.subsSuff )
368                                                 break
369
370                                         case "cloud_clock":
371                                                 log.debug "we have a Nimbus"
372                                                 createNimbusChildren(it.data) //has sub-devices, so we call out to create kids
373                                                 createWinkSubscription( it.subsPath, it.subsSuff )
374                                                 break
375                                 }
376                         }
377                 }
378         }
379 }
380
381 def getDeviceList()
382 {
383         log.debug "In getDeviceList"
384
385         def deviceList = [:]
386         state.deviceDataArr = []
387
388         apiGet("/users/me/wink_devices") { response ->
389                 response.data.data.each() {
390                         if ( it.powerstrip_id ) {
391                                 deviceList["${it.powerstrip_id}"] = it.name
392                                 state.deviceDataArr.push(['name'    : it.name,
393                                         'id'      : it.powerstrip_id,
394                                         'type'    : "powerstrip",
395                                         'serial'  : it.serial,
396                                         'data'    : it,
397                                         'subsSuff': "/powerstripCallback",
398                                         'subsPath': "/powerstrips/${it.powerstrip_id}/subscriptions"
399                                 ])
400                         }
401
402                         /* stubbing out these out for later release
403                         if ( it.sensor_pod_id ) {
404                                 deviceList["${it.sensor_pod_id}"] = it.name
405                                 state.deviceDataArr.push(['name'   : it.name,
406                                         'id'     : it.sensor_pod_id,
407                                         'type'   : "sensor_pod",
408                                         'serial' : it.serial,
409                                         'data'   : it,
410                                         'subsSuff': "/sensor_podCallback",
411                                         'subsPath': "/sensor_pods/${it.sensor_pod_id}/subscriptions"
412
413                                 ])
414                         }
415
416                         if ( it.piggy_bank_id ) {
417                                 deviceList["${it.piggy_bank_id}"] = it.name
418                                 state.deviceDataArr.push(['name'   : it.name,
419                                                                                   'id'     : it.piggy_bank_id,
420                                                                                   'type'   : "piggy_bank",
421                                                                                   'serial' : it.serial,
422                                                                                   'data'   : it,
423                                                                                   'subsSuff': "/piggy_bankCallback",
424                                                                                   'subsPath': "/piggy_banks/${it.piggy_bank_id}/subscriptions"
425                                 ])
426                         }
427                         if ( it.cloud_clock_id ) {
428                                 deviceList["${it.cloud_clock_id}"] = it.name
429                                 state.deviceDataArr.push(['name'   : it.name,
430                                                                                   'id'     : it.cloud_clock_id,
431                                                                                   'type'   : "cloud_clock",
432                                                                                   'serial' : it.serial,
433                                                                                   'data'   : it,
434                                                                                   'subsSuff': "/cloud_clockCallback",
435                                                                                   'subsPath': "/cloud_clocks/${it.cloud_clock_id}/subscriptions"
436                                 ])
437                         }
438                         if ( it.eggtray_id ) {
439                                 deviceList["${it.eggtray_id}"] = it.name
440                                 state.deviceDataArr.push(['name'   : it.name,
441                                                                                   'id'     : it.eggtray_id,
442                                                                                   'type'   : "eggtray",
443                                                                                   'serial' : it.serial,
444                                                                                   'data'   : it,
445                                                                                   'subsSuff': "/eggtrayCallback",
446                                                                                   'subsPath': "/eggtrays/${it.eggtray_id}/subscriptions"
447                                 ])
448                         } */
449
450
451                 }
452         }
453         return deviceList
454 }
455
456 private removeChildDevices(delete)
457 {
458         log.debug "In removeChildDevices"
459
460         log.debug "deleting ${delete.size()} devices"
461
462         delete.each {
463                 deleteChildDevice(it.deviceNetworkId)
464         }
465 }
466
467 def uninstalled()
468 {
469         log.debug "In uninstalled"
470
471         removeWinkSubscriptions()
472
473         removeChildDevices(getChildDevices())
474 }
475
476 def updateWinkSubscriptions()
477 {       //since we don't know when wink subscription dies, we'll delete and recreate on every poll
478         log.debug "In updateWinkSubscriptions"
479
480         state.deviceDataArr.each() {
481                 if (it.subsPath) {
482                         def path = it.subsPath
483                         def suffix = it.subsSuff
484                         apiGet(it.subsPath) { response ->
485                                 response.data.data.each {
486                                         if ( it.subscription_id ) {
487                                                 deleteWinkSubscription(path + "/", it.subscription_id)
488                                                 createWinkSubscription(path, suffix)
489                                         }
490                                 }
491                         }
492                 }
493         }
494 }
495
496 def createWinkSubscription(path, suffix)
497 {
498         log.debug "In createWinkSubscription"
499
500         def callbackUrl = buildCallbackUrl(suffix)
501
502         httpPostJson([
503                 uri : apiUrl(),
504                 path: path,
505                 body: ['callback': callbackUrl],
506                 headers : ['Authorization' : 'Bearer ' + state.vendorAccessToken]
507         ],)
508                 {       response ->
509                         log.debug "Created subscription ID ${response.data.data.subscription_id}"
510                 }
511 }
512
513 def deleteWinkSubscription(path, subscriptionId)
514 {
515         log.debug "Deleting the wink subscription ${subscriptionId}"
516
517         httpDelete([
518                 uri : apiUrl(),
519                 path: path + subscriptionId,
520                 headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ]
521         ],)
522                 {       response ->
523                         log.debug "Subscription ${subscriptionId} deleted"
524                 }
525 }
526
527
528 def removeWinkSubscriptions()
529 {
530         log.debug "In removeSubscriptions"
531
532         try {
533                 state.deviceDataArr.each() {
534                         if (it.subsPath) {
535                                 def path = it.subsPath
536                                 apiGet(it.subsPath) { response ->
537                                         response.data.data.each {
538                                                 if ( it.subscription_id ) {
539                                                         deleteWinkSubscription(path + "/", it.subscription_id)
540                                                 }
541                                         }
542                                 }
543                         }
544                 }
545         } catch (groovyx.net.http.HttpResponseException e) {
546                 log.warn "Caught HttpResponseException: $e, with status: ${e.statusCode}"
547         }
548 }
549
550 def buildCallbackUrl(suffix)
551 {
552         log.debug "In buildRedirectUrl"
553
554         def serverUrl = getServerUrl()
555         return serverUrl + "/api/token/${state.accessToken}/smartapps/installations/${app.id}" + suffix
556 }
557
558 def createChildDevice(deviceFile, dni, name, label)
559 {
560         log.debug "In createChildDevice"
561
562         try {
563                 def existingDevice = getChildDevice(dni)
564                 if(!existingDevice) {
565                         log.debug "Creating child"
566                         def childDevice = addChildDevice("wackford", deviceFile, dni, null, [name: name, label: label, completedSetup: true])
567                 } else {
568                         log.debug "Device $dni already exists"
569                 }
570         } catch (e) {
571                 log.error "Error creating device: ${e}"
572         }
573
574 }
575
576 def listDevices()
577 {
578         log.debug "In listDevices"
579
580         //login()
581
582         def devices = getDeviceList()
583         log.debug "Device List = ${devices}"
584
585         dynamicPage(name: "listDevices", title: "Choose devices", install: true) {
586                 section("Devices") {
587                         input "devices", "enum", title: "Select Device(s)", required: false, multiple: true, options: devices
588                 }
589         }
590 }
591
592 def apiGet(String path, Closure callback)
593 {
594         httpGet([
595                 uri : apiUrl(),
596                 path : path,
597                 headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ]
598         ],)
599                 {
600                         response ->
601                                 callback.call(response)
602                 }
603 }
604
605 def apiPut(String path, cmd, Closure callback)
606 {
607         httpPutJson([
608                 uri : apiUrl(),
609                 path: path,
610                 body: cmd,
611                 headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ]
612         ],)
613
614                 {
615                         response ->
616                                 callback.call(response)
617                 }
618 }
619
620 def installed() {
621         log.debug "Installed with settings: ${settings}"
622
623         //initialize()
624         listDevices()
625 }
626
627 def updated() {
628         log.debug "Updated with settings: ${settings}"
629
630         //unsubscribe()
631         //unschedule()
632         initialize()
633
634         listDevices()
635 }
636
637
638 def poll(childDevice)
639 {
640         log.debug "In poll"
641         log.debug childDevice
642
643         //login()
644
645         def dni = childDevice.device.deviceNetworkId
646
647         log.debug dni
648
649         def deviceType = null
650
651         state.deviceDataArr.each() {
652                 if (it.id == dni) {
653                         deviceType = it.type
654                 }
655         }
656
657         log.debug "device type is: ${deviceType}"
658
659         switch(deviceType) {    //outlets are polled in unique method not here
660
661                 case "sensor_pod":
662                         log.debug "Polling sensor_pod"
663                         getSensorPodUpdate(childDevice)
664                         log.debug "sensor pod status updated"
665                         break
666
667                 case "piggy_bank":
668                         log.debug "Polling piggy_bank"
669                         getPiggyBankUpdate(childDevice)
670                         log.debug "piggy bank status updated"
671                         break
672
673                 case "eggtray":
674                         log.debug "Polling eggtray"
675                         getEggtrayUpdate(childDevice)
676                         log.debug "eggtray status updated"
677                         break
678
679         }
680         updateWinkSubscriptions()
681 }
682
683 def cToF(temp) {
684         return temp * 1.8 + 32
685 }
686
687 def fToC(temp) {
688         return (temp - 32) / 1.8
689 }
690
691 def dollarize(int money)
692 {
693         def value = money.toString()
694
695         if ( value.length() == 1 ) {
696                 value = "00" + value
697         }
698
699         if ( value.length() == 2 ) {
700                 value = "0" + value
701         }
702
703         def newval = value.substring(0, value.length() - 2) + "." + value.substring(value.length()-2, value.length())
704         value = newval
705
706         def pattern = "\$0.00"
707         def moneyform = new DecimalFormat(pattern)
708         String output = moneyform.format(value.toBigDecimal())
709
710         return output
711 }
712
713 def debugEvent(message, displayEvent) {
714
715         def results = [
716                 name: "appdebug",
717                 descriptionText: message,
718                 displayed: displayEvent
719         ]
720         log.debug "Generating AppDebug Event: ${results}"
721         sendEvent (results)
722
723 }
724
725 /////////////////////////////////////////////////////////////////////////
726 //                       START NIMBUS SPECIFIC CODE HERE
727 /////////////////////////////////////////////////////////////////////////
728 def createNimbusChildren(deviceData)
729 {
730         log.debug "In createNimbusChildren"
731
732         def nimbusName = deviceData.name
733         def deviceFile = "Quirky-Wink-Nimbus"
734         def index = 1
735         deviceData.dials.each {
736                 log.debug "creating dial device for ${it.dial_id}"
737                 def dialName = "Dial ${index}"
738                 def dialLabel = "${nimbusName} ${dialName}"
739                 createChildDevice( deviceFile, it.dial_id, dialName, dialLabel )
740                 index++
741         }
742 }
743
744 def cloud_clockEventHandler()
745 {
746         log.debug "In Nimbus Event Handler..."
747
748         def json = request.JSON
749         def dials = json.dials
750
751         def html = """{"code":200,"message":"OK"}"""
752         render contentType: 'application/json', data: html
753
754         if ( dials ) {
755                 dials.each() {
756                         def childDevice = getChildDevice(it.dial_id)
757                         childDevice?.sendEvent( name : "dial", value : it.label , unit : "" )
758                         childDevice?.sendEvent( name : "info", value : it.name , unit : "" )
759                 }
760         }
761 }
762
763 def pollNimbus(dni)
764 {
765
766         log.debug "In pollNimbus using dni # ${dni}"
767
768         //login()
769
770         def dials = null
771
772         apiGet("/users/me/wink_devices") { response ->
773
774                 response.data.data.each() {
775                         if (it.cloud_clock_id  ) {
776                                 log.debug "Found Nimbus #" + it.cloud_clock_id
777                                 dials   = it.dials
778                                 //log.debug dials
779                         }
780                 }
781         }
782
783         if ( dials ) {
784                 dials.each() {
785                         def childDevice = getChildDevice(it.dial_id)
786
787                         childDevice?.sendEvent( name : "dial", value : it.label , unit : "" )
788                         childDevice?.sendEvent( name : "info", value : it.name , unit : "" )
789
790                         //Change the tile/icon to what info is being displayed
791                         switch(it.name) {
792                                 case "Weather":
793                                         childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-weather")
794                                         break
795                                 case "Traffic":
796                                         childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-traffic")
797                                         break
798                                 case "Time":
799                                         childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-time")
800                                         break
801                                 case "Twitter":
802                                         childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-twitter")
803                                         break
804                                 case "Calendar":
805                                         childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-calendar")
806                                         break
807                                 case "Email":
808                                         childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-mail")
809                                         break
810                                 case "Facebook":
811                                         childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-facebook")
812                                         break
813                                 case "Instagram":
814                                         childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-instagram")
815                                         break
816                                 case "Fitbit":
817                                         childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-fitbit")
818                                         break
819                                 case "Egg Minder":
820                                         childDevice?.setIcon("dial", "dial",  "st.quirky.egg-minder.quirky-egg-device")
821                                         break
822                                 case "Porkfolio":
823                                         childDevice?.setIcon("dial", "dial",  "st.quirky.porkfolio.quirky-porkfolio-side")
824                                         break
825                         }
826                         childDevice.save()
827                 }
828         }
829         return
830 }
831
832 /////////////////////////////////////////////////////////////////////////
833 //                       START EGG TRAY SPECIFIC CODE HERE
834 /////////////////////////////////////////////////////////////////////////
835 def getEggtrayUpdate(childDevice)
836 {
837         log.debug "In getEggtrayUpdate"
838
839         apiGet("/eggtrays/" + childDevice.device.deviceNetworkId) { response ->
840
841                 def data = response.data.data
842                 def freshnessPeriod = data.freshness_period
843                 def trayName = data.name
844                 log.debug data
845
846                 int totalEggs = 0
847                 int oldEggs = 0
848
849                 def now = new Date()
850                 def nowUnixTime = now.getTime()/1000
851
852                 data.eggs.each() { it ->
853                         if (it != 0)
854                         {
855                                 totalEggs++
856
857                                 def eggArriveDate = it
858                                 def eggStaleDate = eggArriveDate + freshnessPeriod
859                                 if ( nowUnixTime > eggStaleDate ){
860                                         oldEggs++
861                                 }
862                         }
863                 }
864
865                 int freshEggs = totalEggs - oldEggs
866
867                 if ( oldEggs > 0 ) {
868                         childDevice?.sendEvent(name:"inventory",value:"haveBadEgg")
869                         def msg = "${trayName} says: "
870                         msg+= "Did you know that all it takes is one bad egg? "
871                         msg+= "And it looks like I found one.\n\n"
872                         msg+= "You should probably run an Egg Report before you use any eggs."
873                         sendNotificationEvent(msg)
874                 }
875                 if ( totalEggs == 0 ) {
876                         childDevice?.sendEvent(name:"inventory",value:"noEggs")
877                         sendNotificationEvent("${trayName} says:\n'Oh no, I'm out of eggs!'")
878                         sendNotificationEvent(msg)
879                 }
880                 if ( (freshEggs == totalEggs) && (totalEggs != 0) ) {
881                         childDevice?.sendEvent(name:"inventory",value:"goodEggs")
882                 }
883                 childDevice?.sendEvent( name : "totalEggs", value : totalEggs , unit : "" )
884                 childDevice?.sendEvent( name : "freshEggs", value : freshEggs , unit : "" )
885                 childDevice?.sendEvent( name : "oldEggs", value : oldEggs , unit : "" )
886         }
887 }
888
889 def runEggReport(childDevice)
890 {
891         apiGet("/eggtrays/" + childDevice.device.deviceNetworkId) { response ->
892
893                 def data = response.data.data
894                 def trayName = data.name
895                 def freshnessPeriod = data.freshness_period
896                 def now = new Date()
897                 def nowUnixTime = now.getTime()/1000
898
899                 def eggArray = []
900
901                 def i = 0
902
903                 data.eggs.each()  { it ->
904                         if (it != 0 ) {
905                                 def eggArriveDate = it
906                                 def eggStaleDate = eggArriveDate + freshnessPeriod
907                                 if ( nowUnixTime > eggStaleDate ){
908                                         eggArray.push("Bad  ")
909                                 } else {
910                                         eggArray.push("Good ")
911                                 }
912                         } else {
913                                 eggArray.push("Empty")
914                         }
915                         i++
916                 }
917
918                 def msg = " Egg Report for ${trayName}\n\n"
919                 msg+= "#7:${eggArray[6]}    #14:${eggArray[13]}\n"
920                 msg+= "#6:${eggArray[5]}    #13:${eggArray[12]}\n"
921                 msg+= "#5:${eggArray[4]}    #12:${eggArray[11]}\n"
922                 msg+= "#4:${eggArray[3]}    #11:${eggArray[10]}\n"
923                 msg+= "#3:${eggArray[2]}    #10:${eggArray[9]}\n"
924                 msg+= "#2:${eggArray[1]}      #9:${eggArray[8]}\n"
925                 msg+= "#1:${eggArray[0]}      #8:${eggArray[7]}\n"
926                 msg+= "                 +\n"
927                 msg+= "              ===\n"
928                 msg+= "              ==="
929
930                 sendNotificationEvent(msg)
931         }
932 }
933
934 def eggtrayEventHandler()
935 {
936         log.debug "In  eggtrayEventHandler..."
937
938         def json = request.JSON
939         def dni = getChildDevice(json.eggtray_id)
940
941         log.debug "event received from ${dni}"
942
943         poll(dni) //sometimes events are stale, poll for all latest states
944
945
946         def html = """{"code":200,"message":"OK"}"""
947         render contentType: 'application/json', data: html
948 }
949
950 /////////////////////////////////////////////////////////////////////////
951 //                       START PIGGY BANK SPECIFIC CODE HERE
952 /////////////////////////////////////////////////////////////////////////
953 def getPiggyBankUpdate(childDevice)
954 {
955         apiGet("/piggy_banks/" + childDevice.device.deviceNetworkId) { response ->
956                 def status = response.data.data
957                 def alertData = status.triggers
958
959                 if (( alertData.enabled ) && ( state.lastCheckTime )) {
960                         if ( alertData.triggered_at[0].toInteger() > state.lastCheckTime ) {
961                                 childDevice?.sendEvent(name:"acceleration",value:"active",unit:"")
962                         } else {
963                                 childDevice?.sendEvent(name:"acceleration",value:"inactive",unit:"")
964                         }
965                 }
966
967                 childDevice?.sendEvent(name:"goal",value:dollarize(status.savings_goal),unit:"")
968
969                 childDevice?.sendEvent(name:"balance",value:dollarize(status.balance),unit:"")
970
971                 def now = new Date()
972                 def longTime = now.getTime()/1000
973                 state.lastCheckTime = longTime.toInteger()
974         }
975 }
976
977 def piggy_bankEventHandler()
978 {
979         log.debug "In  piggy_bankEventHandler..."
980
981         def json = request.JSON
982         def dni = getChildDevice(json.piggy_bank_id)
983
984         log.debug "event received from ${dni}"
985
986         poll(dni) //sometimes events are stale, poll for all latest states
987
988
989         def html = """{"code":200,"message":"OK"}"""
990         render contentType: 'application/json', data: html
991 }
992
993 /////////////////////////////////////////////////////////////////////////
994 //                       START SENSOR POD SPECIFIC CODE HERE
995 /////////////////////////////////////////////////////////////////////////
996 def getSensorPodUpdate(childDevice)
997 {
998         apiGet("/sensor_pods/" + childDevice.device.deviceNetworkId) { response ->
999                 def status = response.data.data.last_reading
1000
1001                 status.loudness ? childDevice?.sendEvent(name:"sound",value:"active",unit:"") :
1002                         childDevice?.sendEvent(name:"sound",value:"inactive",unit:"")
1003
1004                 status.brightness ? childDevice?.sendEvent(name:"light",value:"active",unit:"") :
1005                         childDevice?.sendEvent(name:"light",value:"inactive",unit:"")
1006
1007                 status.vibration ? childDevice?.sendEvent(name:"acceleration",value:"active",unit:"") :
1008                         childDevice?.sendEvent(name:"acceleration",value:"inactive",unit:"")
1009
1010                 status.external_power ? childDevice?.sendEvent(name:"powerSource",value:"powered",unit:"") :
1011                         childDevice?.sendEvent(name:"powerSource",value:"battery",unit:"")
1012
1013                 childDevice?.sendEvent(name:"humidity",value:status.humidity,unit:"")
1014
1015                 childDevice?.sendEvent(name:"battery",value:(status.battery * 100).toInteger(),unit:"")
1016
1017                 childDevice?.sendEvent(name:"temperature",value:cToF(status.temperature),unit:"F")
1018         }
1019 }
1020
1021 def sensor_podEventHandler()
1022 {
1023         log.debug "In  sensor_podEventHandler..."
1024
1025         def json = request.JSON
1026         //log.debug json
1027         def dni = getChildDevice(json.sensor_pod_id)
1028
1029         log.debug "event received from ${dni}"
1030
1031         poll(dni)   //sometimes events are stale, poll for all latest states
1032
1033
1034         def html = """{"code":200,"message":"OK"}"""
1035         render contentType: 'application/json', data: html
1036 }
1037
1038 /////////////////////////////////////////////////////////////////////////
1039 //                       START POWERSTRIP SPECIFIC CODE HERE
1040 /////////////////////////////////////////////////////////////////////////
1041
1042 def powerstripEventHandler()
1043 {
1044         log.debug "In Powerstrip Event Handler..."
1045
1046         def json = request.JSON
1047         def outlets = json.outlets
1048
1049         outlets.each() {
1050                 def dni = getChildDevice(it.outlet_id)
1051                 pollOutlet(dni)   //sometimes events are stale, poll for all latest states
1052         }
1053
1054         def html = """{"code":200,"message":"OK"}"""
1055         render contentType: 'application/json', data: html
1056 }
1057
1058 def pollOutlet(childDevice)
1059 {
1060         log.debug "In pollOutlet"
1061
1062         //login()
1063
1064         log.debug "Polling powerstrip"
1065         apiGet("/outlets/" + childDevice.device.deviceNetworkId) { response ->
1066                 def data = response.data.data
1067                 data.powered ? childDevice?.sendEvent(name:"switch",value:"on") :
1068                         childDevice?.sendEvent(name:"switch",value:"off")
1069         }
1070 }
1071
1072 def on(childDevice)
1073 {
1074         //login()
1075
1076         apiPut("/outlets/" + childDevice.device.deviceNetworkId, [powered : true]) { response ->
1077                 def data = response.data.data
1078                 log.debug "Sending 'on' to device"
1079         }
1080 }
1081
1082 def off(childDevice)
1083 {
1084         //login()
1085
1086         apiPut("/outlets/" + childDevice.device.deviceNetworkId, [powered : false]) { response ->
1087                 def data = response.data.data
1088                 log.debug "Sending 'off' to device"
1089         }
1090 }
1091
1092 def createPowerstripChildren(deviceData)
1093 {
1094         log.debug "In createPowerstripChildren"
1095
1096         def powerstripName = deviceData.name
1097         def deviceFile = "Quirky Wink Powerstrip"
1098
1099         deviceData.outlets.each {
1100                 createChildDevice( deviceFile, it.outlet_id, it.name, "$powerstripName ${it.name}" )
1101         }
1102 }
1103
1104 private Boolean canInstallLabs()
1105 {
1106         return hasAllHubsOver("000.011.00603")
1107 }
1108
1109 private Boolean hasAllHubsOver(String desiredFirmware)
1110 {
1111         return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
1112 }
1113
1114 private List getRealHubFirmwareVersions()
1115 {
1116         return location.hubs*.firmwareVersionString.findAll { it }
1117 }
1118