Skip to content

Instantly share code, notes, and snippets.

@jtp10181
Last active February 26, 2023 02:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jtp10181/9f5d72019bcd15f9453960eab1de7d59 to your computer and use it in GitHub Desktop.
Save jtp10181/9f5d72019bcd15f9453960eab1de7d59 to your computer and use it in GitHub Desktop.
Hubitat Generic Z-Wave Tilt/Contact Sensor
/*
* Generic Z-Wave Tilt/Contact Sensor
*
* For Support, Information and Updates:
* https://github.com/jtp10181/Hubitat/tree/main/Drivers/
*
Changelog:
## [0.1.0] - 2023-02-04 (@jtp10181)
### Added
- Initial Release
* Copyright 2023 Jeff Page
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import groovy.transform.Field
@Field static final String VERSION = "0.1.0"
metadata {
definition (
name: "Generic Z-Wave Tilt/Contact Sensor",
namespace: "jtp10181",
author: "Jeff Page (@jtp10181)",
importUrl: "https://raw.githubusercontent.com/jtp10181/Hubitat/main/Drivers/generic/zwave-tilt-contact-sensor.groovy"
) {
capability "Sensor"
capability "ContactSensor"
capability "Battery"
capability "TamperAlert"
command "fullConfigure"
command "forceRefresh"
attribute "external", "string"
attribute "syncStatus", "string"
}
preferences {
command "setParameter",[[name:"parameterNumber*",type:"NUMBER", description:"Parameter Number", constraints:["NUMBER"]],
[name:"value*",type:"NUMBER", description:"Parameter Value", constraints:["NUMBER"]],
[name:"size",type:"NUMBER", description:"Parameter Size", constraints:["NUMBER"]]]
//Logging options similar to other Hubitat drivers
input "txtEnable", "bool", title: fmtTitle("Enable Description Text Logging?"), defaultValue: true
input "debugEnable", "bool", title: fmtTitle("Enable Debug Logging?"), defaultValue: true
}
}
//Preference Helpers
String fmtDesc(String str) {
return "<div style='font-size: 85%; font-style: italic; padding: 1px 0px 4px 2px;'>${str}</div>"
}
String fmtTitle(String str) {
return "<strong>${str}</strong>"
}
//Set Command Class Versions
@Field static final Map commandClassVersions = [
0x70: 2, // Configuration (configurationv2)
0x71: 8, // Notification (notificationv8)
0x80: 1, // Battery (batteryv1)
0x84: 2, // Wakeup (wakeupv2)
0x85: 2, // Association (associationv2)
0x86: 2, // Version (versionv2)
]
/*******************************************************************
***** Core Functions
********************************************************************/
void installed() {
logWarn "installed..."
}
void fullConfigure() {
logWarn "configure..."
if (debugEnable) runIn(1800, debugLogsOff)
if (!pendingChanges || state.resyncAll == null) {
logForceWakeupMessage "Full Re-Configure"
state.resyncAll = true
} else {
logForceWakeupMessage "Pending Configuration Changes"
}
updateSyncingStatus(1)
}
void updated() {
logDebug "updated..."
logDebug "Debug logging is: ${debugEnable == true}"
logDebug "Description logging is: ${txtEnable == true}"
if (debugEnable) runIn(1800, debugLogsOff)
if (!firmwareVersion) {
state.resyncAll = true
state.pendingRefresh = true
logForceWakeupMessage "Full Re-Configure and Refresh"
}
if (pendingChanges) {
logForceWakeupMessage "Pending Configuration Changes"
}
else if (!state.resyncAll && !state.pendingRefresh) {
state.remove("INFO")
}
updateSyncingStatus(1)
}
void forceRefresh() {
logDebug "refresh..."
state.pendingRefresh = true
logForceWakeupMessage "Sensor Info Refresh"
}
String setParameter(paramNum, value, size = null) {
if (paramNum == null || value == null || size == null) {
logWarn "Incomplete parameter list supplied..."
logWarn "Syntax: setParameter(paramNum, value, size)"
return
}
logDebug "setParameter ( number: $paramNum, value: $value, size: $size )"
return secureCmd(configSetCmd([num: paramNum, size: size], value as Integer))
}
/*******************************************************************
***** Z-Wave Reports
********************************************************************/
void parse(String description) {
hubitat.zwave.Command cmd = zwave.parse(description, commandClassVersions)
if (cmd) {
logTrace "parse: ${description} --PARSED-- ${cmd}"
zwaveEvent(cmd)
} else {
logWarn "Unable to parse: ${description}"
}
//Update Last Activity
updateLastCheckIn()
}
void zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions)
logTrace "${cmd} --ENCAP-- ${encapsulatedCmd}"
if (encapsulatedCmd) {
zwaveEvent(encapsulatedCmd)
} else {
logWarn "Unable to extract encapsulated cmd from $cmd"
}
}
//Decodes Multichannel Encapsulated Commands
void zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions)
logTrace "${cmd} --ENCAP-- ${encapsulatedCmd}"
if (encapsulatedCmd) {
zwaveEvent(encapsulatedCmd, cmd.sourceEndPoint as Integer)
} else {
logWarn "Unable to extract encapsulated cmd from $cmd"
}
}
//Decodes Supervision Encapsulated Commands (and replies to device)
void zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd, ep=0) {
def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions)
logTrace "${cmd} --ENCAP-- ${encapsulatedCmd}"
if (encapsulatedCmd) {
zwaveEvent(encapsulatedCmd, ep)
} else {
logWarn "Unable to extract encapsulated cmd from $cmd"
}
sendCommands(secureCmd(zwave.supervisionV1.supervisionReport(sessionID: cmd.sessionID, reserved: 0, moreStatusUpdates: false, status: 0xFF, duration: 0), ep))
}
void zwaveEvent(hubitat.zwave.commands.versionv2.VersionReport cmd) {
logTrace "${cmd}"
String fullVersion = String.format("%d.%02d",cmd.firmware0Version,cmd.firmware0SubVersion)
device.updateDataValue("firmwareVersion", fullVersion)
logDebug "Received Version Report - Firmware: ${fullVersion}"
}
void zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) {
logTrace "${cmd}"
Number val = cmd.scaledConfigurationValue
logDebug "Parameter #${cmd.parameterNumber} = ${val.toString()}"
}
void zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) {
logTrace "${cmd}"
Integer grp = cmd.groupingIdentifier
if (grp == 1) {
logDebug "Lifeline Association: ${cmd.nodeId}"
state.group1Assoc = (cmd.nodeId == [zwaveHubNodeId]) ? true : false
}
else {
logWarn "Unhandled Group: $cmd"
}
}
void zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd, ep=0) {
logTrace "${cmd} (ep ${ep})"
Integer batLvl = cmd.batteryLevel
if (batLvl == 0xFF) {
batLvl = 1
logWarn "LOW BATTERY WARNING"
}
batLvl = validateRange(batLvl, 100, 1, 100)
String descText = "battery level is ${batLvl}%"
sendEventLog(name:"battery", value:batLvl, unit:"%", desc:descText, isStateChange:true)
}
void zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpIntervalReport cmd) {
logTrace "${cmd}"
BigDecimal wakeHrs = safeToDec(cmd.seconds/3600,0,2)
logDebug "WakeUp Interval is $cmd.seconds seconds ($wakeHrs hours)"
device.updateDataValue("zwWakeupInterval", "${cmd.seconds}")
}
void zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpNotification cmd, ep=0) {
logTrace "${cmd} (ep ${ep})"
logDebug "WakeUp Notification Received"
List<String> cmds = []
cmds << batteryGetCmd()
//Refresh all if requested
if (state.pendingRefresh) { cmds += getRefreshCmds() }
//Any configuration needed
cmds += getConfigureCmds()
//This needs a longer delay
cmds << "delay 1000" << wakeUpNoMoreInfoCmd()
//Clear pending status
state.resyncAll = false
state.pendingRefresh = false
state.remove("INFO")
sendCommands(cmds, 400)
}
void zwaveEvent(hubitat.zwave.commands.notificationv8.NotificationReport cmd, ep=0) {
logTrace "${cmd} (ep ${ep})"
switch (cmd.notificationType) {
case 0x07: //Home Security
switch (cmd.event) {
case 0x02: //Intrusion (Tilt Sensor)
sendEventLog(name:"contact", value:(cmd.v1AlarmLevel ? "open" : "closed"))
break
case 0xFE: //Unknown event/state (External Contact)
sendEventLog(name:"external", value:(cmd.v1AlarmLevel ? "open" : "closed"))
break
case 0x03: //Tampering - cover removed
sendEventLog(name:"tamper", value:(cmd.v1AlarmLevel ? "detected" : "clear"))
runIn(120, tamperClear) //Device may never send a clear
break
default:
logWarn "Unhandled NotificationReport event: ${cmd}"
}
break
default:
logWarn "Unhandled NotificationReport notificationType: ${cmd}"
}
}
void tamperClear() {
logDebug "tamperClear - tamper timed out, clearing"
sendEventLog(name:"tamper", value:"clear")
}
void zwaveEvent(hubitat.zwave.Command cmd, ep=0) {
logDebug "Unhandled zwaveEvent: $cmd (ep ${ep})"
}
/*******************************************************************
***** Z-Wave Command Shortcuts
********************************************************************/
//These send commands to the device either a list or a single command
void sendCommands(List<String> cmds, Long delay=200) {
sendHubCommand(new hubitat.device.HubMultiAction(delayBetween(cmds, delay), hubitat.device.Protocol.ZWAVE))
}
//Single Command
void sendCommands(String cmd) {
sendHubCommand(new hubitat.device.HubAction(cmd, hubitat.device.Protocol.ZWAVE))
}
//Consolidated zwave command functions so other code is easier to read
String associationSetCmd(Integer group, List<Integer> nodes) {
return secureCmd(zwave.associationV2.associationSet(groupingIdentifier: group, nodeId: nodes))
}
String associationRemoveCmd(Integer group, List<Integer> nodes) {
return secureCmd(zwave.associationV2.associationRemove(groupingIdentifier: group, nodeId: nodes))
}
String associationGetCmd(Integer group) {
return secureCmd(zwave.associationV2.associationGet(groupingIdentifier: group))
}
String versionGetCmd() {
return secureCmd(zwave.versionV2.versionGet())
}
String wakeUpIntervalGetCmd() {
return secureCmd(zwave.wakeUpV2.wakeUpIntervalGet())
}
String wakeUpIntervalSetCmd(val) {
return secureCmd(zwave.wakeUpV2.wakeUpIntervalSet(seconds:val, nodeid:zwaveHubNodeId))
}
String wakeUpNoMoreInfoCmd() {
return secureCmd(zwave.wakeUpV2.wakeUpNoMoreInformation())
}
String batteryGetCmd() {
return secureCmd(zwave.batteryV1.batteryGet())
}
String notificationGetCmd(notificationType, eventType) {
return secureCmd(zwave.notificationV8.notificationGet(notificationType: notificationType, v1AlarmType:0, event: eventType))
}
String configSetCmd(Map param, Number value) {
//Convert to signed integer for scaledConfigurationValue
Long sizeFactor = Math.pow(256,param.size).round()
if (value >= sizeFactor/2) { value -= sizeFactor }
return secureCmd(zwave.configurationV2.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: value))
}
String configGetCmd(Map param) {
return secureCmd(zwave.configurationV2.configurationGet(parameterNumber: param.num))
}
List configSetGetCmd(Map param, Integer value) {
List<String> cmds = []
cmds << configSetCmd(param, value)
cmds << configGetCmd(param)
return cmds
}
/*******************************************************************
***** Z-Wave Encapsulation
********************************************************************/
//Secure and MultiChannel Encapsulate
String secureCmd(String cmd) {
return zwaveSecureEncap(cmd)
}
String secureCmd(hubitat.zwave.Command cmd, ep=0) {
return zwaveSecureEncap(multiChannelEncap(cmd, ep))
}
//MultiChannel Encapsulate if needed
//This is called from secureCmd or supervisionEncap, do not call directly
String multiChannelEncap(hubitat.zwave.Command cmd, ep) {
//logTrace "multiChannelEncap: ${cmd} (ep ${ep})"
if (ep > 0) {
cmd = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:ep).encapsulate(cmd)
}
return cmd.format()
}
/*******************************************************************
***** Execute / Build Commands
********************************************************************/
List<String> getConfigureCmds() {
logDebug "getConfigureCmds..."
List<String> cmds = []
if (state.resyncAll || !firmwareVersion) {
cmds << versionGetCmd()
cmds << wakeUpIntervalSetCmd(43200)
cmds << wakeUpIntervalGetCmd()
}
cmds += getConfigureAssocsCmds()
if (state.resyncAll) clearVariables()
state.resyncAll = false
if (cmds) updateSyncingStatus(6)
return cmds ?: []
}
List<String> getRefreshCmds() {
List<String> cmds = []
cmds << versionGetCmd()
cmds << wakeUpIntervalGetCmd()
return cmds ?: []
}
void clearVariables() {
logWarn "Clearing state variables and data..."
//Clears State Variables
state.clear()
//Clear Data from other Drivers
device.removeDataValue("zwaveAssociationG1")
device.removeDataValue("zwaveAssociationG2")
device.removeDataValue("zwaveAssociationG3")
}
List getConfigureAssocsCmds() {
List<String> cmds = []
if (!state.group1Assoc || state.resyncAll) {
if (state.group1Assoc == false) {
logDebug "Adding missing lifeline association..."
}
cmds << associationSetCmd(1, [zwaveHubNodeId])
cmds << associationGetCmd(1)
}
return cmds
}
Integer getPendingChanges() {
Integer configChanges = 0
Integer pendingAssocs = Math.ceil(getConfigureAssocsCmds()?.size()/2) ?: 0
return (!state.resyncAll ? (configChanges + pendingAssocs) : configChanges)
}
/*******************************************************************
***** Event Senders
********************************************************************/
//evt = [name, value, type, unit, desc, isStateChange]
void sendEventLog(Map evt, Integer ep=0) {
//Set description if not passed in
evt.descriptionText = evt.desc ?: "${evt.name} set to ${evt.value}${evt.unit ?: ''}"
//Main Device Events
if (evt.name != "syncStatus") {
if (device.currentValue(evt.name).toString() != evt.value.toString()) {
logInfo "${evt.descriptionText}"
} else {
logDebug "${evt.descriptionText} [NOT CHANGED]"
}
}
//Always send event to update last activity
sendEvent(evt)
}
/*******************************************************************
***** Common Functions
********************************************************************/
/*** Parameter Store Map Functions ***/
@Field static Map<String, Map> configsList = new java.util.concurrent.ConcurrentHashMap()
Integer getParamStoredValue(Integer paramNum) {
//Using Data (Map) instead of State Variables
TreeMap configsMap = getParamStoredMap()
return safeToInt(configsMap[paramNum], null)
}
void setParamStoredValue(Integer paramNum, Integer value) {
//Using Data (Map) instead of State Variables
TreeMap configsMap = getParamStoredMap()
configsMap[paramNum] = value
configsList[device.id][paramNum] = value
device.updateDataValue("configVals", configsMap.inspect())
}
Map getParamStoredMap() {
Map configsMap = configsList[device.id]
if (configsMap == null) {
configsMap = [:]
if (device.getDataValue("configVals")) {
try {
configsMap = evaluate(device.getDataValue("configVals"))
}
catch(Exception e) {
logWarn("Clearing Invalid configVals: ${e}")
device.removeDataValue("configVals")
}
}
configsList[device.id] = configsMap
}
return configsMap
}
/*** Other Helper Functions ***/
void updateSyncingStatus(Integer delay=2) {
runIn(delay, refreshSyncStatus)
sendEventLog(name:"syncStatus", value:"Syncing...")
}
void refreshSyncStatus() {
Integer changes = pendingChanges
sendEventLog(name:"syncStatus", value:(changes ? "${changes} Pending Changes" : "Synced"))
}
void updateLastCheckIn() {
if (!isDuplicateCommand(state.lastCheckInTime, 60000)) {
state.lastCheckInTime = new Date().time
state.lastCheckInDate = convertToLocalTimeString(new Date())
}
}
BigDecimal getFirmwareVersion() {
String version = device?.getDataValue("firmwareVersion")
return ((version != null) && version.isNumber()) ? version.toBigDecimal() : 0.0
}
String convertToLocalTimeString(dt) {
def timeZoneId = location?.timeZone?.ID
if (timeZoneId) {
return dt.format("MM/dd/yyyy hh:mm:ss a", TimeZone.getTimeZone(timeZoneId))
} else {
return "$dt"
}
}
List convertIntListToHexList(intList, pad=2) {
def hexList = []
intList?.each {
hexList.add(Integer.toHexString(it).padLeft(pad, "0").toUpperCase())
}
return hexList
}
Integer validateRange(val, Integer defaultVal, Integer lowVal, Integer highVal) {
Integer intVal = safeToInt(val, defaultVal)
if (intVal > highVal) {
return highVal
} else if (intVal < lowVal) {
return lowVal
} else {
return intVal
}
}
Integer safeToInt(val, defaultVal=0) {
if ("${val}"?.isInteger()) { return "${val}".toInteger() }
else if ("${val}"?.isNumber()) { return "${val}".toDouble()?.round() }
else { return defaultVal }
}
BigDecimal safeToDec(val, defaultVal=0, roundTo=-1) {
BigDecimal decVal = "${val}"?.isNumber() ? "${val}".toBigDecimal() : defaultVal
if (roundTo == 0) { decVal = Math.round(decVal) }
else if (roundTo > 0) { decVal = decVal.setScale(roundTo, BigDecimal.ROUND_HALF_UP).stripTrailingZeros() }
if (decVal.scale()<0) { decVal = decVal.setScale(0) }
return decVal
}
boolean isDuplicateCommand(lastExecuted, allowedMil) {
!lastExecuted ? false : (lastExecuted + allowedMil > new Date().time)
}
/*******************************************************************
***** Logging Functions
********************************************************************/
private logForceWakeupMessage(msg) {
String helpText = "Check the manual for how to wake up the device."
logWarn "${msg} will execute the next time the device wakes up. ${helpText}"
state.INFO = "*** ${msg} *** Waiting for device to wake up. ${helpText}"
}
void logsOff() {}
void debugLogsOff() {
logWarn "Debug logging disabled..."
device.updateSetting("debugEnable",[value:"false",type:"bool"])
}
void logWarn(String msg) {
log.warn "${device.displayName}: ${msg}"
}
void logInfo(String msg) {
if (txtEnable) log.info "${device.displayName}: ${msg}"
}
void logDebug(String msg) {
if (debugEnable) log.debug "${device.displayName}: ${msg}"
}
//For Extreme Code Debugging - tracing commands
void logTrace(String msg) {
//Uncomment to Enable
//log.trace "${device.displayName}: ${msg}"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment