[RELEASE] Xiaomi Aqara Mijia Sensors and Switches Driver

If you want to preserve the trailing zero then it needs to be output as a string, i.e.

String.format("%.2f", number.toFloat())
2 Likes

https://images.app.goo.gl/9a8UYEWRQVfGzUDMA

This is waay over my head. You guys (and gals) amaze me with your programming.

1 Like

a fun discussion of the finer points of BigDecimal.ROUND_HALF_UP

I use Markus' drivers whenever I can, his presence library is awesome for device detection stuff - regardless - much of his work uses this method - as well he accepts a default value for the precision - but it's partly bugged due to his non-handling zeros, hence my needs.

Yea, played with that some, but in general nothing I'm doing with HE requires that level of precision.

Like to ask for a version in the notes or date stamp of last update. There are 3 user drivers and i'm trying to track a problem - I want to see if a different driver gives better results.
Currently I'm using Markus' driver, there is also the @birdslikewires (last updated 3/23).
I'm seeing possible an 8/2021 last revision date on this? is that correct?

Important Edit/Update

I've replaced my (ultimately determined unsuccessful) code at the end of this post with an updated version created by Zen Master @kkossev where Health Status is now fully functional!

Thanks again to Krassimir, I think that makes it about a zillion times I/we have thanked him for all his amazing contributions to this community! Scroll to the bottom of this post for his updated version. Krassimir is contacting @chirpy about getting the the updated code integrated back into his github. For now you can pull it from this post.

Original post

I've spoken to @chirpy today and received his permission to post an updated version of his driver, he is happy to see his code continuing to be useful. But...don't get too excited, since it was updated by me. :wink: I'm not a coder by any means, more an occasional light-weight hacker of existing code.

I changed the driver's Presence monitoring to Health monitoring, in part to work w/apps like Device Health Status, and in general because it seems like Zigbee devices are moving towards using Health rather than Presence. @thebearmay was nice enough to take a quick look at the modified code and and identified a few key changes that I implemented, thanks very much to him. Any potential remaining faults/issues are solely mine. :slight_smile:

This is currently a "local" release only (code here in this post) - don't use the URL that Chirpy included in the driver code to install or update the driver via the Import option in HE. Just copy/paste from this post.

I'm using this modified driver on my C8 and C7 with my Aqara contact and leak sensors, and Aqara buttons, and it seems to work as expected. Please try it out, and free free to suggest or offer any additional code changes if you see anything that you feel might improve the driver.

\\ End of original post

=============================

4/23/2024 - Updated code with working Health Status - courtesy of @kkossev !

/**
 *  Xiaomi Aqara Mijia Sensors and Switches:
 *
 *  Xiaomi Aqara Contact Sensor				: MCCGQ11LM [*]
 *  Xiaomi Aqara Motion Sensor				: RTCGQ11LM [*]
 *  Xiaomi Aqara Temperature Sensor			: WSDCGQ11LM
 *  Xiaomi Aqara Vibration Sensor			: DJT11LM [*]
 *  Xiaomi Aqara Water Leak Sensor			: SJCGQ11LM [*]
 *  Xiaomi Aqara Wireless Double Remote Switch		: WXKG02LM [*]
 *  Xiaomi Aqara Wireless Mini Switch			: WXKG11LM
 *  Xiaomi Aqara Wireless Mini Switch with Gyroscope	: WXKG12LM [*]
 *  Xiaomi Aqara Wireless Single Remote Switch		: WXKG03LM [*]
 *  Xiaomi Mijia Door and Window Sensor			: MCCGQ01LM
 *  Xiaomi Mijia Human Body Sensor			: RTCGQ01LM
 *  Xiaomi Mijia Light Sensor				: GZCGQ01LM
 *  Xiaomi Mijia Wireless Switch			: WXKG01LM [*]
 *
 *  [*] These devices have an internal temperature sensor, though with only rough accuracy
 *
 *  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.
 *
 *  Changelog:
 *
 *  v0.2.1 - healthStatus fixes and improvements (@kkossev) 2024-04-23
 *
 *  v0.2.0 - Changed Presence to Health (@Danabw) 2023-10-23
 *
 *  v0.18 - Added the ability to pass a button number and default none to 1 - thanks to @FourEyedPanda
 *          Added the release command to the available list
 *
 *  v0.17 - Check device model is set before interrogating it
 *
 *  v0.16 - Added decoding of the (ir)regular device data for devices that use
 *          the 4C FF02 encoding (i.e. Xiaomi Mijia Door and Window Sensor)
 *
 *  v0.15 - Update contact/water sensors from the (ir)regular device data
 *          updates in case of sensor bounce leaving it in an incorrect state
 *          Added VoltageMeasurement as a capability
 *
 *  v0.14 - Improvements to Presence detection - You should check each device
 *          and hit Configure to ensure each device has the presenceTracker job
 *          schedules
 *          Changed from BETA to RELEASE
 *
 *  v0.13 - Modified Temperature Offset setting to float to allow for decimal corrections (e.g. 2.5)
 *          Fixed virtual Release button number assignment
 *
 *  v0.12 - Added more presence intervals
 *          Added device Commands, however they are all commented out by default
 *          Removed unnecessary "isStateChange:true" for all devices apart from buttons
 *          Added fingerprint for WXKG02LM
 *          Modified Held duration setting to float to allow for milliseconds (e.g 0.5 = 500 milliseconds)
 *          Added configurable virtual Release event for WXKG02LM and WXKG03LM
 *          Added setting to allow WXKG01LM to represent 5 buttons instead of a single button with multiple states
 *
 *  v0.11 - Modified presence to only update if previously not present or set
 *          Added a temperature offset setting
 *          Added experimental support for an internal temperature sensor that some of these devices contain (see above [*])
 *
 *  v0.10 - Added motion to contact sensors via an option for those want it
 *          Fixed button pushed status for WXKG01LM
 *
 *  v0.09 - Added support for WXKG11LM
 *
 *  v0.08 - Added simple presence tracking that checks the devices presence and will change state if no data receieved
 *          Added support for MCCGQ01LM
 *          Added safeguard limits to humidity, pressure and temperature measurements
 *          Fixed the pressure unit description to hPa
 *
 *  v0.07 - Added support for WXKG01LM
 *          Added support for WXKG12LM
 *          Added support for SJCGQ11LM
 *          Removed unnecessary button scheduled reset
 *          Standardised the info logging text
 *
 *  v0.06 - Added battery level detection for older Xiaomi sensors
 *          Fixed lux calculation for RTCGQ11LM
 *
 *  v0.05 - Added workaround for Xiaomi data structure oddities
 *          Added fingerprint for RTCGQ01LM
 *
 *  v0.04 - Fixed temperature calculation for negative temps
 *
 *  v0.03 - Fix for spurious voltage calculation from device data
 *
 *  v0.02 - Added state and schedule cleanup to configure command if you move from an old driver
 *
 *  v0.01 - Initial public release
 *
 */

import hubitat.zigbee.zcl.DataType
import hubitat.helper.HexUtils

static String version() { '0.2.1 2024/04/23' }

metadata {
	definition (name: "Xiaomi Aqara Mijia Sensors and Switches (w/ healthStatus)", namespace: "waytotheweb", author: "Jonathan Michaelson", importUrl: "https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Misc/Xiaomi_Aqara_Mijia_Sensors_and_Switches_w_healthStatus.groovy") {
		capability "Battery"
		capability "VoltageMeasurement"
		capability "Sensor"
		capability "Refresh"
		capability "Configuration"
		capability "HealthCheck"    // Added 2023-10-23

		capability "IlluminanceMeasurement"
		capability "RelativeHumidityMeasurement"
		capability "TemperatureMeasurement"
		capability "PressureMeasurement"
		capability "AccelerationSensor"
		capability "MotionSensor"
		capability "ContactSensor"
		capability "WaterSensor"

		capability "PushableButton"
		capability "HoldableButton"
		capability "DoubleTapableButton"
		capability "ReleasableButton"

		attribute "tilt", "string"
		attribute "taps", "number"
		attribute "released", "number"
		attribute "shaken", "number"
		attribute "temperature", "number"
        attribute 'healthStatus', 'enum', ['offline', 'online', 'unknown']

// If you want to use these you will have to remove the comment prefix for
// those you want. They are not enabled by default as they can make a mess of
// the Attribute states which can only be cleared by deleting and adding the
// device back. If the driver is updated you will have to comment them out
// again.
// Note: The attribute state will be updated regardless of the previous state.

//		command "open"
//		command "closed"
//		command "active"
//		command "inactive"
//		command "online"
//		command "offline"
//		command "push"
//		command "hold"
//		command "doubleTap"
//		command "release"
//		command "shake"
//		command "wet"
//		command "dry"

		fingerprint profileId: "0104", inClusters: "0000,0400,0003,0001", outClusters: "0003", manufacturer: "LUMI", model: "lumi.sen_ill.mgl01", deviceJoinName: "Xiaomi Mijia Light Sensor"
		fingerprint profileId: "0104", inClusters: "0000,0003,FFFF,0402,0403,0405", outClusters: "0000,0004,FFFF", manufacturer: "LUMI", model: "lumi.weather", deviceJoinName: "Xiaomi Aqara Temperature Sensor"
		fingerprint profileId: "0104", inClusters: "0000,FFFF,0406,0400,0500,0001,0003", outClusters: "0000,0019", manufacturer: "LUMI", model: "lumi.sensor_motion.aq2", deviceJoinName: "Xiaomi Aqara Motion Sensor"
		fingerprint profileId: "0104", inClusters: "0000,FFFF,0406,0400,0500,0001,0003", outClusters: "0000,0019", manufacturer: "LUMI", model: "lumi.sensor_motion", deviceJoinName: "Xiaomi Aqara Motion Sensor"
		fingerprint profileId: "0104", inClusters: "0003,0012", outClusters: "0004,0003,0005,0012", manufacturer: "LUMI", model: "lumi.vibration.aq1", deviceJoinName: "Xiaomi Aqara Vibration Sensor"
		fingerprint profileId: "0104", inClusters: "0000,0003,FFFF,0006", outClusters: "0000,0004,FFFF", manufacturer: "LUMI", model: "lumi.sensor_magnet.aq2", deviceJoinName: "Xiaomi Aqara Contact Sensor"
		fingerprint profileId: "0104", inClusters: "0000,0003,FFFF,0019", outClusters: "0000,0004,0003,0006,0008,0005,0019", manufacturer: "LUMI", model: "lumi.sensor_magnet", deviceJoinName: "Xiaomi Mijia Door and Window Sensor"
		fingerprint profileId: "0104", inClusters: "0000,0003,0019,0012,FFFF", outClusters: "0000,0003,0004,0005,0019,0012,FFFF", manufacturer: "LUMI", model: "lumi.remote.b186acn01", deviceJoinName: "Xiaomi Aqara Wireless Single Remote Switch"
		fingerprint profileId: "0104", inClusters: "0000,0003,0019,0012,FFFF", outClusters: "0000,0003,0004,0005,0019,0012,FFFF", manufacturer: "LUMI", model: "lumi.remote.b286acn01", deviceJoinName: "Xiaomi Aqara Wireless Double Remote Switch"
		fingerprint profileId: "0104", inClusters: "0000,0003,0019,0012,FFFF", outClusters: "0000,0003,0004,0005,0019,0012,FFFF", manufacturer: "LUMI", model: "lumi.sensor_86sw1", deviceJoinName: "Xiaomi Aqara Wireless Single Remote Switch"
		fingerprint profileId: "0104", inClusters: "0000,0003,FFFF,0019", outClusters: "0000,0004,0003,0006,0008,0005,0019", manufacturer: "LUMI", model: "lumi.sensor_switch", deviceJoinName: "Xiaomi Mijia Wireless Switch"
		fingerprint profileId: "0104", inClusters: "0000,FFFF,0006", outClusters: "0000,0004,FFFF", manufacturer: "LUMI", model: "lumi.sensor_switch.aq3", deviceJoinName: "Aqara Wireless Mini Switch with Gyroscope"
		fingerprint profileId: "0104", inClusters: "0000,0012,0006,0001", outClusters: "0000", manufacturer: "LUMI", model: "lumi.sensor_swit", deviceJoinName: "Xiaomi Aqara Wireless Mini Switch"
		fingerprint profileId: "0104", inClusters: "0000,0003,0001", outClusters: "0019", manufacturer: "LUMI", model: "lumi.sensor_wleak.aq1", deviceJoinName: "Xiaomi Aqara Water Leak Sensor"

    }
	preferences {
		input name: "infoLogging", type: "bool", title: "Enable info message logging", description: "", defaultValue: true
		input name: "debugLogging", type: "bool", title: "Enable debug message logging", description: "", defaultValue: false
        input name: "healthStatusEnabled", type: "bool", title: "Enable Health Detection", description: "This will keep track of the devices health and will change state if no data received within the Health Timeout. If it does lose presence try pushing the reset button on the device if available.", defaultValue: true
		input name: "healthHours", type: "enum", title: "Health Timeout", description: "The number of hours before a device is considered 'offline'.<br>Note: Some of these devices only update their battery every 6 hours.", defaultValue: "12", options: ["1","2","3","4","6","12","24"]    //Mod
        input name: "holdDuration", type: "number", title: "Button Hold Duration", description: "For WXKG01LM, this is how long the button needs to be pushed to be in a held state.<br> For WXKG02LM, WXKG03LM it is how long the button needs to be held to register a release state.<br> Time is in seconds, decimals can be used, e.g. 0.5 is 500ms", defaultValue: "1"
		input name: "allButtons", type: "bool", title: "WXKG01LM Button Function", description: "By default, the WXKG01LM is treated as a single button. To enable separate buttons for each press type, enable this option.", defaultValue: false
		input name: "temperatureOffset", type: "number", title: "Temperature Offset", description: "This setting compensates for an inaccurate temperature sensor. For example, set to -7 if the temperature is 7 degress too warm.", defaultValue: "0"
		input name: "internalTemperature", type: "bool", title: "Experimental Internal Temperature", description: "Some of these devices have an internal temperature sensor. It only reports when the battery reports (usually every 50 minutes) and is not very accurate and usually requires an offset.", defaultValue: false
		input name: "motionContact", type: "bool", title: "Add Motion To Contact Sensors", description: "This adds a motion state to contact sensors, i.e. 'contact: open' = 'motion: active'", defaultValue: false
	}
}

def parse(String description) {
	if (debugLogging) log.debug "Incoming data from device : $description"

	if (description?.startsWith("zone status ")) {
		if (description?.startsWith("zone status 0x0001")){
			sendEvent("name": "water", "value": "wet")
			if (infoLogging) log.info "$device.displayName water changed to wet"
		}
		else if (description?.startsWith("zone status 0x0000")){
			sendEvent("name": "water", "value": "dry")
			if (infoLogging) log.info "$device.displayName water changed to dry"
		}
	}
	if (description?.startsWith("read attr -")) {
		def mydescMap = description.split(', ').collectEntries {
			entry -> def pair = entry.split(': ')
			[(pair.first()): pair.last()]
		}
		if (mydescMap.attrId == "FF01" || mydescMap.attrId == "FF02") {
			if (debugLogging) log.debug "Processing Xiaomi data (cluster:$mydescMap.cluster, attrId:$mydescMap.attrId)"
			if (mydescMap.cluster == "0000") {
				def MsgLength = mydescMap.value.size()
				if (MsgLength > 20){
					def batteryVoltage = ""
					if (mydescMap.attrId == "FF01" && mydescMap.value[4..5] == "21"){
						batteryVoltage = mydescMap.value[8..9] + mydescMap.value[6..7]
					}
					else if (mydescMap.attrId == "FF02" && mydescMap.value[8..9] == "21"){
						batteryVoltage = mydescMap.value[12..13] + mydescMap.value[10..11]
					}
					
					if (batteryVoltage != ""){
						batteryEvent(Integer.parseInt(batteryVoltage, 16) / 100)
					}

					if (internalTemperature && mydescMap.attrId == "FF01" && mydescMap.value[10..13] == "0328"){
						rawValue = hexStrToSignedInt(mydescMap.value[14..15])
						if (debugLogging) log.debug "Processing Xiaomi data (internal temperature: $rawValue)"
						def Scale = location.temperatureScale
						if (Scale == "F") rawValue = (rawValue * 1.8) + 32
						if (temperatureOffset == null) temperatureOffset = "0"
						def offsetrawValue = (rawValue + Float.valueOf(temperatureOffset))
						rawValue = offsetrawValue
						if (debugLogging) log.debug "Processing Xiaomi data (internal temperature: $rawValue with $internalTempOffset offset and conversion)"
						if (rawValue > 200 || rawValue < -200){
							if (infoLogging) log.info "$device.displayName Ignored internal temperature value: $rawValue\u00B0"+Scale
						} else {
							sendEvent("name": "temperature", "value": rawValue, "unit": "\u00B0"+Scale)
							if (infoLogging) log.info "$device.displayName internal temperature changed to $rawValue\u00B0"+Scale
						}
					}
					if (mydescMap.attrId == "FF01" && mydescMap.value[-6..-3] == "6410"){
						rawValue = (mydescMap.value[-2..-1]).toInteger()
						if (device.getDataValue("model") != null && getDataValue("model").contains("magnet")){
							if (debugLogging) log.debug "Processing Xiaomi data (contact status) = ${rawValue}"
							def contact = "closed"
							if (rawValue == 1) contact = "open"
							sendEvent("name": "contact", "value": contact)
							if (infoLogging) log.info "$device.displayName contact updated to $contact"
							if (motionContact){
								def motion = "inactive"
								if (rawValue == 1) motion = "active"
								sendEvent("name": "motion", "value": motion)
								if (infoLogging) log.info "$device.displayName motion updated to $motion"
							}
						}
						if (device.getDataValue("model") != null && getDataValue("model").contains("leak")){
							if (debugLogging) log.debug "Processing Xiaomi data (leak status) = ${rawValue}"
							def contact = "dry"
							if (rawValue == 1) contact = "wet"
							sendEvent("name": "water", "value": contact)
							if (infoLogging) log.info "$device.displayName water updated to $contact"
						}
					}
					if (mydescMap.attrId == "FF02" && mydescMap.value[0..5] == "060010"){
						rawValue = (mydescMap.value[6..7]).toInteger()
						if (device.getDataValue("model") != null && getDataValue("model").contains("magnet")){
							if (debugLogging) log.debug "Processing Xiaomi data (contact status) = ${rawValue}"
							def contact = "closed"
							if (rawValue == 1) contact = "open"
							sendEvent("name": "contact", "value": contact)
							if (infoLogging) log.info "$device.displayName contact updated to $contact"
							if (motionContact){
								def motion = "inactive"
								if (rawValue == 1) motion = "active"
								sendEvent("name": "motion", "value": motion)
								if (infoLogging) log.info "$device.displayName motion updated to $motion"
							}
						}
					}
				}
			}
		
		}
		else if (mydescMap.cluster == "0000" && mydescMap.attrId == "0005" &&  mydescMap.encoding == "42"){
			if (debugLogging) log.debug "Processing Xiaomi data (cluster:$mydescMap.cluster, attrId:$mydescMap.attrId, encoding:$mydescMap.encoding)"
			if (mydescMap.value.size() > 60){
				def batteryData = mydescMap.value.split('FF42')[1]
				if (batteryData[4..5] == "21"){
					batteryVoltage = batteryData[8..9] + batteryData[6..7]
					if (batteryVoltage != ""){
						batteryEvent(Integer.parseInt(batteryVoltage, 16) / 100)
					}
				}
			}
		} else {
			def descMap = zigbee.parseDescriptionAsMap(description)

			if (debugLogging) log.debug "Processing Xigbee data (cluster:$descMap.cluster, attrId:$descMap.attrId)"

			if (descMap.cluster == "0001" && descMap.attrId == "0020") {
				batteryEvent(Integer.parseInt(descMap.value,16))
			}
			else if (descMap.cluster == "0400" && descMap.attrId == "0000") {
				def rawEncoding = Integer.parseInt(descMap.encoding, 16)
				def rawLux = Integer.parseInt(descMap.value,16)
				def lux = rawLux > 0 ? Math.round(Math.pow(10,(rawLux/10000)) - 1) : 0
				if (getDataValue("model") == "lumi.sensor_motion.aq2") lux = rawLux
				sendEvent("name": "illuminance", "value": lux, "unit": "lux")
				if (infoLogging) log.info "$device.displayName illuminance changed to $lux"
			}
			else if (descMap.cluster == "0402" && descMap.attrId == "0000") {
				def rawValue = hexStrToSignedInt(descMap.value) / 100
				def Scale = location.temperatureScale
				if (Scale == "F") rawValue = (rawValue * 1.8) + 32
				if (temperatureOffset == null) temperatureOffset = "0"
				def offsetrawValue = (rawValue  + Float.valueOf(temperatureOffset))
				rawValue = offsetrawValue
				if (rawValue > 200 || rawValue < -200){
					if (infoLogging) log.info "$device.displayName Ignored temperature value: $rawValue\u00B0"+Scale
				} else {
					sendEvent("name": "temperature", "value": rawValue, "unit": "\u00B0"+Scale)
					if (infoLogging) log.info "$device.displayName temperature changed to $rawValue\u00B0"+Scale
				}
			}
			else if (descMap.cluster == "0403" && descMap.attrId == "0000") {
				def rawValue = Integer.parseInt(descMap.value,16)
				if (rawValue > 2000 || rawValue < 500){
					if (infoLogging) log.info "$device.displayName Ignored pressure value: $rawValue"
				} else {
					sendEvent("name": "pressure", "value": rawValue, "unit": "hPa")
					if (infoLogging) log.info "$device.displayName pressure changed to $rawValue"
				}
			}
			else if (descMap.cluster == "0405" && descMap.attrId == "0000") {
				def rawValue = Integer.parseInt(descMap.value,16) / 100
				if (rawValue > 100 || rawValue < 0){
					if (infoLogging) log.info "$device.displayName Ignored humidity value: $rawValue"
				} else {
					sendEvent("name": "humidity", "value": rawValue, "unit": "%")
					if (infoLogging) log.info "$device.displayName humidity changed to $rawValue"
				}
			}
			else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
				def rawValue = Integer.parseInt(descMap.value,16)
				def status = "inactive"
				if (rawValue == 1) status = "active"
				sendEvent("name": "motion", "value": status)
				if (infoLogging) log.info "$device.displayName motion changed to $status"
				unschedule(resetMotion)
				runIn(65, resetMotion)
			}
			else if (descMap.cluster == "0101" && descMap.attrId == "0508") {
				def status = "active"
				sendEvent("name": "acceleration", "value": status)
				sendEvent("name": "motion", "value": "active")
				if (infoLogging) log.info "$device.displayName acceleration changed to $status"
				unschedule(resetVibration)
				runIn(65, resetVibration)
			}
			else if (descMap.cluster == "0101" && descMap.attrId == "0055") {
				def status = "active"
				sendEvent("name": "tilt", "value": status)
				sendEvent("name": "motion", "value": "active")
				if (infoLogging) log.info "$device.displayName tilt changed to $status"
				unschedule(resetVibration)
				runIn(65, resetVibration)
			}
			else if (descMap.cluster == "0006" && descMap.attrId == "0000") {
				def rawValue = Integer.parseInt(descMap.value,16)
				def contact = "closed"
				if (rawValue == 1) contact = "open"
				sendEvent("name": "contact", "value": contact)
				if (infoLogging) log.info "$device.displayName contact changed to $contact"
				if (motionContact){
					def motion = "inactive"
					if (rawValue == 1) motion = "active"
					sendEvent("name": "motion", "value": motion)
					if (infoLogging) log.info "$device.displayName motion changed to $motion"
				}
				if (getDataValue("model") == "lumi.sensor_switch.aq2"){
					sendEvent("name": "pushed", "value": 1, isStateChange: true)
					if (infoLogging) log.info "$device.displayName was pushed"
				}
				if (getDataValue("model") == "lumi.sensor_switch"){
					if (rawValue == 0){
						int thisHold = Float.valueOf(holdDuration) * 1000
						runInMillis(thisHold, deviceHeld)
						state.held = false
					} else {
						if (state.held == true){
							state.held = false
							unschedule(deviceHeld)
							sendEvent("name": "released", "value":  1, isStateChange: true)
							if (infoLogging) log.info "$device.displayName was released"
						} else {
							unschedule(deviceHeld)
							if (allButtons){
								sendEvent("name": "pushed", "value": 1, isStateChange: true)
								if (infoLogging) log.info "$device.displayName button 1 was pushed"
							} else {
								sendEvent("name": "pushed", "value": 1, isStateChange: true)
								sendEvent("name": "taps", "value": 1, isStateChange: true)
								if (infoLogging) log.info "$device.displayName was pushed"
							}
						}
					}
				}
			}
			else if (descMap.cluster == "0006" && descMap.attrId == "8000" && (getDataValue("model") == "lumi.sensor_switch" || getDataValue("model") == "lumi.sensor_switch.aq2")) {
				def rawValue = Integer.parseInt(descMap.value,16)
				if (rawValue > 4) rawValue = 4
				sendEvent("name": "taps", "value":  rawValue, isStateChange: true)
				if (rawValue == 2 && allButtons == false){
					sendEvent("name": "doubleTapped", "value":  1, isStateChange: true)
					if (infoLogging) log.info "$device.displayName was doubleTapped"
				}
				if (allButtons){
					sendEvent("name": "pushed", "value": rawValue, isStateChange: true)
					if (infoLogging) log.info "$device.displayName button $rawValue was pushed"
				} else {
					if (infoLogging) log.info "$device.displayName tapped $rawValue time(s)"
				}
			}
			else if (descMap.cluster == "0012" && descMap.attrId == "0055") {
				def button = Integer.parseInt(descMap.endpoint,16) 
				def action = Integer.parseInt(descMap.value,16)
				if (debugLogging) log.debug "$device.displayName Button:$button, Action:$action"

				if (action == 0) {
					sendEvent("name": "held", "value":  button, isStateChange: true)
					if (infoLogging) log.info "$device.displayName button $button was held"
					int thisHold = Float.valueOf(holdDuration) * 1000
					state.held = true
					runInMillis(thisHold, deviceReleased, [data: button])
				}
				else if (action == 1) {
					sendEvent("name": "pushed", "value":  button, isStateChange: true)
					if (infoLogging) log.info "$device.displayName button $button was pushed $action time(s)"
				}
				else if (action == 2) {
					sendEvent("name": "doubleTapped", "value":  button, isStateChange: true)
					if (infoLogging) log.info "$device.displayName button $button was double tapped"
				}
				else if (action == 16) {
					sendEvent("name": "held", "value":  button, isStateChange: true)
					if (infoLogging) log.info "$device.displayName button $button was held"
				}
				else if (action == 17) {
					if (state.held) {
						state.held = false
						unschedule(deviceReleased);
					}
					sendEvent("name": "released", "value":  button, isStateChange: true)
					if (infoLogging) log.info "$device.displayName button $button was released"
				}
				else if (action == 18) {
					sendEvent("name": "shaken", "value":  button, isStateChange: true)
					if (infoLogging) log.info "$device.displayName button $button was shaken"
				}
				else if (action == 255) {
					sendEvent("name": "released", "value":  button, isStateChange: true)
					if (infoLogging) log.info "$device.displayName button $button was released"
				}
			}
		}
	}
	if (state.version != version()) { state.version = version() }
	if (settings?.healthStatusEnabled != false) {
		unschedule(presenceStart); unschedule(presenceTracker)
		if (device.currentValue("healthStatus") != "online"){
			sendEvent("name": "healthStatus", "value":  "online")
			if (infoLogging) log.info "$device.displayName is online"
		}
		scheduleDeviceHealthCheck()
	}
}

def open() {
	sendEvent("name": "contact", "value":  "open", isStateChange: true)
	if (infoLogging) log.info "$device.displayName contact changed to open [virtual]"
}

def closed() {
	sendEvent("name": "contact", "value":  "closed", isStateChange: true)
	if (infoLogging) log.info "$device.displayName contact changed to closed [virtual]"
}

//    Defined method for Ping

def ping () {
     refresh()
}

def online() {
	sendEvent("name": "healthStatus", "value":  "online", isStateChange: true)
	if (infoLogging) log.info "$device.displayName contact changed to health [virtual]"
}

def offline() {
	sendEvent("name": "healthStatus", "value":  "offline", isStateChange: true)
	if (infoLogging) log.info "$device.displayName contact changed to offline [virtual]"
}

def active() {
	sendEvent("name": "motion", "value":  "active", isStateChange: true)
	if (infoLogging) log.info "$device.displayName motion changed to active [virtual]"
}

def inactive() {
	sendEvent("name": "motion", "value":  "inactive", isStateChange: true)
	if (infoLogging) log.info "$device.displayName motion changed to inactive [virtual]"
}

def push(button = 1) {
	sendEvent("name": "pushed", "value":  button, isStateChange: true)
	if (infoLogging) log.info "$device.displayName pushed $button [virtual]"
}

def doubleTap(button = 1) {
	sendEvent("name": "doubleTapped", "value":  button, isStateChange: true)
	if (infoLogging) log.info "$device.displayName doubleTapped $button [virtual]"
}

def hold(button = 1) {
	sendEvent("name": "held", "value":  button, isStateChange: true)
	if (infoLogging) log.info "$device.displayName held $button [virtual]"
}

def release(button = 1) {
	sendEvent("name": "released", "value":  button, isStateChange: true)
	if (infoLogging) log.info "$device.displayName released $button [virtual]"
}

def shake() {
	sendEvent("name": "shaken", "value":  1, isStateChange: true)
	if (infoLogging) log.info "$device.displayName shaken [virtual]"
}

def wet() {
	sendEvent("name": "water", "value":  "wet", isStateChange: true)
	if (infoLogging) log.info "$device.displayName changed to wet [virtual]"
}

def dry() {
	sendEvent("name": "water", "value":  "dry", isStateChange: true)
	if (infoLogging) log.info "$device.displayName changed to dry [virtual]"
}

def updated() {
	replacePresenceWithHealthStatus()
	if (settings?.healthStatusEnabled != false) { scheduleDeviceHealthCheck() }
}

void replacePresenceWithHealthStatus() {
	unschedule(presenceStart); unschedule(presenceTracker)
	if (device.currentValue("healthStatus") == null) { 
		if (device.currentValue("presence") == "present") {
			sendEvent("name": "healthStatus", "value":  "online")
		} else if (device.currentValue("presence") == "not present") {
			sendEvent("name": "healthStatus", "value":  "offline")
		} else {
			sendEvent("name": "healthStatus", "value":  "unknown")
		}
	}
	device.deleteCurrentState('presence')
}

void scheduleDeviceHealthCheck() {
	unschedule(presenceStart); unschedule(presenceTracker)
	if (settings?.healthStatusEnabled != true) { 
		unschedule(deviceHealthCheck); 
		if (infoLogging) log.info "$device.displayName health check is disabled"
		return 
	}
	if (settings?.healthHours == null || settings?.healthHours == "") { device.updateSetting('healthHours', [value: '12', type: 'enum']) }
	int scheduleHours = settings?.healthHours.toInteger() * 60 * 60
	if (scheduleHours < 1 || scheduleHours > 86400) { scheduleHours = 43200 }
	if (debugLogging) log.debug "$device.displayName health check in ${settings?.healthHours} hours"
	runIn(scheduleHours, "deviceHealthCheck", [overwrite: true])
}

void deviceHealthCheck() {
	// if this scheduled job is executed, then the device has not reported in the expected time
	if (device.currentValue("healthStatus") != "offline"){
		sendEvent("name": "healthStatus", "value":  "offline")
		if (infoLogging) log.info "$device.displayName offline"
	}
	else {
		if (infoLogging) log.info "$device.displayName still offline"
	}
	scheduleDeviceHealthCheck()	// keep checking
}

// traps for the old periodic presence check methods
void presenceTracker() { updated() }
void presenceStart() { updated() }

def deviceHeld() {
	if (state.held == false){
		state.held = true
		if (allButtons){
			sendEvent("name": "pushed", "value": 5, isStateChange: true)
			if (infoLogging) log.info "$device.displayName button 5 was pushed"
		} else {
			sendEvent("name": "held", "value":  1, isStateChange: true)
			if (infoLogging) log.info "$device.displayName was held"
		}
	}
}

def deviceReleased(button) {
	if (state.held == true){
		state.held = false
		sendEvent("name": "released", "value":  button, isStateChange: true)
		if (infoLogging) log.info "$device.displayName button $button was released"
	}
}

def batteryEvent(rawValue) {
	def batteryVolts = (rawValue / 10).setScale(2, BigDecimal.ROUND_HALF_UP)
	def minVolts = 20
	def maxVolts = 30
	def pct = (((rawValue - minVolts) / (maxVolts - minVolts)) * 100).toInteger()
	def batteryValue = Math.min(100, pct)
	if (batteryValue > 0){
		sendEvent("name": "battery", "value": batteryValue, "unit": "%")
		sendEvent("name": "voltage", "value": batteryVolts, "unit": "volts")
		if (infoLogging) log.info "$device.displayName battery changed to $batteryValue%"
		if (infoLogging) log.info "$device.displayName voltage changed to $batteryVolts volts"
	}

	return
}

def resetMotion() {
	if (device.currentState('motion')?.value == "active"){
		sendEvent("name": "motion", "value": "inactive")
		if (infoLogging) log.info "$device.displayName motion changed to inactive"
	}

	return
}

def resetVibration() {
	if (device.currentState('acceleration')?.value == "active"){
		sendEvent("name": "acceleration", "value": "inactive")
		if (infoLogging) log.info "$device.displayName acceleration changed to inactive"
	}
	if (device.currentState('tilt')?.value != "inactive"){
		sendEvent("name": "tilt", "value": "inactive")
		if (infoLogging) log.info "$device.displayName tilt changed to inactive"
	}
	if (device.currentState('motion')?.value != "inactive"){
		sendEvent("name": "motion", "value": "inactive")
		if (infoLogging) log.info "$device.displayName motion changed to inactive"
	}

	return
}

def refresh() {
	List<String> cmd = []

	if (debugLogging) log.debug "refresh()"
	if (device.currentState('motion')?.value == "active"){
		unschedule(resetMotion)
		resetMotion()
	}
	if (device.currentState('acceleration')?.value == "active"){
		unschedule(resetVibration)
		resetVibration()
	}

	cmd += zigbee.onOffRefresh()
	cmd += zigbee.onOffConfig()
	cmd += zigbee.batteryConfig()

	cmd += zigbee.readAttribute(0x0001, 0x0020)
	cmd += zigbee.readAttribute(0x0000, 0x0004)
	cmd += zigbee.readAttribute(0x0000, 0x0005)
	cmd += zigbee.readAttribute(0x0400, 0x0000)
	cmd += zigbee.readAttribute(0x0402, 0x0000)

	return cmd
}

def configure() {
	Integer zDelay = 100
	List<String> cmd = []

	if (debugLogging) log.debug "configure()"

	unschedule()
	state.clear()

	replacePresenceWithHealthStatus()
    if(healthStatusEnabled !=false) {
		scheduleDeviceHealthCheck()
	}

	cmd = [
		"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x0000 {${device.zigbeeId}} {}",
		"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x0001 {${device.zigbeeId}} {}",
		"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x0003 {${device.zigbeeId}} {}",
		"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x0400 {${device.zigbeeId}} {}",
		"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x0402 {${device.zigbeeId}} {}",
	]

	cmd += zigbee.configureReporting(0x0400, 0x0000, 0x21, 10,   3600, 300)
	cmd += zigbee.configureReporting(0x0402, 0x0000, 0x21, 10,   3600, 300)
	cmd += zigbee.configureReporting(0x0001, 0x0020, 0x20, 3600, 3600, 1)

	return cmd
}
5 Likes

I wonder if your devices appear in the Zigbee Graph with this driver?

The driver used should have no effect on whether a Zigbee device shows on the map or not...
As long as there is some activity coming from this device, it should show on the map after some time.

5 Likes

As kkossev notes, yes, they will appear (and are appearing). :slight_smile:
image

2 Likes

With the Aqara temp sensor WSDCGQ11LM I'm getting the following error in the logs when enabling the Health Detection. Working fine and showing the healthStatus attribute correct for my other Aqara sensors, in example the motion sensor RTCGQ11LM.

org.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack: No signature of method: user_driver_waytotheweb_Xiaomi_Aqara_Mijia_Sensors_Switches_w_Health_695.presenceTracker() is applicable for argument types: () values: [] (method presenceTracker)

Added one light sensor today that had been on the shelf 18 months; have 3.
No Problems thus far. Many Thanks for the Driver and Support work. Merry Christmas to me & you all..

Still working fine on the C8?

Yes, I have a number of Aqara devices on my C8 and they are holding fine...they are all attached via repeaters based on my last review using my Xbee to map my Zigbee network, so they are happiest connected throught a repeater (at least in my home).

2 Likes

I switched all of my Aqara devices to this driver, and so far have been having very good luck with it on my C8 hub. Switched them over about 10 days ago, and haven't seen one drop off and not come back yet. See here for more info about my recent Zigbee troubles, if you are interested in the read. So far, so good!

1 Like

The driver didn't seem to handle the current Aqara Luminance sensor, adding this line addressed the issue:
@chirpy

fingerprint profileId: "0104", inClusters: "0000,0400,0003,0001", outClusters: "0003", manufacturer: "LUMI", model: "lumi.sen_ill.agl01", deviceJoinName: "Xiaomi Aqara Light Sensor"

This one works fine for the new sensor (not yet documented in the discription, but it works)

1 Like

I bought these motion sensors
https://de.aliexpress.com/item/4000030637208.html?spm=a2g0o.order_list.order_list_main.10.21ef5c5faecarM&gatewayAdapt=glo2deu

ID Xiaomi Aqara Motion Sensor : RTCGQ11LM [*]

then I installed these driver

then put motion sensor in paring mode 3 sec reset button

but then i want to pair them via zigbee they are not found
any further tip?

logs showing following
sys:12024-01-22 17:55:29.743infoZigbee Discovery Running

sys:12024-01-22 17:55:29.297infoZigbee Discovery Stopped

sys:12024-01-22 17:54:27.745infoInitializing Zigbee Device 00158D000704F5C8, 32E9

I use those sensors an I can tell they pair on my c8 whit this driver, but I needed to pair it more than once. My C8 hub is a test hub, I don't use repeaters on it. I heard there are some issues with some some zigbee repeaters, maybe this is the issue, I guess this is why people say you need to pair close, on top of the hub or you can use this driver where you can choose witch device you turn on for paring,

Personal I prefer this driver for my Aqara motion & illuminance sensors.

2 Likes

Your preferenced driver worked and the pairing is kinda strange

first start pairing then device pairing then press device pairing button again until device paring started is shown then after 30 second press again one time the pair button then pairing works.

2 Likes

I think the pairing issue is not driver related, I know there are issues with zigbee pairing for some devices. I my test I paired the device with the "chirpy" driver, it was available after pairing but is was not working and there were things missing, after pairing a second time (on top of the hub) the sensor was working.
With my prefered driver (kkossev) it was paired correct at the first attempt, but this is not a guarantee....The reason why I use this driver is because the developer is still active on the forum, he is a fantastic guy, but overal it is a good driver, you can simulate the state of the sensor (motion active or inactive)

2 Likes