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" }
}