/*
* Securifi KeyFob
*
* Description:
* This Hubitat driver is designed for use with the Securifi SZ-KFB01 Key Fob
*
* Note(s):
* Due to the way the keyfob works and the way Hubitat handles the "pushableButton" capability, you need to make a workaround
* to detect if a button has been pressed more than once in a row (the keyfob does not support the "releasableButton" capability).
* A possible workaround is a Rule that triggers when ButtonPresses is changed and has conditional actions
* IF ( Keyfob pushed = 1 ) THEN Do Action 1
* IF ( Keyfob pushed = 2 ) THEN Do Action 2
* IF ( Keyfob pushed = 3 ) THEN Do Action 3
*
* Features List:
* Ability to handle three main buttons (all except the * which is for inclusion/exclusion only)
* Ability to trigger commands from the device or Rules instead of physical presses
* Ability to return battery state
* Ability to check a website (mine) if there is a newer version of the driver available
*
* Licensing:
* Copyright 2024 David Snell
* 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.
*
* Version Control:
* 0.7.3 - Removal of old driver-specific attributes when Preferences are saved and change to driver update checking
* 0.7.2 - Restored broken configuration settings
* 0.7.1 - Removing "*" button from all functions due to it only being for inclusion/exclusion and cleaned out extra ZigBee clusters
* 0.7.0 - Switch to SemVer versioning, replacement of update mechanism and logging, made it easier to find repeated button presses
* 0.6 - Update to driver version checking section
* 0.5 - Updated the ZigBee reporting section
* 0.4 - Added sendEvent for ButtonPresses
* 0.3 - Fixed the fingerprint (accidentally had the Almond Click) and more work on the "*" button detection
* 0.2 - Getting "*" button as a command, preparing for detection method
* 0.1 - Initial version
*
* Thank you(s):
* I would like to thank @Cobra his contributions to the community. Parts based on Cobra's driver update code
* have been included at the bottom of the driver and are noted as such.
*/
import hubitat.zigbee.zcl.DataType
// Returns the driver name
def DriverName(){
return "Securifi KeyFob"
}
// Returns the driver version
def DriverVersion(){
return "0.7.3"
}
metadata{
definition( name: "Securifi KeyFob", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/SecurifiKeyFob.groovy" ){
capability "Battery"
capability "PushableButton"
capability "Actuator"
// Commands that are able to be activated
command "PushButton", [ [ name: "Push Button*", type: "ENUM", description: "Pick which button to push (NOTE: * button is only for inclusion/exclusion)", constraints: [ 1 : "Lock/Top Left", 2 : "Home/Top Right", 3 : "Unlock/Bottom Left" ] ] ]
// Attributes being built into the device
// Driver Related
attribute "DriverName", "string" // Identifies the driver being used for update purposes
attribute "DriverVersion", "string" // Handles version for driver
attribute "DriverStatus", "string" // Handles version notices for driver
// ButtonPresses is used to show the number of button presses
attribute "ButtonPresses", "number"
// ButtonName is used to show the name of the button pressed
attribute "ButtonName", "string"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0500", outClusters: "0003, 0501", manufacturer: "Sercomm Corp.", model: "SZ-KFB01", deviceJoinName: "Securifi Keyfob"
}
preferences{
section{
input( type: "int", name: "MaxPresses", title: "Max button presses before rolling over", required: false, defaultValue: 4 )
input( type: "enum", name: "LogType", title: "Enable Logging?", required: false, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" )
}
}
}
// Called when preferences are saved
// This clears state variables then sets some basic information as well as does a reconfig of the device
def updated(){
Logging( "Updated", 3 )
state.clear()
// Set the driver name and version before update checking is scheduled
if( state."Driver Name" != null ){
state.remove( "Driver Name" )
state.remove( "Driver Version" )
device.deleteCurrentState( "Driver Name" )
device.deleteCurrentState( "Driver Version" )
}
ProcessState( "DriverName", "${ DriverName() }" )
ProcessState( "DriverVersion", "${ DriverVersion() }" )
state.SensorType = "Keyfob"
sendEvent( name:"numberOfButtons", value: 3, isStateChange: false )
state.ButtonPresses = 0
// Schedule the daily driver version check
schedule( new Date(), CheckForUpdate )
ZigBeeReportingCommands()
}
// Parse incoming device messages to generate events
def parse( String description ){
def event = zigbee.getEvent( description )
if( event ){
Logging( "event name is ${ event.name }", 3 )
if( event.name == "power" ){
PowerEvent( description )
} else {
sendEvent( event )
}
} else if( description?.startsWith( "read attr -" ) ){
parseReportAttributeMessage( description )
} else if( description?.startsWith( "zone status" ) ){
Logging( "Zone event: ${ description }", 3 )
} else if( description?.startsWith( "catchall" ) ){
parseReportAttributeMessage( description )
} else {
Logging( "DID NOT PARSE MESSAGE", 3 )
Logging( "${ zigbee.parseDescriptionAsMap( description ) }", 3 )
}
}
/*
* This parses ZigBee reports that do not have immediate events and tries to make sense
* of them by cluster.
*/
private parseReportAttributeMessage( String description ){
def descMap = zigbee.parseDescriptionAsMap( description )
def cluster = descMap.cluster ?: descMap.clusterId
def hexValue = descMap.value
def attrId = descMap.attrId
Logging( "clusterId = ${ cluster }, attrId = ${ attrId } - cmd = ${ descMap.command }, data = ${ descMap.data }. value = ${ hexValue }", 3 )
switch( cluster ){
// Handled clusters
case "0001": // Power configuration
BatteryReport( descMap.command, descMap.data )
break
case "0501": // IAS ACE "Ancillary Control"
HandleIASACE( descMap.data, hexValue )
break
// Ignored Clusters
case "0013": // ZDO Device Announce or Multistate Output
case "0500": // IAS Zone
Logging( "Ignored Cluster ${ cluster }", 4 )
break
// Unhandled Clusters
default:
Logging( "Unhandled Cluster ${ cluster }: descMap:${ descMap }, description:${ description }", 4 )
break
}
}
// Processes temperature related reports
private BatteryReport( cmd, data ){
def BatteryPercent
if( cmd == "07" ){
BatteryPercent = data[ 0 ] as int
Logging( "Battery at ${ BatteryPercent }%", 2 )
sendEvent( name: "battery", value: BatteryPercent, unit: "%", isStateChange: true )
}
}
// Processes IAS reports, at this time button presses from a Click or Keyfob
private HandleIASACE( data, hex ){
if( data == null && hex == null) return
def CleanedData = data[ 0 ] as String
Logging( "ZigBee Button Value = ${ CleanedData }", 4 )
if( state.SensorType == "Keyfob" ){
KeyfobPress( CleanedData )
} else {
Logging( "IASACE but no idea what...", 3 )
}
}
// Used for when the Push Button command is triggered.
private PushButton( Value ){
switch( Value ){
case "Lock/Top Left":
KeyfobPress( "03" )
break
case "Home/Top Right":
KeyfobPress( "02" )
break
case "Unlock/Bottom Left":
KeyfobPress( "00" )
break
}
}
// Handles when a press is reported using a keyfob
private KeyfobPress( Data ){
if( Data == "03" ){
if( state.ButtonName != "Lock" ){
state.ButtonName = "Lock"
state.ButtonPresses = 1
} else {
if( state.ButtonPresses < 4 || state.ButtonPresses == null ){
state.ButtonPresses += 1
} else {
state.ButtonPresses = 0
}
}
sendEvent( name: "pushed", value: 1, isStateChange: true )
} else if( Data == "02" ){
if( state.ButtonName != "Home" ){
state.ButtonName = "Home"
state.ButtonPresses = 1
} else {
if( state.ButtonPresses < 4 || state.ButtonPresses == null ){
state.ButtonPresses += 1
} else {
state.ButtonPresses = 0
}
}
sendEvent( name: "pushed", value: 2, isStateChange: true )
} else if( Data == "00" ){
if( state.ButtonName != "Unlock" ){
state.ButtonName = "Unlock"
state.ButtonPresses = 1
} else {
if( state.ButtonPresses < 4 || state.ButtonPresses == null ){
state.ButtonPresses += 1
} else {
state.ButtonPresses = 0
}
}
sendEvent( name: "pushed", value: 3, isStateChange: true )
}
Logging( "Button presses = ${ state.ButtonPresses }", 4 )
sendEvent( name: "ButtonPresses", value: state.ButtonPresses, isStateChange: true )
sendEvent( name: "ButtonName", value: state.ButtonName, isStateChange: true )
}
// ZigBee Read commands
def ZigBeeReadCommands(){
Logging( "Refreshing device...", 4 )
def cmds = zigbee.readAttribute( zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020 )
cmds += zigbee.readAttribute( zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021 )
cmds += zigbee.enrollResponse()
return cmds
}
// Configures the device, typically at install or when preferences are saved
def ZigBeeReportingCommands(){
Logging( "Configuring device...", 4 )
def cmds = zigbee.configureReporting( 0x0001, 0x0020, DataType.UINT8, 1, 86400, 1 )
cmds += zigbee.configureReporting( zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 1, 3600, 0x10 )
cmds = cmds + ZigBeeReadCommands()
return cmds
}
// installed is called when the device is installed, all it really does is run updated
def installed(){
Logging( "Installed", 2 )
updated()
}
// initialize is called when the device is installed, all it really does is run updated
def initialize(){
Logging( "Initialized", 2 )
updated()
}
// Handles whether logging is enabled and thus what to put there.
def Logging( LogMessage, LogLevel ){
// Add all messages as info logging
if( ( LogLevel == 2 ) && ( LogType != "None" ) ){
log.info( "${ device.displayName } - ${ LogMessage }" )
} else if( ( LogLevel == 3 ) && ( ( LogType == "Debug" ) || ( LogType == "Trace" ) ) ){
log.debug( "${ device.displayName } - ${ LogMessage }" )
} else if( ( LogLevel == 4 ) && ( LogType == "Trace" ) ){
log.trace( "${ device.displayName } - ${ LogMessage }" )
} else if( LogLevel == 5 ){
log.error( "${ device.displayName } - ${ LogMessage }" )
}
}
// Checks drdsnell.com for the latest version of the driver
// Original inspiration from @cobra's version checking
def CheckForUpdate(){
ProcessEvent( "DriverName", DriverName() )
ProcessEvent( "DriverVersion", DriverVersion() )
httpGet( uri: "https://www.drdsnell.com/projects/hubitat/drivers/versions.json", contentType: "application/json" ){ resp ->
switch( resp.status ){
case 200:
if( resp.data."${ DriverName() }" ){
CurrentVersion = DriverVersion().split( /\./ )
if( resp.data."${ DriverName() }".version == "REPLACED" ){
ProcessEvent( "DriverStatus", "Driver replaced, please use ${ resp.data."${ state.DriverName }".file }" )
} else if( resp.data."${ DriverName() }".version == "REMOVED" ){
ProcessEvent( "DriverStatus", "Driver removed and no longer supported." )
} else {
SiteVersion = resp.data."${ DriverName() }".version.split( /\./ )
if( CurrentVersion == SiteVersion ){
Logging( "Driver version up to date", 3 )
ProcessEvent( "DriverStatus", "Up to date" )
} else if( ( CurrentVersion[ 0 ] as int ) > ( SiteVersion [ 0 ] as int ) ){
Logging( "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 )
ProcessEvent( "DriverStatus", "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" )
} else if( ( CurrentVersion[ 1 ] as int ) > ( SiteVersion [ 1 ] as int ) ){
Logging( "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 )
ProcessEvent( "DriverStatus", "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" )
} else if( ( CurrentVersion[ 2 ] as int ) > ( SiteVersion [ 2 ] as int ) ){
Logging( "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 )
ProcessEvent( "DriverStatus", "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" )
} else if( ( SiteVersion[ 0 ] as int ) > ( CurrentVersion[ 0 ] as int ) ){
Logging( "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 )
ProcessEvent( "DriverStatus", "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" )
} else if( ( SiteVersion[ 1 ] as int ) > ( CurrentVersion[ 1 ] as int ) ){
Logging( "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 )
ProcessEvent( "DriverStatus", "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" )
} else if( ( SiteVersion[ 2 ] as int ) > ( CurrentVersion[ 2 ] as int ) ){
Logging( "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 )
ProcessEvent( "DriverStatus", "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" )
}
}
} else {
Logging( "${ DriverName() } is not published on drdsnell.com", 2 )
ProcessEvent( "DriverStatus", "${ DriverName() } is not published on drdsnell.com" )
}
break
default:
Logging( "Unable to check drdsnell.com for ${ DriverName() } driver updates.", 2 )
break
}
}
}