diff --git a/devicetypes/bspranger/xiaomi-aqara-door-window-sensor.src/xiaomi-aqara-door-window-sensor.groovy b/devicetypes/bspranger/xiaomi-aqara-door-window-sensor.src/xiaomi-aqara-door-window-sensor.groovy index 6605b1c4..a70e8a07 100644 --- a/devicetypes/bspranger/xiaomi-aqara-door-window-sensor.src/xiaomi-aqara-door-window-sensor.groovy +++ b/devicetypes/bspranger/xiaomi-aqara-door-window-sensor.src/xiaomi-aqara-door-window-sensor.groovy @@ -13,16 +13,16 @@ * 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 - * + * 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 + * 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") { + definition (name: "Xiaomi Aqara Door/Window Sensor", namespace: "bspranger", author: "bspranger", mnmn: "Xiaomi", vid: "generic-contact-2") { capability "Configuration" capability "Sensor" capability "Contact Sensor" @@ -86,7 +86,7 @@ metadata { } preferences { //Date & Time Config - input description: "", type: "paragraph", element: "paragraph", title: "DATE & CLOCK" + 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 @@ -96,7 +96,7 @@ metadata { 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 @@ -104,7 +104,7 @@ 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 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 @@ -113,7 +113,7 @@ def parse(String description) { Map map = [:] - // Send message data to appropriate parsing function based on the type of report + // Send message data to appropriate parsing function based on the type of report if (result) { log.debug "${device.displayName} Event: ${result}" map = getContactResult(result) @@ -135,7 +135,7 @@ def parse(String description) { // 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 + // but in the case the final zero is dropped then divide by 100 to get actual voltage value def rawVolts = rawValue / 1000 def minVolts @@ -145,12 +145,12 @@ private Map getBatteryResult(rawValue) { 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)) @@ -230,13 +230,13 @@ private Map getContactResult(result) { 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: "lastOpenedDate", value: nowDate, displayed: false) sendEvent(name: "contact", value: "open", descriptionText: "${device.displayName} was manually reset to open") } @@ -288,7 +288,7 @@ def formatDate(batteryReset) { 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 } diff --git a/devicetypes/bspranger/xiaomi-aqara-motion-sensor.src/xiaomi-aqara-motion-sensor.groovy b/devicetypes/bspranger/xiaomi-aqara-motion-sensor.src/xiaomi-aqara-motion-sensor.groovy index 1af26db6..5f59fcae 100644 --- a/devicetypes/bspranger/xiaomi-aqara-motion-sensor.src/xiaomi-aqara-motion-sensor.groovy +++ b/devicetypes/bspranger/xiaomi-aqara-motion-sensor.src/xiaomi-aqara-motion-sensor.groovy @@ -13,17 +13,17 @@ * 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 - * + * 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 + * Pairing Xiaomi sensors can be difficult as they were not designed to use with a SmartThings hub. See * */ metadata { - definition (name: "Xiaomi Aqara Motion Sensor", namespace: "bspranger", author: "bspranger") { + definition (name: "Xiaomi Aqara Motion Sensor", namespace: "bspranger", author: "bspranger", mnmn: "Xiaomi", vid: "generic-motion") { capability "Motion Sensor" capability "Illuminance Measurement" capability "Configuration" @@ -81,7 +81,7 @@ metadata { [value:500, color:"#51b8e1"], [value:750, color:"#66c1e5"], [value:1000, color:"#7ccae9"], - [value:1500, color: "#92d3ed"] + [value:1500, color: "#92d3ed"] ] } standardTile("reset", "device.reset", inactiveLabel: false, decoration:"flat", width: 2, height: 2) { @@ -102,7 +102,7 @@ metadata { input description: "This setting only changes how long MOTION DETECTED is reported in SmartThings. The sensor hardware always remains blind to motion for 60 seconds after any activity.", type: "paragraph", element: "paragraph", title: "MOTION RESET" input "motionreset", "number", title: "", description: "Enter number of seconds (default = 60)", range: "1..7200" //Date & Time Config - input description: "", type: "paragraph", element: "paragraph", title: "DATE & CLOCK" + 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 @@ -112,7 +112,7 @@ metadata { 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 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 - } + } } // Parse incoming device messages to generate events @@ -120,7 +120,7 @@ def parse(String description) { log.debug "${device.displayName} parsing: $description" // Determine current time and date in the user-selected date format and clock style - def now = formatDate() + def now = formatDate() def nowDate = new Date(now).getTime() // Any report - motion, lux & Battery - results in a lastCheckin event and update to Last Event tile @@ -129,8 +129,8 @@ def parse(String description) { sendEvent(name: "lastCheckinDate", value: nowDate, displayed: false) Map map = [:] - - // Send message data to appropriate parsing function based on the type of report + + // Send message data to appropriate parsing function based on the type of report if (description?.startsWith('illuminance:')) { map = parseIlluminance(description) } @@ -219,7 +219,7 @@ private Map parseCatchAllMessage(String description) { // 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 + // 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 @@ -228,12 +228,12 @@ private Map getBatteryResult(rawValue) { 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)) @@ -305,7 +305,7 @@ def formatDate(batteryReset) { 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 } diff --git a/devicetypes/bspranger/xiaomi-aqara-temperature-humidity-sensor.src/xiaomi-aqara-temperature-humidity-sensor.groovy b/devicetypes/bspranger/xiaomi-aqara-temperature-humidity-sensor.src/xiaomi-aqara-temperature-humidity-sensor.groovy index 6cdbbe50..6f063ebd 100644 --- a/devicetypes/bspranger/xiaomi-aqara-temperature-humidity-sensor.src/xiaomi-aqara-temperature-humidity-sensor.groovy +++ b/devicetypes/bspranger/xiaomi-aqara-temperature-humidity-sensor.src/xiaomi-aqara-temperature-humidity-sensor.groovy @@ -13,17 +13,17 @@ * 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, cscheiene, gn0st1c, foz333, jmagnuson, rinkek, ronvandegraaf, snalee, tmleafs, twonk, & veeceeoh - * + * Additional contributions to code by alecm, alixjg, bspranger, cscheiene, 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 + * Pairing Xiaomi sensors can be difficult as they were not designed to use with a SmartThings hub. See * */ metadata { - definition (name: "Xiaomi Aqara Temperature Humidity Sensor", namespace: "bspranger", author: "bspranger") { + definition (name: "Xiaomi Aqara Temperature Humidity Sensor", namespace: "bspranger", author: "bspranger", ocfDeviceType: "x.com.st.d.sensor.multifunction") { capability "Temperature Measurement" capability "Relative Humidity Measurement" capability "Sensor" @@ -138,7 +138,7 @@ metadata { input name:"PressureUnits", type:"enum", title:"Pressure Units", options:["mbar", "kPa", "inHg", "mmHg"], description:"Sets the unit in which pressure will be reported" input description: "NOTE: The temperature unit (C / F) can be changed in the location settings for your hub.", type: "paragraph", element: "paragraph", title: "" //Date & Time Config - input description: "", type: "paragraph", element: "paragraph", title: "DATE & CLOCK" + input description: "", type: "paragraph", element: "paragraph", title: "DATE & CLOCK" input name: "dateformat", type: "enum", title: "Set Date Format\nUS (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 @@ -156,7 +156,7 @@ def parse(String description) { log.debug "${device.displayName}: Parsing description: ${description}" // Determine current time and date in the user-selected date format and clock style - def now = formatDate() + def now = formatDate() def nowDate = new Date(now).getTime() // Any report - temp, humidity, pressure, & battery - results in a lastCheckin event and update to Last Checkin tile @@ -186,7 +186,7 @@ def parse(String description) { map = parseReadAttr(description) } else { log.debug "${device.displayName}: was unable to parse ${description}" - sendEvent(name: "lastCheckin", value: now) + sendEvent(name: "lastCheckin", value: now) } if (map) { @@ -245,7 +245,7 @@ private Map parseReadAttr(String description) { settings.PressureUnits = "mbar" } // log.debug "${device.displayName}: Converting ${pressureval} to ${PressureUnits}" - + switch (PressureUnits) { case "mbar": pressureval = (pressureval/10) as Float @@ -284,7 +284,7 @@ private Map parseReadAttr(String description) { ] } else if (cluster == "0000" && attrId == "0005") { def modelName = "" - + // Parsing the model name for (int i = 0; i < value.length(); i+=2) { def str = value.substring(i, i+2); @@ -299,7 +299,7 @@ private Map parseReadAttr(String description) { // 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 + // 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 @@ -308,12 +308,12 @@ private Map getBatteryResult(rawValue) { 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)) @@ -355,7 +355,7 @@ def resetMinMax() { def updateMinMaxTemps(temp) { temp = temp ? (int) temp : temp if ((temp > device.currentValue('maxTemp')) || (device.currentValue('maxTemp') == null)) - sendEvent(name: "maxTemp", value: temp, displayed: false) + sendEvent(name: "maxTemp", value: temp, displayed: false) if ((temp < device.currentValue('minTemp')) || (device.currentValue('minTemp') == null)) sendEvent(name: "minTemp", value: temp, displayed: false) refreshMultiAttributes() @@ -425,7 +425,7 @@ def formatDate(batteryReset) { 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 } diff --git a/devicetypes/bspranger/xiaomi-aqara-vibration-sensor.src/xiaomi-aqara-vibration-sensor.groovy b/devicetypes/bspranger/xiaomi-aqara-vibration-sensor.src/xiaomi-aqara-vibration-sensor.groovy index 1d41f2a9..75d5e9aa 100644 --- a/devicetypes/bspranger/xiaomi-aqara-vibration-sensor.src/xiaomi-aqara-vibration-sensor.groovy +++ b/devicetypes/bspranger/xiaomi-aqara-vibration-sensor.src/xiaomi-aqara-vibration-sensor.groovy @@ -1,7 +1,7 @@ /** * Xiaomi Aqara Vibration Sensor * Model DJT11LM - * Version 0.71b + * Version 0.91b * * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -19,22 +19,27 @@ * 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 + * Pairing Xiaomi sensors can be difficult as they were not designed to use with a SmartThings hub. * */ metadata { - definition (name: "Xiaomi Aqara Vibration Sensor", namespace: "bspranger", author: "bspranger") { + definition (name: "Xiaomi Aqara Vibration Sensor", namespace: "bspranger", author: "bspranger", mnmn: "Xiaomi", ocfDeviceType: "x.com.st.d.sensor.multifunction") { capability "Acceleration Sensor" capability "Battery" capability "Button" capability "Configuration" + capability "Contact Sensor" capability "Health Check" capability "Motion Sensor" + capability "Refresh" capability "Sensor" capability "Three Axis" attribute "accelSensitivity", "String" + attribute "angleX", "number" + attribute "angleY", "number" + attribute "angleZ", "number" attribute "batteryRuntime", "String" attribute "lastCheckin", "String" attribute "lastCheckinCoRE", "Date" @@ -48,12 +53,14 @@ metadata { attribute "lastVibrationCoRE", "Date" attribute "tiltAngle", "String" attribute "sensorStatus", "enum", ["vibrating", "tilted", "dropped", "Stationary"] - attribute "vibrationLevel", "String" + attribute "activityLevel", "String" fingerprint endpointId: "01", profileId: "0104", deviceId: "000A", inClusters: "0000,0003,0019,0101", outClusters: "0000,0004,0003,0005,0019,0101", manufacturer: "LUMI", model: "lumi.vibration.aq1", deviceJoinName: "Xiaomi Aqara Vibration Sensor" command "resetBatteryRuntime" command "changeSensitivity" + command "setOpenPosition" + command "setClosedPosition" } simulator { @@ -63,11 +70,11 @@ metadata { // Motion used for sensor vibration/shake events multiAttributeTile(name:"sensorStatus", type: "lighting", width: 6, height: 4) { tileAttribute("device.sensorStatus", key: "PRIMARY_CONTROL") { - attributeState "default", label:'Stationary', icon:"st.motion.motion.active", backgroundColor:"#ffffff" - attributeState "Vibration", label:'Vibration/Shock', icon:"st.motion.motion.active", backgroundColor:"#00a0dc" - attributeState "Tilt", label:'Tilt', icon:"st.motion.motion.active", backgroundColor:"#00a0dc" - attributeState "Drop", label:'Drop', icon:"st.motion.motion.active", backgroundColor:"#00a0dc" - attributeState "Stationary", label:'Stationary', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + attributeState "default", label:'Stationary', backgroundColor:"#ffffff" + attributeState "Vibration", label:'Vibration', backgroundColor:"#00a0dc" + attributeState "Tilt", label:'Tilt', backgroundColor:"#00a0dc" + attributeState "Drop", label:'Drop', backgroundColor:"#00a0dc" + attributeState "Stationary", label:'Stationary', backgroundColor:"#ffffff" } tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { attributeState("lastCheckin", label:'Last Event: ${currentValue}') @@ -81,54 +88,53 @@ metadata { [value: 51, color: "#44b621"] ] } - // Motion for vibration events (NOT USED) - standardTile("motion", "device.motion", width: 2, height: 2) { - state("active", label: 'Vibration Detected', icon: "st.motion.motion.active", backgroundColor: "#00a0dc") - state("inactive", label: 'No Vibration', icon: "st.motion.motion.inactive", backgroundColor: "#ffffff") + standardTile("contact", "device.contact", decoration: "flat", width: 2, height: 2) { + state("default", label: 'Unknown', icon: "st.contact.contact.closed", backgroundColor: "#cccccc") + state("open", label: 'Open', icon: "st.contact.contact.open", backgroundColor: "#e86d13") + state("closed", label: 'Closed', icon: "st.contact.contact.closed", backgroundColor: "#00a0dc") + state("unknown", label: 'Unknown', icon: "st.contact.contact.closed", backgroundColor: "#cccccc") } - // Acceleration for tilt events (NOT USED) - standardTile("acceleration", "device.acceleration", width: 2, height: 2) { - state("active", label: '-', icon: "st.motion.acceleration.active", backgroundColor: "#00a0dc") - state("inactive", label: 'Tilt Detected', icon: "st.motion.acceleration.inactive", backgroundColor: "#ffffff") + standardTile("motion", "device.motion", decoration: "flat", width: 2, height: 2) { + state("active", label: 'Motion', icon:"st.motion.motion.active", backgroundColor:"#00a0dc") + state("inactive", label: '${name}', icon: "st.motion.motion.inactive", backgroundColor: "#cccccc") } - // Button Capability used for drop events (NOT USED) - standardTile("button", "device.button", width: 2, height: 2) { - state("default", label: '-', backgroundColor: "#ffffff") - state("pushed", label: 'Drop Detected', backgroundColor: "#00a0dc") + standardTile("acceleration", "device.acceleration", decoration: "flat", width: 2, height: 2) { + state("active", label: 'Accel', icon:"st.motion.acceleration.active", backgroundColor:"#00a0dc") + state("inactive", label: '${name}', icon: "st.motion.acceleration.inactive", backgroundColor: "#cccccc") } - valueTile("vibrationLevel", "device.vibrationLevel", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "default", label:'Vibration Level:\n${currentValue}' + standardTile("setClosedPosition", "device.setClosedPosition", decoration: "flat", width: 2, height: 2) { + state "default", action:"setClosedPosition", icon: "st.contact.contact.closed", label:'Set Close' } - valueTile("tiltAngle", "device.tiltAngle", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "default", label:'Angle Change:\n${currentValue}°' + standardTile("setOpenPosition", "device.setOpenPosition", decoration: "flat", width: 2, height: 2) { + state "default", action:"setOpenPosition", icon: "st.contact.contact.open", label:'Set Open' } - valueTile("threeAxis", "device.threeAxis", decoration: "flat", inactiveLabel: false, width: 4, height: 2) { - state "default", label:'3-Axis:\n${currentValue}' - } - valueTile("accelSensitivity", "device.accelSensitivity", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + valueTile("accelSensitivity", "device.accelSensitivity", decoration: "flat", width: 2, height: 2) { state "default", action: "changeSensitivity", label:'Sensitivity\nLevel:\n${currentValue}' } - valueTile("lastVibration", "device.lastVibration", decoration: "flat", inactiveLabel: false, width: 4, height: 1) { - state "default", label:'Last Vibration Event:\n${currentValue}' + valueTile("activityLevel", "device.activityLevel", decoration: "flat", width: 2, height: 2) { + state "default", label:'Recent Activity Level:\n${currentValue}' + } + valueTile("tiltAngle", "device.tiltAngle", decoration: "flat", width: 2, height: 2) { + state "default", label:'Last Angle Change:\n${currentValue}°' } - valueTile("lastTilt", "device.lastTilt", decoration: "flat", inactiveLabel: false, width: 4, height: 1) { - state "default", label:'Last Tilt Event:\n${currentValue}' + valueTile("threeAxis", "device.threeAxis", decoration: "flat", width: 4, height: 1) { + state "default", label:'3-Axis Angles: ${currentValue}' } - valueTile("lastDrop", "device.lastDrop", decoration: "flat", inactiveLabel: false, width: 4, height: 1) { - state "default", label:'Last Drop Event:\n${currentValue}' + valueTile("batteryRuntime", "device.batteryRuntime", decoration: "flat", width: 4, height: 1) { + state "batteryRuntime", label:'Battery Changed: ${currentValue}' } - valueTile("batteryRuntime", "device.batteryRuntime", decoration: "flat", inactiveLabel: false, width: 4, height: 1) { - state "batteryRuntime", label:'Battery Changed:\n ${currentValue}' + standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) { + state "default", action:"refresh.refresh", icon: "st.secondary.refresh" } - valueTile("spacer", "spacer", decoration: "flat", inactiveLabel: false, width: 1, height: 1) { + valueTile("spacer", "spacer", decoration: "flat", width: 1, height: 1) { state "default", label:'' } - valueTile("spacerlg", "spacerlg", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + valueTile("spacerlg", "spacerlg", decoration: "flat", width: 2, height: 2) { state "default", label:'' } main (["sensorStatus"]) - details(["sensorStatus","threeAxis","battery","tiltAngle","vibrationLevel","accelSensitivity","spacer","batteryRuntime","spacer"]) + details(["sensorStatus","motion","contact","acceleration","setClosedPosition","accelSensitivity","setOpenPosition","tiltAngle","battery","activityLevel","threeAxis","batteryRuntime","refresh"]) } preferences { @@ -203,82 +209,109 @@ private Map parseCatchAllMessage(String description) { // Parse read attr - raw messages (includes all sensor event messages and reset button press, and ) private Map parseReadAttrMessage(String description) { - 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 eventType - Map resultMap = [:] - - if (cluster == "0101") { - // Handles vibration (value 01), tilt (value 02), and drop (value 03) event messages - if (attrId == "0055") { - if (value?.endsWith('0002')) { - eventType = 2 - parseTiltAngle(value[0..3]) - } else { - eventType = Integer.parseInt(value,16) - } - resultMap = mapSensorEvent(eventType) - } - // Handles XYZ Accelerometer value messages (NEEDS FURTHER DEVELOPMENT) - else if (attrId == "0508") { - short x = (short)Integer.parseInt(value[8..11],16) - short y = (short)Integer.parseInt(value[4..7], 16) - short z = (short)Integer.parseInt(value[0..3], 16) - def axisValues = "$x,$y,$z" - def descText = ": Accelerometer axis values = $axisValues" - displayInfoLog(descText) - resultMap = [ - name: 'threeAxis', - value: axisValues, - linkText: getLinkText(device), - // handlerName: name, - isStateChange: isStateChange(device, "threeAxis", axisValues), - descriptionText: "$device.displayName$descText", - ] + 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 eventType + Map resultMap = [:] + + if (cluster == "0101") { + // Handles vibration (value 01), tilt (value 02), and drop (value 03) event messages + if (attrId == "0055") { + if (value?.endsWith('0002')) { + eventType = 2 + parseTiltAngle(value[0..3]) + } else { + eventType = Integer.parseInt(value,16) } - // Handles Vibration level messages (NEEDS TESTING TO VERIFY) - else if (attrId == "0505") { - def level = Integer.parseInt(value[0..3],16) - def descText = ": Vibration level reported at $level" - displayInfoLog(descText) - resultMap = [ - name: 'vibrationLevel', - value: level, - descriptionText: "$device.displayName$descText", - ] + resultMap = mapSensorEvent(eventType) + } + // Handles XYZ Accelerometer values + else if (attrId == "0508") { + short x = (short)Integer.parseInt(value[8..11],16) + short y = (short)Integer.parseInt(value[4..7],16) + short z = (short)Integer.parseInt(value[0..3],16) + float Psi = Math.round(Math.atan(x/Math.sqrt(z*z+y*y))*1800/Math.PI)/10 + float Phi = Math.round(Math.atan(y/Math.sqrt(x*x+z*z))*1800/Math.PI)/10 + float Theta = Math.round(Math.atan(z/Math.sqrt(x*x+y*y))*1800/Math.PI)/10 + def descText = ": Calculated angles are Psi = ${Psi}°, Phi = ${Phi}°, Theta = ${Theta}° " + displayDebugLog(": Raw accelerometer XYZ axis values = $x, $y, $z") + displayDebugLog(descText) + sendEvent(name: "angleX", value: Psi, displayed: false) + sendEvent(name: "angleY", value: Phi, displayed: false) + sendEvent(name: "angleZ", value: Theta, displayed: false) + resultMap = [ + name: 'threeAxis', + value: [Psi, Phi, Theta], + linkText: getLinkText(device), + isStateChange: true, + descriptionText: "$device.displayName$descText", + ] + if (!state.closedX || !state.openX) + displayInfoLog(": Open/Closed position is unknown because Open and/or Closed positions have not been set") + else { + def float cX = Float.parseFloat(state.closedX) + def float cY = Float.parseFloat(state.closedY) + def float cZ = Float.parseFloat(state.closedZ) + def float oX = Float.parseFloat(state.openX) + def float oY = Float.parseFloat(state.openY) + def float oZ = Float.parseFloat(state.openZ) + def float e = 10.0 // Sets range for margin of error + def ocPosition = "unknown" + if ((Psi < cX + e) && (Psi > cX - e) && (Phi < cY + e) && (Phi > cY - e) && (Theta < cZ + e) && (Theta > cZ - e)) + ocPosition = "closed" + else if ((Psi < oX + e) && (Psi > oX - e) && (Phi < oY + e) && (Phi > oY - e) && (Theta < oZ + e) && (Theta > oZ - e)) + ocPosition = "open" + else + displayDebugLog(": The current calculated angle position does not match either stored open/closed positions") + sendpositionEvent(ocPosition) } } - else if (cluster == "0000" && attrId == "0005") { - displayInfoLog(": reset button short press detected") - def modelName = "" - // Parsing the model - for (int i = 0; i < value.length(); i+=2) { - def str = value.substring(i, i+2); - def NextChar = (char)Integer.parseInt(str, 16); - modelName = modelName + NextChar - } - displayDebugLog(" reported model name:${modelName}") + // Handles Recent Activity level value messages + else if (attrId == "0505") { + def level = Integer.parseInt(value[0..3],16) + def descText = ": Recent activity level reported at $level" + displayInfoLog(descText) + resultMap = [ + name: 'activityLevel', + value: level, + descriptionText: "$device.displayName$descText", + ] + } + } + else if (cluster == "0000" && attrId == "0005") { + displayInfoLog(": reset button short press detected") + def modelName = "" + // Parsing the model + for (int i = 0; i < value.length(); i+=2) { + def str = value.substring(i, i+2); + def NextChar = (char)Integer.parseInt(str, 16); + modelName = modelName + NextChar } - return resultMap + displayDebugLog(" reported model name:${modelName}") + } + return resultMap } // Create map of values to be used for vibration, tilt, or drop event private Map mapSensorEvent(value) { - def seconds = (value == 1 || 4) ? (motionreset ? motionreset : 65) : 2 + def seconds = (value == 1 || value == 4) ? (motionreset ? motionreset : 65) : 2 def time = new Date(now() + (seconds * 1000)) def statusType = ["Stationary", "Vibration", "Tilt", "Drop", "", ""] def eventName = ["", "motion", "acceleration", "button", "motion", "acceleration"] def eventType = ["", "active", "active", "pushed", "inactive", "inactive"] def eventMessage = [" is stationary", " was vibrating or moving (Motion active)", " was tilted (Acceleration active)", " was dropped (Button pushed)", ": Motion reset to inactive after $seconds seconds", ": Acceleration reset to inactive"] - updateLastMovementEvent(statusType[value]) - if (value < 4) + if (value < 4) { sendEvent(name: "sensorStatus", value: statusType[value], descriptionText: "$device.displayName${eventMessage[value]}", isStateChange: true, displayed: (value == 0) ? true : false) + updateLastMovementEvent(statusType[value]) + } displayInfoLog("${eventMessage[value]}") if (value == 0) return - else if (value == 1) + else if (value == 1) { runOnce(time, clearmotionEvent) + state.motionactive = 1 + } else if (value == 2) runOnce(time, clearaccelEvent) else if (value == 3) @@ -303,44 +336,10 @@ private parseTiltAngle(value) { descriptionText : "$device.displayName$descText", isStateChange:true, displayed: true - ) displayInfoLog(descText) } -// This function adapted from SmartSense Multisensor device handler by SmartThings -// NOT YET USED -private List parseAxis(List attrData) { - def results = [] - def x = hexToSignedInt(attrData.find { it.attrInt == 0x0012 }?.value) - def y = hexToSignedInt(attrData.find { it.attrInt == 0x0013 }?.value) - def z = hexToSignedInt(attrData.find { it.attrInt == 0x0014 }?.value) - - if ([x, y ,z].any { it == null }) { - return [] - } - - def xyzResults = [:] - // The axises reported by the Device Handler differ from the axises reported by the sensor - xyzResults.x = z - xyzResults.y = x - xyzResults.z = y - - log.debug "${device.displayName}: parseAxis -- ${xyzResults}" - - def value = "${xyzResults.x},${xyzResults.y},${xyzResults.z}" - results << [ - name: "threeAxis", - value: value, - linkText: getLinkText(device), - descriptionText: "${getLinkText(device)} was ${value}", - handlerName: name, - isStateChange: isStateChange(device, "threeAxis", value), - displayed: false - ] - 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 @@ -376,16 +375,6 @@ private Map getBatteryResult(rawValue) { return result } -private def displayDebugLog(message) { - if (debugLogging) - log.debug "${device.displayName}${message}" -} - -private def displayInfoLog(message) { - if (infoLogging || state.prefsSetCount < 3) - log.info "${device.displayName}${message}" -} - //Reset the date displayed in Battery Changed tile to current date def resetBatteryRuntime(paired) { def newlyPaired = paired ? " for newly paired sensor" : "" @@ -404,8 +393,8 @@ def installed() { // configure() runs after installed() when a sensor is paired def configure() { displayInfoLog(": Configuring") - if (!state.sensitivity) - changeSensitivity() + mapSensorEvent(0) + refresh() init(1) checkIntervalEvent("configured") return @@ -423,18 +412,16 @@ def updated() { resetBatteryRuntime() device.updateSetting("battReset", false) } - if (!state.sensitivity) - changeSensitivity() + refresh() checkIntervalEvent("preferences updated") displayInfoLog(": Info message logging enabled") displayDebugLog(": Debug message logging enabled") } def init(displayLog) { - mapSensorEvent(0) if (!device.currentState('batteryRuntime')?.value) resetBatteryRuntime(true) - sendEvent(name: "numberOfButtons", value: 1) + sendEvent(name: "numberOfButtons", value: 1, displayed: false) } // update lastStationary, lastVibration, lastTilt, or lastDrop to current date/time @@ -449,22 +436,31 @@ def clearmotionEvent() { if (device.currentState('sensorStatus')?.value == "Vibration") mapSensorEvent(0) result = mapSensorEvent(4) + state.motionactive = 0 displayDebugLog(": Sending event $result") sendEvent(result) } def clearaccelEvent() { def result = [:] - if (device.currentState('sensorStatus')?.value == "Tilt") - mapSensorEvent(0) + if (device.currentState('sensorStatus')?.value == "Tilt") { + if (state.motionactive == 1) + sendEvent(name: "sensorStatus", value: "Vibration", displayed: false) + else + mapSensorEvent(0) + } result = mapSensorEvent(5) displayDebugLog(": Sending event $result") sendEvent(result) } def cleardropEvent() { - if (device.currentState('sensorStatus')?.value == "Drop") - mapSensorEvent(0) + if (device.currentState('sensorStatus')?.value == "Drop") { + if (state.motionactive == 1) + sendEvent(name: "sensorStatus", value: "Vibration", displayed: false) + else + mapSensorEvent(0) + } } private checkIntervalEvent(text) { @@ -474,10 +470,46 @@ private checkIntervalEvent(text) { sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) } +def setClosedPosition() { + if (device.currentValue('angleX')) { + state.closedX = device.currentState('angleX').value + state.closedY = device.currentState('angleY').value + state.closedZ = device.currentState('angleZ').value + sendpositionEvent("closed") + displayInfoLog(": Closed position successfully set") + displayDebugLog(": Closed position set to $state.closedX°, $state.closedY°, $state.closedZ°") + } + else + displayDebugLog(": Closed position NOT set because no 3-axis accelerometer reports have been received yet") +} + +def setOpenPosition() { + if (device.currentValue('angleX')) { + state.openX = device.currentState('angleX').value + state.openY = device.currentState('angleY').value + state.openZ = device.currentState('angleZ').value + sendpositionEvent("open") + displayInfoLog(": Open position successfully set") + displayDebugLog(": Open position set to $state.openX°, $state.openY°, $state.openZ°") + } + else + displayDebugLog(": Open position NOT set because no 3-axis accelerometer reports have been received yet") +} + +def sendpositionEvent(String ocPosition) { + def descText = ": Calculated position is $ocPosition" + displayInfoLog(descText) + sendEvent( + name: "contact", + value: ocPosition, + isStateChange: true, + descriptionText: "$device.displayName$descText") +} + def changeSensitivity() { - state.sensitivity = (state.sensitivity > 1 || state.sensitivity == null) ? 0 : state.sensitivity + 1 - def attrValue = [0x15, 0x0B, 0x01] - def levelText = ["Low", "Medium", "High"] + state.sensitivity = (state.sensitivity < 3) ? state.sensitivity + 1 : 1 + def attrValue = [0, 0x15, 0x0B, 0x01] + def levelText = ["", "Low", "Medium", "High"] def descText = ": Sensitivity level set to ${levelText[state.sensitivity]}" zigbee.writeAttribute(0x0000, 0xFF0D, 0x20, attrValue[state.sensitivity], [mfgCode: 0x115F]) zigbee.readAttribute(0x0000, 0xFF0D, [mfgCode: 0x115F]) @@ -491,6 +523,29 @@ def changeSensitivity() { displayInfoLog(descText) } +def displayDebugLog(String message) { + if (debugLogging) + log.debug "$device.displayName$message" +} + +def displayInfoLog(String message) { + if (infoLogging || state.prefsSetCount < 3) + log.info "$device.displayName$message" +} + +def refresh() { + displayInfoLog(": Refreshing UI display") + if (!state.sensitivity) { + state.sensitivity = 0 + changeSensitivity() + } + if (device.currentValue('tiltAngle') == null) + sendEvent(name: 'tiltAngle', value: "--", isStateChange: true, displayed: false) + if (device.currentValue('activityLevel') == null) + sendEvent(name: 'activityLevel', value: "--", isStateChange: true, displayed: false) + zigbee.readAttribute(0x0000, 0xFF0D, [mfgCode: 0x115F]) +} + def formatDate(batteryReset) { def correctedTimezone = "" def timeString = clockformat ? "HH:mm:ss" : "h:mm:ss aa" diff --git a/devicetypes/bspranger/xiaomi-door-window-sensor.src/xiaomi-door-window-sensor.groovy b/devicetypes/bspranger/xiaomi-door-window-sensor.src/xiaomi-door-window-sensor.groovy index 2071875c..440e9d73 100644 --- a/devicetypes/bspranger/xiaomi-door-window-sensor.src/xiaomi-door-window-sensor.groovy +++ b/devicetypes/bspranger/xiaomi-door-window-sensor.src/xiaomi-door-window-sensor.groovy @@ -13,8 +13,8 @@ * 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 - * + * 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 @@ -22,31 +22,31 @@ * */ metadata { - definition (name: "Xiaomi Door/Window Sensor", namespace: "bspranger", author: "bspranger") { + definition (name: "Xiaomi Door/Window Sensor", namespace: "bspranger", author: "bspranger", mnmn: "Xiaomi", vid: "generic-contact-2") { 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 "lastOpenedDate", "Date" attribute "batteryRuntime", "String" fingerprint endpointId: "01", profileId: "0104", deviceId: "0104", inClusters: "0000, 0003, FFFF, 0019", outClusters: "0000, 0004, 0003, 0006, 0008, 0005 0019", manufacturer: "LUMI", model: "lumi.sensor_magnet", deviceJoinName: "Xiaomi 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") { @@ -62,7 +62,7 @@ metadata { backgroundColors: [ [value: 10, color: "#bc2323"], [value: 26, color: "#f1d801"], - [value: 51, color: "#44b621"] + [value: 51, color: "#44b621"] ] } valueTile("spacer", "spacer", decoration: "flat", inactiveLabel: false, width: 1, height: 1) { @@ -85,7 +85,7 @@ metadata { } preferences { //Date & Time Config - input description: "", type: "paragraph", element: "paragraph", title: "DATE & CLOCK" + 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 @@ -95,7 +95,7 @@ metadata { 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 @@ -108,13 +108,13 @@ def parse(String description) { // Any report - button press & 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) + sendEvent(name: "lastCheckinDate", value: nowDate, displayed: false) Map map = [:] // Send message data to appropriate parsing function based on the type of report if (description?.startsWith('on/off: ')) { - map = parseCustomMessage(description) + map = parseCustomMessage(description) if (map.value == "open") { sendEvent(name: "lastOpened", value: now, displayed: false) sendEvent(name: "lastOpenedDate", value: nowDate, displayed: false) @@ -124,7 +124,7 @@ def parse(String description) { map = parseCatchAllMessage(description) } else if (description?.startsWith("read attr - raw: ")) { - map = parseReadAttrMessage(description) + map = parseReadAttrMessage(description) } log.debug "${device.displayName}: Parse returned $map" def results = map ? createEvent(map) : null @@ -132,7 +132,7 @@ def parse(String description) { return results; } -private Map parseReadAttrMessage(String description) { +private Map parseReadAttrMessage(String description) { 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() @@ -140,7 +140,7 @@ private Map parseReadAttrMessage(String description) { name: 'Model', value: '' ] - + if (cluster == "0000" && attrId == "0005") { // Parsing the model for (int i = 0; i < value.length(); i+=2) { @@ -155,7 +155,7 @@ private Map parseReadAttrMessage(String description) { // 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 + // 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 @@ -164,12 +164,12 @@ private Map getBatteryResult(rawValue) { minVolts = 2.5 else minVolts = voltsmin - + if(voltsmax == null || voltsmax == "") maxVolts = 3.0 else - maxVolts = voltsmax - + maxVolts = voltsmax + def pct = (rawVolts - minVolts) / (maxVolts - minVolts) def roundedPct = Math.min(100, Math.round(pct * 100)) @@ -180,7 +180,7 @@ private Map getBatteryResult(rawValue) { isStateChange: true, descriptionText : "${device.displayName} Battery at ${roundedPct}% (${rawVolts} Volts)" ] - + log.debug "${device.displayName}: ${result}" return result } @@ -230,13 +230,13 @@ private Map getContactResult(value) { 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: "lastOpenedDate", value: nowDate, displayed: false) sendEvent(name: "contact", value: "open", descriptionText: "${device.displayName} was manually reset to open") } @@ -288,7 +288,7 @@ def formatDate(batteryReset) { 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 } diff --git a/devicetypes/bspranger/xiaomi-motion-sensor.src/xiaomi-motion-sensor.groovy b/devicetypes/bspranger/xiaomi-motion-sensor.src/xiaomi-motion-sensor.groovy index 6dc28332..77299eaf 100644 --- a/devicetypes/bspranger/xiaomi-motion-sensor.src/xiaomi-motion-sensor.groovy +++ b/devicetypes/bspranger/xiaomi-motion-sensor.src/xiaomi-motion-sensor.groovy @@ -13,33 +13,33 @@ * 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 - * + * 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 + * Pairing Xiaomi sensors can be difficult as they were not designed to use with a SmartThings hub. See * */ metadata { - definition (name: "Xiaomi Motion Sensor", namespace: "bspranger", author: "bspranger") { + definition (name: "Xiaomi Motion Sensor", namespace: "bspranger", author: "bspranger", mnmn: "Xiaomi", vid: "generic-motion") { capability "Motion Sensor" capability "Configuration" capability "Battery" capability "Sensor" - capability "Health Check" - + capability "Health Check" + attribute "lastCheckin", "String" attribute "lastCheckinDate", "String" attribute "lastMotion", "String" - attribute "batteryRuntime", "String" + attribute "batteryRuntime", "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 "resetBatteryRuntime" + + command "resetBatteryRuntime" command "stopMotion" - + } simulator { @@ -87,7 +87,7 @@ metadata { input description: "This setting only changes how long MOTION DETECTED is reported in SmartThings. The sensor hardware always remains blind to motion for 60 seconds after any activity.", type: "paragraph", element: "paragraph", title: "MOTION RESET" input "motionreset", "number", title: "", description: "Enter number of seconds (default = 60)", range: "1..7200" //Date & Time Config - input description: "", type: "paragraph", element: "paragraph", title: "DATE & CLOCK" + 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 @@ -103,26 +103,26 @@ metadata { // Parse incoming device messages to generate events def parse(String description) { log.debug "${device.displayName} Parsing: $description" - + // Determine current time and date in the user-selected date format and clock style - def now = formatDate() + def now = formatDate() def nowDate = new Date(now).getTime() // Any report - motion, lux & Battery - results in a lastCheckin event and update to Last Event 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 + // Send message data to appropriate parsing function based on the type of report if (description?.startsWith('read attr -')) { map = parseReportAttributeMessage(description) } else if (description?.startsWith('catchall:')) { map = parseCatchAllMessage(description) } - + log.debug "${device.displayName} Parse returned: $map" def result = map ? createEvent(map) : null @@ -134,7 +134,7 @@ private Map parseReportAttributeMessage(String description) { 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() - + Map resultMap = [:] def now = formatDate() @@ -149,7 +149,7 @@ private Map parseReportAttributeMessage(String description) { ] sendEvent(name: "lastMotion", value: now, displayed: false) runIn(seconds, stopMotion) - } + } else if (cluster == "0000" && attrId == "0005") { def modelName = "" // Parsing the model @@ -188,7 +188,7 @@ private Map parseCatchAllMessage(String description) { // 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 + // 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 @@ -197,12 +197,12 @@ private Map getBatteryResult(rawValue) { 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)) @@ -274,7 +274,7 @@ def formatDate(batteryReset) { 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 } diff --git a/devicetypes/bspranger/xiaomi-temperature-humidity-sensor.src/xiaomi-temperature-humidity-sensor.groovy b/devicetypes/bspranger/xiaomi-temperature-humidity-sensor.src/xiaomi-temperature-humidity-sensor.groovy index 8b8b4236..8c6b8111 100644 --- a/devicetypes/bspranger/xiaomi-temperature-humidity-sensor.src/xiaomi-temperature-humidity-sensor.groovy +++ b/devicetypes/bspranger/xiaomi-temperature-humidity-sensor.src/xiaomi-temperature-humidity-sensor.groovy @@ -23,7 +23,7 @@ */ metadata { - definition (name: "Xiaomi Temperature Humidity Sensor", namespace: "bspranger", author: "bspranger") { + definition (name: "Xiaomi Temperature Humidity Sensor", namespace: "bspranger", author: "bspranger", ocfDeviceType: "x.com.st.d.sensor.multifunction") { capability "Temperature Measurement" capability "Relative Humidity Measurement" capability "Sensor" diff --git a/yeelight-white.groovy b/yeelight-white.groovy new file mode 100644 index 00000000..f48f8b12 --- /dev/null +++ b/yeelight-white.groovy @@ -0,0 +1,172 @@ + +metadata { + definition (name: "Yeelight White", namespace: "simontether", author: "Simon Tether", mnmn: "SmartThings", vid: "generic-dimmer") { + capability "Switch Level" + capability "Color Temperature" + capability "Switch" + capability "Relay Switch" + capability "Refresh" + capability "Actuator" + + command "reset" + command "refresh" + + attribute "colorName", "string" + + command "coolWhite" + command "warmWhite" + command "daylight" + } + + simulator { + } + + //Tiles on Device Panel + standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") { + state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + controlTile("colorTempControl", "device.colorTemperature", "slider", height: 1, width: 1, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"setColorTemperature" + } + standardTile("coolWhite", "device.colorName", height: 1, width: 1, decoration: "flat", inactiveLabel: false, canChangeIcon: false) { + state "off", label:"cool white", action:"coolWhite", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8", defaultState: true + //state "coolWhite", label:"cool white", action:"off", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF33CB" + } + standardTile("warmWhite", "device.colorName", height: 1, width: 1, decoration: "flat", inactiveLabel: false, canChangeIcon: false) { + state "off", label:"warm white", action:"warmWhite", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8", defaultState: true + //state "warmWhite", label:"warm white", action:"off", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF33CB" + } + standardTile("daylight", "device.colorName", height: 1, width: 1, decoration: "flat", inactiveLabel: false, canChangeIcon: false) { + state "off", label:"daylight", action:"daylight", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8", defaultState: true + //state "daylight", label:"daylight", action:"off", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF33CB" + } + main(["switch"]) + details(["switch", "levelSliderControl", "reset", "colorTempControl", "refresh", "coolWhite", "warmWhite", "daylight"]) +} + + //Settings Page + preferences { + input name: "DeviceLocalLan", type: "string", title:"Yeelight IP Address", description:"Enter Bulb's IP address", defaultValue:"", required: true, displayDuringSetup: true + input name: "defaultONLevel", type: "number", title: "Default ON Level:", description:"Enter Default ON Level", defaultValue:75, range: "1..100", required: false, displayDuringSetup: true + input name: "coolWhiteValue", type: "number", title: "Cool White Value (K):", description:"Enter Cool White Value", defaultValue:6500, range: "1700..6500", required: false, displayDuringSetup: true + input name: "warmWhiteValue", type: "number", title: "Warm White Value (K):", description:"Enter Warm White Value", defaultValue:3000, range: "1700..6500", required: false, displayDuringSetup: true + input name: "dayWhiteValue", type: "number", title: "Daylight White Value (K):", description:"Enter Daylight White Value", defaultValue:4500, range: "1700..6500", required: false, displayDuringSetup: true + } + +def installed() { + log.debug "installed" +} + +def updated() { + log.debug "updated" +} + +//Parse incoming from Yeelight -----NOT WORKING----- +def parse(String description) { + log.debug "Response '${description}'" +} + +//Reset Yeelight to White, 6000K +def reset() { + log.debug "reset" + //getProp() + delayBetween([ + on(), + setColorTemperature(6000) + ], 300) +} + +//Send Command to Yeelight +def transmit(yeelightCommand) { + def String ipaddr = DeviceLocalLan + def String hexIp = ipaddr.tokenize('.').collect {String.format('%02X', it.toInteger())}.join() + def String myNetworkID = "${hexIp}:D893" + device.deviceNetworkId = myNetworkID + log.debug "Device Network ID " + myNetworkID + def transmittedData = new physicalgraph.device.HubAction(yeelightCommand, physicalgraph.device.Protocol.LAN, myNetworkID) + log.debug "Sent " + transmittedData + return sendHubCommand(transmittedData) +} + +//Get Current Properties of Yeelight +def getProp() { + log.debug "getProp" + transmit("""{"id":1,"method":"get_prop","params":["power", "bright", "ct", "rgb", "name"]}\r\n""") +} + +//Turn Yeelight ON +def on() { + //getProp() + delayBetween([ + transmit("""{"id": 1, "method": "set_power", "params":["on", "smooth", 500]}\r\n"""), + transmit("""{"id": 1, "method": "set_bright", "params":[${defaultONLevel}, "smooth", 100]}\r\n""") + ], 300) + sendEvent(name: "switch", value: "on") + sendEvent(name: "level", value: defaultONLevel) +} + +//Turn Yeelight OFF +def off() { + //getProp() + transmit("""{"id": 1, "method": "set_power", "params":["off", "smooth", 500]}\r\n""") + sendEvent(name: "switch", value: "off") +} + +//Check if Yeelight is off and turn on if it is - called from setLevel and setColorTemp +def powerCheck() { + def powerState = device.currentValue("switch") + if (powerState == "off") { + transmit("""{"id": 1, "method": "set_power", "params":["on", "sudden"]}\r\n""") + sendEvent(name: "switch", value: "on") + } +} + +//Set Yeelight Dim Level +def setLevel(level) { + //getProp() + powerCheck() + if(level < 2) { + off() + sendEvent(name: "level", value: 0) + } + else { + transmit("""{"id": 1, "method": "set_bright", "params":[$level, "smooth", 100]}\r\n""") + sendEvent(name: "level", value: level) + } +} + +//Set Yeelight Colour Temperature +def setColorTemperature(kelvin) { + //getProp() + powerCheck() + if(kelvin > 6500) kelvin = 6500 + log.debug "setColorTemperature: ${kelvin}K" + transmit("""{"id": 1, "method": "set_ct_abx", "params":[${kelvin}, "smooth", 500]}\r\n""") + sendEvent(name: "colorTemperature", value: kelvin) +} + +// Colour Temperature Shortcuts +def coolWhite() { + setColorTemperature(coolWhiteValue) +} +def warmWhite() { + setColorTemperature(warmWhiteValue) +} +def daylight() { + setColorTemperature(dayWhiteValue) +}