Last active
March 5, 2023 17:15
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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