Zemismart Zigbee PIR Motion sensor stays active and mostly sends active events

hey everyone, I recently bought some Zemismart curtain rails and impulse bought a zigbee Tuya PIR motion sensor from them as well.


All my other devices have been z-wave up til now and they all work fine, this is my first Zigbee device, inhind sight I should have checked the driver list.

So I was able to link the Zigbee device.
Data just reads

  • endpointId: 01
  • application: 40
  • model: TS0202
  • manufacturer: _TYZB01_jytabjkb

Most things seem to work , but I meanly get active events in in the logs, but since they never get an inactive HE doesn't react to it.
logs:
20:48:19.861 infoMotion is active
20:40:25.390 infoMotion is active
20:40:23.365 infoMotion is active
20:40:20.287 infoMotion is active
20:38:44.899 infoMotion is active
20:37:42.615 infoMotion is active
20:37:39.822 infoMotion is inactive

After some digging around it seems like this Motion detector is being used by other brands;
for Example the CR Smart Home TS0202,

And for example zigbee2mqtt having the same issue with it, they seem to reset the activity after x seconds.

Is there something similar I can do, with rules or a custom driver?

The Tuya made sensors I have only send active events and you need to reset them after the time out period (usually around 60 seconds) when they will send another event.
I modified a Xiaomi driver (which works the same way) to work with mine which you could try and see if it works.

1 Like

hey @at9 thanks for the quick reply.
On my first try, the motion sensor just stopped sending events all together.
Probably because fingerprint and clusters don't match up?
Whats the best approach here? Are there things I should try to change in the driver?

I'll try some more after work.

You need to see what the sensor sends on active adjust the driver to react to this.
My device sends a message starting with "catchall:" only for motion so that is what the driver looks for.

ok, after some testing , your driver needed more changes in the parsing to make it compatible with my device.
I stumbled on the following one

Decided to "borrow" it and change the clusters and info according to my device and it just worked fine. It might even work fine without the tweaks, I don't know.
I did have to tweak the configuration a bit. Now I have it working I might try to figure out what the other clusters do an play around with those.
In any case I have a working PIR motion sensor now.
Thanks for the help @at9 and thanks for the driver @muxa.

1 Like

Got the ZemiSmart PIR Sensor.


The "motion" sensor seems to work with the Generic Zigbee Motion Sensor. However, it does not report on the batteyr, temparature or lux.
Is there device specific device driver for this to read the temperature and lux?

Try the hue motion sensor driver, no idea if it will work or not.

Doesn't look like the device you link supports either of them.

Also none of the current driver shows battery.

Could you share the tweaked Konke driver which you used for the Tuya motion sensor? Bouguht a ZemiSmart PIR which has the exact manufactuere ID and model ID as the Tuya's sensor. Lots of rebranding going on.

Hey taysnet, tbh I haven't been tinkering with the hubitat for a while now, so I don't remember the state I left this driver in :expressionless:
looks like I just changed some ids from the driver from Muxa, I'll need some time to check later if it actually works as intended ^^

import hubitat.zigbee.clusters.iaszone.ZoneStatus
import com.hubitat.zigbee.DataType

metadata {
	definition (name: "PIR ZigBee Motion Sensor2", namespace: "muxa", author: "Mikhail Diatchenko") {
		capability "MotionSensor"
		capability "Battery"
        capability "PushableButton"
		capability "Configuration"
        capability "Refresh"

		fingerprint endpointId: "01", profileId: "0104", inClusters: "0000,0001,0003,0402,0500", outClusters: "0003,0019", manufacturer: "_TYZB01_jytabjkb", model: "TS0202" , deviceJoinName: "PIR Motion Sensor"     
	}

	preferences {
		input "motionreset", "number", title: "After motion is detected, wait ___ second(s) until resetting to inactive state. Default = 16 seconds (Hardware resets at 15 seconds)", description: "", range: "1..7200", defaultValue: 16
        input "batteryReportingHours", "number", title: "Report battery every ___ hours. Default = 12h (Minimum 2 h)", description: "", range: "2..12", defaultValue: 12
		
		input name: "infoLogging", type: "bool", title: "Enable info message logging", description: ""
		input name: "debugLogging", type: "bool", title: "Enable debug message logging", description: ""
	}
}

// Parse incoming device messages to generate events
def parse(String description) {

    Map map = [:]
    // logDebug("Parsing: $description")
	if (description?.startsWith('zone status')) {	
        // logDebug("Zone status: $description")    
        def zs = zigbee.parseZoneStatus(description)
        map = parseIasMessage(zs)
    }
    else if (description?.startsWith("catchall") || description?.startsWith("read attr"))
    {
        Map descMap = zigbee.parseDescriptionAsMap(description)        
        if (descMap.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.attrInt == 0x0020) {
            map = parseBattery(descMap.value)
        } else if (descMap.command == "07") {
            // Process "Configure Reporting" response            
            if (descMap.data[0] == "00") {
                switch (descMap.clusterInt) {
                    case zigbee.POWER_CONFIGURATION_CLUSTER:
                        logInfo("Battery reporting configured");                        
                        break
                    default:                    
                        log.warn("Unknown reporting configured: ${descMap}");
                        break
                }
            } else {
                log.warn "Reporting configuration failed: ${descMap}"
            }
        } else if (descMap.clusterInt == 0x0500 && descMap.attrInt == 0x0002) {
            logDebug("Zone status repoted: $descMap")
            def zs = new ZoneStatus(Integer.parseInt(descMap.value, 16))
            map = parseIasMessage(zs)        
        } else if (descMap.clusterInt == 0x0500 && descMap.attrInt == 0x0011) {
            logInfo("IAS Zone ID: ${descMap.value}")
        } else if (descMap.profileId == "0000") {
            // ignore routing table messages
        } else {
            log.warn ("Description map not parsed: $descMap")            
        }
    }
    //------IAS Zone Enroll request------//
	else if (description?.startsWith('enroll request')) {
		logInfo "Sending IAS enroll response..."
		return zigbee.enrollResponse()
	}
    else {
        log.warn "Description not parsed: $description"
    }
    
    if (map != [:]) {
		logInfo(map.descriptionText)
		return createEvent(map)
	} else
		return [:]
}

// helpers -------------------

def parseIasMessage(ZoneStatus zs) {
        if ((zs.alarm1 || zs.alarm2) && zs.battery == 0 && zs.trouble == 0) {
            // Motion detected
	        return handleMotion(true)
        }
        else if (zs.tamper == 1 && zs.battery == 1 && zs.trouble == 1 && zs.ac == 1) {
            logDebug "Device button pressed"
            map = [
		        name: 'pushed',
		        value: 1,
                isStateChange: true,
                descriptionText: "Device button pushed"
	        ]
        }
        else {
            log.warn "Zone status message not parsed"
            if (debugLogging) {
                logDebug "zs.alarm1 = $zs.alarm1"
                logDebug "zs.alarm2 = $zs.alarm2"
                logDebug "zs.tamper = $zs.tamper"
                logDebug "zs.battery = $zs.battery"
                logDebug "zs.supervisionReports = $zs.supervisionReports"
                logDebug "zs.restoreReports = $zs.restoreReports"
                logDebug "zs.trouble = $zs.trouble"
                logDebug "zs.ac = $zs.ac"
                logDebug "zs.test = $zs.test"
                logDebug "zs.batteryDefect = $zs.batteryDefect"
            }
        }
}

private handleMotion(motionActive) {    
    if (motionActive) {
        def timeout = motionreset ?: 16
        // The sensor only sends a motion detected message so reset to motion inactive is performed in code
        runIn(timeout, resetToMotionInactive)        
        if (device.currentState('motion')?.value != "active") {
            state.motionStarted = now()
        }
    }
    
	return getMotionResult(motionActive)
}

def getMotionResult(motionActive) {
	def descriptionText = "Detected motion"
    if (!motionActive) {
		descriptionText = "Motion reset to inactive after ${getSecondsInactive()}s"
    }
	return [
			name			: 'motion',
			value			: motionActive ? 'active' : 'inactive',
			descriptionText : descriptionText
	]
}

def resetToMotionInactive() {
	if (device.currentState('motion')?.value == "active") {
		def descText = "Motion reset to inactive after ${getSecondsInactive()}s"
		sendEvent(
			name:'motion',
			value:'inactive',
			// isStateChange: true,
			descriptionText: descText
		)
		logInfo(descText)
	}
}

def getSecondsInactive() {
    if (state.motionStarted) {
        return Math.round((now() - state.motionStarted)/1000)
    } else {
        return motionreset ?: 16
    }
}

// Convert 2-byte hex string to voltage
// 0x0020 BatteryVoltage -  The BatteryVoltage attribute is 8 bits in length and specifies the current actual (measured) battery voltage, in units of 100mV.
private parseBattery(valueHex) {
	//logDebug("Battery parse string = ${valueHex}")
	def rawVolts = Integer.parseInt(valueHex, 16) / 10 // hexStrToSignedInt(valueHex)/10
	def minVolts = voltsmin ? voltsmin : 2.5
	def maxVolts = voltsmax ? voltsmax : 3.0
	def pct = (rawVolts - minVolts) / (maxVolts - minVolts)
	def roundedPct = Math.min(100, Math.round(pct * 100))
	def descText = "Battery level is ${roundedPct}% (${rawVolts} Volts)"
	//logInfo(descText)
    // sendEvent(name: "batteryLevelLastReceived", value: new Date())    
	def result = [
		name: 'battery',
		value: roundedPct,
		unit: "%",
		//isStateChange: true,
		descriptionText: descText
	]
	return result
}

// lifecycle methods -------------

// installed() runs just after a sensor is paired
def installed() {
	logInfo("Installing")    
	sendEvent(name: "numberOfButtons", value: 1, descriptionText: "Device installed")
    return refresh()
}

// configure() runs after installed() when a sensor is paired or reconnected
def configure() {
	logInfo("Configuring")
    
    return configureReporting()
}

def refresh() {
	logInfo("Refreshing")

    return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) // battery voltage
}

// updated() runs every time user saves preferences
def updated() {
	logInfo("Updating preference settings")
    
    return configureReporting()
}

// helpers -------------

private def configureReporting() {
    def seconds = Math.round((batteryReportingHours ?: 12)*3600)
    
    logInfo("Battery reporting frequency: ${seconds/3600}h")    
    
    return zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020, DataType.UINT8, seconds, seconds, 0x01)
        + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x20)
}

private def logDebug(message) {
	if (debugLogging) log.debug "${device.displayName}: ${message}"
}

private def logInfo(message) {
	if (infoLogging)
		log.info "${device.displayName}: ${message}"
}

Hi Loma. So does the driver work as intended in your case?

I had it stored for a while. Got it out to test and it seems to work. Not sure about the battery since it says 100%

1 Like

Download the Hubitat app