├── Z-Wave Schlage Touchscreen Lock.groovy ├── fibaro_motion_sensor.groovy ├── my_z-wave_garage_door_opener.groovy └── smartApps └── Door Unlock Triggers.groovy /Z-Wave Schlage Touchscreen Lock.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * INSTRUCTIONS: If you scroll down a couple pages, you should find a line that looks like: 4 | * main "toggle" 5 | * and that followed by a line that STARTS with: 6 | * details(["toggle", 7 | * If you want to change the items that are available on the details page of the device (from 'things'), 8 | * you should edit the "details" line to include whatever items you want to see (along with the order 9 | * you want to see them in.) There's a sample "details" line commented out (starts with //) below the 10 | * first one. That sample enables all (or mostly all) the possible items. 11 | * 12 | * After the first time you have the device type installed (or after you've changed it), you might have 13 | * to forcibly terminate the mobile app before the new/changed stuff will show up. (Most mobile apps 14 | * don't actually terminate when you exit them. How to terminate an app depends on your mobile OS.) 15 | * 16 | * If a toggle is showing up as "loading..." on the UI (and you haven't recently changed it), tap the 17 | * tile and it should reload the status within 10 seconds. 18 | * 19 | * 2015-03-07 : When the lock is locked/unlocked automatically, from the keypad, or manually, include that 20 | * information in the map.data, usedCode. (0 for keypad, "manual" for manually, and "auto" for automatic) 21 | * 2015-02-02 : changed state values to prevent UI confusion. (Previously, when setting one item to 'unknown', 22 | * the UI might show ALL the items as 'unknown'.) Also added beeper toggle. 23 | * 24 | * This is a modification of work originally copyrighted by "SmartThings." All modifications to their work 25 | * is released under the following terms: 26 | * 27 | * The original licensing applies, with the following exceptions: 28 | * 1. These modifications may NOT be used without freely distributing all these modifications freely 29 | * and without limitation, in source form. The distribution may be met with a link to source code 30 | * with these modifications. 31 | * 2. These modifications may NOT be used, directly or indirectly, for the purpose of any type of 32 | * monetary gain. These modifications may not be used in a larger entity which is being sold, 33 | * leased, or anything other than freely given. 34 | * 3. To clarify 1 and 2 above, if you use these modifications, it must be a free project, and 35 | * available to anyone with "no strings attached." (You may require a free registration on 36 | * a free website or portal in order to distribute the modifications.) 37 | * 4. The above listed exceptions to the original licensing do not apply to the holder of the 38 | * copyright of the original work. The original copyright holder can use the modifications 39 | * to hopefully improve their original work. In that event, this author transfers all claim 40 | * and ownership of the modifications to "SmartThings." 41 | * 42 | * Original Copyright information: 43 | * 44 | * Copyright 2014 SmartThings 45 | * 46 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 47 | * in compliance with the License. You may obtain a copy of the License at: 48 | * 49 | * http://www.apache.org/licenses/LICENSE-2.0 50 | * 51 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 52 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 53 | * for the specific language governing permissions and limitations under the License. 54 | * 55 | */ 56 | 57 | metadata 58 | { 59 | // Automatically generated. Make future change here. 60 | definition (name: "Z-Wave Schlage Touchscreen Lock", namespace: "garyd9", author: "Gary D") 61 | { 62 | capability "Actuator" 63 | capability "Lock" 64 | capability "Polling" 65 | capability "Refresh" 66 | capability "Sensor" 67 | capability "Lock Codes" 68 | capability "Battery" 69 | 70 | attribute "beeperMode", "string" 71 | attribute "vacationMode", "string" // "on", "off", "unknown" 72 | attribute "lockLeave", "string" // "on", "off", "unknown" 73 | attribute "alarmMode", "string" // "unknown", "Off", "Alert", "Tamper", "Kick" 74 | attribute "alarmSensitivity", "number" // 0 is unknown, otherwise 1-5 scaled to 1-99 75 | attribute "localControl", "string" // "on", "off", "unknown" 76 | attribute "autoLock", "string" // "on", "off", "unknown" 77 | attribute "pinLength", "number" 78 | 79 | command "unlockwtimeout" 80 | 81 | command "setBeeperMode" 82 | command "setVacationMode" 83 | command "setLockLeave" 84 | command "setAlarmMode" 85 | command "setAlarmSensitivity" 86 | command "setLocalControl" 87 | command "setAutoLock" 88 | command "setPinLength" 89 | 90 | fingerprint deviceId: "0x4003", inClusters: "0x98" 91 | fingerprint deviceId: "0x4004", inClusters: "0x98" 92 | } 93 | 94 | simulator { 95 | status "locked": "command: 9881, payload: 00 62 03 FF 00 00 FE FE" 96 | status "unlocked": "command: 9881, payload: 00 62 03 00 00 00 FE FE" 97 | 98 | reply "9881006201FF,delay 4200,9881006202": "command: 9881, payload: 00 62 03 FF 00 00 FE FE" 99 | reply "988100620100,delay 4200,9881006202": "command: 9881, payload: 00 62 03 00 00 00 FE FE" 100 | } 101 | 102 | tiles { 103 | standardTile("toggle", "device.lock", width: 2, height: 2) 104 | { 105 | state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking" 106 | state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" 107 | state "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking" 108 | state "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821" 109 | state "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" 110 | } 111 | standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat") 112 | { 113 | state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking" 114 | } 115 | standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat") 116 | { 117 | state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking" 118 | } 119 | valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") 120 | { 121 | state "battery", label:'${currentValue}% battery', unit:"" 122 | } 123 | standardTile("refresh", "device.lock", inactiveLabel: false, decoration: "flat") 124 | { 125 | state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" 126 | } 127 | standardTile("alarmMode", "device.alarmMode", inactiveLabel: true, canChangeIcon: false) 128 | { 129 | state "unknown_alarmMode", label: 'Alarm Mode\nLoading...', icon:"st.unknown.unknown.unknown", action:"setAlarmMode", nextState:"unknown_alarmMode" 130 | state "Off_alarmMode", label: 'Alarm: Off', icon:"st.alarm.beep.beep", action:"setAlarmMode", nextState:"unknown_alarmMode" 131 | state "Alert_alarmMode", label: 'Alert Alarm', icon:"st.alarm.beep.beep", action:"setAlarmMode", backgroundColor:"#79b821", nextState:"unknown_alarmMode" 132 | state "Tamper_alarmMode", label: 'Tamper Alarm', icon:"st.alarm.beep.beep", action:"setAlarmMode", backgroundColor:"#79b821", nextState:"unknown_alarmMode" 133 | state "Kick_alarmMode", label: 'Kick Alarm', icon:"st.alarm.beep.beep", action:"setAlarmMode", backgroundColor:"#79b821", nextState:"unknown_alarmMode" 134 | } 135 | controlTile("alarmSensitivity", "device.alarmSensitivity", "slider", height: 1, width: 2, inactiveLabel: false) 136 | { 137 | state "alarmSensitivity", label:'Sensitivity', action:"setAlarmSensitivity", backgroundColor:"#ff0000" 138 | } 139 | standardTile("autoLock", "device.autoLock", inactiveLabel: true, canChangeIcon: false) 140 | { 141 | state "unknown_autoLock", label: 'Auto Lock\nLoading...', icon:"st.unknown.unknown.unknown", action:"setAutoLock", nextState:"unknown_autoLock" 142 | state "off_autoLock", label: 'Auto Lock', icon:"st.presence.house.unlocked", action:"setAutoLock", nextState:"unknown_autoLock" 143 | state "on_autoLock", label: 'Auto Lock', icon:"st.presence.house.secured", action:"setAutoLock", backgroundColor:"#79b821", nextState:"unknown_autoLock" 144 | } 145 | 146 | // not included in details 147 | 148 | standardTile("vacationMode", "device.vacationMode", inactiveLabel: true, canChangeIcon: false) 149 | { 150 | state "unknown_vacationMode", label: 'Vacation\nLoading...', icon:"st.unknown.unknown.unknown", action:"setVacationMode", nextState:"unknown_vacationMode" 151 | state "off_vacationMode", label: 'Vacation', icon:"st.Health & Wellness.health2", action:"setVacationMode", nextState:"unknown_vacationMode" 152 | state "on_vacationMode", label: 'Vacation', icon:"st.Health & Wellness.health2", action:"setVacationMode", backgroundColor:"#79b821", nextState:"unknown_vacationMode" 153 | } 154 | 155 | // not included in details 156 | 157 | standardTile("lockLeave", "device.lockLeave", inactiveLabel: true, canChangeIcon: false) 158 | { 159 | state "unknown_lockLeave", label: 'Lock & Leave\nLoading...', icon:"st.unknown.unknown.unknown", action:"setLockLeave", nextState:"unknown_lockLeave" 160 | state "off_lockLeave", label: 'Lock & Leave', icon:"st.Health & Wellness.health12", action:"setLockLeave", nextState:"unknown_lockLeave" 161 | state "on_lockLeave", label: 'Lock & Leave', icon:"st.Health & Wellness.health12", action:"setLockLeave", backgroundColor:"#79b821", nextState:"unknown_lockLeave" 162 | } 163 | 164 | // not included in details 165 | 166 | standardTile("localControl", "device.localControl", inactiveLabel: true, canChangeIcon: false) 167 | { 168 | state "unknown_localControl", label: 'Local Ctrl\nLoading...', icon:"st.unknown.unknown.unknown", action:"setLocalControl", nextState:"unknown_localControl" 169 | state "off_localControl", label: 'Local Ctrl', icon:"st.Home.home3", action:"setLocalControl", nextState:"unknown_localControl" 170 | state "on_localControl", label: 'Local Ctrl', icon:"st.Home.home3", action:"setLocalControl", backgroundColor:"#79b821", nextState:"unknown_localControl" 171 | } 172 | 173 | 174 | // not included in details 175 | 176 | standardTile("beeperMode", "device.beeperMode", inactiveLabel: true, canChangeIcon: false) 177 | { 178 | state "unknown_beeperMode", label: 'Beeper\nLoading...', icon:"st.unknown.unknown.unknown", action:"setBeeperMode", nextState:"unknown_beeperMode" 179 | state "off_beeperMode", label: 'Beeper', icon:"st.unknown.unknown.unknown", action:"setBeeperMode", nextState:"unknown_beeperMode" 180 | state "on_beeperMode", label: 'Beeper', icon:"st.unknown.unknown.unknown", action:"setBeeperMode", backgroundColor:"#79b821", nextState:"unknown_beeperMode" 181 | } 182 | 183 | 184 | main "toggle" 185 | details(["toggle", "lock", "unlock", "alarmMode", "alarmSensitivity", "battery", "autoLock", "lockLeave", "refresh"]) 186 | // details(["toggle", "lock", "unlock", "alarmMode", "alarmSensitivity", "battery", "autoLock", "lockLeave", "vacationMode", "beeperMode", "refresh"]) 187 | } 188 | } 189 | 190 | import physicalgraph.zwave.commands.doorlockv1.* 191 | import physicalgraph.zwave.commands.usercodev1.* 192 | 193 | def parse(String description) 194 | { 195 | def result = null 196 | if (description.startsWith("Err")) 197 | { 198 | if (state.sec) 199 | { 200 | result = createEvent(descriptionText:description, displayed:false) 201 | } 202 | else 203 | { 204 | result = createEvent( 205 | descriptionText: "This lock failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", 206 | eventType: "ALERT", 207 | name: "secureInclusion", 208 | value: "failed", 209 | displayed: true) 210 | } 211 | } 212 | else 213 | { 214 | def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2 ]) 215 | if (cmd) 216 | { 217 | result = zwaveEvent(cmd) 218 | } 219 | } 220 | log.debug "\"$description\" parsed to ${result.inspect()}" 221 | result 222 | } 223 | 224 | def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) 225 | { 226 | def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 2, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1]) 227 | // log.debug "encapsulated: $encapsulatedCommand" 228 | if (encapsulatedCommand) 229 | { 230 | zwaveEvent(encapsulatedCommand) 231 | } 232 | } 233 | 234 | def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) 235 | { 236 | createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful") 237 | } 238 | 239 | def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) 240 | { 241 | state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join() 242 | if (cmd.commandClassControl) 243 | { 244 | state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join() 245 | } 246 | log.debug "Security command classes: $state.sec" 247 | createEvent(name:"secureInclusion", value:"success", descriptionText:"Lock is securely included") 248 | } 249 | 250 | def zwaveEvent(DoorLockOperationReport cmd) 251 | { 252 | def result = [] 253 | def map = [ name: "lock" ] 254 | if (cmd.doorLockMode == 0xFF) 255 | { 256 | map.value = "locked" 257 | } 258 | else if (cmd.doorLockMode >= 0x40) 259 | { 260 | map.value = "unknown" 261 | } 262 | else if (cmd.doorLockMode & 1) 263 | { 264 | map.value = "unlocked with timeout" 265 | } 266 | else 267 | { 268 | map.value = "unlocked" 269 | if (state.assoc != zwaveHubNodeId) 270 | { 271 | log.debug "setting association" 272 | result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))) 273 | result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)) 274 | result << response(secure(zwave.associationV1.associationGet(groupingIdentifier:1))) 275 | } 276 | } 277 | result ? [createEvent(map), *result] : createEvent(map) 278 | } 279 | 280 | /* 0x71 */ 281 | def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) 282 | { 283 | def result = [] 284 | def map = null 285 | if (cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_ACCESS_CONTROL) /* 0x06 */ 286 | { 287 | if (1 <= cmd.zwaveAlarmEvent && cmd.zwaveAlarmEvent < 10) 288 | { 289 | map = [ name: "lock", value: (cmd.zwaveAlarmEvent & 1) ? "locked" : "unlocked" ] 290 | } 291 | switch(cmd.zwaveAlarmEvent) 292 | { 293 | case 1: 294 | map.descriptionText = "$device.displayName was manually locked" 295 | map.data = [ usedCode: "manual" ] 296 | break 297 | case 2: 298 | map.descriptionText = "$device.displayName was manually unlocked" 299 | map.data = [ usedCode: "manual" ] 300 | break 301 | case 5: 302 | if (cmd.eventParameter) 303 | { 304 | map.descriptionText = "$device.displayName was locked with code ${cmd.eventParameter.first()}" 305 | map.data = [ usedCode: cmd.eventParameter[0] ] 306 | } 307 | else 308 | { 309 | map.descriptionText = "$device.displayName was locked with keypad" 310 | map.data = [ usedCode: 0 ] 311 | } 312 | 313 | break 314 | case 6: 315 | if (cmd.eventParameter) 316 | { 317 | map.descriptionText = "$device.displayName was unlocked with code ${cmd.eventParameter.first()}" 318 | map.data = [ usedCode: cmd.eventParameter[0] ] 319 | } 320 | else 321 | { 322 | map.descriptionText = "$device.displayName was unlocked with keypad" 323 | map.data = [ usedCode: 0 ] 324 | } 325 | break 326 | case 9: 327 | map.descriptionText = "$device.displayName was autolocked" 328 | map.data = [ usedCode: "auto" ] 329 | break 330 | case 7: 331 | case 8: 332 | case 0xA: 333 | map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName was not locked fully" ] 334 | break 335 | case 0xB: 336 | map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName is jammed", eventType: "ALERT", displayed: true ] 337 | break 338 | case 0xC: 339 | map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", displayed: true ] 340 | allCodesDeleted() 341 | break 342 | case 0xD: 343 | if (cmd.eventParameter) 344 | { 345 | map = [ name: "codeReport", value: cmd.eventParameter[0], data: [ code: "" ], displayed: true ] 346 | map.descriptionText = "$device.displayName code ${map.value} was deleted" 347 | map.isStateChange = (state["code$map.value"] != "") 348 | state["code$map.value"] = "" 349 | } 350 | else 351 | { 352 | map = [ name: "codeChanged", descriptionText: "$device.displayName: user code deleted", displayed: true ] 353 | } 354 | break 355 | case 0xE: 356 | map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName: user code added", displayed: true ] 357 | if (cmd.eventParameter) 358 | { 359 | map.value = cmd.eventParameter[0] 360 | result << response(requestCode(cmd.eventParameter[0])) 361 | } 362 | break 363 | case 0xF: 364 | map = [ name: "lock", value: "locked", descriptionText: "$device.displayName Alarm! (Too many user code failures.)", eventType: "ALERT", displayed: true, isStateChange: true ] 365 | break 366 | 367 | default: 368 | map = map ?: [ descriptionText: "$device.displayName: alarm event $cmd.zwaveAlarmEvent", display: false ] 369 | break 370 | } 371 | } 372 | else if (cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_BURGLAR) /* 0x07 */ 373 | { 374 | // seen this happen with a schlage touchscreen when banging on the door. 375 | // Might be a tamper or forced entry type alarm. 376 | // In that case, type was 7 and event was 2 377 | map = [ descriptionText: "$device.displayName Burglar Alarm! Event ${cmd.zwaveAlarmEvent}", eventType: "ALERT", displayed: true, isStateChange: true ] 378 | } 379 | else switch(cmd.alarmType) 380 | { 381 | /* // Schlage locks should be using the alarmv2 variables above or lock/unlock events 382 | case 21: // Manually locked 383 | case 18: // Locked with keypad 384 | case 24: // Locked by command (Kwikset 914) 385 | case 27: // Autolocked 386 | map = [ name: "lock", value: "locked" ] 387 | break 388 | case 16: // Note: for levers this means it's unlocked, for non-motorized deadbolt, it's just unsecured and might not get unlocked 389 | case 19: 390 | map = [ name: "lock", value: "unlocked" ] 391 | if (cmd.alarmLevel) 392 | { 393 | map.descriptionText = "$device.displayName was unlocked with code $cmd.alarmLevel" 394 | map.data = [ usedCode: cmd.alarmLevel ] 395 | } 396 | break 397 | case 22: 398 | case 25: // Kwikset 914 unlocked by command 399 | map = [ name: "lock", value: "unlocked" ] 400 | break 401 | */ 402 | case 9: 403 | case 17: 404 | case 23: 405 | case 26: 406 | map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName bolt is jammed" ] 407 | break 408 | case 13: 409 | map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName code $cmd.alarmLevel was added", displayed: true ] 410 | result << response(requestCode(cmd.alarmLevel)) 411 | break 412 | case 32: 413 | map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", displayed: true ] 414 | allCodesDeleted() 415 | case 33: 416 | map = [ name: "codeReport", value: cmd.alarmLevel, data: [ code: "" ], displayed: true ] 417 | map.descriptionText = "$device.displayName code $cmd.alarmLevel was deleted" 418 | map.isStateChange = (state["code$cmd.alarmLevel"] != "") 419 | state["code$cmd.alarmLevel"] = "" 420 | break 421 | case 112: 422 | map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName code $cmd.alarmLevel changed", displayed: true ] 423 | result << response(requestCode(cmd.alarmLevel)) 424 | break 425 | case 130: // Yale YRD batteries replaced 426 | map = [ descriptionText: "$device.displayName batteries replaced", displayed: true ] 427 | break 428 | case 131: 429 | map = [ /*name: "codeChanged", value: cmd.alarmLevel,*/ descriptionText: "$device.displayName code $cmd.alarmLevel is duplicate", displayed: false ] 430 | case 161: 431 | if (cmd.alarmLevel == 2) 432 | { 433 | map = [ descriptionText: "$device.displayName front escutcheon removed", isStateChange: true ] 434 | } 435 | else 436 | { 437 | map = [ descriptionText: "$device.displayName detected failed user code attempt", isStateChange: true ] 438 | } 439 | break 440 | case 167: 441 | if (!state.lastbatt || (new Date().time) - state.lastbatt > 12*60*60*1000) 442 | { 443 | map = [ descriptionText: "$device.displayName: battery low", displayed: true ] 444 | result << response(secure(zwave.batteryV1.batteryGet())) 445 | } 446 | else 447 | { 448 | map = [ name: "battery", value: device.currentValue("battery"), descriptionText: "$device.displayName: battery low", displayed: true ] 449 | } 450 | break 451 | case 168: 452 | map = [ name: "battery", value: 1, descriptionText: "$device.displayName: battery level critical", displayed: true ] 453 | break 454 | case 169: 455 | map = [ name: "battery", value: 0, descriptionText: "$device.displayName: battery too low to operate lock", isStateChange: true ] 456 | break 457 | default: 458 | map = [ displayed: false, descriptionText: "$device.displayName: alarm event $cmd.alarmType level $cmd.alarmLevel" ] 459 | break 460 | } 461 | result ? [createEvent(map), *result] : createEvent(map) 462 | } 463 | 464 | /* 0x63 */ 465 | def zwaveEvent(UserCodeReport cmd) 466 | { 467 | def result = [] 468 | def name = "code$cmd.userIdentifier" 469 | def code = cmd.code 470 | def map = [:] 471 | if (cmd.userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED || 472 | (cmd.userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user && code != "**********")) 473 | { 474 | if (code == "**********") 475 | { // Schlage locks send us this instead of the real code 476 | state.blankcodes = true 477 | code = state["set$name"] ?: state[name] ?: code 478 | state.remove("set$name".toString()) 479 | } 480 | if (!code && cmd.userIdStatus == 1) 481 | { // Schlage touchscreen sends blank code to notify of a changed code 482 | map = [ name: "codeChanged", value: cmd.userIdentifier, displayed: true, isStateChange: true ] 483 | map.descriptionText = "$device.displayName code $cmd.userIdentifier " + (state[name] ? "changed" : "was added") 484 | code = state["set$name"] ?: state[name] ?: "****" 485 | state.remove("set$name".toString()) 486 | } 487 | else 488 | { 489 | map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: code ] ] 490 | map.descriptionText = "$device.displayName code $cmd.userIdentifier is set" 491 | map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode) 492 | map.isStateChange = (code != state[name]) 493 | } 494 | result << createEvent(map) 495 | } 496 | else 497 | { 498 | map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: "" ] ] 499 | if (state.blankcodes && state["reset$name"]) 500 | { // we deleted this code so we can tell that our new code gets set 501 | map.descriptionText = "$device.displayName code $cmd.userIdentifier was reset" 502 | map.displayed = map.isStateChange = false 503 | result << createEvent(map) 504 | state["set$name"] = state["reset$name"] 505 | result << response(setCode(cmd.userIdentifier, state["reset$name"])) 506 | state.remove("reset$name".toString()) 507 | } 508 | else 509 | { 510 | if (state[name]) 511 | { 512 | map.descriptionText = "$device.displayName code $cmd.userIdentifier was deleted" 513 | } 514 | else 515 | { 516 | map.descriptionText = "$device.displayName code $cmd.userIdentifier is not set" 517 | } 518 | map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode) 519 | map.isStateChange = state[name] as Boolean 520 | result << createEvent(map) 521 | } 522 | code = "" 523 | } 524 | state[name] = code 525 | 526 | if (cmd.userIdentifier == state.requestCode) 527 | { // reloadCodes() was called, keep requesting the codes in order 528 | if (state.requestCode + 1 > state.codes) 529 | { 530 | state.remove("requestCode") // done 531 | } 532 | else 533 | { 534 | state.requestCode = state.requestCode + 1 // get next 535 | result << response(requestCode(state.requestCode)) 536 | } 537 | } 538 | if (cmd.userIdentifier == state.pollCode) 539 | { 540 | if (state.pollCode + 1 > state.codes) 541 | { 542 | state.remove("pollCode") // done 543 | } 544 | else 545 | { 546 | state.pollCode = state.pollCode + 1 547 | } 548 | } 549 | log.debug "code report parsed to ${result.inspect()}" 550 | result 551 | } 552 | 553 | /* 0x70, 0x06 */ 554 | def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationBulkReport cmd) 555 | { 556 | log.debug "Got a bulk report, but don't know what to do with it." 557 | // it seems that this lock does NOT support BulkReport. 558 | } 559 | 560 | def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) 561 | { 562 | def result = [] 563 | def map = null // use this for config reports that are handled 564 | 565 | // use desc/val for generic handling of config reports (it will just send a descriptionText for the acitivty stream) 566 | def desc = null 567 | def val = "" 568 | 569 | switch (cmd.parameterNumber) 570 | { 571 | case 0x3: 572 | map = parseBinaryConfigRpt('beeperMode', cmd.configurationValue[0], 'Beeper Mode') 573 | break 574 | 575 | // done: vacation mode toggle 576 | case 0x4: 577 | map = parseBinaryConfigRpt('vacationMode', cmd.configurationValue[0], 'Vacation Mode') 578 | break 579 | 580 | // done: lock and leave mode 581 | case 0x5: 582 | map = parseBinaryConfigRpt('lockLeave', cmd.configurationValue[0], 'Lock & Leave') 583 | break 584 | 585 | // these don't seem to be useful. It's just a bitmap of the code slots used. 586 | case 0x6: 587 | desc = "User Slot Bit Fields" 588 | val = "${cmd.configurationValue[3]} ${cmd.configurationValue[2]} ${cmd.configurationValue[1]} ${cmd.configurationValue[0]}" 589 | break 590 | 591 | // done: the alarm mode of the lock. 592 | case 0x7: 593 | map = [ name:"alarmMode", displayed: true ] 594 | // when getting the alarm mode, also query the sensitivity for that current alarm mode 595 | switch (cmd.configurationValue[0]) 596 | { 597 | case 0x00: 598 | map.value = "Off_alarmMode" 599 | break 600 | case 0x01: 601 | map.value = "Alert_alarmMode" 602 | result << response(secure(zwave.configurationV2.configurationGet(parameterNumber: 0x08))) 603 | break 604 | case 0x02: 605 | map.value = "Tamper_alarmMode" 606 | result << response(secure(zwave.configurationV2.configurationGet(parameterNumber: 0x09))) 607 | break 608 | case 0x03: 609 | map.value = "Kick_alarmMode" 610 | result << response(secure(zwave.configurationV2.configurationGet(parameterNumber: 0x0A))) 611 | break 612 | default: 613 | map.value = "unknown_alarmMode" 614 | } 615 | map.descriptionText = "$device.displayName Alarm Mode set to \"$map.value\"" 616 | break 617 | 618 | // done: alarm sensitivities - one for each mode 619 | case 0x8: 620 | case 0x9: 621 | case 0xA: 622 | def whichMode = null 623 | switch (cmd.parameterNumber) 624 | { 625 | case 0x8: 626 | whichMode = "Alert" 627 | break; 628 | case 0x9: 629 | whichMode = "Tamper" 630 | break; 631 | case 0xA: 632 | whichMode = "Kick" 633 | break; 634 | } 635 | def curAlarmMode = device.currentValue("alarmMode") 636 | val = "${cmd.configurationValue[0]}" 637 | 638 | // the lock has sensitivity values between 1 and 5. ST sliders want a value between 0 and 99. Use a formula 639 | // to make the internal attribute something visually appealing on the UI slider 640 | def modifiedValue = (cmd.configurationValue[0] * 24) - 23 641 | 642 | map = [ descriptionText: "$device.displayName Alarm $whichMode Sensitivity set to $val", displayed: true ] 643 | 644 | if (curAlarmMode == "${whichMode}_alarmMode") 645 | { 646 | map.name = "alarmSensitivity" 647 | map.value = modifiedValue 648 | } 649 | else 650 | { 651 | log.debug "got sensitivity for $whichMode while in $curAlarmMode" 652 | map.isStateChange = true 653 | } 654 | 655 | break 656 | 657 | case 0xB: 658 | map = parseBinaryConfigRpt('localControl', cmd.configurationValue[0], 'Local Alarm Control') 659 | break 660 | 661 | // how many times has the electric motor locked or unlock the device? 662 | case 0xC: 663 | desc = "Electronic Transition Count" 664 | def ttl = cmd.configurationValue[3] + (cmd.configurationValue[2] * 0x100) + (cmd.configurationValue[1] * 0x10000) + (cmd.configurationValue[0] * 0x1000000) 665 | val = "$ttl" 666 | break 667 | 668 | // how many times has the device been locked or unlocked manually? 669 | case 0xD: 670 | desc = "Mechanical Transition Count" 671 | def ttl = cmd.configurationValue[3] + (cmd.configurationValue[2] * 0x100) + (cmd.configurationValue[1] * 0x10000) + (cmd.configurationValue[0] * 0x1000000) 672 | val = "$ttl" 673 | break 674 | 675 | // how many times has there been a failure by the electric motor? (due to jamming??) 676 | case 0xE: 677 | desc = "Electronic Failed Count" 678 | def ttl = cmd.configurationValue[3] + (cmd.configurationValue[2] * 0x100) + (cmd.configurationValue[1] * 0x10000) + (cmd.configurationValue[0] * 0x1000000) 679 | val = "$ttl" 680 | break 681 | 682 | // done: auto lock mode 683 | case 0xF: 684 | map = parseBinaryConfigRpt('autoLock', cmd.configurationValue[0], 'Auto Lock') 685 | break 686 | 687 | // this will be useful as an attribute/command usable by a smartapp 688 | case 0x10: 689 | map = [ name: 'pinLength', value: cmd.configurationValue[0], displayed: true, descriptionText: "$device.displayName PIN length configured to ${cmd.configurationValue[0]} digits"] 690 | break 691 | 692 | // not sure what this one stores 693 | case 0x11: 694 | desc = "Electronic High Preload Transition Count" 695 | def ttl = cmd.configurationValue[3] + (cmd.configurationValue[2] * 0x100) + (cmd.configurationValue[1] * 0x10000) + (cmd.configurationValue[0] * 0x1000000) 696 | val = "$ttl" 697 | break 698 | 699 | // ??? 700 | case 0x12: 701 | desc = "Bootloader Version" 702 | val = "${cmd.configurationValue[0]}" 703 | break 704 | default: 705 | desc = "Unknown parameter ${cmd.parameterNumber}" 706 | val = "${cmd.configurationValue[0]}" 707 | break 708 | } 709 | if (map) 710 | { 711 | result << createEvent(map) 712 | } 713 | else if (desc != null) 714 | { 715 | // generic description text 716 | result << createEvent([ descriptionText: "$device.displayName reports \"$desc\" configured as \"$val\"", displayed: true, isStateChange: true ]) 717 | } 718 | result 719 | } 720 | 721 | def parseBinaryConfigRpt(paramName, paramValue, paramDesc) 722 | { 723 | def map = [ name: paramName, displayed: true ] 724 | 725 | def newVal = "on" 726 | if (paramValue == 0) 727 | { 728 | newVal = "off" 729 | } 730 | map.value = "${newVal}_${paramName}" 731 | map.descriptionText = "$device.displayName $paramDesc has been turned $newVal" 732 | return map 733 | } 734 | 735 | 736 | def zwaveEvent(UsersNumberReport cmd) 737 | { 738 | def result = [] 739 | state.codes = cmd.supportedUsers 740 | if (state.requestCode && state.requestCode <= cmd.supportedUsers) 741 | { 742 | result << response(requestCode(state.requestCode)) 743 | } 744 | result 745 | } 746 | 747 | def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) 748 | { 749 | def result = [] 750 | if (cmd.nodeId.any { it == zwaveHubNodeId }) 751 | { 752 | state.remove("associationQuery") 753 | log.debug "$device.displayName is associated to $zwaveHubNodeId" 754 | result << createEvent(descriptionText: "$device.displayName is associated") 755 | state.assoc = zwaveHubNodeId 756 | if (cmd.groupingIdentifier == 2) 757 | { 758 | result << response(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId)) 759 | } 760 | } 761 | else if (cmd.groupingIdentifier == 1) 762 | { 763 | result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))) 764 | } 765 | else if (cmd.groupingIdentifier == 2) 766 | { 767 | result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)) 768 | } 769 | result 770 | } 771 | 772 | def zwaveEvent(physicalgraph.zwave.commands.timev1.TimeGet cmd) 773 | { 774 | def result = [] 775 | def now = new Date().toCalendar() 776 | if(location.timeZone) now.timeZone = location.timeZone 777 | result << createEvent(descriptionText: "$device.displayName requested time update", displayed: false) 778 | result << response(secure(zwave.timeV1.timeReport( 779 | hourLocalTime: now.get(Calendar.HOUR_OF_DAY), 780 | minuteLocalTime: now.get(Calendar.MINUTE), 781 | secondLocalTime: now.get(Calendar.SECOND)))) 782 | result 783 | } 784 | 785 | def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) 786 | { 787 | // The old Schlage locks use group 1 for basic control - we don't want that, so unsubscribe from group 1 788 | def result = [ createEvent(name: "lock", value: cmd.value ? "unlocked" : "locked") ] 789 | result << response(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId)) 790 | if (state.assoc != zwaveHubNodeId) 791 | { 792 | result << response(zwave.associationV1.associationGet(groupingIdentifier:2)) 793 | } 794 | result 795 | } 796 | 797 | def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) 798 | { 799 | def map = [ name: "battery", unit: "%" ] 800 | if (cmd.batteryLevel == 0xFF) 801 | { 802 | map.value = 1 803 | map.descriptionText = "$device.displayName has a low battery" 804 | } 805 | else 806 | { 807 | map.value = cmd.batteryLevel 808 | } 809 | state.lastbatt = new Date().time 810 | createEvent(map) 811 | } 812 | 813 | def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) 814 | { 815 | def result = [] 816 | 817 | def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) 818 | log.debug "msr: $msr" 819 | updateDataValue("MSR", msr) 820 | 821 | result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) 822 | result 823 | } 824 | 825 | def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) 826 | { 827 | def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" 828 | updateDataValue("fw", fw) 829 | def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" 830 | createEvent(descriptionText: text, isStateChange: false) 831 | } 832 | 833 | def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) 834 | { 835 | def msg = cmd.status == 0 ? "try again later" : 836 | cmd.status == 1 ? "try again in $cmd.waitTime seconds" : 837 | cmd.status == 2 ? "request queued" : "sorry" 838 | createEvent(displayed: false, descriptionText: "$device.displayName is busy, $msg") 839 | } 840 | 841 | def zwaveEvent(physicalgraph.zwave.Command cmd) 842 | { 843 | createEvent(displayed: false, descriptionText: "$device.displayName: $cmd") 844 | } 845 | 846 | def lockAndCheck(doorLockMode) 847 | { 848 | secureSequence([ 849 | zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode), 850 | zwave.doorLockV1.doorLockOperationGet() 851 | ], 4200) 852 | } 853 | 854 | def lock() 855 | { 856 | lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED) 857 | } 858 | 859 | def unlock() 860 | { 861 | lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED) 862 | } 863 | 864 | def unlockwtimeout() 865 | { 866 | lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT) 867 | } 868 | 869 | def refresh() 870 | { 871 | // def cmds = [secure(zwave.doorLockV1.doorLockOperationGet())] 872 | def cmds = secureSequence([ 873 | zwave.doorLockV1.doorLockOperationGet(), 874 | // zwave.configurationV2.configurationBulkGet(numberOfParameters: 3, parameterOffset: 0x8), 875 | // zwave.configurationV2.configurationBulkGet(numberOfParameters: 4, parameterOffset: 0x3), 876 | // zwave.configurationV2.configurationGet(parameterNumber: 0x3), // beeper (done) 877 | // zwave.configurationV2.configurationGet(parameterNumber: 0x4), // vacation mode (done) 878 | // zwave.configurationV2.configurationGet(parameterNumber: 0x5), // lock and leave (done) 879 | // zwave.configurationV2.configurationGet(parameterNumber: 0x6), // user slot bit field (not needed) 880 | zwave.configurationV2.configurationGet(parameterNumber: 0x7), // alarm mode (done) 881 | // zwave.configurationV2.configurationGet(parameterNumber: 0x8), // alert alarm sensitivity (done: retrieved after alarm mode) 882 | // zwave.configurationV2.configurationGet(parameterNumber: 0x9), // tamper alarm sensitivity (done: retrieved after alarm mode) 883 | // zwave.configurationV2.configurationGet(parameterNumber: 0xA), // kick alarm sensititivy (done: retrieved after alarm mode) 884 | // zwave.configurationV2.configurationGet(parameterNumber: 0xB), // local alarm control disable (done) 885 | // zwave.configurationV2.configurationGet(parameterNumber: 0xC), // electronic transition count 886 | // zwave.configurationV2.configurationGet(parameterNumber: 0xD), // mechanical transition count 887 | // zwave.configurationV2.configurationGet(parameterNumber: 0xE), // electronic failure count 888 | // zwave.configurationV2.configurationGet(parameterNumber: 0xF), // autolock (done) 889 | // zwave.configurationV2.configurationGet(parameterNumber: 0x10), // user code pin length (done) 890 | // zwave.configurationV2.configurationGet(parameterNumber: 0x11,) // electronic high preload transition count 891 | // zwave.configurationV2.configurationGet(parameterNumber: 0x12,) // bootloader version 892 | ], 5000) 893 | if (state.assoc == zwaveHubNodeId) 894 | { 895 | log.debug "$device.displayName is associated to ${state.assoc}" 896 | } 897 | else if (!state.associationQuery) 898 | { 899 | log.debug "checking association" 900 | cmds << "delay 4200" 901 | cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() // old Schlage locks use group 2 and don't secure the Association CC 902 | cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) 903 | state.associationQuery = new Date().time 904 | } 905 | else if (new Date().time - state.associationQuery.toLong() > 9000) 906 | { 907 | log.debug "setting association" 908 | cmds << "delay 6000" 909 | cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format() 910 | cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) 911 | cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() 912 | cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) 913 | state.associationQuery = new Date().time 914 | } 915 | log.debug "refresh sending ${cmds.inspect()}" 916 | cmds 917 | } 918 | 919 | def poll() 920 | { 921 | def cmds = [] 922 | if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) 923 | { 924 | log.debug "setting association" 925 | cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format() 926 | cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) 927 | cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() 928 | cmds << "delay 6000" 929 | cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) 930 | cmds << "delay 6000" 931 | state.associationQuery = new Date().time 932 | } 933 | else 934 | { 935 | // Only check lock state if it changed recently or we haven't had an update in an hour 936 | def latest = device.currentState("lock")?.date?.time 937 | if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 67 * 60)) 938 | { 939 | cmds << secure(zwave.doorLockV1.doorLockOperationGet()) 940 | state.lastPoll = (new Date()).time 941 | } 942 | else if (!state.codes) 943 | { 944 | state.pollCode = 1 945 | cmds << secure(zwave.userCodeV1.usersNumberGet()) 946 | } 947 | else if (state.pollCode && state.pollCode <= state.codes) 948 | { 949 | cmds << requestCode(state.pollCode) 950 | } 951 | else if (!state.lastbatt || (new Date().time) - state.lastbatt > 53*60*60*1000) 952 | { 953 | cmds << secure(zwave.batteryV1.batteryGet()) 954 | // } else { 955 | // // TODO: at some intervals, pick up some of the other configuration values 956 | // that might be useful 957 | } 958 | if(cmds) cmds << "delay 6000" 959 | } 960 | reportAllCodes(state) 961 | log.debug "poll is sending ${cmds.inspect()}, state: ${state.inspect()}" 962 | device.activity() // workaround to keep polling from being shut off 963 | cmds ?: null 964 | } 965 | 966 | def reportAllCodes(state) { 967 | def map = [ name: "reportAllCodes", data: state, displayed: true, isStateChange: true ] 968 | sendEvent(map) 969 | } 970 | 971 | def requestCode(codeNumber) 972 | { 973 | secure(zwave.userCodeV1.userCodeGet(userIdentifier: codeNumber)) 974 | } 975 | 976 | def reloadAllCodes() 977 | { 978 | def cmds = [] 979 | if (!state.codes) 980 | { 981 | state.requestCode = 1 982 | cmds << secure(zwave.userCodeV1.usersNumberGet()) 983 | } 984 | else 985 | { 986 | if(!state.requestCode) state.requestCode = 1 987 | cmds << requestCode(codeNumber) 988 | } 989 | cmds 990 | } 991 | 992 | def setCode(codeNumber, code) 993 | { 994 | def strcode = code 995 | log.debug "setting code $codeNumber to $code" 996 | if (code instanceof String) 997 | { 998 | code = code.toList().findResults { if(it > ' ' && it != ',' && it != '-') it.toCharacter() as Short } 999 | } 1000 | else 1001 | { 1002 | strcode = code.collect{ it as Character }.join() 1003 | } 1004 | if (state.blankcodes) 1005 | { 1006 | if (state["code$codeNumber"] != "") { // Can't just set, we won't be able to tell if it was successful 1007 | if (state["setcode$codeNumber"] != strcode) 1008 | { 1009 | state["resetcode$codeNumber"] = strcode 1010 | return deleteCode(codeNumber) 1011 | } 1012 | } 1013 | else 1014 | { 1015 | state["setcode$codeNumber"] = strcode 1016 | } 1017 | } 1018 | secureSequence([ 1019 | zwave.userCodeV1.userCodeSet(userIdentifier:codeNumber, userIdStatus:1, user:code), 1020 | zwave.userCodeV1.userCodeGet(userIdentifier:codeNumber) 1021 | ], 7000) 1022 | } 1023 | 1024 | def deleteCode(codeNumber) 1025 | { 1026 | log.debug "deleting code $codeNumber" 1027 | secureSequence([ 1028 | zwave.userCodeV1.userCodeSet(userIdentifier:codeNumber, userIdStatus:0), 1029 | zwave.userCodeV1.userCodeGet(userIdentifier:codeNumber) 1030 | ], 7000) 1031 | } 1032 | 1033 | def updateCodes(codeSettings) 1034 | { 1035 | if(codeSettings instanceof String) codeSettings = util.parseJson(codeSettings) 1036 | def set_cmds = [] 1037 | def get_cmds = [] 1038 | codeSettings.each { name, updated -> 1039 | def current = state[name] 1040 | if (name.startsWith("code")) 1041 | { 1042 | def n = name[4..-1].toInteger() 1043 | log.debug "$name was $current, set to $updated" 1044 | if (updated?.size() >= 4 && updated != current) 1045 | { 1046 | def cmds = setCode(n, updated) 1047 | set_cmds << cmds.first() 1048 | get_cmds << cmds.last() 1049 | } 1050 | else if ((current && updated == "") || updated == "0") 1051 | { 1052 | def cmds = deleteCode(n) 1053 | set_cmds << cmds.first() 1054 | get_cmds << cmds.last() 1055 | } 1056 | else if (updated && updated.size() < 4) 1057 | { 1058 | // Entered code was too short 1059 | codeSettings["code$n"] = current 1060 | } 1061 | } 1062 | else 1063 | log.warn("unexpected entry $name: $updated") 1064 | } 1065 | if (set_cmds) 1066 | { 1067 | return response(delayBetween(set_cmds, 2200) + ["delay 2200"] + delayBetween(get_cmds, 4200)) 1068 | } 1069 | } 1070 | 1071 | def getCode(codeNumber) 1072 | { 1073 | state["code$codeNumber"] 1074 | } 1075 | 1076 | def getAllCodes() 1077 | { 1078 | state.findAll { it.key.startsWith 'code' } 1079 | } 1080 | 1081 | private secure(physicalgraph.zwave.Command cmd) 1082 | { 1083 | zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() 1084 | } 1085 | 1086 | private secureSequence(commands, delay=4200) 1087 | { 1088 | delayBetween(commands.collect{ secure(it) }, delay) 1089 | } 1090 | 1091 | private Boolean secondsPast(timestamp, seconds) 1092 | { 1093 | if (!(timestamp instanceof Number)) 1094 | { 1095 | if (timestamp instanceof Date) 1096 | { 1097 | timestamp = timestamp.time 1098 | } 1099 | else if ((timestamp instanceof String) && timestamp.isNumber()) 1100 | { 1101 | timestamp = timestamp.toLong() 1102 | } 1103 | else 1104 | { 1105 | return true 1106 | } 1107 | } 1108 | return (new Date().time - timestamp) > (seconds * 1000) 1109 | } 1110 | 1111 | private allCodesDeleted() 1112 | { 1113 | if (state.codes instanceof Integer) 1114 | { 1115 | (1..state.codes).each { n -> 1116 | if (state["code$n"]) 1117 | { 1118 | result << createEvent(name: "codeReport", value: n, data: [ code: "" ], descriptionText: "code $n was deleted", 1119 | displayed: false, isStateChange: true) 1120 | } 1121 | state["code$n"] = "" 1122 | } 1123 | } 1124 | } 1125 | 1126 | // all the on/off parameters work the same way, so make a common method 1127 | // to deal with them 1128 | // 1129 | def setOnOffParameter(paramName, paramNumber) 1130 | { 1131 | def cmds = null 1132 | def cs = device.currentValue(paramName) 1133 | 1134 | // change parameter to the 'unknown' value - it will get refreshed after it is done changing 1135 | sendEvent(name: paramName, value: "unknown_${paramName}", displayed: false ) 1136 | 1137 | if (cs == "on_${paramName}") 1138 | { 1139 | // turn it off 1140 | cmds = secureSequence([zwave.configurationV2.configurationSet(parameterNumber: paramNumber, size: 1, configurationValue: [0])],5000) 1141 | } 1142 | else if (cs == "off_${paramName}") 1143 | { 1144 | // turn it on 1145 | cmds = secureSequence([zwave.configurationV2.configurationSet(parameterNumber: paramNumber, size: 1, configurationValue: [0xFF])],5000) 1146 | } 1147 | else 1148 | { 1149 | // it's in an unknown state, so just query it 1150 | cmds = secureSequence([zwave.configurationV2.configurationGet(parameterNumber: paramNumber)], 5000) 1151 | } 1152 | 1153 | log.debug "set $paramName sending ${cmds.inspect()}" 1154 | 1155 | cmds 1156 | } 1157 | 1158 | def setBeeperMode() 1159 | { 1160 | setOnOffParameter("beeperMode", 0x3) 1161 | } 1162 | 1163 | def setVacationMode() 1164 | { 1165 | setOnOffParameter("vacationMode", 0x4) 1166 | } 1167 | 1168 | def setLockLeave() 1169 | { 1170 | setOnOffParameter("lockLeave", 0x5) 1171 | } 1172 | 1173 | def setLocalControl() 1174 | { 1175 | setOnOffParameter("localControl", 0xB) 1176 | } 1177 | 1178 | def setAutoLock() 1179 | { 1180 | setOnOffParameter("autoLock", 0xF) 1181 | } 1182 | 1183 | def setAlarmMode() 1184 | { 1185 | 1186 | def cs = device.currentValue("alarmMode") 1187 | def newMode = 0x0 1188 | 1189 | def cmds = null 1190 | 1191 | switch (cs) 1192 | { 1193 | case "Off_alarmMode": 1194 | newMode = 0x1 1195 | break 1196 | 1197 | case "Alert_alarmMode": 1198 | newMode = 0x2 1199 | break 1200 | 1201 | case "Tamper_alarmMode": 1202 | newMode = 0x3 1203 | break; 1204 | 1205 | case "Kick_alarmMode": 1206 | newMode = 0x0 1207 | break; 1208 | 1209 | case "unknown_alarmMode": 1210 | default: 1211 | // don't send a mode - instead request the current state 1212 | cmds = secureSequence([zwave.configurationV2.configurationGet(parameterNumber: 0x7)], 5000) 1213 | 1214 | } 1215 | if (cmds == null) 1216 | { 1217 | // change the alarmSensitivity to the 'unknown' value - it will get refreshed after the alarm mode is done changing 1218 | sendEvent(name: 'alarmSensitivity', value: 0, displayed: false ) 1219 | cmds = secureSequence([zwave.configurationV2.configurationSet(parameterNumber: 7, size: 1, configurationValue: [newMode])],5000) 1220 | } 1221 | 1222 | log.debug "setAlarmMode sending ${cmds.inspect()}" 1223 | cmds 1224 | } 1225 | 1226 | def setPinLength(newValue) 1227 | { 1228 | def cmds = null 1229 | if ((newValue == null) || (newValue == 0)) 1230 | { 1231 | // just send a request to refresh the value 1232 | cmds = secureSequence([zwave.configurationV2.configurationGet(parameterNumber: 0x10)],5000) 1233 | } 1234 | else if (newValue <= 8) 1235 | { 1236 | sendEvent(descriptionText: "$device.displayName attempting to change PIN length to $newValue", displayed: true, isStateChange: true) 1237 | cmds = secureSequence([zwave.configurationV2.configurationSet(parameterNumber: 10, size: 1, configurationValue: [newValue])],5000) 1238 | } 1239 | else 1240 | { 1241 | sendEvent(descriptionText: "$device.displayName UNABLE to set PIN length of $newValue", displayed: true, isStateChange: true) 1242 | } 1243 | log.debug "setPinLength sending ${cmds.inspect()}" 1244 | cmds 1245 | } 1246 | 1247 | def setAlarmSensitivity(newValue) 1248 | { 1249 | def cmds = null 1250 | if (newValue != null) 1251 | { 1252 | // newvalue will be between 0 and 99, but we need a value between 1 and 5 inclusive... 1253 | newValue = (newValue / 20) + 1 1254 | newValue = newValue.toInteger(); 1255 | 1256 | // there are three possible values to set. which one depends on the current alarmMode 1257 | def cs = device.currentValue("alarmMode") 1258 | 1259 | def paramToSet = 0 1260 | 1261 | switch(cs) 1262 | { 1263 | case "Off": 1264 | // do nothing. the slider should be disabled anyway 1265 | break 1266 | case "Alert": 1267 | // set param 8 1268 | paramToSet = 0x8 1269 | break; 1270 | case "Tamper": 1271 | paramToSet = 0x9 1272 | break 1273 | case "Kick": 1274 | paramToSet = 0xA 1275 | break 1276 | default: 1277 | sendEvent(descriptionText: "$device.displayName unable to set alarm sensitivity while alarm mode in unknown state", displayed: true, isStateChange: true) 1278 | break 1279 | } 1280 | if (paramToSet != 0) 1281 | { 1282 | // first set the attribute to 0 for UI purposes 1283 | sendEvent(name: 'alarmSensitivity', value: 0, displayed: false ) 1284 | // then add the actual attribute set call 1285 | cmds = secureSequence([zwave.configurationV2.configurationSet(parameterNumber: paramToSet, size: 1, configurationValue: [newValue])],5000) 1286 | log.debug "setAlarmSensitivity sending ${cmds.inspect()}" 1287 | } 1288 | } 1289 | cmds 1290 | } 1291 | -------------------------------------------------------------------------------- /fibaro_motion_sensor.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Device Type Definition File 3 | * 4 | * Device Type: Fibaro Motion Sensor 5 | * File Name: fibaro-motion-sensor.groovy 6 | * Initial Release: 2014-12-10 7 | * Author: Todd Wackford 8 | * Email: todd@wackford.net 9 | * 10 | * Copyright 2014 SmartThings 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 13 | * in compliance with the License. You may obtain a copy of the License at: 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 18 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 19 | * for the specific language governing permissions and limitations under the License. 20 | * 21 | *************************************************************************************** 22 | * 23 | * Change Log: 24 | * 25 | * 1. 20150125 Todd Wackford 26 | * Incorporated Crc16Encap function to support core code changes. Duncan figured it 27 | * out as usual. 28 | * 29 | * 2. 20150125 Todd Wackford 30 | * Leaned out parse and moved most device info getting into configuration method. 31 | */ 32 | 33 | /** 34 | * Sets up metadata, simulator info and tile definition. 35 | * 36 | *

Bugs: There is a bug where it appears that ST is not checking the object name 37 | * for acceleration. So if you also have a motion object, they will step on each other 38 | * with thier 'active/inactive' state changes. "LOOK STBUG" is where future updates 39 | * need to be made once the bug is fixed. 40 | * 41 | * @param none 42 | * 43 | * @return none 44 | */ 45 | metadata { 46 | definition (name: "Fibaro Motion Sensor", namespace: "smartthings", author: "Todd Wackford") { 47 | capability "Motion Sensor" 48 | capability "Temperature Measurement" 49 | capability "Acceleration Sensor" 50 | capability "Configuration" 51 | capability "Illuminance Measurement" 52 | capability "Sensor" 53 | capability "Battery" 54 | 55 | command "resetParams2StDefaults" 56 | command "listCurrentParams" 57 | command "updateZwaveParam" 58 | command "test" 59 | command "configure" 60 | 61 | fingerprint deviceId: "0x2001", inClusters: "0x30,0x84,0x85,0x80,0x8F,0x56,0x72,0x86,0x70,0x8E,0x31,0x9C,0xEF,0x30,0x31,0x9C" 62 | } 63 | 64 | simulator { 65 | // messages the device returns in response to commands it receives 66 | status "motion (basic)" : "command: 2001, payload: FF" 67 | status "no motion (basic)" : "command: 2001, payload: 00" 68 | status "motion (binary)" : "command: 3003, payload: FF" 69 | status "no motion (binary)" : "command: 3003, payload: 00" 70 | 71 | for (int i = 0; i <= 100; i += 20) { 72 | status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( 73 | scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage() 74 | } 75 | 76 | for (int i = 200; i <= 1000; i += 200) { 77 | status "luminance ${i} lux": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( 78 | scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage() 79 | } 80 | 81 | for (int i = 0; i <= 100; i += 20) { 82 | status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport( 83 | batteryLevel: i).incomingMessage() 84 | } 85 | } 86 | 87 | tiles { 88 | standardTile("motion", "device.motion", width: 2, height: 2) { 89 | state "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0" 90 | state "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" 91 | } 92 | valueTile("temperature", "device.temperature", inactiveLabel: false) { 93 | state "temperature", label:'${currentValue}°', 94 | backgroundColors:[ 95 | [value: 31, color: "#153591"], 96 | [value: 44, color: "#1e9cbb"], 97 | [value: 59, color: "#90d2a7"], 98 | [value: 74, color: "#44b621"], 99 | [value: 84, color: "#f1d801"], 100 | [value: 95, color: "#d04e00"], 101 | [value: 96, color: "#bc2323"] 102 | ] 103 | } 104 | valueTile("illuminance", "device.illuminance", inactiveLabel: false) { 105 | state "luminosity", label:'${currentValue} ${unit}', unit:"lux" 106 | } 107 | valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { 108 | state "battery", label:'${currentValue}% battery', unit:"" 109 | } 110 | standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { 111 | state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" 112 | } 113 | 114 | // There is a bug that does not look at the tile name. If you use 'active/inactive', it will step on 115 | // the motion sensor. Delete this tile below and uncomment the tile below this one. LOOK 4 'STBUG' to 116 | // see where else you need to update when bug is fixed. 117 | 118 | standardTile("acceleration", "device.acceleration") { 119 | state("vibration", label:'vibration', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") 120 | state("still", label:'still', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") 121 | } 122 | 123 | /* 124 | standardTile("acceleration", "device.acceleration") { 125 | state("active", label:'vibration', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") 126 | state("inactive", label:'still', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") 127 | } 128 | */ 129 | 130 | main(["motion", "temperature", "acceleration", "illuminance"]) 131 | details(["motion", "temperature", "acceleration", "battery", "illuminance", "configure"]) 132 | } 133 | } 134 | 135 | /** 136 | * Configures the device to settings needed by SmarthThings at device discovery time. 137 | * 138 | * @param none 139 | * 140 | * @return none 141 | */ 142 | def configure() { 143 | log.debug "Configuring Device For SmartThings Use" 144 | def cmds = [] 145 | 146 | // send associate to group 3 to get sensor data reported only to hub 147 | cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format() 148 | 149 | // turn on tamper sensor with active/inactive reports (use it as an acceleration sensor) default is 0, or off 150 | cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 24, size: 1).format() 151 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 24).format() 152 | 153 | // temperature change report threshold (0-255 = 0.1 to 25.5C) default is 1.0 Celcius, setting to .5 Celcius 154 | cmds << zwave.configurationV1.configurationSet(configurationValue: [5], parameterNumber: 60, size: 1).format() 155 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 60).format() 156 | 157 | cmds << response(zwave.batteryV1.batteryGet()) 158 | cmds << response(zwave.versionV1.versionGet().format()) 159 | cmds << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) 160 | cmds << response(zwave.firmwareUpdateMdV2.firmwareMdGet().format()) 161 | 162 | delayBetween(cmds, 500) 163 | } 164 | 165 | // Parse incoming device messages to generate events 166 | def parse(String description) 167 | { 168 | def result = [] 169 | def cmd = zwave.parse(description, [0x72: 2, 0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2, 0x80: 1, 0x86: 1, 0x7A: 1, 0x56: 1]) 170 | 171 | if (description == "updated") { 172 | result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 7200, nodeid:zwaveHubNodeId)) 173 | result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) 174 | } 175 | 176 | if (cmd) { 177 | if( cmd.CMD == "8407" ) { 178 | log.debug "got 8407 - whatever that is" 179 | result << response(zwave.batteryV1.batteryGet().format()) 180 | result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) 181 | } 182 | result << zwaveEvent(cmd) 183 | } 184 | 185 | if ( result[0] != null ) { 186 | log.debug "Parse returned ${result}" 187 | result 188 | } 189 | } 190 | 191 | def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) 192 | { 193 | def versions = [0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2] 194 | // def encapsulatedCommand = cmd.encapsulatedCommand(versions) 195 | def version = versions[cmd.commandClass as Integer] 196 | def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) 197 | def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) 198 | if (!encapsulatedCommand) { 199 | log.debug "Could not extract command from $cmd" 200 | } else { 201 | zwaveEvent(encapsulatedCommand) 202 | } 203 | } 204 | 205 | def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { 206 | updateDataValue("applicationVersion", "${cmd.applicationVersion}") 207 | log.debug "applicationVersion: ${cmd.applicationVersion}" 208 | log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" 209 | log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" 210 | log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" 211 | log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" 212 | } 213 | 214 | def zwaveEvent(physicalgraph.zwave.commands.firmwareupdatemdv1.FirmwareMdReport cmd) { 215 | log.debug "checksum: ${cmd.checksum}" 216 | log.debug "firmwareId: ${cmd.firmwareId}" 217 | log.debug "manufacturerId: ${cmd.manufacturerId}" 218 | } 219 | 220 | def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) 221 | { 222 | def map = [:] 223 | map.name = "acceleration" 224 | 225 | // LOOK STBUG 226 | // When thebug is fized (see tile definition above), uncomment the following two lines and delete the 227 | // two line below them. 228 | /* 229 | map.value = cmd.sensorState ? "active" : "inactive" //UNCOMMENT ME 230 | if (map.value == "active") { //UNCOMMENT ME 231 | */ 232 | map.value = cmd.sensorState ? "vibration" : "still" //DELETE ME 233 | if (map.value == "vibration") { //DELETE ME 234 | map.descriptionText = "$device.displayName detected vibration" 235 | } 236 | else { 237 | map.descriptionText = "$device.displayName vibration has stopped" 238 | } 239 | createEvent(map) 240 | } 241 | 242 | // Event Generation 243 | def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) 244 | { 245 | sendEvent(descriptionText: "${device.displayName} woke up", displayed: true, isStateChange: true) 246 | def result = [] 247 | result << zwave.configurationV1.configurationSet(configurationValue: [26], parameterNumber: 1, size: 1).format() 248 | result << "delay 1200" 249 | result << zwave.wakeUpV1.wakeUpIntervalSet(seconds: 7200, nodeid:zwaveHubNodeId).format() // this should be changed back to 7200 250 | result << zwave.batteryV1.batteryGet().format() 251 | result << "delay 1200" 252 | result << zwave.wakeUpV1.wakeUpNoMoreInformation().format() 253 | 254 | response(result) 255 | 256 | } 257 | 258 | def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) 259 | { 260 | def map = [:] 261 | switch (cmd.sensorType) { 262 | case 1: 263 | // temperature 264 | def cmdScale = cmd.scale == 1 ? "F" : "C" 265 | map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) 266 | map.unit = getTemperatureScale() 267 | map.name = "temperature" 268 | break; 269 | case 3: 270 | // luminance 271 | map.value = cmd.scaledSensorValue.toInteger().toString() 272 | map.unit = "lux" 273 | map.name = "illuminance" 274 | break; 275 | default: 276 | log.debug "HEY! Got unexpected SensorMulti type $cmd.sensorType with a value of $cmd.scaledSensorValue" 277 | } 278 | createEvent(map) 279 | } 280 | 281 | def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { 282 | log.debug cmd 283 | def map = [:] 284 | map.name = "battery" 285 | map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel : 1 286 | map.unit = "%" 287 | map.displayed = true 288 | map.isStateChange = true 289 | createEvent(map) 290 | } 291 | 292 | def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { 293 | def map = [:] 294 | // log.debug "via binary report" 295 | map.value = cmd.sensorValue ? "active" : "inactive" 296 | map.name = "motion" 297 | if (map.value == "active") { 298 | map.descriptionText = "$device.displayName detected motion" 299 | } 300 | else { 301 | map.descriptionText = "$device.displayName motion has stopped" 302 | } 303 | createEvent(map) 304 | } 305 | 306 | def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { 307 | /* 308 | def map = [:] 309 | log.debug "via basicset" 310 | map.value = cmd.value ? "active" : "inactive" 311 | map.name = "motion" 312 | if (map.value == "active") { 313 | map.descriptionText = "$device.displayName detected motion" 314 | } 315 | else { 316 | map.descriptionText = "$device.displayName motion has stopped" 317 | } 318 | map 319 | */ 320 | } 321 | 322 | def zwaveEvent(physicalgraph.zwave.Command cmd) { 323 | log.debug "Catchall reached for cmd: ${cmd.toString()}}" 324 | createEvent([:]) 325 | } 326 | 327 | def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { 328 | log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" 329 | } 330 | 331 | def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { 332 | def result = [] 333 | 334 | def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) 335 | log.debug "msr: $msr" 336 | updateDataValue("MSR", msr) 337 | 338 | if ( msr == "010F-0800-2001" ) { //this is the msr and device type for the fibaro motion sensor 339 | configure() 340 | } 341 | 342 | result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) 343 | result 344 | } 345 | 346 | //used to add "test" button for simulation of user changes to parameters 347 | def test() { 348 | //def params = [paramNumber:80,value:10,size:1] 349 | def params = [paramNumber: 42, value: [14,16], size:2] 350 | updateZwaveParam(params) 351 | } 352 | 353 | /** 354 | * This method will allow the user to update device parameters (behavior) from an app. 355 | * A "Zwave Tweaker" app will be developed as an interface to do this. Or the user can 356 | * write his/her own app to envoke this method. No type or value checking is done to 357 | * compare to what device capability or reaction. It is up to user to read OEM 358 | * documentation prio to envoking this method. 359 | * 360 | *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! 361 | * 362 | * @param List[paramNumber:80,value:10,size:1] 363 | * 364 | * 365 | * @return none 366 | */ 367 | def updateZwaveParam(params) { 368 | if ( params ) { 369 | def pNumber = params.paramNumber 370 | def pSize = params.size 371 | def pValue = params.value 372 | log.debug "Make sure device is awake and in recieve mode" 373 | log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'" 374 | 375 | def cmds = [] 376 | cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() 377 | cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() 378 | delayBetween(cmds, 1000) 379 | } 380 | } 381 | 382 | /** 383 | * Sets all of available Fibaro parameters back to the device defaults except for what 384 | * SmartThings needs to support the stock functionality as released. This will be 385 | * called from the "Fibaro Tweaker" or user's app. 386 | * 387 | *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! 388 | * 389 | * @param none 390 | * 391 | * @return none 392 | */ 393 | def resetParams2StDefaults() { 394 | log.debug "Resetting Sensor Parameters to SmartThings Compatible Defaults" 395 | def cmds = [] 396 | cmds << zwave.configurationV1.configurationSet(configurationValue: [26], parameterNumber: 1, size: 1).format() // motion sensor sensitivity (lower is more sensitive) (def 10) 397 | cmds << zwave.configurationV1.configurationSet(configurationValue: [15], parameterNumber: 2, size: 1).format() // motion sensor "blind" time (def: 15) 398 | cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format() // PIR "pulse counter" (def: 3) 399 | cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 4, size: 1).format() // PIR "window time" (def: 2 (12 seconds)) 400 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0,30], parameterNumber: 6, size: 2).format() // motion alarm cancellation delay (def: 30 seconds) 401 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 8, size: 1).format() // PIR operating mode (def: 0 (always active)) 402 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0,50], parameterNumber: 9, size: 2).format() // lux value greater than this is 'day' (def: 200) 403 | cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 12, size: 1).format() // only send basic "ON" command - not "OFF" 404 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 16, size: 1).format() // 405 | cmds << zwave.configurationV1.configurationSet(configurationValue: [5], parameterNumber: 20, size: 1).format() // tamper sensitivity (def: 15) 406 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0,30], parameterNumber: 22, size: 2).format() // tamper alarm cancel delay (def 30) 407 | cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 24, size: 1).format() // tamper operating mode (def 0, use 4 for ST) 408 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 26, size: 1).format() // tamper alarm broadcast mode (def 0 - no broadcast) 409 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0,100], parameterNumber: 40, size: 2).format() // illumination lux threshold (def 200) 410 | cmds << zwave.configurationV1.configurationSet(configurationValue: [14,16], parameterNumber: 42, size: 2).format() // illumination rpt interval (every 30 min) 411 | cmds << zwave.configurationV1.configurationSet(configurationValue: [5], parameterNumber: 60, size: 1).format() 412 | cmds << zwave.configurationV1.configurationSet(configurationValue: [3,132], parameterNumber: 62, size: 2).format() 413 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 64, size: 2).format() 414 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 66, size: 2).format() 415 | cmds << zwave.configurationV1.configurationSet(configurationValue: [19], parameterNumber: 80, size: 1).format() // LED signaling mode 416 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 81, size: 1).format() 417 | cmds << zwave.configurationV1.configurationSet(configurationValue: [0,10], parameterNumber: 82, size: 2).format() 418 | cmds << zwave.configurationV1.configurationSet(configurationValue: [3,232], parameterNumber: 83, size: 2).format() 419 | cmds << zwave.configurationV1.configurationSet(configurationValue: [18], parameterNumber: 86, size: 1).format() 420 | cmds << zwave.configurationV1.configurationSet(configurationValue: [26], parameterNumber: 87, size: 1).format() 421 | cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 89, size: 1).format() 422 | 423 | cmds << zwave.batteryV1.batteryGet().format() // send this so we can see in the debug log when the configurations are all done sending 424 | delayBetween(cmds, 500) 425 | } 426 | 427 | /** 428 | * Lists all of available Fibaro parameters and thier current settings out to the 429 | * logging window in the IDE This will be called from the "Fibaro Tweaker" or 430 | * user's own app. 431 | * 432 | *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! 433 | * 434 | * @param none 435 | * 436 | * @return none 437 | */ 438 | def listCurrentParams() { 439 | log.debug "Listing of current parameter settings of ${device.displayName}" 440 | def cmds = [] 441 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format() 442 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format() 443 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format() 444 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 4).format() 445 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 6).format() 446 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 8).format() 447 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format() 448 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() 449 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 14).format() 450 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 16).format() 451 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 20).format() 452 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 22).format() 453 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 24).format() 454 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 26).format() 455 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 40).format() 456 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 42).format() 457 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 60).format() 458 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 62).format() 459 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 64).format() 460 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 66).format() 461 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 80).format() 462 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 81).format() 463 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 82).format() 464 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 83).format() 465 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 86).format() 466 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 87).format() 467 | cmds << zwave.configurationV1.configurationGet(parameterNumber: 89).format() 468 | 469 | cmds << response(zwave.versionV1.versionGet().format()) 470 | cmds << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) 471 | cmds << response(zwave.firmwareUpdateMdV2.firmwareMdGet().format()) 472 | 473 | delayBetween(cmds, 500) 474 | } 475 | -------------------------------------------------------------------------------- /my_z-wave_garage_door_opener.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Z-Wave Garage Door Opener 3 | * 4 | * Copyright 2014 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 | metadata { 17 | definition (name: "My Z-Wave Garage Door Opener", namespace: "smartthings", author: "SmartThings") { 18 | capability "Actuator" 19 | capability "Door Control" 20 | capability "Contact Sensor" 21 | capability "Refresh" 22 | capability "Sensor" 23 | capability "Polling" 24 | capability "Switch" 25 | capability "Momentary" 26 | capability "Relay Switch" 27 | 28 | 29 | fingerprint deviceId: "0x4007", inClusters: "0x98" 30 | fingerprint deviceId: "0x4006", inClusters: "0x98" 31 | } 32 | 33 | simulator { 34 | status "closed": "command: 9881, payload: 00 66 03 00" 35 | status "opening": "command: 9881, payload: 00 66 03 FE" 36 | status "open": "command: 9881, payload: 00 66 03 FF" 37 | status "closing": "command: 9881, payload: 00 66 03 FC" 38 | status "unknown": "command: 9881, payload: 00 66 03 FD" 39 | 40 | reply "988100660100": "command: 9881, payload: 00 66 03 FC" 41 | reply "9881006601FF": "command: 9881, payload: 00 66 03 FE" 42 | } 43 | 44 | tiles { 45 | standardTile("toggle", "device.door", width: 2, height: 2) { 46 | state("unknown", label:'${name}', action:"refresh.refresh", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e") 47 | state("closed", label:'${name}', action:"door control.open", icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821", nextState:"opening") 48 | state("open", label:'${name}', action:"door control.close", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e", nextState:"closing") 49 | state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e") 50 | state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e") 51 | 52 | } 53 | standardTile("open", "device.door", inactiveLabel: false, decoration: "flat") { 54 | state "default", label:'open', action:"door control.open", icon:"st.doors.garage.garage-opening" 55 | } 56 | standardTile("close", "device.door", inactiveLabel: false, decoration: "flat") { 57 | state "default", label:'close', action:"door control.close", icon:"st.doors.garage.garage-closing" 58 | } 59 | standardTile("refresh", "device.door", inactiveLabel: false, decoration: "flat") { 60 | state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" 61 | } 62 | main "toggle" 63 | details(["toggle", "open", "close", "refresh"]) 64 | } 65 | } 66 | 67 | 68 | import physicalgraph.zwave.commands.barrieroperatorv1.* 69 | 70 | def parse(String description) { 71 | def result = null 72 | if (description.startsWith("Err")) { 73 | if (state.sec) { 74 | result = createEvent(descriptionText:description, displayed:false) 75 | } else { 76 | result = createEvent( 77 | descriptionText: "This device failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", 78 | eventType: "ALERT", 79 | name: "secureInclusion", 80 | value: "failed", 81 | displayed: true, 82 | ) 83 | } 84 | } else { 85 | def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2 ]) 86 | if (cmd) { 87 | result = zwaveEvent(cmd) 88 | } 89 | } 90 | log.debug "\"$description\" parsed to ${result.inspect()}" 91 | result 92 | } 93 | 94 | def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { 95 | def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1]) 96 | log.debug "encapsulated: $encapsulatedCommand" 97 | if (encapsulatedCommand) { 98 | zwaveEvent(encapsulatedCommand) 99 | } 100 | } 101 | 102 | def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { 103 | createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful") 104 | } 105 | 106 | def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { 107 | state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join() 108 | if (cmd.commandClassControl) { 109 | state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join() 110 | } 111 | log.debug "Security command classes: $state.sec" 112 | createEvent(name:"secureInclusion", value:"success", descriptionText:"$device.displayText is securely included") 113 | } 114 | 115 | def zwaveEvent(BarrierOperatorReport cmd) { 116 | def result = [] 117 | def map = [ name: "door" ] 118 | def switchMap = [ name: "switch" ] 119 | 120 | switch (cmd.barrierState) { 121 | case BarrierOperatorReport.BARRIER_STATE_CLOSED: 122 | map.value = "closed" 123 | result << createEvent(name: "contact", value: "closed", displayed: false) 124 | result << createEvent(name: "switch", value: "off", displayed: false) 125 | break 126 | case BarrierOperatorReport.BARRIER_STATE_UNKNOWN_POSITION_MOVING_TO_CLOSE: 127 | map.value = "closing" 128 | break 129 | case BarrierOperatorReport.BARRIER_STATE_UNKNOWN_POSITION_STOPPED: 130 | map.descriptionText = "$device.displayName door state is unknown" 131 | map.value = "unknown" 132 | break 133 | case BarrierOperatorReport.BARRIER_STATE_UNKNOWN_POSITION_MOVING_TO_OPEN: 134 | map.value = "opening" 135 | result << createEvent(name: "contact", value: "open", displayed: false) 136 | break 137 | case BarrierOperatorReport.BARRIER_STATE_OPEN: 138 | map.value = "open" 139 | result << createEvent(name: "contact", value: "open", displayed: false) 140 | result << createEvent(name: "switch", value: "on", displayed: false) 141 | break 142 | } 143 | result + createEvent(map) 144 | } 145 | 146 | def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { 147 | def result = [] 148 | def map = [:] 149 | if (cmd.notificationType == 6) { 150 | map.displayed = true 151 | switch(cmd.event) { 152 | case 0x40: 153 | if (cmd.eventParameter[0]) { 154 | map.descriptionText = "$device.displayName performing initialization process" 155 | } else { 156 | map.descriptionText = "$device.displayName initialization process complete" 157 | } 158 | break 159 | case 0x41: 160 | map.descriptionText = "$device.displayName door operation force has been exceeded" 161 | break 162 | case 0x42: 163 | map.descriptionText = "$device.displayName motor has exceeded operational time limit" 164 | break 165 | case 0x43: 166 | map.descriptionText = "$device.displayName has exceeded physical mechanical limits" 167 | break 168 | case 0x44: 169 | map.descriptionText = "$device.displayName unable to perform requested operation (UL requirement)" 170 | break 171 | case 0x45: 172 | map.descriptionText = "$device.displayName remote operation disabled (UL requirement)" 173 | break 174 | case 0x46: 175 | map.descriptionText = "$device.displayName failed to perform operation due to device malfunction" 176 | break 177 | case 0x47: 178 | if (cmd.eventParameter[0]) { 179 | map.descriptionText = "$device.displayName vacation mode enabled" 180 | } else { 181 | map.descriptionText = "$device.displayName vacation mode disabled" 182 | } 183 | break 184 | case 0x48: 185 | if (cmd.eventParameter[0]) { 186 | map.descriptionText = "$device.displayName safety beam obstructed" 187 | } else { 188 | map.descriptionText = "$device.displayName safety beam obstruction cleared" 189 | } 190 | break 191 | case 0x49: 192 | if (cmd.eventParameter[0]) { 193 | map.descriptionText = "$device.displayName door sensor ${cmd.eventParameter[0]} not detected" 194 | } else { 195 | map.descriptionText = "$device.displayName door sensor not detected" 196 | } 197 | break 198 | case 0x4A: 199 | if (cmd.eventParameter[0]) { 200 | map.descriptionText = "$device.displayName door sensor ${cmd.eventParameter[0]} has a low battery" 201 | } else { 202 | map.descriptionText = "$device.displayName door sensor has a low battery" 203 | } 204 | result << createEvent(name: "battery", value: 1, unit: "%", descriptionText: map.descriptionText) 205 | break 206 | case 0x4B: 207 | map.descriptionText = "$device.displayName detected a short in wall station wires" 208 | break 209 | case 0x4C: 210 | map.descriptionText = "$device.displayName is associated with non-Z-Wave remote control" 211 | break 212 | default: 213 | map.descriptionText = "$device.displayName: access control alarm $cmd.event" 214 | map.displayed = false 215 | break 216 | } 217 | } else if (cmd.notificationType == 7) { 218 | switch (cmd.event) { 219 | case 1: 220 | case 2: 221 | map.descriptionText = "$device.displayName detected intrusion" 222 | break 223 | case 3: 224 | map.descriptionText = "$device.displayName tampering detected: product cover removed" 225 | break 226 | case 4: 227 | map.descriptionText = "$device.displayName tampering detected: incorrect code" 228 | break 229 | case 7: 230 | case 8: 231 | map.descriptionText = "$device.displayName detected motion" 232 | break 233 | default: 234 | map.descriptionText = "$device.displayName: security alarm $cmd.event" 235 | map.displayed = false 236 | } 237 | } else if (cmd.notificationType){ 238 | map.descriptionText = "$device.displayName: alarm type $cmd.notificationType event $cmd.event" 239 | } else { 240 | map.descriptionText = "$device.displayName: alarm $cmd.v1AlarmType is ${cmd.v1AlarmLevel == 255 ? 'active' : cmd.v1AlarmLevel ?: 'inactive'}" 241 | } 242 | result ? [createEvent(map), *result] : createEvent(map) 243 | } 244 | 245 | def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { 246 | def map = [ name: "battery", unit: "%" ] 247 | if (cmd.batteryLevel == 0xFF) { 248 | map.value = 1 249 | map.descriptionText = "$device.displayName has a low battery" 250 | } else { 251 | map.value = cmd.batteryLevel 252 | } 253 | state.lastbatt = new Date().time 254 | createEvent(map) 255 | } 256 | 257 | def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { 258 | def result = [] 259 | 260 | def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) 261 | log.debug "msr: $msr" 262 | updateDataValue("MSR", msr) 263 | 264 | result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) 265 | result 266 | } 267 | 268 | def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { 269 | def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" 270 | updateDataValue("fw", fw) 271 | def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" 272 | createEvent(descriptionText: text, isStateChange: false) 273 | } 274 | 275 | def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { 276 | def msg = cmd.status == 0 ? "try again later" : 277 | cmd.status == 1 ? "try again in $cmd.waitTime seconds" : 278 | cmd.status == 2 ? "request queued" : "sorry" 279 | createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg") 280 | } 281 | 282 | def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { 283 | createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request") 284 | } 285 | 286 | def zwaveEvent(physicalgraph.zwave.Command cmd) { 287 | createEvent(displayed: false, descriptionText: "$device.displayName: $cmd") 288 | } 289 | 290 | def open() { 291 | secure(zwave.barrierOperatorV1.barrierOperatorSet(requestedBarrierState: BarrierOperatorSet.REQUESTED_BARRIER_STATE_OPEN)) 292 | } 293 | 294 | def close() { 295 | secure(zwave.barrierOperatorV1.barrierOperatorSet(requestedBarrierState: BarrierOperatorSet.REQUESTED_BARRIER_STATE_CLOSE)) 296 | } 297 | 298 | 299 | def refresh() { 300 | secure(zwave.barrierOperatorV1.barrierOperatorGet()) 301 | } 302 | 303 | def poll() { 304 | secure(zwave.barrierOperatorV1.barrierOperatorGet()) 305 | } 306 | 307 | 308 | def on() { 309 | log.debug "on() was called and ignored" 310 | } 311 | 312 | def off() { 313 | log.debug "off() was called and ignored" 314 | } 315 | 316 | def push() { 317 | 318 | // get the current "door" attribute value 319 | // 320 | // For some reason, I can't use "device.doorState" or just "doorState". Not sure why not. 321 | 322 | def lastValue = device.latestValue("door"); 323 | 324 | // if its open, then close the door 325 | if (lastValue == "open") { 326 | return close() 327 | 328 | // if its closed, then open the door 329 | } else if (lastValue == "closed") { 330 | return open() 331 | 332 | } else { 333 | log.debug "push() called when door state is $lastValue - there's nothing push() can do" 334 | } 335 | 336 | } 337 | 338 | 339 | 340 | private secure(physicalgraph.zwave.Command cmd) { 341 | zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() 342 | } 343 | 344 | private secureSequence(commands, delay=200) { 345 | delayBetween(commands.collect{ secure(it) }, delay) 346 | } 347 | -------------------------------------------------------------------------------- /smartApps/Door Unlock Triggers.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Door Unlock Triggers 3 | * 4 | * 28-Feb-2015: fix timezone bug 5 | * 27-Feb-2015: added "nightonly" toggle 6 | * 7 | * Copyright 2015 Gary D 8 | * 9 | * Licensed under the Apache License, Version 2.0 WITH EXCEPTIONS; you may not use this file except 10 | * in compliance with the License AND Exceptions. You may obtain a copy of the License at: 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 15 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 16 | * for the specific language governing permissions and limitations under the License. 17 | * 18 | * 19 | * Exceptions are: 20 | * 1. This code may NOT be used without freely distributing the code freely and without limitation, 21 | * in source form. The distribution may be met with a link to source code, 22 | * 2. This code may NOT be used, directly or indirectly, for the purpose of any type of monetary 23 | * gain. This code may not be used in a larger entity which is being sold, leased, or 24 | * anything other than freely given. 25 | * 3. To clarify 1 and 2 above, if you use this code, it must for your own personal use, or be a 26 | * free project, and available to anyone with "no strings attached." (You may require a free 27 | * registration on a free website or portal in order to distribute the modifications.) 28 | * 4. The above listed exceptions to this code do not apply to "SmartThings, Inc." SmartThings, 29 | * is granted a license to use this code under the terms of the Apache License (version 2) with 30 | * no further exception. 31 | * 32 | */ 33 | definition( 34 | name: "Door Unlock Triggers", 35 | namespace: "garyd9", 36 | author: "Gary D", 37 | description: "Triggers switches, door controls, etc based on door control unlock events", 38 | category: "Convenience", 39 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 40 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 41 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 42 | 43 | 44 | preferences 45 | { 46 | 47 | section("When a door unlocks...") 48 | { 49 | input "lock1", "capability.lock", title: "Door Lock?", required: true 50 | } 51 | 52 | section("...and any of these codes (or methods) opened the door...") 53 | { 54 | input "unlockCode", "enum", title: "User Code", multiple: true, metadata:[values:["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","Manual","Other"]] 55 | } 56 | section("...Then...") 57 | { 58 | input "doors", "capability.doorControl", title: "Close these doors", multiple: true, required: false 59 | input "switches", "capability.switch", title: "Turn on these switches", multiple: true, required: false 60 | input name: "sendPush", type: "bool", title: "Push notification to mobile devices?", defaultValue: false 61 | } 62 | section("Within these limits:") 63 | { 64 | input name: "nightOnly", type: "bool", title: "Only between sunset and sunrise?" 65 | } 66 | 67 | } 68 | 69 | def installed() 70 | { 71 | initialize() 72 | } 73 | 74 | def updated() 75 | { 76 | unsubscribe() 77 | initialize() 78 | } 79 | 80 | def initialize() 81 | { 82 | subscribe(lock1, "lock.unlocked", lockHandler) 83 | if (nightOnly) 84 | { 85 | // force updating the sunrise/sunset data 86 | retrieveSunData(true) 87 | } 88 | 89 | } 90 | 91 | 92 | def performActions(evt) 93 | { 94 | doors.eachWithIndex {s, i -> 95 | def state = s.latestValue("door") 96 | if (state == "open") 97 | { 98 | log.debug "$s is currently $state. Closing it." 99 | s.close(); 100 | } 101 | else 102 | { 103 | log.debug "$s is currently $state. (can't close it)" 104 | } 105 | } 106 | 107 | switches.eachWithIndex {s, i -> 108 | def state = s.latestValue("switch") 109 | if (state == "off") 110 | { 111 | log.debug "$s is currently $state. Turning it on." 112 | s.on() 113 | } 114 | else 115 | { 116 | log.debug "$s is already $state." 117 | } 118 | } 119 | if (sendPush) 120 | { 121 | sendPushMessage(evt.descriptionText) 122 | } 123 | } 124 | 125 | def lockHandler(evt) 126 | { 127 | def bIsValidTime = !nightOnly 128 | if (!bIsValidTime) 129 | { 130 | def tz = TimeZone.getTimeZone("UTC") 131 | // possibly update the sunrise/sunset data. (don't force the update) 132 | retrieveSunData(false) 133 | bIsValidTime = ((now() < timeToday(state.sunriseTime, tz).time) || (now() > timeToday(state.sunsetTime, tz).time)) 134 | } 135 | 136 | if (bIsValidTime) 137 | { 138 | if (evt.data != null) 139 | { 140 | def evData = parseJson(evt.data) 141 | if (unlockCode.contains((evData.usedCode).toString())) 142 | { 143 | performActions(evt) 144 | } 145 | } 146 | else // evdata is null 147 | { 148 | if (evt.descriptionText.contains("manually")) 149 | { 150 | if (unlockCode.contains("Manual")) 151 | { // event describes a manual event, and unlockCode is looking for "manual"... 152 | performActions(evt) 153 | } 154 | } 155 | else if (unlockCode.contains("Other")) 156 | { // event description doesn't contain "manually" and the evt.data is null... so it must be "other" 157 | performActions(evt) 158 | } 159 | } 160 | } 161 | } 162 | 163 | def retrieveSunData(forceIt) 164 | { 165 | if ((true == forceIt) || (now() > state.nextSunCheck)) 166 | { 167 | state.nextSunCheck = now() + (1000 * (60 * 60 *12)) // every 12 hours 168 | log.debug "Updating sunrise/sunset data" 169 | 170 | /* instead of absolute timedate stamps for sunrise and sunset, use just hours/minutes. The reason 171 | is that if we miss updating the sunrise/sunset data for a day or two, at least the times will be 172 | within a few minutes */ 173 | 174 | 175 | def sunData = getSunriseAndSunset() 176 | 177 | state.sunsetTime = sunData.sunset.hours + ':' + sunData.sunset.minutes 178 | state.sunriseTime = sunData.sunrise.hours + ':' + sunData.sunrise.minutes 179 | 180 | log.debug "Sunrise time: ${state.sunriseTime} UTC" 181 | log.debug "Sunset time: ${state.sunsetTime} UTC" 182 | } 183 | } 184 | --------------------------------------------------------------------------------