ThirdReality motion sensor never goes inactive

I have one of those Third Reality motion sensors, (it looks like their button, but a little fatter). As I walk by, the little LED on it flashes.

Hubitat shows it is active, but never does it go inactive. Of course my rule for it doesn’t trigger.

Hubitat is using its “Generic Zigbee Motion Sensor (no temp)”.

Any help?

Thanks, Mark.

Any additional information will help with the troubleshooting :

First thing I would try is factory resetting it back to paring mode, and try to re-pair it with the hub. If it was simply in a non-responsive state, or no longer really connected, it should be found and again and fall back into place.

1 Like

which driver are you running with that? I also had some … anomalies at first. Similar issues.
Things to just take a peek at -

do you have any unknown devices in your zigbee map? I did, and one was the repeater near a failed unit.

The driver I’m now convinced is much better is more specific for the unit. It stops the a double reporting issue, has a much more accurate battery reporting and misc bug fixes.

Feel free to give this a try - it’s pretty lite. I won’t be putting it into HPM, I don’t push any of my custom drivers on the community anymore but maybe it will help?

/* Third Reality Motion Sensor Custom for Hubitat

A Third Reality Motion Sensor Driver For Hubitat

manufacturer: Third Reality, Inc
model: 3RMS16BZ
softwareBuild: v1.00.24

This device gives full bat voltage as 3.5v which .5v to high.
===================================================================================================
v1.2.7  06/20/2026 JAS - further fix double activity reporting
v1.2.6  06/12/2026 JAS - added updateFirmware 
v1.2.5  06/12/2026 JAS - Fixed double reporting issue by separating IAS Zone Status parsing from cluster 0500 parsing.
v1.2.4  06/12/2026 JAS - Optimized logic, fixed scoping, fixed bitwise operator bugs, cleaned up battery processing.
v1.2.3  03/25/2023 added low bat setting
v1.2.2  01/29/2023 Changed anti dupe routines to adding State change.
v1.2.1  01/23/2023 Power up init rewriten
v1.2.0  12/14/2022 Bat code rewrite
v1.1.0  12/11/2022 working
v1.0.0  12/10/2022 First release
===================================================================================================  
Copyright [2022] [tmaster winnfreenet.com]

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.

I modified it to stop the double reporting of the event when triggered, and optimized the battery reporting and some bugs - JAS 6-12-26

*/

import hubitat.zigbee.clusters.iaszone.ZoneStatus
import hubitat.zigbee.zcl.DataType
import hubitat.helper.HexUtils

def clientVersion() {
    def TheVersion = "1.2.7"
    if (state.version != TheVersion) {
        logging("Upgrading ! ${state.version} to ${TheVersion}", "warn")
        state.version = TheVersion
        configure() 
    }
}

metadata {
    definition (name: "Third Reality Motion Sensor (Custom)", namespace: "jshimota", author: "Tmaster", importUrl: "https://raw.githubusercontent.com/tmastersmart/hubitat-code/main/third-reality-motion-sensor.groovy") {
        capability "Health Check"
        capability "Battery"
        capability "Configuration"
        capability "Initialize"
        capability "PresenceSensor"
        capability "Refresh"
        capability "Sensor"
        capability "MotionSensor"

        command "checkPresence"
        command "uninstall"
        command "updateFirmware"
 
		attribute "batteryVoltage", "string"
        
        fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0500", outClusters:"0019", model:"3RMS16BZ", manufacturer:"Third Reality, Inc"
    }
    
    preferences {
        input name: "infoLogging",  type: "bool", title: "Enable info logging", description: "Recommended low level", defaultValue: true, required: true
        input name: "debugLogging", type: "bool", title: "Enable debug logging", description: "MED level Debug", defaultValue: true, required: true
        input name: "traceLogging", type: "bool", title: "Enable trace logging", description: "Insane HIGH level", defaultValue: false, required: true

        input name: "pingYes", type: "bool", title: "Enable schedule Ping", description: "", defaultValue: false, required: true
        input name: "pingIt" , type: "enum", title: "Ping Time", description: "Ping every x mins. Press config after saving", options: ["5","10","15","20","25","30"], defaultValue: "10", required: true
        input name: "minVoff", type: "enum", title: "Min Voltage", description: "Using minVoltTest set the min voltage your sensor will run on", options: ["1.0","1.6","1.7","1.8","1.9","2.0","2.1","2.2","2.3","2.4","2.5","2.6"], defaultValue: "2.2", required: true  

        input name: "pollYes", type: "bool", title: "Enable Presence", description: "", defaultValue: true, required: true
        input name: "pollHR" , type: "enum", title: "Check Presence Hours", description: "Press config after saving", options: ["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20"], defaultValue: "8", required: true 
    }
}

void installed() {
    logging("Installed", "warn")    
    state.DataUpdate = false
    device.updateSetting("pollHR", [value: "10", type: "enum"])
    device.updateSetting("pingIt", [value: "30", type: "enum"]) 
    state.minVoltTest = 2.1   
    configure()   
    updated()
}

void initialize() {
    logging("initialize", "debug")
    clientVersion()    
    def randomSixty = Math.abs(new Random().nextInt() % 180)
    runIn(randomSixty, "refresh")
}

void uninstall() {
    unschedule()
    state.clear()
    logging("Uninstalled - States removed you may now switch drivers", "info")
}

void updated() {
    logging("Updated", "info")
    loggingUpdate()
    clientVersion()
}

List<String> refresh() {
    if (state.MFR) { logging("Refreshing ${state.MFR} Model:${state.model} Ver:${state.version}", "info") }
    else { logging("Refreshing -unknown device- Ver:${state.version}", "info") }

    return zigbee.readAttribute(0x0500, 0x0002) + // contact     
           zigbee.readAttribute(0x0000, 0x0004) + // mf
           zigbee.readAttribute(0x0000, 0x0005) + // model
           zigbee.readAttribute(0x0001, 0x0020) + // battery
           zigbee.readAttribute(0x0402, 0x0000) + // temp
           zigbee.readAttribute(0x0500, 0x0003)   // contact     
}

List<String> ping() {
    logging("Ping", "info")
    return zigbee.readAttribute(0x0500, 0x0002)
}

List<String> configure() {
    logging("Config", "info")
    getIcons() 
    
    sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])
 
    unschedule()
  
    if (pollYes) { 
        def randomSixty = Math.abs(new Random().nextInt() % 60)
        def randomSixty2 = Math.abs(new Random().nextInt() % 60)    
        def randomTwentyFour = Math.abs(new Random().nextInt() % 24)
        logging("CHRON: ${randomSixty2} ${randomSixty} ${randomTwentyFour}/${pollHR} * * ? *", "debug") 
        schedule("${randomSixty2} ${randomSixty} ${randomTwentyFour}/${pollHR} * * ? *", "checkPresence")    
        logging("Presence Check Every ${pollHR}hrs starting at ${randomTwentyFour}:${randomSixty}:${randomSixty2}", "info") 
    }    

    if (pingYes) {    
        def randomSixty = Math.abs(new Random().nextInt() % 60)
        logging("CHRON: ${randomSixty} 0/${pingIt} * * * ? *", "debug")      
        schedule("${randomSixty} 0/${pingIt} * * * ? *", "ping")
        logging("PING every ${pingIt} mins", "info")   
    }

    return zigbee.batteryConfig() +
           zigbee.configureReporting(0x0500, 0x0002, DataType.BITMAP16, 30, 3600, null) +
           zigbee.temperatureConfig(30, 1800) +
           zigbee.enrollResponse()  
}

def parse(String description) {
    logging("Parse: [${description}]", "trace")
    state.lastCheckin = now()
    checkPresence()
   
    if (description?.startsWith('enroll request')) { 
        return zigbee.enrollResponse()
    }  

    // Primary motion processor
    if (description?.startsWith("zone status")) {
        ZoneStatus zoneStatus = zigbee.parseZoneStatus(description)
        processStatus(zoneStatus)
        return
    }
    
    Map descMap = zigbee.parseDescriptionAsMap(description) 
    logging("MAP: ${descMap}", "trace")    

    if (descMap.clusterId) { descMap.cluster = descMap.clusterId }
    
    if (descMap.cluster == "0000") {
        if (descMap.attrId == "0004" && descMap.attrInt == 4) {
            logging("Manufacturer :${descMap.value}", "debug") 
            state.MFR = descMap.value 
            updateDataValue("manufacturer", state.MFR)
            state.DataUpdate = true 
            return    
        } 
        if (descMap.attrId == "0005" && descMap.attrInt == 5) {
            logging("Model :${descMap.value}", "debug")
            state.model = descMap.value    
            updateDataValue("model", state.model)
            state.DataUpdate = true    
            return    
        } 
    }
    
    def evt = zigbee.getEvent(description)
    if (evt) { logging("Event: ${evt}", "debug") } 

    if (descMap.cluster == "0001" && descMap.attrId == "0020") {
        def powerLast = device.currentValue("battery")
        def batVolts = device.currentValue("batteryVoltage")
        
        def minVolts = minVoff ? minVoff.toDouble() : 2.1
        def maxVolts = 3.2
        
        def rawValue = Integer.parseInt(descMap.value, 16)
        if (rawValue == 0 || rawValue == 255) { return } 
        def batteryVoltage = rawValue / 10
        
        logging("value:${rawValue} bat${batteryVoltage}v batLast${batVolts}v batLast${powerLast}% MinV ${state.minVoltTest}v", "trace")
        logging("${batteryVoltage} -${minVolts} / ${maxVolts} - ${minVolts}", "trace")
        
        def pct = (batteryVoltage - minVolts) / (maxVolts - minVolts)
        def roundedPct = Math.round(pct * 100)
        if (roundedPct <= 0) roundedPct = 1
        def batteryPercentage = Math.min(100, roundedPct)
        
        logging("Battery: now:${batteryPercentage}% Last:${powerLast}% ${batteryVoltage}V", "debug")   
        def isChange = (powerLast != batteryPercentage)
        
        logging("Battery:${batteryPercentage}%  ${batteryVoltage}V", "info")  
        sendEvent(name: "battery", value: batteryPercentage, unit: "%", isStateChange: isChange)      
        sendEvent(name: "batteryVoltage", value: batteryVoltage, unit: "V", isStateChange: isChange)
    } 
    else if (descMap.cluster == "0500") {
        // Only treat attribute 0002 here as motion fallback if it didn't come in through "zone status"
        if (descMap.attrId == "0002" && !description.contains("zone status")) {
            logging("0500 ${state.MFR} fallback processing value:${descMap.value}", "debug")    
            def value = Integer.parseInt(descMap.value, 16)
            if (value == 0) { motionOFF() }
            else if (value == 1) { motionON() } 
        } 
        else if (descMap.commandInt == "07" || descMap.commandInt == 7) {
            if (descMap.data && descMap.data[0] == "00") {
                logging("IAS ZONE REPORTING CONFIG RESPONSE: ", "info")
                sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])
            } else {
                logging("IAS ZONE REPORTING CONFIG FAILED", "warn")
            }
            return
        }   
        if (descMap.data) {
            logging("0500 command:${descMap.command} options:${descMap.options} clusterInt${descMap.clusterInt} data:${descMap.data}", "debug")
            return   
        }
    } 
    else if (descMap.cluster == "8034") {
        logging("8034 Unsubscribe received clusterInt:${descMap.clusterInt} data:${descMap.data}", "warn")
    } 
    else if (["0500","0013","0006","0000","0001","0402","8021","8038","8005","8013"].contains(descMap.cluster)) {
        def text = ""
        if (descMap.cluster == "8001") { text = "GENERAL" }
        else if (descMap.cluster == "8021") { text = "BIND RESPONSE" }
        else if (descMap.cluster == "8031") { text = "Link Quality" }
        else if (descMap.cluster == "8032") { text = "Routing Table" }
        else if (descMap.cluster == "8013") { text = "Multistate event" } 
       
        if (descMap.data) { text = "${text} clusterInt:${descMap.clusterInt} command:${descMap.command} options:${descMap.options} data:${descMap.data}" }
        logging("Cluster${descMap.cluster} Ignoring ${descMap.data} ${text}", "debug") 
    } 
    else {
        logging("New unknown Cluster${descMap.cluster} Detected: ${descMap}", "warn")
    }
}

void processStatus(ZoneStatus status) {
    logging("ZoneStatus Alarm1:${status.isAlarm1Set()} Alarm2:${status.isAlarm2Set()}", "debug")
    if (status.isAlarm1Set() || status.isAlarm2Set()) { motionON() }
    else { motionOFF() }
}

private void motionON() {
    def lastMotion = device.currentValue("motion")
    
    // Only execute if there is an actual state transition
    if (lastMotion != "active") {
        logging("Motion : Active", "info")
        sendEvent(name: "motion", value: "active", isStateChange: true)
        
        // Optional: Only keep this if you absolutely require a physical state verification loop
        // runIn(10, "ping") 
    } else {
        logging("Motion : Already Active (Ignored duplicate event)", "debug")
    }
}

private void motionOFF() {
    def lastMotion = device.currentValue("motion")
    
    // Only execute if there is an actual state transition
    if (lastMotion != "inactive") {
        logging("Motion : Inactive", "info")
        sendEvent(name: "motion", value: "inactive", isStateChange: true)
        
        // Optional: Only keep this if you absolutely require a physical state verification loop
        // runIn(10, "ping")
    } else {
        logging("Motion : Already Inactive (Ignored duplicate event)", "debug")
    }
}

/**
 * Update Firmware Command
 * @return List of zigbee commands
 */
List<String> updateFirmware() {
    log.info "checking for firmware updates"
    return zigbee.updateFirmware()
}


void checkPresence() {
    if (!state.tries) { state.tries = 0 } 
    state.lastPoll = new Date().format('MM/dd/yyyy h:mm a', location.timeZone) 
    def checkMin = 20
    def timeSinceLastCheckin = (now() - (state.lastCheckin ?: 0)) / 1000
    state.lastCheckInMin = timeSinceLastCheckin / 60
    
    logging("Check Presence its been ${state.lastCheckInMin} mins Timeout:${checkMin} Tries:${state.tries}", "debug")
    
    if (state.lastCheckInMin <= checkMin) { 
        state.tries = 0
        def test = device.currentValue("presence")
        if (test != "present") {
            def value = "present"
            logging("Creating presence event: ${value}", "info")
            sendEvent(name: "presence", value: value, descriptionText: "${value} ${state.version}", isStateChange: true)
            return    
        }
    }
    if (state.lastCheckInMin >= checkMin) { 
        state.tries = state.tries + 1
        if (state.tries >= 5) {
            def test = device.currentValue("presence")
            if (test != "not present") {
                def value = "not present"
                logging("Creating presence event: ${value}", "warn")
                sendEvent(name: "presence", value: value, descriptionText: "${value} ${state.version}", isStateChange: true)
                sendEvent(name: "battery", value: 0, unit: "%", descriptionText: "Simulated ${state.version}", isStateChange: true)
                state.minVoltTest = device.currentValue("batteryVoltage")   
                return 
            }
        } 
        runIn(2, "ping")
        if (state.tries < 4) {
            logging("Recovery in process Last checkin ${state.lastCheckInMin} min ago", "warn") 
            runIn(50, "checkPresence")
        }
    }
}

void getIcons() {
    state.icon = "<img src='https://raw.githubusercontent.com/tmastersmart/hubitat-code/main/images/3RMS16BZ.jpg' >"
    state.donate = "<a href='https://www.paypal.com/paypalme/tmastersat?locale.x=en_US'><img src='https://raw.githubusercontent.com/tmastersmart/hubitat-code/main/images/paypal2.gif'></a>"
}

void loggingUpdate() {
    logging("Logging Info:[${infoLogging}] Debug:[${debugLogging}] Trace:[${traceLogging}]", "infoBypass")
    if (debugLogging) {
        logging("Debug log:off in 3000s", "warn")
        runIn(3000, "debugLogOff")
    }
    if (traceLogging) {
        logging("Trace log: off in 1800s", "warn")
        runIn(1800, "traceLogOff")
    }
}

void traceLogOff() {
    device.updateSetting("traceLogging", [value: "false", type: "bool"])
    log.trace "${device} : Trace Logging : Automatically Disabled"
}

void debugLogOff() {
    device.updateSetting("debugLogging", [value: "false", type: "bool"])
    log.debug "${device} : Debug Logging : Automatically Disabled"
}

private void logging(String message, String level) {
    if (level == "infoBypass") { log.info  "${device} : $message" }
    if (level == "error") { log.error "${device} : $message" }
    if (level == "warn") { log.warn  "${device} : $message" }
    if (level == "trace" && traceLogging) { log.trace "${device} : $message" }
    if (level == "debug" && debugLogging) { log.debug "${device} : $message" }
    if (level == "info"  && infoLogging)  { log.info  "${device} : $message" }
}
1 Like

I tried a factory reset, and it is still not working. I am giving up, and getting one of those plug in motion sensors with an led light. It’ll be more reliable anyway being mains powered.

Thanks for your help.

Mark.