Skip to content

Instantly share code, notes, and snippets.

@alttext
Created December 11, 2014 01:54
Show Gist options
  • Save alttext/304d02b8e8f05e11272f to your computer and use it in GitHub Desktop.
Save alttext/304d02b8e8f05e11272f to your computer and use it in GitHub Desktop.
/**
* 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