Compare commits

...

1 Commits

Author SHA1 Message Date
CHARALAMPOS PANTELIDIS
5ce6fbc287 MSA-2690: Xiaomi Aqara Door/Window Sensor 2018-03-09 11:11:03 -08:00

View File

@@ -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)
}
}