/*********************************************************************************************************************** * * A Hubitat App for managing Elk M1 Integration * * License: * This program is free software: you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * Name: Elk M1 Application * * Special Thanks to Doug Beard for the framework of this application! *** See Release Notes at the bottom*** ***********************************************************************************************************************/ public static String version() { return "v0.2.6.1" } import groovy.transform.Field definition( name: "Elk M1 Application", namespace: "belk", author: "Mike Magrann", description: "Integrate an Elk M1 Security System", category: "Security", iconUrl: "", iconX2Url: "", iconX3Url: "", importUrl: "https://raw.githubusercontent.com/thecaptncode/hubitat-elkm1/master/Elk-M1-Application.groovy", documentationLink: "https://github.com/thecaptncode/hubitat-elkm1/wiki" ) preferences { page(name: "mainPage", nextPage: "deviceMapsPage") page(name: "deviceMapsPage", nextPage: "mainPage") page(name: "notificationPage", nextPage: "mainPage") page(name: "lockPage", nextPage: "mainPage") page(name: "hsmPage", nextPage: "mainPage") page(name: "locationModePage", nextPage: "mainPage") page(name: "defineDeviceMap", nextPage: "deviceMapsPage") page(name: "defineLightMap", nextPage: "deviceMapsPage") page(name: "defineDeviceMapImport", nextPage: "deviceMapsPage") page(name: "importingDevices", nextPage: "deviceMapsPage") page(name: "editDeviceMapPage", nextPage: "deviceMapsPage") } //App Pages/Views Map mainPage() { if (getChildDevices().size() != 0) { subscriptionCleanup() } app.removeSetting("elkM1Password") app.removeSetting("isDebug") state.remove("ElkM1DeviceName") state.remove("ElkM1IP") state.remove("ElkM1Port") state.remove("ElkM1Code") state.remove("ElkM1Password") state.remove("allZones") state.remove("ElkM1Installed") state.remove("isDebug") state.remove("creatingZone") state.remove("editedZoneDNI") state.remove("creatingDevice") state.remove("editedDeviceDNI") if (dbgEnable) log.debug app.name + ": Showing mainPage" dynamicPage(name: "mainPage", title: "", install: false, uninstall: true) { if (getChildDevices().size() == 0) { section("Define your Elk M1 device") { input name: "elkM1Name", type: "text", title: "Elk M1 Name", required: true, defaultValue: "Elk M1" input name: "elkM1IP", type: "text", title: "Elk M1 IP Address", required: true input name: "elkM1Port", type: "number", title: "Elk M1 Port", range: "1..65535", required: true, defaultValue: 2101 input name: "elkM1Keypad", type: "number", title: "Elk M1 Keypad", range: "1..16", required: true, defaultValue: 1 input name: "elkM1Code", type: "text", title: "Elk M1 User Code", required: true } } else { section("

Device Mapping and Integration

") { href(name: "deviceMapsPage", title: "Devices", description: "Create Virtual Devices and Map them to Existing Zone, Output, Task, Lighting, Thermostat, Keypad, " + "Custom, Counter and/or Speech Devices in your Elk M1 setup.\nIntegrate virtual Elk M1 devices with physical devices.", page: "deviceMapsPage") } section("

Notifications

") { href(name: "notificationPage", title: "Notifications", description: "Enable Push and TTS Messages", page: "notificationPage") } section("

Integration

") { href(name: "lockPage", title: "Locks", description: "Integrate Locks", page: "lockPage") href(name: "hsmPage", title: "HSM", description: "Integrate Hubitat Safety Monitor", page: "hsmPage") href(name: "locationModePage", title: "Location Mode", description: "Integrate Hub Location Mode", page: "locationModePage") } } section("") { input name: "dbgEnable", type: "bool", title: "Enable Debug Logging", defaultValue: false, submitOnChange: true } } } Map notificationPage() { dynamicPage(name: "notificationPage", title: none) { section("

Notifications

") { paragraph "Enable TTS and Notification integration will announcing arming and disarming over your supported audio " + "and/or push enabled device" paragraph "

Notification Text

" input name: "disarmedBool", type: "bool", title: "Enable Disarmed Notification", defaultValue: false, submitOnChange: true if (disarmedBool) { input name: "disarmedText", type: "text", title: "Notification for Disarmed", defaultValue: "Disarmed" } input name: "armingAwayBool", type: "bool", title: "Enable Arming Away Notification", defaultValue: false, submitOnChange: true if (armingAwayBool) { input name: "armingAwayText", type: "text", title: "Notification for Arming Away", defaultValue: "Arming Away" } input name: "armingHomeBool", type: "bool", title: "Enable Arming Home Notification", defaultValue: false, submitOnChange: true if (armingHomeBool) { input name: "armingHomeText", type: "text", title: "Notification for Arming Home", defaultValue: "Arming Home" } input name: "armingNightBool", type: "bool", title: "Enable Arming Night Notification", defaultValue: false, submitOnChange: true if (armingNightBool) { input name: "armingNightText", type: "text", title: "Notification for Arming Night", defaultValue: "Arming Night" } input name: "armingVacationBool", type: "bool", title: "Enable Arming Vacation Notification", defaultValue: false, submitOnChange: true if (armingVacationBool) { input name: "armingVacationText", type: "text", title: "Notification for Arming Vacation", defaultValue: "Arming Vacation" } input name: "armedBool", type: "bool", title: "Enable Armed Notification", defaultValue: false, submitOnChange: true if (armedBool) { input name: "armedText", type: "text", title: "Notification for Armed", defaultValue: "Armed" } input name: "entryDelayAlarmBool", type: "bool", title: "Enable Entry Delay Notification", defaultValue: false, submitOnChange: true if (entryDelayAlarmBool) { input name: "entryDelayAlarmText", type: "text", title: "Notification for Entry Delay", defaultValue: "Entry Delay in Progress, Alarm eminent" } input name: "alarmBool", type: "bool", title: "Enable Alarm Notification", defaultValue: false, submitOnChange: true if (alarmBool) { input name: "alarmText", type: "text", title: "Notification for Alarm", defaultValue: "Alarm, Alarm, Alarm, Alarm, Alarm" } paragraph "

Notification Devices

" input name: "speechDevices", type: "capability.speechSynthesis", title: "Which speech devices?", multiple: true, submitOnChange: true input name: "notificationDevices", type: "capability.notification", title: "Which notification devices?", multiple: true, submitOnChange: true } } } Map lockPage() { if (dbgEnable) log.debug app.name + ": Showing lockPage" dynamicPage(name: "lockPage", title: none) { section("

Locks

") { paragraph "Enable Lock Integration, selected locks will lock when armed and/or unlock when disarmed" input name: "armLocks", type: "capability.lock", title: "Which locks to lock when armed?", multiple: true, submitOnChange: true input name: "disarmLocks", type: "capability.lock", title: "Which locks to unlock when disarmed?", multiple: true, submitOnChange: true } } } Map hsmPage() { if (dbgEnable) log.debug app.name + ": Showing hsmPage" if (enableHSMtoElk) subscribe(location, "hsmStatus", statusHandler) else unsubscribe(location, "hsmStatus") dynamicPage(name: "hsmPage", title: none) { section("

Hubitat Safety Monitor

") { paragraph "Hubitat Safety Monitor Integration will tie your Elk M1 Arm Modes to the status of HSM. " + " Enable both switches for full integration." paragraph "HSM Status will be set to Arm Away, Arm Home, Arm Night or Disarm when the Elk M1 Arm Mode changes." input name: "enableElktoHSM", type: "bool", title: "Enable Elk M1 to HSM Integration", defaultValue: false, submitOnChange: true paragraph "Your Elk M1 will receive the Arm Away, Arm Home, Arm Night or Disarm commands when the HSM Status changes." input name: "enableHSMtoElk", type: "bool", title: "Enable HSM to Elk M1 Integration", defaultValue: false, submitOnChange: true } } } Map locationModePage() { if (dbgEnable) log.debug app.name + ": Showing locationModePage" dynamicPage(name: "locationModePage", title: none) { section("

Location Modes

") { paragraph "Location Mode Integration will set your hub's location mode based on your Elk M1 Arm Mode." input name: "enableLocationMode", type: "bool", title: "Enable Location Mode Integration", defaultValue: false, submitOnChange: true if (enableLocationMode) { Map modes = ['': "(unchanged)"] location.getModes().each { modes.put(it.name, it.name) } input name: "modeDisarmed", type: "enum", title: "Location Mode when Disarmed", required: true, defaultValue: "", options: modes input name: "modeArmedAway", type: "enum", title: "Location Mode when Armed Away", required: true, defaultValue: "", options: modes input name: "modeArmedHome", type: "enum", title: "Location Mode when Armed Home", required: true, defaultValue: "", options: modes input name: "modeArmedNight", type: "enum", title: "Location Mode when Armed Night", required: true, defaultValue: "", options: modes input name: "modeArmedVacation", type: "enum", title: "Location Mode when Armed Vacation", required: true, defaultValue: "", options: modes } } } } Map deviceMapsPage() { app.removeSetting("deviceNumber") state.remove("buttonClicked") if (dbgEnable) log.debug app.name + ": Showing deviceMapsPage" if (getChildDevices().size() == 0) { createElkM1ParentDevice() } dynamicPage(name: "deviceMapsPage", title: "", install: true, uninstall: false) { com.hubitat.app.DeviceWrapper deviceInfo = getChildDevice(state.ElkM1DNI) if (deviceInfo == null) { section("

A problem was detected

") { paragraph "The Elk M1 parent device was not found." } } else { state.editedDeviceDNI = null Map> integrationMap = [:] settings.findAll { it.key.startsWith("integrate:") }.each { String[] integrations = it.key.split(':') if (integrations.size() > 2) { if (integrationMap[integrations[1]] == null) { integrationMap[integrations[1]] = [integrations[2]] } else { integrationMap[integrations[1]] << integrations[2] } } } section("

Device Maps

") { paragraph "The partition of your Elk M1 Installation may consist of Zone, Output, Task, Lighting, Thermostat, Keypad, " + "Custom, Counter and Speech Devices. You can choose to map the devices manually or use the import method. " paragraph "You'll want to determine the device number as it is defined in your Elk M1 setup. " + " Define a new device in Elk M1 application and the application will then create either a Virtual sensor component device or" + " an Elk Child device, which will report the state of the Elk M1 device to which it is mapped. " + " The devices can be used in Rule Machine or any other application that is capable of leveraging the devices capability." } section("

Create New Devices

") { href(name: "createDeviceImportPage", title: "Import Elk Devices", description: "Click to import Elk devices", page: "defineDeviceMapImport") href(name: "createDeviceMapPage", title: "Create a Device Map", description: "Create a Virtual Device Manually", page: "defineDeviceMap") href(name: "createLightMapPage", title: "Create a Lighting Device Map", description: "Create a Virtual Lighting Device Manually", page: "defineLightMap") } section("

Existing Devices

") { paragraph "View or change the integration of existing Elk M1 devices with physical devices." href(name: "editDeviceMapPage", title: "${deviceInfo.label}", description: integrationOptions(integrationMap[deviceInfo.deviceNetworkId], deviceInfo), params: [deviceNetworkId: deviceInfo.deviceNetworkId], page: "editDeviceMapPage") List children = deviceInfo.getChildDevices() if (children != null && children.size() > 0) { children.sort { it.deviceNetworkId }.each { href(name: "editDeviceMapPage", title: "${it.label}", description: integrationOptions(integrationMap[it.deviceNetworkId], it), params: [deviceNetworkId: it.deviceNetworkId], page: "editDeviceMapPage") } } else { paragraph "No children devices for ${deviceInfo.label} have been created yet." } } } } } String integrationOptions(List integrationList, com.hubitat.app.DeviceWrapper deviceInfo) { boolean hasCapabilities = false deviceInfo.getCapabilities().each { if (capabilityMap[it.name] != null) hasCapabilities = true } String description if (integrationList != null && (integrationList.size() > 1 || integrationList.first() != "smart")) { boolean hasSmart = integrationList.indexOf("smart") >= 0 List smartList = getSmartList(deviceInfo.deviceNetworkId, integrationList) integrationList.each { if (it != "smart") { if (description == null) description = it else description += ", " + it if (hasSmart && smartList.indexOf(it) >= 0) description += " (smart refresh)" } } description = "Integrated using " + description } else if (hasCapabilities) { description = "Add Integrations" } else { description = "Device Details" } } Map defineDeviceMap() { String message = null if (state.buttonClicked == "btnCreateDevice") message = createDevice() if (message != null && message.length() == 0) { importingDevices() } else { if (dbgEnable) log.debug app.name + ": Showing defineDeviceMap" dynamicPage(name: "defineDeviceMap", title: "") { section("

Create a Device Map

") { paragraph "Create a Map for a device in Elk M1" input name: "deviceName", type: "text", title: "Device Name", defaultValue: "Zone x" input name: "deviceNumber", type: "number", title: "Which Device 1 - 208", defaultValue: 1, range: "1..208" input name: "deviceType", type: "enum", title: "Zone, Output, Task, Thermostat, Keypad, Custom, Counter or Speech Device?", options: [['00': "Zone (1 - 208)"], ['04': "Output (1 - 208)"], ['05': "Task (1 - 32)"], ['11': "Thermostat (1 - 16)"], ['03': "Keypad (1 - 16)"], ['09': "Custom (1 - 20)"], ['10': "Counter (1 - 64)"], ['SP': "Speech (1)"]] input name: "btnCreateDevice", type: "button", title: "Create device", submitOnChange: true if (message) { paragraph "" + message + "" } } } } } Map defineLightMap() { String message = null if (state.buttonClicked == "btnCreateDevice") message = createDevice() if (message != null && message.length() == 0) { importingDevices() } else { if (dbgEnable) log.debug app.name + ": Showing defineLightMap" dynamicPage(name: "defineLightMap", title: "") { section("

Create a Device Map

") { paragraph "Create a Map for a lighting device in Elk M1" input name: "deviceName", type: "text", title: "Device Name", defaultValue: "Zone x" input name: "deviceType", type: "enum", title: "Which Lighting Group?", options: [['A': "A"], ['B': "B"], ['C': "C"], ['D': "D"], ['E': "E"], ['F': "F"], ['G': "G"], ['H': "H"], ['I': "I"], ['J': "J"], ['K': "K"], ['L': "L"], ['M': "M"], ['N': "N"], ['O': "O"], ['P': "P"]] input name: "deviceNumber", type: "number", title: "Which Device 1 - 16", defaultValue: 1, range: "1..16" input name: "btnCreateDevice", type: "button", title: "Create device", submitOnChange: true if (message) { paragraph "" + message + "" } } } } } Map defineDeviceMapImport() { String message = null if (state.buttonClicked == "btnImportDevices") message = importDevices() if (message != null && message.length() == 0) { importingDevices() } else { if (dbgEnable) log.debug app.name + ": Showing defineDeviceMapImport" dynamicPage(name: "defineDeviceMapImport", title: "") { section("

Import Elk Devices

") { paragraph "Create a Map for a device in Elk M1" input name: "deviceType", type: "enum", title: "Select Device Type", options: [['00': "Zones"], ['04': "Output"], ['05': "Task"], ['07': "Lighting"], ['11': "Thermostat"], ['03': "Keypad"], ['09': "Custom"], ['10': "Counter"]] input name: "btnImportDevices", type: "button", title: "Import devices", submitOnChange: true if (message) { paragraph "" + message + "" } } } } } Map importingDevices() { state.remove("buttonClicked") if (dbgEnable) log.debug app.name + ": Showing importingDevices" dynamicPage(name: "defineDeviceMapImport", title: "") { section("

Import Elk Devices

") { paragraph "Starting import of devices for Elk M1 - Click Next to continue" } } } Map editDeviceMapPage(message) { if (message != null) { state.editedDeviceDNI = message.deviceNetworkId } if (dbgEnable) log.debug app.name + ": Showing editDeviceMapPage for ${state.editedDeviceDNI}" com.hubitat.app.DeviceWrapper deviceInfo if (state.editedDeviceDNI == state.ElkM1DNI) deviceInfo = getChildDevice(state.ElkM1DNI) else deviceInfo = getChildDevice(state.ElkM1DNI).getChildDevice(state.editedDeviceDNI) String paragraphText = "No device capabilities exist that can be integrated. Current capabilities are:\n" List capList = subscribeDevice(deviceInfo) List smartList = [] if (capList.size() == 0) { deviceInfo.getCapabilities().each { if (it.name != "Actuator") paragraphText = paragraphText + it.name + "\n" } } else { smartList = getSmartList(deviceInfo.deviceNetworkId, capList) paragraphText = "Choose any devices you want to integrate with ${deviceInfo.label}" } dynamicPage(name: "editDeviceMapPage", title: "") { section("

${deviceInfo.label}

") { href(name: "editChild", title: "Edit Device", description: "Click to edit this device", url: "/device/edit/${deviceInfo.id}") if (smartList.size() > 0) { input name: "integrate:$state.editedDeviceDNI:smart", type: "bool", title: "Use smart refresh for ${smartList.join(', ')} *", defaultValue: false, submitOnChange: true } paragraph paragraphText capList.each { cap -> input name: "integrate:$state.editedDeviceDNI:$cap", type: "${capabilityMap[cap]}", title: "$cap device to integrate", submitOnChange: true } if (smartList.size() > 0) paragraph "* Smart refresh will refresh the integrated device first before refreshing certain child device capabilities. " + "This prevents an Elk refresh from changing the physical device's settings." } } } List subscribeDevice(com.hubitat.app.DeviceWrapper deviceInfo) { String targetDNID = deviceInfo.deviceNetworkId String targetDevID = deviceInfo.id.toString() String attribute String triggerValue String attributeValue List capList = [] deviceInfo.getCapabilities().each { if (capabilityMap[it.name] != null) capList << it.name } List smartList = [] if (settings["integrate:$targetDNID:smart"]) smartList = getSmartList(targetDNID, capList) Map> mySubscription = [:] atomicState.subscriptionMap.each { k, v -> List subscriptions = [] v.findAll { !it.startsWith(targetDNID + ":") }.each { subscriptions << it } if (subscriptions.size() > 0) mySubscription[k] = subscriptions } Map> myRevSubscription = atomicState.RevSubscriptionMap myRevSubscription.remove(targetDevID) boolean stateSaved = false capList.each { capability -> if (settings["integrate:$targetDNID:$capability"] instanceof com.hubitat.app.DeviceWrapper) { com.hubitat.app.DeviceWrapper integratedDevice = settings["integrate:$targetDNID:$capability"] if (integratedDevice != null && integratedDevice.deviceNetworkId != targetDNID) { if (dbgEnable) log.debug app.name + ": Subscribing ${integratedDevice.label} capability ${capability} to target ${deviceInfo.label}" if (mySubscription[integratedDevice.deviceId.toString()] == null) { mySubscription[integratedDevice.deviceId.toString()] = [targetDNID + ":" + capability] } else { mySubscription[integratedDevice.deviceId.toString()] << (targetDNID + ":" + capability) mySubscription[integratedDevice.deviceId.toString()] = mySubscription[integratedDevice.deviceId.toString()].unique() } if (myRevSubscription[targetDevID] == null) { myRevSubscription[targetDevID] = [targetDNID + ":" + capability] } else { myRevSubscription[targetDevID] << (targetDNID + ":" + capability) myRevSubscription[targetDevID] = myRevSubscription[targetDevID].unique() } atomicState.subscriptionMap = mySubscription atomicState.RevSubscriptionMap = myRevSubscription stateSaved = true attributeMap.each { key, value -> if (key.startsWith(capability + ":")) { attribute = key.substring(capability.length() + 1) subscribe(integratedDevice, attribute, integrationHandler) subscribe(deviceInfo, attribute, revIntegrationHandler) int ndx = attribute.indexOf('.') if (ndx < 1) { triggerValue = "" } else { triggerValue = attribute.substring(ndx + 1) attribute = attribute.substring(0, ndx) } if (smartList.indexOf(capability) < 0) { attributeValue = deviceInfo.currentState(attribute)?.value if (attributeValue != null && (triggerValue == "" || triggerValue == attributeValue)) deviceInfo.sendEvent(name: attribute, value: attributeValue, isStateChange: true, descriptionText: integratedDevice.label + " was subscribed to " + app.name + " for " + deviceInfo.label) } else { attributeValue = integratedDevice.currentState(attribute)?.value if (attributeValue != null && (triggerValue == "" || triggerValue == attributeValue)) integratedDevice.sendEvent(name: attribute, value: attributeValue, isStateChange: true, descriptionText: integratedDevice.label + " was subscribed to " + app.name + " for " + deviceInfo.label) } } } } } } if (!stateSaved) { atomicState.subscriptionMap = mySubscription atomicState.RevSubscriptionMap = myRevSubscription } return capList } void subscriptionCleanup() { boolean fixSubscriptions = false com.hubitat.app.DeviceWrapper deviceInfo settings.findAll { it.key.startsWith("integrate:") }.each { String[] integrations = it.key.split(':') if (integrations.size() < 3 || (integrations[2] != "smart" && !(it.value instanceof com.hubitat.app.DeviceWrapper))) { log.trace app.name + ": Removing malformed integration setting $it.key" app.removeSetting(it.key) fixSubscriptions = true } else { if (integrations[1] == state.ElkM1DNI) deviceInfo = getChildDevice(state.ElkM1DNI) else deviceInfo = getChildDevice(state.ElkM1DNI).getChildDevice(integrations[1]) if (deviceInfo == null) { log.trace app.name + ": Removing integration setting for missing child device $it.key" app.removeSetting(it.key) fixSubscriptions = true } else if (integrations[2] == "smart") { if (!it.value || getSmartList(deviceInfo.deviceNetworkId, deviceInfo.getCapabilities().collect { return it.name }).size() == 0) app.removeSetting(it.key) } else if (!deviceInfo.hasCapability(integrations[2])) { log.trace app.name + ": Removing integration setting for child device no longer supporting capability $it.key" app.removeSetting(it.key) fixSubscriptions = true } else { if (!it.value?.hasCapability(integrations[2])) { log.trace app.name + ": ${it.value?.label} no longer supports capability ${integrations[2]}" fixSubscriptions = true } } } } if (fixSubscriptions) { resubscribe() } else { if (atomicState.subscriptionMap == null) atomicState.subscriptionMap = [:] if (atomicState.RevSubscriptionMap == null) atomicState.RevSubscriptionMap = [:] } } int resubscribe() { int cnt = 0 String lastDNID = "" com.hubitat.app.DeviceWrapper deviceInfo log.trace app.name + ": Rebuilding integration subscriptions" unsubscribe() if (enableHSMtoElk) subscribe(location, "hsmStatus", statusHandler) atomicState.subscriptionMap = [:] atomicState.RevSubscriptionMap = [:] settings.findAll { it.key.startsWith("integrate:") }.each { String[] integrations = it.key.split(':') if (integrations[1] != lastDNID) { if (integrations[1] == state.ElkM1DNI) deviceInfo = getChildDevice(state.ElkM1DNI) else deviceInfo = getChildDevice(state.ElkM1DNI).getChildDevice(integrations[1]) if (deviceInfo != null) { subscribeDevice(deviceInfo) cnt++ } lastDNID = integrations[1] } } return cnt } List getSmartList(String deviceNetworkId, List capabilityList) { String deviceType = deviceNetworkId == state.ElkM1DNI ? "__" : deviceNetworkId.substring(state.ElkM1DNI.length()).take(3) List smartList = [] capabilityList.each { if (smartMap[it] != null && (smartMap[it] == "" || smartMap[it].indexOf(deviceType) < 0)) smartList << it } return smartList } void createElkM1ParentDevice() { if (dbgEnable) log.debug app.name + ": Creating Parent ElkM1 Device" if (getChildDevice(state.ElkM1DNI) == null) { state.ElkM1DNI = UUID.randomUUID().toString() if (dbgEnable) log.debug app.name + ": Setting state.ElkM1DNI ${state.ElkM1DNI}" addChildDevice("belk", "Elk M1 Driver", state.ElkM1DNI, null, [name: elkM1Name, isComponent: true, label: elkM1Name]) getChildDevice(state.ElkM1DNI).updateSetting("ip", [type: "text", value: elkM1IP]) getChildDevice(state.ElkM1DNI).updateSetting("port", [type: "number", value: elkM1Port]) getChildDevice(state.ElkM1DNI).updateSetting("keypad", [type: "number", value: elkM1Keypad]) getChildDevice(state.ElkM1DNI).updateSetting("code", [type: "text", value: elkM1Code]) sendEvent(name: "createElkM1ParentDevice", value: state.ElkM1DNI, descriptionText: "${app.name} created parent device ${state.ElkM1DNI}") pauseExecution(10000) if (dbgEnable) { if (getChildDevice(state.ElkM1DNI)) log.debug app.name + ": Found a Child Elk M1 ${getChildDevice(state.ElkM1DNI).label}" else log.debug app.name + ": Did not find a Parent Elk M1" } } } String createDevice() { if (deviceType == "SP") deviceNumber = 1 String DeviceType = deviceType int DeviceNumber = deviceNumber if (DeviceType != null && DeviceType.length() == 1 && DeviceType >= 'A' && DeviceType <= 'P') { DeviceNumber = "ABCDEFGHIJKLMNOP".indexOf(DeviceType) * 16 + DeviceNumber DeviceType = '07' } if (DeviceType == null || elkTextDescriptionsTypes[DeviceType] == null) { return "Please select a device type." } else if (DeviceNumber == null || DeviceNumber < 1 || DeviceNumber > elkTextDescriptionsMax[DeviceType]) { return "Please enter a device number between 1 and ${elkTextDescriptionsMax[DeviceType]} for ${elkTextDescriptionsTypes[DeviceType]}." } else if (deviceName == null || deviceName.trim().length() == 0) { return "Please enter a device name." } else { sendEvent(name: "createDevice", value: elkTextDescriptionsTypes[DeviceType] + " " + DeviceNumber + ":" + deviceName, descriptionText: "${app.name} starting import of type ${elkTextDescriptionsTypes[DeviceType]}, " + "number ${DeviceNumber}, name ${deviceName}") String deviceText = null if (dbgEnable) log.debug app.name + ": Starting validation of ${deviceName} DeviceType: ${DeviceType} DeviceNumber: ${DeviceNumber}" getChildDevice(state.ElkM1DNI).createDevice([deviceNumber: String.format("%03d", DeviceNumber), deviceName: deviceName, deviceType : DeviceType, deviceText: deviceText]) return "" } } String importDevices() { if (deviceType == null || elkTextDescriptionsTypes[deviceType] == null) { return "Please select a device type." } else { sendEvent(name: "importDevices", value: elkTextDescriptionsTypes[deviceType], descriptionText: "${app.name} starting import of type ${elkTextDescriptionsTypes[deviceType]}") getChildDevice(state.ElkM1DNI).startCreatingDevice(deviceType) return "" } } //General App Events void appButtonHandler(btn) { state.buttonClicked = btn } void installed() { initialize() } void updated() { log.info app.name + " updated" initialize() } void initialize() { log.info app.name + " initialize" } void uninstalled() { getChildDevices().each { deleteChildDevice(it.deviceNetworkId) } } void setArmMode(String armMode) { if (state.armMode == null || state.armMode != armMode) { if (dbgEnable) log.debug "${app.name}: Arm Mode changing from ${state.armMode} to ${armMode}" String hsmSetArm String locationMode switch (armMode) { case "Disarmed": hsmSetArm = "disarm" locationMode = modeDisarmed break case "Away": hsmSetArm = "armAway" locationMode = modeArmedAway break case "Home": hsmSetArm = "armHome" locationMode = modeArmedHome break case "Night": hsmSetArm = "armNight" locationMode = modeArmedNight break case "Vacation": hsmSetArm = "armAway" locationMode = modeArmedVacation break } if (hsmSetArm == "disarm") { unlockIt() speakDisarmed() } else { lockIt() speakArmed() } String hsmStatus = location.hsmStatus if (dbgEnable && enableElktoHSM) log.debug "${app.name}: HSM arm changing from ${hsmStatus} to ${hsmSetArm}" if (enableElktoHSM && ((hsmSetArm == "armAway" && hsmStatus != "armedAway" && hsmStatus != "armingAway") || (hsmSetArm == "armHome" && hsmStatus != "armedHome" && hsmStatus != "armingHome") || (hsmSetArm == "armNight" && hsmStatus != "armedNight" && hsmStatus != "armingNight") || (hsmSetArm == "disarm" && hsmStatus != "disarmed" && hsmStatus != "allDisarmed"))) sendLocationEvent(name: "hsmSetArm", value: hsmSetArm, descriptionText: getChildDevice(state.ElkM1DNI).label + " was armed " + armMode) if (enableLocationMode && locationMode != null && locationMode != "") { if (location.getModes().findIndexOf { it.name == locationMode } != -1) { String curmode = location.currentMode.name if (dbgEnable) log.debug "${app.name}: Location Mode changing from $curmode to $locationMode" location.setMode(locationMode) } } state.armMode = armMode } } void smartRefresh() { String[] smartArr com.hubitat.app.DeviceWrapper childDevice String deviceType String[] integrationArr String[] attributeArr String attributes String triggerValue String attributeValue int integerValue String cmd atomicState.smartRefresh = now() if (dbgEnable) log.debug "${app.name}: Starting smartRefresh ${atomicState.smartRefresh}" runIn(10, stopSmartRefresh) // Find smart refresh child devices settings.findAll { k, v -> k.startsWith("integrate:") && k.endsWith(":smart") && v }.each { smartKey, smartValue -> smartArr = smartKey.split(':') if (smartArr.size() > 2) { if (smartArr[1] == state.ElkM1DNI) { childDevice = getChildDevice(state.ElkM1DNI) deviceType = "__" } else { childDevice = getChildDevice(state.ElkM1DNI).getChildDevice(smartArr[1]) deviceType = smartArr[1].substring(state.ElkM1DNI.length()).take(3) } // Find devices integrated with child device if (childDevice != null) { settings.findAll { k, v -> k.startsWith("integrate:" + smartArr[1] + ":") && !k.endsWith(":smart") }.each { integrationKey, deviceInfo -> integrationArr = integrationKey.split(':') // If this integration's capability is a smart refresh capability... if (smartMap[integrationArr[2]] != null && (smartMap[integrationArr[2]] == "" || smartMap[integrationArr[2]].indexOf(deviceType) < 0)) { if (deviceInfo?.hasCapability(integrationArr[2]) && !deviceInfo?.deviceNetworkId.startsWith(state.ElkM1DNI)) { // Find all attributes integrated for this capability attributeMap.findAll { k, v -> k.startsWith(integrationArr[2] + ":") }.each { attributeKey, attributeCmd -> attributeArr = attributeKey.split(":") if (attributeArr.size() > 1 && attributeCmd != "attributeonly" && childDevice.hasCommand(attributeCmd)) { int ndx = attributeArr[1].indexOf('.') if (ndx < 1) { triggerValue = "" attributes = attributeArr[1] } else { triggerValue = attributeArr[1].substring(ndx + 1) attributes = attributeArr[1].substring(0, ndx) } // Refresh child device with the attribute's current value attributeValue = deviceInfo.currentState(attributes)?.value if (attributeValue != null && (triggerValue == "" || triggerValue == attributeValue)) { integerValue = attributeValue.isInteger() ? attributeValue.toInteger() : 0 if (dbgEnable) log.debug app.name + " refreshing " + deviceInfo.label + " " + attributes + " value " + attributeValue + " on " + childDevice.label triggerCmd(childDevice, attributeCmd, attributeValue, integerValue) } } } } } } } } } } void stopSmartRefresh() { if (dbgEnable) log.debug "${app.name}: smartRefresh complete ${atomicState.smartRefresh}" atomicState.remove("smartRefresh") } void integrationHandler(com.hubitat.hub.domain.Event evt) { //log.debug "Unhandled integration event:" //evt?.properties?.each { item -> log.debug "$item.key = $item.value" } //log.debug "Device $evt.displayName, attribute $evt.name = $evt.value" int triggerCnt = 0 com.hubitat.app.DeviceWrapper deviceInfo String cmd List targetList = atomicState.subscriptionMap[evt.deviceId.toString()] if (targetList != null) { targetList.each { String[] targetArr = it.split(':') if (targetArr.size() > 1) { cmd = attributeMap[targetArr[1] + ":" + evt.name + "." + evt.value] if (cmd == null) cmd = attributeMap[targetArr[1] + ":" + evt.name] if (cmd != null) { if (targetArr[0] == state.ElkM1DNI) deviceInfo = getChildDevice(state.ElkM1DNI) else deviceInfo = getChildDevice(state.ElkM1DNI).getChildDevice(targetArr[0]) if (deviceInfo != null && deviceInfo.hasCapability(targetArr[1])) { triggerCnt += updateDevice(evt, deviceInfo, cmd, true) } } } } } if (triggerCnt == 0) { // This subscription is no longer needed unsubscribe(evt.getDevice(), evt.name, integrationHandler) unsubscribe(evt.getDevice(), evt.name + "." + evt.value, integrationHandler) } } void revIntegrationHandler(com.hubitat.hub.domain.Event evt) { //log.debug "Device $evt.displayName, attribute $evt.name = $evt.value" int triggerCnt = 0 com.hubitat.app.DeviceWrapper deviceInfo String cmd List targetList = atomicState.RevSubscriptionMap[evt.deviceId.toString()] if (targetList != null) { targetList.each { String[] targetArr = it.split(':') if (targetArr.size() > 1) { cmd = attributeMap[targetArr[1] + ":" + evt.name + "." + evt.value] if (cmd == null) cmd = attributeMap[targetArr[1] + ":" + evt.name] if (cmd != null) { deviceInfo = settings["integrate:${targetArr[0]}:${targetArr[1]}"] if (deviceInfo?.hasCapability(targetArr[1])) { triggerCnt += updateDevice(evt, deviceInfo, cmd, false) } } } } } if (triggerCnt == 0) { // This subscription is no longer needed unsubscribe(evt.getDevice(), evt.name, revIntegrationHandler) unsubscribe(evt.getDevice(), evt.name + "." + evt.value, revIntegrationHandler) } } int updateDevice(com.hubitat.hub.domain.Event evt, com.hubitat.app.DeviceWrapper deviceInfo, String cmd, boolean fromIntegrated) { //log.debug "Device $evt.displayName, attribute $evt.name = $evt.value ($evt.integerValue), todevice $deviceInfo.label, command: $cmd" int triggerCnt = 0 boolean isSmartRefresh = (atomicState.smartRefresh != null && now() - (long) (atomicState.smartRefresh) < 10000) if (cmd != "attributeonly" && deviceInfo.hasCommand(cmd)) { triggerCnt = 1 if ((deviceInfo.currentState(evt.name)?.value == null || deviceInfo.currentState(evt.name).value != evt.value) && (!isSmartRefresh || fromIntegrated)) { if (dbgEnable) log.debug app.name + ": ${evt.displayName} triggering command ${cmd}(${evt.value}) on ${deviceInfo.label}" + " isSmartRefresh ${isSmartRefresh} (${atomicState.smartRefresh}) fromIntegrated ${fromIntegrated}" triggerCmd(deviceInfo, cmd, evt.value, evt.integerValue) } } else if (deviceInfo.hasAttribute(evt.name)) { triggerCnt = 1 if ((deviceInfo.currentState(evt.name)?.value == null || deviceInfo.currentState(evt.name).value != evt.value) && (!isSmartRefresh || fromIntegrated)) { if (dbgEnable) log.debug app.name + ": ${evt.displayName} setting attribute ${evt.name} = ${evt.value} on ${deviceInfo.label}" + " isSmartRefresh ${isSmartRefresh} (${atomicState.smartRefresh}) fromIntegrated ${fromIntegrated}" String descriptionText = evt.descriptionText if (descriptionText == null || descriptionText == "") descriptionText = deviceInfo.label + " was updated by " + app.name + " with event from " + evt.displayName deviceInfo.sendEvent(name: evt.name, source: evt.source, type: evt.type, unit: evt.unit, value: evt.value, descriptionText: descriptionText) } } return triggerCnt } void triggerCmd(deviceInfo, String cmd, String value, int integerValue) { switch (cmd) { case "off": deviceInfo.off() break case "open": deviceInfo.open() break case "close": deviceInfo.close() break case "on": deviceInfo.on() break case "lock": deviceInfo.lock() break case "unlock": deviceInfo.unlock() break case "push": deviceInfo.push(integerValue) break case "setLevel": deviceInfo.setLevel(integerValue) break case "setCoolingSetpoint": deviceInfo.setCoolingSetpoint(integerValue) break case "setHeatingSetpoint": deviceInfo.setHeatingSetpoint(integerValue) break case "setThermostatFanMode": deviceInfo.setThermostatFanMode(value) break case "setThermostatMode": deviceInfo.setThermostatMode(value) break case "setTemperature": deviceInfo.setTemperature(integerValue) break case "disarm": deviceInfo.disarm() break case "armAway": deviceInfo.armAway() break case "armHome": deviceInfo.armHome() break case "armNight": deviceInfo.armNight() break } } void statusHandler(com.hubitat.hub.domain.Event evt) { if (evt?.source == "LOCATION" && evt?.name == "hsmStatus" && evt?.value != null) { boolean lock if (enableHSMtoElk && !lock && evt?.value && evt.value != state.hsmStatus) { lock = true log.info "HSM Alert: $evt.value" if (dbgEnable) log.debug app.name + ": HSM is enabled" com.hubitat.app.DeviceWrapper parentDevice = getChildDevice(state.ElkM1DNI) switch (evt.value) { case "armedAway": case "armingAway": if (dbgEnable) log.debug app.name + ": Sending Arm Away" if (parentDevice.currentValue("armState") == "Ready to Arm") { parentDevice.armAway() } break case "armedHome": case "armingHome": if (dbgEnable) log.debug app.name + ": Sending Arm Home" if (parentDevice.currentValue("armState") == "Ready to Arm") { parentDevice.armHome() } break case "armedNight": case "armingNight": if (dbgEnable) log.debug app.name + ": Sending Arm Night" if (parentDevice.currentValue("armState") == "Ready to Arm") { parentDevice.armNight() } break case "disarmed": case "allDisarmed": if (dbgEnable) log.debug app.name + ": Sending Disarm" if (parentDevice.currentValue("armStatus") != "Disarmed") { parentDevice.disarm() } break } lock = false; } state.hsmStatus = evt.value } else { log.debug "Unhandled event:" evt?.properties?.each { item -> log.debug "$item.key = $item.value" } } } void speakArmed() { if (!armedText) armedText = "Armed" speakIt(armedText, armedBool == null ? false : armedBool) } void speakArmingAway() { if (!armingAwayText) armingAwayText = "Arming Away" speakIt(armingAwayText, armingAwayBool == null ? false : armingAwayBool) } void speakArmingVacation() { if (!armingVacationText) armingVacationText = "Arming Vacation" speakIt(armingVacationText, armingVacationBool == null ? false : armingVacationBool) } void speakArmingHome() { if (!armingHomeText) armingHomeText = "Arming Home" speakIt(armingHomeText, armingHomeBool == null ? false : armingHomeBool) } void speakArmingNight() { if (!armingNightText) armingNightText = "Arming Night" speakIt(armingNightText, armingNightBool == null ? false : armingNightBool) } void speakDisarmed() { if (!disarmedText) disarmedText = "Disarmed" speakIt(disarmedText, disarmedBool == null ? false : disarmedBool) } void speakEntryDelay() { if (!entryDelayAlarmText) entryDelayAlarmText = "Entry Delay in Progress, Alarm eminent" speakIt(entryDelayAlarmText, entryDelayAlarmBool == null ? false : entryDelayAlarmBool) } void speakAlarm() { if (!alarmText) alarmText = "Alarm, Alarm, Alarm, Alarm, Alarm" speakIt(alarmText, alarmBool == null ? false : alarmBool) } void speakIt(String str, boolean isEnabled = true) { if (dbgEnable) log.debug app.name + ": TTS: $str" if (isEnabled && speechDevices) { if (atomicState.speaking) { if (dbgEnable) log.debug app.name + ": Already Speaking" runOnce(new Date(now() + 10000), speakRetry, [overwrite: false, data: [str: str]]) } else { if (dbgEnable) log.debug app.name + ": Found Speech Devices" atomicState.speaking = true speechDevices.speak(str) atomicState.speaking = false if (notificationDevices) { if (dbgEnable) log.debug app.name + ": Found Notification Devices" notificationDevices.deviceNotification(str) } } } } void lockIt() { if (dbgEnable) log.debug app.name + ": Lock" if (armLocks) { if (dbgEnable) log.debug app.name + ": Found Lock" armLocks.lock() } } void unlockIt() { if (dbgEnable) log.debug app.name + ": Unlock" if (disarmLocks) { if (dbgEnable) log.debug app.name + ": Found Lock" disarmLocks.unlock() } } void speakRetry(data) { if (data.str) speakIt(data.str); } @Field static final String ZoneName = "Zone" @Field static final String AreaName = "Area" @Field static final String UserName = "User" @Field static final String Keypad = "Keypad" @Field static final String OutputName = "Output" @Field static final String TaskName = "Task" @Field static final String TelephoneName = "Telephone" @Field static final String LightName = "Light" @Field static final String AlarmDurationName = "Alarm Duration" @Field static final String CustomSettings = "Custom Setting" @Field static final String CountersNames = "Counter" @Field static final String ThermostatNames = "Thermostat" @Field static final String FunctionKey1Name = "Keypad button 1" @Field static final String FunctionKey2Name = "Keypad button 2" @Field static final String FunctionKey3Name = "Keypad button 3" @Field static final String FunctionKey4Name = "Keypad button 4" @Field static final String FunctionKey5Name = "Keypad button 5" @Field static final String FunctionKey6Name = "Keypad button 6" @Field static final String AudioZoneName = "Audio Zone" @Field static final String AudioSourceName = "Audio Source" @Field static final String Speech = "Speech" @Field final Map elkTextDescriptionsTypes = [ '00': ZoneName, '01': AreaName, '02': UserName, '03': Keypad, '04': OutputName, '05': TaskName, '06': TelephoneName, '07': LightName, '08': AlarmDurationName, '09': CustomSettings, '10': CountersNames, '11': ThermostatNames, '12': FunctionKey1Name, '13': FunctionKey2Name, '14': FunctionKey3Name, '15': FunctionKey4Name, '16': FunctionKey5Name, '17': FunctionKey6Name, '18': AudioZoneName, '19': AudioSourceName, 'SP': Speech ] @Field final Map elkTextDescriptionsMax = [ '00': 208, // ZoneName '01': 8, // AreaName '02': 199, // UserName '03': 16, // Keypad '04': 64, // OutputName '05': 32, // TaskName '06': 8, // TelephoneName '07': 256, // LightName '08': 12, // AlarmDurationName '09': 20, // CustomSettings '10': 64, // CountersNames '11': 16, // ThermostatNames '12': 16, // FunctionKey1Name '13': 16, // FunctionKey2Name '14': 16, // FunctionKey3Name '15': 16, // FunctionKey4Name '16': 16, // FunctionKey5Name '17': 16, // FunctionKey6Name '18': 18, // AudioZoneName '19': 12, // AudioSourceName 'SP': 1 // Speech ] @Field final Map smartMap = [ // Does not apply to which device types. Double understore = parent. "PushableButton" : "__,_P_", "RelativeHumidityMeasurement": "", "Switch" : "", "SwitchLevel" : "", "TemperatureMeasurement" : "", "Thermostat" : "" ] @Field final Map capabilityMap = [ "Lock" : "capability.lock", "PushableButton" : "capability.pushableButton", "RelativeHumidityMeasurement": "capability.relativeHumidityMeasurement", "SecurityKeypad" : "capability.securityKeypad", "Switch" : "capability.switch", "SwitchLevel" : "capability.switchLevel", "TemperatureMeasurement" : "capability.temperatureMeasurement", "Thermostat" : "capability.thermostat" ] @Field final Map attributeMap = [ "Lock:lock.locked" : "lock", "Lock:lock.unlocked" : "unlock", "PushableButton:pushed" : "push", "RelativeHumidityMeasurement:humidity" : "attributeonly", "SecurityKeypad:securityKeypad.disarmed" : "disarm", "SecurityKeypad:securityKeypad.armed away" : "armAway", "SecurityKeypad:securityKeypad.armed home" : "armHome", "SecurityKeypad:securityKeypad.armed night" : "armNight", "SecurityKeypad:securityKeypad.arming away" : "armAway", "SecurityKeypad:securityKeypad.arming home" : "armHome", "SecurityKeypad:securityKeypad.arming night": "armNight", "Switch:switch.on" : "on", "Switch:switch.off" : "off", "SwitchLevel:level" : "setLevel", "TemperatureMeasurement:temperature" : "setTemperature", "Thermostat:coolingSetpoint" : "setCoolingSetpoint", "Thermostat:heatingSetpoint" : "setHeatingSetpoint", "Thermostat:supportedThermostatFanModes" : "attributeonly", "Thermostat:supportedThermostatModes" : "attributeonly", "Thermostat:temperature" : "setTemperature", "Thermostat:thermostatFanMode" : "setThermostatFanMode", "Thermostat:thermostatMode" : "setThermostatMode", "Thermostat:thermostatOperatingState" : "attributeonly", "Thermostat:thermostatSetpoint" : "attributeonly" ] /*********************************************************************************************************************** * * Release Notes * * Version: 0.2.6 * Fixed issue with the app hanging when it is first installed. * * Version: 0.2.5 * Renamed setThermostatTemperature to setTemperature to match other Hubitat drivers. * * Version: 0.2.4 * Fixed a bug not properly managing integrated device subscriptions. * Fixed long standing issue that caused the start of importing devices when you used the back button. * Renamed Stay mode to Home mode to align with Hubitat terminology. * Added Hub Location Mode integration. * Added integration of main Elk device, no longer just child devices, to physical devices. * Added Smart Refresh to refresh integrated devices before refreshing Elk child devices. * * Version: 0.2.3 * Fixed null variable issue. * * Version: 0.2.2 * Added import of Custom and Counter value devices. * * Version: 0.2.1 * Added integration of virtual child devices with physical HE devices. * * Version: 0.2.0 * Added import of Speech device. Also finished TTS, Notification and HSM Integration code. * * Version: 0.1.11 * Removed unused password setting. * * Version: 0.1.10 * Added import of Lighting devices. * * Version: 0.1.9 * Added Thermostat to list of automatic import devices. * * Version: 0.1.8 * Added Keypad device type to import for their built in temperature sensor. * * Version: 0.1.7 * Combined Zone Type creation "Contact" and "Motion" and made "Zone" to fix a zone creation bug. * * Version: 0.1.6 * Moved some zone creation code to Elk Driver * * Version: 0.1.5 * Added support for manual inclusion of Elk M1 outputs and tasks * Added support for importing Elk M1 outputs, tasks and thermostats * Cleaned up app and related code * * Version: 0.1.4 * Added support for importing Elk M1 zones * * Version: 0.1.3 * Removed any unsupported features for now * * Version: 0.1.2 * Added thermostat support (data receipt only) * * Version: 0.1.1 * Ported code from Doug Beard Envisalink Integration * HSM integration not currently supported * All functionality not fully tested * ***********************************************************************************************************************/ /*********************************************************************************************************************** * * Feature Request & Known Issues * * I - Must initialize the Elk M1 Device prior to using the Elk M1 App Import Zone functions * */