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