Created
December 11, 2014 01:54
-
-
Save alttext/304d02b8e8f05e11272f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 | |
* | |
* Update10:2014-04-24 (twackford) | |
* fixed null battery and temperature errors in spotter | |
* | |
* Update11:2014-04-26 (twackford) | |
* fixed multiple instances of same device getting installed | |
* | |
* Update12:2014-05-20 (twackford) | |
* Added Aros | |
* | |
* Update13:2014-06-05 (twackford) | |
* Multiple Aros bug fixes after initial QA | |
* | |
* Update14:2014-09-22 (twackford) | |
* Added Refuel | |
* | |
* Update14:2014-10-04 (twackford) | |
* Added enum of sub-child devices | |
* added renew subscription every 12 hours | |
* Added ability to delete from child device and reflect in parent | |
* Added delete of child if unselected in device list | |
* un-stubbed spotter, nimbus, eggtray and porkfolio | |
* Fixed page display of quirky and ST icons on connection | |
* Fixed deleting the last device from parent | |
* Fixed bad login handling | |
* | |
* Update15:2014-11-29 (twackford) | |
* Added check for token expiration (wink just set to 6 weeks) | |
*/ | |
import java.text.DecimalFormat | |
import groovy.json.JsonSlurper | |
// Wink API stuff | |
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/smartapp-icons/Partner/quirky@2x.png" } | |
private getClientId() { "c22d82a7fc3d6faf06dcff1bcf0feb52" } // Dan Lieberman's | |
private getClientSecret() { "bd44c524a1df9dce134235d174350603" } | |
private getServerUrl() { "https://graph.api.smartthings.com" } | |
// Automatically generated. Make future change here. | |
definition( | |
name: "Quirky (Connect)", | |
namespace: "wackford", | |
author: "Todd Wackford", | |
description: "Connect your Quirky to SmartThings.", | |
category: "My Apps", | |
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png", | |
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png" | |
) | |
mappings { | |
path("/receivedToken") { action:[ POST: "receivedToken", GET: "receivedToken"] } | |
path("/receiveToken") { action:[ POST: "receiveToken", GET: "receiveToken"] } | |
path("/propaneTankCallback") { action:[ POST: "propaneTankEventHandler", GET: "subscriberIdentifyVerification"]} | |
path("/airConditionerCallback") { action:[ POST: "airConditionerEventHandler", GET: "subscriberIdentifyVerification"]} | |
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"]} | |
} | |
preferences { | |
page(name: "Credentials", title: "Fetch OAuth2 Credentials", content: "authPage", install: false) | |
page(name: "listDevices", title: "Quirky Devices", content: "listDevices", install: false) | |
} | |
def installed() { | |
log.debug "Installed with settings: ${settings}" | |
schedule("5 0,12 * * * ?", updateWinkSubscriptions) //renew subscriptions every 12 hours | |
listDevices() | |
} | |
def updated() { | |
log.debug "Updated with settings: ${settings}" | |
unschedule() | |
schedule("5 0,12 * * * ?", updateWinkSubscriptions) //renew subscriptions every 12 hours | |
initialize() | |
listDevices() | |
} | |
def listDevices() | |
{ | |
log.debug "In listDevices" | |
def devices = getDeviceList() | |
log.debug "Device List = ${devices}" | |
log.debug "Settings List = ${settings}" | |
dynamicPage(name: "listDevices", title: "Choose devices", install: true) { | |
section("Devices") { | |
input "devices", "enum", title: "Select Device(s)", required: false, multiple: true, options: devices | |
} | |
} | |
} | |
def getDeviceList() | |
{ | |
log.debug "In getDeviceList" | |
def deviceList = [:] | |
state.deviceDataArr = [] | |
apiGet("/users/me/wink_devices") { response -> | |
response.data.data.each() { | |
if ( it.propane_tank_id ) { | |
deviceList["${it.propane_tank_id}"] = it.name | |
state.deviceDataArr.push(['name' : it.name, | |
'id' : it.propane_tank_id, | |
'type' : "propane_tank", | |
'serial' : it.serial, | |
'data' : it, | |
'subsSuff': "/propaneTankCallback", | |
'subsPath': "/propane_tanks/${it.propane_tank_id}/subscriptions" | |
]) | |
} | |
if ( it.air_conditioner_id ) { | |
deviceList["${it.air_conditioner_id}"] = it.name | |
state.deviceDataArr.push(['name' : it.name, | |
'id' : it.air_conditioner_id, | |
'type' : "air_conditioner", | |
'serial' : it.serial, | |
'data' : it, | |
'subsSuff': "/airConditionerCallback", | |
'subsPath': "/air_conditioners/${it.air_conditioner_id}/subscriptions" | |
]) | |
} | |
if ( it.cloud_clock_id ) { | |
//log.debug "${it.dials[0]}" | |
def dial1Data = it.dials[0] | |
deviceList[dial1Data.dial_id] = it.name + " Dial 1" | |
state.deviceDataArr.push(['name' : it.name + " Dial 1", | |
'id' : dial1Data.dial_id, | |
'type' : "nimbusDial", | |
'serial' : it.serial, | |
'data' : it, | |
'subsSuff': "/cloud_clockCallback", | |
'subsPath': "/cloud_clocks/${it.cloud_clock_id}/subscriptions" | |
]) | |
def dial2Data = it.dials[1] | |
deviceList[dial2Data.dial_id] = it.name + " Dial 2" | |
state.deviceDataArr.push(['name' : it.name + " Dial 2", | |
'id' : dial2Data.dial_id, | |
'type' : "nimbusDial", | |
'serial' : it.serial, | |
'data' : it, | |
'subsSuff': "/cloud_clockCallback", | |
'subsPath': "/cloud_clocks/${it.cloud_clock_id}/subscriptions" | |
]) | |
def dial3Data = it.dials[2] | |
deviceList[dial3Data.dial_id] = it.name + " Dial 3" | |
state.deviceDataArr.push(['name' : it.name + " Dial 3", | |
'id' : dial3Data.dial_id, | |
'type' : "nimbusDial", | |
'serial' : it.serial, | |
'data' : it, | |
'subsSuff': "/cloud_clockCallback", | |
'subsPath': "/cloud_clocks/${it.cloud_clock_id}/subscriptions" | |
]) | |
def dial4Data = it.dials[3] | |
deviceList[dial4Data.dial_id] = it.name + " Dial 4" | |
state.deviceDataArr.push(['name' : it.name + " Dial 4", | |
'id' : dial4Data.dial_id, | |
'type' : "nimbusDial", | |
'serial' : it.serial, | |
'data' : it, | |
'subsSuff': "/cloud_clockCallback", | |
'subsPath': "/cloud_clocks/${it.cloud_clock_id}/subscriptions" | |
]) | |
//dials are handled individually now, commented out | |
/*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.powerstrip_id ) { | |
def outlet1Data = it.outlets[0] | |
deviceList[outlet1Data.outlet_id] = it.name + " " + outlet1Data.name | |
state.deviceDataArr.push(['name' : it.name + " " + outlet1Data.name, | |
'id' : outlet1Data.outlet_id, | |
'type' : "powerstripOutlet", | |
'serial' : it.serial, | |
'data' : it, | |
'subsSuff': "/powerstripCallback", | |
'subsPath': "/powerstrips/${it.powerstrip_id}/subscriptions" | |
]) | |
def outlet2Data = it.outlets[1] | |
deviceList[outlet2Data.outlet_id] = it.name + " " + outlet2Data.name | |
state.deviceDataArr.push(['name' : it.name + " " + outlet2Data.name, | |
'id' : outlet2Data.outlet_id, | |
'type' : "powerstripOutlet", | |
'serial' : it.serial, | |
'data' : it, | |
'subsSuff': "/powerstripCallback", | |
'subsPath': "/powerstrips/${it.powerstrip_id}/subscriptions" | |
]) | |
//this is commented out cuz we install and handle outlets individualy | |
/* | |
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" | |
])*/ | |
} | |
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.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 | |
} | |
def initialize() | |
{ | |
log.debug "Initialized with settings: ${settings}" | |
settings.devices.each { | |
def deviceId = it | |
state.deviceDataArr.each { | |
if ( it.id == deviceId ) { | |
switch(it.type) { | |
case "propane_tank": | |
log.debug "we have a Refuel" | |
createChildDevice("Quirky Refuel", deviceId, it.name, it.label) | |
createWinkSubscription( it.subsPath, it.subsSuff ) | |
pollPropaneTank(getChildDevice(deviceId)) | |
break | |
case "air_conditioner": | |
log.debug "we have an Aros" | |
createChildDevice("Quirky Aros", deviceId, it.name, it.label) | |
createWinkSubscription( it.subsPath, it.subsSuff ) | |
pollAros(getChildDevice(deviceId)) | |
break | |
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 "powerstripOutlet": | |
log.debug "we have a Pivot Power Genius Outlet" | |
createChildDevice( "Quirky Pivot Power Genius", deviceId, it.name, it.name ) | |
createWinkSubscription( it.subsPath, it.subsSuff ) | |
pollOutlet(getChildDevice(deviceId)) | |
break | |
case "nimbusDial": | |
log.debug "we have a Nimbus Dial" | |
//createNimbusChildren(it.data) //has sub-devices, so we call out to create kids | |
createChildDevice( "Quirky Nimbus", deviceId, it.name, it.name ) | |
createWinkSubscription( it.subsPath, it.subsSuff ) | |
pollNimbus(getChildDevice(deviceId)) | |
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 ) | |
pollNimbus(getChildDevice(deviceId)) | |
break | |
case "sensor_pod": | |
log.debug "we have a Spotter" | |
createChildDevice("Quirky Spotter", deviceId, it.name, it.label) | |
createWinkSubscription( it.subsPath, it.subsSuff ) | |
getSensorPodUpdate(getChildDevice(deviceId)) | |
break | |
case "piggy_bank": | |
log.debug "we have a Piggy Bank" | |
createChildDevice("Quirky Porkfolio", deviceId, it.name, it.label) | |
createWinkSubscription( it.subsPath, it.subsSuff ) | |
getPiggyBankUpdate(getChildDevice(deviceId)) | |
break | |
case "eggtray": | |
log.debug "we have an Egg Minder" | |
createChildDevice("Quirky Eggtray", deviceId, it.name, it.label) | |
createWinkSubscription( it.subsPath, it.subsSuff ) | |
getEggtrayUpdate(getChildDevice(deviceId)) | |
break | |
} | |
} | |
} | |
} | |
// Delete any that are no longer in settings | |
def delete = getChildDevices().findAll { !settings.devices?.contains(it.deviceNetworkId) } | |
log.debug "deleting ${delete}" | |
delete.each() { | |
uninstallChildDevice(it) | |
deleteChildDevice(it.deviceNetworkId) | |
} | |
} | |
def createChildDevice(deviceFile, dni, name, label) | |
{ | |
log.debug "In createChildDevice" | |
try { | |
def existingDevice = getChildDevice(dni) | |
log.debug "existingDevice = ${existingDevice}" | |
if(!existingDevice) { | |
log.debug "Creating child" | |
def childDevice = addChildDevice(app.namespace, 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 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 subscriberIdentifyVerification() | |
{ | |
log.debug "In subscriberIdentifyVerification" | |
def challengeToken = params.hub.challenge | |
render contentType: 'text/plain', data: challengeToken | |
} | |
def uninstalled() | |
{ | |
log.debug "In uninstalled" | |
removeWinkSubscriptions() | |
removeChildDevices(getChildDevices()) | |
unschedule() | |
} | |
def removeWinkSubscriptions() | |
{ | |
log.debug "In removeWinkSubscriptions" | |
state.deviceDataArr.each() { | |
if (it.subsPath) { | |
def path = it.subsPath | |
apiGet(it.subsPath) { response -> | |
response.data.data.each { | |
if ( it.subscription_id ) { | |
log.debug "Deleting Subscription: ${path}" + "/" + "${it.subscription_id}" | |
deleteWinkSubscription(path + "/", it.subscription_id) | |
} | |
} | |
} | |
} | |
} | |
} | |
private removeChildDevices(delete) | |
{ | |
log.debug "In removeChildDevices" | |
log.debug "deleting ${delete.size()} devices" | |
delete.each { | |
deleteChildDevice(it.deviceNetworkId) | |
} | |
} | |
def uninstallChildDevice(childDevice) | |
{ | |
log.debug "in uninstallChildDevice" | |
// Remove the childs subscription | |
def deviceData = state.deviceDataArr.findAll { it.id == childDevice.device.deviceNetworkId } | |
deviceData.each() { | |
def path = it.subsPath | |
apiGet(it.subsPath) { response -> | |
response.data.data.each { | |
if ( it.subscription_id ) { | |
deleteWinkSubscription(path + "/", it.subscription_id) | |
} | |
} | |
} | |
} | |
//now remove the child from settings. Unselects from list of devices, not delete | |
log.debug "Settings size = ${settings['devices']}" | |
if (!settings['devices']) //empty list, bail | |
return | |
def newDeviceList = settings['devices'] - childDevice.device.deviceNetworkId | |
app.updateSetting("devices", newDeviceList) | |
} | |
def updateWinkSubscriptions() | |
{ //since we don't know when wink subscription dies, call this from a scheduled job | |
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 deleteWinkSubscription(path, subscriptionId) | |
{ | |
httpDelete([ | |
uri : apiUrl(), | |
path: path + subscriptionId, | |
headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ] | |
],) | |
{ response -> | |
log.debug "Subscription ${subscriptionId} deleted" | |
} | |
} | |
def buildCallbackUrl(suffix) | |
{ | |
log.debug "In buildRedirectUrl" | |
def serverUrl = getServerUrl() | |
return serverUrl + "/api/token/${state.accessToken}/smartapps/installations/${app.id}" + suffix | |
} | |
def checkToken() { | |
log.debug "In checkToken" | |
def tokenStatus = "bad" | |
//check existing token by calling user device list | |
try { | |
httpGet([ uri : apiUrl(), | |
path : "/users/me/wink_devices", | |
headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ] | |
]) | |
{ response -> | |
debugOut "Response is: ${response.status}" | |
if ( response.status == 200 ) { | |
debugOut "The current token is good" | |
tokenStatus = "good" | |
} | |
} | |
} | |
catch(Exception e) { | |
debugOut "Current access token did not work. Trying refresh Token now." | |
} | |
if ( tokenStatus == "bad" ) { | |
//Let's try the refresh token now | |
debugOut "Trying to refresh tokens" | |
def tokenParams = [ client_id : getClientId(), | |
client_secret : getClientSecret(), | |
grant_type : "refresh_token", | |
refresh_token : state.vendorRefreshToken ] | |
def tokenUrl = getVendorTokenPath() + toQueryString(tokenParams) | |
def params = [ | |
uri: tokenUrl, | |
] | |
try { | |
httpPost(params) { response -> | |
debugOut "Successfully refreshed tokens with code: ${response.status}" | |
state.vendorRefreshToken = response.data.refresh_token | |
state.vendorAccessToken = response.data.access_token | |
tokenStatus = "good" | |
} | |
} | |
catch(Exception e) { | |
debugOut "Unable to refresh token. Error ${e}" | |
} | |
} | |
if ( tokenStatus == "bad" ) { | |
return "Error: Unable to refresh Token" | |
} else { | |
return null //no errors | |
} | |
} | |
def apiGet(String path, Closure callback) | |
{ | |
log.debug "In apiGet with path: $path" | |
//check to see if our token has expired | |
def status = checkToken() | |
debugOut "Status of checktoken: ${status}" | |
if ( status ) { | |
debugOut "Error! Status: ${status}" | |
return | |
} else { | |
debugOut "Token is good. Call the command" | |
} | |
httpGet([ | |
uri : apiUrl(), | |
path : path, | |
headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ] | |
]) | |
{ | |
response -> | |
callback.call(response) | |
} | |
} | |
def apiPut(String path, cmd, Closure callback) | |
{ | |
log.debug "In apiPut with path: $path and cmd: $cmd" | |
//check to see if our token has expired | |
def status = checkToken() | |
debugOut "Status of checktoken: ${status}" | |
if ( status ) { | |
debugOut "Error! Status: ${status}" | |
return | |
} else { | |
debugOut "Token is good. Call the command" | |
} | |
httpPutJson([ | |
uri : apiUrl(), | |
path: path, | |
body: cmd, | |
headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ] | |
]) | |
{ | |
response -> | |
callback.call(response) | |
} | |
} | |
def poll(childDevice) | |
{ | |
log.debug "In poll" //we should not ever get here, devices have unique polls | |
} | |
def cToF(temp) { | |
return temp * 1.8 + 32 | |
} | |
def fToC(temp) { | |
return (temp - 32) / 1.8 | |
} | |
def debugOut(msg) { | |
log.debug msg | |
//sendNotificationEvent(msg) //Uncomment this for troubleshooting only | |
} | |
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) | |
} | |
String toQueryString(Map m) { | |
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") | |
} | |
///////////////////////////////////////////////////////////////////////// | |
// START REFUEL SPECIFIC CODE HERE | |
///////////////////////////////////////////////////////////////////////// | |
def pollPropaneTank(childDevice) | |
{ | |
log.debug "Polling Refuel ${childDevice.device.deviceNetworkId}" | |
apiGet("/propane_tanks/" + childDevice.device.deviceNetworkId) { response -> | |
def status = response.data.data.last_reading | |
log.debug "Got tank data!" | |
childDevice?.sendEvent(name:"battery", value:status.battery * 100, unit:"") | |
childDevice?.sendEvent(name:"tankLevel", value:status.remaining * 100, unit:"") | |
//childDevice?.sendEvent(name:"tankChanged", value:new Date((status.tank_changed_at as long)*1000), unit:"") | |
} | |
} | |
def propaneTankEventHandler() | |
{ | |
log.debug "In propaneTankEventHandler..." | |
def json = request.JSON | |
def dni = getChildDevice(json.propane_tank_id) | |
pollPropaneTank(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:"") | |
if (status.battery != null) | |
childDevice?.sendEvent(name:"battery",value:(status.battery * 100).toInteger(),unit:"") | |
else | |
childDevice?.sendEvent(name:"battery",value:0,unit:"") | |
// Need to get users pref of temp scale here | |
if ( status.temperature != null ) | |
childDevice?.sendEvent(name:"temperature",value:cToF(status.temperature),unit:"F") | |
} | |
} | |
def sensor_podEventHandler() | |
{ | |
log.debug "In sensor_podEventHandler..." | |
def json = request.JSON | |
def dni = getChildDevice(json.sensor_pod_id) | |
log.debug "event received from ${dni}" | |
getSensorPodUpdate(dni) //sometimes events are stale, poll for all latest states | |
def html = """{"code":200,"message":"OK"}""" | |
render contentType: 'application/json', data: html | |
} | |
///////////////////////////////////////////////////////////////////////// | |
// START NIMBUS SPECIFIC CODE HERE | |
///////////////////////////////////////////////////////////////////////// | |
def createNimbusChildren(deviceData) | |
{ | |
log.debug "In createNimbusChildren" | |
def nimbusName = deviceData.name | |
def deviceFile = "Quirky 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) | |
if (!childDevice) // not a smartthings device, user did not pick it | |
return | |
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}" | |
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) | |
if (!childDevice) //this is not a SmartThings dial (user did not pick) | |
return | |
//log.debug "Dial event ${childDevice}" | |
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.nimbus.quirky-nimbus-egg") | |
break | |
case "Porkfolio": | |
childDevice?.setIcon("dial", "dial", "st.quirky.nimbus.quirky-nimbus-porkfolio") | |
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}" | |
getPiggyBankUpdate(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 | |
def html = """{"code":200,"message":"OK"}""" | |
render contentType: 'application/json', data: html | |
outlets.each() { | |
def dni = getChildDevice(it.outlet_id) | |
if(!dni) //this is not a smartthings device cuz user did not pick it | |
return | |
pollOutlet(dni) //sometimes events are stale, poll for all latest states | |
} | |
} | |
def pollOutlet(childDevice) | |
{ | |
log.debug "In pollOutlet" | |
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) | |
{ | |
apiPut("/outlets/" + childDevice.device.deviceNetworkId, [powered : true]) { response -> | |
def data = response.data.data | |
log.debug "Sending 'on' to device" | |
} | |
} | |
def off(childDevice) | |
{ | |
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 Pivot Power Genius" | |
deviceData.outlets.each { | |
createChildDevice( "Quirky Pivot Power Genius", it.outlet_id, it.name, "$powerstripName ${it.name}" ) | |
pollOutlet(getChildDevice(it.outlet_id)) | |
} | |
} | |
///////////////////// | |
// Aros stuff here | |
//////////////////// | |
def arosCoolingSetpoint(childDevice, setPoint) | |
{ | |
apiPut("/air_conditioners/" + childDevice.device.deviceNetworkId, ["desired_state": setPoint]) { response -> | |
def data = response.data.data | |
log.debug "sending 'setPoint' to device" | |
} | |
} | |
def arosFanSpeed(childDevice, fanSpeed) | |
{ | |
apiPut("/air_conditioners/" + childDevice.device.deviceNetworkId, ["desired_state": fanSpeed]) { response -> | |
def data = response.data.data | |
log.debug "New Fan Speed: ${fanSpeed}" | |
} | |
} | |
def arosMode(childDevice, newMode) | |
{ | |
apiPut("/air_conditioners/" + childDevice.device.deviceNetworkId, [desired_state: [mode: newMode]]) { response -> | |
def data = response.data.data | |
log.debug "New arosMode: ${mode}" | |
} | |
} | |
def arosOff(childDevice) | |
{ | |
apiPut("/air_conditioners/" + childDevice.device.deviceNetworkId, [desired_state: [powered : false]]) { response -> | |
def data = response.data.data | |
log.debug "Sending 'off' to device" | |
} | |
} | |
def arosOn(childDevice) | |
{ | |
apiPut("/air_conditioners/" + childDevice.device.deviceNetworkId, [desired_state: [powered : true]]) { response -> | |
def data = response.data.data | |
log.debug "Sending 'on' to device" | |
} | |
} | |
def airConditionerEventHandler() | |
{ | |
log.debug "In airConditionerEventHandler..." | |
def json = request.JSON | |
def dni = getChildDevice(json.air_conditioner_id) | |
pollAros(dni) //sometimes events are stale, poll for all latest states | |
def html = """{"code":200,"message":"OK"}""" | |
render contentType: 'application/json', data: html | |
} | |
def pollAros(childDevice) | |
{ | |
log.debug "In pollAros" | |
def locationScale = location.getTemperatureScale() | |
//log.debug "Polling Aros ${childDevice.device.deviceNetworkId}" | |
//log.debug "Location Scale: ${locationScale}" | |
apiGet("/air_conditioners/" + childDevice.device.deviceNetworkId) { response -> | |
def status = response.data.data.last_reading | |
status.powered ? childDevice?.sendEvent(name:"switch",value:"on") : | |
childDevice?.sendEvent(name:"switch",value:"off") | |
log.debug "Powered Status: ${status.powered}" | |
if ( status.temperature != null ) { | |
if ( locationScale == "F" ) { | |
def displayValue = cToF(status.temperature) as int | |
childDevice?.sendEvent(name:"temperature",value:displayValue,unit:"F") | |
} else { | |
def displayValue = status.temperature as int | |
childDevice?.sendEvent(name:"temperature",value:displayValue,unit:"C") | |
} | |
log.debug "Temperature Status: ${status.temperature}" | |
} | |
if ( status.desired_max_set_point != null ) { | |
if ( locationScale == "F" ) { | |
def displaySetPointValue = cToF(status.desired_max_set_point) | |
displaySetPointValue = displaySetPointValue as double | |
displaySetPointValue = displaySetPointValue.trunc(2) | |
childDevice?.sendEvent(name:"coolingSetpoint",value:displaySetPointValue) | |
} else { | |
def displaySetPointCValue = status.desired_max_set_point as double | |
displaySetPointCValue = displaySetPointCValue.trunc(2) | |
childDevice?.sendEvent(name:"coolingSetpoint",value:displaySetPointCValue) | |
} | |
log.debug "coolingSetpoint Status: ${status.desired_max_set_point}" | |
} | |
if ( status.desired_fan_speed != null ) { | |
if ( (status.desired_fan_speed == 0.333) || (status.desired_fan_speed == 0.33) ) | |
childDevice?.sendEvent(name:"fanMode",value:"fanLow", descriptionText: "$childDevice fan speed is LOW") | |
if ( (status.desired_fan_speed == 0.666) || (status.desired_fan_speed == 0.66) ) | |
childDevice?.sendEvent(name:"fanMode",value:"fanMed", descriptionText: "$childDevice fan speed is MED") | |
if ( (status.desired_fan_speed == 0.999) || (status.desired_fan_speed == 1.0) ) | |
childDevice?.sendEvent(name:"fanMode",value:"fanHigh", descriptionText: "$childDevice fan speed is HIGH") | |
} | |
log.debug "desired_fan_speed Status: ${status.desired_fan_speed}" | |
if ( status.desired_mode != null ) { | |
if ( status.desired_mode == "fan_only" ) | |
childDevice?.sendEvent(name:"mode",value:"fan_only", descriptionText: "$childDevice mode is FAN ONLY") | |
if ( status.desired_mode == "auto_eco" ) | |
childDevice?.sendEvent(name:"mode",value:"auto_eco", descriptionText: "$childDevice mode is ECO") | |
if ( status.desired_mode == "cool_only" ) | |
childDevice?.sendEvent(name:"mode",value:"cool_only", descriptionText: "$childDevice mode is COOL") | |
} | |
log.debug "desired_mode Status: ${status.desired_mode}" | |
} | |
} | |
/////////////////////// | |
// HUB STUFF | |
/////////////////////// | |
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 } | |
} | |
/////////////////////// | |
// AUTH STUFF | |
/////////////////////// | |
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 = """Before you can participate in SmartThings Labs we need to update your hub. | |
Please contact our support team at labs@smartthings.com and tell them you want access to SmartThings Labs!""" | |
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 html = "" | |
def badCredentials = false | |
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" */ | |
try | |
{ | |
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 | |
log.debug "Vendor Access Token: ${state.vendorAccessToken}" | |
log.debug "Vendor Refresh Token: ${state.vendorRefreshToken}" | |
} | |
} | |
catch(Exception e) | |
{ | |
badCredentials = true | |
log.debug "Login unsuccessful!" | |
} | |
def displayMessage = "" | |
if ( badCredentials ) { | |
displayMessage = "Unable to login to your " + getVendorName() + " account using these Credentials" | |
} else { | |
displayMessage = "We have located your " + getVendorName() + " account." | |
} | |
//log.debug "token = ${state.vendorAccessToken}" | |
/* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */ | |
html = """ | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="width=100%"> | |
<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: 100%; | |
padding: 0px; | |
/*background: #eee;*/ | |
text-align: center; | |
} | |
img { | |
vertical-align: middle; | |
} | |
img:nth-child(2) { | |
margin: 0 10px; | |
} | |
p { | |
font-size: 1.5em; | |
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" style="width: 30%;max-height: 30%"/> | |
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" style="width: 10%;max-height: 10%"/> | |
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" style="width: 30%;max-height: 30%"/> | |
<p>$displayMessage</p> | |
<p>Tap 'Done' to continue.</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="100%" /> | |
<title>Quirky 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: 100%; | |
padding: 0px; | |
/*background: #eee;*/ | |
text-align: center; | |
} | |
img { | |
vertical-align: middle; | |
} | |
img:nth-child(2) { | |
margin: 0 10px; | |
} | |
p { | |
font-size: 1.5em; | |
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" style="width: 30%;max-height: 30%"/> | |
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" style="width: 10%;max-height: 10%"/> | |
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" style="width: 30%;max-height: 30%"/> | |
<p>Your Quirky account is connected to SmartThings. Tap 'Done' to continue to choose devices.</p> | |
</div> | |
</body> | |
</html> | |
""" | |
render contentType: 'text/html', data: html | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment