├── AeotecMultiSensor6.groovy ├── README.md ├── docs ├── AeotecMultiSensor6.json ├── version2.json └── versions.json └── v1_7 └── AeotecMultiSensor6.groovy /AeotecMultiSensor6.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * IMPORT URL: https://raw.githubusercontent.com/HubitatCommunity/AeotecMultiSensor6/master/AeotecMultisensor6.groovy 3 | * 4 | * Copyright 2015 SmartThings 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | * 16 | * v2.1.0 Added in optional Auto No Motion timer. 17 | * Remove UpdateCheck, rely on HPM to check for a new version. 18 | * Refactor methods using createEvent to use sendEvent. 19 | * 20 | * v2.0.2 Extended Motion Sensitivity to allow 0 (zero) which might be "off" depending on firmware. 21 | * v2.0.1 Added in optional Param Set / Get commands. 22 | * Added Fingerprint for this device type. 23 | * Added Limit Check on Temp, Illuminance, Humitity and UV values for wildly out of range cases. 24 | * Integrated portions of basicZWaveTool (2020-08-14 maxwell & Copyright 2016 -> 2020 Hubitat Inc. All Rights Reserved) 25 | * v2.0.0 Converted to Hubitat Security method [zwaveSecureEncap()] for S2+S0 This device still only offers unsecure or S0. 26 | * 27 | * v1.7.1 removed Preference hiding (settingEnable) 28 | * LED Options reflect firmware v1.3 specs. (On or Off only) 29 | * Mostion Sensitivity changed to Enum 30 | * v1.7 refactored using inputValidationCheck 31 | * added de-duplication code for Motion Events (motion Events can occur in <1ms) 32 | * | Motion reports (unsolicited) are duplicated: a NotificationReport (7) plus either: 33 | * | (1) for basicSet; (2) for sensorBinary set via Param 5. 34 | * | Motion Detection via NotificationReport can be considered a duplicate. 35 | * | This version removes motion event processing from NotificationReport relying 36 | * | on basicSet or SensorBinary. 37 | * | Added detail to Motion is... to identify the path. 38 | * removed duplicate call to config() 39 | * lots of cosmetic formatting cleanup, cosmetic reordering of modules 40 | * 41 | * v1.6.13 version report NPE (thanks Christi999) 42 | * tempOffset to "as Int" 43 | * v1.6.12 corrected NPE from malformed Packet. (thanks Mike Maxwell) 44 | * v1.6.11 corrected ledOption scaling. (thanks LJP-hubitat) 45 | * BAB v1.6.10a Changed to HALF_ROUND_UP, standardized DescriptionText 46 | * v1.6.10 Swapped to latest updateCheck() code. 47 | * v1.6.9 Added Initialize to preset ledOptions to prevent an NPE when sending the option value to the device. 48 | * revised updateCheck to use version2.json's format. 49 | * v1.6.8 revised Contact Sensor per @Wounded suggestion, restoring tamperAlert 50 | * v1.6.7 replaced updateCheck with asynchttp version -- removed setVersion, etc. 51 | * added descTextEnable as option to reduce log.info 52 | * v1.6.6 corrected limitation on Humidity Offset 53 | * v1.6.5 alternate description for settingEnabled input 54 | * v1.6.4 corrected temp offset -128 to 127 in configure() 55 | * v1.6.3 added Preference hiding (settingEnable) 56 | * v1.6.2 removed mapping to ledOption(s) 57 | * v1.6.1 added degree symbol to temp scale 58 | * v1.6 deleted isStateChange throughout 59 | * v1.5 Added LED Options 60 | * v1.4 Added selectiveReport, enabled humidChangeAmount, luxChangeAmount 61 | * v1.3 Restored if (debugOutput) log.debug as default logging is too much. cSteele 62 | * v1.2 Merged Chuckles updates 63 | * 64 | * Chuckles V1.2 of multisensor 6 65 | * IMPORTANT NOTE: Assumes device has firmware version 1.10 or later. A warning will be logged if this is not the case. 66 | * Changes: 67 | 1. Implement standard "Power Source" capability to track whether USB power ("dc") or battery is in use 68 | - Remove custom "powerSupply" and "batteryStatus" attributes it replaces. 69 | 2. Use hub's timezone rather than setting a timezone for each device individually 70 | 3. Use hub's temperature scale setting rather than setting each device individually 71 | 4. Add event handler for ManufacturerSpecificReport (V1) 72 | 5. Corrected configuration parameter number (4) for PIR sensitivity 73 | 6. Corrected getTimeOptionValueMap 74 | 7. Disabled selective reporting on threshold 75 | 8. Device only supports V1 of COMMAND_CLASS_SENSOR_BINARY (0x30) - not V2 76 | 9. Device only supports V1 of COMMAND_CLASS_CONFIGURATION (0x70) - not V2 77 | 10. Motion detection in NotificationReportV3 is event 8, not event 7 78 | 11. Numerous minor bug fixes (e.g. motionDelayTime and reportInterval should always hold enumerated values, not number of seconds) 79 | 12. Numerous small changes for internationalisation (e.g. region agnostic date formats) 80 | 81 | * v1.1d Added remote version checking ** Cobra (CobraVmax) for his original version check code 82 | * csteele v1.1c converted to Hubitat. 83 | * 84 | * LGK V1 of multisensor 6 with customizeable settings , changed some timeouts, also changed tamper to vibration so we can 85 | * use that as well (based on stock device type and also some changes copied from Robert Vandervoort device type. 86 | * Changes 87 | 1. changes colors of temp slightly, add colors to humidity, add color to battery level 88 | 2. remove tamper and instead use feature as contact and acceleration ie vibration sensor 89 | 3. slightly change reporting interval times. (also fix issue where 18 hours was wrong time) 90 | 4. add last update time.. sets when configured and whenever temp is reported. 91 | This is used so at a glance you see the last update time tile to know the device is still reporting easily without looking 92 | at the logs. 93 | 5. add a temp and humidity offset -12 to 12 to enable tweaking the settings if they appear off. 94 | 6. added power status tile to display, currently was here but not displayed. 95 | 7. added configure and refresh tiles. 96 | 8. also added refresh capability so refresh can be forced from other smartapps like pollster. (refresh not currently working all the time for some reason) 97 | 9. changed the sensitivity to have more values than min max etc, now is integer 0 - 127. 98 | 10. fix uv setting which in one release was value of 2 now it is 16. 99 | 11. added icons for temp and humidity 100 | 12. also change the default wakeup time to be the same as the report interval, 101 | otherwise when on battery it disregarded the report interval. (only if less than the default 5 minutes). 102 | 13. added a config option for the min change needed to report temp changes and set it in parameter 41. 103 | 14. incresed range and colors for lux values, as when mine is in direct sun outside it goes as high as 1900 104 | 15. support for celsius added. set in input options. 105 | */ 106 | 107 | public static String version() { return "v2.1.0" } 108 | import groovy.transform.Field 109 | 110 | metadata { 111 | definition (name: "AeotecMultiSensor6", namespace: "cSteele", author: "cSteele", importUrl: "https://raw.githubusercontent.com/HubitatCommunity/AeotecMultiSensor6/master/AeotecMultisensor6.groovy") { 112 | capability "Motion Sensor" 113 | capability "Temperature Measurement" 114 | capability "Relative Humidity Measurement" 115 | capability "Illuminance Measurement" 116 | capability "Ultraviolet Index" 117 | capability "Configuration" 118 | capability "Sensor" 119 | capability "Battery" 120 | capability "Power Source" 121 | capability "Acceleration Sensor" 122 | capability "TamperAlert" 123 | 124 | command "refresh" 125 | command "getParameterReport", [[name:"parameterNumber",type:"NUMBER", description:"Parameter Number (omit for a complete listing of parameters that have been set)", constraints:["NUMBER"]]] 126 | command "setParameter",[[name:"parameterNumber",type:"NUMBER", description:"Parameter Number", constraints:["NUMBER"]],[name:"size",type:"NUMBER", description:"Parameter Size", constraints:["NUMBER"]],[name:"value",type:"NUMBER", description:"Parameter Value", constraints:["NUMBER"]]] 127 | 128 | attribute "firmware", "decimal" 129 | 130 | fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A" 131 | fingerprint mfr:"0086", prod:"0102", deviceId:"0064", inClusters:"0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A,0x5A" 132 | } 133 | 134 | 135 | preferences { 136 | // Note: Hubitat doesn't appear to honour 'sections' in device handler preferences just now, but hopefully one day... 137 | section("Motion sensor settings") { 138 | input "motionDelayTime", "enum", title: "Motion Sensor Delay Time?", 139 | options: ["20 seconds", "30 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"], defaultValue: "1 minute", displayDuringSetup: true 140 | input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity?", options: [5:"Very High", 4:"High", 3:"Medium High", 2:"Medium", 1:"Low", 0:"Off"], defaultValue: 5, displayDuringSetup: true 141 | } 142 | 143 | section("Automatic report settings") { 144 | input "reportInterval", "enum", title: "Sensors Report Interval?", 145 | options: ["20 seconds", "30 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "5 minutes", displayDuringSetup: true 146 | input "tempChangeAmount", "number", title: "Temperature Change Amount (Tenths of a degree)?", range: "1..70", description: "
The tenths of degrees the temperature must change to induce an automatic report.
", defaultValue: 2, required: false 147 | input "humidChangeAmount", "number", title: "Humidity Change Amount (%)?", range: "1..100", description: "
The percentage the humidity must change to induce an automatic report.
", defaultValue: 10, required: false 148 | input "luxChangeAmount", "number", title: "Luminance Change Amount (LUX)?", range: "-1000..1000", description: "
The amount of LUX the luminance must change to induce an automatic report.
", defaultValue: 100, required: false 149 | } 150 | 151 | section("Calibration settings") { 152 | input "tempOffset", "number", title: "Temperature Offset?", range: "-127..128", description: "
-128 to +127 (Tenths of a degree)
If your temperature is inaccurate this will offset/adjust it by this many tenths of a degree.

", defaultValue: 0, required: false, displayDuringSetup: true 153 | input "humidOffset", "number", title: "Humidity Offset/Adjustment -50 to +50 in percent?", range: "-50..50", description: "
If your humidity is inaccurate this will offset/adjust it by this percent.
", defaultValue: 0, required: false, displayDuringSetup: true 154 | input "luxOffset", "number", title: "Luminance Offset/Adjustment -10 to +10 in LUX?", range: "-10..10", description: "
If your luminance is inaccurate this will offset/adjust it by this percent.
", defaultValue: 0, required: false, displayDuringSetup: true 155 | } 156 | 157 | input "ledOptions", "enum", title: "LED Options", 158 | options: [0:"Fully Enabled", 1:"Fully Disabled", 2:"Disable When Motion (Aeon v1.10 only)"], defaultValue: "0", displayDuringSetup: true 159 | input name: "selectiveReporting", type: "bool", title: "Enable Selective Reporting?", defaultValue: false 160 | input "autoInactive", "enum", title: "Auto Inactive", description: "
Choose to automatically turn off Motion (inactive) after a period of time.
", 161 | options: [0:"Disable", 2:"2 minutes", 5:"5 minutes", 10:"10 minutes", 20:"20 minutes", 40:"40 minutes", 60: "1 hour"], defaultValue: "0", displayDuringSetup: true 162 | 163 | input name: "debugOutput", type: "bool", title: "Enable debug logging?", description: "
", defaultValue: true 164 | input name: "descTextEnable", type: "bool", title: "Enable descriptionText logging?", defaultValue: true 165 | } 166 | } 167 | 168 | 169 | /* 170 | updated 171 | 172 | When "Save Preferences" gets clicked... 173 | */ 174 | def updated() { 175 | if (debugOutput) log.debug "In Updated with settings: ${settings}" 176 | if (debugOutput) log.debug "${device.displayName} is now on ${device.latestValue("powerSource")} power" 177 | unschedule() 178 | initialize() 179 | dbCleanUp() // remove antique db entries created in older versions and no longer used. 180 | if (debugOutput) runIn(1800,logsOff) 181 | 182 | // Check for any null settings and change them to default values 183 | inputValidationCheck() 184 | 185 | if (device.latestValue("powerSource") == "dc") { //case1: USB powered 186 | // response(configure(2)) 187 | } else if (device.latestValue("powerSource") == "battery") { //case2: battery powered 188 | // setConfigured("false") is used by WakeUpNotification 189 | setConfigured("false") //wait until the next time device wakeup to send configure command after user change preference 190 | selectiveReport = 0 // battery, selective reporting is not supported 191 | } else { //case3: power source is not identified, ask user to properly pair the sensor again 192 | log.warn "power source is not identified, check that sensor is powered by USB, if so > configure()" 193 | def request = [] 194 | request << zwave.configurationV1.configurationGet(parameterNumber: 9) // Retrieve current power mode 195 | response(commands(request)) 196 | } 197 | return(configure(1)) 198 | } 199 | 200 | 201 | /* 202 | parse 203 | 204 | Respond to received zwave commands. 205 | */ 206 | def parse(String description) { 207 | // log.debug "In parse() for description: $description" 208 | def result = null 209 | if (description.startsWith("Err 106")) { 210 | log.warn "parse() >> Err 106" 211 | result = sendEvent( name: "secureInclusion", value: "failed", 212 | descriptionText: "This sensor (${device.displayName}) failed to complete the network security key exchange. If you are unable to control it via Hubitat, you must remove it from your network and add it again.") 213 | } else if (description != "updated") { 214 | // log.debug "About to zwave.parse($description)" 215 | def cmd = zwave.parse(description, [0x31: 5, 0x30: 1, 0x70: 1, 0x72: 1, 0x84: 1]) 216 | if (cmd) { 217 | // log.debug "About to call handler for ${cmd.toString()}" 218 | result = zwaveEvent(cmd) 219 | } 220 | } 221 | //log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}" 222 | return result 223 | } 224 | 225 | 226 | /* 227 | installed 228 | 229 | Doesn't do much other than call initialize(). 230 | */ 231 | void installed() 232 | { 233 | initialize() 234 | } 235 | 236 | 237 | /* 238 | initialize 239 | 240 | Doesn't do much. 241 | */ 242 | def initialize() { 243 | if (settings.ledOptions == null) settings.ledOptions = 0 // default to Full 244 | state.firmware = state.firmware ?: 0.0d 245 | state.Copyright = "${thisCopyright} -- ${version()}" 246 | } 247 | 248 | 249 | /* 250 | Beginning of Z-Wave Commands 251 | */ 252 | 253 | //this notification will be sent only when device is battery powered 254 | def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) { 255 | def result = [sendEvent(descriptionText: "${device.displayName} woke up")] 256 | def cmds = [] 257 | if (!isConfigured()) { 258 | if (debugOutput) log.debug("late configure") 259 | result << response(configure(3)) 260 | } else { 261 | //if (debugOutput) log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") 262 | cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() 263 | result << response(cmds) 264 | } 265 | result 266 | } 267 | 268 | 269 | def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { 270 | def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 1, 0x70: 1, 0x72: 1, 0x84: 1]) 271 | state.sec = 1 272 | if (debugOutput) log.debug "encapsulated: ${encapsulatedCommand}" 273 | if (encapsulatedCommand) { 274 | zwaveEvent(encapsulatedCommand) 275 | } else { 276 | log.warn "Unable to extract encapsulated cmd from $cmd" 277 | sendEvent(descriptionText: cmd.toString()) 278 | } 279 | } 280 | 281 | 282 | def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv1.ManufacturerSpecificReport cmd) { 283 | if (debugOutput) log.debug "ManufacturerSpecificReport cmd = $cmd" 284 | if (debugOutput) log.debug "manufacturerId: ${cmd.manufacturerId}" 285 | if (debugOutput) log.debug "manufacturerName: ${cmd.manufacturerName}" 286 | if (debugOutput) log.debug "productId: ${cmd.productId}" 287 | def model = "" // We'll decode the specific model for the log, but we don't currently use this info 288 | switch(cmd.productTypeId >> 8) { 289 | case 0: model = "EU" 290 | break 291 | case 1: model = "US" 292 | break 293 | case 2: model = "AU" 294 | break 295 | case 10: model = "JP" 296 | break 297 | case 29: model = "CN" 298 | break 299 | default: model = "unknown" 300 | } 301 | if (debugOutput) log.debug "model: ${model}" 302 | if (debugOutput) log.debug "productTypeId: ${cmd.productTypeId}" 303 | def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) 304 | updateDataValue("MSR", msr) 305 | } 306 | 307 | 308 | def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { 309 | if (debugOutput) log.debug "In BatteryReport $cmd" 310 | if (cmd.batteryLevel == 0xFF) { 311 | sendEvent(name: "battery", unit: "%", value: 1, descriptionText: "${device.displayName} battery is low") 312 | } else { 313 | sendEvent(name: "battery", unit: "%", value: cmd.batteryLevel.toInteger(), descriptionText: "${device.displayName} battery is ${cmd.batteryLevel}%") 314 | } 315 | } 316 | 317 | 318 | @Field static Map LIMIT_VALUES = [ tempC: [upper: 100, lower: -40], tempF: [upper: 212, lower: -40], lux: [upper: 30000, lower: 0], rh: [upper: 100, lower: 0], uv: [upper: 11, lower: 1]] 319 | 320 | def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){ 321 | if (debugOutput) log.debug "In multi level report cmd = $cmd" 322 | 323 | if (cmd.scaledSensorValue == null) return 324 | def map = [:] 325 | switch (cmd.sensorType) { 326 | case 1: 327 | if (debugOutput) log.debug "raw temp = $cmd.scaledSensorValue" 328 | if (cmd.scale == 1) { if ((cmd.scaledSensorValue > LIMIT_VALUES.tempF.upper) || (cmd.scaledSensorValue < LIMIT_VALUES.tempF.lower)) { return } } 329 | if (cmd.scale == 0) { if ((cmd.scaledSensorValue > LIMIT_VALUES.tempC.upper) || (cmd.scaledSensorValue < LIMIT_VALUES.tempC.lower)) { return } } 330 | // Convert temperature (if needed) to the system's configured temperature scale 331 | map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) 332 | if (debugOutput) log.debug "finalval = $map.value" 333 | 334 | map.unit = "\u00b0" + getTemperatureScale() 335 | map.name = "temperature" 336 | map.descriptionText = "${device.displayName} temperature is ${map.value}${map.unit}" 337 | if (descTextEnable) log.info "Temperature is ${map.value}${map.unit}" 338 | break 339 | case 3: 340 | if (debugOutput) log.debug "raw illuminance = $cmd.scaledSensorValue" 341 | if (cmd.scaledSensorValue >= LIMIT_VALUES.lux.upper || cmd.scaledSensorValue < LIMIT_VALUES.lux.lower) return 342 | map.name = "illuminance" 343 | map.value = cmd.scaledSensorValue.toInteger() // roundIt((cmd.scaledSensorValue / 2.0),0) as Integer // .toInteger() 344 | map.unit = "lux" 345 | map.descriptionText = "${device.displayName} illuminance is ${map.value} Lux" 346 | if (descTextEnable) log.info "Illuminance is ${map.value} Lux" 347 | break 348 | case 5: 349 | if (debugOutput) log.debug "raw humidity = $cmd.scaledSensorValue" 350 | if (cmd.scaledSensorValue >= LIMIT_VALUES.rh.upper || cmd.scaledSensorValue < LIMIT_VALUES.rh.lower) return 351 | map.value = roundIt(cmd.scaledSensorValue, 0) as Integer // .toInteger() 352 | map.unit = "%" 353 | map.name = "humidity" 354 | map.descriptionText = "${device.displayName} humidity is ${map.value}%" 355 | if (descTextEnable) log.info "Humidity is ${map.value}%" 356 | break 357 | case 27: 358 | if (debugOutput) log.debug "raw uv index = $cmd.scaledSensorValue" 359 | if (cmd.scaledSensorValue >= LIMIT_VALUES.uv.upper || cmd.scaledSensorValue < LIMIT_VALUES.uv.lower) return 360 | map.name = "ultravioletIndex" 361 | map.value = roundIt(cmd.scaledSensorValue, 0) as Integer // .toInteger() 362 | map.descriptionText = "${device.displayName} ultraviolet index is ${map.value}" 363 | if (descTextEnable) log.info "Ultraviolet index is ${map.value}" 364 | break 365 | default: 366 | map.descriptionText = cmd.toString() 367 | } 368 | sendEvent(map) 369 | } 370 | 371 | 372 | def zwaveEvent(hubitat.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { 373 | motionEvent(cmd.sensorValue, "SensorBinaryReport") 374 | } 375 | 376 | 377 | def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { 378 | // Sensor sends value 0xFF on motion, 0x00 on no motion (after expiry interval) 379 | motionEvent(cmd.value, "BasicSet") 380 | } 381 | 382 | 383 | def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) { 384 | if (debugOutput) log.debug "NotificationReport: ${cmd}" 385 | def result = [] 386 | if (cmd.notificationType == 7) { 387 | // spec says type 7 is 'Home Security' 388 | switch (cmd.event) { 389 | case 0: 390 | // spec says this is 'clear previous alert' 391 | // sendEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed", displayed: true) 392 | // result << motionEvent(0, "NotificationReport") // see Change Note for v1.7 393 | result << sendEvent(name: "tamper", value: "clear", descriptionText: "${device.displayName} tamper cleared", displayed: true) 394 | if (descTextEnable) log.info "Tamper cleared by NotificationReport" 395 | result << sendEvent(name: "acceleration", value: "inactive", descriptionText: "${device.displayName} acceleration is inactive", displayed: true) 396 | if (descTextEnable) log.info "Acceleration is inactive by NotificationReport" 397 | break 398 | case 3: 399 | // spec says this is 'tamper' 400 | //sendEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open", displayed: true) 401 | result << sendEvent(name: "tamper", value: "detected", descriptionText: "${device.displayName} tamper detected", displayed: true) 402 | if (descTextEnable) log.info "Tamper detected" 403 | result << sendEvent(name: "acceleration", value: "active", descriptionText: "${device.displayName} acceleration is active", displayed: true) 404 | if (descTextEnable) log.info "Acceleration is active" 405 | break 406 | case 8: 407 | // spec says this is 'unknown motion detection' 408 | // result << motionEvent(1, "NotificationReport") // see Change Note for v1.7 409 | break 410 | } 411 | } else { 412 | log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}" 413 | result << sendEvent(descriptionText: cmd.toString()) 414 | } 415 | result 416 | } 417 | 418 | 419 | def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { 420 | if (debugOutput) log.debug "---CONFIGURATION REPORT V1--- ${device.displayName} parameter ${cmd.parameterNumber} with a byte size of ${cmd.size} is set to ${cmd.configurationValue}" 421 | def result = [] 422 | def value 423 | if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 0) { 424 | value = "dc" 425 | if (!isConfigured()) { 426 | if (debugOutput) log.debug("Configuration Report: configuring device") 427 | result << response(configure(4)) 428 | } 429 | result << sendEvent(name: "powerSource", value: value, descriptionText: "${device.displayName} power source is dc (mains)", displayed: false) 430 | if (descTextEnable) log.info "Power source is DC (mains)" 431 | } 432 | else if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 1) { 433 | value = "battery" 434 | result << sendEvent(name: "powerSource", value: value, descriptionText: "${device.displayName} power source is battery", displayed: false) 435 | if (descTextEnable) log.info "Power source is battery" 436 | } 437 | else if (cmd.parameterNumber == 101){ 438 | result << response(configure(5)) 439 | } 440 | result 441 | } 442 | 443 | 444 | def zwaveEvent(hubitat.zwave.Command cmd) { 445 | if (debugOutput) log.debug "General zwaveEvent cmd: ${cmd}" 446 | sendEvent(descriptionText: cmd.toString(), isStateChange: false) 447 | } 448 | 449 | 450 | def zwaveEvent(hubitat.zwave.commands.versionv1.VersionCommandClassReport cmd) { 451 | if (debugOutput) log.debug "in version command class report" 452 | if (debugOutput) log.debug "---VERSION COMMAND CLASS REPORT V1--- ${device.displayName} has version: ${cmd.commandClassVersion} for command class ${cmd.requestedCommandClass} - payload: ${cmd.payload}" 453 | } 454 | 455 | 456 | def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { 457 | if (debugOutput) log.debug "in version report" 458 | // SubVersion is in 1/100ths so that 1.01 < 1.08 < 1.10, etc.// state.firmware = 0.0d 459 | if (cmd.firmware0Version) { 460 | BigDecimal fw = cmd.firmware0Version + (cmd.firmware0SubVersion/100) 461 | state.firmware = fw 462 | } 463 | if (debugOutput) log.debug "---VERSION REPORT V1--- ${device.displayName} is running firmware version: ${String.format("%1.2f",state.firmware)}, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" 464 | if(state.firmware < 1.10) 465 | log.warn "--- WARNING: Device handler expects devices to have firmware 1.10 or later" 466 | } 467 | 468 | 469 | def zwaveEvent(hubitat.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd) { 470 | // NOTE: This command class is not yet implemented by Hubitat... 471 | if (debugOutput) log.debug "---FIRMWARE METADATA REPORT V2 ${device.displayName} manufacturerId: ${cmd.manufacturerId} firmwareId: ${cmd.firmwareId}" 472 | } 473 | 474 | 475 | private command(hubitat.zwave.Command cmd) { 476 | if (debugOutput) log.debug "Sending Z-wave command: ${cmd.toString()}" 477 | return zwaveSecureEncap(cmd.format()) 478 | } 479 | 480 | 481 | private commands(commands, delay=1000) { 482 | //if (descTextEnable) log.info "sending commands: ${commands}" 483 | return delayBetween(commands.collect{ command(it) }, delay) 484 | } 485 | 486 | 487 | String secureCmd(cmd) { 488 | if (getDataValue("zwaveSecurePairingComplete") == "true" && getDataValue("S2") == null) { 489 | return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() 490 | } else { 491 | return secure(cmd) 492 | } 493 | } 494 | 495 | 496 | String secure(String cmd) { return zwaveSecureEncap(cmd) } 497 | String secure(hubitat.zwave.Command cmd) { return zwaveSecureEncap(cmd) } 498 | 499 | 500 | /* 501 | End of Z-Wave Commands 502 | 503 | Beginning of Driver Commands 504 | 505 | */ 506 | def configure(ccc) { 507 | if (debugOutput) log.debug "ccc: $ccc" 508 | // This sensor joins as a secure device if you double-click the button to include it 509 | if (descTextEnable) log.info "${device.displayName} is configuring its settings" 510 | 511 | if (device.currentValue('tamper') == null) { 512 | sendEvent(name: 'tamper', value: 'clear', descriptionText: '${device.displayName} tamper cleared') 513 | sendEvent(name: 'acceleration', value: 'inactive', descriptionText: "$device.displayName} acceleration is inactive") 514 | } 515 | 516 | // inputValidationCheck() 517 | 518 | if (debugOutput) { 519 | log.debug "Report Interval = $reportInterval" 520 | log.debug "Motion Delay Time = $motionDelayTime" 521 | log.debug "Motion Sensitivity = $motionSensitivity" 522 | log.debug "Temperature adjust = $tempOffset (${tempOffset/10}°)" 523 | log.debug "Humidity adjust = $humidOffset" 524 | log.debug "Min Temp change for reporting = $tempChangeAmount" 525 | log.debug "Min Humidity change for reporting = $humidChangeAmount" 526 | log.debug "Min Lux change for reporting = $luxChangeAmount" 527 | log.debug "LED Option = $ledOptions" 528 | } 529 | 530 | def now = new Date() 531 | def tf = new java.text.SimpleDateFormat("dd-MMM-yyyy h:mm a") 532 | tf.setTimeZone(location.getTimeZone()) 533 | def newtime = "${tf.format(now)}" as String 534 | sendEvent(name: "lastUpdate", value: newtime, descriptionText: "${device.displayName} configured at ${newtime}") 535 | 536 | setConfigured("true") 537 | def waketime 538 | 539 | if (timeOptionValueMap[settings.reportInterval] < 300) 540 | waketime = timeOptionValueMap[settings.reportInterval] 541 | else waketime = 300 542 | 543 | if (debugOutput) log.debug "wake time reset to $waketime" 544 | if (debugOutput) log.debug "Current firmware: ${sprintf ("%1.2f", state.firmware)}" 545 | 546 | // Retrieve local temperature scale: "C" = Celsius, "F" = Fahrenheit 547 | // Convert to a value of 1 or 2 as used by the device to select the scale 548 | if (debugOutput) log.debug "Location temperature scale: ${location.getTemperatureScale()}" 549 | byte tempScaleByte = (location.getTemperatureScale() == "C" ? 1 : 2) 550 | selectiveReport = selectiveReporting ? 1 : 0 551 | 552 | def request = [ 553 | // set wakeup interval to report time otherwise it doesnt report in time 554 | zwave.wakeUpV1.wakeUpIntervalSet(seconds:waketime, nodeid:zwaveHubNodeId), 555 | 556 | zwave.versionV1.versionGet(), 557 | zwave.manufacturerSpecificV1.manufacturerSpecificGet(), 558 | 559 | // Hubitat have not yet implemented the firmwareUpdateMdV2 class 560 | //zwave.firmwareUpdateMdV2.firmwareMdGet(), 561 | 562 | //1. set association groups for hub 563 | zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId), 564 | zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId), 565 | 566 | //2. automatic report flags 567 | // params 101-103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 16 ultraviolet sensor, 1 battery sensor -> send command 241 to get all reports 568 | zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 241), //association group 1 - all reports 569 | zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1), //association group 2 - battery report 570 | 571 | //3. no-motion report x seconds after motion stops (default 60 secs) 572 | zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 60), 573 | 574 | //4. motion sensitivity: 0 (least sensitive) - 5 (most sensitive) 575 | zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: motionSensitivity as int), 576 | //5. report every x minutes (threshold reports don't work on battery power, default 8 mins) 577 | zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval]), //association group 1 578 | // battery report time.. too long at every 6 hours change to 2 hours. 579 | zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 2*60*60), //association group 2 580 | //6. enable/disable selective reporting only on thresholds 581 | zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: selectiveReport), 582 | // Set the temperature scale for automatic reports 583 | // US units default to reporting in Fahrenheit, whilst all others default to reporting in Celsius, but we can configure the preferred scale with this setting 584 | zwave.configurationV1.configurationSet(parameterNumber: 64, size: 1, configurationValue: [tempScaleByte]), 585 | // Automatically generate a report when temp changes by specified amount 586 | zwave.configurationV1.configurationSet(parameterNumber: 41, size: 4, configurationValue: [0, tempChangeAmount, tempScaleByte, 0]), 587 | // Automatically generate a report when humidity changes by specified amount 588 | zwave.configurationV1.configurationSet(parameterNumber: 42, size: 1, scaledConfigurationValue: humidChangeAmount), 589 | // Automatically generate a report when lux changes by specified amount 590 | zwave.configurationV1.configurationSet(parameterNumber: 43, size: 2, scaledConfigurationValue: luxChangeAmount), 591 | // send (1) BasicSet or (2) SensorBinary report for motion 592 | zwave.configurationV1.configurationSet(parameterNumber: 0x05, size: 1, scaledConfigurationValue: 2), 593 | // Set temperature calibration offset 594 | zwave.configurationV1.configurationSet(parameterNumber: 201, size: 2, configurationValue: [tempOffset as int, tempScaleByte]), 595 | // Set humidity calibration offset 596 | zwave.configurationV1.configurationSet(parameterNumber: 202, size: 1, scaledConfigurationValue: humidOffset), 597 | // Set luminance calibration offset 598 | zwave.configurationV1.configurationSet(parameterNumber: 203, size: 2, scaledConfigurationValue: luxOffset), 599 | // Set LED Option value 600 | zwave.configurationV1.configurationSet(parameterNumber: 81, size: 1, configurationValue: [ledOptions as int]), 601 | //7. query sensor data 602 | // zwave.configurationV1.configurationGet(parameterNumber: 9), // Retrieve current power mode 603 | zwave.batteryV1.batteryGet(), 604 | zwave.sensorBinaryV1.sensorBinaryGet(), //motion 605 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1), //temperature 606 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3), //illuminance 607 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 5), //humidity 608 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 27) //ultravioletIndex 609 | ] 610 | return commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] 611 | } 612 | 613 | 614 | def refresh() { 615 | if (debugOutput) log.debug "in refresh" 616 | 617 | return commands([ 618 | zwave.versionV1.versionGet(), // Retrieve version info (includes firmware version) 619 | // zwave.firmwareUpdateMdV2.firmwareMdGet(), // Command class not implemented by Hubitat yet 620 | zwave.configurationV1.configurationGet(parameterNumber: 9), // Retrieve current power mode 621 | zwave.batteryV1.batteryGet(), 622 | zwave.sensorBinaryV1.sensorBinaryGet(), //motion 623 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1), //temperature 624 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3), //illuminance 625 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 5), //humidity 626 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 27) //ultravioletIndex 627 | ]) 628 | } 629 | 630 | /* 631 | Begin support methods 632 | */ 633 | 634 | def motionEvent(value, by) { 635 | if (value) { 636 | if (descTextEnable) log.info "Motion is active by $by" 637 | sendEvent(name: "motion", value: "active", descriptionText: "${device.displayName} motion is active by $by", isStateChange:true) 638 | if (autoInactive > "0") { 639 | def delay = autoInactive.toInteger() * 60 640 | runIn(delay, motionInactivate) 641 | } 642 | } else { 643 | if (descTextEnable) log.info "Motion is inactive by $by" 644 | sendEvent(name: "motion", value: "inactive", descriptionText: "${device.displayName} motion is inactive by $by", isStateChange:true) 645 | unschedule(motionInactivate) 646 | } 647 | } 648 | 649 | 650 | /* 651 | Auto reset motion to inactive after timeout period. 652 | */ 653 | def motionInactivate() { 654 | if (descTextEnable) log.info "Motion is inactive by Time Out" 655 | sendEvent(name: "motion", value: "inactive", descriptionText: "${device.displayName} motion is inactive by Time Out") 656 | } 657 | 658 | 659 | def getTimeOptionValueMap() { [ 660 | "20 seconds" : 20, 661 | "30 seconds" : 30, 662 | "1 minute" : 60, 663 | "2 minutes" : 2*60, 664 | "3 minutes" : 3*60, 665 | "4 minutes" : 4*60, 666 | "5 minutes" : 5*60, 667 | "10 minutes" : 10*60, 668 | "15 minutes" : 15*60, 669 | "30 minutes" : 30*60, 670 | "1 hour" : 1*60*60, 671 | "6 hours" : 6*60*60, 672 | "12 hours" : 12*60*60, 673 | "18 hours" : 18*60*60, 674 | "24 hours" : 24*60*60, 675 | ]} 676 | 677 | 678 | // Check for any null settings and change them to default values 679 | void inputValidationCheck () { 680 | 681 | motionSensitivity = motionSensitivity ?: 0 682 | motionDelayTime = motionDelayTime ?: "1 minute" 683 | reportInterval = reportInterval ?: "5 minute" 684 | tempChangeAmount = tempChangeAmount ?: 2 685 | humidChangeAmount = humidChangeAmount ?: 10 686 | luxChangeAmount = luxChangeAmount ?: 100 687 | tempOffset = tempOffset ?: 0 688 | ledOptions = ledOptions ?: 0 689 | 690 | // Validate Input Ranges 691 | def motionRange = 0..5 692 | def tempRange = 0..128 693 | def humidRange = 0..50 694 | def luxRange = 0..1000 695 | 696 | if ( !motionRange.contains(motionSensitivity as int) ) { motionSensitivity = 3 ; log.warn "Selection out of Range: Motion Sensitivity set to 3"; } 697 | if ( !tempRange.contains( tempOffset.abs() as int ) ) { tempOffset = 0 ; log.warn "Selection out of Range: Temperature Offset set to 0"; } 698 | if ( !humidRange.contains( humidOffset.abs() as int ) ) { humidOffset = 0 ; log.warn "Selection out of Range: Humidity Offset set to 0"; } 699 | if ( !luxRange.contains( luxOffset.abs() as int ) ) { luxOffset = 0 ; log.warn "Selection out of Range: Luminance Offset set to 0"; } 700 | } 701 | 702 | 703 | private setConfigured(configure) { 704 | updateDataValue("configured", configure) 705 | } 706 | 707 | 708 | private isConfigured() { 709 | getDataValue("configured") == "true" 710 | } 711 | 712 | 713 | def roundIt( value, decimals=0 ) { 714 | return (value == null) ? null : value.toBigDecimal().setScale(decimals, BigDecimal.ROUND_HALF_UP) 715 | } 716 | 717 | 718 | def roundIt( BigDecimal value, decimals=0) { 719 | return (value == null) ? null : value.setScale(decimals, BigDecimal.ROUND_HALF_UP) 720 | } 721 | 722 | 723 | def logsOff(){ 724 | log.warn "debug logging disabled..." 725 | device.updateSetting("debugOutput",[value:"false",type:"bool"]) 726 | } 727 | 728 | 729 | private dbCleanUp() { 730 | // clean up state variables that are obsolete 731 | // state.remove("tempOffset") 732 | // state.remove("version") 733 | // state.remove("Version") 734 | // state.remove("sensorTemp") 735 | // state.remove("author") 736 | // state.remove("Copyright") 737 | state.remove("verUpdate") 738 | state.remove("verStatus") 739 | state.remove("Type") 740 | state.remove("Status") 741 | state.remove("InternalName") 742 | state.remove("UpdateInfo") 743 | } 744 | 745 | List setParameter(parameterNumber = null, size = null, value = null){ 746 | if (parameterNumber == null || size == null || value == null) { 747 | log.warn "incomplete parameter list supplied..." 748 | log.info "syntax: setParameter(parameterNumber,size,value)" 749 | } else { 750 | return delayBetween([ 751 | secureCmd(zwave.configurationV1.configurationSet(scaledConfigurationValue: value, parameterNumber: parameterNumber, size: size)), 752 | secureCmd(zwave.configurationV1.configurationGet(parameterNumber: parameterNumber)) 753 | ],500) 754 | } 755 | } 756 | 757 | List getParameterReport(param = null){ 758 | if (!debugOutput) { 759 | log.warn "debug logging auto-enabled..." 760 | device.updateSetting("debugOutput",[value:"true",type:"bool"]) 761 | runIn(1800,logsOff) 762 | } 763 | 764 | List cmds = [] 765 | if (param != null) { 766 | cmds = [secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param))] 767 | } else { 768 | 0.upto(255, { 769 | cmds.add(secureCmd(zwave.configurationV1.configurationGet(parameterNumber: it))) 770 | }) 771 | } 772 | log.trace "configurationGet command(s) ($param) sent..." 773 | return delayBetween(cmds,500) 774 | } 775 | 776 | 777 | String getThisCopyright(){"© 2019 C Steele "} 778 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AeotecMultiSensor6 2 | Aeon Labs or Aeotec's MultiSensor6 Hubitat Driver 3 | 4 | There are far too many "AeonMultiSensor6 V1" drivers out there and no clear way to distinguish. 5 | 6 | This driver has been through many coders with the most recent being Chuckles. 7 | 8 | Thanks to all who have contributed. 9 | 10 | Hubitat introduced a common Security Method in Platform v2.2.3 that encompasses S0 and S2. However, that means using the method requires the platform, first. 11 | 12 | Aeotec MultiSensor 6 v2 therefore needs a minimum of Hubitat Platform v2.2.3. 13 | 14 | Aeotec MultiSensor 6 v1.x will work on Hubitat Platform v2.2.3 and earlier. 15 | -------------------------------------------------------------------------------- /docs/AeotecMultiSensor6.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "AeotecMultiSensor6", 3 | "releaseNotes": "v2.1.0 Added in optional Auto No Motion timer\n Remove UpdateCheck\nv2.0.2 Extended Motion Sensitivity to allow 0 (zero) which might be 'off' depending on firmware.", 4 | "documentationLink": "https://github.com/HubitatCommunity/AeotecMultiSensor6/blob/master/README.md", 5 | "communityLink": "https://community.hubitat.com/t/aeotec-multisensor-6/1204/20", 6 | "author": "CSteele", 7 | "version": "2.1.0", 8 | "minimumHEVersion": "2.0.1", 9 | "dateReleased": "2023-04-28", 10 | "category": "Control", 11 | "tags": [ 12 | "ZWave", 13 | "Lights & Switches", 14 | "Multi Sensors" 15 | ], 16 | "drivers": [ 17 | { 18 | "id": "41a8de23-9da5-4204-9048-d8038fe21dc4", 19 | "name": "AeotecMultiSensor6", 20 | "namespace": "cSteele", 21 | "location": "https://raw.githubusercontent.com/HubitatCommunity/AeotecMultiSensor6/master/AeotecMultiSensor6.groovy", 22 | "required": true, 23 | "oauth": false, 24 | "description": "A more preference filled alternative to the Built-In driver." 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /docs/version2.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "C Steele", 3 | "comment": "", 4 | "copyright": " Ⓒ 2019 cSteele", 5 | "application": { 6 | }, 7 | "driver": { 8 | "AeotecMultiSensor6": {"ver": "1.7.1", "updated": "08/16/2020"}, 9 | "AeotecMultiSensor6_2": {"ver": "2.0.2", "updated": "07/21/2021"} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "", 3 | "copyright": " Ⓒ 2018 cSteele", 4 | "author":"cSteele", 5 | "versions": { 6 | "Application": { 7 | }, 8 | "Driver": { 9 | "AeotecMultiSensor6": "1.6.13" 10 | }, 11 | 12 | "UpdateInfo": { 13 | "Application": { 14 | }, 15 | "Driver": { 16 | "AeotecMultiSensor6": "Updated: 05/28/2020" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /v1_7/AeotecMultiSensor6.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * IMPORT URL: https://raw.githubusercontent.com/HubitatCommunity/AeotecMultiSensor6/master/v1_7/AeotecMultisensor6.groovy 3 | * 4 | * Copyright 2015 SmartThings 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | * 16 | * v1.7.1 removed Preference hiding (settingEnable) 17 | * LED Options reflect firmware v1.3 specs. (On or Off only) 18 | * Mostion Sensitivity changed to Enum 19 | * v1.7 refactored using inputValidationCheck 20 | * added de-duplication code for Motion Events (motion Events can occur in <1ms) 21 | * | Motion reports (unsolicited) are duplicated: a NotificationReport (7) plus either: 22 | * | (1) for basicSet; (2) for sensorBinary set via Param 5. 23 | * | Motion Detection via NotificationReport can be considered a duplicate. 24 | * | This version removes motion event processing from NotificationReport relying 25 | * | on basicSet or SensorBinary. 26 | * | Added detail to Motion is... to identify the path. 27 | * removed duplicate call to config() 28 | * lots of cosmetic formatting cleanup, cosmetic reordering of modules 29 | * 30 | * v1.6.13 version report NPE (thanks Christi999) 31 | * tempOffset to "as Int" 32 | * v1.6.12 corrected NPE from malformed Packet. (thanks Mike Maxwell) 33 | * v1.6.11 corrected ledOption scaling. (thanks LJP-hubitat) 34 | * BAB v1.6.10a Changed to HALF_ROUND_UP, standardized DescriptionText 35 | * v1.6.10 Swapped to latest updateCheck() code. 36 | * v1.6.9 Added Initialize to preset ledOptions to prevent an NPE when sending the option value to the device. 37 | * revised updateCheck to use version2.json's format. 38 | * v1.6.8 revised Contact Sensor per @Wounded suggestion, restoring tamperAlert 39 | * v1.6.7 replaced updateCheck with asynchttp version -- removed setVersion, etc. 40 | * added descTextEnable as option to reduce log.info 41 | * v1.6.6 corrected limitation on Humidity Offset 42 | * v1.6.5 alternate description for settingEnabled input 43 | * v1.6.4 corrected temp offset -128 to 127 in configure() 44 | * v1.6.3 added Preference hiding (settingEnable) 45 | * v1.6.2 removed mapping to ledOption(s) 46 | * v1.6.1 added degree symbol to temp scale 47 | * v1.6 deleted isStateChange throughout 48 | * v1.5 Added LED Options 49 | * v1.4 Added selectiveReport, enabled humidChangeAmount, luxChangeAmount 50 | * v1.3 Restored if (debugOutput) log.debug as default logging is too much. cSteele 51 | * v1.2 Merged Chuckles updates 52 | * 53 | * Chuckles V1.2 of multisensor 6 54 | * IMPORTANT NOTE: Assumes device has firmware version 1.10 or later. A warning will be logged if this is not the case. 55 | * Changes: 56 | 1. Implement standard "Power Source" capability to track whether USB power ("dc") or battery is in use 57 | - Remove custom "powerSupply" and "batteryStatus" attributes it replaces. 58 | 2. Use hub's timezone rather than setting a timezone for each device individually 59 | 3. Use hub's temperature scale setting rather than setting each device individually 60 | 4. Add event handler for ManufacturerSpecificReport (V1) 61 | 5. Corrected configuration parameter number (4) for PIR sensitivity 62 | 6. Corrected getTimeOptionValueMap 63 | 7. Disabled selective reporting on threshold 64 | 8. Device only supports V1 of COMMAND_CLASS_SENSOR_BINARY (0x30) - not V2 65 | 9. Device only supports V1 of COMMAND_CLASS_CONFIGURATION (0x70) - not V2 66 | 10. Motion detection in NotificationReportV3 is event 8, not event 7 67 | 11. Numerous minor bug fixes (e.g. motionDelayTime and reportInterval should always hold enumerated values, not number of seconds) 68 | 12. Numerous small changes for internationalisation (e.g. region agnostic date formats) 69 | 70 | * v1.1d Added remote version checking ** Cobra (CobraVmax) for his original version check code 71 | * csteele v1.1c converted to Hubitat. 72 | * 73 | * LGK V1 of multisensor 6 with customizeable settings , changed some timeouts, also changed tamper to vibration so we can 74 | * use that as well (based on stock device type and also some changes copied from Robert Vandervoort device type. 75 | * Changes 76 | 1. changes colors of temp slightly, add colors to humidity, add color to battery level 77 | 2. remove tamper and instead use feature as contact and acceleration ie vibration sensor 78 | 3. slightly change reporting interval times. (also fix issue where 18 hours was wrong time) 79 | 4. add last update time.. sets when configured and whenever temp is reported. 80 | This is used so at a glance you see the last update time tile to know the device is still reporting easily without looking 81 | at the logs. 82 | 5. add a temp and humidity offset -12 to 12 to enable tweaking the settings if they appear off. 83 | 6. added power status tile to display, currently was here but not displayed. 84 | 7. added configure and refresh tiles. 85 | 8. also added refresh capability so refresh can be forced from other smartapps like pollster. (refresh not currently working all the time for some reason) 86 | 9. changed the sensitivity to have more values than min max etc, now is integer 0 - 127. 87 | 10. fix uv setting which in one release was value of 2 now it is 16. 88 | 11. added icons for temp and humidity 89 | 12. also change the default wakeup time to be the same as the report interval, 90 | otherwise when on battery it disregarded the report interval. (only if less than the default 5 minutes). 91 | 13. added a config option for the min change needed to report temp changes and set it in parameter 41. 92 | 14. incresed range and colors for lux values, as when mine is in direct sun outside it goes as high as 1900 93 | 15. support for celsius added. set in input options. 94 | */ 95 | 96 | public static String version() { return "v1.7.1" } 97 | 98 | metadata { 99 | definition (name: "AeotecMultiSensor6", namespace: "cSteele", author: "cSteele", importUrl: "https://raw.githubusercontent.com/HubitatCommunity/AeotecMultiSensor6/master/AeotecMultisensor6.groovy") { 100 | capability "Motion Sensor" 101 | capability "Temperature Measurement" 102 | capability "Relative Humidity Measurement" 103 | capability "Illuminance Measurement" 104 | capability "Ultraviolet Index" 105 | capability "Configuration" 106 | capability "Sensor" 107 | capability "Battery" 108 | capability "Power Source" 109 | capability "Acceleration Sensor" 110 | capability "TamperAlert" 111 | 112 | command "refresh" 113 | // command "updateCheck" // **---** delete for Release 114 | 115 | attribute "firmware", "decimal" 116 | 117 | fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A" 118 | } 119 | 120 | 121 | preferences { 122 | // Note: Hubitat doesn't appear to honour 'sections' in device handler preferences just now, but hopefully one day... 123 | section("Motion sensor settings") { 124 | input "motionDelayTime", "enum", title: "Motion Sensor Delay Time?", 125 | options: ["20 seconds", "30 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"], defaultValue: "1 minute", displayDuringSetup: true 126 | input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity?", options: [5:"Very High", 4:"High", 3:"Medium High", 2:"Medium", 1:"Low"], defaultValue: 5, displayDuringSetup: true 127 | } 128 | 129 | section("Automatic report settings") { 130 | input "reportInterval", "enum", title: "Sensors Report Interval?", 131 | options: ["20 seconds", "30 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "5 minutes", displayDuringSetup: true 132 | input "tempChangeAmount", "number", title: "Temperature Change Amount (Tenths of a degree)?", range: "1..70", description: "
The tenths of degrees the temperature must change to induce an automatic report.
", defaultValue: 2, required: false 133 | input "humidChangeAmount", "number", title: "Humidity Change Amount (%)?", range: "1..100", description: "
The percentage the humidity must change to induce an automatic report.
", defaultValue: 10, required: false 134 | input "luxChangeAmount", "number", title: "Luminance Change Amount (LUX)?", range: "-1000..1000", description: "
The amount of LUX the luminance must change to induce an automatic report.
", defaultValue: 100, required: false 135 | } 136 | 137 | section("Calibration settings") { 138 | input "tempOffset", "number", title: "Temperature Offset?", range: "-127..128", description: "
-128 to +127 (Tenths of a degree)
If your temperature is inaccurate this will offset/adjust it by this many tenths of a degree.

", defaultValue: 0, required: false, displayDuringSetup: true 139 | input "humidOffset", "number", title: "Humidity Offset/Adjustment -50 to +50 in percent?", range: "-50..50", description: "
If your humidity is inaccurate this will offset/adjust it by this percent.
", defaultValue: 0, required: false, displayDuringSetup: true 140 | input "luxOffset", "number", title: "Luminance Offset/Adjustment -10 to +10 in LUX?", range: "-10..10", description: "
If your luminance is inaccurate this will offset/adjust it by this percent.
", defaultValue: 0, required: false, displayDuringSetup: true 141 | } 142 | 143 | input "ledOptions", "enum", title: "LED Options", 144 | options: [0:"Fully Enabled", 1:"Fully Disabled", 2:"Disable When Motion (Aeon v1.10 only)"], defaultValue: "0", displayDuringSetup: true 145 | input name: "selectiveReporting", type: "bool", title: "Enable Selective Reporting?", defaultValue: false 146 | 147 | input name: "debugOutput", type: "bool", title: "Enable debug logging?", description: "
", defaultValue: true 148 | input name: "descTextEnable", type: "bool", title: "Enable descriptionText logging?", defaultValue: true 149 | } 150 | } 151 | 152 | 153 | /* 154 | updated 155 | 156 | When "Save Preferences" gets clicked... 157 | */ 158 | def updated() { 159 | if (debugOutput) log.debug "In Updated with settings: ${settings}" 160 | if (debugOutput) log.debug "${device.displayName} is now on ${device.latestValue("powerSource")} power" 161 | unschedule() 162 | initialize() 163 | dbCleanUp() // remove antique db entries created in older versions and no longer used. 164 | schedule("0 0 8 ? * FRI *", updateCheck) 165 | if (debugOutput) runIn(1800,logsOff) 166 | runIn(20, updateCheck) 167 | 168 | // Check for any null settings and change them to default values 169 | inputValidationCheck() 170 | 171 | if (device.latestValue("powerSource") == "dc") { //case1: USB powered 172 | // response(configure(2)) 173 | } else if (device.latestValue("powerSource") == "battery") { //case2: battery powered 174 | // setConfigured("false") is used by WakeUpNotification 175 | setConfigured("false") //wait until the next time device wakeup to send configure command after user change preference 176 | selectiveReport = 0 // battery, selective reporting is not supported 177 | } else { //case3: power source is not identified, ask user to properly pair the sensor again 178 | log.warn "power source is not identified, check that sensor is powered by USB, if so > configure()" 179 | def request = [] 180 | request << zwave.configurationV1.configurationGet(parameterNumber: 101) 181 | response(commands(request)) 182 | } 183 | return(configure(1)) 184 | } 185 | 186 | 187 | /* 188 | parse 189 | 190 | Respond to received zwave commands. 191 | */ 192 | def parse(String description) { 193 | // log.debug "In parse() for description: $description" 194 | def result = null 195 | if (description.startsWith("Err 106")) { 196 | log.warn "parse() >> Err 106" 197 | result = createEvent( name: "secureInclusion", value: "failed", 198 | descriptionText: "This sensor (${device.displayName}) failed to complete the network security key exchange. If you are unable to control it via Hubitat, you must remove it from your network and add it again.") 199 | } else if (description != "updated") { 200 | // log.debug "About to zwave.parse($description)" 201 | def cmd = zwave.parse(description, [0x31: 5, 0x30: 1, 0x70: 1, 0x72: 1, 0x84: 1]) 202 | if (cmd) { 203 | // log.debug "About to call handler for ${cmd.toString()}" 204 | result = zwaveEvent(cmd) 205 | } 206 | } 207 | //log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}" 208 | return result 209 | } 210 | 211 | 212 | /* 213 | installed 214 | 215 | Doesn't do much other than call initialize(). 216 | */ 217 | void installed() 218 | { 219 | initialize() 220 | } 221 | 222 | 223 | 224 | /* 225 | initialize 226 | 227 | Doesn't do much. 228 | */ 229 | def initialize() { 230 | if (settings.ledOptions == null) settings.ledOptions = 0 // default to Full 231 | state.firmware = state.firmware ?: 0.0d 232 | } 233 | 234 | 235 | /* 236 | Beginning of Z-Wave Commands 237 | */ 238 | 239 | //this notification will be sent only when device is battery powered 240 | def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) { 241 | def result = [createEvent(descriptionText: "${device.displayName} woke up")] 242 | def cmds = [] 243 | if (!isConfigured()) { 244 | if (debugOutput) log.debug("late configure") 245 | result << response(configure(3)) 246 | } else { 247 | //if (debugOutput) log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") 248 | cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() 249 | result << response(cmds) 250 | } 251 | result 252 | } 253 | 254 | 255 | def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { 256 | def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 1, 0x70: 1, 0x72: 1, 0x84: 1]) 257 | state.sec = 1 258 | if (debugOutput) log.debug "encapsulated: ${encapsulatedCommand}" 259 | if (encapsulatedCommand) { 260 | zwaveEvent(encapsulatedCommand) 261 | } else { 262 | log.warn "Unable to extract encapsulated cmd from $cmd" 263 | createEvent(descriptionText: cmd.toString()) 264 | } 265 | } 266 | 267 | 268 | def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { 269 | if (descTextEnable) log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd" 270 | state.sec = 1 271 | } 272 | 273 | 274 | def zwaveEvent(hubitat.zwave.commands.securityv1.NetworkKeyVerify cmd) { 275 | state.sec = 1 276 | if (descTextEnable) log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)" 277 | def result = [createEvent(name:"secureInclusion", value:"success", descriptionText:"${device.displayName} - Secure inclusion was successful")] 278 | result 279 | } 280 | 281 | 282 | def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv1.ManufacturerSpecificReport cmd) { 283 | if (debugOutput) log.debug "ManufacturerSpecificReport cmd = $cmd" 284 | if (debugOutput) log.debug "manufacturerId: ${cmd.manufacturerId}" 285 | if (debugOutput) log.debug "manufacturerName: ${cmd.manufacturerName}" 286 | if (debugOutput) log.debug "productId: ${cmd.productId}" 287 | def model = "" // We'll decode the specific model for the log, but we don't currently use this info 288 | switch(cmd.productTypeId >> 8) { 289 | case 0: model = "EU" 290 | break 291 | case 1: model = "US" 292 | break 293 | case 2: model = "AU" 294 | break 295 | case 10: model = "JP" 296 | break 297 | case 29: model = "CN" 298 | break 299 | default: model = "unknown" 300 | } 301 | if (debugOutput) log.debug "model: ${model}" 302 | if (debugOutput) log.debug "productTypeId: ${cmd.productTypeId}" 303 | def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) 304 | updateDataValue("MSR", msr) 305 | } 306 | 307 | 308 | def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) { 309 | if (debugOutput) log.debug "In BatteryReport" 310 | def result = [] 311 | def map = [ name: "battery", unit: "%" ] 312 | if (cmd.batteryLevel == 0xFF) { 313 | map.value = 1 314 | map.descriptionText = "${device.displayName} battery is low" 315 | } else { 316 | map.value = cmd.batteryLevel 317 | map.descriptionText = "${device.displayName} battery is ${map.value}%" 318 | } 319 | createEvent(map) 320 | } 321 | 322 | 323 | def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){ 324 | if (debugOutput) log.debug "In multi level report cmd = $cmd" 325 | 326 | if (cmd.scaledSensorValue == null) return 327 | def map = [:] 328 | switch (cmd.sensorType) { 329 | case 1: 330 | if (debugOutput) log.debug "raw temp = $cmd.scaledSensorValue" 331 | // Convert temperature (if needed) to the system's configured temperature scale 332 | def finalval = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) 333 | 334 | if (debugOutput) log.debug "finalval = $finalval" 335 | 336 | map.value = finalval 337 | map.unit = "\u00b0" + getTemperatureScale() 338 | map.name = "temperature" 339 | map.descriptionText = "${device.displayName} temperature is ${map.value}${map.unit}" 340 | if (descTextEnable) log.info "Temperature is ${map.value}${map.unit}" 341 | break 342 | case 3: 343 | if (debugOutput) log.debug "raw illuminance = $cmd.scaledSensorValue" 344 | map.name = "illuminance" 345 | map.value = cmd.scaledSensorValue.toInteger() // roundIt((cmd.scaledSensorValue / 2.0),0) as Integer // .toInteger() 346 | map.unit = "lux" 347 | map.descriptionText = "${device.displayName} illuminance is ${map.value} Lux" 348 | if (descTextEnable) log.info "Illuminance is ${map.value} Lux" 349 | break 350 | case 5: 351 | if (debugOutput) log.debug "raw humidity = $cmd.scaledSensorValue" 352 | map.value = roundIt(cmd.scaledSensorValue, 0) as Integer // .toInteger() 353 | map.unit = "%" 354 | map.name = "humidity" 355 | map.descriptionText = "${device.displayName} humidity is ${map.value}%" 356 | if (descTextEnable) log.info "Humidity is ${map.value}%" 357 | break 358 | case 27: 359 | if (debugOutput) log.debug "raw uv index = $cmd.scaledSensorValue" 360 | map.name = "ultravioletIndex" 361 | map.value = roundIt(cmd.scaledSensorValue, 0) as Integer // .toInteger() 362 | map.descriptionText = "${device.displayName} ultraviolet index is ${map.value}" 363 | if (descTextEnable) log.info "Ultraviolet index is ${map.value}" 364 | break 365 | default: 366 | map.descriptionText = cmd.toString() 367 | } 368 | createEvent(map) 369 | } 370 | 371 | 372 | def zwaveEvent(hubitat.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { 373 | motionEvent(cmd.sensorValue, "SensorBinaryReport") 374 | } 375 | 376 | 377 | def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) { 378 | // Sensor sends value 0xFF on motion, 0x00 on no motion (after expiry interval) 379 | motionEvent(cmd.value, "BasicSet") 380 | } 381 | 382 | 383 | def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) { 384 | if (debugOutput) log.debug "NotificationReport: ${cmd}" 385 | def result = [] 386 | if (cmd.notificationType == 7) { 387 | // spec says type 7 is 'Home Security' 388 | switch (cmd.event) { 389 | case 0: 390 | // spec says this is 'clear previous alert' 391 | //sendEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed", displayed: true) 392 | // result << motionEvent(0, "NotificationReport") // see Change Note for v1.7 393 | result << createEvent(name: "tamper", value: "clear", descriptionText: "${device.displayName} tamper cleared", displayed: true) 394 | if (descTextEnable) log.info "Tamper cleared by NotificationReport" 395 | result << createEvent(name: "acceleration", value: "inactive", descriptionText: "${device.displayName} acceleration is inactive", displayed: true) 396 | if (descTextEnable) log.info "Acceleration is inactive by NotificationReport" 397 | break 398 | case 3: 399 | // spec says this is 'tamper' 400 | //sendEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open", displayed: true) 401 | result << createEvent(name: "tamper", value: "detected", descriptionText: "${device.displayName} tamper detected", displayed: true) 402 | if (descTextEnable) log.info "Tamper detected" 403 | result << createEvent(name: "acceleration", value: "active", descriptionText: "${device.displayName} acceleration is active", displayed: true) 404 | if (descTextEnable) log.info "Acceleration is active" 405 | break 406 | case 8: 407 | // spec says this is 'unknown motion detection' 408 | // result << motionEvent(1, "NotificationReport") // see Change Note for v1.7 409 | break 410 | } 411 | } else { 412 | log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}" 413 | result << createEvent(descriptionText: cmd.toString()) 414 | } 415 | result 416 | } 417 | 418 | 419 | def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { 420 | if (debugOutput) log.debug "---CONFIGURATION REPORT V1--- ${device.displayName} parameter ${cmd.parameterNumber} with a byte size of ${cmd.size} is set to ${cmd.configurationValue}" 421 | def result = [] 422 | def value 423 | if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 0) { 424 | value = "dc" 425 | if (!isConfigured()) { 426 | if (debugOutput) log.debug("Configuration Report: configuring device") 427 | result << response(configure(4)) 428 | } 429 | result << createEvent(name: "powerSource", value: value, descriptionText: "${device.displayName} power source is dc (mains)", displayed: false) 430 | if (descTextEnable) log.info "Power source is DC (mains)" 431 | } 432 | else if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 1) { 433 | value = "battery" 434 | result << createEvent(name: "powerSource", value: value, descriptionText: "${device.displayName} power source is battery", displayed: false) 435 | if (descTextEnable) log.info "Power source is battery" 436 | } 437 | else if (cmd.parameterNumber == 101){ 438 | result << response(configure(5)) 439 | } 440 | result 441 | } 442 | 443 | 444 | def zwaveEvent(hubitat.zwave.Command cmd) { 445 | if (debugOutput) log.debug "General zwaveEvent cmd: ${cmd}" 446 | createEvent(descriptionText: cmd.toString(), isStateChange: false) 447 | } 448 | 449 | 450 | def zwaveEvent(hubitat.zwave.commands.versionv1.VersionCommandClassReport cmd) { 451 | if (debugOutput) log.debug "in version command class report" 452 | if (debugOutput) log.debug "---VERSION COMMAND CLASS REPORT V1--- ${device.displayName} has version: ${cmd.commandClassVersion} for command class ${cmd.requestedCommandClass} - payload: ${cmd.payload}" 453 | } 454 | 455 | 456 | def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { 457 | if (debugOutput) log.debug "in version report" 458 | // SubVersion is in 1/100ths so that 1.01 < 1.08 < 1.10, etc.// state.firmware = 0.0d 459 | if (cmd.firmware0Version) { 460 | BigDecimal fw = cmd.firmware0Version + (cmd.firmware0SubVersion/100) 461 | state.firmware = fw 462 | } 463 | if (debugOutput) log.debug "---VERSION REPORT V1--- ${device.displayName} is running firmware version: ${String.format("%1.2f",state.firmware)}, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" 464 | if(state.firmware < 1.10) 465 | log.warn "--- WARNING: Device handler expects devices to have firmware 1.10 or later" 466 | } 467 | 468 | 469 | def zwaveEvent(hubitat.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd) { 470 | // NOTE: This command class is not yet implemented by Hubitat... 471 | if (debugOutput) log.debug "---FIRMWARE METADATA REPORT V2 ${device.displayName} manufacturerId: ${cmd.manufacturerId} firmwareId: ${cmd.firmwareId}" 472 | } 473 | 474 | 475 | private command(hubitat.zwave.Command cmd) { 476 | if (state.sec) { 477 | if (debugOutput) log.debug "Sending secure Z-wave command: ${cmd.toString()}" 478 | return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() 479 | } else { 480 | if (debugOutput) log.debug "Sending Z-wave command: ${cmd.toString()}" 481 | return cmd.format() 482 | } 483 | } 484 | 485 | 486 | private commands(commands, delay=1000) { 487 | //if (descTextEnable) log.info "sending commands: ${commands}" 488 | return delayBetween(commands.collect{ command(it) }, delay) 489 | } 490 | /* 491 | End of Z-Wave Commands 492 | 493 | Beginning of Driver Commands 494 | 495 | */ 496 | def configure(ccc) { 497 | if (debugOutput) log.debug "ccc: $ccc" 498 | // This sensor joins as a secure device if you double-click the button to include it 499 | if (descTextEnable) log.info "${device.displayName} is configuring its settings" 500 | 501 | if (device.currentValue('tamper') == null) { 502 | sendEvent(name: 'tamper', value: 'clear', descriptionText: '${device.displayName} tamper cleared') 503 | sendEvent(name: 'acceleration', value: 'inactive', descriptionText: "$device.displayName} acceleration is inactive") 504 | } 505 | 506 | // inputValidationCheck() 507 | 508 | if (debugOutput) { 509 | log.debug "Report Interval = $reportInterval" 510 | log.debug "Motion Delay Time = $motionDelayTime" 511 | log.debug "Motion Sensitivity = $motionSensitivity" 512 | log.debug "Temperature adjust = $tempOffset (${tempOffset/10}°)" 513 | log.debug "Humidity adjust = $humidOffset" 514 | log.debug "Min Temp change for reporting = $tempChangeAmount" 515 | log.debug "Min Humidity change for reporting = $humidChangeAmount" 516 | log.debug "Min Lux change for reporting = $luxChangeAmount" 517 | log.debug "LED Option = $ledOptions" 518 | } 519 | 520 | def now = new Date() 521 | def tf = new java.text.SimpleDateFormat("dd-MMM-yyyy h:mm a") 522 | tf.setTimeZone(location.getTimeZone()) 523 | def newtime = "${tf.format(now)}" as String 524 | sendEvent(name: "lastUpdate", value: newtime, descriptionText: "${device.displayName} configured at ${newtime}") 525 | 526 | setConfigured("true") 527 | def waketime 528 | 529 | if (timeOptionValueMap[settings.reportInterval] < 300) 530 | waketime = timeOptionValueMap[settings.reportInterval] 531 | else waketime = 300 532 | 533 | if (debugOutput) log.debug "wake time reset to $waketime" 534 | if (debugOutput) log.debug "Current firmware: ${sprintf ("%1.2f", state.firmware)}" 535 | 536 | // Retrieve local temperature scale: "C" = Celsius, "F" = Fahrenheit 537 | // Convert to a value of 1 or 2 as used by the device to select the scale 538 | if (debugOutput) log.debug "Location temperature scale: ${location.getTemperatureScale()}" 539 | byte tempScaleByte = (location.getTemperatureScale() == "C" ? 1 : 2) 540 | selectiveReport = selectiveReporting ? 1 : 0 541 | 542 | def request = [ 543 | // set wakeup interval to report time otherwise it doesnt report in time 544 | zwave.wakeUpV1.wakeUpIntervalSet(seconds:waketime, nodeid:zwaveHubNodeId), 545 | 546 | zwave.versionV1.versionGet(), 547 | zwave.manufacturerSpecificV1.manufacturerSpecificGet(), 548 | 549 | // Hubitat have not yet implemented the firmwareUpdateMdV2 class 550 | //zwave.firmwareUpdateMdV2.firmwareMdGet(), 551 | 552 | //1. set association groups for hub 553 | zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId), 554 | zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId), 555 | 556 | //2. automatic report flags 557 | // params 101-103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 16 ultraviolet sensor, 1 battery sensor -> send command 241 to get all reports 558 | zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 241), //association group 1 - all reports 559 | zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1), //association group 2 - battery report 560 | 561 | //3. no-motion report x seconds after motion stops (default 60 secs) 562 | zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 60), 563 | 564 | //4. motion sensitivity: 0 (least sensitive) - 5 (most sensitive) 565 | zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: motionSensitivity as int), 566 | //5. report every x minutes (threshold reports don't work on battery power, default 8 mins) 567 | zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval]), //association group 1 568 | // battery report time.. too long at every 6 hours change to 2 hours. 569 | zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 2*60*60), //association group 2 570 | //6. enable/disable selective reporting only on thresholds 571 | zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: selectiveReport), 572 | // Set the temperature scale for automatic reports 573 | // US units default to reporting in Fahrenheit, whilst all others default to reporting in Celsius, but we can configure the preferred scale with this setting 574 | zwave.configurationV1.configurationSet(parameterNumber: 64, size: 1, configurationValue: [tempScaleByte]), 575 | // Automatically generate a report when temp changes by specified amount 576 | zwave.configurationV1.configurationSet(parameterNumber: 41, size: 4, configurationValue: [0, tempChangeAmount, tempScaleByte, 0]), 577 | // Automatically generate a report when humidity changes by specified amount 578 | zwave.configurationV1.configurationSet(parameterNumber: 42, size: 1, scaledConfigurationValue: humidChangeAmount), 579 | // Automatically generate a report when lux changes by specified amount 580 | zwave.configurationV1.configurationSet(parameterNumber: 43, size: 2, scaledConfigurationValue: luxChangeAmount), 581 | // send (1) BasicSet or (2) SensorBinary report for motion 582 | zwave.configurationV1.configurationSet(parameterNumber: 0x05, size: 1, scaledConfigurationValue: 2), 583 | // Set temperature calibration offset 584 | zwave.configurationV1.configurationSet(parameterNumber: 201, size: 2, configurationValue: [tempOffset as int, tempScaleByte]), 585 | // Set humidity calibration offset 586 | zwave.configurationV1.configurationSet(parameterNumber: 202, size: 1, scaledConfigurationValue: humidOffset), 587 | // Set luminance calibration offset 588 | zwave.configurationV1.configurationSet(parameterNumber: 203, size: 2, scaledConfigurationValue: luxOffset), 589 | // Set LED Option value 590 | zwave.configurationV1.configurationSet(parameterNumber: 81, size: 1, configurationValue: [ledOptions as int]), 591 | //7. query sensor data 592 | zwave.batteryV1.batteryGet(), 593 | zwave.sensorBinaryV1.sensorBinaryGet(), //motion 594 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1), //temperature 595 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3), //illuminance 596 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 5), //humidity 597 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 27) //ultravioletIndex 598 | ] 599 | return commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] 600 | } 601 | 602 | 603 | def refresh() { 604 | if (debugOutput) log.debug "in refresh" 605 | 606 | return commands([ 607 | zwave.versionV1.versionGet(), // Retrieve version info (includes firmware version) 608 | // zwave.firmwareUpdateMdV2.firmwareMdGet(), // Command class not implemented by Hubitat yet 609 | zwave.configurationV1.configurationGet(parameterNumber: 9), // Retrieve current power mode 610 | zwave.batteryV1.batteryGet(), 611 | zwave.sensorBinaryV1.sensorBinaryGet(), //motion 612 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1), //temperature 613 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3), //illuminance 614 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 5), //humidity 615 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 27) //ultravioletIndex 616 | ]) 617 | } 618 | 619 | /* 620 | Begin support methods 621 | */ 622 | 623 | def motionEvent(value, by) { 624 | def map = [name: "motion"] 625 | if (value) { 626 | if (descTextEnable) log.info "Motion is active by $by" 627 | map.value = "active" 628 | map.descriptionText = "${device.displayName} motion is active by $by" 629 | } else { 630 | if (descTextEnable) log.info "Motion is inactive by $by" 631 | map.value = "inactive" 632 | map.descriptionText = "${device.displayName} motion is inactive by $by" 633 | } 634 | createEvent(map) 635 | } 636 | 637 | 638 | def getTimeOptionValueMap() { [ 639 | "20 seconds" : 20, 640 | "30 seconds" : 30, 641 | "1 minute" : 60, 642 | "2 minutes" : 2*60, 643 | "3 minutes" : 3*60, 644 | "4 minutes" : 4*60, 645 | "5 minutes" : 5*60, 646 | "10 minutes" : 10*60, 647 | "15 minutes" : 15*60, 648 | "30 minutes" : 30*60, 649 | "1 hour" : 1*60*60, 650 | "6 hours" : 6*60*60, 651 | "12 hours" : 12*60*60, 652 | "18 hours" : 18*60*60, 653 | "24 hours" : 24*60*60, 654 | ]} 655 | 656 | 657 | // Check for any null settings and change them to default values 658 | void inputValidationCheck () { 659 | 660 | motionSensitivity = motionSensitivity ?: 0 661 | motionDelayTime = motionDelayTime ?: "1 minute" 662 | reportInterval = reportInterval ?: "5 minute" 663 | tempChangeAmount = tempChangeAmount ?: 2 664 | humidChangeAmount = humidChangeAmount ?: 10 665 | luxChangeAmount = luxChangeAmount ?: 100 666 | tempOffset = tempOffset ?: 0 667 | ledOptions = ledOptions ?: 0 668 | 669 | // Validate Input Ranges 670 | def motionRange = 0..5 671 | def tempRange = 0..128 672 | def humidRange = 0..50 673 | def luxRange = 0..1000 674 | 675 | if ( !motionRange.contains(motionSensitivity as int) ) { motionSensitivity = 3 ; log.warn "Selection out of Range: Motion Sensitivity set to 3"; } 676 | if ( !tempRange.contains( tempOffset.abs() as int ) ) { tempOffset = 0 ; log.warn "Selection out of Range: Temperature Offset set to 0"; } 677 | if ( !humidRange.contains( humidOffset.abs() as int ) ) { humidOffset = 0 ; log.warn "Selection out of Range: Humidity Offset set to 0"; } 678 | if ( !luxRange.contains( luxOffset.abs() as int ) ) { luxOffset = 0 ; log.warn "Selection out of Range: Luminance Offset set to 0"; } 679 | } 680 | 681 | 682 | private setConfigured(configure) { 683 | updateDataValue("configured", configure) 684 | } 685 | 686 | 687 | private isConfigured() { 688 | getDataValue("configured") == "true" 689 | } 690 | 691 | 692 | def roundIt( value, decimals=0 ) { 693 | return (value == null) ? null : value.toBigDecimal().setScale(decimals, BigDecimal.ROUND_HALF_UP) 694 | } 695 | 696 | 697 | def roundIt( BigDecimal value, decimals=0) { 698 | return (value == null) ? null : value.setScale(decimals, BigDecimal.ROUND_HALF_UP) 699 | } 700 | 701 | 702 | def logsOff(){ 703 | log.warn "debug logging disabled..." 704 | device.updateSetting("debugOutput",[value:"false",type:"bool"]) 705 | } 706 | 707 | 708 | private dbCleanUp() { 709 | // clean up state variables that are obsolete 710 | // state.remove("tempOffset") 711 | // state.remove("version") 712 | // state.remove("Version") 713 | // state.remove("sensorTemp") 714 | // state.remove("author") 715 | // state.remove("Copyright") 716 | state.remove("verUpdate") 717 | state.remove("verStatus") 718 | state.remove("Type") 719 | } 720 | 721 | 722 | // Check Version ***** with great thanks and acknowlegment to Cobra (CobraVmax) for his original code **** 723 | def updateCheck() 724 | { 725 | def paramsUD = [uri: "https://hubitatcommunity.github.io/AeotecMultiSensor6/version2.json"] 726 | asynchttpGet("updateCheckHandler", paramsUD) 727 | } 728 | 729 | 730 | def updateCheckHandler(resp, data) { 731 | 732 | state.InternalName = "AeotecMultiSensor6" 733 | 734 | if (resp.getStatus() == 200 || resp.getStatus() == 207) { 735 | respUD = parseJson(resp.data) 736 | // log.warn " Version Checking - Response Data: $respUD" // Troubleshooting Debug Code - Uncommenting this line should show the JSON response from your webserver 737 | state.Copyright = "${thisCopyright} -- ${version()}" 738 | // uses reformattted 'version2.json' 739 | def newVer = padVer(respUD.driver.(state.InternalName).ver) 740 | def currentVer = padVer(version()) 741 | state.UpdateInfo = (respUD.driver.(state.InternalName).updated) 742 | // log.debug "updateCheck: ${respUD.driver.(state.InternalName).ver}, $state.UpdateInfo, ${respUD.author}" 743 | 744 | switch(newVer) { 745 | case { it == "NLS"}: 746 | state.Status = "** This Driver is no longer supported by ${respUD.author} **" 747 | if (descTextEnable) log.warn "** This Driver is no longer supported by ${respUD.author} **" 748 | break 749 | case { it > currentVer}: 750 | state.Status = "New Version Available (Version: ${respUD.driver.(state.InternalName).ver})" 751 | if (descTextEnable) log.warn "** There is a newer version of this Driver available (Version: ${respUD.driver.(state.InternalName).ver}) **" 752 | if (descTextEnable) log.warn "** $state.UpdateInfo **" 753 | break 754 | case { it < currentVer}: 755 | state.Status = "You are using a Test version of this Driver (Expecting: ${respUD.driver.(state.InternalName).ver})" 756 | if (descTextEnable) log.warn "You are using a Test version of this Driver (Expecting: ${respUD.driver.(state.InternalName).ver})" 757 | break 758 | default: 759 | state.Status = "Current" 760 | if (descTextEnable) log.info "You are using the current version of this driver" 761 | break 762 | } 763 | sendEvent(name: "chkUpdate", value: state.UpdateInfo) 764 | sendEvent(name: "chkStatus", value: state.Status) 765 | } 766 | else 767 | { 768 | log.error "Something went wrong: CHECK THE JSON FILE AND IT'S URI" 769 | } 770 | } 771 | 772 | /* 773 | padVer 774 | 775 | Version progression of 1.4.9 to 1.4.10 would mis-compare unless each duple is padded first. 776 | 777 | */ 778 | String padVer(ver) { 779 | def pad = "" 780 | ver.replaceAll( "[vV]", "" ).split( /\./ ).each { pad += it.padLeft( 2, '0' ) } 781 | return pad 782 | } 783 | 784 | String getThisCopyright(){"© 2019 C Steele "} 785 | --------------------------------------------------------------------------------