Skip to content

Instantly share code, notes, and snippets.

@jtp10181
Last active April 18, 2024 20:39
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/a4ba1225e4c9d9144a0a3903f990dca6 to your computer and use it in GitHub Desktop.
Save jtp10181/a4ba1225e4c9d9144a0a3903f990dca6 to your computer and use it in GitHub Desktop.
FIBARO 6-Button Keyfob
/*
* FIBARO 6-Button Keyfob
* - Model: FGKF-601
*
* For Support, Information, and Updates:
* https://community.hubitat.com/u/jtp10181/
* https://github.com/jtp10181/Hubitat
*
Changelog:
## [0.2.0] - 2024-04-14 (@jtp10181)
- Added settings for Lock Mode
- Updated Library and common code
## [0.1.0] - 2023-12-08 (@jtp10181)
- Initial Release
* Copyright 2023-2024 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.2.0"
@Field static final String DRIVER = "FGKF-601"
@Field static final String COMM_LINK = "https://community.hubitat.com/t/fibaro-6-key-fob-button/129527/14"
@Field static final Map deviceModelNames = ["010F:1001:2000":"FGKF-601"]
metadata {
definition (
name: "FIBARO 6-Button Keyfob",
namespace: "jtp10181",
author: "Jeff Page (@jtp10181)",
importUrl: ""
) {
capability "Actuator"
capability "Battery"
capability "PushableButton"
capability "HoldableButton"
capability "ReleasableButton"
capability "DoubleTapableButton"
command "fullConfigure"
command "forceRefresh"
// command "setParameter",[[name:"parameterNumber*",type:"NUMBER", description:"Parameter Number"],
// [name:"value*",type:"NUMBER", description:"Parameter Value"],
// [name:"size",type:"NUMBER", description:"Parameter Size"]]
//DEBUGGING
//command "debugShowVars"
attribute "syncStatus", "string"
fingerprint mfr:"010F", prod:"1001", deviceId:"2000", inClusters:"0x00,0x00" //Fibaro FGKF-601
}
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
}
}
}
}
}
void debugShowVars() {
log.warn "settings ${settings.hashCode()} ${settings}"
log.warn "paramsList ${paramsList.hashCode()} ${paramsList}"
log.warn "paramsMap ${paramsMap.hashCode()} ${paramsMap}"
}
//Association Settings
@Field static final int maxAssocGroups = 1
@Field static final int maxAssocNodes = 1
/*** Static Lists and Settings ***/
//None
//Main Parameters Listing
@Field static Map<String, Map> paramsMap =
[
unlockSeq: [ num: 1,
title: "Unlocking Sequence",
description: "Check the manual for how to program sequences",
size: 2, defaultVal: 0,
range: 0..28086
],
lockTimeout: [ num: 2,
title: "Time to Auto-Lock / Lock Button",
description: "0 = Disabled, 5-255 = Time in seconds. Check the online manual for lock button calculator (optional).",
size: 2, defaultVal: 60,
range: 0..1791
],
scene1: [ num: 3,
title: "Scene Sequence 1 (Button 7)",
description: "Check the manual for how to program sequences",
size: 2, defaultVal: 0,
range: 0..28086
],
scene2: [ num: 4,
title: "Scene Sequence 2 (Button 8)",
description: "Check the manual for how to program sequences",
size: 2, defaultVal: 0,
range: 0..28086
],
scene3: [ num: 5,
title: "Scene Sequence 3 (Button 9)",
description: "Check the manual for how to program sequences",
size: 2, defaultVal: 0,
range: 0..28086
],
scene4: [ num: 6,
title: "Scene Sequence 4 (Button 10)",
description: "Check the manual for how to program sequences",
size: 2, defaultVal: 0,
range: 0..28086
],
scene5: [ num: 7,
title: "Scene Sequence 5 (Button 11)",
description: "Check the manual for how to program sequences",
size: 2, defaultVal: 0,
range: 0..28086
],
scene6: [ num: 8,
title: "Scene Sequence 6 (Button 12)",
description: "Check the manual for how to program sequences",
size: 2, defaultVal: 0,
range: 0..28086
],
seqTimeout: [ num: 9,
title: "Sequences Timeout (10 = 1s)",
description: "Time from last click of a button to check if the sequence is valid",
size: 1, defaultVal: 10,
range: 5..30
],
sceneAct1: [ num: 21,
title: "Scene activation for ( ▢ )",
description: "Enabling multi-taps will add a slight delay to single tap recognition",
size: 1, defaultVal: 9,
options: [9:"Press / Hold Only", 11:"Up to Double Tap", 15:"Up to Triple Tap"]
],
sceneAct2: [ num: 22,
title: "Scene activation for ( ◯ )",
description: "Enabling multi-taps will add a slight delay to single tap recognition",
size: 1, defaultVal: 9,
options: [9:"Press / Hold Only", 11:"Up to Double Tap", 15:"Up to Triple Tap"]
],
sceneAct3: [ num: 23,
title: "Scene activation for ( X )",
description: "Enabling multi-taps will add a slight delay to single tap recognition",
size: 1, defaultVal: 9,
options: [9:"Press / Hold Only", 11:"Up to Double Tap", 15:"Up to Triple Tap"]
],
sceneAct4: [ num: 24,
title: "Scene activation for ( △ )",
description: "Enabling multi-taps will add a slight delay to single tap recognition",
size: 1, defaultVal: 9,
options: [9:"Press / Hold Only", 11:"Up to Double Tap", 15:"Up to Triple Tap"]
],
sceneAct5: [ num: 25,
title: "Scene activation for ( ‒ )",
description: "Enabling multi-taps will add a slight delay to single tap recognition",
size: 1, defaultVal: 9,
options: [9:"Press / Hold Only", 11:"Up to Double Tap", 15:"Up to Triple Tap"]
],
sceneAct6: [ num: 26,
title: "Scene activation for ( + )",
description: "Enabling multi-taps will add a slight delay to single tap recognition",
size: 1, defaultVal: 9,
options: [9:"Press / Hold Only", 11:"Up to Double Tap", 15:"Up to Triple Tap"]
],
]
//Set Command Class Versions
@Field static final Map commandClassVersions = [
0x5B: 3, // centralScene
0x6C: 1, // supervision
0x70: 1, // configuration
0x71: 8, // notification
0x85: 2, // association
0x86: 2, // version
0x8E: 3, // multiChannelAssociation
]
/*******************************************************************
***** Core Functions
********************************************************************/
void installed() {
logWarn "installed..."
}
void fullConfigure() {
logWarn "configure..."
if (!pendingChanges || state.resyncAll == null) {
logForceWakeupMessage "Full Re-Configure"
state.resyncAll = true
} else {
logForceWakeupMessage "Pending Configuration Changes"
}
updateSyncingStatus(1)
}
void updated() {
logDebug "updated..."
checkLogLevel()
if (!firmwareVersion || !state.deviceModel) {
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")
}
state.protection = (getParamValue("unlockSeq") == 0 || getParamValue("lockTimeout") == 0) ? 0 : 1
updateSyncingStatus(1)
}
void forceRefresh() {
logDebug "refresh..."
state.pendingRefresh = true
logForceWakeupMessage "State Info Refresh"
}
/*******************************************************************
***** Driver Commands
********************************************************************/
/*** Capabilities ***/
//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") }
/*** Custom Commands ***/
String setParameter(paramNum, value, size = null) {
Map param = getParam(paramNum)
if (param && !size) { size = param.size }
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 )" + (param ? " [${param.name}]" : "")
return secureCmd(configSetCmd([num: paramNum, size: size], value as Integer))
}
/*******************************************************************
***** Z-Wave Reports
********************************************************************/
void parse(String description) {
zwaveParse(description)
sendEvent(name:"numberOfButtons", value:18)
}
void zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd, ep=0) {
zwaveSupervision(cmd,ep)
}
void zwaveEvent(hubitat.zwave.commands.configurationv1.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.title} (#${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 {
logDebug "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 = ["delay 0"]
cmds << batteryGetCmd()
//Set Protection State
cmds << secureCmd(zwave.protectionV1.protectionSet(protectionState: (state.protection ?: 0)))
//Refresh all if requested
if (state.pendingRefresh) { cmds += getRefreshCmds() }
//Any configuration needed
cmds += getConfigureCmds()
//This needs a longer delay
cmds << "delay 1400" << wakeUpNoMoreInfoCmd()
//Clear pending status
state.resyncAll = false
state.pendingRefresh = false
state.remove("INFO")
sendCommands(cmds,300)
}
void zwaveEvent(hubitat.zwave.commands.centralscenev3.CentralSceneNotification cmd, ep=0) {
logTrace "${cmd} (ep ${ep})"
Integer physicalButtons = 6
Integer btnBaseNum = cmd.sceneNumber ?: 0
Map sceneEvt = [name: "", value: btnBaseNum, desc: "", type:"physical", isStateChange:true]
cmd.keyAttributes = cmd.keyAttributes as Integer
String btnDesc = ""
if (cmd.keyAttributes == 3) sceneEvt.name = "doubleTapped"
else if (cmd.keyAttributes == 2) sceneEvt.name = "held"
else if (cmd.keyAttributes == 1) sceneEvt.name = "released"
else sceneEvt.name = "pushed"
if (cmd.keyAttributes > 3) {
//Adjust button number
btnDesc = " [button ${btnBaseNum} pushed ${cmd.keyAttributes - 1}x]"
sceneEvt.value = btnBaseNum + (physicalButtons * (cmd.keyAttributes - 2))
}
//Sequences extra description text
else if (btnBaseNum > 6) {
btnDesc = " [sequence ${btnBaseNum-6}]"
}
if (btnBaseNum) {
sceneEvt.desc = "button ${sceneEvt.value} ${sceneEvt.name}${btnDesc}"
sendEventLog(sceneEvt)
}
}
void zwaveEvent(hubitat.zwave.commands.notificationv8.NotificationReport cmd, ep=0) {
logTrace "${cmd} (ep ${ep})"
switch (cmd.notificationType) {
default:
logDebug "Unhandled: ${cmd}"
}
}
/*******************************************************************
***** 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 ?: ''}".trim()
//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 sendBasicButtonEvent(buttonId, String name) {
String desc = "button ${buttonId} ${name} (digital)"
sendEventLog(name:name, value:buttonId, type:"digital", desc:desc, isStateChange:true)
}
/*******************************************************************
***** Execute / Build Commands
********************************************************************/
List<String> getConfigureCmds() {
logDebug "getConfigureCmds..."
List<String> cmds = []
if (state.resyncAll) {
clearVariables()
cmds << wakeUpIntervalSetCmd(43200)
cmds << wakeUpIntervalGetCmd()
}
if (state.resyncAll || !firmwareVersion || !state.deviceModel) {
cmds << versionGetCmd()
}
cmds += getConfigureAssocsCmds()
configParams.each { param ->
Integer paramVal = getParamValueAdj(param)
Integer storedVal = getParamStoredValue(param.num)
if ((paramVal != null) && (state.resyncAll || (storedVal != paramVal))) {
logDebug "Changing ${param.name} - ${param.title} (#${param.num}) from ${storedVal} to ${paramVal}"
cmds += configSetGetCmd(param, paramVal)
}
}
state.resyncAll = false
if (cmds) updateSyncingStatus(6)
return cmds ?: []
}
List<String> getRefreshCmds() {
List<String> cmds = []
cmds << versionGetCmd()
cmds << wakeUpIntervalGetCmd()
cmds << batteryGetCmd()
return cmds ?: []
}
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
}
private logForceWakeupMessage(msg) {
String helpText = "Check the manual for how to wake the device."
if (state.deviceModel == "FGKF-601") { helpText = "Click O and + simultaneously to wake the device." }
logWarn "${msg} will execute the next time the device wakes up. ${helpText}"
state.INFO = "*** ${msg} *** Waiting for device to wake up. ${helpText}"
}
/*******************************************************************
***** Required for Library
********************************************************************/
//These have to be added in after the fact or groovy complains
void fixParamsMap() {
paramsMap['settings'] = [fixed: true]
}
Integer getParamValueAdj(Map param) {
return getParamValue(param)
}
//#include jtp10181.zwaveDriverLibrary
/*******************************************************************
*******************************************************************
***** Z-Wave Driver Library by Jeff Page (@jtp10181)
*******************************************************************
********************************************************************
Changelog:
2023-05-10 - First version used in drivers
2023-05-12 - Adjustments to community links
2023-05-14 - Updates for power metering
2023-05-18 - Adding requirement for getParamValueAdj in driver
2023-05-24 - Fix for possible RuntimeException error due to bad cron string
2023-10-25 - Less saving to the configVals data, and some new functions
2023-10-26 - Added some battery shortcut functions
2023-11-08 - Added ability to adjust settings on firmware range
2024-01-28 - Adjusted logging settings for new / upgrade installs, added mfgSpecificReport
********************************************************************/
library (
author: "Jeff Page (@jtp10181)",
category: "zwave",
description: "Z-Wave Driver Library",
name: "zwaveDriverLibrary",
namespace: "jtp10181",
documentationLink: ""
)
/*******************************************************************
***** Z-Wave Reports (COMMON)
********************************************************************/
//Include these in Driver
//void parse(String description) {zwaveParse(description)}
//void zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {zwaveMultiChannel(cmd)}
//void zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd, ep=0) {zwaveSupervision(cmd,ep)}
void zwaveParse(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()
}
//Decodes Multichannel Encapsulated Commands
void zwaveMultiChannel(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
hubitat.zwave.Command 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 zwaveSupervision(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd, ep=0) {
hubitat.zwave.Command 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)
String zwaveVersion = String.format("%d.%02d",cmd.zWaveProtocolVersion,cmd.zWaveProtocolSubVersion)
device.updateDataValue("firmwareVersion", fullVersion)
device.updateDataValue("protocolVersion", zwaveVersion)
device.updateDataValue("hardwareVersion", "${cmd.hardwareVersion}")
logDebug "Received Version Report - Firmware: ${fullVersion}"
setDevModel(new BigDecimal(fullVersion))
}
void zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
logTrace "${cmd}"
device.updateDataValue("manufacturer",cmd.manufacturerId.toString())
device.updateDataValue("deviceType",cmd.productTypeId.toString())
device.updateDataValue("deviceId",cmd.productId.toString())
logDebug "fingerprint mfr:\"${hubitat.helper.HexUtils.integerToHexString(cmd.manufacturerId, 2)}\", "+
"prod:\"${hubitat.helper.HexUtils.integerToHexString(cmd.productTypeId, 2)}\", "+
"deviceId:\"${hubitat.helper.HexUtils.integerToHexString(cmd.productId, 2)}\", "+
"inClusters:\"${device.getDataValue("inClusters")}\""+
(device.getDataValue("secureInClusters") ? ", secureInClusters:\"${device.getDataValue("secureInClusters")}\"" : "")
}
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 mfgSpecificGetCmd() {
return secureCmd(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
}
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 switchMultilevelSetCmd(Integer value, Integer duration, Integer ep=0) {
return secureCmd(zwave.switchMultilevelV4.switchMultilevelSet(dimmingDuration: duration, value: value), ep)
}
String switchMultilevelGetCmd(Integer ep=0) {
return secureCmd(zwave.switchMultilevelV4.switchMultilevelGet(), ep)
}
String switchMultilevelStartLvChCmd(Boolean upDown, Integer duration, Integer ep=0) {
//upDown: false=up, true=down
return secureCmd(zwave.switchMultilevelV4.switchMultilevelStartLevelChange(upDown: upDown, ignoreStartLevel:1, dimmingDuration: duration), ep)
}
String switchMultilevelStopLvChCmd(Integer ep=0) {
return secureCmd(zwave.switchMultilevelV4.switchMultilevelStopLevelChange(), ep)
}
String meterGetCmd(meter, Integer ep=0) {
return secureCmd(zwave.meterV3.meterGet(scale: meter.scale), ep)
}
String meterResetCmd(Integer ep=0) {
return secureCmd(zwave.meterV3.meterReset(), ep)
}
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 sensorMultilevelGetCmd(sensorType) {
Integer scale = (temperatureScale == "F" ? 1 : 0)
return secureCmd(zwave.sensorMultilevelV11.sensorMultilevelGet(scale: scale, sensorType: sensorType))
}
String notificationGetCmd(notificationType, eventType, Integer ep=0) {
return secureCmd(zwave.notificationV3.notificationGet(notificationType: notificationType, v1AlarmType:0, event: eventType), 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.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: value))
}
String configGetCmd(Map param) {
return secureCmd(zwave.configurationV1.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()
}
/*******************************************************************
***** 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
Map 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() {
TreeMap 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() }
}
}
tmpMap.changesFR.each { m, changes ->
if (firmware >= m.getFrom() && firmware <= m.getTo()) {
tmpMap.putAll(changes)
if (changes.options) { tmpMap.options = changes.options.clone() }
}
}
//Don't need this anymore
tmpMap.remove("changes")
tmpMap.remove("changesFR")
//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()
}
//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(String search) {
verifyParamsList()
return configParams.find{ it.name == search }
}
Map getParam(Number search) {
verifyParamsList()
return configParams.find{ it.num == search }
}
//Convert Param Value if Needed
BigDecimal getParamValue(String paramName) {
return getParamValue(getParam(paramName))
}
BigDecimal getParamValue(Map param) {
if (param == null) return
BigDecimal paramVal = safeToDec(settings."configParam${param.num}", param.defaultVal)
//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
}
/*** Preference Helpers ***/
String fmtTitle(String str) {
return "<strong>${str}</strong>"
}
String fmtDesc(String str) {
return "<div style='font-size: 85%; font-style: italic; padding: 1px 0px 4px 2px;'>${str}</div>"
}
String fmtHelpInfo(String str) {
String info = "${DRIVER} v${VERSION}"
String prefLink = "<a href='${COMM_LINK}' target='_blank'>${str}<br><div style='font-size: 70%;'>${info}</div></a>"
String topStyle = "style='font-size: 18px; padding: 1px 12px; border: 2px solid Crimson; border-radius: 6px;'" //SlateGray
String topLink = "<a ${topStyle} href='${COMM_LINK}' target='_blank'>${str}<br><div style='font-size: 14px;'>${info}</div></a>"
return "<div style='font-size: 160%; font-style: bold; padding: 2px 0px; text-align: center;'>${prefLink}</div>" +
"<div style='text-align: center; position: absolute; top: 46px; right: 60px; padding: 0px;'><ul class='nav'><li>${topLink}</ul></li></div>"
}
private getTimeOptionsRange(String name, Integer multiplier, List range) {
return range.collectEntries{ [(it*multiplier): "${it} ${name}${it == 1 ? '' : 's'}"] }
}
/*** 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"))
device.updateDataValue("configVals", getParamStoredMap()?.inspect())
}
void updateLastCheckIn() {
def nowDate = new Date()
state.lastCheckInDate = convertToLocalTimeString(nowDate)
Long lastExecuted = state.lastCheckInTime ?: 0
Long allowedMil = 24 * 60 * 60 * 1000 //24 Hours
if (lastExecuted + allowedMil <= nowDate.time) {
state.lastCheckInTime = nowDate.time
// if (lastExecuted) runIn(4, doCheckIn)
// scheduleCheckIn()
}
}
// void scheduleCheckIn() {
// def cal = Calendar.getInstance()
// cal.add(Calendar.MINUTE, -1)
// Integer hour = cal[Calendar.HOUR_OF_DAY]
// Integer minute = cal[Calendar.MINUTE]
// schedule( "0 ${minute} ${hour} * * ?", doCheckIn)
// }
// void doCheckIn() {
// String devModel = (state.deviceModel ?: "NA") + (state.subModel ? ".${state.subModel}" : "")
// String checkUri = "http://jtp10181.gateway.scarf.sh/${DRIVER}/chk-${devModel}-v${VERSION}"
// try {
// httpGet(uri:checkUri, timeout:4) { logDebug "Driver ${DRIVER} ${devModel} v${VERSION}" }
// state.lastCheckInTime = (new Date()).time
// } catch (Exception e) { }
// }
Integer getPendingChanges() {
Integer configChanges = configParams.count { param ->
Integer paramVal = getParamValueAdj(param)
((paramVal != null) && (paramVal != getParamStoredValue(param.num)))
}
Integer pendingAssocs = Math.ceil(getConfigureAssocsCmds()?.size()/2) ?: 0
return (!state.resyncAll ? (configChanges + pendingAssocs) : configChanges)
}
//iOS app has no way of clearing string input so workaround is to have users enter 0.
String getAssocDNIsSetting(grp) {
String val = settings."assocDNI$grp"
return ((val && (val.trim() != "0")) ? val : "")
}
List getAssocDNIsSettingNodeIds(grp) {
String dni = getAssocDNIsSetting(grp)
List 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
}
//Used with configure to reset variables
void clearVariables() {
logWarn "Clearing state variables and data..."
//Backup
String devModel = state.deviceModel
def engTime = state.energyTime
//Clears State Variables
state.clear()
//Clear Config Data
configsList["${device.id}"] = [:]
device.removeDataValue("configVals")
//Clear Data from other Drivers
device.removeDataValue("zwaveAssociationG1")
device.removeDataValue("zwaveAssociationG2")
device.removeDataValue("zwaveAssociationG3")
//Restore
if (devModel) state.deviceModel = devModel
if (engTime) state.energyTime = engTime
state.resyncAll = true
}
//Stash the model in a state variable
String setDevModel(BigDecimal firmware) {
if (!device) return
def devTypeId = convertIntListToHexList([safeToInt(device.getDataValue("manufacturer")),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 convertLevel(level, userLevel=false) {
if (levelCorrection) {
Integer brightmax = getParamValue("maximumBrightness")
Integer brightmin = getParamValue("minimumBrightness")
brightmax = (brightmax == 99) ? 100 : brightmax
brightmin = (brightmin == 1) ? 0 : brightmin
if (userLevel) {
//This converts what the user selected into a physical level within the min/max range
level = ((brightmax-brightmin) * (level/100)) + brightmin
state.levelActual = level
level = validateRange(Math.round(level), brightmax, brightmin, brightmax)
}
else {
//This takes the true physical level and converts to what we want to show to the user
if (Math.round(state.levelActual ?: 0) == level) level = state.levelActual
else state.levelActual = level
level = ((level - brightmin) / (brightmax - brightmin)) * 100
level = validateRange(Math.round(level), 100, 1, 100)
}
}
else if (state.levelActual) {
state.remove("levelActual")
}
return level
}
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(Long lastExecuted, Long allowedMil) {
!lastExecuted ? false : (lastExecuted + allowedMil > new Date().time)
}
/*******************************************************************
***** Logging Functions
********************************************************************/
//Logging Level Options
@Field static final Map LOG_LEVELS = [0:"Error", 1:"Warn", 2:"Info", 3:"Debug", 4:"Trace"]
@Field static final Map LOG_TIMES = [0:"Indefinitely", 30:"30 Minutes", 60:"1 Hour", 120:"2 Hours", 180:"3 Hours", 360:"6 Hours", 720:"12 Hours", 1440:"24 Hours"]
/*//Command to set log level, OPTIONAL. Can be copied to driver or uncommented here
command "setLogLevel", [ [name:"Select Level*", description:"Log this type of message and above", type: "ENUM", constraints: LOG_LEVELS],
[name:"Debug/Trace Time", description:"Timer for Debug/Trace logging", type: "ENUM", constraints: LOG_TIMES] ]
*/
//Additional Preferences
preferences {
//Logging Options
input name: "logLevel", type: "enum", title: fmtTitle("Logging Level"),
description: fmtDesc("Logs selected level and above"), defaultValue: 3, options: LOG_LEVELS
input name: "logLevelTime", type: "enum", title: fmtTitle("Logging Level Time"),
description: fmtDesc("Time to enable Debug/Trace logging"),defaultValue: 30, options: LOG_TIMES
//Help Link
input name: "helpInfo", type: "hidden", title: fmtHelpInfo("Community Link")
}
//Call this function from within updated() and configure() with no parameters: checkLogLevel()
void checkLogLevel(Map levelInfo = [level:null, time:null]) {
unschedule(logsOff)
//Set Defaults
if (settings.logLevel == null) {
device.updateSetting("logLevel",[value:"3", type:"enum"])
levelInfo.level = 3
}
if (settings.logLevelTime == null) {
device.updateSetting("logLevelTime",[value:"30", type:"enum"])
levelInfo.time = 30
}
//Schedule turn off and log as needed
if (levelInfo.level == null) levelInfo = getLogLevelInfo()
String logMsg = "Logging Level is: ${LOG_LEVELS[levelInfo.level]} (${levelInfo.level})"
if (levelInfo.level >= 3 && levelInfo.time > 0) {
logMsg += " for ${LOG_TIMES[levelInfo.time]}"
runIn(60*levelInfo.time, logsOff)
}
logInfo(logMsg)
//Store last level below Debug
if (levelInfo.level <= 2) state.lastLogLevel = levelInfo.level
}
//Function for optional command
void setLogLevel(String levelName, String timeName=null) {
Integer level = LOG_LEVELS.find{ levelName.equalsIgnoreCase(it.value) }.key
Integer time = LOG_TIMES.find{ timeName.equalsIgnoreCase(it.value) }.key
device.updateSetting("logLevel",[value:"${level}", type:"enum"])
checkLogLevel(level: level, time: time)
}
Map getLogLevelInfo() {
Integer level = settings.logLevel != null ? settings.logLevel as Integer : 1
Integer time = settings.logLevelTime != null ? settings.logLevelTime as Integer : 30
return [level: level, time: time]
}
//Legacy Support
void debugLogsOff() {
device.removeSetting("logEnable")
device.updateSetting("debugEnable",[value:false, type:"bool"])
}
//Current Support
void logsOff() {
logWarn "Debug and Trace logging disabled..."
if (logLevelInfo.level >= 3) {
Integer lastLvl = state.lastLogLevel != null ? state.lastLogLevel as Integer : 2
device.updateSetting("logLevel",[value:lastLvl.toString(), type:"enum"])
logWarn "Logging Level is: ${LOG_LEVELS[lastLvl]} (${lastLvl})"
}
}
//Logging Functions
void logErr(String msg) {
log.error "${device.displayName}: ${msg}"
}
void logWarn(String msg) {
if (logLevelInfo.level>=1) log.warn "${device.displayName}: ${msg}"
}
void logInfo(String msg) {
if (logLevelInfo.level>=2) log.info "${device.displayName}: ${msg}"
}
void logDebug(String msg) {
if (logLevelInfo.level>=3) log.debug "${device.displayName}: ${msg}"
}
void logTrace(String msg) {
if (logLevelInfo.level>=4) log.trace "${device.displayName}: ${msg}"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment