Skip to content

Instantly share code, notes, and snippets.

@jtp10181
Last active March 5, 2023 17:15
Show Gist options
  • Save jtp10181/95ee3d2dc8d4cc81565a07a5f94fcb0a to your computer and use it in GitHub Desktop.
Save jtp10181/95ee3d2dc8d4cc81565a07a5f94fcb0a to your computer and use it in GitHub Desktop.
ZEN52 Driver with extra option to track state of S1 and S2 inputs.
/*
* Zooz ZEN52 Double Relay Advanced
* - Model: ZEN52 - MINIMUM FIRMWARE 1.40
*
* For Support, Information, and Updates:
* https://community.hubitat.com/t/zooz-relays-advanced/98194
* https://github.com/jtp10181/Hubitat/tree/main/Drivers/zooz
*
Changelog:
## [1.0.2] - 2023-02-28 (@jtp10181)
- Added Association Group Support
- Fixed made some typing more lax to prevent errors
- Fixed lifeline association configuration for ZEN52
## [0.2.0] - 2021-08-22 (@jtp10181)
- Initial Release of ZEN52
- Minor fixes for ZEN51
* Copyright 2022 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 = "1.0.2"
@Field static final Map deviceModelNames = ["0104:0202":"ZEN52", "0904:0202":"ZEN52"]
metadata {
definition (
name: "Zooz ZEN52 Double Relay Advanced with CONTACT",
namespace: "jtp10181",
author: "Jeff Page (@jtp10181)",
importUrl: "https://raw.githubusercontent.com/jtp10181/Hubitat/main/Drivers/zooz/zooz-zen52-double-relay.groovy"
) {
capability "Actuator"
//capability "Switch"
capability "Configuration"
capability "Refresh"
capability "PushableButton"
capability "HoldableButton"
capability "ReleasableButton"
capability "DoubleTapableButton"
capability "Flash"
command "refreshParams"
//DEBUGGING
//command "debugShowVars"
attribute "S1", "string"
attribute "S2", "string"
attribute "syncStatus", "string"
fingerprint mfr:"027A", prod:"0104", deviceId:"0202", inClusters:"0x5E,0x55,0x9F,0x6C", deviceJoinName:"Zooz ZEN52 Double Relay"
fingerprint mfr:"027A", prod:"0104", deviceId:"0202", inClusters:"0x5E,0x55,0x9F,0x6C,0x25,0x70,0x85,0x59,0x8E,0x86,0x72,0x5A,0x73,0x7A,0x60,0x22,0x5B,0x87", deviceJoinName:"Zooz ZEN52 Double Relay"
fingerprint mfr:"027A", prod:"0904", deviceId:"0202", deviceJoinName:"Zooz ZEN52 Double Relay LR"
}
preferences {
configParams.each { param ->
if (!param.hidden) {
Integer paramVal = getParamValue(param)
if (param.options) {
input "configParam${param.num}", "enum",
title: fmtTitle("${param.title}"),
description: fmtDesc("• Parameter #${param.num}, Selected: ${paramVal}" + (param?.description ? "<br>• ${param?.description}" : '')),
defaultValue: paramVal,
options: param.options,
required: false
}
else if (param.range) {
input "configParam${param.num}", "number",
title: fmtTitle("${param.title}"),
description: fmtDesc("• Parameter #${param.num}, Range: ${(param.range).toString()}, DEFAULT: ${param.defaultVal}" + (param?.description ? "<br>• ${param?.description}" : '')),
defaultValue: paramVal,
range: param.range,
required: false
}
}
}
for(int i in 2..maxAssocGroups) {
input "assocDNI$i", "string",
title: fmtTitle("Device Associations - Group $i"),
description: fmtDesc("Supports up to ${maxAssocNodes} Hex Device IDs separated by commas. Check device documentation for more info. Save as blank or 0 to clear."),
required: false
}
input "childScene", "bool",
title: fmtTitle("Scene Events on Child"),
description: fmtDesc("Send scene events to child devices and allow up to 5x button push events. Child devices must be using Central Scene Switch driver for this to work.<br>" +
"If disabled scene events are limited to 2x and posted on the parent device as two buttons."),
defaultValue: false
//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>"
}
void debugShowVars() {
log.warn "paramsList ${paramsList.hashCode()} ${paramsList}"
log.warn "paramsMap ${paramsMap.hashCode()} ${paramsMap}"
log.warn "settings ${settings.hashCode()} ${settings}"
}
//Association Settings
@Field static final int maxAssocGroups = 3
@Field static final int maxAssocNodes = 5
//Main Parameters Listing
@Field static Map<String, Map> paramsMap =
[
ledIndicator: [ num: 2,
title: "LED Indicator (On when On)",
size: 1, defaultVal: 1,
options: [1:"LED Enabled", 0:"LED Disabled"],
],
offTimer: [ num: 3,
title: "Auto Turn-Off Timer (R1)",
size: 2, defaultVal: 0,
description: "0 = Disabled",
range: 0..65535,
],
onTimer: [ num: 4,
title: "Auto Turn-On Timer (R1)",
size: 2, defaultVal: 0,
description: "0 = Disabled",
range: 0..65535,
],
timerUnits: [ num: 7,
title: "Time Units (R1)",
size: 1, defaultVal: 1,
options: [1:"minutes",2:"seconds"],
],
offTimer2: [ num: 5,
title: "Auto Turn-Off Timer (R2)",
size: 2, defaultVal: 0,
description: "0 = Disabled",
range: 0..65535,
],
onTimer2: [ num: 6,
title: "Auto Turn-On Timer (R2)",
size: 2, defaultVal: 0,
description: "0 = Disabled",
range: 0..65535,
],
timerUnits2: [ num: 8,
title: "Time Units (R2)",
size: 1, defaultVal: 1,
options: [1:"minutes",2:"seconds"],
],
powerFailure: [ num: 14,
title: "Behavior After Power Failure (R1)",
size: 1, defaultVal: 2,
options: [2:"Restores Last Status", 0:"Forced to Off", 1:"Forced to On"],
],
powerFailure2: [ num: 15,
title: "Behavior After Power Failure (R2)",
size: 1, defaultVal: 2,
options: [2:"Restores Last Status", 0:"Forced to Off", 1:"Forced to On"],
],
sceneControl: [ num: 16,
title: "Scene Control Events",
description: "Enable to get push and multi-tap events (for momentary switches)",
size: 1, defaultVal: 0,
options: [0:"Disabled", 1:"Enabled"],
],
loadControl: [ num: 17,
title: "Smart Bulb Mode - Load Control (R1)",
description: "When control is disabled the relay will still report on/off states",
size: 1, defaultVal: 1,
options: [1:"Enable Button, Switch and Z-Wave", 0:"Disable Button/Switch Control", 2:"Disable Button, Switch and Z-Wave Control"],
],
loadControl2: [ num: 18,
title: "Smart Bulb Mode - Load Control (R2)",
description: "When control is disabled the relay will still report on/off states",
size: 1, defaultVal: 1,
options: [1:"Enable Button, Switch and Z-Wave", 0:"Disable Button/Switch Control", 2:"Disable Button, Switch and Z-Wave Control"],
],
switchType: [ num: 20,
title: "External Switch Type (R1)",
size: 1, defaultVal: 2,
options: [0:"Toggle Switch", 1:"Momentary Switch", 2:"On/Off Switch", 3:"3-way Impulse Control", 4:"Garage Door Mode"],
],
switchType2: [ num: 21,
title: "External Switch Type (R2)",
size: 1, defaultVal: 2,
options: [0:"Toggle Switch", 1:"Momentary Switch", 2:"On/Off Switch", 3:"3-way Impulse Control", 4:"Garage Door Mode"],
],
relayType: [ num: 25,
title: "Relay Type Behavior (R1)",
size: 1, defaultVal: 0,
options: [0:"NO: Relay Open when Off", 1:"NC: Relay Closed when Off"],
],
relayType2: [ num: 26,
title: "Relay Type Behavior (R2)",
size: 1, defaultVal: 0,
options: [0:"NO: Relay Open when Off", 1:"NC: Relay Closed when Off"],
],
impulseDuration: [ num: 22,
title: "Impulse Duration for 3-way (R1) [seconds]",
size: 1, defaultVal: 10,
range: 2..200,
],
impulseDuration2: [ num: 23,
title: "Impulse Duration for 3-way (R2) [seconds]",
size: 1, defaultVal: 10,
range: 2..200,
],
assocReports: [ num: 24,
title: "Association Reports",
size: 1, defaultVal: 1,
options: [0:"Binary for Z-Wave, Basic for Physical", 1:"Always Binary Reports"],
hidden: true
],
]
/* ZEN52
CommandClassReport - class:0x22, version:1
CommandClassReport - class:0x25, version:2
CommandClassReport - class:0x55, version:2
CommandClassReport - class:0x59, version:3
CommandClassReport - class:0x5A, version:1
CommandClassReport - class:0x5B, version:3
CommandClassReport - class:0x5E, version:2
CommandClassReport - class:0x60, version:4
CommandClassReport - class:0x6C, version:1
CommandClassReport - class:0x70, version:4
CommandClassReport - class:0x72, version:2
CommandClassReport - class:0x73, version:1
CommandClassReport - class:0x7A, version:5
CommandClassReport - class:0x85, version:2
CommandClassReport - class:0x86, version:3
CommandClassReport - class:0x87, version:3
CommandClassReport - class:0x8E, version:3
CommandClassReport - class:0x9F, version:1
*/
//Set Command Class Versions
@Field static final Map commandClassVersions = [
0x25: 1, // Switch Binary
0x5B: 3, // CentralScene
0x60: 3, // Multi Channel
0x6C: 1, // Supervision
0x70: 2, // Configuration
0x72: 2, // ManufacturerSpecific
0x85: 2, // Association
0x86: 2, // Version
0x8E: 3, // Multi Channel Association
]
/*** Static Lists and Settings ***/
@Field static final Map multiChan = [ZEN52:[endpoints:1..2]]
/*******************************************************************
***** Core Functions
********************************************************************/
void installed() {
logWarn "installed..."
createChildDevices()
initialize()
}
void initialize() {
logWarn "initialize..."
refresh()
}
void configure() {
logWarn "configure..."
if (debugEnable) runIn(1800, debugLogsOff)
createChildDevices()
if (!pendingChanges || state.resyncAll == null) {
logDebug "Enabling Full Re-Sync"
state.resyncAll = true
}
updateSyncingStatus(6)
runIn(1, executeRefreshCmds)
runIn(4, executeConfigureCmds)
}
void updated() {
logDebug "updated..."
logDebug "Debug logging is: ${debugEnable == true}"
logDebug "Description logging is: ${txtEnable == true}"
if (debugEnable) runIn(1800, debugLogsOff)
//Check to make sure childScene setting can work
childDevices.each { child ->
if (child.hasCapability("PushableButton")) {
child.sendEvent(name:"numberOfButtons", value:(childScene ? 5 : 0))
}
else {
child.deleteCurrentState("numberOfButtons")
if (childScene) {
logWarn "$child is missing PushableButton, turning off 'Scene Events to Child'"
device.updateSetting("childScene",[value:"false",type:"bool"])
childScene = false
}
}
}
//Configure for childScene setting
sendEvent(name:"numberOfButtons", value:(childScene ? 0 : 2))
if (childScene) {
logDebug "Removing unnecessary attributes"
device.deleteCurrentState("doubleTapped")
device.deleteCurrentState("held")
device.deleteCurrentState("pushed")
device.deleteCurrentState("released")
}
runIn(1, executeConfigureCmds)
}
void refresh() {
logDebug "refresh..."
executeRefreshCmds()
}
/*******************************************************************
***** Driver Commands
********************************************************************/
/*** Capabilities ***/
String on() {
logDebug "on..."
flashStop()
return getOnOffCmds(0xFF)
}
String off() {
logDebug "off..."
flashStop()
return getOnOffCmds(0x00)
}
//Button commands required with capabilities
void push(buttonId) { sendBasicButtonEvent(buttonId, "pushed") }
void hold(buttonId) { sendBasicButtonEvent(buttonId, "held") }
void release(buttonId) { sendBasicButtonEvent(buttonId, "released") }
void doubleTap(buttonId) { sendBasicButtonEvent(buttonId, "doubleTapped") }
//Flashing Capability
void flash(rateToFlash = 1500) {
logInfo "Flashing started with rate of ${rateToFlash}ms"
//Min rate of 1 sec, max of 30, max run time of 5 minutes
rateToFlash = validateRange(rateToFlash, 1500, 1000, 30000)
Integer maxRun = validateRange((rateToFlash*30)/1000, 30, 30, 300)
state.flashNext = device.currentValue("switch") ?: "on"
//Start the flashing
runIn(maxRun,flashStop,[data:true])
flashHandler(rateToFlash)
}
void flashStop(Boolean turnOn = false) {
if (state.flashNext != null) {
logInfo "Flashing stopped..."
unschedule("flashHandler")
state.remove("flashNext")
if (turnOn) { runIn(1,on) }
}
}
void flashHandler(Integer rateToFlash) {
if (state.flashNext == "on") {
logDebug "Flash On"
state.flashNext = "off"
runInMillis(rateToFlash, flashHandler, [data:rateToFlash])
sendCommands(getOnOffCmds(0xFF))
}
else if (state.flashNext == "off") {
logDebug "Flash Off"
state.flashNext = "on"
runInMillis(rateToFlash, flashHandler, [data:rateToFlash])
sendCommands(getOnOffCmds(0x00))
}
}
/*** Custom Commands ***/
void refreshParams() {
List<String> cmds = []
cmds << mcAssociationGetCmd(1)
for (int i = 1; i <= maxAssocGroups; i++) {
cmds << associationGetCmd(i)
}
configParams.each { param ->
cmds << configGetCmd(param)
}
if (cmds) sendCommands(cmds)
}
/*** Child Capabilities ***/
def componentOn(cd) {
logDebug "componentOn from ${cd.displayName} (${cd.deviceNetworkId})"
sendCommands(getOnOffCmds(0xFF, getChildEP(cd)))
}
def componentOff(cd) {
logDebug "componentOff from ${cd.displayName} (${cd.deviceNetworkId})"
sendCommands(getOnOffCmds(0x00, getChildEP(cd)))
}
def componentRefresh(cd) {
logDebug "componentRefresh from ${cd.displayName} (${cd.deviceNetworkId})"
sendCommands(getChildRefreshCmds(getChildEP(cd)))
}
/*******************************************************************
***** 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()
sendEvent(name:"numberOfButtons", value:(childScene ? 0 : 2))
}
//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}"
setDevModel(new BigDecimal(fullVersion))
}
void zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) {
logTrace "${cmd}"
updateSyncingStatus()
Map param = getParam(cmd.parameterNumber)
Integer val = cmd.scaledConfigurationValue
if (param) {
//Convert scaled signed integer to unsigned
Long sizeFactor = Math.pow(256,param.size).round()
if (val < 0) { val += sizeFactor }
logDebug "${param.name} (#${param.num}) = ${val.toString()}"
setParamStoredValue(param.num, val)
}
else {
logDebug "Parameter #${cmd.parameterNumber} = ${val.toString()}"
}
}
void zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) {
logTrace "${cmd}"
updateSyncingStatus()
Integer grp = cmd.groupingIdentifier
if (grp == 1) {
logDebug "Lifeline Association: ${cmd.nodeId}"
//state.group1Assoc = (cmd.nodeId == [zwaveHubNodeId]) ? true : false
}
else if (grp > 1 && grp <= maxAssocGroups) {
logDebug "Group $grp Association: ${cmd.nodeId}"
if (cmd.nodeId.size() > 0) {
state["assocNodes$grp"] = cmd.nodeId
} else {
state.remove("assocNodes$grp".toString())
}
String dnis = convertIntListToHexList(cmd.nodeId)?.join(", ")
//sendEventLog(name:"assocDNI$grp", value:(dnis ?: "none"))
device.updateSetting("assocDNI$grp", [value:"${dnis}", type:"string"])
}
else {
logDebug "Unhandled Group: $cmd"
}
}
void zwaveEvent(hubitat.zwave.commands.multichannelassociationv3.MultiChannelAssociationReport cmd) {
logTrace "${cmd}"
updateSyncingStatus()
if (cmd.groupingIdentifier == 1) {
logDebug "Lifeline Association: ${cmd.nodeId} | MC: ${cmd.multiChannelNodeIds}"
state.group1Assoc = (cmd.multiChannelNodeIds == [[nodeId:zwaveHubNodeId, bitAddress:0, endPointId:0]] ? true : false)
}
else {
logDebug "Unhandled Group: $cmd"
}
}
//To catch if parameter gets changed somehow
void zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, ep=0) {
logTrace "${cmd} (ep ${ep})"
flashStop() //Stop flashing if its running
sendSwitchEvents(cmd.value, "physical", ep)
}
void zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep=0) {
logTrace "${cmd} (ep ${ep})"
String type = (state."isDigital$ep" ? "digital" : "physical")
state.remove("isDigital$ep" as String)
if (ep == 0) type = ""
if (type == "physical") {
flashStop()
sendEvent(name:"S${ep}", value:(cmd.value ? "on" : "off"))
}
sendSwitchEvents(cmd.value, type, ep)
}
void zwaveEvent(hubitat.zwave.commands.centralscenev3.CentralSceneNotification cmd, ep=0){
if (state.lastSequenceNumber != cmd.sequenceNumber) {
state.lastSequenceNumber = cmd.sequenceNumber
logTrace "${cmd} (ep ${ep})"
Map scene = [name: "pushed", value: cmd.sceneNumber, desc: "", type:"physical", isStateChange:true]
String actionType
String btnVal
switch (cmd.sceneNumber) {
case 1:
case 2:
actionType = "S${cmd.sceneNumber}"
if (childScene) {
ep = cmd.sceneNumber
scene.value = 1
}
break
default:
logDebug "Unknown sceneNumber: ${cmd}"
}
switch (cmd.keyAttributes){
case 0:
btnVal = "${actionType} 1x"
break
case 1:
scene.name = "released"
btnVal = "${actionType} released"
break
case 2:
scene.name = "held"
btnVal = "${actionType} held"
break
case 3:
scene.name = "doubleTapped"
btnVal = "${actionType} 2x"
break
case {it >=4 && it <= 6 && childScene}:
scene.value = cmd.keyAttributes - 1
btnVal = "${actionType} ${cmd.keyAttributes - 1}x"
break
default:
logDebug "Unhandled keyAttributes: ${cmd}"
}
if (actionType && btnVal) {
scene.desc = "button ${scene.value} ${scene.name} [${btnVal}]"
sendEventLog(scene, ep)
}
}
}
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 mcAssociationGetCmd(Integer group) {
return secureCmd(zwave.multiChannelAssociationV3.multiChannelAssociationGet(groupingIdentifier: group))
}
String versionGetCmd() {
return secureCmd(zwave.versionV2.versionGet())
}
String switchBinarySetCmd(Integer value, Integer ep=0) {
return secureCmd(zwave.switchBinaryV1.switchBinarySet(switchValue: value), ep)
}
String switchBinaryGetCmd(Integer ep=0) {
return secureCmd(zwave.switchBinaryV1.switchBinaryGet(), ep)
}
String configSetCmd(Map param, Integer value) {
//Convert from unsigned to signed 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
********************************************************************/
void executeConfigureCmds() {
logDebug "executeConfigureCmds..."
List<String> cmds = []
if (!firmwareVersion || !state.deviceModel) {
cmds << versionGetCmd()
}
cmds += getConfigureAssocsCmds()
configParams.each { param ->
Integer paramVal = getParamValue(param, true)
Integer storedVal = getParamStoredValue(param.num)
if ((paramVal != null) && (state.resyncAll || (storedVal != paramVal))) {
logDebug "Changing ${param.name} (#${param.num}) from ${storedVal} to ${paramVal}"
cmds += configSetGetCmd(param, paramVal)
}
}
if (state.resyncAll) clearVariables()
state.resyncAll = false
if (cmds) sendCommands(cmds)
}
void executeRefreshCmds() {
List<String> cmds = []
if (state.resyncAll || !firmwareVersion || !state.deviceModel) {
cmds << versionGetCmd()
}
//Refresh Switch
cmds << switchBinaryGetCmd()
//Refresh Childs
multiChan[state.deviceModel]?.endpoints.each { endPoint ->
cmds += getChildRefreshCmds(endPoint)
}
sendCommands(cmds)
}
void clearVariables() {
logWarn "Clearing state variables and data..."
//Backup
String devModel = state.deviceModel
//Clears State Variables
state.clear()
//Clear Config Data
configsList["${device.id}"] = [:]
device.removeDataValue("configVals")
//Clear Data from other Drivers
device.removeDataValue("protocolVersion")
device.removeDataValue("hardwareVersion")
device.removeDataValue("zwaveAssociationG1")
device.removeDataValue("zwaveAssociationG2")
device.removeDataValue("zwaveAssociationG3")
//Restore
if (devModel) state.deviceModel = devModel
}
List getConfigureAssocsCmds() {
List<String> cmds = []
if (!state.group1Assoc || state.resyncAll) {
if (!state.group1Assoc) {
logDebug "Setting lifeline association..."
cmds << secureCmd(zwave.multiChannelAssociationV3.multiChannelAssociationRemove(groupingIdentifier: 1, nodeId:[], multiChannelNodeIds:[]))
cmds << secureCmd(zwave.multiChannelAssociationV3.multiChannelAssociationSet(groupingIdentifier: 1, multiChannelNodeIds: [[nodeId: zwaveHubNodeId, bitAddress:0, endPointId: 0]]))
}
cmds << mcAssociationGetCmd(1)
}
for (int i = 2; i <= maxAssocGroups; i++) {
List<String> cmdsEach = []
List settingNodeIds = getAssocDNIsSettingNodeIds(i)
//Need to remove first then add in case we are at limit
List oldNodeIds = state."assocNodes$i"?.findAll { !(it in settingNodeIds) }
if (oldNodeIds) {
logDebug "Removing Nodes: Group $i - $oldNodeIds"
cmdsEach << associationRemoveCmd(i, oldNodeIds)
}
List newNodeIds = settingNodeIds.findAll { !(it in state."assocNodes$i") }
if (newNodeIds) {
logDebug "Adding Nodes: Group $i - $newNodeIds"
cmdsEach << associationSetCmd(i, newNodeIds)
}
if (cmdsEach || state.resyncAll) {
cmdsEach << associationGetCmd(i)
cmds += cmdsEach
}
}
return cmds
}
List getAssocDNIsSettingNodeIds(grp) {
def dni = getAssocDNIsSetting(grp)
def nodeIds = convertHexListToIntList(dni.split(","))
if (dni && !nodeIds) {
logWarn "'${dni}' is not a valid value for the 'Device Associations - Group ${grp}' setting. All z-wave devices have a 2 character Device Network ID and if you're entering more than 1, use commas to separate them."
}
else if (nodeIds.size() > maxAssocNodes) {
logWarn "The 'Device Associations - Group ${grp}' setting contains more than ${maxAssocNodes} IDs so some (or all) may not get associated."
}
return nodeIds
}
// iOS app has no way of clearing string input so workaround is to have users enter 0.
String getAssocDNIsSetting(grp) {
def val = settings."assocDNI$grp"
return ((val && (val.trim() != "0")) ? val : "")
}
List getChildRefreshCmds(Integer endPoint) {
List<String> cmds = []
cmds << switchBinaryGetCmd(endPoint)
return cmds
}
Integer getPendingChanges() {
Integer configChanges = configParams.count { param ->
Integer paramVal = getParamValue(param, true)
((paramVal != null) && (paramVal != getParamStoredValue(param.num)))
}
Integer pendingAssocs = Math.ceil(getConfigureAssocsCmds()?.size()/2) ?: 0
return (!state.resyncAll ? (configChanges + pendingAssocs) : configChanges)
}
String getOnOffCmds(val, Integer endPoint=0) {
if (endPoint > 0) {
state."isDigital$endPoint" = true
} else {
multiChan[state.deviceModel]?.endpoints.each { state."isDigital$it" = true }
}
return switchBinarySetCmd(val ? 0xFF : 0x00, endPoint)
}
/*******************************************************************
***** 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 ?: ''}"
//Endpoint Events
if (ep) {
def childDev = getChildByEP(ep)
String logEp = "Switch ${ep} "
if (childDev) {
if (childDev.currentValue(evt.name).toString() != evt.value.toString() || evt.isStateChange) {
evt.descriptionText = "${childDev}: ${evt.descriptionText}"
childDev.parse([evt])
} else {
logDebug "${logEp}${evt.descriptionText} [NOT CHANGED]"
childDev.sendEvent(evt)
}
}
else {
log.error "No device for endpoint (${ep}). Press Configure to create child devices."
}
return
}
//Main Device Events
if (device.currentValue(evt.name).toString() != evt.value.toString() || evt.isStateChange) {
logInfo "${evt.descriptionText}"
} else {
logDebug "${evt.descriptionText} [NOT CHANGED]"
}
//Always send event to update last activity
sendEvent(evt)
}
void sendSwitchEvents(rawVal, String type, Integer ep=0) {
String value = (rawVal ? "on" : "off")
String desc = "switch is turned ${value}" + (type ? " (${type})" : "")
sendEventLog(name:"switch", value:value, type:type, desc:desc, ep)
}
void sendBasicButtonEvent(buttonId, String name) {
String desc = "button ${buttonId} ${name} (digital)"
sendEventLog(name:name, value:buttonId, type:"digital", desc:desc, isStateChange:true)
}
/*******************************************************************
***** 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
}
//Parameter List Functions
//This will rebuild the list for the current model and firmware only as needed
//paramsList Structure: MODEL:[FIRMWARE:PARAM_MAPS]
//PARAM_MAPS [num, name, title, description, size, defaultVal, options, firmVer]
@Field static Map<String, Map<String, List>> paramsList = new java.util.concurrent.ConcurrentHashMap()
void updateParamsList() {
logDebug "Update Params List"
String devModel = state.deviceModel
Short modelNum = deviceModelShort
Short modelSeries = Math.floor(modelNum/10)
BigDecimal firmware = firmwareVersion
List<Map> tmpList = []
paramsMap.each { name, pMap ->
Map tmpMap = pMap.clone()
tmpMap.options = tmpMap.options?.clone()
//Save the name
tmpMap.name = name
//Apply custom adjustments
tmpMap.changes.each { m, changes ->
if (m == devModel || m == modelNum || m ==~ /${modelSeries}X/) {
tmpMap.putAll(changes)
if (changes.options) { tmpMap.options = changes.options.clone() }
}
}
//Don't need this anymore
tmpMap.remove("changes")
//Set DEFAULT tag on the default
tmpMap.options.each { k, val ->
if (k == tmpMap.defaultVal) {
tmpMap.options[(k)] = "${val} [DEFAULT]"
}
}
//Save to the temp list
tmpList << tmpMap
}
//Remove invalid or not supported by firmware
tmpList.removeAll { it.num == null }
tmpList.removeAll { firmware < (it.firmVer ?: 0) }
tmpList.removeAll {
if (it.firmVerM) {
(firmware-(int)firmware)*100 < it.firmVerM[(int)firmware]
}
}
//Save it to the static list
if (paramsList[devModel] == null) paramsList[devModel] = [:]
paramsList[devModel][firmware] = tmpList
}
//Verify the list and build if its not populated
void verifyParamsList() {
String devModel = state.deviceModel
BigDecimal firmware = firmwareVersion
if (!paramsMap.settings?.fixed) fixParamsMap()
if (paramsList[devModel] == null) updateParamsList()
if (paramsList[devModel][firmware] == null) updateParamsList()
}
//These have to be added in after the fact or groovy complains
void fixParamsMap() {
paramsMap['settings'] = [fixed: true]
}
//Gets full list of params
List<Map> getConfigParams() {
//logDebug "Get Config Params"
if (!device) return []
String devModel = state.deviceModel
BigDecimal firmware = firmwareVersion
//Try to get device model if not set
if (devModel) { verifyParamsList() }
else { runInMillis(200, setDevModel) }
//Bail out if unknown device
if (!devModel || devModel == "UNK00") return []
return paramsList[devModel][firmware]
}
//Get a single param by name or number
Map getParam(search) {
//logDebug "Get Param (${search} | ${search.class})"
Map param = [:]
verifyParamsList()
if (search instanceof String) {
param = configParams.find{ it.name == search }
} else {
param = configParams.find{ it.num == search }
}
return param
}
//Convert Param Value if Needed
Integer getParamValue(String paramName) {
return getParamValue(getParam(paramName))
}
Number getParamValue(Map param, Boolean adjust=false) {
if (param == null) return
Number paramVal = safeToInt(settings."configParam${param.num}", param.defaultVal)
if (!adjust) return paramVal
//Reset hidden parameters to default
if (param.hidden && settings."configParam${param.num}" != null) {
logWarn "Resetting hidden parameter ${param.name} (${param.num}) to default ${param.defaultVal}"
device.removeSetting("configParam${param.num}")
paramVal = param.defaultVal
}
return paramVal
}
/*** Child Helper Functions ***/
void createChildDevices() {
multiChan[state.deviceModel]?.endpoints.each { endPoint ->
if (!getChildByEP(endPoint)) {
addChild(endPoint)
}
}
}
void addChild(endPoint) {
Map deviceType = [namespace:"hubitat", typeName:"Generic Component Central Scene Switch"]
Map deviceTypeBak = [namespace:"hubitat", typeName:"Generic Component Switch"]
String dni = getChildDNI(endPoint)
Map properties = [name: "${device.name} - Switch ${endPoint}", isComponent: false]
def childDev
logDebug "Creating 'Endpoint ${endPoint}' Child Device"
try {
childDev = addChildDevice(deviceType.namespace, deviceType.typeName, dni, properties)
}
catch (e) {
logWarn "The '${deviceType}' driver failed"
if (deviceTypeBak) {
logWarn "Defaulting to '${deviceTypeBak}' instead"
childDev = addChildDevice(deviceTypeBak.namespace, deviceTypeBak.typeName, dni, properties)
}
}
if (childDev) {
childDev.updateDataValue("endPoint","$endPoint")
childDev.sendEvent(name:"numberOfButtons", value:(childScene ? 5 : 0))
}
}
private getChildByEP(endPoint) {
def dni = getChildDNI(endPoint)
return getChildByDNI(dni)
}
private getChildByDNI(dni) {
return childDevices?.find { it.deviceNetworkId == dni }
}
private getChildEP(childDev) {
Integer endPoint = safeToInt(childDev.getDataValue("endPoint"))
if (!endPoint) {
logDebug "Finding endPoint for $childDev"
String[] dni = childDev.deviceNetworkId.split('-')
endPoint = safeToInt(dni[1])
if (endPoint) {
childDev.updateDataValue("endPoint","$endPoint")
} else {
logWarn "Cannot determine endPoint number for $childDev, defaulting to 0"
}
}
return endPoint
}
String getChildDNI(endPoint) {
return "${device.deviceId}-${endPoint}"
}
/*** Other Helper Functions ***/
void updateSyncingStatus(Integer delay=2) {
runIn(delay, refreshSyncStatus)
sendEvent(name:"syncStatus", value:"Syncing...")
}
void refreshSyncStatus() {
Integer changes = pendingChanges
sendEvent(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())
}
}
//Stash the model in a state variable
String setDevModel(BigDecimal firmware) {
if (!device) return
def devTypeId = convertIntListToHexList([safeToInt(device.getDataValue("deviceType")),safeToInt(device.getDataValue("deviceId"))],4)
String devModel = deviceModelNames[devTypeId.join(":")] ?: "UNK00"
if (!firmware) { firmware = firmwareVersion }
state.deviceModel = devModel
device.updateDataValue("deviceModel", devModel)
logDebug "Set Device Info - Model: ${devModel} | Firmware: ${firmware}"
if (devModel == "UNK00") {
logWarn "Unsupported Device USE AT YOUR OWN RISK: ${devTypeId}"
state.WARNING = "Unsupported Device Model - USE AT YOUR OWN RISK!"
}
else state.remove("WARNING")
//Setup parameters if not set
verifyParamsList()
return devModel
}
Integer getDeviceModelShort() {
return safeToInt(state.deviceModel?.drop(3))
}
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
}
List convertHexListToIntList(String[] hexList) {
def intList = []
hexList?.each {
try {
it = it.trim()
intList.add(Integer.parseInt(it, 16))
}
catch (e) { }
}
return intList
}
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
********************************************************************/
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