/** * 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 = """ ${getVendorName()} Connection
Vendor icon connected device icon SmartThings logo

We have located your """ + getVendorName() + """ account.

Tap 'Done' to process your credentials.

""" render contentType: 'text/html', data: html } def receivedToken() { log.debug "In receivedToken" def html = """ Withings Connection
Vendor icon connected device icon SmartThings logo

Tap 'Done' to continue to Devices.

""" 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 } }