Compare commits
1 Commits
MSA-2997-3
...
MSA-2368-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01f4fffe8a |
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* Xiaomi Door/Window Sensor
|
||||
*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Based on original DH by Eric Maycock 2015 and Rave from Lazcad
|
||||
* change log:
|
||||
* added DH Colours
|
||||
* added 100% battery max
|
||||
* fixed battery parsing problem
|
||||
* added lastcheckin attribute and tile
|
||||
* added extra tile to show when last opened
|
||||
* colours to confirm to new smartthings standards
|
||||
* added ability to force override current state to Open or Closed.
|
||||
* added experimental health check as worked out by rolled54.Why
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Xiaomi Door/Window Sensor", namespace: "a4refillpad", author: "a4refillpad") {
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Contact Sensor"
|
||||
capability "Refresh"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "lastCheckin", "String"
|
||||
attribute "lastOpened", "String"
|
||||
|
||||
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"
|
||||
|
||||
command "enrollResponse"
|
||||
command "resetClosed"
|
||||
command "resetOpen"
|
||||
|
||||
}
|
||||
|
||||
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:"#e86d13"
|
||||
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc"
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
standardTile("icon", "device.refresh", inactiveLabel: false, decoration: "flat", width: 4, height: 1) {
|
||||
state "default", label:'Last Opened:', icon:"st.Entertainment.entertainment15"
|
||||
}
|
||||
valueTile("lastopened", "device.lastOpened", decoration: "flat", inactiveLabel: false, width: 4, height: 1) {
|
||||
state "default", label:'${currentValue}'
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
standardTile("resetClosed", "device.resetClosed", inactiveLabel: false, decoration: "flat", width: 3, height: 1) {
|
||||
state "default", action:"resetClosed", label: "Override Close", icon:"st.contact.contact.closed"
|
||||
}
|
||||
standardTile("resetOpen", "device.resetOpen", inactiveLabel: false, decoration: "flat", width: 3, height: 1) {
|
||||
state "default", action:"resetOpen", label: "Override Open", icon:"st.contact.contact.open"
|
||||
}
|
||||
|
||||
|
||||
main (["contact"])
|
||||
details(["contact","battery","icon","lastopened","resetClosed","resetOpen"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
|
||||
// send event for heartbeat
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
|
||||
Map map = [:]
|
||||
|
||||
log.debug "${resultMap}"
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
sendEvent(name: "lastOpened", value: now)
|
||||
}
|
||||
if (description?.startsWith('catchall:'))
|
||||
map = parseCatchAllMessage(description)
|
||||
log.debug "Parse returned $map"
|
||||
def results = map ? createEvent(map) : null
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
def volts = rawValue / 1
|
||||
def maxVolts = 100
|
||||
|
||||
if (volts > maxVolts) {
|
||||
volts = maxVolts
|
||||
}
|
||||
|
||||
result.value = volts
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (cluster) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
resultMap = getBatteryResult(cluster.data.get(23))
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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 = 0x01
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0000 0x0000", "delay 200"
|
||||
|
||||
] //+ enrollResponse()
|
||||
}
|
||||
*/
|
||||
|
||||
def refresh() {
|
||||
log.debug "refreshing"
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0", "delay 250",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
def resetClosed() {
|
||||
sendEvent(name:"contact", value:"closed")
|
||||
}
|
||||
|
||||
def resetOpen() {
|
||||
sendEvent(name:"contact", value:"open")
|
||||
}
|
||||
|
||||
def installed() {
|
||||
// Device wakes up every 1 hour, this interval allows us to miss one wakeup notification before marking offline
|
||||
log.debug "Configured health checkInterval when installed()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def updated() {
|
||||
// Device wakes up every 1 hours, this interval allows us to miss one wakeup notification before marking offline
|
||||
log.debug "Configured health checkInterval when updated()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Xiaomi Motion Sensor
|
||||
*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Based on original DH by Eric Maycock 2015
|
||||
* modified 29/12/2016 a4refillpad
|
||||
* Added fingerprinting
|
||||
* Added heartbeat/lastcheckin for monitoring
|
||||
* Added battery and refresh
|
||||
* Motion background colours consistent with latest DH
|
||||
* Fixed max battery percentage to be 100%
|
||||
* Added Last update to main tile
|
||||
* Added last motion tile
|
||||
* Heartdeat icon plus improved localisation of date
|
||||
* removed non working tiles and changed layout and incorporated latest colours
|
||||
* added experimental health check as worked out by rolled54.Why
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Xiaomi Motion Sensor", namespace: "a4refillpad", author: "a4refillpad") {
|
||||
capability "Motion Sensor"
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "lastCheckin", "String"
|
||||
attribute "lastMotion", "String"
|
||||
|
||||
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). \n\n(The device will always remain blind to motion for 60seconds following first detected motion. This value just clears the 'active' status after the number of seconds you set here but the device will still remain blind for 60seconds in normal operation.)", 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:"#00a0dc"
|
||||
attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff"
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 1) {
|
||||
state "default", action:"reset", label: "Reset Motion"
|
||||
}
|
||||
standardTile("icon", "device.refresh", inactiveLabel: false, decoration: "flat", width: 4, height: 1) {
|
||||
state "default", label:'Last Motion:', icon:"st.Entertainment.entertainment15"
|
||||
}
|
||||
valueTile("lastmotion", "device.lastMotion", decoration: "flat", inactiveLabel: false, width: 4, height: 1) {
|
||||
state "default", label:'${currentValue}'
|
||||
}
|
||||
|
||||
main(["motion"])
|
||||
details(["motion", "battery", "icon", "lastmotion", "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
|
||||
// send event for heartbeat
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
|
||||
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 getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
def volts = rawValue / 1
|
||||
|
||||
def maxVolts = 100
|
||||
|
||||
if (volts > maxVolts) {
|
||||
volts = maxVolts
|
||||
}
|
||||
|
||||
result.value = volts
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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 = 0x01
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0000 0x0000", "delay 200"
|
||||
// "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0000", "delay 200"
|
||||
] //+ enrollResponse()
|
||||
}
|
||||
|
||||
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 = [:]
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
|
||||
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"
|
||||
sendEvent(name: "lastMotion", value: now)
|
||||
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 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
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
def stopMotion() {
|
||||
sendEvent(name:"motion", value:"inactive")
|
||||
}
|
||||
|
||||
def reset() {
|
||||
sendEvent(name:"motion", value:"inactive")
|
||||
}
|
||||
|
||||
def installed() {
|
||||
// Device wakes up every 1 hour, this interval allows us to miss one wakeup notification before marking offline
|
||||
log.debug "Configured health checkInterval when installed()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def updated() {
|
||||
// Device wakes up every 1 hours, this interval allows us to miss one wakeup notification before marking offline
|
||||
log.debug "Configured health checkInterval when updated()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Copyright 2017 A4refillpad
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 2017-03 First release of the Xiaomi Temp/Humidity Device Handler
|
||||
* 2017-03 Includes battery level (hope it works, I've only had access to a device for a limited period, time will tell!)
|
||||
* 2017-03 Last checkin activity to help monitor health of device and multiattribute tile
|
||||
* 2017-03 Changed temperature to update on .1° changes - much more useful
|
||||
* 2017-03-08 Changed the way the battery level is being measured. Very different to other Xiaomi sensors.
|
||||
* 2017-03-23 Added Fahrenheit support
|
||||
* 2017-03-25 Minor update to display unknown battery as "--", added fahrenheit colours to main and device tiles
|
||||
* 2017-03-29 Temperature offset preference added to handler
|
||||
*
|
||||
* known issue: these devices do not seem to respond to refresh requests left in place in case things change
|
||||
* known issue: tile formatting on ios and android devices vary a little due to smartthings app - again, nothing I can do about this
|
||||
* known issue: there's nothing I can do about the pairing process with smartthings. it is indeed non standard, please refer to community forum for details
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Xiaomi Temperature Humidity Sensor", namespace: "a4refillpad", author: "a4refillpad") {
|
||||
capability "Temperature Measurement"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Refresh"
|
||||
|
||||
attribute "lastCheckin", "String"
|
||||
|
||||
fingerprint profileId: "0104", deviceId: "0302", inClusters: "0000,0001,0003,0009,0402,0405"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
for (int i = 0; i <= 100; i += 10) {
|
||||
status "${i}F": "temperature: $i F"
|
||||
}
|
||||
|
||||
for (int i = 0; i <= 100; i += 10) {
|
||||
status "${i}%": "humidity: ${i}%"
|
||||
}
|
||||
}
|
||||
|
||||
preferences {
|
||||
section {
|
||||
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter '-5'. If 3 degrees too cold, enter '+3'. Please note, any changes will take effect only on the NEXT temperature change.", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"temperature", type:"generic", width:6, height:4) {
|
||||
tileAttribute("device.temperature", key:"PRIMARY_CONTROL"){
|
||||
attributeState("default", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#153591"],
|
||||
[value: 5, color: "#1e9cbb"],
|
||||
[value: 10, color: "#90d2a7"],
|
||||
[value: 15, color: "#44b621"],
|
||||
[value: 20, color: "#f1d801"],
|
||||
[value: 25, color: "#d04e00"],
|
||||
[value: 30, color: "#bc2323"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}', icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
standardTile("humidity", "device.humidity", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'${currentValue}%', icon:"st.Weather.weather12"
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "default", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
valueTile("temperature2", "device.temperature", decoration: "flat", inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}°', icon: "st.Weather.weather2",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#153591"],
|
||||
[value: 5, color: "#1e9cbb"],
|
||||
[value: 10, color: "#90d2a7"],
|
||||
[value: 15, color: "#44b621"],
|
||||
[value: 20, color: "#f1d801"],
|
||||
[value: 25, color: "#d04e00"],
|
||||
[value: 30, color: "#bc2323"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["temperature2"])
|
||||
details(["temperature", "battery", "humidity","refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "RAW: $description"
|
||||
def name = parseName(description)
|
||||
log.debug "Parsename: $name"
|
||||
def value = parseValue(description)
|
||||
log.debug "Parsevalue: $value"
|
||||
def unit = name == "temperature" ? getTemperatureScale() : (name == "humidity" ? "%" : null)
|
||||
def result = createEvent(name: name, value: value, unit: unit)
|
||||
log.debug "Evencreated: $name, $value, $unit"
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
return result
|
||||
}
|
||||
|
||||
private String parseName(String description) {
|
||||
|
||||
if (description?.startsWith("temperature: ")) {
|
||||
return "temperature"
|
||||
|
||||
} else if (description?.startsWith("humidity: ")) {
|
||||
return "humidity"
|
||||
|
||||
} else if (description?.startsWith("catchall: ")) {
|
||||
return "battery"
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
private String parseValue(String description) {
|
||||
|
||||
if (description?.startsWith("temperature: ")) {
|
||||
def value = ((description - "temperature: ").trim()) as Float
|
||||
|
||||
if (getTemperatureScale() == "C") {
|
||||
if (tempOffset) {
|
||||
return (Math.round(value * 10))/ 10 + tempOffset as Float
|
||||
} else {
|
||||
return (Math.round(value * 10))/ 10 as Float
|
||||
}
|
||||
} else {
|
||||
if (tempOffset) {
|
||||
return (Math.round(value * 90/5))/10 + 32 + offset as Float
|
||||
} else {
|
||||
return (Math.round(value * 90/5))/10 + 32 as Float
|
||||
}
|
||||
}
|
||||
|
||||
} else if (description?.startsWith("humidity: ")) {
|
||||
def pct = (description - "humidity: " - "%").trim()
|
||||
|
||||
if (pct.isNumber()) {
|
||||
return Math.round(new BigDecimal(pct)).toString()
|
||||
}
|
||||
} else if (description?.startsWith("catchall: ")) {
|
||||
return parseCatchAllMessage(description)
|
||||
} else {
|
||||
log.debug "unknown: $description"
|
||||
sendEvent(name: "unknown", value: description)
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
private String parseCatchAllMessage(String description) {
|
||||
def result = '--'
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (cluster) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
result = getBatteryResult(cluster.data.get(6))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private String getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
log.debug rawValue
|
||||
|
||||
def result = '--'
|
||||
def maxBatt = 100
|
||||
def battLevel = Math.round(rawValue * 100 / 255)
|
||||
|
||||
if (battLevel > maxBatt) {
|
||||
battLevel = maxBatt
|
||||
}
|
||||
|
||||
return battLevel
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh called"
|
||||
def refreshCmds = [
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x00", "delay 2000",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 2000"
|
||||
]
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 900) // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 2000"
|
||||
]
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Xiaomi Zigbee Button
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Based on original DH by Eric Maycock 2015 and Rave from Lazcad
|
||||
* change log:
|
||||
* added 100% battery max
|
||||
* fixed battery parsing problem
|
||||
* added lastcheckin attribute and tile
|
||||
* added a means to also push button in as tile on smartthings app
|
||||
* fixed ios tile label problem and battery bug
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Xiaomi Zigbee Button", namespace: "a4refillpad", author: "a4refillpad") {
|
||||
capability "Battery"
|
||||
capability "Button"
|
||||
capability "Holdable Button"
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Momentary"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
|
||||
attribute "lastPress", "string"
|
||||
attribute "batterylevel", "string"
|
||||
attribute "lastCheckin", "string"
|
||||
|
||||
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) {
|
||||
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) {
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState("on", label:' push', action: "momentary.push", backgroundColor:"#53a7c0")
|
||||
attributeState("off", label:' push', action: "momentary.push", backgroundColor:"#ffffff", nextState: "on")
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
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, decoration: "flat", width: 2, height: 2) {
|
||||
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
}
|
||||
|
||||
main (["switch"])
|
||||
details(["switch", "battery", "refresh", "configure"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
// send event for heartbeat
|
||||
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
|
||||
def results = []
|
||||
if (description?.startsWith('on/off: '))
|
||||
results = parseCustomMessage(description)
|
||||
if (description?.startsWith('catchall:'))
|
||||
results = parseCatchAllMessage(description)
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
def configure(){
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 2 0 {${device.zigbeeId}} {}", "delay 5000",
|
||||
"zcl global send-me-a-report 2 0 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 2"
|
||||
]
|
||||
}
|
||||
|
||||
def refresh(){
|
||||
"st rattr 0x${device.deviceNetworkId} 1 2 0"
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0 0"
|
||||
log.debug "refreshing"
|
||||
|
||||
createEvent([name: 'batterylevel', value: '100', data:[buttonNumber: 1], displayed: false])
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (cluster) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0000:
|
||||
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 Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
int battValue = rawValue
|
||||
|
||||
def maxbatt = 100
|
||||
|
||||
if (battValue > maxbatt) {
|
||||
battValue = maxbatt
|
||||
}
|
||||
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: battValue,
|
||||
unit: "%",
|
||||
isStateChange:true,
|
||||
descriptionText : "${linkText} battery was ${battValue}%"
|
||||
]
|
||||
|
||||
log.debug result.descriptionText
|
||||
state.lastbatt = new Date().time
|
||||
return createEvent(result)
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
if (description == 'on/off: 0') //button pressed
|
||||
return createPressEvent(1)
|
||||
else if (description == 'on/off: 1') //button released
|
||||
return 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 createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true)
|
||||
else
|
||||
return createEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true)
|
||||
}
|
||||
|
||||
private createPressEvent(button) {
|
||||
return createEvent([name: 'lastPress', value: now(), data:[buttonNumber: button], displayed: false])
|
||||
}
|
||||
|
||||
//Need to reverse array of size 2
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
def push() {
|
||||
sendEvent(name: "switch", value: "on", isStateChange: true, displayed: false)
|
||||
sendEvent(name: "switch", value: "off", isStateChange: true, displayed: false)
|
||||
sendEvent(name: "momentary", value: "pushed", isStateChange: true)
|
||||
sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], descriptionText: "$device.displayName button 1 was pushed", isStateChange: true)
|
||||
}
|
||||
|
||||
def on() {
|
||||
push()
|
||||
}
|
||||
|
||||
def off() {
|
||||
push()
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Based on on original by Lazcad / RaveTam
|
||||
* 01/2017 corrected the temperature reading
|
||||
* 02/2017 added heartbeat to monitor connectivity health of outlet
|
||||
* 02/2017 added multiattribute tile
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Xiaomi Zigbee Outlet", namespace: "a4refillpad", author: "a4refillpad") {
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
attribute "lastCheckin", "string"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9")
|
||||
}
|
||||
}
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main (["switch", "temperature"])
|
||||
details(["switch", "temperature", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${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)
|
||||
}
|
||||
else if (description?.startsWith('on/off: ')){
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
log.debug "${resultMap}"
|
||||
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
// send event for heartbeat
|
||||
def now = new Date()
|
||||
sendEvent(name: "lastCheckin", value: now)
|
||||
|
||||
def results = map ? createEvent(map) : null
|
||||
return results;
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
|
||||
if (cluster.clusterId == 0x0006 && cluster.command == 0x01){
|
||||
def onoff = cluster.data[-1]
|
||||
if (onoff == 1)
|
||||
resultMap = createEvent(name: "switch", value: "on")
|
||||
else if (onoff == 0)
|
||||
resultMap = createEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
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(convertHexToInt(descMap.value / 2))
|
||||
}
|
||||
if (descMap.cluster == "0002" && descMap.attrId == "0000") {
|
||||
resultMap = createEvent(name: "temperature", value: zigbee.parseHATemperatureValue("temperature: " + (convertHexToInt(descMap.value) / 2), "temperature: ", getTemperatureScale()), unit: getTemperatureScale())
|
||||
log.debug "Temperature Hex convert to ${resultMap.value}%"
|
||||
}
|
||||
else if (descMap.cluster == "0008" && descMap.attrId == "0000") {
|
||||
resultMap = createEvent(name: "switch", value: "off")
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refreshing"
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 250",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 2 0", "delay 250",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0", "delay 250",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0 0"
|
||||
]
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
def result
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
if (description == 'on/off: 0')
|
||||
result = createEvent(name: "switch", value: "off")
|
||||
else if (description == 'on/off: 1')
|
||||
result = createEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
Reference in New Issue
Block a user