Compare commits
1 Commits
MSA-2773-2
...
MSA-2690-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce6fbc287 |
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Xiaomi Aqara Door/Window Sensor
|
||||
* Version 1.1
|
||||
*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Original device handler code by a4refillpad, adapted for use with Aqara model by bspranger
|
||||
* Additional contributions to code by alecm, alixjg, bspranger, gn0st1c, foz333, jmagnuson, rinkek, ronvandegraaf, snalee, tmleafs, twonk, & veeceeoh
|
||||
*
|
||||
* Known issues:
|
||||
* Xiaomi sensors do not seem to respond to refresh requests
|
||||
* Inconsistent rendering of user interface text/graphics between iOS and Android devices - This is due to SmartThings, not this device handler
|
||||
* Pairing Xiaomi sensors can be difficult as they were not designed to use with a SmartThings hub. See
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Xiaomi Aqara Door/Window Sensor", namespace: "bspranger", author: "bspranger") {
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Contact Sensor"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "lastCheckin", "String"
|
||||
attribute "lastCheckinDate", "Date"
|
||||
attribute "lastOpened", "String"
|
||||
attribute "lastOpenedDate", "Date"
|
||||
attribute "batteryRuntime", "String"
|
||||
|
||||
fingerprint endpointId: "01", profileId: "0104", deviceId: "5F01", inClusters: "0000,0003,FFFF,0006", outClusters: "0000,0004,FFFF", manufacturer: "LUMI", model: "lumi.sensor_magnet.aq2", deviceJoinName: "Xiaomi Aqara Door Sensor"
|
||||
|
||||
command "resetBatteryRuntime"
|
||||
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.lastOpened", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'Last Opened: ${currentValue}')
|
||||
}
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}%', unit:"%", icon:"https://raw.githubusercontent.com/bspranger/Xiaomi/master/images/XiaomiBattery.png",
|
||||
backgroundColors:[
|
||||
[value: 10, color: "#bc2323"],
|
||||
[value: 26, color: "#f1d801"],
|
||||
[value: 51, color: "#44b621"]
|
||||
]
|
||||
}
|
||||
valueTile("spacer", "spacer", decoration: "flat", inactiveLabel: false, width: 1, height: 1) {
|
||||
state "default", label:''
|
||||
}
|
||||
valueTile("lastcheckin", "device.lastCheckin", decoration: "flat", inactiveLabel: false, width: 4, height: 1) {
|
||||
state "default", label:'Last Event:\n${currentValue}'
|
||||
}
|
||||
standardTile("resetClosed", "device.resetClosed", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"resetClosed", label:'Override Close', icon:"st.contact.contact.closed"
|
||||
}
|
||||
standardTile("resetOpen", "device.resetOpen", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"resetOpen", label:'Override Open', icon:"st.contact.contact.open"
|
||||
}
|
||||
valueTile("batteryRuntime", "device.batteryRuntime", inactiveLabel: false, decoration: "flat", width: 4, height: 1) {
|
||||
state "batteryRuntime", label:'Battery Changed:\n ${currentValue}'
|
||||
}
|
||||
|
||||
main (["contact"])
|
||||
details(["contact","battery","resetClosed","resetOpen","spacer","lastcheckin", "spacer", "spacer", "batteryRuntime", "spacer"])
|
||||
}
|
||||
preferences {
|
||||
//Date & Time Config
|
||||
input description: "", type: "paragraph", element: "paragraph", title: "DATE & CLOCK"
|
||||
input name: "dateformat", type: "enum", title: "Set Date Format\n US (MDY) - UK (DMY) - Other (YMD)", description: "Date Format", options:["US","UK","Other"]
|
||||
input name: "clockformat", type: "bool", title: "Use 24 hour clock?"
|
||||
//Battery Reset Config
|
||||
input description: "If you have installed a new battery, the toggle below will reset the Changed Battery date to help remember when it was changed.", type: "paragraph", element: "paragraph", title: "CHANGED BATTERY DATE RESET"
|
||||
input name: "battReset", type: "bool", title: "Battery Changed?"
|
||||
//Battery Voltage Offset
|
||||
input description: "Only change the settings below if you know what you're doing.", type: "paragraph", element: "paragraph", title: "ADVANCED SETTINGS"
|
||||
input name: "voltsmax", title: "Max Volts\nA battery is at 100% at __ volts\nRange 2.8 to 3.4", type: "decimal", range: "2.8..3.4", defaultValue: 3, required: false
|
||||
input name: "voltsmin", title: "Min Volts\nA battery is at 0% (needs replacing) at __ volts\nRange 2.0 to 2.7", type: "decimal", range: "2..2.7", defaultValue: 2.5, required: false
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
def result = zigbee.getEvent(description)
|
||||
|
||||
// Determine current time and date in the user-selected date format and clock style
|
||||
def now = formatDate()
|
||||
def nowDate = new Date(now).getTime()
|
||||
// Any report - contact sensor & Battery - results in a lastCheckin event and update to Last Checkin tile
|
||||
// However, only a non-parseable report results in lastCheckin being displayed in events log
|
||||
sendEvent(name: "lastCheckin", value: now, displayed: false)
|
||||
sendEvent(name: "lastCheckinDate", value: nowDate, displayed: false)
|
||||
|
||||
Map map = [:]
|
||||
|
||||
// Send message data to appropriate parsing function based on the type of report
|
||||
if (result) {
|
||||
log.debug "${device.displayName} Event: ${result}"
|
||||
map = getContactResult(result)
|
||||
if (map.value == "open") {
|
||||
sendEvent(name: "lastOpened", value: now, displayed: false)
|
||||
sendEvent(name: "lastOpenedDate", value: nowDate, displayed: false)
|
||||
}
|
||||
} else if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
} else if (description?.startsWith('read attr - raw:')) {
|
||||
map = parseReadAttr(description)
|
||||
}
|
||||
|
||||
log.debug "${device.displayName}: Parse returned ${map}"
|
||||
def results = map ? createEvent(map) : null
|
||||
return results
|
||||
}
|
||||
|
||||
// Convert raw 4 digit integer voltage value into percentage based on minVolts/maxVolts range
|
||||
private Map getBatteryResult(rawValue) {
|
||||
// raw voltage is normally supplied as a 4 digit integer that needs to be divided by 1000
|
||||
// but in the case the final zero is dropped then divide by 100 to get actual voltage value
|
||||
def rawVolts = rawValue / 1000
|
||||
|
||||
def minVolts
|
||||
def maxVolts
|
||||
|
||||
if(voltsmin == null || voltsmin == "")
|
||||
minVolts = 2.5
|
||||
else
|
||||
minVolts = voltsmin
|
||||
|
||||
if(voltsmax == null || voltsmax == "")
|
||||
maxVolts = 3.0
|
||||
else
|
||||
maxVolts = voltsmax
|
||||
|
||||
def pct = (rawVolts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.min(100, Math.round(pct * 100))
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: roundedPct,
|
||||
unit: "%",
|
||||
isStateChange:true,
|
||||
descriptionText : "${device.displayName} Battery at ${roundedPct}% (${rawVolts} Volts)"
|
||||
]
|
||||
|
||||
log.debug "${device.displayName}: ${result}"
|
||||
return result
|
||||
}
|
||||
|
||||
// Check catchall for battery voltage data to pass to getBatteryResult for conversion to percentage report
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def catchall = zigbee.parse(description)
|
||||
log.debug catchall
|
||||
|
||||
if (catchall.clusterId == 0x0000) {
|
||||
def MsgLength = catchall.data.size()
|
||||
// Xiaomi CatchAll does not have identifiers, first UINT16 is Battery
|
||||
if ((catchall.data.get(0) == 0x01 || catchall.data.get(0) == 0x02) && (catchall.data.get(1) == 0xFF)) {
|
||||
for (int i = 4; i < (MsgLength-3); i++) {
|
||||
if (catchall.data.get(i) == 0x21) { // check the data ID and data type
|
||||
// next two bytes are the battery voltage
|
||||
resultMap = getBatteryResult((catchall.data.get(i+2)<<8) + catchall.data.get(i+1))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
// Parse raw data on reset button press to retrieve reported battery voltage
|
||||
private Map parseReadAttr(String description) {
|
||||
log.debug "${device.displayName}: reset button press detected"
|
||||
def buttonRaw = (description - "read attr - raw:")
|
||||
Map resultMap = [:]
|
||||
|
||||
def cluster = description.split(",").find {it.split(":")[0].trim() == "cluster"}?.split(":")[1].trim()
|
||||
def attrId = description.split(",").find {it.split(":")[0].trim() == "attrId"}?.split(":")[1].trim()
|
||||
def value = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
|
||||
def model = value.split("01FF")[0]
|
||||
def data = value.split("01FF")[1]
|
||||
|
||||
if (cluster == "0000" && attrId == "0005") {
|
||||
def modelName = ""
|
||||
// Parsing the model
|
||||
for (int i = 0; i < model.length(); i+=2)
|
||||
{
|
||||
def str = model.substring(i, i+2);
|
||||
def NextChar = (char)Integer.parseInt(str, 16);
|
||||
modelName = modelName + NextChar
|
||||
}
|
||||
log.debug "${device.displayName} reported: cluster: ${cluster}, attrId: ${attrId}, value: ${value}, model:${modelName}, data:${data}"
|
||||
}
|
||||
if (data[4..7] == "0121") {
|
||||
resultMap = getBatteryResult(Integer.parseInt((data[10..11] + data[8..9]),16))
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map getContactResult(result) {
|
||||
def value = result.value == "on" ? "open" : "closed"
|
||||
def descriptionText = "${device.displayName} was ${value == "open" ? value + "ed" : value}"
|
||||
return [
|
||||
name: 'contact',
|
||||
value: value,
|
||||
isStateChange: true,
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
}
|
||||
|
||||
def resetClosed() {
|
||||
sendEvent(name: "contact", value: "closed", descriptionText: "${device.displayName} was manually reset to closed")
|
||||
}
|
||||
|
||||
def resetOpen() {
|
||||
def now = formatDate()
|
||||
def nowDate = new Date(now).getTime()
|
||||
sendEvent(name: "lastOpened", value: now, displayed: false)
|
||||
sendEvent(name: "lastOpenedDate", value: nowDate, displayed: false)
|
||||
sendEvent(name: "contact", value: "open", descriptionText: "${device.displayName} was manually reset to open")
|
||||
}
|
||||
|
||||
//Reset the date displayed in Battery Changed tile to current date
|
||||
def resetBatteryRuntime(paired) {
|
||||
def now = formatDate(true)
|
||||
def newlyPaired = paired ? " for newly paired sensor" : ""
|
||||
sendEvent(name: "batteryRuntime", value: now)
|
||||
log.debug "${device.displayName}: Setting Battery Changed to current date${newlyPaired}"
|
||||
}
|
||||
|
||||
// installed() runs just after a sensor is paired using the "Add a Thing" method in the SmartThings mobile app
|
||||
def installed() {
|
||||
state.battery = 0
|
||||
if (!batteryRuntime) resetBatteryRuntime(true)
|
||||
checkIntervalEvent("installed")
|
||||
}
|
||||
|
||||
// configure() runs after installed() when a sensor is paired
|
||||
def configure() {
|
||||
log.debug "${device.displayName}: configuring"
|
||||
state.battery = 0
|
||||
if (!batteryRuntime) resetBatteryRuntime(true)
|
||||
checkIntervalEvent("configured")
|
||||
return
|
||||
}
|
||||
|
||||
// updated() will run twice every time user presses save in preference settings page
|
||||
def updated() {
|
||||
checkIntervalEvent("updated")
|
||||
if(battReset){
|
||||
resetBatteryRuntime()
|
||||
device.updateSetting("battReset", false)
|
||||
}
|
||||
}
|
||||
|
||||
private checkIntervalEvent(text) {
|
||||
// Device wakes up every 1 hours, this interval allows us to miss one wakeup notification before marking offline
|
||||
log.debug "${device.displayName}: Configured health checkInterval when ${text}()"
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def formatDate(batteryReset) {
|
||||
def correctedTimezone = ""
|
||||
def timeString = clockformat ? "HH:mm:ss" : "h:mm:ss aa"
|
||||
|
||||
// If user's hub timezone is not set, display error messages in log and events log, and set timezone to GMT to avoid errors
|
||||
if (!(location.timeZone)) {
|
||||
correctedTimezone = TimeZone.getTimeZone("GMT")
|
||||
log.error "${device.displayName}: Time Zone not set, so GMT was used. Please set up your location in the SmartThings mobile app."
|
||||
sendEvent(name: "error", value: "", descriptionText: "ERROR: Time Zone not set, so GMT was used. Please set up your location in the SmartThings mobile app.")
|
||||
}
|
||||
else {
|
||||
correctedTimezone = location.timeZone
|
||||
}
|
||||
if (dateformat == "US" || dateformat == "" || dateformat == null) {
|
||||
if (batteryReset)
|
||||
return new Date().format("MMM dd yyyy", correctedTimezone)
|
||||
else
|
||||
return new Date().format("EEE MMM dd yyyy ${timeString}", correctedTimezone)
|
||||
}
|
||||
else if (dateformat == "UK") {
|
||||
if (batteryReset)
|
||||
return new Date().format("dd MMM yyyy", correctedTimezone)
|
||||
else
|
||||
return new Date().format("EEE dd MMM yyyy ${timeString}", correctedTimezone)
|
||||
}
|
||||
else {
|
||||
if (batteryReset)
|
||||
return new Date().format("yyyy MMM dd", correctedTimezone)
|
||||
else
|
||||
return new Date().format("EEE yyyy MMM dd ${timeString}", correctedTimezone)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user