/**
* Homebridge SmartThing/Hubitat Interface
* Loosely Modelled off of Paul Lovelace's JSON API
* Copyright 2018 Anthony Santilli
*/
String appVersion() { return "1.5.2" }
String appModified() { return "10-22-2018" }
String platform() { return "Hubitat" }
String appIconUrl() { return "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-tonesto7/master/images/hb_tonesto7@2x.png" }
String getAppImg(imgName) { return "https://raw.githubusercontent.com/tonesto7/smartthings-tonesto7-public/master/resources/icons/$imgName" }
Boolean isST() { return (platform() == "SmartThings") }
definition(
name: "Homebridge (${platform()})",
namespace: "tonesto7",
author: "Anthony Santilli",
description: "Provides the API interface between Homebridge (HomeKit) and ${platform()}",
category: "My Apps",
iconUrl: "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-tonesto7/master/images/hb_tonesto7@1x.png",
iconX2Url: "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-tonesto7/master/images/hb_tonesto7@2x.png",
iconX3Url: "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-tonesto7/master/images/hb_tonesto7@3x.png",
oauth: true)
preferences {
page(name: "mainPage")
page(name: "confirmPage")
}
def appInfoSect() {
section() {
if(isST()) {
paragraph "${app?.name}\nv${appVersion()}", image: appIconUrl()
paragraph "Any Device Changes will require a restart of the Homebridge Service", required: true, state: null, image: getAppImg("error.png")
} else {
def str = """
- ${app?.name}
- Copyright\u00A9 2018 Anthony Santilli
- Version: ${appVersion()}
"""
paragraph "${str}"
paragraph 'Notice: Any Device Changes will require a restart of the Homebridge Service'
}
}
}
def mainPage() {
if (!state?.accessToken) {
createAccessToken()
}
Boolean isInst = (state?.isInstalled == true)
if(isST()) {
return dynamicPage(name: "mainPage", title: "Homebridge Device Configuration", nextPage: (isInst ? "confirmPage" : ""), install: !isInst, uninstall:true) {
appInfoSect()
section("Define Specific Categories:") {
paragraph "Each category below will adjust the device attributes to make sure they are recognized as the desired device type under HomeKit", state: "complete"
input "lightList", "capability.switch", title: "Lights: (${lightList ? lightList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("light_on.png")
input "fanList", "capability.switch", title: "Fans: (${fanList ? fanList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("fan_on.png")
input "speakerList", "capability.switch", title: "Speakers: (${speakerList ? speakerList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("media_player.png")
}
section("All Other Devices:") {
input "sensorList", "capability.sensor", title: "Sensor Devices: (${sensorList ? sensorList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("sensors.png")
input "switchList", "capability.switch", title: "Switch Devices: (${switchList ? switchList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("switch.png")
input "deviceList", "capability.refresh", title: "Other Devices: (${deviceList ? deviceList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("devices2.png")
}
section("Restrict Temp Device Creation") {
input "noTemp", "bool", title: "Remove Temp from Contacts and Water Sensors?", required: false, defaultValue: false, submitOnChange: true
if(settings?.noTemp) {
input "sensorAllowTemp", "capability.sensor", title: "Allow Temp on these Sensors", multiple: true, submitOnChange: true, required: false, image: getAppImg("temperature.png")
}
}
section("Create Devices for Modes in HomeKit?") {
paragraph title: "What are these for?", "A virtual switch will be created for each mode in HomeKit.\nThe switch will be ON when that mode is active.", state: "complete", image: getAppImg("info.png")
def modes = location?.modes?.sort{it?.name}?.collect { [(it?.id):it?.name] }
input "modeList", "enum", title: "Create Devices for these Modes", required: false, multiple: true, options: modes, submitOnChange: true, image: getAppImg("mode.png")
}
section("Create Devices for Routines in HomeKit?") {
paragraph title: "What are these?", "A virtual device will be created for each routine in HomeKit.\nThese are very useful for use in Home Kit scenes", state: "complete", image: getAppImg("info.png")
def routines = location.helloHome?.getPhrases()?.sort { it?.label }?.collect { [(it?.id):it?.label] }
input "routineList", "enum", title: "Create Devices for these Routines", required: false, multiple: true, options: routines, submitOnChange: true, image: getAppImg("routine.png")
}
section("Smart Home Monitor Support (SHM):") {
input "addSecurityDevice", "bool", title: "Allow SHM Control in HomeKit?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("alarm_home.png")
}
section("Review Configuration:") {
href url: getAppEndpointUrl("config"), style: "embedded", required: false, title: "View the Configuration Data for Homebridge", description: "Tap, select, copy, then click \"Done\""
paragraph "Selected Device Count:\n${getDeviceCnt()}", image: getAppImg("info.png")
}
section("Options:") {
input "showLogs", "bool", title: "Show Events in Live Logs?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("debug.png")
input "allowLocalCmds", "bool", title: "Send HomeKit Commands Locally?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("command2.png")
label title: "SmartApp Label (optional)", description: "Rename this App", defaultValue: app?.name, required: false, image: getAppImg("name_tag.png")
}
}
} else {
return dynamicPage(name: "mainPage", title: "", nextPage: (isInst ? "confirmPage" : ""), install: !isInst, uninstall:true) {
appInfoSect()
section(sectionTitleStr("Define Specific Categories:")) {
paragraph 'These Categories will add the necessary capabilities to make sure they are recognized by HomeKit as the specific device type
'
input "lightList", "capability.switch", title: inputTitleStr("Lights: (${lightList ? lightList?.size() : 0} Selected)"), description: "Tap to select", multiple: true, submitOnChange: true, required: false
input "fanList", "capability.switch", title: inputTitleStr("Fans: (${fanList ? fanList?.size() : 0} Selected)"), description: "Tap to select", multiple: true, submitOnChange: true, required: false
input "speakerList", "capability.switch", title: inputTitleStr("Speakers: (${speakerList ? speakerList?.size() : 0} Selected)"), description: "Tap to select", multiple: true, submitOnChange: true, required: false
input "shadesList", "capability.windowShade", title: inputTitleStr("Window Shades: (${shadesList ? shadesList?.size() : 0} Selected)"), description: "Tap to select", multiple: true, submitOnChange: true, required: false
}
section(sectionTitleStr("All Other Devices:")) {
input "sensorList", "capability.sensor", title: inputTitleStr("Sensor Devices: (${sensorList ? sensorList?.size() : 0} Selected)"), description: "Tap to select", multiple: true, submitOnChange: true, required: false
input "switchList", "capability.switch", title: inputTitleStr("Switch Devices: (${switchList ? switchList?.size() : 0} Selected)"), description: "Tap to select", multiple: true, submitOnChange: true, required: false
input "deviceList", "capability.refresh", title: inputTitleStr("Other Devices: (${deviceList ? deviceList?.size() : 0} Selected)"), description: "Tap to select", multiple: true, submitOnChange: true, required: false
}
section(sectionTitleStr("Restrict Temp Device Creation")) {
input "noTemp", "bool", title: inputTitleStr("Remove Temp from Contacts and Water Sensors?"), required: false, defaultValue: false, submitOnChange: true
if(settings?.noTemp) {
input "sensorAllowTemp", "capability.sensor", title: inputTitleStr("Allow Temp on these Sensors"), multiple: true, submitOnChange: true, required: false
}
}
section("${sectionTitleStr("Create Mode Devices in HomeKit?")}") {
paragraph 'Description:
A virtual switch will be created for each mode in HomeKit.The switch will be ON when that mode is active.', state: "complete"
def modes = location?.modes?.sort{it?.name}?.collect { [(it?.id):it?.name] }
input "modeList", "enum", title: inputTitleStr("Create Devices for these Modes"), required: false, multiple: true, options: modes, submitOnChange: true
}
section("
${sectionTitleStr("Hubitat Safety Monitor Support:")}") {
input "addSecurityDevice", "bool", title: inputTitleStr("Allow Hubitat Safety Monitor Control in Homekit?"), required: false, defaultValue: false, submitOnChange: true
}
section("
${sectionTitleStr("Plug-In Configuration Data:")}") {
href url: getAppEndpointUrl("config"), style: "embedded", required: false, title: inputTitleStr("View the Configuration Data for Homebridge"), description: """Tap, select, copy, then click Done"""
paragraph "Selected Device Count:\n${getDeviceCnt()}
"
}
section("
${sectionTitleStr("Options:")}") {
input "showLogs", "bool", title: inputTitleStr("Show Events in Live Logs?"), required: false, defaultValue: true, submitOnChange: true
label title: inputTitleStr("App Label (optional)"), description: "Rename App", defaultValue: app?.name, required: false
}
}
}
}
def confirmPage() {
if(isST()) {
return dynamicPage(name: "confirmPage", title: "Confirm Page", install: true, uninstall:true) {
section("") {
paragraph "Would you like to restart the Homebridge Service to apply any device changes you made?", required: true, state: null, image: getAppImg("info.png")
input "restartService", "bool", title: "Restart Homebridge plugin when you press Save?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("reset2.png")
}
}
} else {
return dynamicPage(name: "confirmPage", title: "", install: true, uninstall:true) {
section("") {
paragraph 'Notice:
Would you like to restart the Homebridge Service to apply any device changes you made?', state: "complete"
input "restartService", "bool", title: inputTitleStr("Restart Homebridge plugin when you press Save?"), required: false, defaultValue: false, submitOnChange: true
}
section("") {
paragraph 'Notice:
Would you like to stop direct Updates to the Homebridge Service?', state: "complete"
input "resetDirectUpdates", "bool", title: inputTitleStr("Do you want to stop direct Updates?"), required: false, defaultValue: false, submitOnChange: true
}
}
}
}
def sectionTitleStr(title) { return "$title
" }
def inputTitleStr(title) { return "$title" }
def pageTitleStr(title) { return "$title
" }
def imgTitle(imgSrc, imgWidth, imgHeight, titleStr, color=null) {
def imgStyle = ""
imgStyle += imgWidth ? "width: ${imgWidth}px !important;" : ""
imgStyle += imgHeight ? "${imgWidth ? " " : ""}height: ${imgHeight}px !important;" : ""
if(color) { return """
${titleStr}
""" }
else { return """
${titleStr}""" }
}
def getDeviceCnt() {
def devices = []
def items = ["deviceList", "sensorList", "switchList", "lightList", "fanList", "speakerList", "modeList"]
if(isST()) { items?.push("routineList") }
if(!isST()) { items?.push("shadesList") }
items?.each { item ->
if(settings[item]?.size() > 0) {
devices = devices + settings[item]
}
}
return devices?.unique()?.size() ?: 0
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
state?.isInstalled = true
if(!state?.accessToken) {
createAccessToken()
}
runIn(2, "registerDevices", [overwrite: true])
runIn(4, "registerSensors", [overwrite: true])
runIn(6, "registerSwitches", [overwrite: true])
if(settings?.addSecurityDevice) {
if(!isST()) {
subscribe(location, "hsmStatus", changeHandler)
subscribe(location, "hsmRules", changeHandler)
subscribe(location, "hsmAlert", changeHandler)
subscribe(location, "hsmSetArm", changeHandler)
} else { subscribe(location, "alarmSystemStatus", changeHandler) }
}
if(settings?.modeList) {
log.debug "Registering (${settings?.modeList?.size() ?: 0}) Virtual Mode Devices"
subscribe(location, "mode", changeHandler)
if(state.lastMode == null) { state?.lastMode = location.mode?.toString() }
}
if(isST()) {
state?.subscriptionRenewed = 0
subscribe(app, onAppTouch)
if(settings?.allowLocalCmds != false) { subscribe(location, null, lanEventHandler, [filterEvents:false]) }
if(settings?.routineList) {
log.debug "Registering (${settings?.routineList?.size() ?: 0}) Virtual Routine Devices"
subscribe(location, "routineExecuted", changeHandler)
}
}
if(settings?.restartService == true) {
log.warn "Sent Request to Homebridge Service to Stop... Service should restart automatically"
attemptServiceRestart()
settingUpdate("restartService", "false", "bool")
}
if (settings?.resetDirectUpdates == true)
{
state?.directIP = ""
state?.directPort = ""
unsubscribe()
}
if (state?.directIP)
runIn((settings?.restartService ? 60 : 10), "updateServicePrefs")
}
def onAppTouch(event) {
updated()
}
def renderDevices() {
def deviceData = []
def items = ["deviceList", "sensorList", "switchList", "lightList", "fanList", "speakerList", "modeList"]
if(isST()) { items?.push("routineList") }
if(!isST()) { items?.push("shadesList") }
items?.each { item ->
if(settings[item]?.size()) {
settings[item]?.each { dev->
try {
def dData = getDeviceData(item, dev)
if(dData && dData?.size()) { deviceData?.push(dData) }
} catch (e) {
log.error("Error Occurred Parsing Device ${dev?.displayName}, Error " + e.message)
}
}
}
}
if(settings?.addSecurityDevice == true) { deviceData?.push(getSecurityDevice()) }
return deviceData
}
def getDeviceData(type, sItem) {
// log.debug "getDeviceData($type, $sItem)"
def curType = null
def devId = sItem
def obj = null
def name = null
def attrVal = null
def isVirtual = false
switch(type) {
case "routineList":
isVirtual = true
curType = "Routine"
obj = getRoutineById(sItem)
if(obj) {
name = "Routine - " + obj?.label
attrVal = "off"
}
break
case "modeList":
isVirtual = true
curType = "Mode"
obj = getModeById(sItem)
if(obj) {
name = "Mode - " + obj?.name
attrVal = modeSwitchState(obj?.name)
}
break
default:
curType = "device"
obj = sItem
break
}
if(curType && obj) {
return [
name: !isVirtual ? sItem?.displayName : name,
basename: !isVirtual ? sItem?.name : name,
deviceid: !isVirtual ? sItem?.id : devId,
status: !isVirtual ? sItem?.status : "Online",
manufacturerName: (!isVirtual ? (isST() ? sItem?.getManufacturerName() : sItem?.getDataValue("manufacturer")) : platform()) ?: platform(),
modelName: !isVirtual ? ((isST() ? sItem?.getModelName() : sItem?.getDataValue("model")) ?: sItem?.getTypeName()) : "${curType} Device",
serialNumber: !isVirtual ? sItem?.getDeviceNetworkId() : "${curType}${devId}",
firmwareVersion: "1.0.0",
lastTime: !isVirtual ? (isST() ? sItem?.getLastActivity() : null) : now(),
capabilities: !isVirtual ? deviceCapabilityList(sItem) : ["${curType}": 1],
commands: !isVirtual ? deviceCommandList(sItem) : [on:[]],
attributes: !isVirtual ? deviceAttributeList(sItem) : ["switch": attrVal]
]
} else { return null }
}
String modeSwitchState(String mode) {
return location?.mode?.toString() == mode ? "on" : "off"
}
def getSecurityDevice() {
return [
name: (!isST() ? "Hubitat Safety Monitor Alarm" : "Security Alarm"),
basename: (!isST() ? "HSM Alarm" : "Security Alarm"),
deviceid: "alarmSystemStatus_${location?.id}",
status: "ACTIVE",
manufacturerName: platform(),
modelName: (!isST() ? "Safety Monitor" : "Security System"),
serialNumber: (!isST() ? "HSM" : "SHM"),
firmwareVersion: "1.0.0",
lastTime: null,
capabilities: ["Alarm System Status":1, "Alarm":1],
commands: [],
attributes: ["alarmSystemStatus": getSecurityStatus()]
]
}
def findDevice(paramid) {
def device = deviceList.find { it?.id == paramid }
if (device) return device
device = sensorList.find { it?.id == paramid }
if (device) return device
device = switchList.find { it?.id == paramid }
if (device) return device
device = lightList.find { it?.id == paramid }
if (device) return device
device = fanList.find { it?.id == paramid }
if (device) return device
device = speakerList.find { it?.id == paramid }
if (device) return device
if(!isST()) {
device = shadesList.find { it?.id == paramid }
}
return device
}
def authError() {
return [error: "Permission denied"]
}
def getSecurityStatus(retInt=false) {
if(isST()) {
def cur = location.currentState("alarmSystemStatus")?.value
def inc = getShmIncidents()
if(inc != null && inc?.size()) { cur = 'alarm_active' }
if(retInt) {
switch (cur) {
case 'stay':
return 0
case 'away':
return 1
case 'night':
return 2
case 'off':
return 3
case 'alarm_active':
return 4
}
} else { return cur ?: "disarmed" }
} else {
return location?.hsmStatus ?: "disarmed"
}
}
private setSecurityMode(mode) {
if(!isST()) {
switch(mode) {
case "stay":
mode = "armHome"
break
case "away":
mode = "armAway"
break
case "night":
mode = "armHome"
break
case "off":
mode = "disarm"
break
}
}
log.info "Setting the ${isST() ? "Smart Home Monitor" : "Hubitat Safety Monitor"} Mode to (${mode})..."
sendLocationEvent(name: (isST() ? 'alarmSystemStatus' : 'hsmSetArm'), value: mode.toString())
}
def renderConfig() {
Map jsonMap = [
platforms: [
[
platform: platform(),
name: platform(),
app_url: (isST() ? apiServerUrl("/api/smartapps/installations/") : fullLocalApiServerUrl('')),
access_token: state?.accessToken
]
]
]
if(isST()) { jsonMap?.platforms[0]["app_id"] = app.id }
def configJson = new groovy.json.JsonOutput().toJson(jsonMap)
def configString = new groovy.json.JsonOutput().prettyPrint(configJson)
render contentType: "text/plain", data: configString
}
def renderLocation() {
return [
latitude: location?.latitude,
longitude: location?.longitude,
mode: location?.mode,
name: location?.name,
temperature_scale: location?.temperatureScale,
zip_code: location?.zipCode,
hubIP: (isST() ? location?.hubs[0]?.localIP : location.hubs[0]?.getDataValue("localIP")),
app_version: appVersion()
]
}
def CommandReply(statusOut, messageOut) {
def replyJson = new groovy.json.JsonOutput().toJson([status: statusOut, message: messageOut])
render contentType: "application/json", data: replyJson
}
def lanEventHandler(evt) {
// log.trace "lanStreamEvtHandler..."
def msg = parseLanMessage(evt?.description)
Map headerMap = msg?.headers
// log.trace "lanEventHandler... | headers: ${headerMap}"
try {
Map msgData = [:]
if (headerMap?.size()) {
if (headerMap?.evtSource && headerMap?.evtSource == "Homebridge_${platform()}") {
if (msg?.body != null) {
def slurper = new groovy.json.JsonSlurper()
msgData = slurper?.parseText(msg?.body as String)
log.debug "msgData: $msgData"
if(headerMap?.evtType) {
switch(headerMap?.evtType) {
case "hkCommand":
// log.trace "hkCommand($msgData)"
def val1 = msgData?.value1 ?: null
def val2 = msgData?.value2 ?: null
processCmd(msgData?.deviceid, msgData?.command, val1, val2, true)
break
case "enableDirect":
// log.trace "enableDirect($msgData)"
state?.directIP = msgData?.ip
state?.directPort = msgData?.port
activateDirectUpdates(true)
break
}
}
}
}
}
} catch (ex) {
log.error "lanEventHandler Exception:", ex
}
}
def deviceCommand() {
// log.info("Command Request: $params")
def val1 = request?.JSON?.value1 ?: null
def val2 = request?.JSON?.value2 ?: null
processCmd(params?.id, params?.command, val1, val2)
}
private processCmd(devId, cmd, value1, value2, local=false) {
log.info("Process Command${local ? "(LOCAL)" : ""} | DeviceId: $devId | Command: ($cmd)${value1 ? " | Param1: ($value1)" : ""}${value2 ? " | Param2: ($value2)" : ""}")
def device = findDevice(devId)
def command = cmd
if(settings?.addSecurityDevice != false && devId == "alarmSystemStatus_${location?.id}") {
setSecurityMode(command)
CommandReply("Success", "Security Alarm, Command $command")
} else if (settings?.modeList && command == "mode") {
log.debug "Virtual Mode Received: ${value1}"
if(value1) { changeMode(value1 as String) }
CommandReply("Success", "Mode Device, Command $command")
} else if (settings?.routineList && command == "routine") {
log.debug "Virtual Routine Received: ${value1}"
if(value1) { runRoutine(value1) }
CommandReply("Success", "Routine Device, Command $command")
} else {
if (!device) {
log.error("Device Not Found")
CommandReply("Failure", "Device Not Found")
} else if (!device.hasCommand(command)) {
log.error("Device ${device.displayName} does not have the command $command")
CommandReply("Failure", "Device ${device.displayName} does not have the command $command")
} else {
try {
if (value2 != null) {
device."$command"(value1,value2)
log.info("Command Successful for Device ${device.displayName} | Command ${command}($value1, $value2)")
} else if (value1 != null) {
device."$command"(value1)
log.info("Command Successful for Device ${device.displayName} | Command ${command}($value1)")
} else {
device."$command"()
log.info("Command Successful for Device ${device.displayName} | Command ${command}()")
}
CommandReply("Success", "Device ${device.displayName} | Command ${command}()")
} catch (e) {
log.error("Error Occurred for Device ${device.displayName} | Command ${command}()")
CommandReply("Failure", "Error Occurred For Device ${device.displayName} | Command ${command}()")
}
}
}
}
def changeMode(mode) {
if(mode) {
mode = mode.replace("Mode - ", "")
log.info "Setting the Location Mode to (${mode})..."
setLocationMode(mode)
state.lastMode = mode
}
}
def runRoutine(rt) {
if(rt) {
rt = rt.replace("Routine - ", "")
log.info "Executing the (${rt}) Routine..."
location?.helloHome?.execute(rt)
}
}
def deviceAttribute() {
def device = findDevice(params?.id)
def attribute = params?.attribute
if (!device) {
httpError(404, "Device not found")
} else {
return [currentValue: device?.currentValue(attribute)]
}
}
def findVirtModeDevice(id) {
return getModeById(id) ?: null
}
def findVirtRoutineDevice(id) {
return getRoutineById(id) ?: null
}
def deviceQuery() {
log.trace "deviceQuery(${params?.id}"
def device = findDevice(params?.id)
if (!device) {
def mode = findVirtModeDevice(params?.id)
def routine = isST() ? findVirtModeDevice(params?.id) : null
def obj = mode ? mode : routine ?: null
if(!obj) {
device = null
httpError(404, "Device not found")
} else {
def name = routine ? obj?.label : obj?.name
def type = routine ? "Routine" : "Mode"
def attrVal = routine ? "off" : modeSwitchState(obj?.name)
try {
deviceData?.push([
name: name,
deviceid: params?.id,
capabilities: ["${type}": 1],
commands: [on:[]],
attributes: ["switch": attrVal]
])
} catch (e) {
log.error("Error Occurred Parsing ${item} ${type} ${name}, Error " + e.message)
}
}
}
if (result) {
def jsonData = [
name: device.displayName,
deviceid: device.id,
capabilities: deviceCapabilityList(device),
commands: deviceCommandList(device),
attributes: deviceAttributeList(device)
]
def resultJson = new groovy.json.JsonOutput().toJson(jsonData)
render contentType: "application/json", data: resultJson
}
}
def deviceCapabilityList(device) {
def items = device?.capabilities?.collectEntries { capability-> [ (capability?.name):1 ] }
if(settings?.lightList.find { it?.id == device?.id }) {
items["LightBulb"] = 1
}
if(settings?.fanList.find { it?.id == device?.id }) {
items["Fan"] = 1
}
if(settings?.speakerList.find { it?.id == device?.id }) {
items["Speaker"] = 1
}
if(!isST() && settings?.shadesList.find { it?.id == device?.id }) {
items["WindowShade"] = 1
}
if(settings?.noTemp && items["Temperature Measurement"] && (items["Contact Sensor"] || items["Water Sensor"])) {
Boolean remTemp = true
if(settings?.sensorAllowTemp) {
List aItems = settings?.sensorAllowTemp?.collect { it?.getId() as String } ?: []
if(aItems?.contains(device?.id as String)) { remTemp = false }
}
if(remTemp) { items.remove("Temperature Measurement") }
}
return items
}
def deviceCommandList(device) {
return device.supportedCommands.collectEntries { command-> [ (command?.name): (command?.arguments) ] }
}
def deviceAttributeList(device) {
return device.supportedAttributes.collectEntries { attribute->
try {
[(attribute?.name): device?.currentValue(attribute?.name)]
} catch(e) {
[(attribute?.name): null]
}
}
}
String getAppEndpointUrl(subPath) { return isST() ? "${apiServerUrl("/api/smartapps/installations/${app.id}${subPath ? "/${subPath}" : ""}?access_token=${state.accessToken}")}" : "${getApiServerUrl()}/${getHubUID()}/apps/${app?.id}${subPath ? "/${subPath}" : ""}?access_token=${state?.accessToken}" }
String getLocalEndpointUrl(subPath) { return "${getLocalApiServerUrl()}/apps/${app?.id}${subPath ? "/${subPath}" : ""}?access_token=${state?.accessToken}" }
def getAllData() {
if(isST()) {
state?.subscriptionRenewed = now()
state?.devchanges = []
}
def deviceJson = new groovy.json.JsonOutput().toJson([location: renderLocation(), deviceList: renderDevices()])
render contentType: "application/json", data: deviceJson
}
def registerDevices() {
//This has to be done at startup because it takes too long for a normal command.
log.debug "Registering (${settings?.fanList?.size() ?: 0}) Fans"
registerChangeHandler(settings?.fanList)
log.debug "Registering (${settings?.deviceList?.size() ?: 0}) Other Devices"
registerChangeHandler(settings?.deviceList)
}
def registerSensors() {
//This has to be done at startup because it takes too long for a normal command.
log.debug "Registering (${settings?.sensorList?.size() ?: 0}) Sensors"
registerChangeHandler(settings?.sensorList)
log.debug "Registering (${settings?.speakerList?.size() ?: 0}) Speakers"
registerChangeHandler(settings?.speakerList)
}
def registerSwitches() {
//This has to be done at startup because it takes too long for a normal command.
log.debug "Registering (${settings?.switchList?.size() ?: 0}) Switches"
registerChangeHandler(settings?.switchList)
log.debug "Registering (${settings?.lightList?.size() ?: 0}) Lights"
registerChangeHandler(settings?.lightList)
if(!isST()) {
log.debug "Registering (${settings?.shadesList?.size() ?: 0}) Window Shades"
registerChangeHandler(settings?.shadesList)
}
log.debug "Registered (${getDeviceCnt()} Devices)"
}
def ignoreTheseAttributes() {
return [
'DeviceWatch-DeviceStatus', 'checkInterval', 'devTypeVer', 'dayPowerAvg', 'apiStatus', 'yearCost', 'yearUsage','monthUsage', 'monthEst', 'weekCost', 'todayUsage',
'maxCodeLength', 'maxCodes', 'readingUpdated', 'maxEnergyReading', 'monthCost', 'maxPowerReading', 'minPowerReading', 'monthCost', 'weekUsage', 'minEnergyReading',
'codeReport', 'scanCodes', 'verticalAccuracy', 'horizontalAccuracyMetric', 'altitudeMetric', 'latitude', 'distanceMetric', 'closestPlaceDistanceMetric',
'closestPlaceDistance', 'leavingPlace', 'currentPlace', 'codeChanged', 'codeLength', 'lockCodes', 'healthStatus', 'horizontalAccuracy', 'bearing', 'speedMetric',
'speed', 'verticalAccuracyMetric', 'altitude', 'indicatorStatus', 'todayCost', 'longitude', 'distance', 'previousPlace','closestPlace', 'places', 'minCodeLength',
'arrivingAtPlace', 'lastUpdatedDt', 'scheduleType', 'zoneStartDate', 'zoneElapsed', 'zoneDuration', 'watering'
]
}
def registerChangeHandler(devices, showlog=false) {
devices?.each { device ->
List theAtts = device?.supportedAttributes?.collect { it?.name as String }?.unique()
if(showlog) { log.debug "atts: ${theAtts}" }
theAtts?.each {att ->
if(!(ignoreTheseAttributes().contains(att))) {
if(settings?.noTemp && att == "temperature" && (device?.hasAttribute("contact") || device?.hasAttribute("water"))) {
Boolean skipAtt = true
if(settings?.sensorAllowTemp) {
List aItems = settings?.sensorAllowTemp?.collect { it?.getId() as String } ?: []
if(aItems?.contains(device?.id as String)) { skipAtt = false }
}
if(skipAtt) { return }
}
subscribe(device, att, "changeHandler")
if(showlog) { log.debug "Registering ${device?.displayName}.${att}" }
}
}
}
}
def changeHandler(evt) {
def sendItems = []
def sendNum = 1
def src = evt?.source
def deviceid = evt?.deviceId
def deviceName = evt?.displayName
def attr = evt?.name
def value = evt?.value
def dt = evt?.date
def sendEvt = true
switch(evt?.name) {
case "hsmStatus":
deviceid = "alarmSystemStatus_${location?.id}"
attr = "alarmSystemStatus"
sendItems?.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: "", evtDate: dt])
break
case "hsmAlert":
if(evt?.value == "intrusion") {
deviceid = "alarmSystemStatus_${location?.id}"
attr = "alarmSystemStatus"
value = "alarm_active"
sendItems?.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: "", evtDate: dt])
} else { sendEvt = false }
break
case "hsmRules":
case "hsmSetArm":
sendEvt = false
break
case "alarmSystemStatus":
deviceid = "alarmSystemStatus_${location?.id}"
sendItems?.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: "", evtDate: dt])
break
case "mode":
settings?.modeList?.each { id->
def md = getModeById(id)
if(md && md?.id) { sendItems?.push([evtSource: "MODE", evtDeviceName: "Mode - ${md?.name}", evtDeviceId: md?.id, evtAttr: "switch", evtValue: modeSwitchState(md?.name), evtUnit: "", evtDate: dt]) }
}
break
case "routineExecuted":
settings?.routineList?.each { id->
def rt = getRoutineById(id)
if(rt && rt?.id) {
sendItems?.push([evtSource: "ROUTINE", evtDeviceName: "Routine - ${rt?.label}", evtDeviceId: rt?.id, evtAttr: "switch", evtValue: "off", evtUnit: "", evtDate: dt])
}
}
break
default:
sendItems?.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: "", evtDate: dt])
break
}
if (sendEvt && state?.directIP != "" && sendItems?.size()) {
//Send Using the Direct Mechanism
sendItems?.each { send->
if(settings?.showLogs) {
String unitStr = ""
switch(send?.evtAttr as String) {
case "temperature":
unitStr = "\u00b0${send?.evtUnit}"
break
case "humidity":
case "level":
case "battery":
unitStr = "%"
break
case "power":
unitStr = "W"
break
case "illuminance":
unitStr = " Lux"
break
default:
unitStr = "${send?.evtUnit}"
break
}
log.debug "Sending${" ${send?.evtSource}" ?: ""} Event (${send?.evtDeviceName} | ${send?.evtAttr.toUpperCase()}: ${send?.evtValue}${unitStr}) to Homebridge at (${state?.directIP}:${state?.directPort})"
}
def params = [
method: "POST",
path: "/update",
headers: [
HOST: "${state?.directIP}:${state?.directPort}",
'Content-Type': 'application/json'
],
body: [
change_name: send?.evtDeviceName,
change_device: send?.evtDeviceId,
change_attribute: send?.evtAttr,
change_value: send?.evtValue,
change_date: send?.evtDate
]
]
// def result = new physicalgraph.device.HubAction(params)
//def result = new hubitat.device.HubAction(params)
//sendHubCommand(result)
sendHomebridgeCommand(params)
}
}
}
def getModeById(String mId) {
return location?.modes?.find{it?.id?.toString() == mId}
}
def getRoutineById(String rId) {
return location?.helloHome?.getPhrases()?.find{it?.id == rId}
}
def getModeByName(String name) {
return location?.modes?.find{it?.name?.toString() == name}
}
def getRoutineByName(String name) {
return location?.helloHome?.getPhrases()?.find{it?.label == name}
}
def getShmIncidents() {
//Thanks Adrian
def incidentThreshold = now() - 604800000
return location.activeIncidents.collect{[date: it?.date?.time, title: it?.getTitle(), message: it?.getMessage(), args: it?.getMessageArgs(), sourceType: it?.getSourceType()]}.findAll{ it?.date >= incidentThreshold } ?: null
}
void settingUpdate(name, value, type=null) {
if(name && type) {
app?.updateSetting("$name", [type: "$type", value: value])
}
else if (name && type == null){ app?.updateSetting(name.toString(), value) }
}
private activateDirectUpdates(isLocal=false) {
log.trace "activateDirectUpdates: ${state?.directIP}:${state?.directPort}${isLocal ? " | (Local)" : ""}"
// def result = new physicalgraph.device.HubAction(method: "GET", path: "/initial", headers: [HOST: "${state?.directIP}:${state?.directPort}"])
def params = [
method: "GET",
path: "/initial",
headers: [
HOST: "${state?.directIP}:${state?.directPort}"
]
]
//def result = new hubitat.device.HubAction(method: "GET", path: "/initial", headers: [HOST: "${state?.directIP}:${state?.directPort}"])
//sendHubCommand(result)
sendHomebridgeCommand(params)
}
private attemptServiceRestart(isLocal=false) {
log.trace "attemptServiceRestart: ${state?.directIP}:${state?.directPort}${isLocal ? " | (Local)" : ""}"
// def result = new physicalgraph.device.HubAction(method: "GET", path: "/restart", headers: [HOST: "${state?.directIP}:${state?.directPort}"])
def params = [
method: "GET",
path: "/restart",
headers: [
HOST: "${state?.directIP}:${state?.directPort}"
]
]
//def result = new hubitat.device.HubAction(method: "GET", path: "/restart", headers: [HOST: "${state?.directIP}:${state?.directPort}"])
//sendHubCommand(result)
sendHomebridgeCommand(params)
}
private updateServicePrefs(isLocal=false) {
log.trace "updateServicePrefs: ${state?.directIP}:${state?.directPort}${isLocal ? " | (Local)" : ""}"
def params = [
method: "POST",
path: "/updateprefs",
headers: [
HOST: "${state?.directIP}:${state?.directPort}",
'Content-Type': 'application/json'
],
body: [
local_commands: isST() ? (settings?.allowLocalCmds != false) : false,
local_hub_ip: isST() ? location?.hubs[0]?.localIP : location.hubs[0]?.getDataValue("localIP")
]
]
// def result = new physicalgraph.device.HubAction(params)
//def result = new hubitat.device.HubAction(params)
//sendHubCommand(result)
sendHomebridgeCommand(params)
}
def enableDirectUpdates() {
// log.trace "enableDirectUpdates: ($params)"
state?.directIP = params?.ip
state?.directPort = params?.port
activateDirectUpdates()
}
private static Class HubActionClass() {
try {
return 'physicalgraph.device.HubAction' as Class
} catch(all) {
return 'hubitat.device.HubAction' as Class
}
}
def asyncHttpResponse(response, data)
{
if (response.status != 200)
log.error "asyncHttpResponse: received invalid response ${response.status} for params ${data["requestParams"]}"
}
def sendHomebridgeCommand(params)
{
if (isST() == true)
{
sendHubCommand(HubActionClass().newInstance(params))
}
else
{
if (params?.method)
{
switch (params.method)
{
case "GET":
def getParams = [
uri: "http://${state?.directIP}:${state?.directPort}" + params.path ,
requestContentType: 'application/json'
]
asynchttpGet("asyncHttpResponse", getParams, [requestParams: params])
break
case "POST":
def postParams = [
uri: "http://${state?.directIP}:${state?.directPort}" + params.path ,
requestContentType: 'application/json',
contentType: 'application/json',
body : params.body
]
asynchttpPost("asyncHttpResponse", postParams, [requestParams: params])
break
default:
log.error "sendHomebridgeCommand: invalid http method ${params.method} called"
break
}
}
}
}
mappings {
if (isST() && (!params?.access_token || (params?.access_token && params?.access_token != state?.accessToken))) {
path("/devices") { action: [GET: "authError"] }
path("/config") { action: [GET: "authError"] }
path("/location") { action: [GET: "authError"] }
path("/:id/command/:command") { action: [POST: "authError"] }
path("/:id/query") { action: [GET: "authError"] }
path("/:id/attribute/:attribute") { action: [GET: "authError"] }
path("/getUpdates") { action: [GET: "authError"] }
path("/startDirect/:ip/:port") { action: [GET: "authError"] }
} else {
path("/devices") { action: [GET: "getAllData"] }
path("/config") { action: [GET: "renderConfig"] }
path("/location") { action: [GET: "renderLocation"] }
path("/:id/command/:command") { action: [POST: "deviceCommand"] }
path("/:id/query") { action: [GET: "deviceQuery"] }
path("/:id/attribute/:attribute") { action: [GET: "deviceAttribute"] }
path("/getUpdates") { action: [GET: "getChangeEvents"] }
path("/startDirect/:ip/:port") { action: [GET: "enableDirectUpdates"] }
}
}