/* * 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 } } }