Created
July 22, 2017 07:59
-
-
Save keithnorm/e11798c390df02dcab1762a40858e1c0 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
/** | |
* iQuue Access Control | |
* | |
* Copyright 2016 iQuue | |
* | |
*/ | |
import groovy.json.JsonSlurper | |
include 'asynchttp_v1'; | |
def getAPIURL() { | |
return "https://manage.iquue.com" | |
} | |
definition( | |
name: "iQuue", | |
namespace: "iquue", | |
author: "iQuue", | |
description: "Smart Multi-Family Properties", | |
category: "My Apps", | |
iconUrl: "https://account.iquue.com/wp-content/uploads/2016/05/doorcodes@1x.png", | |
iconX2Url: "http://account.iquue.com/wp-content/uploads/2016/05/doorcodes@2x.png", | |
iconX3Url: "http://account.iquue.com/wp-content/uploads/2016/05/doorcodes@2x.png", | |
oauth: [displayName: "iQuue", displayLink: "app.iquue.com"]) | |
preferences { | |
section("Send SMS Notifications?") { | |
input "sendPush", "bool", required: false, | |
title: "Send SMS iQuue Notifications?" | |
} | |
section("Send SMS messages to this number (optional)") { | |
input "phone", "phone", required: false | |
} | |
section("iQuue Enabled Hubs") { | |
input "hub", "hub", multiple: true, title: "Select a hub" | |
} | |
section("Front Door Lock") { | |
input "frontDoor", "capability.lock", multiple: false, required: true | |
} | |
section("all devices") { | |
input "actuators", "capability.actuator", multiple: true, required: true | |
input "sensors", "capability.sensor", multiple: true, required: false | |
} | |
} | |
mappings { | |
path("/hub") { | |
action: [ | |
GET: "getHubData" | |
] | |
} | |
path("/devices") { | |
action: [ | |
GET: "listDevices" | |
] | |
} | |
path("/locks/:deviceId/codes") { | |
action: [ | |
POST: "setCode", | |
DELETE: "deleteAllCodes" | |
] | |
} | |
path("/locks/:deviceId/codes/:slot") { | |
action: [ | |
POST: "setCode", | |
DELETE: "deleteCode" | |
] | |
} | |
path("/devices/:deviceID") { | |
action: [ | |
POST: "runCommands" | |
] | |
} | |
path("/auth") { | |
action: [ | |
POST: "iqAuthKeys" | |
] | |
} | |
} | |
def installed() { | |
log.debug "Installed with settings: ${settings}" | |
unsubscribe() | |
initialize() | |
} | |
def updated() { | |
log.debug "Updated with settings: ${settings}"; | |
unsubscribe(); | |
initialize(); | |
} | |
def initialize() { | |
subscribe(hub, "hubStatus", hubHandler, [filterEvents: false]); | |
subscribe(locks, "codeReportAll", codeReportHandler); | |
registerAll() | |
runIn(5, fetchLockCodes) | |
runEvery5Minutes(pollLocks); | |
state.transactions = [:] | |
} | |
def registerAll() { | |
subscribe(locks, "allCodesDeleted", allCodesDeletedHandler); | |
(actuators + (sensors?:[])).each { device -> | |
def attributes = device.capabilities.collectMany { capability -> | |
return capability.attributes.collect { | |
it.name | |
} | |
} | |
attributes.each { attribute -> | |
log.debug "Registering ${device.displayName}.${attribute}" | |
subscribe(device, attribute, changeHandler, [filterEvents: false]) | |
} | |
} | |
} | |
def getHubData() { | |
def response = [:] | |
def attrs = hub[0].properties | |
def hub = hub[0] | |
response << [name: hub.name, locationId: location.id, status: hub.status.toLowerCase()] | |
response.devices = listDevices() | |
def json = new groovy.json.JsonOutput().toJson(response) | |
render contentType: "application/json", data: json | |
} | |
def listDevices() { | |
locks.each {lock -> | |
lock.refresh() | |
} | |
state.transactions=[:] | |
def allDevices = [] | |
(actuators + (sensors?:[])).each { | |
device -> | |
def out = [name: device.displayName, id: device.id, deviceType: device.typeName, status: device.status.toLowerCase(), manufacturer: device.manufacturerName] | |
out.capabilities = device.capabilities.collect{it.name} | |
def attributes = device.capabilities.collectMany { | |
capability -> | |
return capability.attributes.collect { | |
it.name | |
} | |
} | |
if (device.capabilities.collect { | |
it.name | |
}.indexOf('Lock') > -1) { | |
out.isFrontDoor = device.id == frontDoor[0].id | |
} | |
def allAttributes = device.supportedAttributes.collect({it.name}) + attributes | |
out << allAttributes.collectEntries { | |
attribute -> | |
log.debug("try ${attribute}") | |
try { | |
[ | |
(attribute): device.currentValue(attribute) | |
] | |
} catch (e) { | |
[ | |
(attribute): null | |
] | |
} | |
} | |
allDevices << out | |
} | |
return allDevices | |
} | |
/** Lock related functions **/ | |
def getLocks() { | |
return actuators.findAll{device -> | |
return device.capabilities.collect { it.name }.indexOf('Lock') > -1 | |
} | |
} | |
def fetchLockCodes() { | |
state.requestingAllCodes = true | |
locks.each {lock -> | |
lock.reloadAllCodes() | |
} | |
} | |
def deleteCode() { | |
def slot = params.slot | |
log.info "delete ${slot} for ${params.deviceId}" | |
locks.each { lock -> | |
lock.deleteCode(slot.toInteger()) | |
} | |
} | |
def setCode() { | |
def body = request.JSON | |
log.debug("set code ${body}") | |
def codes = [:] | |
// set code on all locks | |
locks.each { lock -> | |
lock.setCode(body.slot, body.code) | |
} | |
} | |
def deleteAllCodes() { | |
locks.each{ lock -> | |
lock.deleteAllCodesKwikset() | |
} | |
} | |
def pollLocks() { | |
locks.each { it.poll() } | |
} | |
/** END Lock related functions **/ | |
/** | |
* Provides an interface to call any commands exposed via any of the registered devices | |
* API route: POST /devices/:deviceID | |
* the post body should contain a Map of the commands and arguments to execute | |
* e.g.: | |
* POST {"on": [], "setLevel": [100], "setColor": [{"hue": 99, "saturation": 100}]} | |
* when called on a device with switch and color control capabilities would turn the switch on and set color to hsl(99, 100, 100) | |
*/ | |
def runCommands() { | |
def command = params.command | |
def deviceID = params.deviceID | |
def body = request.JSON | |
def transactionID = params.transactionID | |
def theDevice = actuators.find { | |
it.id == deviceID | |
} | |
def capabilities = theDevice.capabilities | |
def attributes = capabilities.collectMany { | |
capability -> | |
return capability.attributes.collect { | |
it.name | |
} | |
} | |
def commands = capabilities.collectMany { | |
capability -> | |
return capability.commands.collect { | |
it.name | |
} | |
} | |
body.each {method, args -> | |
if (commands.indexOf(method) > -1) { | |
if (transactionID) { | |
def anticipatedChanges = mapCommandToValueChanges(theDevice, method, *args); | |
anticipatedChanges.each{ change -> | |
queueTransaction(transactionID, deviceID, change[0], change[1]) | |
} | |
} | |
theDevice."$method"(*args) | |
} | |
} | |
} | |
/* | |
Event Handlers | |
*/ | |
void hubHandler(evt) { | |
log.debug "HUB HANDLER @ $evt.name $evt.value" | |
def jsonParams = [ | |
uri: "${APIURL}/api", | |
body: [ | |
token: state.iquueToken, | |
action: [ | |
type: "HUB_STATUS_CHANGE", | |
payload: [ | |
id: evt.locationId, | |
status: ["active", "zb_radio_on", "zw_radio_on"].indexOf(evt.value) > -1 ? "online" : "offline" | |
] | |
] | |
] | |
] | |
try { | |
httpPostJson(jsonParams) { | |
resp -> | |
resp.headers.each { | |
log.debug "${it.name} : ${it.value}" | |
} | |
log.debug "response contentType: ${resp.contentType}" | |
} | |
} catch (e) { | |
log.debug "something went wrong: $e" | |
} | |
} | |
/* | |
* On initialize we register subscribers for all attributes of all devices | |
* any change that happens will end up triggering a call to this function | |
* whose sole responsibility is to post the change back to the iQuue API | |
*/ | |
def changeHandler(evt) { | |
// Change Handler [device:fc25273e-68e1-4099-bd0d-6e8c8ce89fbf, attribute:switch, value:on, date:Thu Jun 08 19:14:02 UTC 2017] | |
// when requesting all codes we will receive a `codeReport` event for each slot | |
// we want to avoid sending this event back to iQuue in this case and avoid a "Too Many Requests" error from ST | |
log.debug("GOT EVENT ${evt}") | |
def deviceData = [deviceId: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date.format("yyyy-MM-dd'T'hh:mm:ssZ", TimeZone.getTimeZone('GMT'))] | |
if (evt.data) { | |
deviceData.data = new groovy.json.JsonSlurper().parseText(evt.data) | |
} | |
// for lock events eventType indicates the method by which the lock was locked or unlocked | |
if (evt.eventType && evt.eventType.isInteger()) { | |
deviceData.eventType = evt.eventType as Integer | |
} | |
def optionalTransaction = dequeueTransaction(deviceData.deviceId, evt.name, evt.value) | |
if (optionalTransaction) { | |
deviceData.transaction = optionalTransaction | |
} | |
//httpGet(uri) | |
def params = [ | |
uri: "${APIURL}/api", | |
body: [ | |
token: state.iquueToken, | |
action: [ | |
type: 'DEVICE_CHANGE', | |
payload: deviceData | |
] | |
] | |
] | |
if (evt.name != "codeReport" || !state.requestingAllCodes) { | |
httpPostJson(params) | |
} | |
} | |
def codeReportHandler(evt) { | |
log.debug "IN CODE REPORT ALL ${evt.name} ${evt.value}" | |
state.requestingAllCodes = false | |
try { | |
def jsonParams = [ | |
uri: "${APIURL}/api", | |
body: [ | |
token: state.iquueToken, | |
action: [ | |
type: "DEVICE_CODE_REPORT_ALL", | |
payload: [ | |
deviceId: evt.deviceId, | |
codes: new groovy.json.JsonSlurper().parseText(evt.value) | |
] | |
] | |
] | |
] | |
httpPostJson(jsonParams) | |
} catch (e) { | |
log.error "error here ${e}" | |
} | |
} | |
def allCodesDeletedHandler(evt) { | |
log.debug("DELETED ALL CODES") | |
} | |
/* | |
* This gets called when a new hub is authenticated. iQuue generates a key (a hash of the authenticated unit) and | |
* sends it to the SmartApp to be attached to any ST -> iQuue requests as a means of basic authentication for iQuue API endpoints | |
*/ | |
void iqAuthKeys() { | |
def params = request.JSON | |
state.iquueToken = params.token | |
} | |
/* | |
Transactions handle tying requests received from iQuue to events posted from device handlers. | |
for example, iQuue initiates an "addLockCode" with a transactionID. The transactionID gets stored | |
in state in this way: | |
[ | |
[deviceID]: | |
[addLockCode]: [transactionID] | |
] | |
so that later when we receive a "codeReport" event from the device handler, we can look up any pending transactions in state for | |
that device and action, and re-attach the transactionID in our notification back to iQuue, closing the loop on the initial request. | |
*/ | |
def queueTransaction(id, deviceID, attribute, value) { | |
if (!state.transactions) { | |
state.transactions = [:] | |
} | |
if (!state.transactions[deviceID]) { | |
state.transactions[deviceID] = [:] | |
} | |
if (!state.transactions[deviceID]["${attribute}.${value}"]) { | |
state.transactions[deviceID]["${attribute}.${value}"] = [] | |
} | |
state.transactions[deviceID]["${attribute}.${value}"] << id | |
} | |
def dequeueTransaction(deviceID, attribute, value) { | |
if (state.transactions[deviceID] && state.transactions[deviceID]["${attribute}.${value}"] && state.transactions[deviceID]["${attribute}.${value}"] instanceof Collection) { | |
return state.transactions[deviceID]["${attribute}.${value}"].remove(0) | |
} | |
} | |
def mapCommandToValueChanges(device, command, args = []) { | |
def lock = [ | |
'lock': ['lock', 'locked'], | |
'unlock': ['lock', 'unlocked'] | |
] | |
def light = [ | |
'on': ['switch', 'on'], | |
'off': ['switch', 'off'] | |
] | |
def thermostat = [ | |
'auto': ['thermostatMode', 'auto'], | |
'cool': ['thermostatMode', 'cool'], | |
'heat': ['thermostatMode', 'heat'], | |
'off': ['thermostatMode', 'off'], | |
'fanAuto': ['thermostatFanMode', 'auto'], | |
'fanCirculate': ['thermostatFanMode', 'circulate'], | |
'fanOn': ['thermostatFanMode', 'on'] | |
] | |
def genericType = genericDeviceType(device) | |
switch(genericType) { | |
case 'light': | |
if (light[command]) { | |
return [light[command]]; | |
} | |
break; | |
case 'lock': | |
if (lock[command]) { | |
return [lock[command]]; | |
} | |
break; | |
case 'thermostat': | |
if (thermostat[command]) { | |
return [thermostat[command]]; | |
} | |
break; | |
} | |
def name = (command - ~/^set/) | |
name = name[0].toLowerCase() + name.substring(1) | |
def out = [] | |
// e.g. setTemperature, [77] | |
if (args instanceof List) { | |
out << [name, args[0]] | |
} | |
// e.g. setColor [hue: 55, saturation: 90] | |
else if(args instanceof Map) { | |
args.each{attr, value -> | |
out << [attr, value] | |
} | |
} | |
return out | |
} | |
def genericDeviceType(device) { | |
def capabilities = device.capabilities.collect{it.name} | |
if(capabilities.indexOf('Light') > -1) { | |
return 'light' | |
} | |
if(capabilities.indexOf('Thermostat') > -1) { | |
return 'thermostat' | |
} | |
if(capabilities.indexOf('Lock') > -1) { | |
return 'lock' | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment