-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathAJB SmartLock Zigbee.groovy
1202 lines (1125 loc) · 44.1 KB
/
AJB SmartLock Zigbee.groovy
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* ZigBee Lock
*
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
import physicalgraph.zigbee.zcl.DataType
metadata {
definition (name: "AJB SmartLock Zigbee", namespace: "abuttino", author: "abuttino", genericHandler: "Zigbee") {
capability "Actuator"
capability "Lock"
capability "Polling"
capability "Refresh"
capability "Sensor"
capability "Lock Codes"
capability "Battery"
capability "Configuration"
capability "Health Check"
fingerprint profileId: "0104", inClusters: "0000,0001,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale Touch Screen Lever Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD210 PB DB", deviceJoinName: "Yale Push Button Deadbolt Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL210 PB LL", deviceJoinName: "Yale Push Button Lever Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD226/246 TSDB", deviceJoinName: "Yale Assure Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020,0B05", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD446 BLE TSDB", deviceJoinName: "Yale Assure Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD216 PBDB", deviceJoinName: "Yale Push Button Deadbolt Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL226 TSLL", deviceJoinName: "Yale Assure Touch Screen Lever Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020,0B05", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL216 PB", deviceJoinName: "Yale Assure Keypad Lever Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_5", deviceJoinName: "Kwikset 5-Button Deadbolt"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_LEVER_5", deviceJoinName: "Kwikset 5-Button Lever"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10", deviceJoinName: "Kwikset 10-Button Deadbolt"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset 10-Button Touch Deadbolt"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0101", manufacturer:"Kwikset", model:"Smartcode", deviceJoinName: "Kwikset Smartcode Lock"
fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0009, 0020, 0101, 0B05, FC00", outClusters: "000A, 0019", manufacturer: "Schlage", model: "BE468", deviceJoinName: "Schlage Connect Smart Deadbolt"
}
tiles(scale: 2) {
multiAttributeTile(name:"toggle", type:"generic", width:6, height:4) {
tileAttribute ("device.lock", key:"PRIMARY_CONTROL") {
attributeState "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#00A0DC", nextState:"unlocking"
attributeState "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking"
attributeState "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking"
attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#00A0DC"
attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
}
}
standardTile("lock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking"
}
standardTile("unlock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking"
}
valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "battery", label:'${currentValue}% battery', unit:""
}
standardTile("refresh", "device.refresh", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "toggle"
details(["toggle", "lock", "unlock", "battery", "refresh"])
}
}
// Globals - Cluster IDs
private getCLUSTER_POWER() { 0x0001 }
private getCLUSTER_DOORLOCK() { 0x0101 }
private getCLUSTER_ALARM() { 0x0009 }
// Globals - Command IDs
private getDOORLOCK_CMD_LOCK_DOOR() { 0x00 }
private getDOORLOCK_CMD_UNLOCK_DOOR() { 0x01 }
private getDOORLOCK_CMD_USER_CODE_SET() { 0x05 }
private getDOORLOCK_CMD_USER_CODE_GET() { 0x06 }
private getDOORLOCK_CMD_CLEAR_USER_CODE() { 0x07 }
private getDOORLOCK_RESPONSE_OPERATION_EVENT() { 0x20 }
private getDOORLOCK_RESPONSE_PROGRAMMING_EVENT() { 0x21 }
private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 }
private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 }
private getDOORLOCK_ATTR_NUM_PIN_USERS() { 0x0012 }
private getDOORLOCK_ATTR_MAX_PIN_LENGTH() { 0x0017 }
private getDOORLOCK_ATTR_MIN_PIN_LENGTH() { 0x0018 }
private getDOORLOCK_ATTR_SEND_PIN_OTA() { 0x0032 }
private getALARM_ATTR_ALARM_COUNT() { 0x0000 }
private getALARM_CMD_ALARM() { 0x00 }
/**
* Called on app installed
*/
def installed() {
log.trace "ZigBee DTH - Executing installed() for device ${device.displayName}"
}
/**
* Called on app uninstalled
*/
def uninstalled() {
def deviceName = device.displayName
log.trace "ZigBee DTH - Executing uninstalled() for device $deviceName"
sendEvent(name: "lockRemoved", value: device.id, isStateChange: true, displayed: false)
}
/**
* Executed when the user taps on the 'Done' button on the device settings screen. Sends the values to lock.
*
* @return The list of commands to be executed
*/
def updated() {
try {
if (!state.init || !state.configured) {
// Executed when the lock is being paired
state.init = true
log.trace "ZigBee DTH - Returning commands for lock operation get and battery get"
def cmds = []
if (!state.configured) {
cmds << doConfigure()
}
cmds << zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE)
cmds << zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING)
cmds = cmds.flatten()
log.info "ZigBee DTH - updated() returning with cmds:- $cmds"
return response(cmds)
}
} catch (e) {
log.warn "ZigBee DTH - updated() threw exception:- $e"
}
return null
}
/**
* Ping is used by Device-Watch in attempt to reach the device
*/
def ping() {
log.trace "ZigBee DTH - Executing ping() for device ${device.displayName}"
refresh()
}
/**
* Called by the Smart Things platform in case Polling capability is added to the device type
*/
def poll() {
log.trace "ZigBee DTH - Executing poll() for device ${device.displayName}"
def cmds = []
def latest = device.currentState("lock")?.date?.time
if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
cmds << zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE)
state.lastPoll = now()
} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) {
cmds << zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING)
state.lastbatt = now()
}
if (cmds) {
log.info "ZigBee DTH - poll() returning with cmds:- $cmds"
return cmds
} else {
// workaround to keep polling from stopping due to lack of activity
sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false)
return null
}
}
/**
* Called when the user taps on the refresh button
*/
def refresh() {
log.trace "ZigBee DTH - Executing refresh() for device ${device.displayName}"
def cmds =
zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) +
zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING)
log.info "ZigBee DTH - refresh() returning with cmds:- $cmds"
return cmds
}
/**
* Configures the device to settings needed by SmarthThings at device discovery time
*
*/
def configure() {
log.trace "ZigBee DTH - Executing configure() for device ${device.displayName}"
def cmds = doConfigure()
log.info "ZigBee DTH - configure() returning with cmds:- $cmds"
cmds
}
/**
* Returns the list of commands to be executed when the device is being configured/paired
*
*/
def doConfigure() {
log.trace "ZigBee DTH - Executing doConfigure() for device ${device.displayName}"
state.configured = true
// Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time)
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])
def cmds =
zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE,
DataType.ENUM8, 0, 3600, null) +
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,
DataType.UINT8, 600, 21600, 0x01) +
zigbee.configureReporting(CLUSTER_ALARM, ALARM_ATTR_ALARM_COUNT,
DataType.UINT16, 0, 21600, null)
def allCmds = refresh() + cmds + reloadAllCodes()
log.info "ZigBee DTH - doConfigure() returning with cmds:- $allCmds"
allCmds // send refresh and reloadAllCodes cmds as part of configureDevice
}
/**
* Executes lock command on a Zigbee lock
*/
def lock() {
log.trace "ZigBee DTH - Executing lock() for device ${device.displayName}"
def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR)
log.info "ZigBee DTH - lock() returning with cmds:- $cmds"
return cmds
}
/**
* Executes unlock command on a Zigbee lock
*/
def unlock() {
log.trace "ZigBee DTH - Executing unlock() for device ${device.displayName}"
def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR)
log.info "ZigBee DTH - unlock() returning with cmds:- $cmds"
return cmds
}
/**
* API endpoint for server smart app to scan the lock and populate the attributes. Called only when the attributes are not populated.
*
* @return cmds: The command(s) fired for reading attributes
*/
def reloadAllCodes() {
log.trace "ZigBee DTH - Executing reloadAllCodes() for device ${device.displayName}"
sendEvent(name: "scanCodes", value: "Scanning", descriptionText: "Code scan in progress", displayed: false)
def lockCodes = loadLockCodes()
sendEvent(lockCodesEvent(lockCodes))
def cmds = validateAttributes()
if (isYaleLock()) {
state.checkCode = state.checkCode ?: 1
} else {
state.checkCode = state.checkCode ?: 0
}
cmds += requestCode(state.checkCode)
log.info "ZigBee DTH - reloadAllCodes() returning with cmds:- $cmds"
return cmds
}
/**
* API endpoint for setting a user code on a Zigbee lock
*
* @param codeID: The code slot number
*
* @param code: The code PIN
*
* @param codeName: The name of the code
*
* @returns cmds: The commands fired for creation and checking of a lock code
*/
def setCode(codeID, code, codeName = null) {
if (!code) {
log.trace "ZigBee DTH - Executing nameSlot() for device ${device.displayName}"
nameSlot(codeID, codeName)
return
}
log.trace "ZigBee DTH - Executing setCode() for device ${device.displayName}"
if (isValidCodeID(codeID) && isValidCode(code)) {
log.debug "Zigbee DTH - setting code in slot number $codeID"
def cmds = []
def attrCmds = validateAttributes()
def setPayload = getPayloadToSetCode(codeID, code)
if (isYaleLock()) {
// Executing both user code set and get commands as Yale lock do not generate programming event while creating
// a user code from the SmartApp
cmds << zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_USER_CODE_SET, setPayload).first()
cmds << requestCode(codeID).first()
state["setcode$codeID"] = encrypt(code.toString())
cmds = delayBetween(cmds, 4200)
} else {
cmds << zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_USER_CODE_SET, setPayload).first()
}
def strname = (codeName ?: "Code $codeID")
state["setname$codeID"] = strname
if(attrCmds) {
cmds = attrCmds + cmds
}
return cmds
} else {
log.warn "Zigbee DTH - Invalid input: Unable to set code in slot number $codeID"
return null
}
}
/**
* Validates attributes and if attributes are not populated, adds the command maps to list of commands
* @return List of command maps or empty list
*/
def validateAttributes() {
def cmds = []
if (!state.attrAlarmCountSet) {
state.attrAlarmCountSet = true
cmds += zigbee.configureReporting(CLUSTER_ALARM, ALARM_ATTR_ALARM_COUNT,
DataType.UINT16, 0, 21600, null)
}
// DOORLOCK_ATTR_SEND_PIN_OTA is sometimes getting reset to 0. Hence, writing it explicitly to 1.
cmds += zigbee.writeAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_SEND_PIN_OTA, DataType.BOOLEAN, 1)
if(!device.currentValue("maxCodes")) {
cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_NUM_PIN_USERS)
}
if(!device.currentValue("minCodeLength")) {
cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_MIN_PIN_LENGTH)
}
if(!device.currentValue("maxCodeLength")) {
cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_MAX_PIN_LENGTH)
}
cmds = cmds.flatten()
log.trace "validateAttributes returning commands list: " + cmds
cmds
}
/**
* API endpoint for deleting a user code on a Zigbee lock
*
* @param codeID: The code slot number
*
* @returns cmds: The command fired for deletion of a lock code
*/
def deleteCode(codeID) {
log.trace "ZigBee DTH - Executing deleteCode() for device ${device.displayName}"
def cmds = []
if (isValidCodeID(codeID)) {
log.debug "Zigbee DTH - deleting code slot number $codeID"
// Calling user code get when deleting a code because some Kwikset locks do not generate
// programming event when a code is deleted manually on the lock.
// This will also help in resolving the failure cases during deletion of a lock code.
cmds = zigbee.writeAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_SEND_PIN_OTA, DataType.BOOLEAN, 1)
cmds += zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_CLEAR_USER_CODE, getLittleEndianHexString(codeID))
cmds += requestCode(codeID)
} else {
log.warn "Zigbee DTH - Invalid input: Unable to delete slot number $codeID"
}
log.info "ZigBee DTH - deleteCode() returning with cmds:- $cmds"
return cmds
}
/**
* API endpoint for setting/deleting multiple user codes on a lock
*
* @param codeSettings: The map with code slot numbers and code pins (in case of update)
*
* @returns The commands fired for creation and deletion of lock codes
*/
def updateCodes(codeSettings) {
log.trace "ZigBee DTH - Executing updateCodes() for device ${device.displayName}"
if(codeSettings instanceof String) codeSettings = util.parseJson(codeSettings)
def set_cmds = []
def get_cmds = []
codeSettings.each { name, updated ->
if (name.startsWith("code")) {
def n = name[4..-1].toInteger()
if (updated && updated.size() >= 4 && updated.size() <= 8) {
log.debug "Setting code number $n"
def setPayload = getPayloadToSetCode(n, updated)
set_cmds << zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_USER_CODE_SET, setPayload).first()
if (isYaleLock()) {
get_cmds << requestCode(n).first()
}
} else if (updated == null || updated == "" || updated == "0") {
log.debug "Deleting code number $n"
set_cmds << zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_CLEAR_USER_CODE, getLittleEndianHexString(n)).first()
get_cmds << requestCode(n).first()
}
} else log.warn("unexpected entry $name: $updated")
}
if (set_cmds && get_cmds) {
def allCmds = []
allCmds = delayBetween(set_cmds, 2200) + ["delay 2200"] + delayBetween(get_cmds, 4200)
return response(allCmds)
} else if (set_cmds) {
return response(delayBetween(set_cmds, 4200))
}
return null
}
/**
* Renames an existing lock code slot
*
* @param codeSlot: The code slot number
*
* @param codeName The new name of the code
*/
def nameSlot(codeSlot, codeName) {
def lockCodes = loadLockCodes()
codeSlot = codeSlot.toString()
if (lockCodes[codeSlot]) {
def deviceName = device.displayName
log.trace "ZigBee DTH - Executing nameSlot() for device $deviceName"
def oldCodeName = getCodeName(lockCodes, codeSlot)
def newCodeName = codeName ?: "Code $codeSlot"
lockCodes[codeSlot] = newCodeName
sendEvent(lockCodesEvent(lockCodes))
sendEvent(name: "codeChanged", value: "$codeSlot renamed", data: [ lockName: deviceName, notify: false, notificationText: "Renamed \"$oldCodeName\" to \"$newCodeName\" in $deviceName at ${location.name}" ],
descriptionText: "Renamed \"$oldCodeName\" to \"$newCodeName\"", displayed: true, isStateChange: true)
}
}
/**
* Constructs the ZigBee command for user code get
*
* @param codeID: The code slot number
*
* @return The command for user code get
*/
def requestCode(codeID) {
return zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_USER_CODE_GET, getLittleEndianHexString(codeID))
}
/**
* Responsible for parsing incoming device messages to generate events
*
* @param description The incoming description from the device
*
* @return result: The list of events to be sent out
*
*/
def parse(String description) {
log.trace "ZigBee DTH - Executing parse() for device ${device.displayName}"
def result = null
if (description) {
if (description.startsWith('read attr -')) {
result = parseAttributeResponse(description)
} else {
result = parseCommandResponse(description)
}
}
return result
}
/**
* Responsible for handling attribute responses
*
* @param description The description to be parsed
*
* @return result: The list of events to be sent out
*/
private def parseAttributeResponse(String description) {
Map descMap = zigbee.parseDescriptionAsMap(description)
log.trace "ZigBee DTH - Executing parseAttributeResponse() for device ${device.displayName} with description map:- $descMap"
def result = []
Map responseMap = [:]
def clusterInt = descMap.clusterInt
def attrInt = descMap.attrInt
def deviceName = device.displayName
if (clusterInt == CLUSTER_POWER && attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
responseMap.name = "battery"
responseMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2)
// Handling Yale locks incorrect battery reporting issue
if (reportsBatteryIncorrectly()) {
responseMap.value = Integer.parseInt(descMap.value, 16)
}
responseMap.descriptionText = "Battery is at ${responseMap.value}%"
} else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_LOCKSTATE) {
def value = Integer.parseInt(descMap.value, 16)
responseMap.name = "lock"
if (value == 0) {
responseMap.value = "unknown"
responseMap.descriptionText = "Unknown state"
} else if (value == 1) {
responseMap.value = "locked"
responseMap.descriptionText = "Locked"
} else if (value == 2) {
responseMap.value = "unlocked"
responseMap.descriptionText = "Unlocked"
} else {
responseMap.value = "unknown"
responseMap.descriptionText = "Unknown state"
}
} else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_MIN_PIN_LENGTH && descMap.value) {
def minCodeLength = Integer.parseInt(descMap.value, 16)
responseMap = [name: "minCodeLength", value: minCodeLength, descriptionText: "Minimum PIN length is ${minCodeLength}", displayed: false]
} else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_MAX_PIN_LENGTH && descMap.value) {
def maxCodeLength = Integer.parseInt(descMap.value, 16)
responseMap = [name: "maxCodeLength", value: maxCodeLength, descriptionText: "Maximum PIN length is ${maxCodeLength}", displayed: false]
} else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_NUM_PIN_USERS && descMap.value) {
def maxCodes = Integer.parseInt(descMap.value, 16)
responseMap = [name: "maxCodes", value: maxCodes, descriptionText: "Maximum Number of user codes supported is ${maxCodes}", displayed: false]
} else {
log.trace "ZigBee DTH - parseAttributeResponse() - ignoring attribute response"
return null
}
if (responseMap.data) {
responseMap.data.lockName = deviceName
} else {
responseMap.data = [ lockName: deviceName ]
}
result << createEvent(responseMap)
log.info "ZigBee DTH - parseAttributeResponse() returning with result:- $result"
return result
}
/**
* Responsible for handling command responses
*
* @param description The description to be parsed
*
* @return result: The list of events to be sent out
*/
private def parseCommandResponse(String description) {
Map descMap = zigbee.parseDescriptionAsMap(description)
def deviceName = device.displayName
log.trace "ZigBee DTH - Executing parseCommandResponse() for device ${deviceName}"
def result = []
Map responseMap = [:]
def data = descMap.data
def lockCodes = loadLockCodes()
def cmd = descMap.commandInt
def clusterInt = descMap.clusterInt
if (clusterInt == CLUSTER_DOORLOCK && (cmd == DOORLOCK_CMD_LOCK_DOOR || cmd == DOORLOCK_CMD_UNLOCK_DOOR)) {
log.trace "ZigBee DTH - Executing DOOR LOCK/UNLOCK SUCCESS for device ${deviceName} with description map:- $descMap"
// Reading lock state with a delay of 4200 as some locks do not report their state change
def cmdList = []
cmdList << "delay 4200"
cmdList << zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE).first()
result << response(cmdList)
} else if (clusterInt == CLUSTER_DOORLOCK && cmd == DOORLOCK_RESPONSE_OPERATION_EVENT) {
log.trace "ZigBee DTH - Executing DOORLOCK_RESPONSE_OPERATION_EVENT for device ${deviceName} with description map:- $descMap"
def eventSource = Integer.parseInt(data[0], 16)
def eventCode = Integer.parseInt(data[1], 16)
responseMap.name = "lock"
responseMap.displayed = true
responseMap.isStateChange = true
def desc = ""
def codeName = ""
if (eventSource == 0) {
def codeID = Integer.parseInt(data[3] + data[2], 16)
if (!isValidCodeID(codeID, true)) {
// invalid code slot number reported by lock
log.debug "Invalid slot number := $codeID"
return null
}
codeName = getCodeName(lockCodes, codeID)
responseMap.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
} else if (eventSource == 1) {
responseMap.data = [ method: "command" ]
} else if (eventSource == 2) {
desc = "manually"
responseMap.data = [ method: "manual" ]
}
switch (eventCode) {
case 1:
responseMap.value = "locked"
if(codeName) {
responseMap.descriptionText = "Locked by \"$codeName\""
} else {
responseMap.descriptionText = "Locked ${desc}"
}
break
case 2:
responseMap.value = "unlocked"
if(codeName) {
responseMap.descriptionText = "Unlocked by \"$codeName\""
} else {
responseMap.descriptionText = "Unlocked ${desc}"
}
break
case 3: //Lock Failure Invalid Pin
break
case 4: //Lock Failure Invalid Schedule
break
case 5: //Unlock Invalid PIN
break
case 6: //Unlock Invalid Schedule
break
case 7: // locked by touching the keypad
responseMap.value = "keypadLocked"
responseMap.descriptionText = "locked by keypad"
break
case 8: // locked using the key
case 13: // locked using the Thumbturn
responseMap.value = "locked"
responseMap.descriptionText = "Locked ${desc}"
break
case 9: // unlocked using the key
case 14: // unlocked using the Thumbturn
responseMap.value = "unlocked"
responseMap.descriptionText = "Unlocked ${desc}"
break
case 10: //Auto lock
responseMap.value = "locked"
responseMap.descriptionText = "Auto locked"
responseMap.data = [ method: "auto" ]
break
default:
break
}
} else if (clusterInt == CLUSTER_DOORLOCK && cmd == DOORLOCK_CMD_USER_CODE_SET) {
log.trace "ZigBee DTH - Executing DOORLOCK_CMD_USER_CODE_SET for device ${deviceName} with description map:- $descMap"
def status = Integer.parseInt(data[0], 16)
switch (status) {
case 0:
log.debug "Lock code creation successful"
// Lock code creation is successful but we do not have the codeID/code number here.
// Hence, code creation success event will be sent from DOORLOCK_RESPONSE_PROGRAMMING_EVENT response.
break
case 1:
log.debug "Lock code creation failed - General failure"
break
case 2:
log.debug "Lock code creation failed - Memory full"
break
case 3:
log.debug "Lock code creation failed - Duplicate Code error"
break
default:
break
}
} else if (clusterInt == CLUSTER_DOORLOCK && cmd == DOORLOCK_RESPONSE_PROGRAMMING_EVENT) {
log.trace "ZigBee DTH - Executing DOORLOCK_RESPONSE_PROGRAMMING_EVENT for device ${deviceName} with description map:- $descMap"
// Programming event is generated when the user creates/updates/deletes a code manually on the lock.
// Ideally it should be generated even when the user tries to create/update a code through the
// SmartApp as well, but that is not the case with Yale locks.
responseMap.name = "codeChanged"
responseMap.isStateChange = true
responseMap.displayed = true
def codeID = Integer.parseInt(data[3] + data[2], 16)
def codeName
def eventCode = Integer.parseInt(data[1], 16)
switch (eventCode) {
case 1: // MasterCodeChanged
codeName = "Master Code"
responseMap.value = "0 set"
responseMap.descriptionText = "${getStatusForDescription('set')} \"Master Code\""
responseMap.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}" ]
break
case 3: // PINCodeDeleted
if (codeID == 255) {
result = allCodesDeletedEvent()
responseMap.value = "all deleted"
responseMap.descriptionText = "Deleted all user codes"
responseMap.data = [notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"]
result << createEvent(name: "lockCodes", value: util.toJson([:]), displayed: false, descriptionText: "'lockCodes' attribute updated")
} else {
if (lockCodes[codeID.toString()]) {
codeName = getCodeName(lockCodes, codeID)
responseMap.value = "$codeID deleted"
responseMap.descriptionText = "Deleted \"$codeName\""
responseMap.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ]
result << codeDeletedEvent(lockCodes, codeID)
}
}
break
case 2: // PINCodeAdded
case 4: // PINCodeChanged
if (isValidCodeID(codeID)) {
codeName = getCodeNameFromState(lockCodes, codeID)
def changeType = getChangeType(lockCodes, codeID)
responseMap.value = "$codeID $changeType"
responseMap.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\""
responseMap.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ]
result << codeSetEvent(lockCodes, codeID, codeName)
} else {
// invalid code slot number reported by lock
log.debug "Invalid slot number := $codeID"
}
break
default:
break
}
} else if (clusterInt == CLUSTER_DOORLOCK && cmd == DOORLOCK_CMD_USER_CODE_GET) {
log.trace "ZigBee DTH - Executing DOORLOCK_CMD_USER_CODE_GET for device ${deviceName}"
// This is called only when the user creates/updates a code using the SmartApp (in case of Yale locks)
// or when the user tries to scan the lock by calling reloadAllCodes()
def userStatus = Integer.parseInt(data[2], 16)
def codeID = Integer.parseInt(data[1] + data[0], 16)
def codeName = getCodeNameFromState(lockCodes, codeID)
// PIN code saved in the state - it will be non null only in case of Yale locks
def localCode = decrypt(state["setcode$codeID"])
responseMap.name = "codeChanged"
responseMap.isStateChange = true
responseMap.displayed = true
// userStatus = 1 indicates that the code slot is occupied
if (userStatus == 1) {
if (localCode && isYaleLock()) {
// This will be applicable for Yale locks - both create and update through the SmartApp
// PIN code fetched from the lock
def serverCode = getCodeFromOctet(data)
if (localCode == serverCode) {
// Code set successfully
log.debug "Code matches - lock code creation successful"
def changeType = getChangeType(lockCodes, codeID)
responseMap.value = "$codeID $changeType"
responseMap.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\""
responseMap.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ]
result << codeSetEvent(lockCodes, codeID, codeName)
} else {
// Code update failed
log.debug "Code update failed"
responseMap.value = "$codeID failed"
responseMap.descriptionText = "Failed to update code '$codeName'"
//It should be OK to mark this as duplicate pin code error because in case lock batteries are down,
// or lock is out of range, or there is wireless interference, the Lock will not be able to respond
// back with user code get response.
responseMap.data = [isCodeDuplicate: true]
}
} else {
// This will be applicable when a slot is found occupied during scanning of lock
// Populating the 'lockCodes' attribute after scanning a code slot
log.debug "Scanning lock - code $codeID is occupied"
def changeType = getChangeType(lockCodes, codeID)
responseMap.value = "$codeID $changeType"
responseMap.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\""
responseMap.data = [ codeName: codeName ]
if ("set" == changeType) {
result << codeSetEvent(lockCodes, codeID, codeName)
} else {
responseMap.displayed = false
}
}
} else {
// Code slot is empty - can happen when code creation fails or a slot is empty while scanning the lock
if (localCode != null && isYaleLock()) {
// Code slot found empty during creation of a user code
log.debug "Code creation failed"
responseMap.value = "$codeID failed"
responseMap.descriptionText = "Failed to set code '$codeName'"
//It should be OK to mark this as duplicate pin code error because in case lock batteries are down,
// or lock is out of range, or there is wireless interference, the Lock will not be able to respond
// back with user code get response.
responseMap.data = [isCodeDuplicate: true]
def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ]
codeReportMap.descriptionText = "Code $codeID is not set"
result << createEvent(codeReportMap)
} else if (lockCodes[codeID.toString()]) {
codeName = getCodeName(lockCodes, codeID)
responseMap.value = "$codeID deleted"
responseMap.descriptionText = "Deleted \"$codeName\""
responseMap.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ]
result << codeDeletedEvent(lockCodes, codeID)
} else {
// Code slot is empty - can happen when a slot is found empty while scanning the lock
responseMap.value = "$codeID unset"
responseMap.descriptionText = "Code slot $codeID found empty during scanning"
responseMap.displayed = false
}
}
clearStateForSlot(codeID)
if (codeID == state.checkCode) {
log.debug "Code scanning in progress..."
def defaultMaxCodes = isYaleLock() ? 8 : 7
def maxCodes = device.currentValue("maxCodes") ?: defaultMaxCodes
// Hard coding it to defaultMaxCodes as we do not want to scan all the codes.
maxCodes = defaultMaxCodes
if (state.checkCode >= maxCodes) {
log.debug "Code scanning complete"
state["checkCode"] = null
sendEvent(name: "scanCodes", value: "Complete", descriptionText: "Code scan completed", displayed: false)
} else {
log.debug "More codes to scan..."
state.checkCode = state.checkCode + 1
result << response(requestCode(state.checkCode))
}
}
} else if (clusterInt == CLUSTER_ALARM && cmd == ALARM_CMD_ALARM) {
log.trace "ZigBee DTH - Executing ALARM_CMD_ALARM for device ${deviceName} with description map:- $descMap"
def alarmCode = Integer.parseInt(data[0], 16)
switch (alarmCode) {
case 0: // Deadbolt Jammed
responseMap = [ name: "lock", value: "unknown", descriptionText: "Was in unknown state" ]
break
case 1: // Lock Reset to Factory Defaults
responseMap = [ name: "lock", value: "unknown", descriptionText: "Has been reset to factory defaults" ]
break
case 2: // Reserved
break
case 3: // RF Module Power Cycled
responseMap = [ descriptionText: "Batteries replaced", isStateChange: true ]
break
case 4: // Tamper Alarm - wrong code entry limit
responseMap = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true ]
break
case 5: // Tamper Alarm - front escutcheon removed from main
responseMap = [ name: "tamper", value: "detected", descriptionText: "Front escutcheon removed", isStateChange: true ]
break
case 6: // Forced Door Open under Door Locked Condition
responseMap = [ name: "tamper", value: "detected", descriptionText: "Door forced open under door locked condition", isStateChange: true ]
break
case 16: // Battery too low to operate
responseMap = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery too low to operate lock", isStateChange: true ]
break
case 17: // Battery level critical
responseMap = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery level critical", isStateChange: true ]
break
case 18: // Battery very low
responseMap = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery very low", isStateChange: true ]
break
case 19: // Battery low
responseMap = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery low", isStateChange: true ]
break
default:
break
}
} else {
log.trace "ZigBee DTH - parseCommandResponse() - ignoring command response"
}
if(responseMap["value"]) {
if (responseMap.data) {
responseMap.data.lockName = deviceName
} else {
responseMap.data = [ lockName: deviceName ]
}
result << createEvent(responseMap)
}
if (result) {
result = result.flatten()
} else {
result = null
}
log.debug "ZigBee DTH - parseCommandResponse() returning with result:- $result"
return result
}
/**
* Creates the event map for user code creation
*
* @param lockCodes: The user codes in a lock
*
* @param codeID: The code slot number
*
* @param codeName: The name of the user code
*
* @return The list of events to be sent out
*/
private def codeSetEvent(lockCodes, codeID, codeName) {
clearStateForSlot(codeID)
lockCodes[codeID.toString()] = (codeName ?: "Code $codeID")
def result = []
result << lockCodesEvent(lockCodes)
def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ]
codeReportMap.descriptionText = "${device.displayName} code $codeID is set"
result << createEvent(codeReportMap)
result
}
/**
* Creates the event map for user code deletion
*
* @param lockCodes: The user codes in a lock
*
* @param codeID: The code slot number
*
* @return The list of events to be sent out
*/
private def codeDeletedEvent(lockCodes, codeID) {
lockCodes.remove("$codeID".toString())
clearStateForSlot(codeID)
def result = []
result << lockCodesEvent(lockCodes)
def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ]
codeReportMap.descriptionText = "${device.displayName} code $codeID was deleted"
result << createEvent(codeReportMap)
result
}
/**
* Creates the event map for all user code deletion
*
* @return The List of events to be sent out
*/
private def allCodesDeletedEvent() {
def result = []
def lockCodes = loadLockCodes()
def deviceName = device.displayName
lockCodes.each { id, code ->
result << createEvent(name: "codeReport", value: id, data: [ code: "" ], descriptionText: "code $id was deleted",
displayed: false, isStateChange: true)
def codeName = code
result << createEvent(name: "codeChanged", value: "$id deleted",
data: [ codeName: codeName, lockName: deviceName, notify: true,
notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ],
descriptionText: "Deleted \"$codeName\"",
displayed: true, isStateChange: true)
clearStateForSlot(id)
}
result
}
/**
* Populates the 'lockCodes' attribute by calling send event
*
* @param lockCodes The codes in a lock
*/
private Map lockCodesEvent(lockCodes) {
createEvent(name: "lockCodes", value: util.toJson(lockCodes), displayed: false, descriptionText: "'lockCodes' attribute updated")
}
/**
* Reads the 'lockCodes' attribute and parses the same
*
* @returns Map: The lockCodes map
*/
private Map loadLockCodes() {
parseJson(device.currentValue("lockCodes") ?: "{}") ?: [:]
}
/**
* Converts the code octet to code PIN
*
* @param data The data map returned in case of user code get
*
* @return code: The code string
*/
private def getCodeFromOctet(data) {
def code = ""
def codeLength = Integer.parseInt(data[4], 16)
if (codeLength >= device.currentValue("minCodeLength") && codeLength <= device.currentValue("maxCodeLength")) {
for (def i = 5; i < (5 + codeLength); i++) {
code += (char) (zigbee.convertHexToInt(data[i]))
}
}
return code
}
/**
* Checks if the slot number is within the allowed limits
*
* @param codeID The code slot number
*
* @param allowMasterCode Flag to indicate if master code slot should be allowed as a valid slot
*
* @return true if valid, false if not
*/
private boolean isValidCodeID(codeID, allowMasterCode = false) {
def defaultMaxCodes = isYaleLock() ? 250 : 30
def minCodeId = isYaleLock() ? 1 : 0
if (allowMasterCode) {
minCodeId = 0
}
def maxCodes = device.currentValue("maxCodes") ?: defaultMaxCodes
if (codeID.toInteger() >= minCodeId && codeID.toInteger() <= maxCodes) {
return true
}
return false
}
/**
* Checks if the code PIN is valid
*
* @param code The code PIN
*
* @return true if valid, false if not
*/
private boolean isValidCode(code) {
def minCodeLength = device.currentValue("minCodeLength") ?: 4
def maxCodeLength = device.currentValue("maxCodeLength") ?: 8
if (code.toString().size() <= maxCodeLength && code.toString().size() >= minCodeLength && code.isNumber()) {
return true
}
return false
}
/**
* Checks if a change type is set or update