Skip to content

Instantly share code, notes, and snippets.

@keithnorm
Created July 22, 2017 07:59
Show Gist options
  • Save keithnorm/e11798c390df02dcab1762a40858e1c0 to your computer and use it in GitHub Desktop.
Save keithnorm/e11798c390df02dcab1762a40858e1c0 to your computer and use it in GitHub Desktop.
/**
* 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