Xiaomi Zigbee Door/Window Sensor, Motion Sensor, & Smart Button Device Type [beta]

I have right now working: Button, Motion & Door Sensors.
All of them long past the 60minute threshold.

My hope is, that they keep on working as they did once in the past.

BTW in case you want to try my enhanced device handlers (They automatically install the correct device once paired.)

Motion

[details=Summary]> metadata {

definition (name: "Xiaomi Motion", namespace: "X", author: "X") {
  capability "Motion Sensor"
  capability "Configuration"
  capability "Battery"
  capability "Sensor"
    fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006"
    fingerprint profileId: "0104", inClusters: "0000, 0003, 0006", outClusters: "0003, 0006, 0019, 0406", manufacturer: "Leviton", model: "ZSS-10", deviceJoinName: "Leviton Switch"
    fingerprint profileId: "0104", deviceId: "0104", inClusters: "0000, 0003, FFFF, 0019", outClusters: "0000, 0004, 0003, 0006, 0008, 0005, 0019", manufacturer: "LUMI", model: "lumi.sensor_motion", deviceJoinName: "Xiaomi Motion"
 command "reset"

}

simulator {
}

preferences {
input “motionReset”, “number”, title: “Number of seconds after the last reported activity to report that motion is inactive (in seconds).”, description: “”, value:120, displayDuringSetup: false
}

tiles(scale: 2) {
multiAttributeTile(name:“motion”, type: “generic”, width: 6, height: 4){
tileAttribute (“device.motion”, key: “PRIMARY_CONTROL”) {
attributeState “active”, label:‘motion’, icon:“st.motion.motion.active”, backgroundColor:"#53a7c0"
attributeState “inactive”, label:‘no motion’, icon:“st.motion.motion.inactive”, backgroundColor:"#ffffff"
}
}
valueTile(“battery”, “device.battery”, decoration: “flat”, inactiveLabel: false, width: 2, height: 2) {
state “battery”, label:’${currentValue}% battery’, unit:""
}
standardTile(“reset”, “device.reset”, inactiveLabel: false, decoration: “flat”, width: 2, height: 2) {
state “default”, action:“reset”, label: “Reset Motion” //icon:“st.secondary.refresh”
}

  main(["motion"])
  details(["motion", "reset"])

}
}

def parse(String description) {
log.debug "description: $description"
def value = zigbee.parse(description)?.text
log.debug "Parse: $value"
Map map = [:]
if (description?.startsWith(‘catchall:’)) {
map = parseCatchAllMessage(description)
}
else if (description?.startsWith(‘read attr -’)) {
map = parseReportAttributeMessage(description)
}

log.debug "Parse returned $map"
def result = map ? createEvent(map) : null

if (description?.startsWith('enroll request')) {
	List cmds = enrollResponse()
    log.debug "enroll response: ${cmds}"
    result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
   return result

}

private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
log.debug cluster
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
resultMap = getBatteryResult(cluster.data.last())
break

  	case 0xFC02:
  	log.debug 'ACCELERATION'
  	break
  	case 0x0402:
  	log.debug 'TEMP'
  		// temp is last 2 data values. reverse to swap endian
  		String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
  		def value = getTemperature(temp)
  		resultMap = getTemperatureResult(value)
  		break
  }

}

return resultMap
}

private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
}

private Map parseReportAttributeMessage(String description) {
Map descMap = (description - “read attr - “).split(”,”).inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
//log.debug “Desc Map: $descMap”

Map resultMap = [:]

if (descMap.cluster == “0001” && descMap.attrId == “0020”) {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
}
else if (descMap.cluster == “0406” && descMap.attrId == “0000”) {
def value = descMap.value.endsWith(“01”) ? “active” : "inactive"
if (settings.motionReset == null || settings.motionReset == “” ) settings.motionReset = 120
if (value == “active”) runIn(settings.motionReset, stopMotion)
resultMap = getMotionResult(value)
}
return resultMap
}

private Map parseCustomMessage(String description) {
Map resultMap = [:]
return resultMap
}

private Map parseIasMessage(String description) {
List parsedMsg = description.split(’ ')
String msgCode = parsedMsg[2]

Map resultMap = [:]
switch(msgCode) {
    case '0x0020': // Closed/No Motion/Dry
    	resultMap = getMotionResult('inactive')
        break
    case '0x0021': // Open/Motion/Wet
    	resultMap = getMotionResult('active')
        break
    case '0x0022': // Tamper Alarm
    	log.debug 'motion with tamper alarm'
    	resultMap = getMotionResult('active')
        break
    case '0x0023': // Battery Alarm
        break
    case '0x0024': // Supervision Report
    	log.debug 'no motion with tamper alarm'
    	resultMap = getMotionResult('inactive')
        break
    case '0x0025': // Restore Report
        break
    case '0x0026': // Trouble/Failure
    	log.debug 'motion with failure alarm'
    	resultMap = getMotionResult('active')
        break
    case '0x0028': // Test Mode
        break
}
return resultMap

}

private Map getBatteryResult(rawValue) {
log.debug 'Battery’
def linkText = getLinkText(device)

log.debug rawValue

def result = [
name: ‘battery’,
value: ‘–’
]

def volts = rawValue / 10
def descriptionText

if (rawValue == 0) {}
else {
if (volts > 3.5) {
result.descriptionText = “${linkText} battery has too much power (${volts} volts).”
}
else if (volts > 0){
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = “${linkText} battery was ${result.value}%”
}
}

return result
}

private Map getMotionResult(value) {
log.debug 'motion’
String linkText = getLinkText(device)
String descriptionText = value == ‘active’ ? “${linkText} detected motion” : "${linkText} motion has stopped"
def commands = [
name: ‘motion’,
value: value,
descriptionText: descriptionText
]
return commands
}

def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = []
return configCmds + refresh() // send refresh cmds as part of config
}

def enrollResponse() {
log.debug “Sending enroll response”
}

def stopMotion() {
sendEvent(name:“motion”, value:“inactive”)
}

def reset() {
sendEvent(name:“motion”, value:“inactive”)
}[/details]

Button:

[details=Summary]> metadata {

definition (name: “Xiaomi Button”, namespace: “X”, author: “X”) {
capability "Button"
capability "Configuration"
capability "Sensor"
capability "Refresh"
capability “Polling”

    attribute "lastPress", "string"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
    fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
    fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb"
    fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ"
    fingerprint profileId: "0104", deviceId: "0104", inClusters: "0000, 0003", outClusters: "0000, 0004, 0003, 0006, 0008, 0005", manufacturer: "LUMI", model: "lumi.sensor_switch", deviceJoinName: "Xiaomi Button"

}

simulator {
    status "button 1 pressed": "on/off: 0"
  status "button 1 released": "on/off: 1"
}

preferences{
	input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"",
    		defaultValue: 4, displayDuringSetup: false)
}

tiles(scale: 2) {
standardTile(“button”, “device.button”, decoration: “flat”, width: 2, height: 2) {
state “default”, icon: “st.unknown.zwave.remote-controller”, backgroundColor: “#ffffff
}
standardTile(“refresh”, “device.refresh”, inactiveLabel: false, decoration: “flat”, width: 2, height: 2) {
state “default”, action:“refresh.refresh”, icon:“st.secondary.refresh”
}

  main (["button"])
  details(["button","refresh"])

}
}

def parse(String description) {
log.debug "Parsing ‘${description}’"
def descMap = zigbee.parseDescriptionAsMap(description)
def results = []
if (description?.startsWith('on/off: '))
results = parseCustomMessage(description)
return results;
}

def configure(){
refresh()
}

def refresh(){
}

private Map parseCustomMessage(String description) {
if (description?.startsWith('on/off: ')) {
if (description == ‘on/off: 0’) //button pressed
createPressEvent(1)
else if (description == ‘on/off: 1’) //button released
createButtonEvent(1)
}
}

//this method determines if a press should count as a push or a hold and returns the relevant event type
private createButtonEvent(button) {
def currentTime = now()
def startOfPress = device.latestState(‘lastPress’).date.getTime()
def timeDif = currentTime - startOfPress
def holdTimeMillisec = (settings.holdTime?:3).toInteger() * 1000

if (timeDif < 0) 
	return []	//likely a message sequence issue. Drop this press and wait for another. Probably won't happen...
else if (timeDif < holdTimeMillisec) 
	return createButtonPushedEvent(button)
else 
	return createButtonHeldEvent(button)

}

private createPressEvent(button) {
return createEvent([name: ‘lastPress’, value: now(), data:[buttonNumber: button], displayed: false])
}

private createButtonPushedEvent(button) {
log.debug "Button ${button} pushed"
return createEvent([
name: “button”,
value: “pushed”,
data:[buttonNumber: button],
descriptionText: “${device.displayName} button ${button} was pushed”,
isStateChange: true,
displayed: true])
}

private createButtonHeldEvent(button) {
log.debug "Button ${button} held"
return createEvent([
name: “button”,
value: “held”,
data:[buttonNumber: button],
descriptionText: “${device.displayName} button ${button} was held”,
isStateChange: true])
}[/details]

Door:

[details=Summary]> metadata {

definition (name: “Xiaomi Door/Window Sensor”, namespace: “X”, author: “X”) {
capability "Configuration"
capability "Sensor"
capability "Contact Sensor"
capability "Refresh"
capability “Polling”

command “enrollResponse”

 fingerprint profileId: "0104", deviceId: "0104", inClusters: "0000, 0003", outClusters: "0000, 0004, 0003, 0006, 0008, 0005", manufacturer: "LUMI", model: "lumi.sensor_magnet", deviceJoinName: "Xiaomi Door Sensor"

//fingerprint endpointId: “01”, inClusters: “0000,0001”, outClusters: “1234”//, model: “3320-L”, manufacturer: “CentraLite”
//fingerprint endpoint: “01”,
//profileId: “0104”,
//inClusters: “0000,0001”
//outClusters: “1234”

}

simulator {
status “closed”: "on/off: 0"
status “open”: “on/off: 1”
}

tiles(scale: 2) {
multiAttributeTile(name:“contact”, type: “generic”, width: 6, height: 4){
tileAttribute (“device.contact”, key: “PRIMARY_CONTROL”) {
attributeState “open”, label:’${name}’, icon:“st.contact.contact.open”, backgroundColor:"#ffa81e"
attributeState “closed”, label:’${name}’, icon:“st.contact.contact.closed”, backgroundColor:"#79b821"
}
}
standardTile(“refresh”, “device.refresh”, inactiveLabel: false, decoration: “flat”, width: 2, height: 2) {
state “default”, action:“refresh.refresh”, icon:“st.secondary.refresh”
}
standardTile(“configure”, “device.configure”, inactiveLabel: false, width: 2, height: 2, decoration: “flat”) {
state “configure”, label:’’, action:“configuration.configure”, icon:“st.secondary.configure”
}

  main (["contact"])
  details(["contact","refresh","configure"])

}
}

def parse(String description) {
log.debug "Parsing ‘${description}’"
Map map = [:]
//def descMap = zigbee.parseDescriptionAsMap(description)
def resultMap = zigbee.getKnownDescription(description)
log.debug "${resultMap}"
if (description?.startsWith('on/off: '))
map = parseCustomMessage(description)
log.debug "Parse returned $map"
def results = map ? createEvent(map) : null
return results;
}

def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "${device.deviceNetworkId}"
def endpointId = 1
log.debug "${device.zigbeeId}"
log.debug "${zigbeeEui}“
def configCmds = [
//battery reporting and heartbeat
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}”, “delay 200”,
“zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}”, “delay 200”,
“send 0x${device.deviceNetworkId} 1 ${endpointId}”, “delay 1500”,

  	// Writes CIE attribute on end device to direct reports to the hub's EUID
  	"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
  	"send 0x${device.deviceNetworkId} 1 1", "delay 500",

]

log.debug "configure: Write IAS CIE"
return configCmds
}

def enrollResponse() {
log.debug “Enrolling device into the IAS Zone”
[
// Enrolling device into the IAS Zone
"raw 0x500 {01 23 00 00 00}", “delay 200”,
“send 0x${device.deviceNetworkId} 1 1”
]
}

def refresh() {
log.debug "Refreshing Battery"
def endpointId = 1
[
“st rattr 0x${device.deviceNetworkId} ${endpointId} 1 0x20”, “delay 200”
] + enrollResponse()
}

private Map parseCustomMessage(String description) {
def result
if (description?.startsWith('on/off: ')) {
if (description == ‘on/off: 0’) //contact closed
result = getContactResult(“closed”)
else if (description == ‘on/off: 1’) //contact opened
result = getContactResult(“open”)
return result
}
}

private Map getContactResult(value) {
def linkText = getLinkText(device)
def descriptionText = "${linkText} was ${value == ‘open’ ? ‘opened’ : ‘closed’}"
return [
name: ‘contact’,
value: value,
descriptionText: descriptionText
]
}

private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
private getEndpointId() {
new BigInteger(device.endpointId, 16).toString()
}

Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}

private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;

while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j–;
i++;
}

return array
}[/details]

1 Like

What type of batteries do these use?

I’ve had a motion sensor working for days now, but it’s literally sitting on the hub so…

I believe the door sensor uses a CR1632 battery (don’t own one myself, so not 100% sure)

I don’t own the sensor yet, but when trying to create the handler i get this error:

Grails.validation.ValidationException: Validation Error(s) occurred during save(): - Field error in object ‘physicalgraph.device.DeviceType’ on field ‘author’: rejected value [null]; codes

And then multiple times

Edit:
Figured it out, it was missing the author:

definition (name: “Xiaomi Door/Window Sensor”, namespace: “Xiaomi”, author: “AlmostSerious”)

Be aware that with “2pcs” they (Banggood) mean a sensor and a magnet. Basically a pair.
Quite a few customers there felt tricked and got compensation.
Weird they didn’t update the product page…

Not sure about Gearbest.They clearly state about two.

Package Contents: 2 x Xiaomi Smart Door and Window Sensor

Sorry, my mistake… forgot to add namespace and author. Its edited now :slight_smile:

1 Like

They’re still working fine?

So far so good. Even showing as ACTIVE in the device list :slight_smile:
However, who knows if the next update might brake it again …

1 Like

Well, I ordered a button and a motion detectors that’s really all I need right now.

Thx for the updates!

My button is still working well.
I’m going to order some more. Do the motion sensors also report temperature?
Martin

No battery or temperature, with the DH I use anyway.

I also wasn’t able to find the battery and other items. Anyway seems like the devices do not report it anyway… looking at the zigbee fingerprint i cannot see the correct values.

Thanks @almostserious for pioneering the fix for these (cheap) devices. Apologies for the newbie question, but what are the steps to get the button working? I’ve copied the code and created and published the device handler in the graph.api.smartthings.com site, and then tried to add from the mobile device a bunch of times, and have reset the button a bunch as well. Am I missing something? Or do I just need to keep adding/resetting until ST finds the button?

I recommend keeping the life logging website of Smartthings open to check the progress. I have the best success with a long press reset and then several short presses. But you need to be persistent… it sometimes takes quite a while. Have not yet cracked the fool proof pairing process. Sometimes Smartthings will catch the device with the catchall Zigbee in the log but it will not pair it. Then you need to reset again and try again.

I can also confirm two buttons installed and working for over 24 hours.

I’m going to have to order some of the Xiaomi devices if they are now
working. This is great news!

1 Like

I’m connected! Thanks again!!!

Not sure if it belongs here, but it’s also Xiaomi: the (zigbee plug), anyone tried it?

(still looking for a cheap zigbee extender)

Edit
Just got a reply, it cannot act as a zigbee repeater.
Isn’t that just weird? This means the whole Xiaomi system does not use a mesh at all ? Ah well

I dont know if is already known in this thread… but I think that the solution for this sensors may be in using xiaomi hub connected via MQTTT
somthing is already beeing developed for Home Assistant

1 Like