/* * Securifi Buttons * * Description: * This Hubitat driver is designed for use with the following Securifi devices: * ZB2-BU01 Click button - WORKING * SZ-KFB01 Key Fob - WORKING * * Features List: * v0.1 - Ability to handle multiple buttons for the Key Fob * v0.1 - Ability to register button presses (short, double, and long) from an Almond Click, counting up to 4 * v0.1 - Ability to determine type based on model * v0.1 - Ability to return battery state * v0.1 - Ability to check a website (mine) if there is a newer version of the driver available * * Licensing: * Copyright 2020 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.5.0 - Switching version control method and other rework to newer methods * 0.41 - General cleanup and rework of reporting * 0.4 - Update to driver version checking section * 0.3 - Updated the ZigBee reporting section * 0.2 - Correction to state changed for events * 0.1 - Initial version split from Securifi Sensors driver * * 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 metadata{ definition( name: "Securifi Buttons", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/SecurifiButtons.groovy" ){ capability "Configuration" capability "Refresh" capability "Sensor" capability "Battery" capability "Pushable Button" capability "HoldableButton" capability "DoubleTapableButton" // Attributes being built into the device // Driver identifies the driver being used for update purposes attribute "Driver", "string" // Version number, meant more for when I add in self-updating and related notifications attribute "Version", "string" // SensorType is used to determine what type of sensor is actually being used attribute "SensorType", "string" // PressType is used for the Almond Click and Keyfobs to show if a press was long or short attribute "PressType", "string" // ButtonPresses is used for the Almond Click and Keyfobs to show the number of button presses attribute "ButtonPresses", "int" // Message is used to provide a message to the user of the device attribute "Message", "string" fingerprint profileId: "0104", inClusters: "0000, 0003, 0500, 0501", outClusters: "0003, 0501", manufacturer: "Sercomm Corp.", model: "SZ-KFB01", deviceJoinName: "Keyfob" fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0013, 0402, 0500, 0501, 0B05", outClusters: "0019", manufacturer: "Securifi", model: "ZB2-BU01", deviceJoinName: "Almond Click" } preferences{ section{ input( type: "enum", name: "LogType", title: "Enable Logging?", required: false, defaultValue: "2", multiple: false, options: [ [ "1" : "None" ], [ "2" : "Info" ], [ "3" : "Debug" ], [ "4" : "Trace" ] ] ) } } } // 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() state.Driver = "SecurifiButtons" state.Version = "0.5.0" DetermineDeviceType() // Set up the regular checks for driver version schedule( new Date(), CheckForUpdate ) configure() } // Check the model information to determine what the device is private DetermineDeviceType(){ if( device.data.model.startsWith( "SZ-KFB01" ) ){ Logging( "model ${ device.data.model } = keyfob", 3 ) state.SensorType = "Keyfob" sendEvent( name:"numberOfButtons", value: 4 ) state.PressType = null state.ButtonPresses = 0 sendEvent( name: "pushed", value: 0 ) sendEvent( name: "held", value: 0 ) sendEvent( name: "doubleTapped", value: 0 ) sendEvent( name: "ButtonPresses", value: 0 ) } else if( device.data.model.startsWith( "ZB2-BU01" ) ){ Logging( "model ${ device.data.model } = Click", 3 ) sendEvent( name:"numberOfButtons", value: 1 ) state.SensorType = "Click" state.PressType = null state.ButtonPresses = 0 sendEvent( name: "pushed", value: 0 ) sendEvent( name: "held", value: 0 ) sendEvent( name: "doubleTapped", value: 0 ) sendEvent( name: "ButtonPresses", value: 0 ) } else { Logging( "model ${ device.data.model } = unknown sensor", 3 ) } } // 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. I am including as many clusters as possible in here, even ones this * driver will never handle, so that it can be a useful reference for future work or others. */ private parseReportAttributeMessage( String description ){ def descMap = zigbee.parseDescriptionAsMap( description ) def cluster = descMap.cluster ?: descMap.clusterId def hexValue = descMap.value def attrId = descMap.attrId def ClusterIdentified = "Unknown" as String def Handled = false as boolean switch( cluster ){ // General Clusters case "0000": // Basic ClusterIdentified = "Basic" BasicReport( descMap ) Handled = true break case "0001": // Power config ClusterIdentified = "Power config" PowerConfigReport( descMap ) Handled = true break case "0002": // Device temperature configuration break case "0003": // Identify break case "0004": // Groups break case "0005": // Scenes break case "0006": // On/Off break case "0007": // On/Off switch configuration break case "0008": // Level control break case "0009": // Alarms break case "0013": // Multistate Output & ZDO Device Announce ClusterIdentified = "Multistate Output & ZDO Device Announce" AnnounceReport( descMap ) Handled = true break case "000A": // Time break case "000B": // RSSI Location break case "000C": // Analog Input break case "000D": // Analog Output break case "000E": // Analog Value break case "000F": // Binary Input break case "0010": // Binary Output break case "0011": // Binary Value break case "0012": // Multistate Input break case "0013": // Multistate Output break case "0014": // Multistate Value break case "0015": // Commissioning break case "0016": // Discovery_Store_req break case "0017": // Node_Desc_Store_req break case "0018": // Power_Desc_Store_req break case "0019": // Active_EP_Store_req break case "001A": // Simple_Desc_Store_req break case "001B": // Remove_Node_Cache_req break case "001C": // Find_Node_Cache_req break // End Device Bind, Unbind and Bind Management case "0020": // End_Dev_Bind_req break case "0021": // Bind_req break case "0022": // Unbind_req break case "0023": // Bind_Register_req break case "0024": // Replace_Device_req break case "0025": // Store_Bkup_Bind_Entry_req break case "0026": // Rm_Bkup_Bind_Entry_req break case "0027": // Backup_Bind_Table_req break case "0028": // Recover_Bind_Table_req break case "0029": // Backup_Source_Bind_req break case "002A": // Recover_Source_Bind_req break // Network Management case "0030": // Mgmt_NWK_Disc_req break case "0031": // Mgmt_LQI_req break case "0032": // Mgmt_Rtg_req break case "0033": // Mgmt_Bind_req break case "0034": // Mgmt_Leave_req break case "0035": // Mgmt_Direct_Join_req break case "0036": // Mgmt_Permit_Join_req break case "0037": // Mgmt_Cache_req break // Closures Clusters case "0100": // Shade configuration break case "0101": // Door Lock break // HVAC Clusters case "0200": // Pump configuration and control break case "0201": // Thermostat break case "0202": // Fan control break case "0203": // Dehumidifier control break case "0204": // Thermostat user interface configuration break // Lighting Clusters case "0300": // Color control break case "0301": // Ballast configuration break // Measurement and Sensing Clusters case "0400": // Luminance measurement break case "0401": // Luminance level sensing break case "0402": // Temperature measurement break case "0403": // Pressure measurement break case "0404": // Flow measurement break case "0405": // Relative humidity measurement break case "0406": // Occupancy sensing break // Security & Safety Clusters case "0500": // IAS Zone break case "0501": // IAS ACE "Ancillary Control" ClusterIdentified = "IAS ACE Ancillary Control" HandleIASACE( descMap.data, hexValue ) Handled = true break case "0502": // IAS WD "Warning Devices" break // Protocol Interface Clusters case "0600": // Generic Tunnel break case "0601": // BACnet Protocol Tunnel break case "0602": // Analog Input (BACnet Regular) break case "0603": // Analog Input (BACnet Extended) break case "0604": // Analog Output (BACnet Regular) break case "0605": // Analog Output (BACnet Extended) break case "0606": // Analog Value (BACnet Regular) break case "0607": // Analog Value (BACnet Extended) break case "0608": // Binary Input (BACnet Regular) break case "0609": // Binary Input (BACnet Extended) break case "060A": // Binary Output (BACnet Regular) break case "060B": // Binary Output (BACnet Extended) break case "060C": // Binary Value (BACnet Regular) break case "060D": // Binary Value (BACnet Extended) break case "060E": // Multistate Input (BACnet Regular) break case "060F": // Multistate Input (BACnet Extended) break case "0610": // Multistate Output (BACnet Regular) break case "0611": // Multistate Output (BACnet Extended) break case "0612": // Multistate Value (BACnet Regular) break case "0613": // Multistate Value (BACnet Extended) break case "0702": // seMetering break case "2820": // Electrical Measurement break case "8000": // NWK_ADDR_RSP break case "8001": // IEEE_ADDR_RSP break case "8002": // NODE_DESC_RSP break case "8003": // POWER_DESC_RSP break case "8004": // SIMPLE_DESC_RSP break case "8005": // ACTIVE_EP_RSP break case "8006": // MATCH_DESC_RSP break case "8010": // Complex_Desc_rsp break case "8011": // User_Desc_rsp break case "8012": // Discovery_Cache_rsp break case "8014": // User_Desc_Conf break case "8015": // System_Server_Discover_rsp break case "8016": // Discovery_Store_rsp break case "8017": // Node_Desc_Store_rso break case "8018": // Power_Desc_Store_rsp break case "8019": // Active_EP_Store_rsp break case "801A": // Simple_Desc_Store_rsp break case "801B": // Remove_Node_Cache_rsp break case "801C": // Find_Node_Cache_rsp break // End Device Bind, Unbind and Bind Management case "8020": // End_Dev_Bind_rsp break case "8021": // Bind_rsp break case "8022": // Unbind_rsp break case "8023": // Bind_Register_rsp break case "8024": // Replace_Device_rsp break case "8025": // Store_Bkup_Bind_Entry_rsp break case "8026": // Rm_Bkup_Bind_Entry_rsp break case "8027": // Backup_Bind_Table_rsp break case "8028": // Recover_Bind_Table_rsp break case "8029": // Backup_Source_Bind_rsp break case "802A": // Recover_Source_Bind_rsp break // Network Management case "8030": // Mgmt_NWK_Disc_rsp break case "8031": // Mgmt_LQI_rsp break case "8032": // Mgmt_Rtg_rsp break case "8033": // Mgmt_Bind_rsp break case "8034": // Mgmt_Leave_rsp break case "8035": // Mgmt_Direct_Join_rsp break case "8036": // Mgmt_Permit_Join_rsp break case "8037": // Mgmt_Cache_rsp break // Clusters I have seen reported but do not know what they are at this time case "0B02": // ??? break case "0B04": // haElectricalMeasurement ? break case "0B05": // haDiagnostic ? break case "FD00": // Halo Smoke Detector Only ??? break case "FD01": // Halo Smoke Detector Only ??? break case "FD02": // Halo Smoke Detector Only ??? break default: Logging( "Unknown ClusterID: descMap:${ descMap }, description:${ description }", 3 ) break } } // Handles ZDO Announce reports def AnnounceReport( descMap ){ switch( descMap.command ){ case "00": Logging( "Device alive/awake", 3 ) break default: Logging( "Announce Report ${ descMap }", 3 ) break } } // Processes temperature related reports private BatteryReport( descMap ){ def BatteryPercent if( descMap.command == "07" ){ BatteryPercent = descMap.data[ 0 ] as int Logging( "Battery at ${ BatteryPercent }%", 2 ) sendEvent( name: "battery", value: BatteryPercent, unit: "%" ) } } // 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 }", 3 ) if( state.SensorType == "Click" ){ if( CleanedData == "00" ){ DoublePress() } else if( CleanedData == "03" ){ SinglePress() } else if( CleanedData == "20" ){ LongButtonPress() } else if( CleanedData == "02" ){ LongButtonPress() } } else if( state.SensorType == "Keyfob" ){ KeyfobPress( CleanedData ) } else { Logging( "IASACE but no idea what...", 3 ) } } // Handles when a press is reported using a keyfob private KeyfobPress( Data ){ if( Data == "03" ){ if( state.PressType != "Lock" ){ state.PressType = "Lock" state.ButtonPresses = 1 } else { if( state.ButtonPresses < 4 || state.ButtonPresses == null ){ state.ButtonPresses += 1 } else { state.ButtonPresses = 0 } } sendEvent( name: "pushed", value: 1 ) } else if( Data == "02" ){ if( state.PressType != "Home" ){ state.PressType = "Home" state.ButtonPresses = 1 } else { if( state.ButtonPresses < 4 || state.ButtonPresses == null ){ state.ButtonPresses += 1 } else { state.ButtonPresses = 0 } } sendEvent( name: "pushed", value: 2 ) } else if( Data == "00" ){ if( state.PressType != "Unlock" ){ state.PressType = "Unlock" state.ButtonPresses = 1 } else { if( state.ButtonPresses < 4 || state.ButtonPresses == null ){ state.ButtonPresses += 1 } else { state.ButtonPresses = 0 } } sendEvent( name: "pushed", value: 3 ) } Logging( "Button presses = ${ state.ButtonPresses }", 3 ) sendEvent( name: "ButtonPresses", value: state.ButtonPresses ) sendEvent( name: "PressType", value: state.PressType ) } // Handles when a single press has been reported private SinglePress(){ if( state.PressType == "Long" || state.PressType == "Double" || state.PressType == null ){ state.PressType = "Single" state.ButtonPresses = 0 } if( state.ButtonPresses < 4 || state.ButtonPresses == null ){ state.ButtonPresses += 1 } else { state.ButtonPresses = 0 } Logging( "Short button presses = ${ state.ButtonPresses }", 3 ) sendEvent( name: "ButtonPresses", value: state.ButtonPresses ) sendEvent( name: "PressType", value: state.PressType ) sendEvent( name: "pushed", value: 1 ) sendEvent( name: "doubleTapped", value: 0 ) sendEvent( name: "held", value: 0 ) //sendEvent( name: "buttonPressed", value: "single" ) } // Handles when a double press has been reported private DoublePress(){ if( state.PressType == "Long" || state.PressType == "Single" || state.PressType == null ){ state.PressType = "Double" state.ButtonPresses = 0 } if( state.ButtonPresses < 4 || state.ButtonPresses == null ){ state.ButtonPresses += 1 } else { state.ButtonPresses = 0 } Logging( "Double button presses = ${ state.ButtonPresses }", 3 ) sendEvent( name: "ButtonPresses", value: state.ButtonPresses ) sendEvent( name: "PressType", value: state.PressType ) sendEvent( name: "pushed", value: 1 ) sendEvent( name: "doubleTapped", value: 1 ) sendEvent( name: "held", value: 0 ) //sendEvent( name: "buttonPressed", value: "double" ) } // Handles when a long press has been reported (>5 seconds in length) private LongButtonPress(){ if( state.PressType == "Single" || state.PressType == "Double" || state.PressType == null ){ state.PressType = "Long" state.ButtonPresses = 0 } if( state.ButtonPresses < 4 || state.ButtonPresses == null ){ state.ButtonPresses += 1 } else { state.ButtonPresses = 0 } Logging( "Long button presses = ${ state.ButtonPresses }", 3 ) sendEvent( name: "ButtonPresses", value: state.ButtonPresses ) sendEvent( name: "PressType", value: state.PressType ) sendEvent( name: "pushed", value: 1 ) sendEvent( name: "held", value: 1 ) sendEvent( name: "doubleTapped", value: 0 ) } // Sets the configuration reporting def ConfigureReporting(){ def ReportStructure = zigbee.configureReporting( 0x0001, 0x0021, 0x20, 1, 86400, 0x01 ) + zigbee.readAttribute( 0x0001, 0x0021 ) return ReportStructure } // Configures the device, typically at install or when preferences are saved def configure(){ Logging( "Configuring device...", 3 ) return ConfigureReporting() } // Refresh reporting mechanism def RefreshReport(){ def ReportStructure = zigbee.readAttribute( 0x0001, 0x0021 ) + zigbee.configureReporting( 0x0001, 0x0021, 0x20, 1, 86400, 0x01 ) return ReportStructure } // Refreshes the device information def refresh(){ Logging( "Refreshing device...", 3 ) state.Message = null return RefreshReport() } // 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 initialized, 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( LogType >= "2" && LogLevel == 2 ){ log.info( "${ device.displayName } - ${ LogMessage }" ) } else if( LogType >= "3" && LogLevel == 3 ){ log.debug( "${ device.displayName } - ${ LogMessage }" ) } else if( LogType >= "4" && LogLevel == 4 ){ 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(){ state.Driver = "SecurifiButtons" state.Version = "0.5.0" httpGet( uri: "https://www.drdsnell.com/projects/hubitat/drivers/versions.json", contentType: "application/json" ){ resp -> switch( resp.status ){ case 200: if( resp.data."${ state.Driver }" ){ CurrentVersion = state.Version.split( /\./ ) SiteVersion = resp.data."${ state.Driver }".version.split( /\./ ) if( CurrentVersion == SiteVersion ){ Logging( "Driver version up to date", 3 ) sendEvent( name: "Version", value: "Up to date" ) } else if( CurrentVersion[ 0 ] > SiteVersion [ 0 ] ){ Logging( "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) sendEvent( name: "Version", value: "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( CurrentVersion[ 1 ] > SiteVersion [ 1 ] ){ Logging( "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) sendEvent( name: "Version", value: "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( CurrentVersion[ 2 ] > SiteVersion [ 2 ] ){ Logging( "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) sendEvent( name: "Version", value: "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( SiteVersion[ 0 ] > CurrentVersion[ 0 ] ){ Logging( "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) sendEvent( name: "Version", value: "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } else if( SiteVersion[ 1 ] > CurrentVersion[ 1 ] ){ Logging( "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) sendEvent( name: "Version", value: "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } else if( SiteVersion[ 2 ] > CurrentVersion[ 2 ] ){ Logging( "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) sendEvent( name: "Version", value: "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } } else { Logging( "Unpublished driver", 3 ) sendEvent( name: "Version", value: "Unpublished driver" ) } break default: Logging( "Unable to check drdsnell.com for driver updates.", 3 ) break } } }