Checking in all the SmartThings apps; both official and third-party.
[smartapps.git] / official / quirky-connect.groovy
diff --git a/official/quirky-connect.groovy b/official/quirky-connect.groovy
new file mode 100755 (executable)
index 0000000..30f5b55
--- /dev/null
@@ -0,0 +1,1118 @@
+/**
+ *  Quirky (Connect)
+ *
+ *  Author: todd@wackford.net
+ *  Date: 2014-02-15
+ *
+ *  Update: 2014-02-22
+ *                     Added eggtray
+ *                     Added device specific methods called from poll (versus in poll)
+ *
+ *  Update2:2014-02-22
+ *                     Added nimbus
+ *
+ *  Update3:2014-02-26
+ *                     Improved eggtray integration
+ *                     Added notifications to hello home
+ *                     Introduced Quirky Eggtray specific icons (Thanks to Dane)
+ *                     Added an Egg Report that outputs to hello home.
+ *                     Switched to Dan Lieberman's client and secret
+ *                     Still not browser flow (next update?)
+ *
+ *  Update4:2014-03-08
+ *                     Added Browser Flow OAuth
+ *
+ *
+ *  Update5:2014-03-14
+ *                     Added dynamic icon/tile updating to the nimbus. Changes the device icon from app.
+ *
+ *  Update6:2014-03-31
+ *                     Stubbed out creation and choice of nimbus, eggtray and porkfolio per request.
+ *
+ *  Update7:2014-04-01
+ *          Renamed to 'Quirky (Connect)' and updated device names
+ *
+ *  Update8:2014-04-08 (dlieberman)
+ *         Stubbed out Spotter
+ *
+ *  Update9:2014-04-08 (twackford)
+ *         resubscribe to events on each poll
+ */
+
+import java.text.DecimalFormat
+
+// Wink API
+private apiUrl()                       { "https://winkapi.quirky.com/" }
+private getVendorName()        { "Quirky Wink" }
+private getVendorAuthPath()    { "https://winkapi.quirky.com/oauth2/authorize?" }
+private getVendorTokenPath(){ "https://winkapi.quirky.com/oauth2/token?" }
+private getVendorIcon()                { "https://s3.amazonaws.com/smartthings-device-icons/custom/quirky/quirky-device@2x.png" }
+private getClientId()          { appSettings.clientId }
+private getClientSecret()      { appSettings.clientSecret }
+private getServerUrl()                 { appSettings.serverUrl }
+
+definition(
+    name: "Quirky (Connect)",
+    namespace: "wackford",
+    author: "SmartThings",
+    description: "Connect your Quirky to SmartThings.",
+    category: "SmartThings Labs",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png",
+    singleInstance: true
+) {
+       appSetting "clientId"
+       appSetting "clientSecret"
+    appSetting "serverUrl"
+}
+
+preferences {
+       page(name: "Credentials", title: "Fetch OAuth2 Credentials", content: "authPage", install: false)
+       page(name: "listDevices", title: "Quirky Devices", content: "listDevices", install: false)
+}
+
+mappings {
+       path("/receivedToken")          { action:[ POST: "receivedToken",                       GET: "receivedToken"] }
+       path("/receiveToken")           { action:[ POST: "receiveToken",                        GET: "receiveToken"] }
+       path("/powerstripCallback")     { action:[ POST: "powerstripEventHandler",      GET: "subscriberIdentifyVerification"]}
+       path("/sensor_podCallback") { action:[ POST: "sensor_podEventHandler",  GET: "subscriberIdentifyVerification"]}
+       path("/piggy_bankCallback") { action:[ POST: "piggy_bankEventHandler",  GET: "subscriberIdentifyVerification"]}
+       path("/eggtrayCallback")        { action:[ POST: "eggtrayEventHandler",         GET: "subscriberIdentifyVerification"]}
+       path("/cloud_clockCallback"){ action:[ POST: "cloud_clockEventHandler", GET: "subscriberIdentifyVerification"]}
+}
+
+def authPage() {
+       log.debug "In authPage"
+       if(canInstallLabs()) {
+               def description = null
+
+               if (state.vendorAccessToken == null) {
+                       log.debug "About to create access token."
+
+                       createAccessToken()
+                       description = "Tap to enter Credentials."
+
+                       def redirectUrl = oauthInitUrl()
+
+
+                       return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) {
+                               section { 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 }
+                       }
+               }
+       }
+       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"."""
+
+
+               return dynamicPage(name:"Credentials", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
+                       section {
+                               paragraph "$upgradeNeeded"
+                       }
+               }
+
+       }
+}
+
+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") ]
+
+       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(),
+               grant_type: "authorization_code",
+               code: params.code ]
+
+       def tokenUrl = getVendorTokenPath() + toQueryString(oauthParams)
+       def params = [
+               uri: tokenUrl,
+       ]
+
+       /* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */
+       httpPost(params) { response ->
+
+               def data = response.data.data
+
+               state.vendorRefreshToken = data.refresh_token //these may need to be adjusted depending on depth of returned data
+               state.vendorAccessToken = data.access_token
+       }
+
+       if ( !state.vendorAccessToken ) {  //We didn't get an access token, bail on install
+               return
+       }
+
+       /* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */
+
+       def html = """
+        <!DOCTYPE html>
+        <html>
+        <head>
+        <meta name="viewport" content="width=50%,height=50%,  user-scalable = yes">
+        <title>${getVendorName()} Connection</title>
+        <style type="text/css">
+            @font-face {
+                font-family: 'Swiss 721 W01 Thin';
+                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
+                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
+                font-weight: normal;
+                font-style: normal;
+            }
+            @font-face {
+                font-family: 'Swiss 721 W01 Light';
+                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
+                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
+                font-weight: normal;
+                font-style: normal;
+            }
+            .container {
+                width: 560px;
+                padding: 40px;
+                /*background: #eee;*/
+                text-align: center;
+            }
+            img {
+                vertical-align: middle;
+            }
+            img:nth-child(2) {
+                margin: 0 30px;
+            }
+            p {
+                font-size: 2.2em;
+                font-family: 'Swiss 721 W01 Thin';
+                text-align: center;
+                color: #666666;
+                padding: 0 40px;
+                margin-bottom: 0;
+            }
+        /*
+            p:last-child {
+                margin-top: 0px;
+            }
+        */
+            span {
+                font-family: 'Swiss 721 W01 Light';
+            }
+        </style>
+        </head>
+        <body>
+            <div class="container">
+                <img src=""" + getVendorIcon() + """ alt="Vendor icon" />
+                <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
+                <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
+                <p>We have located your """ + getVendorName() + """ account.</p>
+                <p>Tap 'Done' to process your credentials.</p>
+                       </div>
+        </body>
+        </html>
+        """
+       render contentType: 'text/html', data: html
+}
+
+def receivedToken() {
+       log.debug "In receivedToken"
+
+       def html = """
+        <!DOCTYPE html>
+        <html>
+        <head>
+        <meta name="viewport" content="width=50%,height=50%,  user-scalable = yes">
+        <title>Withings Connection</title>
+        <style type="text/css">
+            @font-face {
+                font-family: 'Swiss 721 W01 Thin';
+                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
+                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
+                font-weight: normal;
+                font-style: normal;
+            }
+            @font-face {
+                font-family: 'Swiss 721 W01 Light';
+                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
+                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
+                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
+                font-weight: normal;
+                font-style: normal;
+            }
+            .container {
+                width: 560px;
+                padding: 40px;
+                /*background: #eee;*/
+                text-align: center;
+            }
+            img {
+                vertical-align: middle;
+            }
+            img:nth-child(2) {
+                margin: 0 30px;
+            }
+            p {
+                font-size: 2.2em;
+                font-family: 'Swiss 721 W01 Thin';
+                text-align: center;
+                color: #666666;
+                padding: 0 40px;
+                margin-bottom: 0;
+            }
+        /*
+            p:last-child {
+                margin-top: 0px;
+            }
+        */
+            span {
+                font-family: 'Swiss 721 W01 Light';
+            }
+        </style>
+        </head>
+        <body>
+            <div class="container">
+                <img src=""" + getVendorIcon() + """ alt="Vendor icon" />
+                <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
+                <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
+                <p>Tap 'Done' to continue to Devices.</p>
+                       </div>
+        </body>
+        </html>
+        """
+       render contentType: 'text/html', data: html
+}
+
+String toQueryString(Map m) {
+       return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
+}
+
+
+def subscriberIdentifyVerification()
+{
+       log.debug "In subscriberIdentifyVerification"
+
+       def challengeToken = params.hub.challenge
+
+       render contentType: 'text/plain', data: challengeToken
+}
+
+def initialize()
+{
+       log.debug "Initialized with settings: ${settings}"
+
+       //createAccessToken()
+
+       //state.oauthInitState = UUID.randomUUID().toString()
+
+       settings.devices.each {
+               def deviceId = it
+
+               state.deviceDataArr.each {
+                       if ( it.id == deviceId ) {
+                               switch(it.type) {
+
+                                       case "powerstrip":
+                                               log.debug "we have a Pivot Power Genius"
+                                               createPowerstripChildren(it.data) //has sub-devices, so we call out to create kids
+                                               createWinkSubscription( it.subsPath, it.subsSuff )
+                                               break
+
+                                       case "sensor_pod":
+                                               log.debug "we have a Spotter"
+                                               addChildDevice("wackford", "Quirky Wink Spotter", deviceId, null, [name: it.name, label: it.label, completedSetup: true])
+                                               createWinkSubscription( it.subsPath, it.subsSuff )
+                                               break
+
+                                       case "piggy_bank":
+                                               log.debug "we have a Piggy Bank"
+                                               addChildDevice("wackford", "Quirky Wink Porkfolio", deviceId, null, [name: it.name, label: it.label, completedSetup: true])
+                                               createWinkSubscription( it.subsPath, it.subsSuff )
+                                               break
+
+                                       case "eggtray":
+                                               log.debug "we have a Egg Minder"
+                                               addChildDevice("wackford", "Quirky Wink Eggtray", deviceId, null, [name: it.name, label: it.label, completedSetup: true])
+                                               createWinkSubscription( it.subsPath, it.subsSuff )
+                                               break
+
+                                       case "cloud_clock":
+                                               log.debug "we have a Nimbus"
+                                               createNimbusChildren(it.data) //has sub-devices, so we call out to create kids
+                                               createWinkSubscription( it.subsPath, it.subsSuff )
+                                               break
+                               }
+                       }
+               }
+       }
+}
+
+def getDeviceList()
+{
+       log.debug "In getDeviceList"
+
+       def deviceList = [:]
+       state.deviceDataArr = []
+
+       apiGet("/users/me/wink_devices") { response ->
+               response.data.data.each() {
+                       if ( it.powerstrip_id ) {
+                               deviceList["${it.powerstrip_id}"] = it.name
+                               state.deviceDataArr.push(['name'    : it.name,
+                                       'id'      : it.powerstrip_id,
+                                       'type'    : "powerstrip",
+                                       'serial'  : it.serial,
+                                       'data'    : it,
+                                       'subsSuff': "/powerstripCallback",
+                                       'subsPath': "/powerstrips/${it.powerstrip_id}/subscriptions"
+                               ])
+                       }
+
+                       /* stubbing out these out for later release
+                       if ( it.sensor_pod_id ) {
+                               deviceList["${it.sensor_pod_id}"] = it.name
+                               state.deviceDataArr.push(['name'   : it.name,
+                                       'id'     : it.sensor_pod_id,
+                                       'type'   : "sensor_pod",
+                                       'serial' : it.serial,
+                                       'data'   : it,
+                                       'subsSuff': "/sensor_podCallback",
+                                       'subsPath': "/sensor_pods/${it.sensor_pod_id}/subscriptions"
+
+                               ])
+                       }
+
+                       if ( it.piggy_bank_id ) {
+                               deviceList["${it.piggy_bank_id}"] = it.name
+                               state.deviceDataArr.push(['name'   : it.name,
+                                                                                 'id'     : it.piggy_bank_id,
+                                                                                 'type'   : "piggy_bank",
+                                                                                 'serial' : it.serial,
+                                                                                 'data'   : it,
+                                                                                 'subsSuff': "/piggy_bankCallback",
+                                                                                 'subsPath': "/piggy_banks/${it.piggy_bank_id}/subscriptions"
+                               ])
+                       }
+                       if ( it.cloud_clock_id ) {
+                               deviceList["${it.cloud_clock_id}"] = it.name
+                               state.deviceDataArr.push(['name'   : it.name,
+                                                                                 'id'     : it.cloud_clock_id,
+                                                                                 'type'   : "cloud_clock",
+                                                                                 'serial' : it.serial,
+                                                                                 'data'   : it,
+                                                                                 'subsSuff': "/cloud_clockCallback",
+                                                                                 'subsPath': "/cloud_clocks/${it.cloud_clock_id}/subscriptions"
+                               ])
+                       }
+                       if ( it.eggtray_id ) {
+                               deviceList["${it.eggtray_id}"] = it.name
+                               state.deviceDataArr.push(['name'   : it.name,
+                                                                                 'id'     : it.eggtray_id,
+                                                                                 'type'   : "eggtray",
+                                                                                 'serial' : it.serial,
+                                                                                 'data'   : it,
+                                                                                 'subsSuff': "/eggtrayCallback",
+                                                                                 'subsPath': "/eggtrays/${it.eggtray_id}/subscriptions"
+                               ])
+                       } */
+
+
+               }
+       }
+       return deviceList
+}
+
+private removeChildDevices(delete)
+{
+       log.debug "In removeChildDevices"
+
+       log.debug "deleting ${delete.size()} devices"
+
+       delete.each {
+               deleteChildDevice(it.deviceNetworkId)
+       }
+}
+
+def uninstalled()
+{
+       log.debug "In uninstalled"
+
+       removeWinkSubscriptions()
+
+       removeChildDevices(getChildDevices())
+}
+
+def updateWinkSubscriptions()
+{      //since we don't know when wink subscription dies, we'll delete and recreate on every poll
+       log.debug "In updateWinkSubscriptions"
+
+       state.deviceDataArr.each() {
+               if (it.subsPath) {
+                       def path = it.subsPath
+                       def suffix = it.subsSuff
+                       apiGet(it.subsPath) { response ->
+                               response.data.data.each {
+                                       if ( it.subscription_id ) {
+                                               deleteWinkSubscription(path + "/", it.subscription_id)
+                                               createWinkSubscription(path, suffix)
+                                       }
+                               }
+                       }
+               }
+       }
+}
+
+def createWinkSubscription(path, suffix)
+{
+       log.debug "In createWinkSubscription"
+
+       def callbackUrl = buildCallbackUrl(suffix)
+
+       httpPostJson([
+               uri : apiUrl(),
+               path: path,
+               body: ['callback': callbackUrl],
+               headers : ['Authorization' : 'Bearer ' + state.vendorAccessToken]
+       ],)
+               {       response ->
+                       log.debug "Created subscription ID ${response.data.data.subscription_id}"
+               }
+}
+
+def deleteWinkSubscription(path, subscriptionId)
+{
+       log.debug "Deleting the wink subscription ${subscriptionId}"
+
+       httpDelete([
+               uri : apiUrl(),
+               path: path + subscriptionId,
+               headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ]
+       ],)
+               {       response ->
+                       log.debug "Subscription ${subscriptionId} deleted"
+               }
+}
+
+
+def removeWinkSubscriptions()
+{
+       log.debug "In removeSubscriptions"
+
+       try {
+               state.deviceDataArr.each() {
+                       if (it.subsPath) {
+                               def path = it.subsPath
+                               apiGet(it.subsPath) { response ->
+                                       response.data.data.each {
+                                               if ( it.subscription_id ) {
+                                                       deleteWinkSubscription(path + "/", it.subscription_id)
+                                               }
+                                       }
+                               }
+                       }
+               }
+       } catch (groovyx.net.http.HttpResponseException e) {
+               log.warn "Caught HttpResponseException: $e, with status: ${e.statusCode}"
+       }
+}
+
+def buildCallbackUrl(suffix)
+{
+       log.debug "In buildRedirectUrl"
+
+       def serverUrl = getServerUrl()
+       return serverUrl + "/api/token/${state.accessToken}/smartapps/installations/${app.id}" + suffix
+}
+
+def createChildDevice(deviceFile, dni, name, label)
+{
+       log.debug "In createChildDevice"
+
+       try {
+               def existingDevice = getChildDevice(dni)
+               if(!existingDevice) {
+                       log.debug "Creating child"
+                       def childDevice = addChildDevice("wackford", deviceFile, dni, null, [name: name, label: label, completedSetup: true])
+               } else {
+                       log.debug "Device $dni already exists"
+               }
+       } catch (e) {
+               log.error "Error creating device: ${e}"
+       }
+
+}
+
+def listDevices()
+{
+       log.debug "In listDevices"
+
+       //login()
+
+       def devices = getDeviceList()
+       log.debug "Device List = ${devices}"
+
+       dynamicPage(name: "listDevices", title: "Choose devices", install: true) {
+               section("Devices") {
+                       input "devices", "enum", title: "Select Device(s)", required: false, multiple: true, options: devices
+               }
+       }
+}
+
+def apiGet(String path, Closure callback)
+{
+       httpGet([
+               uri : apiUrl(),
+               path : path,
+               headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ]
+       ],)
+               {
+                       response ->
+                               callback.call(response)
+               }
+}
+
+def apiPut(String path, cmd, Closure callback)
+{
+       httpPutJson([
+               uri : apiUrl(),
+               path: path,
+               body: cmd,
+               headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ]
+       ],)
+
+               {
+                       response ->
+                               callback.call(response)
+               }
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+
+       //initialize()
+       listDevices()
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+
+       //unsubscribe()
+       //unschedule()
+       initialize()
+
+       listDevices()
+}
+
+
+def poll(childDevice)
+{
+       log.debug "In poll"
+       log.debug childDevice
+
+       //login()
+
+       def dni = childDevice.device.deviceNetworkId
+
+       log.debug dni
+
+       def deviceType = null
+
+       state.deviceDataArr.each() {
+               if (it.id == dni) {
+                       deviceType = it.type
+               }
+       }
+
+       log.debug "device type is: ${deviceType}"
+
+       switch(deviceType) {    //outlets are polled in unique method not here
+
+               case "sensor_pod":
+                       log.debug "Polling sensor_pod"
+                       getSensorPodUpdate(childDevice)
+                       log.debug "sensor pod status updated"
+                       break
+
+               case "piggy_bank":
+                       log.debug "Polling piggy_bank"
+                       getPiggyBankUpdate(childDevice)
+                       log.debug "piggy bank status updated"
+                       break
+
+               case "eggtray":
+                       log.debug "Polling eggtray"
+                       getEggtrayUpdate(childDevice)
+                       log.debug "eggtray status updated"
+                       break
+
+       }
+       updateWinkSubscriptions()
+}
+
+def cToF(temp) {
+       return temp * 1.8 + 32
+}
+
+def fToC(temp) {
+       return (temp - 32) / 1.8
+}
+
+def dollarize(int money)
+{
+       def value = money.toString()
+
+       if ( value.length() == 1 ) {
+               value = "00" + value
+       }
+
+       if ( value.length() == 2 ) {
+               value = "0" + value
+       }
+
+       def newval = value.substring(0, value.length() - 2) + "." + value.substring(value.length()-2, value.length())
+       value = newval
+
+       def pattern = "\$0.00"
+       def moneyform = new DecimalFormat(pattern)
+       String output = moneyform.format(value.toBigDecimal())
+
+       return output
+}
+
+def debugEvent(message, displayEvent) {
+
+       def results = [
+               name: "appdebug",
+               descriptionText: message,
+               displayed: displayEvent
+       ]
+       log.debug "Generating AppDebug Event: ${results}"
+       sendEvent (results)
+
+}
+
+/////////////////////////////////////////////////////////////////////////
+//                      START NIMBUS SPECIFIC CODE HERE
+/////////////////////////////////////////////////////////////////////////
+def createNimbusChildren(deviceData)
+{
+       log.debug "In createNimbusChildren"
+
+       def nimbusName = deviceData.name
+       def deviceFile = "Quirky-Wink-Nimbus"
+       def index = 1
+       deviceData.dials.each {
+               log.debug "creating dial device for ${it.dial_id}"
+               def dialName = "Dial ${index}"
+               def dialLabel = "${nimbusName} ${dialName}"
+               createChildDevice( deviceFile, it.dial_id, dialName, dialLabel )
+               index++
+       }
+}
+
+def cloud_clockEventHandler()
+{
+       log.debug "In Nimbus Event Handler..."
+
+       def json = request.JSON
+       def dials = json.dials
+
+       def html = """{"code":200,"message":"OK"}"""
+       render contentType: 'application/json', data: html
+
+       if ( dials ) {
+               dials.each() {
+                       def childDevice = getChildDevice(it.dial_id)
+                       childDevice?.sendEvent( name : "dial", value : it.label , unit : "" )
+                       childDevice?.sendEvent( name : "info", value : it.name , unit : "" )
+               }
+       }
+}
+
+def pollNimbus(dni)
+{
+
+       log.debug "In pollNimbus using dni # ${dni}"
+
+       //login()
+
+       def dials = null
+
+       apiGet("/users/me/wink_devices") { response ->
+
+               response.data.data.each() {
+                       if (it.cloud_clock_id  ) {
+                               log.debug "Found Nimbus #" + it.cloud_clock_id
+                               dials   = it.dials
+                               //log.debug dials
+                       }
+               }
+       }
+
+       if ( dials ) {
+               dials.each() {
+                       def childDevice = getChildDevice(it.dial_id)
+
+                       childDevice?.sendEvent( name : "dial", value : it.label , unit : "" )
+                       childDevice?.sendEvent( name : "info", value : it.name , unit : "" )
+
+                       //Change the tile/icon to what info is being displayed
+                       switch(it.name) {
+                               case "Weather":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-weather")
+                                       break
+                               case "Traffic":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-traffic")
+                                       break
+                               case "Time":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-time")
+                                       break
+                               case "Twitter":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-twitter")
+                                       break
+                               case "Calendar":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-calendar")
+                                       break
+                               case "Email":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-mail")
+                                       break
+                               case "Facebook":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-facebook")
+                                       break
+                               case "Instagram":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-instagram")
+                                       break
+                               case "Fitbit":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.nimbus.quirky-nimbus-fitbit")
+                                       break
+                               case "Egg Minder":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.egg-minder.quirky-egg-device")
+                                       break
+                               case "Porkfolio":
+                                       childDevice?.setIcon("dial", "dial",  "st.quirky.porkfolio.quirky-porkfolio-side")
+                                       break
+                       }
+                       childDevice.save()
+               }
+       }
+       return
+}
+
+/////////////////////////////////////////////////////////////////////////
+//                      START EGG TRAY SPECIFIC CODE HERE
+/////////////////////////////////////////////////////////////////////////
+def getEggtrayUpdate(childDevice)
+{
+       log.debug "In getEggtrayUpdate"
+
+       apiGet("/eggtrays/" + childDevice.device.deviceNetworkId) { response ->
+
+               def data = response.data.data
+               def freshnessPeriod = data.freshness_period
+               def trayName = data.name
+               log.debug data
+
+               int totalEggs = 0
+               int oldEggs = 0
+
+               def now = new Date()
+               def nowUnixTime = now.getTime()/1000
+
+               data.eggs.each() { it ->
+                       if (it != 0)
+                       {
+                               totalEggs++
+
+                               def eggArriveDate = it
+                               def eggStaleDate = eggArriveDate + freshnessPeriod
+                               if ( nowUnixTime > eggStaleDate ){
+                                       oldEggs++
+                               }
+                       }
+               }
+
+               int freshEggs = totalEggs - oldEggs
+
+               if ( oldEggs > 0 ) {
+                       childDevice?.sendEvent(name:"inventory",value:"haveBadEgg")
+                       def msg = "${trayName} says: "
+                       msg+= "Did you know that all it takes is one bad egg? "
+                       msg+= "And it looks like I found one.\n\n"
+                       msg+= "You should probably run an Egg Report before you use any eggs."
+                       sendNotificationEvent(msg)
+               }
+               if ( totalEggs == 0 ) {
+                       childDevice?.sendEvent(name:"inventory",value:"noEggs")
+                       sendNotificationEvent("${trayName} says:\n'Oh no, I'm out of eggs!'")
+                       sendNotificationEvent(msg)
+               }
+               if ( (freshEggs == totalEggs) && (totalEggs != 0) ) {
+                       childDevice?.sendEvent(name:"inventory",value:"goodEggs")
+               }
+               childDevice?.sendEvent( name : "totalEggs", value : totalEggs , unit : "" )
+               childDevice?.sendEvent( name : "freshEggs", value : freshEggs , unit : "" )
+               childDevice?.sendEvent( name : "oldEggs", value : oldEggs , unit : "" )
+       }
+}
+
+def runEggReport(childDevice)
+{
+       apiGet("/eggtrays/" + childDevice.device.deviceNetworkId) { response ->
+
+               def data = response.data.data
+               def trayName = data.name
+               def freshnessPeriod = data.freshness_period
+               def now = new Date()
+               def nowUnixTime = now.getTime()/1000
+
+               def eggArray = []
+
+               def i = 0
+
+               data.eggs.each()  { it ->
+                       if (it != 0 ) {
+                               def eggArriveDate = it
+                               def eggStaleDate = eggArriveDate + freshnessPeriod
+                               if ( nowUnixTime > eggStaleDate ){
+                                       eggArray.push("Bad  ")
+                               } else {
+                                       eggArray.push("Good ")
+                               }
+                       } else {
+                               eggArray.push("Empty")
+                       }
+                       i++
+               }
+
+               def msg = " Egg Report for ${trayName}\n\n"
+               msg+= "#7:${eggArray[6]}    #14:${eggArray[13]}\n"
+               msg+= "#6:${eggArray[5]}    #13:${eggArray[12]}\n"
+               msg+= "#5:${eggArray[4]}    #12:${eggArray[11]}\n"
+               msg+= "#4:${eggArray[3]}    #11:${eggArray[10]}\n"
+               msg+= "#3:${eggArray[2]}    #10:${eggArray[9]}\n"
+               msg+= "#2:${eggArray[1]}      #9:${eggArray[8]}\n"
+               msg+= "#1:${eggArray[0]}      #8:${eggArray[7]}\n"
+               msg+= "                 +\n"
+               msg+= "              ===\n"
+               msg+= "              ==="
+
+               sendNotificationEvent(msg)
+       }
+}
+
+def eggtrayEventHandler()
+{
+       log.debug "In  eggtrayEventHandler..."
+
+       def json = request.JSON
+       def dni = getChildDevice(json.eggtray_id)
+
+       log.debug "event received from ${dni}"
+
+       poll(dni) //sometimes events are stale, poll for all latest states
+
+
+       def html = """{"code":200,"message":"OK"}"""
+       render contentType: 'application/json', data: html
+}
+
+/////////////////////////////////////////////////////////////////////////
+//                      START PIGGY BANK SPECIFIC CODE HERE
+/////////////////////////////////////////////////////////////////////////
+def getPiggyBankUpdate(childDevice)
+{
+       apiGet("/piggy_banks/" + childDevice.device.deviceNetworkId) { response ->
+               def status = response.data.data
+               def alertData = status.triggers
+
+               if (( alertData.enabled ) && ( state.lastCheckTime )) {
+                       if ( alertData.triggered_at[0].toInteger() > state.lastCheckTime ) {
+                               childDevice?.sendEvent(name:"acceleration",value:"active",unit:"")
+                       } else {
+                               childDevice?.sendEvent(name:"acceleration",value:"inactive",unit:"")
+                       }
+               }
+
+               childDevice?.sendEvent(name:"goal",value:dollarize(status.savings_goal),unit:"")
+
+               childDevice?.sendEvent(name:"balance",value:dollarize(status.balance),unit:"")
+
+               def now = new Date()
+               def longTime = now.getTime()/1000
+               state.lastCheckTime = longTime.toInteger()
+       }
+}
+
+def piggy_bankEventHandler()
+{
+       log.debug "In  piggy_bankEventHandler..."
+
+       def json = request.JSON
+       def dni = getChildDevice(json.piggy_bank_id)
+
+       log.debug "event received from ${dni}"
+
+       poll(dni) //sometimes events are stale, poll for all latest states
+
+
+       def html = """{"code":200,"message":"OK"}"""
+       render contentType: 'application/json', data: html
+}
+
+/////////////////////////////////////////////////////////////////////////
+//                      START SENSOR POD SPECIFIC CODE HERE
+/////////////////////////////////////////////////////////////////////////
+def getSensorPodUpdate(childDevice)
+{
+       apiGet("/sensor_pods/" + childDevice.device.deviceNetworkId) { response ->
+               def status = response.data.data.last_reading
+
+               status.loudness ? childDevice?.sendEvent(name:"sound",value:"active",unit:"") :
+                       childDevice?.sendEvent(name:"sound",value:"inactive",unit:"")
+
+               status.brightness ? childDevice?.sendEvent(name:"light",value:"active",unit:"") :
+                       childDevice?.sendEvent(name:"light",value:"inactive",unit:"")
+
+               status.vibration ? childDevice?.sendEvent(name:"acceleration",value:"active",unit:"") :
+                       childDevice?.sendEvent(name:"acceleration",value:"inactive",unit:"")
+
+               status.external_power ? childDevice?.sendEvent(name:"powerSource",value:"powered",unit:"") :
+                       childDevice?.sendEvent(name:"powerSource",value:"battery",unit:"")
+
+               childDevice?.sendEvent(name:"humidity",value:status.humidity,unit:"")
+
+               childDevice?.sendEvent(name:"battery",value:(status.battery * 100).toInteger(),unit:"")
+
+               childDevice?.sendEvent(name:"temperature",value:cToF(status.temperature),unit:"F")
+       }
+}
+
+def sensor_podEventHandler()
+{
+       log.debug "In  sensor_podEventHandler..."
+
+       def json = request.JSON
+       //log.debug json
+       def dni = getChildDevice(json.sensor_pod_id)
+
+       log.debug "event received from ${dni}"
+
+       poll(dni)   //sometimes events are stale, poll for all latest states
+
+
+       def html = """{"code":200,"message":"OK"}"""
+       render contentType: 'application/json', data: html
+}
+
+/////////////////////////////////////////////////////////////////////////
+//                      START POWERSTRIP SPECIFIC CODE HERE
+/////////////////////////////////////////////////////////////////////////
+
+def powerstripEventHandler()
+{
+       log.debug "In Powerstrip Event Handler..."
+
+       def json = request.JSON
+       def outlets = json.outlets
+
+       outlets.each() {
+               def dni = getChildDevice(it.outlet_id)
+               pollOutlet(dni)   //sometimes events are stale, poll for all latest states
+       }
+
+       def html = """{"code":200,"message":"OK"}"""
+       render contentType: 'application/json', data: html
+}
+
+def pollOutlet(childDevice)
+{
+       log.debug "In pollOutlet"
+
+       //login()
+
+       log.debug "Polling powerstrip"
+       apiGet("/outlets/" + childDevice.device.deviceNetworkId) { response ->
+               def data = response.data.data
+               data.powered ? childDevice?.sendEvent(name:"switch",value:"on") :
+                       childDevice?.sendEvent(name:"switch",value:"off")
+       }
+}
+
+def on(childDevice)
+{
+       //login()
+
+       apiPut("/outlets/" + childDevice.device.deviceNetworkId, [powered : true]) { response ->
+               def data = response.data.data
+               log.debug "Sending 'on' to device"
+       }
+}
+
+def off(childDevice)
+{
+       //login()
+
+       apiPut("/outlets/" + childDevice.device.deviceNetworkId, [powered : false]) { response ->
+               def data = response.data.data
+               log.debug "Sending 'off' to device"
+       }
+}
+
+def createPowerstripChildren(deviceData)
+{
+       log.debug "In createPowerstripChildren"
+
+       def powerstripName = deviceData.name
+       def deviceFile = "Quirky Wink Powerstrip"
+
+       deviceData.outlets.each {
+               createChildDevice( deviceFile, it.outlet_id, it.name, "$powerstripName ${it.name}" )
+       }
+}
+
+private Boolean canInstallLabs()
+{
+       return hasAllHubsOver("000.011.00603")
+}
+
+private Boolean hasAllHubsOver(String desiredFirmware)
+{
+       return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
+}
+
+private List getRealHubFirmwareVersions()
+{
+       return location.hubs*.firmwareVersionString.findAll { it }
+}
+