-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathHousePanel.groovy
More file actions
3604 lines (3195 loc) · 143 KB
/
HousePanel.groovy
File metadata and controls
3604 lines (3195 loc) · 143 KB
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
/*
* HousePanel
*
* Copyright by Kenneth Washington
*
* 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.
*
* This is a Hubitat app that works with the HousePanel smart dashboard platform
* It also supports Hubitat NodeServer installations on the Universal Devices platform
* Removed history info to keep it now in the devhistory.js file for display in app
*/
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.transform.Field
import java.text.SimpleDateFormat
import java.util.concurrent.Semaphore
public static String handle() { return "HousePanel" }
/**********************************************
STATICALLY DEFINED VARIABLES
inspired by Tonesto7 homebridge2 app
***********************************************/
@Field static final String appVersionFLD = '3.5.22'
@Field static final String sNULL = (String) null
@Field static final String sBLANK = ''
@Field static final String sLINEBR = '<br>'
@Field static final String sSMALL = 'small'
@Field static final String sCLRBLUE = '#3360ff'
@Field static final String sCLRRED = '#ff6033'
@Field static final String sCLRGRY = '#999999'
@Field static final String sBLACK = '#000000'
/**********************************************
APP HELPER FUNCTIONS
inspired by Tonesto7 homebridge2 app
***********************************************/
static String getAppImg(String imgName) { return "https://housepanel.net/wp-content/uploads/${imgName}" }
static String sectH3TS(String t, String st, String i = sNULL, String c=sCLRBLUE) { return "<h3 style='color:${c};font-weight: bold'>${i ? "<img src='${i}' width='48'> " : sBLANK} ${t?.replaceAll("\\n", sLINEBR)}</h3>${st ?: sBLANK}" }
static String sectHead(String str, String img = sNULL) { return str ? "<h3 style='margin-top:0;margin-bottom:0;'>" + spanImgStr(img) + span(str, sCLRBLUE, sNULL, true) + '</h3>' + "<hr style='background-color:${sCLRGRY};font-style:italic;height:1px;border:0;margin-top:0;margin-bottom:0;'>" : sBLANK }
// Root HTML Objects
static String span(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "<span ${(clr || sz || bld) ? "style='${clr ? "color: ${clr};" : sBLANK}${sz ? "font-size: ${sz};" : sBLANK}${bld ? 'font-weight: bold;' : sBLANK}'" : sBLANK}>${str}</span>${br ? sLINEBR : sBLANK}" : sBLANK }
static String div(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "<div ${(clr || sz || bld) ? "style='${clr ? "color: ${clr};" : sBLANK}${sz ? "font-size: ${sz};" : sBLANK}${bld ? 'font-weight: bold;' : sBLANK}'" : sBLANK}>${str}</div>${br ? sLINEBR : sBLANK}" : sBLANK }
static String spanImgStr(String img=sNULL) { return img ? span("<img src='${(!img.startsWith('http')) ? getAppImg(img) : img}' width='42'> ") : sBLANK }
static String divSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true) : sBLANK }
static String htmlLine(String color=sCLRBLUE, Integer width = null) { return "<hr style='background-color:${color};height:1px;border:0;margin-top:0;margin-bottom:0;${width ? "width: ${width}px;" : sBLANK}'>" }
static String lineBr(Boolean show=true) { return show ? sLINEBR : sBLANK }
static String inputFooter(String str, String clr=sCLRBLUE, Boolean noBr=false) { return str ? lineBr(!noBr) + divSmBld(str, clr) : sBLANK }
// Custom versions of the root objects above
static String spanSm(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL) : sBLANK }
static String spanSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, false, true) : sBLANK }
static String spanSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true) : sBLANK }
static String spanSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true, true) : sBLANK }
definition(
name: "${handle()}",
namespace: "kewashi",
author: "Ken Washington",
description: "Tap here to install ${handle()} - a highly customizable smarthome dashboard. ",
category: "Convenience",
iconUrl: "",
iconX2Url: "",
oauth: [displayName: "HousePanel", displayLink: ""])
preferences {
page(name: 'startPage')
page(name: 'mainPage')
page(name: 'deviceSelectPage')
page(name: 'settingsPage')
page(name: 'variablesPage')
}
mappings {
path("/getallthings") { action: [ POST: "getAllThings" ] }
path("/doaction") { action: [ POST: "doAction" ] }
path("/doquery") { action: [ POST: "doQuery" ] }
path("/gethubinfo") { action: [ POST: "getHubInfo" ] }
}
def appInfoSect() {
Boolean isNote = false
def verinfo = getVersion()
String tStr = spanSmBld('HousePanel Version:', sCLRGRY) + spanSmBr(" ${appVersionFLD}", sCLRGRY)
String serverStr = spanSmBld('Latest Version on GitHub:', sCLRGRY) + spanSmBr(" ${verinfo.version}", sCLRGRY)
section (sectH3TS((String)app.name, " ${tStr} ${serverStr}", getAppImg('hpicon3x.png'), 'blue')) {
paragraph htmlLine(sCLRGRY)
}
}
def startPage() {
if (!getAccessToken()) { return dynamicPage(name: 'mainPage', install: false, uninstall: true) {
section() { paragraph spanSmBldBr('OAuth Error', sCLRRED) + spanSmBld("OAuth is not Enabled for ${app?.getName()}!.<br><br>Please click remove and Enable Oauth under the Hubitat App Settings in the App Code page.") } }
} else {
return mainPage()
}
}
def mainPage() {
Boolean isInst = (state.isInstalled == true)
// if ((Boolean)settings.enableWebCoRE && !webCoREFLD) { webCoRE_init() }
if ( !state.globalVars ) {
state.globalVars = getAllGlobalVars()
}
return dynamicPage(name: 'mainPage', nextPage: sBLANK, install: !isInst, uninstall: true) {
appInfoSect()
section(sectHead('Device Configuration:')) {
String desc = sBLANK
desc += myswitches ? spanSmBld("Switch${myswitches.size() > 1 ? 'es' : sBLANK}") + spanSmBr(" (${myswitches.size()})") : sBLANK
desc += mydimmers ? spanSmBld("Dimmers${mydimmers.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mydimmers.size()})") : sBLANK
desc += mybuttons ? spanSmBld("Pushable Button${mybuttons.size() > 1 ? "s" : sBLANK}") + spanSmBr(" (${mybuttons.size()})") : sBLANK
desc += mybulbs ? spanSmBld("Bulb${mybulbs.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mybulbs.size()})") : sBLANK
desc += mypowers ? spanSmBld("Power${mypowers.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mypowers.size()})") : sBLANK
desc += mypresences ? spanSmBld("Presence${mypresences.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mypresences.size()})") : sBLANK
desc += mymotions ? spanSmBld("Motion Sensor${mymotions.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mymotions.size()})") : sBLANK
desc += mybeds ? spanSmBld("Sleep Sensor${mybeds.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mybeds.size()})") : sBLANK
desc += mycontacts ? spanSmBld("Contact Sensor${mycontacts.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mycontacts.size()})") : sBLANK
desc += mydoors ? spanSmBld("Door${mydoors.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mydoors.size()})") : sBLANK
desc += mygarages ? spanSmBld("Garage Door${mygarages.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mygarages.size()})") : sBLANK
desc += mylocks ? spanSmBld("Lock${mylocks.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mylocks.size()})") : sBLANK
desc += myshades ? spanSmBld("Shade${myshades.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${myshades.size()})") : sBLANK
desc += mythermostats ? spanSmBld("Thermostat${mythermostats.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mythermostats.size()})") : sBLANK
desc += mytemperatures ? spanSmBld("Temperature${mytemperatures.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mytemperatures.size()})") : sBLANK
desc += myilluminances ? spanSmBld("Illuminance${myilluminances.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${myilluminances.size()})") : sBLANK
desc += mywaters ? spanSmBld("Water${mywaters.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mywaters.size()})") : sBLANK
desc += myvalves ? spanSmBld("Valve${myvalves.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${myvalves.size()})") : sBLANK
desc += mysmokes ? spanSmBld("Smoke${mysmokes.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mysmokes.size()})") : sBLANK
desc += mycosensors ? spanSmBld("Smoke${mycosensors.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mycosensors.size()})") : sBLANK
desc += myco2sensors ? spanSmBld("Smoke${myco2sensors.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${myco2sensors.size()})") : sBLANK
desc += mymusics ? spanSmBld("Music${mymusics.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${mymusics.size()})") : sBLANK
desc += myaudios ? spanSmBld("Audio${myaudios.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${myaudios.size()})") : sBLANK
desc += myothers ? spanSmBld("Sensor${myothers.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${myothers.size()})") : sBLANK
desc += myactuators ? spanSmBld("Actuator${myactuators.size() > 1 ? 's' : sBLANK}") + spanSmBr(" (${myactuators.size()})") : sBLANK
desc += htmlLine(sCLRBLUE, 200)
desc += inputFooter('Tap to select devices...')
href 'deviceSelectPage', title: spanSmBld('Device Selection'), required: false, description: (desc ? spanSm(desc, sCLRBLUE) : inputFooter('Tap to select devices...', sCLRGRY, true))
}
section(sectHead('Variable Configuration:')) {
String vdesc = sBLANK
def numvar = 0
state.globalVars.each { String varname, Map infomap ->
numvar += settings["var_${varname}"] ? 1 : 0
}
vdesc += numvar > 0 ? spanSmBld("Variable${ (numvar > 1) ? 's' : sBLANK}") + spanSmBr(" (${numvar})") : sBLANK
vdesc += htmlLine(sCLRBLUE, 200)
vdesc += inputFooter("Tap to select Variables...", sCLRBLUE, false)
href 'variablesPage', title: spanSmBld('Variable Selection'), description: (vdesc ? spanSm(vdesc, sCLRBLUE) : inputFooter('Tap to select variables...', sCLRGRY, true))
}
section(sectHead('App Preferences:')) {
String vdesc = sBLANK
vdesc += spanSmBld("Hub Prefix: ", sCLRBLUE) + spanSmBr("${settings.hubprefix}" )
// vdesc += spanSmBld("Ambient Weather Application key: ", sCLRBLUE) + spanSmBr("${settings.ambientappkey}" )
// vdesc += spanSmBld("Ambient Weather API key: ", sCLRBLUE) + spanSmBr("${settings.ambientapikeykeykey}" )
// vdesc += spanSmBld("Weather Tomorrow.io API key: ", sCLRBLUE) + spanSmBr("${settings.tomorrowapi}" )
// vdesc += spanSmBld("Weather ZipCode: ", sCLRBLUE) + spanSmBr("${settings.zipcode}" )
vdesc += spanSmBld("Power skip value: ", sCLRBLUE) + spanSmBr("${settings.powerskip}" )
vdesc += spanSmBld("callback IP 1: ", sCLRBLUE) + spanSmBr("${settings.webHostIP}:${settings.webHostPort}" )
vdesc += spanSmBld("callback IP 2: ", sCLRBLUE) + spanSmBr("${settings.webHostIP2}:${settings.webHostPort2}" )
vdesc += spanSmBld("callback IP 3: ", sCLRBLUE) + spanSmBr("${settings.webHostIP3}:${settings.webHostPort3}" )
vdesc += spanSmBld("Userid: ", sCLRBLUE) + spanSmBr("${settings.userid}" )
vdesc += spanSmBld("Using Cloud? ", sCLRBLUE) + spanSmBr("${settings.usecloud}" )
vdesc += htmlLine(sCLRBLUE, 600)
vdesc += inputFooter("Tap to update App Preferences...", sCLRBLUE, false)
href 'settingsPage', title: spanSmBld('App Preferences'), description: vdesc
}
section(sectHead('Hub Information:')) {
String vdesc = sBLANK
vdesc += spanSmBld("accessToken: ", sCLRBLUE) + spanSmBr("${state.accessToken}" )
vdesc += spanSmBld("App ID: ", sCLRBLUE) + spanSmBr("${app.id}" )
vdesc += spanSmBld("Hub ID: ", sCLRBLUE) + spanSmBr("${state.hubid}" )
vdesc += spanSmBld("Hubname: ", sCLRBLUE) + spanSmBr("${state.hubname}" )
vdesc += spanSmBld("Cloud Endpt: ", sCLRBLUE) + spanSmBr("${state.cloudendpt}" )
vdesc += spanSmBld("Local Endpt: ", sCLRBLUE) + spanSmBr("${state.endpt}" )
vdesc += htmlLine(sCLRBLUE, 600)
paragraph vdesc
paragraph inputFooter(" ", sCLRBLUE, false)
label title: spanSmBld('Label this Instance (optional)'), description: 'Rename this App', defaultValue: app?.name, required: false
}
}
}
def settingsPage() {
return dynamicPage(name: 'settingsPage', nextPage: sBLANK, title: sBLANK, install: false, uninstall: false) {
appInfoSect()
section("HousePanel Configuration Options") {
String vdesc
vdesc = "Note: settings for weather devices are now provided in the main GUI since the supported weather services are not Hubitat dependent."
paragraph spanSmBldBr("Configure the settings for your HousePanel installation here. ", sBLACK) + spanSm(vdesc, sCLRBLUE)
vdesc = spanSmBld("Select a prefix to uniquely identify hsm, mode, and variable tiles for this hub. This only matters if you are using more than one hub. The prefix for each hub must be unique. ")
paragraph vdesc
input (name: "hubprefix", type: "text", multiple: false, title: "Hub Prefix:", required: false, defaultValue: "h_")
// vdesc = spanSmBld("The Ambient Weather API and Application keys are used to pull in weather data from the Ambient Weather service. If you have an Ambient Weather station then you can use these keys to pull in your station data. If you don't have a station but want to pull in weather data for your location then you can use these keys with the free tier of the Ambient Weather API which allows pulling in data for any location based on zip code. If you don't care about weather data or want to use a different service for weather data then these can be safely ignored.")
// paragraph vdesc
// input (name: "ambientappkey", type: "text", multiple: false, title: "Ambient Application key", required: false, defaultValue: "7aeab49a6fa5462fa8ddd52c049b4201984f0f14e9d5476e8cb3e5e4ca233bc7")
// input (name: "ambientapikey", type: "text", multiple: false, title: "Ambient Weather API key", required: false, defaultValue: "")
// vdesc = spanSmBld("The Tomorrow.io API key is used to pull in weather data from the Tomorrow.io service. If you have a Tomorrow.io account then you can use this key to pull in weather data for your location. If you don't care about weather data or want to use a different service for weather data then this can be safely ignored.")
// paragraph vdesc
// input (name: "tomorrowapi", type: "text", multiple: false, title: "Tomorrow.io API key", required: false, defaultValue: "")
// input (name: "zipcode", type: "text", multiple: false, title: "Zip Code for Weather", required: false, defaultValue: "")
vdesc = spanSmBld("The Power Skip value is used to filter out power readings from devices that are below a certain threshold. This is useful for filtering out noise from power meters and other devices that may report small power readings when they are actually off. The value should be between 0 and 1, where 0 means no filtering and 1 means all readings are filtered out. A good starting point is around 0.15, but you may need to adjust this based on your specific devices and setup.")
paragraph vdesc
input (name: "powerskip", type: "float", multiple: false, title: "Power Skip (0 ... 1.0)", required: false, defaultValue: 0.15)
vdesc = spanSmBld("Provide userid from the HousePanel login. This is only needed if multiple users with different hubs exist" +
"and you want to ensure hub pushes are sent to the correct user. If only one user exists or if all users share the same hubs then this can be left as 0." )
paragraph vdesc
input (name: "userid", type: "number", multiple: false, title: "User ID:", required: false, defaultValue: 0)
input (name: "usecloud", type: "bool", multiple: false, title: "Use Cloud?", required: false, defaultValue: false)
// paragraph "Enable Pistons? You must have WebCore installed for this to work. Beta feature for Hubitat hubs."
// input (name: "usepistons", type: "bool", multiple: false, title: "Use Pistons?", required: false, defaultValue: false)
vdesc = spanSmBld("Specify at least the first Host IP and Port set of parameters to enable your panel to stay in sync with things when they change in your home. " +
"These parameters are used to communicate between your Hubitat hub and your HousePanel installation. " +
"Users must change the Host IP to match the IP of the machine where HousePanel is hosted. ")
paragraph vdesc;
input "webHostIP", "text", title: "Host IP", defaultValue: "192.168.1.50", required: true
input "webHostPort", "text", title: "Port", defaultValue: "8580", required: true
vdesc = spanSmBld("The Alternate 2nd and 3rd Host IP and Port values/ranges are completely optional. They are used to send hub pushes to additional installations of HousePanel. " +
"If left as 0 additional hub pushes will not occur. Only use this if you are hosting two or three versions of HousePanel on different machines. " +
"This app also supports the ISY Hubitat Node Server, and for that case you should set one of these to the IP and Port to match that of your ISY Node Server. " +
"Otherwise, these settings can be safely ignored.")
paragraph vdesc;
input "webHostIP2", "text", title: "2nd Host IP", defaultValue: "0", required: false
input "webHostPort2", "text", title: "2nd Port or Range", defaultValue: "0", required: false
input "webHostIP3", "text", title: "3rd Host IP", defaultValue: "0", required: false
input "webHostPort3", "text", title: "3rd Port or Range", defaultValue: "0", required: false
input (
name: "configLogLevel",
title: "IDE Live Logging Level:\nMessages with this level and higher will be logged to the IDE.",
type: "enum",
options: ["0" : "None", "1" : "Error", "2" : "Warning", "3" : "Info", "4" : "Debug", "5" : "Trace"],
defaultValue: "3",
displayDuringSetup: true,
required: false
)
}
}
}
def deviceSelectPage() {
return dynamicPage(name: 'deviceSelectPage', title: sBLANK, install: false, uninstall: false) {
appInfoSect()
section(sectHead('Define Specific Categories:')) {
paragraph spanSmBldBr('Description:', sCLRBLUE) + spanSm('Select devices to show in each category below', sCLRBLUE)
paragraph spanSmBldBr('NOTE: ') + spanSmBldBr('Duplicates are allowed, but not recommended.')
}
section(sectHead("Switches, Dimmers and Buttons", "bulbon.png")) {
input "myswitches", "capability.switch", multiple: true, required: false, title: "Switches"
input "mydimmers", "capability.switchLevel", hideWhenEmpty: true, multiple: true, required: false, title: "Switch Level Dimmers"
input "mybuttons", "capability.pushableButton", hideWhenEmpty: true, multiple: true, required: false, title: "Buttons"
input "mybulbs", "capability.colorControl", hideWhenEmpty: true, multiple: true, required: false, title: "Color Control Bulbs"
input "mypowers", "capability.powerMeter", hideWhenEmpty: true, multiple: true, required: false, title: "Power Meters"
}
section (sectHead("Presence, Motions, and Sleep","boypresent.png")) {
input "mypresences", "capability.presenceSensor", hideWhenEmpty: true, multiple: true, required: false, title: "Presence"
input "mymotions", "capability.motionSensor", multiple: true, required: false, title: "Motion"
input "mybeds", "capability.sleepSensor", multiple: true, required: false, title: "Sleep"
}
section (sectHead("Doors, Contacts, Locks, and Shades","closedoor.png")) {
input "mycontacts", "capability.contactSensor", hideWhenEmpty: true, multiple: true, required: false, title: "Contact Sensors"
input "mydoors", "capability.doorControl", hideWhenEmpty: true, multiple: true, required: false, title: "Doors"
input "mygarages", "capability.garageDoorControl", hideWhenEmpty: true, multiple: true, required: false, title: "Garage Doors"
input "mylocks", "capability.lock", hideWhenEmpty: true, multiple: true, required: false, title: "Locks"
input "myshades", "capability.windowShade", hideWhenEmpty: true, multiple: true, required: false, title: "Window Shades"
}
section (sectHead("Thermostats and Climate","thermometer.png")) {
input "mythermostats", "capability.thermostat", hideWhenEmpty: true, multiple: true, required: false, title: "Thermostats"
input "mytemperatures", "capability.temperatureMeasurement", hideWhenEmpty: true, multiple: true, required: false, title: "Temperature Measures"
input "myilluminances", "capability.illuminanceMeasurement", hideWhenEmpty: true, multiple: true, required: false, title: "Illuminance Measurements"
}
section (sectHead("Water, Sprinklers and Detectors","sprinkler-on.png")) {
input "mywaters", "capability.waterSensor", hideWhenEmpty: true, multiple: true, required: false, title: "Water Sensors"
input "myvalves", "capability.valve", hideWhenEmpty: true, multiple: true, required: false, title: "Sprinklers"
input "mysmokes", "capability.smokeDetector", hideWhenEmpty: true, multiple: true, required: false, title: "Smoke Detectors"
input "mycosensors", "capability.carbonMonoxideDetector", hideWhenEmpty: true, multiple: true, required: false, title: "CO Detectors"
input "myco2sensors", "capability.carbonDioxideMeasurement", hideWhenEmpty: true, multiple: true, required: false, title: "CO2 Detectors"
}
section (sectHead("Music and Audio","unmute.png")) {
paragraph "Music things use the legacy Sonos device handler. Audio things use the new Audio handler that works with multiple audio device types including Sonos."
input "mymusics", "capability.musicPlayer", hideWhenEmpty: true, multiple: true, required: false, title: "Music Players"
input "myaudios", "capability.audioNotification", hideWhenEmpty: true, multiple: true, required: false, title: "Audio Devices"
}
section (sectHead("Other Sensors and Actuators","switchon.png")) {
paragraph "Any thing can be added as an Other sensor or actuator. Other sensors and actuators bring in ALL fields and commands supported by the device."
input "myothers", "capability.sensor", multiple: true, required: false, title: "Which Other Sensors"
input "myactuators", "capability.actuator", multiple: true, required: false, title: "Which Other Actuators"
}
}
}
def variablesPage() {
if ( !state.globalVars ) {
state.globalVars = getAllGlobalVars()
}
return dynamicPage(name: 'variablesPage', title: sBLANK, install: false, uninstall: false) {
appInfoSect()
section(sectHead('Select variables to include:')) {
state.globalVars.each { String varname, Map infomap ->
def vartype = infomap["type"]
def curval = infomap["value"]
paragraph "Variable ${varname} (${vartype}) = ${curval}"
input (name: "var_${varname}", type: "bool", multiple: false, title: "${varname}?", required: false, defaultValue: false)
}
}
}
}
def installed() {
initialize()
}
def updated() {
unsubscribe()
initialize()
}
def initialize() {
// state.usepistons = settings?.usepistons ?: false
state.usepistons = false
// reset variable usage
removeAllInUseGlobalVar()
state.directIP = settings?.webHostIP ?: "0"
state.directIP = state.directIP.trim()
state.directPort = settings?.webHostPort ?: "0"
state.directPort = state.directPort.trim()
state.directIP2 = settings?.webHostIP2 ?: "0"
state.directIP2 = state.directIP2.trim()
state.directPort2 = settings?.webHostPort2 ?: "0"
state.directPort2 = state.directPort2.trim()
state.directIP3 = settings?.webHostIP3 ?: "0"
state.directIP3 = state.directIP3.trim()
state.directPort3 = settings?.webHostPort3 ?: "0"
state.directPort3 = state.directPort3.trim()
state.powerskip = settings.powerskip ?: 0.15
state.prefix = settings?.hubprefix ?: getPrefix()
state.powervals = [:]
state.usecloud = settings?.usecloud ?: false
state.hubtimer = settings?.hubtimer ?: 0
state.userid = settings?.userid ?: 0
configureHub();
if ( state.usepistons ) {
webCoRE_init()
}
state.loggingLevelIDE = settings.configLogLevel?.toInteger() ?: 3
logger("Installed hub ${state.hubname} with settings: ${settings} ", "debug")
def pattern = ~/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
def portpatt = ~/\d{3,}-?(\d{3,})?/
Map hubdata = [accesstoken: state.accessToken, appid: app.id, hubname: state.hubname, hubid: state.hubid,
cloudendpt: state.cloudendpt, localendpt: state.endpt, hubtimer: state.hubtimer, usecloud: state.usecloud]
// report and send to servers name=hubname, id=app.id, type=Hubitat, value=array(accessToken, endpt, cloudendpt)
if ( (state.directIP.startsWith("http") || state.directIP ==~ pattern) && state.directPort ==~ portpatt ) {
postHubRange(state.directIP, state.directPort, "initialize", state.hubname, app.id, "", "Hubitat", hubdata)
logger("state changes will be posted to HP at IP: ${state.directIP}:${state.directPort} ", "info")
} else {
state.directIP = "0"
}
if ( (state.directIP2.startsWith("http") || state.directIP2 ==~ pattern) && state.directPort2 ==~ portpatt ) {
postHubRange(state.directIP2, state.directPort2, "initialize", state.hubname, app.id, "", "Hubitat", hubdata)
logger("state changes will also be posted to HP at: ${state.directIP2}:${state.directPort2} ", "info")
} else {
state.directIP2 = "0"
}
if ( (state.directIP3.startsWith("http") || state.directIP3 ==~ pattern) && state.directPort3 ==~ portpatt ) {
postHubRange(state.directIP3, state.directPort3, "initialize", state.hubname, app.id, "", "Hubitat", hubdata)
logger("state changes will also be posted to HP at: ${state.directIP3}:${state.directPort3} ", "info")
} else {
state.directIP3 = "0"
}
// register callbacks if one is available
if ( state.directIP!="0" || state.directIP2!="0" || state.directIP3!="0" ) {
registerAll()
} else {
logger("state changes will not be posted to HP because no server IP was provided", "warn")
}
}
def getVersion() {
def version = appVersionFLD // Fallback
def logmessage = ""
try {
def params = [
uri: "https://raw.githubusercontent.com/kewashi/HousePanel/master/devhistory.js",
contentType: "text/plain"
]
httpGet(params) { resp ->
if (resp.success && resp.data) {
def content = resp.data.text
// Extract version from first line like "3.5.13 01/12/2026..."
def matcher = content =~ /(\d+\.\d+\.\d+)\s+(.*)$/
if (matcher.find()) {
version = matcher.group(1)
logmessage = matcher.group(2)
logger("Latest HousePanel version on GitHub: ${version} ${logmessage}", "info")
}
}
}
} catch (Exception e) {
logger("Could not retrieve version from GitHub, using fallback: ${e.message}", "warn")
}
return [version: version, message: logmessage]
}
private String getPrefix() {
def hubpre = 'h_'
return hubpre
}
def configureHub() {
if (!state.accessToken) {
state.accessToken = createAccessToken()
}
state.endpt = app.getFullLocalApiServerUrl()
state.cloudendpt = app.getFullApiServerUrl()
state.hubid = app.getHubUID()
state.hubname = location.getName()
logger("Setup values Access Token, App ID, Hub Name, Hub ID, and End Points shown will be pushed or can be entered manually","info")
logger("Access Token = ${state.accessToken}","info")
logger("App ID = ${app.id}", "info")
logger("Hub Name = ${state.hubname}", "info")
logger("Hubitat Local EndPoint = ${state.endpt}", "info")
logger("Hubitat Cloud EndPoint = ${state.cloudendpt}", "info")
logger("Hub ID = ${state.hubid}", "info")
logger("Updates pushed to the following IP addresses and ports","debug")
logger("1st IP Address = ${state.directIP}", "debug")
logger("1st Port = ${state.directPort}", "debug")
logger("2nd IP Address = ${state.directIP2}", "debug")
logger("2nd Port = ${state.directPort2}", "debug")
logger("3rd IP Address = ${state.directIP3}", "debug")
logger("3rd Port = ${state.directPort3}", "debug")
}
def addStatus(resp, item) {
try {
def newstatus = item?.getStatus() ?: "UNKNOWN"
resp["status_"] ? resp["status_"] = newstatus : resp.put("status_", newstatus)
} catch(e) {
logger(e, "error")
}
return resp
}
def addBattery(resp, item) {
addAttr(resp, item, "battery")
}
def addAttr(resp, item, attr) {
if ( resp && item && item.hasAttribute(attr) ) {
resp.put(attr, item.currentValue(attr))
}
return resp
}
def getSwitch(swid, item=null) {
getThing(myswitches, swid, item)
}
def getDimmer(swid, item=null) {
def resp = getThing(mydimmers, swid, item)
// resp.put("levelval", resp["level"] ?: 0)
resp.put("_dim","dim")
resp.put("_brighten","brighten")
return resp
}
def getBulb(swid, item=null) {
def resp = getThing(mybulbs, swid, item)
// resp.put("levelval", resp["level"] ?: 0)
resp.put("_dim","dim")
resp.put("_brighten","brighten")
return resp
}
def getActuator(swid, item=null) {
getThing(myactuators, swid, item)
}
def getButton(swid, item=null) {
getThing(mybuttons, swid, item)
}
def getMotion(swid, item=null) {
getThing(mymotions, swid, item)
}
def getContact(swid, item=null) {
getThing(mycontacts, swid, item)
}
def getLock(swid, item=null) {
getThing(mylocks, swid, item)
}
def getShade(swid, item=null) {
def resp = getThing(myshades, swid, item)
// resp.put("_stop","stopPositionChange")
// resp.put("levelval", resp["level"] ?: 0)
resp.put("_raise","raise")
resp.put("_lower","lower")
return resp
}
def clone(obj) {
def jsonslurper = new JsonSlurper()
def strobj = JsonOutput.toJson(obj)
def newobj = jsonslurper.parseText(strobj)
return newobj
}
def translateObjects(pvalue, musicmap) {
Map newvalue = [:]
def moreobjects = false
def jsonslurper = new JsonSlurper()
def tval
if ( pvalue instanceof Map ) {
pvalue.each{String tkey, stval ->
try {
tval = jsonslurper.parseText(stval)
} catch(e) {
tval = stval
}
if ( tval instanceof Map ) {
// if this key has an object as the value, break it into parts
tval.each{String jtkey, jtval ->
// convert the key using our map
if ( musicmap.containsKey(jtkey) ) {
jtkey = musicmap[jtkey]
}
// skip this element if the mapping has an empty string
// otherwise add the element. create augmented key if the key exists
if ( jtkey != "" ) {
// logger("jtkey: ${jtkey} jtval: ${jtval}","warn")
// set flag to recurse if there is an object in the value
if ( jtval instanceof Map ) {
moreobjects = true
}
if ( newvalue.containsKey(jtkey) ){
if ( (jtval!="NA" && jtval!="" && jtval!="undefined") ||
(newvalue[jtkey]=="" || newvalue[jtkey]=="NA" || newvlaue[jtkey]=="undefined") ) {
newvalue[jtkey] = jtval
}
} else {
newvalue.put(jtkey,jtval)
}
}
}
// if the map includes the key of objects then save the json string in that key
if ( musicmap.containsKey(tkey) && musicmap[tkey]!="" ) {
newvalue.put(tkey, JsonOutput.toJson(tval))
}
// if the value is not an object then we save it as-is
// but change the key using the mapping
} else {
if ( musicmap.containsKey(tkey) ) {
tkey = musicmap[tkey]
}
// skip if the mapping is a blank string
// overwrite existing fields unless the value is blank or NA or similar
if ( tkey!="" ) {
// fix up image art field
if ( tkey == "trackImage" ) {
if ( (tval && tval instanceof String) && tval.indexOf("http")>0 ) {
def j1 = tval.indexOf(">http") + 1
def j2 = tval.indexOf("<", j1+1)
if ( j1==-1 || j2==-1) {
tval = ""
} else {
tval = tval.substring(j1, j2)
// must escape backslash twice because it is special in groovy
tval = tval.replaceAll("\\\\","")
}
}
}
if ( newvalue.containsKey(tkey) ) {
if ( (tval!="NA" && tval!="" && tval!="undefined") ||
(newvalue[tkey]=="" || newvalue[tkey]=="NA" || newvlaue[tkey]=="undefined") ) {
newvalue[tkey] = tval
}
} else {
newvalue.put(tkey,tval)
}
// newvalue.containsKey(tkey) ? newvalue[tkey] = tval : newvalue.put(tkey, tval)
}
}
}
}
// recurse if there are more objects
if ( moreobjects ) {
return translateObjects(newvalue, musicmap)
} else {
return newvalue
}
}
// this was updated to use the real key names so that push updates work
// note -changes were also made in housepanel.php and elsewhere to support this
def getMusic(swid, item=null) {
// now we translate music tiles here instead of inside hpserver
// this way ISY node server and hpserver both get same info
// without duplicating the translation code in both places
def musicmap = ["name": "trackDescription", "artist": "currentArtist", "album": "currentAlbum",
"trackData": "", "metaData":"", "trackMetaData":"trackMetaData",
"trackNumber":"trackNumber", "music":"", "trackUri":"", "uri":"", "transportUri":"", "enqueuedUri":"",
"audioSource": "mediaSource", "station":"",
"status": "status", "level":"level", "mute":"mute"]
def resp = getThing(mymusics, swid, item)
def pvalue = translateObjects(resp, musicmap)
logger("original music tile: ${resp}","debug")
logger("translated music tile: ${pvalue}","debug")
// get image from metadata of sonos
String jtval = ""
String ktval = ""
if ( pvalue.containsKey("trackMetaData") ) {
jtval = pvalue["trackMetaData"]
if ( jtval && jtval instanceof String ) {
def j1 = jtval.indexOf("https")
if ( j1 > 0 ) {
def j2 = jtval.indexOf("<", j1+1)
if ( j2 != -1) {
jtval = jtval.substring(j1, j2)
ktval = jtval.replaceAll("\\\\/","/")
} else {
ktval = ""
}
pvalue.containsKey("trackImage") ? pvalue["trackImage"] = ktval : pvalue.put("trackImage",ktval)
}
pvalue.remove("trackMetaData")
}
}
logger("image added to tile: ${pvalue}, jtval: ${jtval}, j1: ${j1}, j2: ${j2}","debug")
return pvalue
}
// this mapping works for Alexa Speaks echo devices
def getAudio(swid, item=null) {
def audiomap = ["name":"name", "title":"trackDescription", "artist": "currentArtist", "album": "currentAlbum",
"albumArtUrl": "", "mediaSource": "", "deviceIcon":"deviceIcon", "alexaPlaylists":"",
"phraseSpoken":"", "supportedMusic":"", "trackData":"", "trackImageHtml":"", "wakeWords":"", "alexaWakeWord":"",
"My Likes":"", "deviceFamily":"", "lastUpdated":"", "deviceStatus":"", "onlineStatus":"", "btDeviceConnected":"",
"wasLastSpokenToDevice":"", "doNotDisturb":"", "firmwareVer":"", "phraseSpoken":"", "lastAnnouncement":"",
"lastVoiceActivity":"", "lastSpeakCmd":"", "wasLastSpokenToDevice":"", "followUpMode":"", "currentStation":"",
"deviceSerial":"", "permissions":"", "doNotDisturb":"", "btDevicesPaired":"", "alarmVolume":"", "_togglePlayback":"",
"_getDeviceActivity":"", "_noOp":"", "_replayText":"", "_speechTest":"", "_refresh":"refresh", "deviceType":"",
"_doNotDisturbOff":"", "_doNotDisturbOn":"", "_getBluetoothDevices":"", "_restoreLastVolume":"", "_stopAllDevices":"",
"_disconnectBluetooth":"", "_sendTestAnnouncement":"", "_sendTestAnnouncementAll":"", "_storeCurrentVolume":""]
def resp = getThing(myaudios, swid, item)
def pvalue = translateObjects(resp, audiomap)
logger("original audio tile: ${resp}","debug")
logger("adjusted audio tile: ${pvalue}","debug")
return pvalue
}
// this was updated to use the real key names so that push updates work
// note -changes were also made in housepanel.php and elsewhere to support this
def getThermostat(swid, item=null) {
def resp = getThing(mythermostats, swid, item)
resp.put("_heat", "heat")
resp.put("_cool", "cool")
resp.put("_auto", "auto")
resp.put("_off", "off")
return resp
}
// use absent instead of "not present" for absence state
def getPresence(swid, item=null) {
getThing(mypresences, swid, item)
}
def getBed(swid, item=null) {
getThing(mybeds, swid, item)
}
def getWater(swid, item=null) {
getThing(mywaters, swid, item)
}
def getValve(swid, item=null) {
getThing(myvalves, swid, item)
}
def getDoor(swid, item=null) {
getThing(mydoors, swid, item)
}
def getGarage(swid, item=null) {
getThing(mygarages, swid, item)
}
def getIlluminance(swid, item=null) {
item = item ? item : myilluminances.find{it.id == swid }
def resp = [:]
if ( item ) {
resp = [name: item.displayName]
resp = addBattery(resp, item)
resp.put("illuminance", item.currentValue("illuminance") )
resp = addStatus(resp, item)
}
return resp
}
def getSmoke(swid, item=null) {
getThing(mysmokes, swid, item)
}
def getCO(swid, item=null) {
getThing(mycosensors, swid, item)
}
def getCO2(swid, item=null) {
getThing(myco2sensors, swid, item)
}
// return temperature for this capability and humidity if it has one
def getTemperature(swid, item=null) {
item = item ? item : mytemperatures.find{it.id == swid }
def resp = [:]
if ( item ) {
resp = [name: item.displayName]
resp = addBattery(resp, item)
resp.put("temperature", item.currentValue("temperature") )
if ( item.hasAttribute("humidity") ) {
resp.put("humidity", item.currentValue("humidity") )
}
resp = addStatus(resp, item)
}
return resp
}
def getOther(swid, item=null) {
getThing(myothers, swid, item)
}
def getPower(swid, item=null) {
def resp = getThing(mypowers, swid, item)
def idstr = swid.toString()
try {
state.powervals[idstr] = resp.power.toFloat()
} catch (e) {
state.powervals[idstr] = 0.0
}
return resp
}
def getMyMode(swid, item=null) {
def curmode = location.getCurrentMode()
def resp = [ name: "Mode", subname: location.getName(), themode: curmode?.getName() ];
def allmodes = location.getModes()
for (defcmd in allmodes) {
def modecmd = defcmd.getName()
resp.put("_${modecmd}",modecmd)
}
return resp
}
def getHSMState(swid, item=null) {
// uses Hubitat specific call for HSM per
// https://community.hubitat.com/t/hubitat-safety-monitor-api/934/11
def cmds = ["armAway", "armHome", "armNight", "disarm", "armRules", "disarmRules", "disarmAll", "armAll", "cancelAlerts"]
def keys = ["armedAway", "armedHome", "armedNight", "disarmed"]
// def keynames = ["Away", "Home", "Night", "Disarmed"]
def key = location.hsmStatus ?: false
def resp
if ( !key ) {
resp = [name : "Hubitat Safety Monitor", state: "Not Installed", "status_":"OFFLINE"]
logger("location hsmStatus is invalid; it is not installed","debug")
} else {
resp = [name : "Hubitat Safety Monitor", state: key, "status_":"ONLINE"]
logger("location hsmStatus returned: ${key}","debug")
for (defcmd in cmds) {
resp.put("_${defcmd}",defcmd)
}
}
return resp
}
// change pistonName to name to be consistent
// but retain original for backward compatibility reasons
def getPiston(swid, item=null) {
item = item ? item : webCoRE_list().find{it.id == swid}
def resp = [name: item.name, pistonName: "idle"]
return resp
}
// a generic device getter to streamline code
def getDevice(mydevices, swid, item=null) {
def resp = false
if ( mydevices ) {
item = item ? item : mydevices.find{it.id == swid }
if (item) {
resp = [:]
resp.put("name", item.displayName)
def attrs = item.getSupportedAttributes()
attrs.each {att ->
try {
def attname = att.name
def attval = item.currentValue(attname)
resp.put(attname,attval)
} catch (e) {
logger("Attempt to read device attribute for ${swid} failed ${e}", "error")
}
}
// updated this to return the number of parameters in the key of the command
// this allows HousePanel to know how many parameters to prompt for
def reserved = ignoredCommands()
item.getSupportedCommands().each { comm ->
try {
def comname = comm.toString()
def nparms = comm.parameters?.size?: 0
// skip reserved commands
if ( reserved.contains(comname) || nparms > 6 ) {
logger("skipped command: ${comname} with ${nparms} parameters","debug")
} else {
// now we embed the number of parameters in the command key
// this allows HousePanel to know how many parameters to prompt for
// and we consistently put the command name as the value
def comkey = nparms == 0 ? "_${comname}" : "_${comname}#${nparms}"
resp.put(comkey, comname)
}
} catch (ex) {
logger("Attempt to read command for ${swid} failed ${ex}", "error")
}
}
resp = addStatus(resp, item)
}
}
return resp
}
def ignoredAttributes() {
// thanks to the authors of HomeBridge for this list
def ignore = [
'DeviceWatch-DeviceStatus', 'DeviceWatch-Enroll', 'checkInterval', 'healthStatus', 'devTypeVer', 'dayPowerAvg', 'apiStatus',
'yearCost', 'yearUsage','monthUsage', 'monthEst', 'weekCost', 'todayUsage',
'supportedPlaybackCommands', 'groupPrimaryDeviceId', 'groupId', 'supportedTrackControlCommands', 'presets', 'released',
'maxCodeLength', 'maxCodes', 'readingUpdated', 'maxEnergyReading', 'monthCost', 'maxPowerReading', 'minPowerReading', 'monthCost', 'weekUsage', 'minEnergyReading',
'codeReport', 'scanCodes', 'verticalAccuracy', 'horizontalAccuracyMetric', 'altitudeMetric', 'distanceMetric', 'closestPlaceDistanceMetric',
'closestPlaceDistance', 'leavingPlace', 'currentPlace', 'codeChanged', 'codeLength', 'lockCodes', 'horizontalAccuracy', 'bearing', 'speedMetric',
'speed', 'verticalAccuracyMetric', 'altitude', 'indicatorStatus', 'todayCost', 'distance', 'previousPlace','closestPlace', 'places', 'minCodeLength',
'arrivingAtPlace', 'lastUpdatedDt', 'firmware', 'firmware0', 'firmware1', 'lastEvent', 'lastActivity', 'groups',
'commStatus', 'bypassed', 'connectivity', 'tempScale', 'lqi', 'rssi', 'batteryVoltage', 'hubMeshDisabled', 'reachable',
'supportedPlaybackCommands','supportedTrackControlCommands','supportedButtonValues','supportedThermostatModes','supportedThermostatFanModes',
'dmv','di','pi','mnml','mnmn','mnpv','mnsl','icv','washerSpinLevel','mnmo','mnos','mnhw','mnfv','supportedCourses','washerCycle'
]
return ignore
}
def ignoredCommands() {
def ignore = ["enrollResponse","setAssociationGroup","markDeviceOnline","markDeviceOffline","updateFirmware",
"childOff","childOn","childRefresh","childSetLevel","updateIpAddress",
"componentOff","componentOn","componentRefresh","componentSetColor","componentSetColorTemperatures","componentSetLevel"
]
return ignore
}
// make a generic thing getter to streamline the code
def getThing(things, swid, item=null) {
def resp = [:]
item = item ? item : things?.find{it.id == swid }
def reservedcap = ignoredAttributes()
if ( item ) {
resp.put("name",item.displayName)
item.getSupportedAttributes().each{attr ->
try {
def othername = attr.name
if ( othername && !reservedcap.contains(othername) ) {
def othervalue = item.currentValue(othername, true)
resp.put(othername,othervalue)
}
} catch (ex) {
logger("Attempt to read attribute for ${swid} failed ${ex}", "error")
}
}
def reserved = ignoredCommands()
// we now return commands that have 6 or fewer parameters
// commands with more than 6 parameters are ignored
item.getSupportedCommands().each { comm ->
try {
def comname = comm.toString()
def nparms = comm.parameters?.size?: 0
// skip reserved commands
if ( reserved.contains(comname) || nparms > 6 ) {
logger("skipped command: ${comname} with ${nparms} parameters","debug")
} else {
// now we embed the number of parameters in the command key
// this allows HousePanel to know how many parameters to prompt for
// and we consistently put the command name as the value
def comkey = nparms == 0 ? "_${comname}" : "_${comname}#${nparms}"
resp.put(comkey, comname)
}
} catch (ex) {
logger("Attempt to read command for ${swid} failed ${ex}", "error")
}
}
// fix color - must have either hue/saturation or colorTemperature plus level
if ( ((resp["hue"] && resp["saturation"]) || resp["colorTemperature"]) && resp["level"] ) {
def h = resp["hue"] ? resp["hue"].toInteger() : 0
def s = resp["saturation"] ? resp["saturation"].toInteger() : 0
def ct = resp["colorTemperature"] ? resp["colorTemperature"].toInteger() : 0
def v = resp["level"].toInteger()
resp["hue"] = h
resp["saturation"] = s
resp["level"] = v
// resp["levelval"] = v
// set color based on mode
def newcolor
if ( resp["colorMode"] && resp["colorMode"] == "CT" && ct ) {
newcolor = ct2rgb( ct, v )
} else {
h = Math.round((h*360)/100)
newcolor = hsv2rgb(h, s, v)
}
resp["color"] = newcolor
}
resp = addStatus(resp, item)
}
return resp
}
// make a generic thing list getter to streamline the code
def getThings(resp, things, thingtype) {
def n = things ? things.size() : 0
logger("Number of things of type ${thingtype} = ${n}", "debug")