[Help needed] Fibaro FGK-10x z-wave Door window sensor temperature reporting

I made a few changes in this version:

  • Logging enable/disable/level in preferences
  • Sensor settings in preferences
  • Added driver versioning
  • Getting/displaying sensor firmware version // Not sure about displayed format...
  • Initialize all the sensor parameters on first wake-up of the device
  • Cleaned-up unused code for Hubitat application

I hope I didn't break anything...

/**
 *  Fibaro Z-Wave FGK-101 Temperature & Door/Window Sensor Handler [v0.9.5.4, 3 December 2018]
 *		
 *  Copyright 2014 Jean-Jacques GUILLEMAUD
 *
 *  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.
 *
 */
 
/******************************************************************************************************************************
 *	Fibaro Z-Wave FGK-101 Marketing Description is at :
 *		http://www.fibaro.com/en/the-fibaro-system/door-window-sensor
 *
 *  Fibaro FGK-10x Operating Manual can be downloaded at :
 *		http://www.fibaro.com/files/instrukcje/eng/DoorWindowSensor%20FGK-101-107%20ENG_v21-v23.pdf
 *
 *	The current version of this Handler is parameterized to force Device's wakeup :
 *		- on any open<->closed state change
 *		- in case of Tampering Alarm triggering
 *		- every 60mn (wakeUpIntervalSet(seconds:60*60), hard coded)
 *		- whenever Temperature delta change since last report is greater than 0.31°C (Parameter#12, hard coded)
 *		also :
 *		- Temperature is natively reported by sensor in Celsius (SensorMultilevelReport[scale:0]);
 *		  convertion is needed for Fahrenheit display 
 *
 *  A few specificities of this device that are relevant to better understand some parts of this Handler :
 *		- it is a battery operated device, so Commands can only be sent to it whenever it wakes up
 *		- it is a multi-channel Device, and the multi-level temperature sensor reports only from EndPoint#2
 *		- specific configurable parameters are documented in the above Operating Manual
 *		- some of those parameters must be modified to activate the anti-Tampering Alarm
 *		- some of the "scaffolding" has been left in place as comments, since it may help other people to understand/modify this Handler
 *		- BEWARE : the optional DS18B20 sensor must be connected BEFORE the Device is activated (otherwise, reset the Device)
 *		- IMPORTANT : for debugging purpose, it is much better to change the wake-up period from the default 60mn to 1mn or so;
 *					but unless you force the early wake up of the sensor (forcing open/closed for instance), you will have to
 *					wait up to 60mn for the new value to become effective.
 *
 * Z-Wave Device Class: GENERIC_TYPE_SENSOR_BINARY / SPECIFIC_TYPE_ROUTING_SENSOR_BINARY
 * FGK-101 Raw Description [EndPoint:0] : "0 0 0x2001 0 0 0 c 0x30 0x9C 0x60 0x85 0x72 0x70 0x86 0x80 0x84 0x7A 0xEF 0x2B"
 * Command Classes supported according to Z-Wave Certificate ZC08-14070004 for FGK-101\US :
 * 	Used in Handler :
 *		- 0x20 - 32  : BASIC					V1
 *		  0x30 - 48  : SENSOR_BINARY			!V1! V2
 *		- 0x31 - 49  : SENSOR_MULTILEVEL		V1 !V2! V3 V4 V5
 *		- 0x56 - 86  : CRC_16_ENCAP				V1
 *		  0x60 - 96  : MULTI_CHANNEL			V3
 *		  0x70 - 112 : CONFIGURATION			V1 !V2!
 *		  0x72 - 114 : MANUFACTURER_SPECIFIC 	V1 !V2!
 *		  0x80 - 128 : BATTERY					V1
 *		  0x84 - 132 : WAKE_UP					V1 !V2!
 *		  0x85 - 133 : ASSOCIATION				V1 !V2!
 *		  0x86 - 134 : VERSION					V1
 *		  0x98 - 152 : SECURITY					V1 [only latest versions of FGK-101]
 *		  0x9C - 156 : SENSOR_ALARM				V1
 *	NOT used in Handler :
 *		  0x2B - 43  : SCENE_ACTIVATION			V1	
 *
 *	 also found in FGK-101 Raw Description, in addition to Z-Wave Certificate for FGK-101\US [?!!] :
 *		+ 0x7A - 122 : FIRMWARE_UPDATE_MD		V1 V2
 *		+ 0xEF - 239 : MARK  					V1
 *
 * Version Control:
 *
 *     0.1 - 2022-09-21 - Initial port to Hubitat of @geejiit Github code for ST by christi999@hubitat
 *
 *
 *
 ******************************************************************************************************************************/
public static String version()      {  return "0.1"  }
metadata {
	definition (name: "JJ's Fibaro FGK-101 Handler", namespace: "JJG2014", author: "Jean-Jacques GUILLEMAUD") {
		capability "Contact Sensor"
		capability "Battery"
		capability "Configuration"
		capability "Temperature Measurement"
		capability "Sensor"
		capability "Alarm"
        
        command "reportNext", ["string"]
        command "test"

        
        attribute "reportASAP", "number"
        attribute "deviceTime", "number"
		attribute "driverVersion",  "string"   
        

        // FGK-101 Raw Description [EndPoint:0] : "0 0 0x2001 0 0 0 c 0x30 0x9C 0x60 0x85 0x72 0x70 0x86 0x80 0x84 0x7A 0xEF 0x2B"
		fingerprint deviceId: "0x2001", inClusters: "0x30, 0x60, 0x70, 0x72, 0x80, 0x84, 0x85, 0x9C"  // should include "0x20, 0x31" too ?!!
	}
	preferences {
   		input name:"wakeUpInterval", type:"number", title: "<b>Wake-up Interval?</b>", description:"seconds (default: 3600)", defaultValue:3600, range: "0..65535", required: true   //Actual min max???
        
   		input name:"param1", type:"number",  title: "<b>Param. 1 - Input IN alarm cancellation delay?</b>", description:"seconds (default: 0)", defaultValue:0, range: "0..65535", required: true
   		input "param2", "enum",              title: "<b>Param. 2 - Status change signalled by LED?</b>", description:"(default: 0 - OFF)", options: [0:"OFF",1:"ON"], defaultValue:0, required: true
   		input "param3", "enum",              title: "<b>Param. 3 - Type of IN input?</b>", description:"(default: 0 – INPUT_NC)", options: [0:"INPUT_NC (Normal Close)", 1:"INPUT_NO (Normal Open)", 2:"INPUT_MONOSTABLE", 3:"INPUT_BISTABLE"], defaultValue:0, required: true
   		input "param5", "enum",              title: "<b>Param. 5 - Type of control frame transmitted for association group 1?</b>", description:"(Default value: 255 – BASIC SET)", options: [0:"ALARM GENERIC frame", 1:"ALARM SMOKE frame", 2:"ALARM CO frame",3:"ALARM CO2 frame",4:"ALARM HEAT frame",5:"ALARM WATER frame",255:"Control frame BASIC_SET"], defaultValue:255, required: true
   		input name:"param7",type:"number",   title: "<b>Param. 7 - forced level of dimming/opening ?</b>", description:"1-99 or 255 (default: 255 - activate to previous level)", defaultValue:255, range: "1..255", required: true   // need changes to limit values to valid range...
   		input "param9", "enum",              title: "<b>Param. 9 - Deactivating transmission of the alarm cancelling frame or the control frame deactivating the device?</b>", description:"(default: 0, Sent)", options: [0:"Sent",1:"Not Sent"], defaultValue:0, required: true
   		input name:"param12", type:"number", title: "<b>Param. 12 - Sensitivity to temperature changes?</b>", description:"In 1/16 degC units, If the value is set to 0 and wake-up interval is set to 255 seconds, temperature report will be sent according to the interval. If the value is set to 0 and the wake-up interval is set to over 255, temperature report will be sent each ca. 4 minutes (default: 8 [0.5degC])", defaultValue:8, range: "0..255", required: true
   		input "param13", "enum",             title: "<b>Param. 13 - Sending an alarm or control frame (for IN input, depending on parameter no.5 value), and TMP button alarm frame?</b>", description:"(default: 0, IN and TMP Broadcast mode inactive)", options: [0:"IN and TMP Broadcast mode inactive", 1:"IN broadcast mode active, TMP broadcast mode inactive", 2:"IN broadcast mode inactive, TMP broadcast mode active", 3:"IN and TMP broadcast mode active"], defaultValue:0, required: true         
   		input "param14", "enum",             title: "<b>Param. 14 - Scene activation functionality?</b>", description:"(default: 0 - functionality deactivated)", options: [0:"functionality deactivated", 1:"functionality activated"], defaultValue:0, required: true
        
		input name:"sensorOffset", type:"decimal", title:"<b>Sensor Temperature Offset</b>", description:"degrees", defaultValue:0.0, range: "-20..20", required: true
        input name: "debugOutput",   type: "bool", title: "<b>Enable debug logging?</b>",   description: "<br>", defaultValue: true , required: true        
		input "debugLevel", "enum", title: "<b>Debug Level?</b>", options: [1:"1",2:"2",3:"3"], defaultValue: 1, required: true
    }
}

//---------------------------
//
//---------------------------
def test() {
    parse("zw device: 14, command: 8407, payload: , isMulticast: false")
    parse("zw device: 16, command: 600D, payload: 02 02 31 05 01 44 00 00 0B 79 , isMulticast: false")
    parse("zw device: 16, command: 2001, payload: 00 , isMulticast: false")                             // Basic set
    parse("zw device: 16, command: 3003, payload: 00 , isMulticast: false")                               //SensorBinaryReport
    parse("zw device: 16, command: 2001, payload: FF , isMulticast: false")
    parse("zw device: 16, command: 3003, payload: FF , isMulticast: false")
    parse("zw device: 14, command: 7006, payload: 0F 01 00 , isMulticast: false")
    parse("zw device: 14, command: 9C02, payload: 14 00 FF 00 00 , isMulticast: false")
    parse("zw device: 14, command: 9C02, payload: 14 00 00 00 00 , isMulticast: false")
    parse("zw device: 16, command: 600D, payload: 02 02 31 05 01 44 00 00 05 79 , isMulticast: false")    
}


//---------------------------
//
//---------------------------
def parse(String description) {
		state.parseCount=state.parseCount+1
		logDebug 1, "--------------------------Parsing... ; state.parseCount: ${state.parseCount}--------------------------"
    
		logDebug 2, "Parsing... '${description}'"
        def result = null
        def cmd = zwave.parse(description, [0x20:1, 0x30:1, 0x31:2, 0x56:1, 0x60:3, 0x70:2, 0x72:2, 0x80:1, 0x84:2, 0x85:2, 0x9C:1])
        if (cmd) {
                result = zwaveEvent(cmd)
                logDebug 1, "Parsed ${cmd} to ${result.inspect()}"
        } else {
                logDebug 3, "Non-parsed event: ${description}"
        }
        return result
}


//---------------------------
//
//---------------------------
def temperatureScaleFC(tempvalue) {
	//FGK-101 is natively °C; convert to °F if selected in settings
	def float tempFC = tempvalue
	if (location.temperatureScale == "F") {
		tempFC = tempvalue * 1.8 + 32
	}
	return tempFC
}

//---------------------------
//
//---------------------------
def wakeUpResponse(cmdBlock) {
	//Initialization... (executed only once, when the Handler has been updated)
    //All untouched parameters are supposed to be DEFAULT (as factory-set)
     if (state.isInitialized == false) {
    	logDebug 2, "state.isInitialized : ${state.isInitialized}"
        cmdBlock << zwave.wakeUpV2.wakeUpIntervalSet(seconds:wakeUpInterval, nodeid:zwaveHubNodeId).format() // NB : may have to wait 60mn for that value to be refreshed !
        cmdBlock << "delay 1200"
        // NOTE : any asynchronous temperature query thru SensorMultilevelGet() does NOT reset the delta-Temp base value (managed by DS18B20 hardware)
        // Adjust temperature report sensitivity for outside thermometers whose displayName starts with "*"
        cmdBlock << zwave.configurationV2.configurationSet(parameterNumber: 1, size: 2, configurationValue: (1..0).collect { (param1.toInteger() >> (it * 8)) & 0xFF}).format()
        cmdBlock << "delay 1200"     
        cmdBlock << zwave.configurationV2.configurationSet(parameterNumber: 2, size: 1, configurationValue: [param2.toInteger()]).format()
        cmdBlock << "delay 1200"     
        cmdBlock << zwave.configurationV2.configurationSet(parameterNumber: 3, size: 1, configurationValue: [param3.toInteger()]).format()
        cmdBlock << "delay 1200"     
        cmdBlock << zwave.configurationV2.configurationSet(parameterNumber: 5, size: 1, configurationValue: [param5.toInteger()]).format()
        cmdBlock << "delay 1200"     
        cmdBlock << zwave.configurationV2.configurationSet(parameterNumber: 7, size: 1, configurationValue: [param7.toInteger()]).format()
        cmdBlock << "delay 1200"     
        cmdBlock << zwave.configurationV2.configurationSet(parameterNumber: 9, size: 1, configurationValue: [param9.toInteger()]).format()
        cmdBlock << "delay 1200"     
        cmdBlock << zwave.configurationV2.configurationSet(parameterNumber: 12, size: 1, configurationValue: [param12.toInteger()]).format()
        cmdBlock << "delay 1200"     
        cmdBlock << zwave.configurationV2.configurationSet(parameterNumber: 13, size: 1, configurationValue: [param13.toInteger()]).format()
        cmdBlock << "delay 1200"     
        cmdBlock << zwave.configurationV2.configurationSet(parameterNumber: 14, size: 1, configurationValue: [param14.toInteger()]).format()
        cmdBlock << "delay 1200"     
        // inclusion of Device in Association#3 is needed to get delta-Temperature notification messages [cf Parameter#12 above]
        cmdBlock << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format()
        cmdBlock << "delay 1200"
        // inclusion of Device in Association#2 is needed to enable SensorAlarmReport() Command [anti-Tampering protection]
        cmdBlock << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]).format()
        cmdBlock << "delay 1200"
        // inclusion of Device in Association#4 is needed for backward compatibility with non Z-Wave+ controlers
        cmdBlock << zwave.associationV2.associationSet(groupingIdentifier:4, nodeId:[zwaveHubNodeId]).format()
        cmdBlock << "delay 1200"
        // inclusion of Device in Association#5 is needed for backward compatibility with non Z-Wave+ controlers
        cmdBlock << zwave.associationV2.associationSet(groupingIdentifier:5, nodeId:[zwaveHubNodeId]).format()
        cmdBlock << "delay 1200"
        // Firmware Version
        cmdBlock << zwave.versionV1.versionGet().format()
        cmdBlock << "delay 1200"
        state.isInitialized = true
        logDebug 2, "state.isInitialized : ${state.isInitialized}"
    }
    
	//Regular Commands...
    def long nowTime = new Date().getTime()
    // Next line needed because "update()" does not seem to work anymore
    state.batteryInterval = (long) (24*60-45)*60*1000  // 1 day
    if (nowTime-state.lastReportBattery > state.batteryInterval) {
		cmdBlock << zwave.batteryV1.batteryGet().format()
        cmdBlock << "delay 1200"
        //next 2 lines redondant since any open/closed status change is asynchronously notified... but useful in case of missing basicSet notification
    	cmdBlock << zwave.basicV1.basicGet().format()
    	cmdBlock << "delay 1200"
    }

    cmdBlock << zwave.wakeUpV2.wakeUpIntervalGet().format() // NB : may have to wait 60mn for that value to be refreshed !
    cmdBlock << "delay 1200"
    cmdBlock << zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint: 2, destinationEndPoint: 2, commandClass:0x31/*Sensor Multilevel*/, command:4/*Get*/).format()
    cmdBlock << "delay 1200"
    cmdBlock << zwave.wakeUpV2.wakeUpNoMoreInformation().format()
    cmdBlock << "delay 2000"
    
    logDebug 2, "wakeUpNoMoreInformation()"
    logDebug 2, "cmdBlock : ${cmdBlock}"
    
    return cmdBlock
}

//---------------------------
//
//---------------------------
def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpNotification cmd) {
		// IMPORTANT NOTE : when the batteryLevel becomes too low, Device reports become erratic, all periodic wakeUpNotifications stop
        // and consequently BATTERYLEVEL IS NOT UPDATED ANYMORE every 24 hours, continuing to display the last (and obsolete) reported value.
        // Curiously, asynchronous sensorMultilevelReports continue to arrive, for some time, making the Device look (partially) "alive"
    	logDebug 2, "wakeupv2.WakeUpNotification $cmd"
        def event = sendEvent(descriptionText: "${device.displayName} woke up", isStateChange: true, displayed: false)
        def cmdBlock = []
        cmdBlock=wakeUpResponse(cmdBlock)
        return [event, response(cmdBlock)]
}

//---------------------------
//
//---------------------------
def zwaveEvent(hubitat.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) {
	// IMPORTANT NOTE : when the batteryLevel becomes too low, Device reports become erratic, all periodic wakeUpNotifications stop
	// and consequently BATTERYLEVEL IS NOT UPDATED ANYMORE every 24 hours, continuing to display the last (and obsolete) reported value.
	// Curiously, asynchronous sensorMultilevelReports continue to arrive, for some time, making the Device look (partially) "alive"
	// This section resets the displayed battery level to 1% when the battery level is obsolete by more than 48h.
    state.batteryInterval = (long) (24*60-45)*60*1000  // 1 day
    def long nowTime = new Date().getTime()
    if (nowTime-state.lastReportBattery > 3*state.batteryInterval) {  // reset batteryLevel to 1% if no update for 48-72 hours
    	logDebug 3, "obsolete (likely low) battery value : ${((nowTime-state.lastReportBattery)/3600000)} hours old"
        sendEvent(name: "battery", displayed: true, isStateChange:true, unit: "%", value: 1, descriptionText: "${device.displayName} has a low battery")
	    state.lastReportBattery = nowTime
	}
			//  Dirty temporary recovery fix for remote Devices which lost wakeUp capability but still get asynchromous SensorMultilevelReports
			//  Forcing with the magnet a close/open transition after replacing the battery should (in most cases...) restore wakeUps
                //def cmdBlock = []
        		//cmdBlock=wakeUpResponse(cmdBlock)
        		//return [response(cmdBlock)]
        		//configure()
        def float scaledSensorValue = cmd.scaledSensorValue
    
        scaledSensorValue = scaledSensorValue + 1.0*sensorOffset 
    
        //Round to nearest 1 decimal temperature value; convert to °F if needed
        def float ftempSign = temperatureScaleFC(scaledSensorValue) < 0 ? -1 : +1
		def float ftemp = ftempSign * ((((temperatureScaleFC(scaledSensorValue).abs()*100+5)/10).intValue()*1.0)/10)
        	logDebug 2, "ftempSign : ${ftempSign}"
        	logDebug 2, "ftemp : ${ftemp}"

    // Next line needed because "update()" does not seem to work anymore
    	state.maxEventInterval = (long) (4*60-10)*60*1000  // at least 1 Temperature Report event every 4 hours
        nowTime = new Date().getTime()
       	logDebug 2, "cmd.scaledSensorValue : ${cmd.scaledSensorValue}"
       	logDebug 2, "correction : ${scaledSensorValue-cmd.scaledSensorValue}"
   		logDebug 2, "device.displayName : ${device.displayName}"
   		logDebug 2, "'Date().getTime()' : ${new Date().getTime()}"
        logDebug 2, "state.forcedWakeUp : ${state.forcedWakeUp}"
        logDebug 2, "state.maxEventInterval : ${state.maxEventInterval}"
   		logDebug 2, "state.lastReportTime : ${state.lastReportTime}"
   		logDebug 2, "nowTime : ${nowTime}"
   		logDebug 2, "(nowTime-state.lastReportTime > state.maxEventInterval) : ${(nowTime-state.lastReportTime > state.maxEventInterval)}"
   		logDebug 2, "ftemp : ${ftemp}"
        logDebug 2, "state.lastReportedTemp: ${state.lastReportedTemp}"

        def float tempQuantum
        tempQuantum = temperatureScaleFC(param12.toFloat()/16.0)-temperatureScaleFC(0)
        logDebug 1, "((ftemp-state.lastReportedTemp).abs()>${tempQuantum}): ${(ftemp-state.lastReportedTemp).abs()>tempQuantum}"
        if (((ftemp-state.lastReportedTemp).abs()>tempQuantum) || ((nowTime-state.lastReportTime) > state.maxEventInterval) || state.forcedWakeUp) {
        	def map = [ displayed: true, value: ftemp.toString(), isStateChange:true, linkText:"${device.displayName}" ]
        	switch (cmd.sensorType) {
                case 1:
                        map.name = "temperature"
                        map.unit = cmd.scale == 1 ? "F" : "C"
                        //ignores Device's native temperature scale, ftemp already converted to °F if settings as such
                        map.unit = location.temperatureScale
                        logDebug 1, "map.value : ${map.value}"
                        logDebug 1, "map.unit : ${map.unit}"
                        break;
        	}
            logDebug 2, "temperature Command : ${map.inspect()}"
        	
        	state.lastReportedTemp = ftemp
            state.lastReportTime = nowTime
            state.forcedWakeUp = false
            // For Test purpose; redondant with reportNext() => state.forcedWakeUp=1
            if (device.currentValue('reportASAP')==1) {sendEvent(name: "reportASAP", value: 0, isStateChange: true)}
        	return sendEvent(map)
        }
}

//---------------------------
//
//---------------------------
def sensorValueEvent(value) {
	if (value) {
		sendEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open")
	} else {
		sendEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed")
	}
}

//---------------------------
// BasicReport should not be necessary since all status change notifications are asynchronous via BasicSet
// But useful as defensive programming, in case of missed notifications, to make sure latest change has been properly reported and registered
//---------------------------
def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) {
	sensorValueEvent(cmd.value)
    logDebug 2, "basicv1.BasicReport $cmd.value"
    def cmdValue = cmd.value
	return openClosed(cmd, cmdValue)
}

//---------------------------
// To check that WakeUpInterval does not revert to 1mn instead of 1h
//---------------------------

def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpIntervalReport cmd) {
    logDebug 2, "WakeUpIntervalReport $cmd"
    if (cmd.seconds!=wakeUpInterval) {
    	def result = sendEvent(name:"WakeUpIntervalReport", value:"${cmd.seconds}", descriptionText:"${device.displayName} had ${cmd.seconds} seconds wakeUp period", isStateChange:true, displayed:true, linkText:"${device.displayName}")
   		configure()
    }
    return result
}

//---------------------------
//
//---------------------------
def openClosed(cmd, cmdValue) {
    def theState = cmdValue == 0 ? "closed" : "open"
    logDebug 2, "openClosed $cmd"
    // Use closed/open sensor notification to trigger push of updated Temperature value and immediate setting of updated device parameters
    // Sometimes, Temperature forced refresh stops working : SensorMultilevelGet() Commands are stacked but not executed immediately;
    // will restart after some time, and stacked Commands will be executed !
    def event = sendEvent(name:"contact", value:"${theState}", descriptionText:"${device.displayName} is ${theState}", isStateChange:true, displayed:true, linkText:"${device.displayName}")
    state.forcedWakeUp = true
    def cmdBlock = []
    cmdBlock=wakeUpResponse(cmdBlock)
    return [event, response(cmdBlock)]
}
    
//---------------------------
//
//---------------------------
def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) {
    logDebug 2, "basicv1.BasicSet $cmd"
    def cmdValue = cmd.value
	return openClosed(cmd, cmdValue)
}

//---------------------------
// SensorBinaryReport should never occur since all status change notifications are asynchronous via BasicSet
//---------------------------
def zwaveEvent(hubitat.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) {
    logDebug 2, "sensorbinaryv1.SensorBinaryReport $cmd"
    def cmdValue = cmd.sensorValue
	return openClosed(cmd, cmdValue)
}

//---------------------------
//
//---------------------------
def zwaveEvent(hubitat.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) {
	//def event = sensorValueEvent(cmd.sensorState)
    logDebug 2, "sensoralarmv1.SensorAlarmReport $cmd.sensorState"
    def event = sendEvent(name:"alarm", value:"$cmd.sensorState", descriptionText:"${device.displayName} is tampered with !", isStateChange:true, displayed:true, linkText:"${device.displayName}")
    def cmdBlock = []
    state.forcedWakeUp = true
    cmdBlock=wakeUpResponse(cmdBlock)
    return [event, response(cmdBlock)]
}


//---------------------------
//
//---------------------------
def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) {
    // Next line needed because "update()" does not seem to work anymore
    state.batteryInterval = (long) (24*60-45)*60*1000  // 1 day
    def long nowTime = new Date().getTime()
   	logDebug 2, "batteryv1.BatteryReport ${cmd.batteryLevel}"
   	logDebug 2, "nowTime : ${nowTime}"
   	logDebug 2, "state.lastReportBattery : ${state.lastReportBattery}"
   	logDebug 2, "state.batteryInterval : ${state.batteryInterval}"
    logDebug 2, "state.forcedWakeUp : ${state.forcedWakeUp}"

    if ((nowTime-state.lastReportBattery > state.batteryInterval) || state.forcedWakeUp) {
		def map = [ name: "battery", displayed: true, isStateChange:true, unit: "%" ]
		if (cmd.batteryLevel == 0xFF) {
			map.value = 1
			map.descriptionText = "${device.displayName} has a low battery"
			map.isStateChange = true
		} else {
			map.value = cmd.batteryLevel
		}
    	state.lastReportBattery = nowTime
        logDebug 1, "battery map : ${map}"
    	return [sendEvent(map)]
    }
}

//---------------------------
//
//---------------------------
def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) {
    logDebug 2, "ConfigurationReport - Parameter#${cmd.parameterNumber}: ${cmd.configurationValue}"
}

//---------------------------
//
//---------------------------
def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelEndPointReport cmd) {
    logDebug 2, "multichannelv3.MultiChannelCapabilityReport: ${cmd}"
}

//---------------------------
//
//---------------------------
def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) {
    logDebug 2, "multichannelv3.MultiChannelCapabilityReport: ${cmd}"
}
 
//---------------------------
//
//---------------------------
def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) {
    logDebug 2, "versionv1.VersionReport: ${cmd}"
	BigDecimal fw = cmd.firmware0Version //applicationVersion
	fw = fw + cmd.firmware0SubVersion/10 // applicationSubVersion / 100
    
	state.firmware = fw
}


//---------------------------
// MultiChannelCmdEncap and MultiInstanceCmdEncap are ways that devices can indicate that a message
// is coming from one of multiple subdevices or "endpoints" that would otherwise be indistinguishable
//---------------------------
def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
        def encapsulatedCommand = cmd.encapsulatedCommand([0x30: 1, 0x31: 2]) // can specify command class versions here like in zwave.parse
        logDebug 2, ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}")
        if (encapsulatedCommand) {
                return zwaveEvent(encapsulatedCommand)
        }
}

//---------------------------
//
//---------------------------
// Catch All command Handler in case of unexpected message
def zwaveEvent(hubitat.zwave.Command cmd) {
	sendEvent(descriptionText: "!!! $device.displayName: ${cmd}", displayed: false)
}

//---------------------------
// When a Temperature Event got lost in transit, the Watchdog requests a forced report at next wake up
// The "reportNext()" alarm command is used to signal back from the Watchdog SmartApp to the sleepy Device Handler
//---------------------------
def reportNext(commandMsg) {
	logDebug 3, "reportNext !"
    logDebug 3, "commandMsg : ${commandMsg}"
    state.forcedWakeUp = true
		// IMPORTANT NOTE : when the batteryLevel becomes too low, Device reports become erratic, all periodic wakeUpNotifications stop
        // and consequently BATTERYLEVEL IS NOT UPDATED ANYMORE every 24 hours, continuing to display the last (and obsolete) reported value.
        // Curiously, asynchronous sensorMultilevelReports continue to arrive, for some time, making the Device look (partially) "alive"
    	// This section resets the displayed battery level to 1% when the battery level is obsolete by more than 48h.
	// Next line may be needed because "update()" does not seem to work reliably anymore
    state.batteryInterval = (long) (24*60-45)*60*1000  // 1 day
    def long nowTime = new Date().getTime()
    if (nowTime-state.lastReportBattery > 3*state.batteryInterval) {  // reset batteryLevel to 1% if no update for 48-72 hours
    	logDebug 3, "obsolete (likely low) battery value : ${((nowTime-state.lastReportBattery)/3600000)} hours old"
        sendEvent(name: "battery", displayed: true, isStateChange:true, unit: "%", value: 1, descriptionText: "${device.displayName} has a low battery")
	    state.lastReportBattery = nowTime
	}
	return []
}

///////////////////
// For Tests Purpose
///////////////////

//---------------------------
// Executed each time the Handler is updated
//---------------------------
def updated() {
	logDebug 1, "Updated !"
    
    logDebug 1, "param1 = $param1"
    logDebug 1, "param2 = $param2"
    logDebug 1, "param3 = $param3"
    logDebug 1, "param5 = $param5"
    logDebug 1, "param7 = $param7"
    logDebug 1, "param9 = $param9"
    logDebug 1, "param12 = $param12"
    logDebug 1, "param13 = $param13"
    logDebug 1, "param14 = $param14"
    
    
    // All state.xxx attributes are Device-local, NOT Location-wide
    state.isInitialized = false
    state.lastReportedTemp = (float) -1000
    state.lastReportTime = (long) 0
    state.lastReportBattery = (long) 0
	// Real-time clock of sensors (ceramic resonator) is up to 3% inaccurate
    state.batteryInterval = (long) (24*60-45)*60*1000  // 1 Battery Report event every 24 hours, rounded up to the nearest hourly wakeup
    state.maxEventInterval = (long) (4*60-10)*60*1000  // at least 1 Temperature Report event every 3:50 hours
    state.parseCount=(int) 0
    state.forcedWakeUp = true
    if (!(state.deviceID)) {state.deviceID = device.name}
    logDebug 1, "state.deviceID: ${state.deviceID}"
    logDebug 1, "state.batteryInterval : ${state.batteryInterval}"
    logDebug 1, "state.maxEventInterval : ${state.maxEventInterval}"
    // For Test purpose; redondant with reportNext() => state.forcedWakeUp=1
    sendEvent(name: "reportASAP", value: 1, isStateChange: true)
    logDebug 1, "device.currentValue('reportASAP') : ${device.currentValue('reportASAP')}"

    infos()
}

//---------------------------
//
//---------------------------
def installed()
{
	logDebug 1, "installed"
	state.driverVersion = "${version()}"
}

//---------------------------
// If you add the Configuration capability to your device type, this command will be called right
// after the device joins to set device-specific configuration commands.
//---------------------------
def configure() {
	logDebug 1, "Configuring..."
    logDebug 1, "device.displayName.substring(0,1) : ${device.displayName.substring(0,1)}"
	delayBetween([
		// Make sure sleepy battery-powered sensors send their WakeUpNotifications to the hub
		zwave.wakeUpV2.wakeUpIntervalSet(seconds:wakeUpInterval, nodeid:zwaveHubNodeId).format(),
		// NOTE : any asynchronous temperature query thru SensorMultilevelGet() does NOT reset the delta-Temp base value (managed by DS18B20 hardware)
        zwave.configurationV2.configurationSet(parameterNumber: 1, size: 2, configurationValue: (1..0).collect { (param1.toInteger() >> (it * 8)) & 0xFF}).format(),
        zwave.configurationV2.configurationSet(parameterNumber: 2, size: 1, configurationValue: [param2.toInteger()]).format(),
        zwave.configurationV2.configurationSet(parameterNumber: 3, size: 1, configurationValue: [param3.toInteger()]).format(),
        zwave.configurationV2.configurationSet(parameterNumber: 5, size: 1, configurationValue: [param5.toInteger()]).format(),
        zwave.configurationV2.configurationSet(parameterNumber: 7, size: 1, configurationValue: [param7.toInteger()]).format(),
        zwave.configurationV2.configurationSet(parameterNumber: 9, size: 1, configurationValue: [param9.toInteger()]).format(),
        zwave.configurationV2.configurationSet(parameterNumber: 12, size: 1, configurationValue: [param12.toInteger()]).format(),
        zwave.configurationV2.configurationSet(parameterNumber: 13, size: 1, configurationValue: [param13.toInteger()]).format(),
        zwave.configurationV2.configurationSet(parameterNumber: 14, size: 1, configurationValue: [param14.toInteger()]).format(),
        
        // inclusion of Device in Association#3 is needed to get delta-Temperature notification messages [cf Parameter#12 above]
        zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format(),
        // inclusion of Device in Association#2 is needed to enable SensorAlarmReport() Command [anti-Tampering protection]
        zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]).format(),
        // get zwave version information
        zwave.versionV1.versionGet().format()
	],1200)
}

//---------------------------
//
//---------------------------
def infos() {
	if (!state.devices) { state.devices = [:] }
    logDebug 1, "zwaveHubNodeId: ${zwaveHubNodeId}"					// -> "1"
    logDebug 1, "device.displayName: ${device.displayName}"			// -> "JJG"
    logDebug 1, "device.id: ${device.id}"							// -> "75841488-ae76-4cac-b523-a2694e72c25a"
    logDebug 1, "device.name: ${device.name}"						// -> "T001"
    logDebug 1, "device.label: ${device.label}"						// -> "JJG"
    logDebug 1, "device.data: ${device.data}"   					// -> "[endpointId:0, version: 2.1, MSR:010F-0700-2000]"
    //logDebug 1, "'device.rawDescription': ${device.rawDescription}"	// -> "0 0 0x2001 0 0 0 c 0x30 0x9C 0x60 0x85 0x72 0x70 0x86 0x80 0x84 0x7A 0xEF 0x2B"
}

//---------------------------
//
//---------------------------
private logDebug(level, msg) {
	if ((level>=debugLevel.toInteger()) && (settings?.debugOutput || settings?.debugOutput == null)) {
		log.debug "$msg"
	}
}
1 Like