/
totalconnect-2-0.groovy
1599 lines (1394 loc) · 72.5 KB
/
totalconnect-2-0.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
/**
* TotalConnect
*
* Version: v0.1
* Changes [June 6th, 2017]
* - Started from code by mhatrey @ https://github.com/mhatrey/TotalConnect/blob/master/TotalConnect.groovy
* - Modified locationlist to work (as List)
* - Added Autodiscover for Automation and Security Panel DeviceIDs
* - Added Iteration of devices to add (did all automation devices even though I only want a non-zwave garage door)
* - Removed Success Page (unneccesary)
*
* Version: v1.0
* Changes [Jun 19th, 2017]
* - Too many to list. Morphed into full service manager.
* - Code credits for TotalConnect pieces go to mhatrey, bdwilson, QCCowboy. Without these guys, this would have never started
* - Reference credit to StrykerSKS (for Ecobee SM) and infofiend (for FLEXi Lighting) where ideas and code segments came from for the SM piece
* - Moved from open ended polling times (in seconds) to a list. This allows us to use better scheduling with predictable options 1 minute and over.
* Changes [Jun 26, 2017] - v1.1
* - Updated deviceID detection code to use deviceClassId instead of Name since that works on more panels (Vista 20P tested)
* - Updated polling methods to fix Minute vs Minutes typo
*
* Version 2.0
* Changes [July 7, 2017]
* - Moved polling methods to async methods (increased timeout, etc)
* Changes [July 10, 2017] - v2.0.1
* - Changed PartitionID to 1 from 0 for enchanced capability
* - Added initialization of statusRefresh time variables to avoid errors (set to Long of 0)
* Changes [July 11, 2017] - v2.1
* - Went to generateEvents method in automationUpdater to deal with different types of devices in handlers using unified status
* Changes [July 13, 2017] - v2.2
* - Changes control methods to Async methods
* - Currently having issues with Synchronous HTTP methods (investigating)
* - Cleaned up code from v2.0 & v2.2 changes
* Changes [July 13, 2017] - v2.2.1
* - Added in Arming and Disarming statuses with an automatic refresh 3 seconds later
*
* Future Changes Needed
* - Add a settings to change credentials in preferences (currently can't get back into credentials page after initial setup unless credentials are failing login)
* - Implement Dimmers, Thermostats, & Locks
* - Any logic to run like harmony with hubs (automationDevice vs securityDevice) and subdevices? seems unnecessarily complicated for this, but could provide a device that would give a dashboard view
* - Armed Away from Armed Stay or vice versa does not work. Must disarm first (does not currently handle)
*
* Copyright 2017 Jeremy Stroebel
*
* 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.
*
*/
include 'asynchttp_v1'
definition(
name: "TotalConnect 2.0",
namespace: "Security",
author: "Jeremy Stroebel",
description: "Total Connect 2.0 Service Manager",
category: "My Apps", //Safety & Security"
iconUrl: "https://s3.amazonaws.com/yogi/TotalConnect/150.png",
iconX2Url: "https://s3.amazonaws.com/yogi/TotalConnect/300.png",
singleInstance: true)
preferences {
page(name: "credentials", content: "authPage")
//dynamic page that has duplicate code with deviceSetup (should be a way to remove, maybe with content: and methods?)
page(name: "deviceSetup", content: "deviceSetup")
//only runs on first install
page(name: "sensorSetup", content: "sensorSetup")
//only runs when sensors are selected, should change to only ask if sensor list has changed?
}
/////////////////////////////////////
// PREFERENCE PAGES
/////////////////////////////////////
def authPage() {
if (!isTokenValid() && settings.userName != null && settings.password != null) {
log.debug "Login Token Does not Exist or is likely invalid - Will attempt Login"
state.token = login()
}//Check if there is no login token, but there are login details (this will check to make sure we're logged in before any refresh as well)
if (!isTokenValid())
{
log.debug "Either Login failed or there are no credentials to attempt login yet"
state.firstRun == true //this essentially reset credentials if login fails, but backend values will stay set (unknown how to resolve that, can't clear settings)
}//only show credentials if login failed or there are no credentials yet (need a reset credentials option)
if(state.firstRun == null || state.firstRun != false) {
//Login Credentials
dynamicPage(name:"credentials", title:"TotalConnect 2.0 Login", nextPage: "deviceSetup", uninstall: true, install:false) {
section ("TotalConnect 2.0 Login Credentials") {
paragraph "Give your Total Connect credentials. Recommended to make another user for SmartThings"
input("userName", "text", title: "Username", description: "Your username for TotalConnect")
input("password", "password", title: "Password", description: "Your Password for TotalConnect", submitOnChange:true)
}//section
//Location Selection
def hideLocation = false //default to showing location
def locations
def defaultLocation
def deviceMap
def options = []
if(settings.userName != null && settings.password != null) {
if(settings.selectedLocation == null) {
}//if no location is set, expand this section
locations = locationFound()
deviceMap = getDeviceIDs(locations.get(selectedLocation))
options = locations.keySet() as List ?: []
log.debug "Options: " + options
} else {
hideLocation = true
}//hide location when there is no username or password
section("Select from the following Locations for Total Connect.", hideable: true, hidden: hideLocation) {
input "selectedLocation", "enum", required:true, title:"Select the Location", multiple:false, submitOnChange:true, options:options
}//section
//Backend Values (at bottom)
section("Backend TotalConnect 2.0 Values - DO NOT CHANGE", hideable: true, hidden: true) {
paragraph "These are required for login:"
input "applicationId", "text", title: "Application ID - It is '14588' currently", description: "Application ID", defaultValue: "14588"
input "applicationVersion", "text", title: "Application Version - use '3.0.32'", description: "Application Version", defaultValue: "3.0.32"
paragraph "These are required for device control:"
input "locationId", "text", title: "Location ID - Do not change", description: "Location ID", defaultValue: locations?.get(selectedLocation) ?: ""
input "securityDeviceId", "text", title: "Security Device ID - Do not change", description: "Device ID", defaultValue: deviceMap?.get("1") //deviceMap?.get("Security Panel")
input "automationDeviceId", "text", required:false, title: "Automation Device ID - Do not change", description: "Device ID", defaultValue: deviceMap?.get("3") //deviceMap?.get("Automation")
}//section
}//dynamicPage, Only show this page if missing authentication
} else {
deviceSetup()
}//if this isn't the first run, go straight to device setup
}
private deviceSetup() {
def nextPage = null //default to no, assuming no sensors
def install = true //default to true to allow install with no sensors
if(zoneDevices) {
nextPage = "sensorSetup"
install = false
}//if we have sensors, make us go to sensorSetup
return dynamicPage(name:"deviceSetup", title:"Pulling up the TotalConnect Device List!",nextPage: nextPage, install: install, uninstall: true) {
if(zoneDevices) {
nextPage = "sensorSetup"
install = false
} else {
nextPage = null
install = true
} //only set nextPage if sensors are selected (and disable install)
def zoneMap
def automationMap
def thermostatMap
def lockMap
discoverSensors() //have to find zone sensors first
zoneMap = sensorsDiscovered()
if(settings.automationDeviceId) {
log.debug "Automation Discovery Happened"
discoverSwitches() //have to find switches first
automationMap = switchesDiscovered()
discoverThermostats() //have to find thermostats first
thermostatMap = thermostatsDiscovered()
discoverLocks() //have to find locks first
lockMap = locksDiscovered()
}//only discover Automation Devices if there is an automation device with the given location
def hideAlarmOptions = true
if(alarmDevice) {
hideAlarmOptions = false
}//If alarm is selected, expand options
def hidePollingOptions = true
if(pollOn) {
hidePollingOptions = false
}//If alarm is selected, expand options
Map pollingOptions = [5: "5 seconds", 10: "10 seconds", 15: "15 seconds", 20: "20 seconds", 30: "30 seconds", 60: "1 minute", 300: "5 minutes", 600: "10 minutes"]
section("Select from the following Security devices to add in SmartThings.") {
input "alarmDevice", "bool", required:true, title:"Honeywell Alarm", defaultValue:false, submitOnChange:true
input "zoneDevices", "enum", required:false, title:"Select any Zone Sensors", multiple:true, options:zoneMap, submitOnChange:true
}//section
section("Alarm Integration Options:", hideable: true, hidden: hideAlarmOptions) {
input "shmIntegration", "bool", required: true, title:"Sync alarm status and SHM status", default:false
}//section
section("Select from the following Automation devices to add in SmartThings. (Suggest adding devices directly to SmartThings if compatible)") {
input "automationDevices", "enum", required:false, title:"Select any Automation Devices", multiple:true, options:automationMap, hideWhenEmpty:true, submitOnChange:true
input "thermostatDevices", "enum", required:false, title:"Select any Thermostat Devices", multiple:true, options:thermostatMap, hideWhenEmpty:true, submitOnChange:true
input "lockDevices", "enum", required:false, title:"Select any Lock Devices", multiple:true, options:lockMap, hideWhenEmpty:true, submitOnChange:true
}//section
section("Enable Polling?") {
input "pollOn", "bool", title: "Polling On?", description: "Pause or Resume Polling", submitOnChange:true
}
section("Polling Options (advise not to set any under 10 secs):", hideable: true, hidden: hidePollingOptions) {
//input "panelPollingInterval", "number", required:pollOn, title: "Alarm Panel Polling Interval (in secs)", description: "How often the SmartApp will poll TC2.0"
input "panelPollingInterval", "enum", required:(pollOn && settings.alarmDevice), title: "Alarm Panel Polling Interval", description: "How often the SmartApp will poll TC2.0", options:pollingOptions, default:60
//input "zonePollingInterval", "number", required:pollOn, title: "Zone Sensor Polling Interval (in secs)", description: "How often the SmartApp will poll TC2.0"
input "zonePollingInterval", "enum", required:(pollOn && settings.zoneDevices), title: "Zone Sensor Polling Interval", description: "How often the SmartApp will poll TC2.0", options:pollingOptions, default:60
//input "automationPollingInterval", "number", required:pollOn, title: "Automation Polling Interval (in secs)", description: "How often the SmartApp will poll TC2.0"
input "automationPollingInterval", "enum", required:(pollOn && (settings.automationDevices || settings.thermostatDevices || settings.lockDevices)), title: "Automation Polling Interval", description: "How often the SmartApp will poll TC2.0", options:pollingOptions, default:60
}//section
}//dynamicpage
}//deviceSetup
private sensorSetup() {
dynamicPage(name:"sensorSetup", title:"Configure Sensor Types", install: true, uninstall: true) {
def options = ["contactSensor", "motionSensor"] //sensor options
section("Select a sensor type for each sensor") {
settings.zoneDevices.each { dni ->
input "${dni}_zoneType", "enum", required:true, title:"${state.sensors.find { ("TC-${settings.securityDeviceId}-${it.value.id}") == dni }?.value.name}", multiple:false, options:options
}//iterate through selected sensors to get sensor type
}//section
}//dynamicPage
}//sensorSetup()
/////////////////////////////////////
// Setup/Device Discovery Functions
/////////////////////////////////////
Map locationFound() {
log.debug "Executed location function during Setup"
def locationId
def locationName
def locationMap = [:]
def response = tcCommand("GetSessionDetails", [SessionID: state.token, ApplicationID: applicationId, ApplicationVersion: applicationVersion])
response.data.Locations.LocationInfoBasic.each { LocationInfoBasic ->
locationName = LocationInfoBasic.LocationName
locationId = LocationInfoBasic.LocationID
locationMap["${locationName}"] = "${locationId}"
}//LocationInfoBasic.each
log.debug "This is map during Settings " + locationMap
return locationMap
}//locationFound()
Map getDeviceIDs(targetLocationId) {
log.debug "Executed DeviceID function during Setup"
if(targetLocationId == null) {
log.debug "LocationID not yet defined"
return [:]
}
log.debug "TargetLocationID: ${targetLocationId}"
def locationId
String deviceName
String deviceClassId
String deviceId
Map deviceMap = [:]
def response = tcCommand("GetSessionDetails", [SessionID: state.token, ApplicationID: applicationId, ApplicationVersion: applicationVersion])
response.data.Locations.LocationInfoBasic.each { LocationInfoBasic ->
locationId = LocationInfoBasic.LocationID
if(locationId == targetLocationId) {
//deviceId = LocationInfoBasic.SecurityDeviceID
LocationInfoBasic.DeviceList.DeviceInfoBasic.each { DeviceInfoBasic ->
//deviceName = DeviceInfoBasic.DeviceName
deviceClassId = DeviceInfoBasic.DeviceClassID
deviceId = DeviceInfoBasic.DeviceID
//deviceMap.put(deviceName, deviceId)
deviceMap.put(deviceClassId, deviceId)
}//iterate throught DeviceIDs
}//Only get DeviceIDs for the desired location
}//LocationInfoBasic.each
log.debug "DeviceID map is " + deviceMap
return deviceMap
} // Should return Map of Devices associated to the given location
def discoverSensors() {
def sensors = [:]
def response = tcCommand("GetPanelMetaDataAndFullStatusEx", [SessionID: state.token, LocationID: settings.locationId, LastSequenceNumber: 0, LastUpdatedTimestampTicks: 0, PartitionID: 1])
response.data.PanelMetadataAndStatus.Zones.ZoneInfo.each { ZoneInfo ->
//zoneID = ZoneInfo.'@ZoneID'
//zoneName = ZoneInfo.'@ZoneDescription'
//zoneType //needs to come from input
sensors[ZoneInfo.'@ZoneID'] = [id: "${ZoneInfo.'@ZoneID'}", name: "${ZoneInfo.'@ZoneDescription'}"]
}//iterate through zones
log.debug "TotalConnect2.0 SM: ${sensors.size()} sensors found"
//log.debug sensors
state.sensors = sensors
} //Should discover sensor information and save to state
Map sensorsDiscovered() {
def sensors = state.sensors //needs some error checking likely
def map = [:]
sensors.each {
def value = "${it?.value?.name}"
def key = "TC-${settings.securityDeviceId}-${it?.value?.id}" //Sets DeviceID to "TC-${SecurityID}-${ZoneID}. Follows format of Harmony activites
map[key] = value
}//iterate through discovered sensors to find value
//log.debug "Sensors Options: " + map
return map
}//returns list of sensors for preferences page
// Discovers Switch Devices (Switches, Dimmmers, & Garage Doors)
def discoverSwitches() {
def switches = [:]
def response = tcCommand("GetAllAutomationDeviceStatusEx", [SessionID: state.token, DeviceID: automationDeviceId, AdditionalInput: ''])
response.data.AutomationData.AutomationSwitch.SwitchInfo.each { SwitchInfo ->
//switchID = SwitchInfo.SwitchID
//switchName = SwitchInfo.SwitchName
//switchType = SwitchInfo.SwitchType
//switchIcon = SwitchInfo.SwitchIconID // 0-Light, 1-Switch, 255-Garage Door, maybe use for default?
//switchState = SwitchInfo.SwitchState // 0-Off, 1-On, maybe set initial value?
//switchLevel = SwitchInfo.SwitchLevel // 0-99, maybe to set intial value?
switches[SwitchInfo.SwitchID] = [id: "${SwitchInfo.SwitchID}", name: "${SwitchInfo.SwitchName}", type: "${SwitchInfo.SwitchType}"] //use "${var}" to typecast into String
}//iterate through Switches
log.debug "TotalConnect2.0 SM: ${switches.size()} switches found"
//log.debug switches
state.switches = switches
} //Should discover switch information and save to state (could combine all automation to turn 3 calls into 1 or pass XML section for each type to discovery...)
Map switchesDiscovered() {
def switches = state.switches //needs some error checking likely
def map = [:]
switches.each {
def value = "${it?.value?.name}"
def key = "TC-${settings.automationDeviceId}-${it?.value?.id}" //Sets DeviceID to "TC-${AutomationID}-${SwitchID}. Follows format of Harmony activites
map[key] = value
}//iterate through discovered switches to find value
//log.debug "Switches Options: " + map
return map
}//returns list of switches for preferences page
// Discovers Thermostat Devices
def discoverThermostats() {
def thermostats = [:]
def response = tcCommand("GetAllAutomationDeviceStatusEx", [SessionID: state.token, DeviceID: automationDeviceId, AdditionalInput: ''])
response.data.AutomationData.AutomationThermostat.ThermostatInfo.each { ThermostatInfo ->
//thermostatID = ThermostatInfo.ThermostatID
//thermostatName = ThermostatInfo.ThermostatName
thermostats[ThermostatInfo.ThermostatID] = [id: "${ThermostatInfo.ThermostatID}", name: "${ThermostatInfo.ThermostatName}"] //use "${var}" to typecast into String
}//ThermostatInfo.each
log.debug "TotalConnect2.0 SM: ${thermostats.size()} thermostats found"
//log.debug thermostatMap
state.thermostats = thermostats
} //Should return thermostat information
Map thermostatsDiscovered() {
def thermostats = state.thermostats
def map = [:]
thermostats.each {
def value = "${it?.value?.name}"
def key = "TC-${settings.automationDeviceId}-${it?.value?.id}" //Sets DeviceID to "TC-${AutomationID}-${ThermostatID}. Follows format of Harmony activites
map[key] = value
}//iterate through discovered thermostats to find value
//log.debug "Thermostat Options: " + map
return map
}//thermostatsDiscovered()
// Discovers Lock Devices
def discoverLocks() {
def locks = [:]
def response = tcCommand("GetAllAutomationDeviceStatusEx", [SessionID: state.token, DeviceID: automationDeviceId, AdditionalInput: ''])
response.data.AutomationData.AutomationLock.LockInfo_Transitional.each { LockInfo_Transitional ->
//lockID = LockInfo_Transitional.LockID
//lockName = LockInfo_Transitional.LockName
locks[LockInfo_Transitional.LockID] = [id: "${LockInfo_Transitional.LockID}", name: "${LockInfo_Transitional.LockName}"] //use "${var}" to typecast into String
}//iterate through Locks
log.debug "TotalConnect2.0 SM: ${locks.size()} locks found"
//log.debug locks
state.locks = locks
} //Should discover locks information and save to state (could combine all automation to turn 3 calls into 1 or pass XML section for each type to discovery...)
Map locksDiscovered() {
def locks = state.locks //needs some error checking likely
def map = [:]
locks.each {
def value = "${it?.value?.name}"
def key = "TC-${settings.automationDeviceId}-${it?.value?.id}" //Sets DeviceID to "TC-${AutomationID}-${LockID}. Follows format of Harmony activites
map[key] = value
}//iterate through discovered locks to find value
//log.debug "Locks Options: " + map
return map
}//returns list of locks for preferences page
/////////////////////////////////////
// TC2.0 Authentication Methods
/////////////////////////////////////
// Login Function. Returns SessionID for rest of the functions (doesn't seem to test if login is incorrect...)
def login() {
log.debug "Executed login"
String token
def paramsLogin = [
uri: "https://rs.alarmnet.com/TC21API/TC2.asmx/AuthenticateUserLogin",
body: [userName: settings.userName , password: settings.password, ApplicationID: settings.applicationId, ApplicationVersion: settings.applicationVersion]
]
httpPost(paramsLogin) { response ->
token = response.data.SessionID
}
state.tokenRefresh = now()
String refreshDate = new Date(state.tokenRefresh).format("EEE MMM d HH:mm:ss Z", location.timeZone)
log.debug "Smart Things has logged in at ${refreshDate} SessionID: ${token}"
return token
} // Returns token
// Keep Alive Command to keep session alive to reduce login/logout calls. Keep alive does not confirm it worked so we will use GetSessionDetails instead.
// Currently there is no check to see if this is needed. Logic for if keepAlive is needed would be state.token != null && now()-state.tokenRefresh < 240000.
// This works on tested assumption token is valid for 4 minutes (240000 milliseconds)
def keepAlive() {
log.debug "KeepAlive. State.token: '" + state.token + "'"
String resultCode
String resultData
def response = tcCommand("GetSessionDetails", [SessionID: state.token, ApplicationID: settings.applicationId, ApplicationVersion: settings.applicationVersion])
//this check code is redundant
resultCode = response.data.ResultCode
if(resultCode == "0") {
String refreshDate = new Date(state.tokenRefresh).format("EEE MMM d HH:mm:ss Z", location.timeZone)
log.debug "Session kept alive at ${refreshDate}"
//tokenRefresh already updated
} else {
log.debug "Session keep alive failed at ${refreshDate}"
}//doublecheck this worked
}//keepAlive
def isTokenValid() {
//return false if token doesn't exist
if(state.token == null) {
return false }
Long timeSinceRefresh = now() - state.tokenRefresh
//return false if time since refresh is over 4 minutes (likely timeout)
if(timeSinceRefresh > 240000) {
return false }
return true
} // This is a logical check only, assuming known timeout values and clearing token on loggout. This method does no testing of the actual token against TC2.0.
// Logout Function. Called after every mutational command. Ensures the current user is always logged Out.
def logout() {
log.debug "During logout - ${state.token}"
def paramsLogout = [
uri: "https://rs.alarmnet.com/TC21API/TC2.asmx/Logout",
body: [SessionID: state.token]
]
httpPost(paramsLogout) { responseLogout ->
log.debug "Smart Things has successfully logged out"
}
state.token = null
state.tokenRefresh = null
}
/////////////////////////////////////
// SmartThings defaults
/////////////////////////////////////
def initialize() {
state.token = login()
log.debug "Initialize. Login produced token: " + state.token
// Combine all selected devices into 1 variable to make sure we have devices and to deleted unused ones
state.selectedDevices = (settings.zoneDevices?:[]) + (settings.automationDevices?:[]) + (settings.thermostatDevices?:[]) + (settings.lockDevices?:[])
if(settings.alarmDevice) {
//state.selectedDevices.add("TC-${settings.securityDeviceId}") //this doesn't work, but should... its a typecasting thing
def d = getChildDevice("TC-${settings.securityDeviceId}")
log.debug "deviceNetworkId: ${d?.deviceNetworkId}"
state.selectedDevices.add(d?.deviceNetworkId)
}//if alarm device is selected
//log.debug "Selected Devices: ${state.selectedDevices}"
//delete devices that are not selected anymore (something is wrong here... it likes to delete the alarm device)
def delete = getChildDevices().findAll { !state.selectedDevices.contains(it.deviceNetworkId) }
log.debug "Devices to delete: ${delete}"
removeChildDevices(delete)
if (state.selectedDevices || settings.alarmDevice) {
log.debug "Running addDevices()"
addDevices()
}//addDevices if we have any
//initialize last refresh variables to avoid possible Null condition
state.alarmStatusRefresh = 0L
state.zoneStatusRefresh = 0L
state.automationStatusRefresh = 0L
pollChildren()
if (settings.alarmDevice && settings.shmIntegration) {
log.debug "Setting up SHM + TC Alarm Integration"
//subscribe(location, "alarmSystemStatus", modeChangeHandler) //Check for changes to location mode (not the same as SHM)
subscribe(location, "alarmSystemStatus", alarmHandler) //Check for changes to SHM and set alarm
}//if alarm enabled & smh integration enabled
else {
log.debug "SHM + TC Alarm Integration not enabled. alarmDevice: ${settings.alarmDevice}, shmIntegration: ${shmIntegration}"
}//if SHM + TC Alarm integration is off
//Check for our schedulers and token every 2 minutes. Well inside the tested 4.5+ min expiration
schedule("0 0/2 * 1/1 * ? *", scheduleChecker)
spawnDaemon()
}//initialize()
def installed() {
log.debug "Installed with settings: ${settings}"
state.firstRun = false //only run authentication on 1st setup. After installed, it won't run again
initialize()
}//installed()
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
unschedule()
initialize()
}//updated()
def uninstalled() {
removeChildDevices(getChildDevices())
}//uninstalled()
/////////////////////////////////////
// Scheduling and Updating
/////////////////////////////////////
def scheduleChecker() {
if(settings.pollOn) {
if(settings.alarmDevice) {
if(((now()-state.alarmStatusRefresh)/1000) > (settings.panelPollingInterval.toInteger()*1.5)) {
panelAutoUpdater()
log.debug "Panel AutoUpdater Restarted"
}//if we've potentially missed 2 updates
}//if there is an alarm device
if(settings.zoneDevices) {
if(((now()-state.zoneStatusRefresh)/1000) > (settings.zonePollingInterval.toInteger()*1.5)) {
zoneAutoUpdater()
log.debug "Zone AutoUpdater Restarted"
}//if we've potentially missed 2 updates
}//if theres a zoneDevice
if(settings.automationDevices) {
if(((now()-state.automationStatusRefresh)/1000) > (settings.automationPollingInterval.toInteger()*1.5)) {
automationAutoUpdater()
log.debug "Automation AutoUpdater Restarted"
}//if we've potentially missed 2 updates
}//if there is an automationDevice
}//if polling is on
if(((now()-state.tokenRefresh)/1000) > 149) {
keepAlive()
}//if token will expire before we run again (every 2 minutes), by putting this at the end, we may have already refreshed the token
}//scheduleChecker()
def spawnDaemon() {
if(settings.pollOn) {
log.debug "Starting AutoUpdate schedules at ${new Date()}"
if(settings.alarmDevice) {
switch(settings.panelPollingInterval.toInteger()) {
case 60:
runEvery1Minute(panelAutoUpdater)
break
case 300:
runEvery5Minutes(panelAutoUpdater)
break
case 600:
runEvery10Minutes(panelAutoUpdater)
break
default:
runIn(settings.panelPollingInterval.toInteger(), panelAutoUpdater)
break
}//switch
}//if alarmDevice is selected
if(settings.zoneDevices) {
switch(settings.zonePollingInterval.toInteger()) {
case 60:
runEvery1Minute(zoneAutoUpdater)
break
case 300:
runEvery5Minutes(zoneAutoUpdater)
break
case 600:
runEvery10Minutes(zoneAutoUpdater)
break
default:
runIn(settings.zonePollingInterval.toInteger(), zoneAutoUpdater)
break
}//switch
}//if zoneDevices are selected
if(settings.automationDevices) {
switch(settings.automationPollingInterval.toInteger()) {
case 60:
runEvery1Minute(automationAutoUpdater)
break
case 300:
runEvery5Minutes(automationAutoUpdater)
break
case 600:
runEvery10Minutes(automationAutoUpdater)
break
default:
runIn(settings.automationPollingInterval.toInteger(), automationAutoUpdater)
break
}//switch
}//if automationDevices are selected
} else {
log.debug "Polling is turned off. AutoUpdate canceled"
}//if polling is on
}//spawnDaemon()
//AutoUpdates run if the time since last update (manual or scheduled) is 1/2 the setting (for example setting is 30 seconds, we'll poll after 15 have passed and schedule next one for 30 seconds)
def panelAutoUpdater() {
if(((now()-state.alarmStatusRefresh)/1000) > (settings.panelPollingInterval.toInteger()/2)) {
log.debug "AutoUpdate Panel Status at ${new Date()}"
//tcCommandAsync("GetPanelMetaDataAndFullStatusEx", "SessionID=${state.token}&LocationID=${settings.locationId}&LastSequenceNumber=0&LastUpdatedTimestampTicks=0&PartitionID=1") //This updates panel status
tcCommandAsync("GetPanelMetaDataAndFullStatusEx", [SessionID: state.token, LocationID: settings.locationId, LastSequenceNumber: 0, LastUpdatedTimestampTicks: 0, PartitionID: 1]) //This updates panel status
//state.alarmStatus = alarmPanelStatus()
//updateStatuses()
} else {
log.debug "Update has happened since last run, skipping this execution"
}//if its not time to update
if(settings.panelPollingInterval.toInteger() < 60) {
runIn(settings.panelPollingInterval.toInteger(), panelAutoUpdater)
}//if our polling interval is less than 60 seconds, we need to manually schedule next occurance
}//updates panel status
def zoneAutoUpdater() {
if(((now()-state.zoneStatusRefresh)/1000) > (settings.zonePollingInterval.toInteger()/2)) {
log.debug "AutoUpdate Zone Status at ${new Date()}"
tcCommandAsync("GetZonesListInStateEx", [SessionID: state.token, LocationID: settings.locationId, PartitionID: 1, ListIdentifierID: 0])
//state.zoneStatus = zoneStatus()
//updateStatuses()
} else {
log.debug "Update has happened since last run, skipping this execution"
}//if its not time to update
if(settings.zonePollingInterval.toInteger() < 60) {
runIn(settings.zonePollingInterval.toInteger(), zoneAutoUpdater)
}//if our polling interval is less than 60 seconds, we need to manually schedule next occurance
}//updates zone status(es)
def automationAutoUpdater() {
if(((now()-state.automationStatusRefresh)/1000) > (settings.automationPollingInterval.toInteger()/2)) {
log.debug "AutoUpdate Automation Status at ${new Date()}"
tcCommandAsync("GetAllAutomationDeviceStatusEx", [SessionID: state.token, DeviceID: settings.automationDeviceId, AdditionalInput: ''])
//state.switchStatus = automationDeviceStatus()
//updateStatuses()
} else {
log.debug "Update has happened since last run, skipping this execution"
}//if its not time to update
if(settings.automationPollingInterval.toInteger() < 60) {
runIn(settings.automationPollingInterval.toInteger(), automationAutoUpdater)
}//if our polling interval is less than 60 seconds, we need to manually schedule next occurance
}//updates automation status(es)
/////////////////////////////////////
// HANDLERS
/////////////////////////////////////
/*
// Logic for Triggers based on mode change of SmartThings
def modeChangeHandler(evt) {
log.debug "Mode Change handler triggered. Evt: ${evt.value}"
//create alarmPanel object to use as shortcut
state.alarmPanel = getChildDevice("TC-${settings.securityDeviceId}")
if (evt.value == "Away") {
log.debug "Mode is set to Away, Performing ArmAway"
alarmPanel.armAway()
//alarmPanel.lock()
}//if mode changes to Away
else if (evt.value == "Night") {
log.debug "Mode is set to Night, Performing ArmStay"
alarmPanel.armStay()
//alarmPanel.on()
}//if mode changes to Night
else if (evt.value == "Home") {
log.debug "Mode is set to Home, Performing Disarm"
alarmPanel.disarm()
//alarmPanel.off()
}//if mode changes to Home
}//modeChangeHandler(evt)
*/
// Logic for Triggers based on mode change of SmartThings
def alarmHandler(evt) {
//create alarmPanel object to use as shortcut
def alarmPanel = getChildDevice("TC-${settings.securityDeviceId}")
//log.debug "SHM Change handler triggered. Evt: ${evt.value}"
//log.debug "Alarm Panel status is: ${alarmPanel.currentStatus}"
if (evt.value == "away" && !(alarmPanel.currentStatus == "Armed Away" || alarmPanel.currentStatus == "Armed Away - Instant")) {
log.debug "SHM Mode is set to Away, Performing ArmAway"
alarmPanel.armAway()
//alarmPanel.lock()
}//if mode changes to Away and the Alarm isn't already in that state (since we fire events to SHM on updates)
else if (evt.value == "stay"&& !(alarmPanel.currentStatus == "Armed Stay" || alarmPanel.currentStatus == "Armed Stay - Instant")) {
log.debug "SHM Mode is set to Stay, Performing ArmStay"
alarmPanel.armStay()
//alarmPanel.on()
}//if mode changes to Stay and the Alarm isn't already in that state (since we fire events to SHM on updates)
else if (evt.value == "off" && alarmPanel.currentStatus != "Disarmed") {
log.debug "SHM Mode is set to Off, Performing Disarm"
alarmPanel.disarm()
//alarmPanel.off()
}//if mode changes to Off and the Alarm isn't already in that state (since we fire events to SHM on updates)
}//alarmHandler(evt)
/////////////////////////////////////
// CHILD DEVICE MANAGEMENT
/////////////////////////////////////
def addDevices() {
/* SmartThings Documentation adding devices, maybe add Try and Catch Block?
settings.devices.each {deviceId ->
try {
def existingDevice = getChildDevice(deviceId)
if(!existingDevice) {
def childDevice = addChildDevice("smartthings", "Device Name", deviceId, null, [name: "Device.${deviceId}", label: device.name, completedSetup: true])
}
} catch (e) {
log.error "Error creating device: ${e}"
}
}
*/
if(settings.alarmDevice) {
def deviceID = "TC-${settings.securityDeviceId}"
def d = getChildDevice(deviceID)
if(!d) {
d = addChildDevice("jhstroebel", "TotalConnect Alarm", deviceID, null /*Hub ID*/, [name: "Device.${deviceID}", label: "TotalConnect Alarm", completedSetup: true])
}//Create Alarm Device if doesn't exist
}//if Alarm is selected
if(settings.zoneDevices) {
//log.debug "zoneDevices: " + settings.zoneDevices
def sensors = state.sensors
settings.zoneDevices.each { dni ->
def d = getChildDevice(dni)
if(!d) {
def newSensor
newSensor = sensors.find { ("TC-${settings.securityDeviceId}-${it.value.id}") == dni }
log.debug "dni: ${dni}, newSensor: ${newSensor}"
if(settings["${dni}_zoneType"] == "motionSensor") {
d = addChildDevice("jhstroebel", "TotalConnect Motion Sensor", dni, null /*Hub ID*/, [name: "Device.${dni}", label: "${newSensor?.value.name}", completedSetup: true])
}
if(settings["${dni}_zoneType"] == "contactSensor") {
d = addChildDevice("jhstroebel", "TotalConnect Contact Sensor", dni, null /*Hub ID*/, [name: "Device.${dni}", label: "${newSensor?.value.name}", completedSetup: true])
}
}//if it doesn't already exist
}//for each selected sensor
}//if there are zoneDevices
if(settings.automationDevices) {
//log.debug "automationDevices: " + settings.automationDevices
def switches = state.switches
settings.automationDevices.each { dni ->
def d = getChildDevice(dni)
if(!d) {
def newSwitch
newSwitch = switches.find { ("TC-${settings.automationDeviceId}-${it.value.id}") == dni }
if("${newSwitch?.value.type}" == "1") {
d = addChildDevice("jhstroebel", "TotalConnect Switch", dni, null /*Hub ID*/, [name: "Device.${dni}", label: "${newSwitch?.value.name}", completedSetup: true])
}
if("${newSwitch?.value.type}" == "2") {
d = addChildDevice("jhstroebel", "TotalConnect Dimmer", dni, null /*Hub ID*/, [name: "Device.${dni}", label: "${newSwitch?.value.name}", completedSetup: true])
}
if("${newSwitch?.value.type}" == "3") {
d = addChildDevice("jhstroebel", "TotalConnect Garage Door", dni, null /*Hub ID*/, [name: "Device.${dni}", label: "${newSwitch?.value.name}", completedSetup: true])
}
}//if it doesn't already exist
}//for each selected sensor
}//if automation devices are selected
//No device handler exists yet... commented out addChildDeviceLine until that is resolved...
if(settings.thermostatDevices) {
//log.debug "thermostatDevices: " + settings.thermostatDevices
def thermostats = state.thermostats
settings.thermostatDevices.each { dni ->
def d = getChildDevice(dni)
if(!d) {
def newThermostat
newThermostat = thermostats.find { ("TC-${settings.automationDeviceId}-${it.value.id}") == dni }
// d = addChildDevice("jhstroebel", "TotalConnect Thermostat", dni, null /*Hub ID*/, [name: "Device.${dni}", label: "${newThermostat?.value.name}", completedSetup: true])
}//if it doesn't already exist
}//for each selected thermostat
}//if there are thermostatDevices
//No device handler exists yet... commented out addChildDeviceLine until that is resolved...
if(settings.lockDevices) {
//log.debug "lockDevices: " + settings.lockDevices
def locks = state.locks
settings.lockDevices.each { dni ->
def d = getChildDevice(dni)
if(!d) {
def newLock
newLock = locks.find { ("TC-${settings.automationDeviceId}-${it.value.id}") == dni }
// d = addChildDevice("jhstroebel", "TotalConnect Lock", dni, null /*Hub ID*/, [name: "Device.${dni}", label: "${newLock?.value.name}", completedSetup: true])
}//if it doesn't already exist
}//for each selected lock
}//if there are lockDevices
}//addDevices()
private removeChildDevices(delete) {
delete.each {
deleteChildDevice(it.deviceNetworkId)
}
}
/////////////////////////////////////
// CHILD DEVICE METHODS
/////////////////////////////////////
// Arm Function. Performs arming function
def armAway(childDevice) {
log.debug "TotalConnect2.0 SM: Executing 'armAway'"
tcCommandAsync("ArmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.securityDeviceId, ArmType: 0, UserCode: '-1'])
//we do nothing with response (its almost useless on arm, they want you to poll another command to check for success)
/* this code may not make sense... Alarm shows as armed during countdown. Maybe push arming, then push status? Also what happens if it doesn't arm? this runs forever?
* Also can't use pause(60000), it will exceed the 20 second method execution time... maybe try a runIn() in the device handler to refresh status
pause(60000) //60 second pause for arming countdown
def alarmCode = alarmPanelStatus()
while(alarmCode != 10201) {
pause(3000) // 3 second pause to retry alarm status
alarmCode = alarmPanelStatus()
}//while alarm has not armed
//log.debug "Home is now Armed successfully"
sendEvent(it, [name: "status", value: "Armed Away", displayed: "true", description: "Refresh: Alarm is Armed Away"])
*/
}//armaway
def armAwayInstant(childDevice) {
log.debug "TotalConnect2.0 SM: Executing 'armAwayInstant'"
tcCommandAsync("ArmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.securityDeviceId, ArmType: 3, UserCode: '-1'])
//we do nothing with response (its almost useless on arm, they want you to poll another command to check for success)
/*
def metaData = panelMetaData(token, locationId) // Get AlarmCode
while( metaData.alarmCode != 10205 ){
pause(3000) // 3 Seconds Pause to relieve number of retried on while loop
metaData = panelMetaData(token, locationId)
}
//log.debug "Home is now Armed successfully"
sendPush("Home is now Armed successfully")
*/
}//armaway
def armStay(childDevice) {
log.debug "TotalConnect2.0 SM: Executing 'armStay'"
tcCommandAsync("ArmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.securityDeviceId, ArmType: 1, UserCode: '-1'])
/*
def metaData = panelMetaData(token, locationId) // Gets AlarmCode
while( metaData.alarmCode != 10203 ){
pause(3000) // 3 Seconds Pause to relieve number of retried on while loop
metaData = panelMetaData(token, locationId)
}
//log.debug "Home is now Armed for Night successfully"
sendPush("Home is armed in Night mode successfully")
*/
}//armstay
def armStayInstant(childDevice) {
log.debug "TotalConnect2.0 SM: Executing 'armStayInstant'"
tcCommandAsync("ArmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.securityDeviceId, ArmType: 2, UserCode: '-1'])
/*
def metaData = panelMetaData(token, locationId) // Gets AlarmCode
while( metaData.alarmCode != 10209 ){
pause(3000) // 3 Seconds Pause to relieve number of retried on while loop
metaData = panelMetaData(token, locationId)
}
//log.debug "Home is now Armed for Night successfully"
sendPush("Home is armed in Night mode successfully")
*/
}//armstay
def disarm(childDevice) {
log.debug "TotalConnect2.0 SM: Executing 'disarm'"
tcCommandAsync("DisarmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.securityDeviceId, UserCode: '-1'])
/*
def metaData = panelMetaData(token, locationId) // Gets AlarmCode
while( metaData.alarmCode != 10200 ){
pause(3000) // 3 Seconds Pause to relieve number of retried on while loop
metaData = panelMetaData(token, locationId)
}
// log.debug "Home is now Disarmed successfully"
sendPush("Home is now Disarmed successfully")
*/
}//disarm
def bypassSensor(childDevice) {
def childDeviceInfo = childDevice.getDeviceNetworkId().split("-") //takes deviceId & zoneId from deviceNetworkID in format "TC-DeviceID-SwitchID"
def deviceId = childDeviceInfo[1]
def zoneId = childDeviceInfo[2]
log.debug "TotalConnect2.0 SM: Bypassing Sensor"
log.debug "Bypassing Zone: ${zoneId}"
tcCommandAsync("Bypass", [SessionID: state.token, LocationID: settings.locationId, DeviceID: deviceId, Zone: zoneId, UserCode: '-1'])
}//bypassSensor
def controlSwitch(childDevice, int switchAction) {
def childDeviceInfo = childDevice.getDeviceNetworkId().split("-") //takes deviceId & switchId from deviceNetworkID in format "TC-DeviceID-SwitchID"
def deviceId = childDeviceInfo[1]
def switchId = childDeviceInfo[2]
tcCommandAsync("ControlASwitch", [SessionID: state.token, DeviceID: deviceId, SwitchID: switchId, SwitchAction: switchAction])
}//controlSwitch
def pollChildren(childDevice = null) {
//log.debug "pollChildren() - forcePoll: ${state.forcePoll}, lastPoll: ${state.lastPoll}, now: ${now()}"
if(!isTokenValid())
{
log.error "Token is likely expired. Check Keep alive function in SmartApp"
state.token = login().toString()
}//check if token is likely still valid or login. Might add a sendCommand(command) method and check before sending any commands...
if(childDevice == null) {
log.debug "pollChildren: No child device passed in, will update all devices"
//update all devices (after checking that they exist)
if(settings.alarmDevice) {
//update alarm
tcCommandAsync("GetPanelMetaDataAndFullStatusEx", [SessionID: state.token, LocationID: settings.locationId, LastSequenceNumber: 0, LastUpdatedTimestampTicks: 0, PartitionID: 1]) //This updates panel status
//state.alarmStatus = alarmPanelStatus()
//updateAlarmStatus()
}
if(settings.zoneDevices) {
//update zoneDevices
tcCommandAsync("GetZonesListInStateEx", [SessionID: state.token, LocationID: settings.locationId, PartitionID: 1, ListIdentifierID: 0])
//state.zoneStatus = zoneStatus()
//updateZoneStatuses()
}
if(settings.automationDevices || settings.thermostatDevices || settings.lockDevices) {
//update automationDevices
tcCommandAsync("GetAllAutomationDeviceStatusEx", [SessionID: state.token, DeviceID: settings.automationDeviceId, AdditionalInput: ''])
//state.switchStatus = automationDeviceStatus()
//updateSwitchStatuses()
}//automation devices are 1 call... if any exist update all 3 types
//updateStatuses()
}//check device type and update all of that type only (scheduled polling)
else {
log.debug "pollChildren: childDevice: ${childDevice} passed in"
def childDeviceInfo = childDevice.getDeviceNetworkId().split("-") //takes deviceId & subDeviceId from deviceNetworkID in format "TC-DeviceID-SubDeviceID"
def deviceId = childDeviceInfo[1]