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