/* * WADWAZ-1 Contact Sensor * * Description: * This Hubitat driver is designed for use with the following sensor(s): * WADWAZ-1 Door/Window Sensor * * Features List: * Ability to enable the external contact switch * Battery reporting * Ability to determine reported contact state based on internal or external switch * Ability to determine contact switch state * Ability to show tampered state and reset tampering * Ability to check a website (mine) if there is a newer version of the driver available * * Licensing: * Copyright 2021 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.2 - Replaced a boolean attribute with a string as boolean attributes are not valid * 0.7.1 - Corrected version listings, made it so Internal/External Contact are now published as events, removed refresh, * Added ability to enable the external contact (hopefully) * 0.7.0 - Transition to newer coding methods, semver versioning, updated logging, general changes. * 0.6 - Update to driver version checking section * 0.5 - Correction to state changed for events * 0.4 - Changes to the update notification code * 0.3 - Realized I had extra preferences for contact type that were not needed and combined them * 0.2 - Added ability to determine how contact should be reported and revised handlers * 0.1 - Initial attempt at WADWAZ support based on my 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. */ metadata{ definition( name: "WADWAZ", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/WADWAZ.groovy" ){ //capability "Configuration" // No relevant commands to use this for at this time //capability "Refresh" capability "Sensor" capability "Battery" capability "Contact Sensor" capability "Tamper Alert" command "ResetTamper" command "GetParameter", [ [ name:"Parameter", type:"NUMBER", description:"Parameter Number (blank gets all)", constraints:[ "NUMBER" ] ] ] command "SetParameter",[ [ name:"Parameter", type:"NUMBER", description:"Parameter Number", constraints:[ "NUMBER" ] ], [ name:"Size", type:"NUMBER", description:"Size", constraints:[ "NUMBER" ] ], [ name:"Value", type:"NUMBER", description:"Value", constraints:[ "NUMBER" ] ] ] // Attributes being built into the device attribute "Driver", "string" // Driver identifies the driver being used for update purposes attribute "Version", "string" // Version number, meant more for when I add in self-updating and related notifications attribute "InternalContact", "string" // String to handle the value of the internal contact switch attribute "ExternalContact", "string" // String to handle the value of the external contact switch attribute "ExternalEnabled", "string" // Is the external contact enabled or not? fingerprint deviceId: "0x2001", deviceType: "203", inClusters: "0x5E, 0x72, 0x5A, 0x80, 0x73, 0x86, 0x84, 0x85, 0x59, 0x71, 0x70, 0x7A, 0x98", outClusters: "", manufacturer: "0109", model: "WADWAZ-1", deviceJoinName: "WADWAZ Door/Window Sensor" } preferences{ section{ if( ShowAllPreferences || ShowAllPreferences == null ){ // Show the preferences options input( type: "enum", name: "PrimaryContact", title: "Which to trigger from?", required: true, multiple: false, options: [ "Internal", "External", "Either Open / Both Closed", "Either Closed / Both Open" ], defaultValue: "Internal" ) input( type: "bool", name: "EnableExternal", title: "Enable External Contact?", defaultValue: true ) input( type: "enum", name: "LogType", title: "Enable Logging?", required: true, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" ) input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true ) } else { input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true ) } } } } // Sets some basic values of the driver def updated(){ Logging( "Updated", 2 ) // Need to "backup" the current contact states because it can mess up later checks if they are null def TempInternal = state.InternalContact def TempExternal = state.ExternalContact ProcessState( "Driver", "WADWAZ" ) ProcessState( "Version", "0.7.2" ) // Restore the contact states and set contact ProcessState( "InternalContact", TempInternal ) // Only restore the external if the external contact is enabled if( EnableExternal ){ ExternalContactControl( true ) ProcessState( "ExternalContact", TempExternal ) } else { ExternalContactControl( false ) } CheckPrimaryContact( "RESTORED", null ) // Reset the tamper notice ResetTamper() // Schedule the daily driver version check schedule( new Date(), CheckForUpdate ) } // Refreshes the device information by clearing states and tamper def refresh(){ ProcessState( "InternalContact", null ) ProcessState( "ExternalContact", null ) ResetTamper() } // parse handles splitting up the z-wave reports and making sure the commands are handled def parse( String description ){ def Split = description.split() def CommandNumber1 = Split[ 4 ].take( 2 ) def CommandNumber2 = Split[ 4 ].drop( 2 ).take( 2 ) def Type = Split[ 6 ].take( 2 ) def Value = Split[ 7 ].take( 2 ) Logging( "Z-Wave data: ${ description }", 4 ) if( description.startsWith( "Err" ) ){ Logging( "Parse Error: ${ description }", 3 ) } else { switch( CommandNumber1 ){ case "20": // Basic HandleBasic( CommandNumber2, Type ) break case "70": // CONFIGURATION HandleConfiguration( CommandNumber2, Type, Value ) break case "71": // Alarm/Notification def Payload = Split[ 11 ].take( 2 ) HandleAlarm( Type, Payload, Value ) break case "80": // Battery HandleBattery( CommandNumber2, Value ) // Not exactly sure if Value is the correct one at this time break case "84": // WakeUp HandleWakeUp( CommandNumber2, Type, Value ) break case "86": // Version HandleVersion( CommandNumber2, Type, Value ) break default: Logging( "Unknown Command1=${ CommandNumber1 } Command2=${ CommandNumber2 } Description=${ description }", 3 ) break } } } // Handle the basic events from Z-Wave def HandleBasic( CommandNumber2, Type ){ def TypeInt = Integer.parseInt( Type, 16 ) switch( CommandNumber2 ){ case "01": //Contact( "internal", TypeInt ) // Disabling while duplication bug exists if( TypeInt == 255 ){ Logging( "Basic Response: Internal Contact reported Open", 4 ) } else if( TypeInt == 0 ){ Logging( "Basic Response: Internal Contact reported Closed", 4 ) } else { Logging( "Basic Response: Internal Contact reported something odd ${ TypeInt }", 4 ) } break default: Logging( "Unknown Basic report. CommandNumber2=${ CommandNumber2 } Type=${ TypeInt }", 3 ) break } } // Handle the basic events from Z-Wave def HandleConfiguration( CommandNumber2, Type, Value ){ switch( CommandNumber2 ){ case "06": // Something Logging( "Parameter ${ Type } = ${ Value }", 3 ) break default: Logging( "Unknown Configuration Command2:${ CommandNumber2 } Type:${ Type } = ${ Value }", 3 ) break } } // Handle the basic events from Z-Wave def HandleVersion( CommandNumber2, Type, Value ){ switch( CommandNumber2 ){ case "12": // Something Logging( "Version #12: ${ Type } = ${ Value }", 3 ) break case "14": // Something Logging( "Version #14: ${ Type } = ${ Value }", 3 ) break default: Logging( "Unknown Version Command2:${ CommandNumber2 } Type:${ Type } = ${ Value }", 3 ) break } } // Handle the alarm reports from Z-Wave def HandleAlarm( Type, Payload, Value ){ def ValueInt = Integer.parseInt( Value, 16 ) switch( Type ){ case "06": switch( Payload ){ case "16": Contact( "internal", ValueInt ) break case "17": Contact( "internal", ValueInt ) break default: Logging( "Unknown Alarm 06 payload=${ Payload } & Value=${ ValueInt }", 3 ) break } break case "07": switch( Payload ){ case "00": Logging( "Case closed (tamper pressed)", 4 ) break case "02": Contact( "internal", ValueInt ) break case "03": ProcessEvent( "tamper", "tampered" ) break case "FE": Contact( "external", ValueInt ) break default: Logging( "Unknown Alarm 07 payload=${ Payload } & Value=${ ValueInt }", 3 ) break } break default: Logging( "Unknown Alarm type=${ Type }, payload=${ Payload } & ValueInt=${ ValueInt }", 3 ) break } } // Sets the contact value for based on preferences private Contact( Type, Value ){ if( Type == "internal" ){ if( Value == 255 ){ Logging( "Internal Contact Opened", 2 ) ProcessEvent( "InternalContact", "open" ) } else if( Value == 0 ){ Logging( "Internal Contact Closed", 2 ) ProcessEvent( "InternalContact", "closed" ) } } else { if( Value == 255 ){ Logging( "External Contact Opened", 2 ) ProcessEvent( "ExternalContact", "open" ) } else if( Value == 0 ){ Logging( "External Contact Closed", 2 ) ProcessEvent( "ExternalContact", "closed" ) } } CheckPrimaryContact( Type, Value ) } // Handle the Wake Up events from Z-Wave def HandleWakeUp( CommandNumber2, Type, Value ){ switch( CommandNumber2 ){ case "07": Logging( "WakeUp Interval Report Type: ${ Type } = ${ Value }", 4 ) // Set the interval to every 10 minutes (60000 milliseconds) //sendEvent( name: "checkInterval", value: 60000, displayed: false, data: [ protocol: "zwave", hubHardwareId: device.hub.hardwareID ] ) default: Logging( "Unknown WakeUp Command=${ CommandNumber2 }, Type: ${ Type } = ${ Value }", 3 ) break } } // Handle the battery related events from Z-Wave def HandleBattery( CommandNumber2, Value ){ if( Value > 100 ){ Value = 100 } state.lastBatteryReport = new Date().time Logging( "BatteryReport: ${ CommandNumber2 }, battery at ${ Value }%", 4 ) sendEvent( name: "battery", value: Value, unit: "%", isStateChange: true ) } // Checks against changes to see if the contact needs to be altered private CheckPrimaryContact( Type, Value ){ switch( PrimaryContact ){ case "Internal": if( Type == "internal" ){ Logging( "Primary = internal, internal changed to ${ state.InternalContact }", 4 ) ProcessEvent( "contact", "${ state.InternalContact }" ) } else if( Type == "external" ){ Logging( "Primary = internal, external changed so no reporting", 4 ) } else if( Type == "RESTORED" ){ Logging( "Primary = internal, RESTORED so reporting internal", 4 ) ProcessEvent( "contact", "${ state.InternalContact }" ) } break case "External": if( Type == "external" ){ Logging( "Primary = external, external changed to ${ state.ExternalContact }", 4 ) ProcessEvent( "contact", "${ state.ExternalContact }" ) } else if( Type == "internal" ){ Logging( "Primary = external, internal changed so no reporting", 4 ) } else if( Type == "RESTORED" ){ Logging( "Primary = external, RESTORED so reporting external", 4 ) ProcessEvent( "contact", "${ state.ExternalContact }" ) } break case "Either Open / Both Closed": if( Value == 255 ){ if( Type == "internal" && state.ExternalContact == "closed" ){ Logging( "Primary = either open, ${ Type } opened so setting contact to open", 4 ) ProcessEvent( "contact", "open" ) } else if( Type == "external" && state.InternalContact == "closed" ){ Logging( "Primary = either open, ${ Type } opened so setting contact to open", 4 ) ProcessEvent( "contact", "open" ) } else if( Type == "internal" ){ Logging( "Primary = either open, internal changed but external open so no contact report", 4 ) } else { Logging( "Primary = either open, external changed but internal open so no contact report", 4 ) } } else if( Value == 0 ){ if( ( Type == "internal" && state.ExternalContact == "closed" ) || ( Type == "external" && state.InternalContact == "closed" ) ){ Logging( "Primary = either open, both closed so setting contact to closed", 4 ) ProcessEvent( "contact", "closed" ) } else if( Type == "internal" ){ Logging( "Primary = either open, internal changed but external open so no contact report", 4 ) } else { Logging( "Primary = either open, external changed but internal open so no contact report", 4 ) } } else if( Type == "RESTORED" && Value == null ){ if( InternalContact == "open" || state.ExternalContact == "open" ){ Logging( "Primary = either open, a contact is open so reporting open", 4 ) ProcessEvent( "contact", "open" ) } else { Logging( "Primary = either open, both contacts are closed so reporting closed", 4 ) ProcessEvent( "contact", "closed" ) } } break case "Either Closed / Both Open": if( Value == 0 ){ if( ( Type == "internal" && state.ExternalContact == "open" ) || ( Type == "external" && state.InternalContact == "open" ) ){ Logging( "Primary = either closed, ${ Type } closed so setting contact to closed", 4 ) ProcessEvent( "contact", "closed" ) } else if( Type == "internal" ){ Logging( "Primary = either closed, internal changed but external closed so no contact report", 4 ) } else { Logging( "Primary = either closed, external changed but internal closed so no contact report", 4 ) } } else if( Value == 255 ){ if( ( Type == "internal" && state.ExternalContact == "open" ) || ( Type == "external" && state.InternalContact == "open" ) ){ Logging( "Primary = either closed, both open so setting contact to open", 4 ) ProcessEvent( "contact", "open" ) } else if( Type == "internal" ){ Logging( "Primary = either closed, internal changed but external closed so no contact report", 4 ) } else { Logging( "Primary = either closed, external changed but internal closed so no contact report", 4 ) } } else if( Type == "RESTORED" && Value == null ){ if( InternalContact == "closed" || state.ExternalContact == "closed" ){ Logging( "Primary = either closed, a contact is closed so reporting closed", 4 ) ProcessEvent( "contact", "closed" ) } else { Logging( "Primary = either closed, both contacts are open so reporting open", 4 ) ProcessEvent( "contact", "open" ) } } break default: if( Value == 255 ){ Logging( "Primary = ${ PrimaryContact }, ${ Type } was opened but nothing responded", 4 ) } else if( Value == 0 ){ Logging( "Primary = ${ PrimaryContact }, ${ Type } was closed but nothing responded", 4 ) } else { Logging( "Primary = ${ PrimaryContact }, ${ Type } was ${ Value } but nothing responded", 4 ) } break } } // ExternalContactControl to set the parameter to enable/disable the external contact def ExternalContactControl( Enable = true ){ if( Enable ){ Logging( "Attempting to enable external contact", 4 ) return delayBetween( [ SecuredCommands( zwave.configurationV1.configurationSet( scaledConfigurationValue: 255, parameterNumber: 1, size: 1 ) ), SecuredCommands( zwave.configurationV1.configurationGet( parameterNumber: 1 ) ) ], 500 ) } else { Logging( "Attempting to disable external contact", 4 ) return delayBetween( [ SecuredCommands( zwave.configurationV1.configurationSet( scaledConfigurationValue: 0, parameterNumber: 1, size: 1 ) ), SecuredCommands( zwave.configurationV1.configurationGet( parameterNumber: 1 ) ) ], 500 ) } } // SetParameter to set a parameter, based on Hubitat examples List SetParameter( Parameter, Size, Value ){ if( Parameter == null ){ Logging( "Missing required setting: Parameter", 5 ) } else if( Size == null ){ Logging( "Missing required setting: Size", 5 ) } else if( Value == null ){ Logging( "Missing required setting: Value", 5 ) } else { Logging( "Setting ${ Parameter } to ${ Value }", 4 ) return delayBetween( [ SecuredCommands( zwave.configurationV1.configurationSet( scaledConfigurationValue: Value, parameterNumber: Parameter, size: Size ) ), SecuredCommands( zwave.configurationV1.configurationGet( parameterNumber: Parameter ) ) ], 500 ) } } // GetParameter to provide information on a parameter, based on Hubitat examples List GetParameter( Parameter = null ){ List Commands = [] if( Parameter != null ){ Commands = [ SecuredCommands( zwave.configurationV1.configurationGet( parameterNumber: Parameter ) ) ] } else { 0.upto( 255, { Commands.add( SecuredCommands( zwave.configurationV1.configurationGet( parameterNumber: it ) ) ) } ) } if( Parameter != null ){ Logging( "Attempting to get Parameter ${ Parameter }", 4 ) } else { Logging( "Attempting to get all parameters", 4 ) } return delayBetween( Commands, 500 ) } // Securing commands, attempting to handle for S2, based on Hubitat examples String SecuredCommands( Command ){ if( getDataValue( "zwaveSecurePairingComplete" ) == "true" && getDataValue( "S2" ) == null ){ return zwave.securityV1.securityMessageEncapsulation().encapsulate( Command ).format() } else { return Secured( Command ) } } // Not thrilled on overloading classes but this is the standard method, based on Hubitat examples String Secured( String Command ){ return zwaveSecureEncap( Command ) } String Secured( hubitat.zwave.Command Command ){ return zwaveSecureEncap( Command ) } // Configures the device, typically at install or when preferences are saved def configure(){ Logging( "Configuring device...", 2 ) updated() } // 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() } // Used to clear a tamper alert on the device def ResetTamper() { Logging( "Resetting the tamper notification", 2 ) ProcessEvent( "tamper", "clear" ) } // Process data to check against current state value and then send an event if it has changed def ProcessEvent( Variable, Value, Unit = null ){ if( state."${ Variable }" != Value ){ state."${ Variable }" = Value if( Unit != null ){ Logging( "Event: ${ Variable } = ${ Value }${ Unit }", 4 ) sendEvent( name: "${ Variable }", value: Value, unit: Unit, isStateChanged: true ) } else { Logging( "Event: ${ Variable } = ${ Value }", 4 ) sendEvent( name: "${ Variable }", value: Value, isStateChanged: true ) } } } // Process data to check against current state value and then send an event if it has changed def ProcessState( Variable, Value ){ if( state."${ Variable }" != Value ){ Logging( "State: ${ Variable } = ${ Value }", 4 ) state."${ Variable }" = Value } } // 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(){ state.Version = "0.7.2" state.Driver = "WADWAZ" 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 } } }