-
Notifications
You must be signed in to change notification settings - Fork 10
/
hpserver.js
11414 lines (10201 loc) · 494 KB
/
hpserver.js
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
#!/usr/bin/env node
"use strict";
process.title = 'hpserver';
// debug options
var EMULATEHUB = false; // this emulates hub pushes and skips ISY sockets for local testing
const DEBUG1 = false; // basic debug info - file loading, hub loading
const DEBUG2 = false; // hub authorization
const DEBUG3 = false; // passwords
const DEBUG4 = false; // filters and options
const DEBUG5 = false; // hub node detail
const DEBUG6 = false; // tile adds and position moves
const DEBUG7 = false; // hub responses
const DEBUG8 = false; // API calls
const DEBUG9 = false; // ISY webSocket success
const DEBUG10 = false; // sibling tag
const DEBUG11 = false; // rules and lists
const DEBUG12 = false; // hub push updates
const DEBUG13 = false; // URL callbacks
const DEBUG14 = false; // tile link details
const DEBUG15 = false; // new user and forgot password
const DEBUG16 = false; // writing, custom names, and image deletes
const DEBUG17 = false; // push client
const DEBUG22 = false; // login info
const DEBUG23 = false; // customize post call debugs
const DEBUGcurl = false; // detailed inspection
const DEBUGisy = false; // ISY debug info
const DEBUGtmp = true; // used to debug anything temporarily using ||
// websocket and http servers
const webSocketServer = require('websocket').server;
const webSocketClient = require('websocket').client;
const path = require('path');
const http = require('http');
const https = require('https');
const fs = require('fs');
const express = require('express');
// const bodyParser = require('body-parser');
const xml2js = require('xml2js').parseString;
const crypto = require('crypto');
const UTIL = require('util');
const cookieParser = require('cookie-parser');
const request = require('request');
const url = require('url');
const nodemailer = require('nodemailer');
const multer = require('multer');
// const countrytime = require('countries-and-timezones');
// load supporting modules
var sqlclass = require("./mysqlclass");
var devhistory = require("./devhistory.js");
// global variables are all part of GLB object
var GLB = {};
GLB.devhistory = devhistory.DEV;
GLB.HPVERSION = GLB.devhistory.substring(1,9).trim();
GLB.APPNAME = 'HousePanel V' + GLB.HPVERSION;
// set a secret that changes every day - API calls must match this value
// it will fail if the call happens on exactly the border of a day but that should be extremely rare rare
var d = new Date().toString();
var k = d.indexOf(" ",14);
var dstr = d.substring(0, k);
GLB.apiSecret = getNewCode(dstr);
// GLB.warnonce = {};
GLB.defaultrooms = {
"Kitchen": 1 ,
"Family": 2,
"Living": 3,
"Office": 4,
"Bedroom": 5,
"Outside": 6,
"Music": 7
};
// any attribute here will be ignored for events and display
// this now includes ignoring washer and robot crazy fields
GLB.ignoredAttributes = [
'sensor', 'actuator', 'DeviceWatch-DeviceStatus', 'DeviceWatch-Enroll', 'checkInterval', 'healthStatus', 'devTypeVer', 'dayPowerAvg', 'apiStatus',
'yearCost', 'yearUsage','monthUsage', 'monthEst', 'weekCost', 'todayUsage', 'groupPrimaryDeviceId', 'groupId', 'presets',
'maxCodeLength', 'maxCodes', 'readingUpdated', 'maxEnergyReading', 'monthCost', 'maxPowerReading', 'minPowerReading', 'monthCost', 'weekUsage', 'minEnergyReading',
'codeReport', 'scanCodes', 'verticalAccuracy', 'horizontalAccuracyMetric', 'distanceMetric', 'closestPlaceDistanceMetric',
'closestPlaceDistance', 'codeChanged', 'codeLength', 'lockCodes', 'horizontalAccuracy',
'verticalAccuracyMetric', 'indicatorStatus', 'todayCost', 'previousPlace','closestPlace', 'minCodeLength',
'arrivingAtPlace', 'lastUpdatedDt', 'custom.disabledComponents',
'disabledCapabilities','enabledCapabilities','supportedCapabilities',
'supportedPlaybackCommands','supportedTrackControlCommands','supportedButtonValues','supportedThermostatModes','supportedThermostatFanModes',
'dmv','di','pi','mnml','mnmn','mnpv','mnsl','icv','washerSpinLevel','mnmo','mnos','mnhw','mnfv','supportedCourses','washerCycle','cycle'
];
GLB.ignoredISY = [
"parent__", "deviceClass", "pnode", "startDelay", "endDelay"
];
// this maps all supported types to their equivalents in ISY
// if the type is not supported then nothing is translated
// first element is a map of ISY id's to HousePanel id's
// second element is a map of commands from HE to ISY with underscore items added to the device
// ST maps are special, they include :on and :off variants for state handling or whatever states
// third element is an array of hints that tell us the device type
// final element is a map of ST states to ST names
// switch inludes 4.16.x to support Zwave switches that show up with this hint
GLB.mainISYMap = {
"isy": [{"ST": "ST"}, {"_query":"QUERY","_queryall":"QUERYALL","_discover":"DISCOVER"}, "0.0." ],
"switch": [{"GV0": "status_", "ST": "switch"}, {"_query":"QUERY","_on":"DON","_off":"DOF","switch":"ST","ST:on":"DON","ST:off":"DOF"}, ["1.1.","4.16."] ],
"switchlevel": [{"GV0": "status_", "ST": "switch", "OL": "level"}, {"_query":"QUERY","_on":"DON","_off":"DOF","level":"SET_BRI","_dim":"DIM","_brighten":"BRIGHTEN",
"level-up":"BRIGHTEN","level-dn":"DIM","switch":"ST","ST:on":"DON","ST:off":"DOF"}, "1.2." ],
"bulb": [{"GV0": "status_", "ST": "switch", "OL": "level", "GV1":"color",
"GV2":"colorname", "GV3": "hue", "GV4":"saturation"}, {"_query":"QUERY","_on":"DON","_off":"DOF","level":"SET_BRI","_dim":"DIM","_brighten":"BRIGHTEN",
"hue":"SET_HUE","saturation":"SET_SAT","hue-up":"HUE_UP","saturation-up":"SAT_UP","hue-dn":"HUE_DN","saturation-dn":"SAT_DN",
"switch":"ST","ST:on":"DON","ST:off":"DOF"}, "1.3." ],
"button": [{"GV0": "status_", "BATLVL": "battery", "ST": "numberOfButtons",
"GV1": "pushed", "GV2":"held", "GV3":"doubleTapped"}, {"_query":"QUERY","_push":"push","_hold":"hold","_doubleTap":"doubleTap","_release":"release"}, "1.4." ],
"power": [{"GV0": "status_", "ST": "switch", "CPW": "power",
"TPW":"energy", "CV":"voltage", "CC":"current"}, {"_query":"QUERY","_on":"DON", "_off":"DOF"}, "1.5" ],
"water": [{"GV0": "status_", "BATLVL": "battery", "ST": "water"}, {"_query":"QUERY","_wet":"WET", "_dry":"DRY"}, "1.6" ],
"contact": [{"GV0": "status_", "BATLVL": "battery", "ST": "contact"}, {"_query":"QUERY","_open":"OPEN","_close":"CLOSE"}, "7.1" ],
"motion": [{"GV0": "status_", "BATLVL": "battery", "ST": "motion"}, {"_query":"QUERY"}, "7.2.0" ],
"aqaramotion": [{"GV0": "status_", "BATLVL": "battery", "ST": "motion",
"GV1": "presence", "GV2":"presence_type",
"GV3":"roomState", "GV4":"roomActivity"}, {"_query":"QUERY", "_setMotion":"setMotion", "_resetState":"resetState"}, "7.2.1" ],
"presence": [{"GV0": "status_", "BATLVL": "battery", "ST": "presence"}, {"_query":"QUERY","_arrived":"ARRIVE","_departed":"DEPART"}, "7.3" ],
"cosensor": [{"GV0": "status_", "BATLVL": "battery", "ST": "carbonMonoxide"}, {"_query":"QUERY","_clear":"CLEAR","_detected":"DETECTED","_test":"TEST"}, "7.4" ],
"co2sensor": [{"GV0": "status_", "BATLVL": "battery", "CO2LVL":"carbonDioxide"}, {"_query":"QUERY"}, "7.5" ],
"smoke": [{"GV0": "status_", "BATLVL": "battery", "ST": "smoke"}, {"_query":"QUERY","_clear":"CLEAR","_detected":"DETECTED","_test":"TEST"}, "7.6" ],
"sleep": [{"GV0": "status_", "BATLVL": "battery", "ST": "sleepSensor"}, {"_query":"QUERY","_arrived":"ARRIVE","_departed":"DEPART"}, "7.7" ],
"door": [{"GV0": "status_", "BATLVL": "battery", "ST": "door"}, {"_query":"QUERY","_open":"OPEN","_close":"CLOSE","door":"ST","ST:open":"OPEN","ST:close":"CLOSE"}, "2.1" ],
"garage": [{"GV0": "status_", "BATLVL": "battery", "ST": "door"}, {"_query":"QUERY","_open":"OPEN","_close":"CLOSE","door":"ST","ST:open":"OPEN","ST:close":"CLOSE"}, "2.2" ],
"shade": [{"GV0": "status_", "BATLVL": "battery", "ST": "windowShade",
"OL": "position"}, {"_query":"QUERY","_open":"OPEN","_close":"CLOSE","_stop":"STOP","position":"SET_POS",
"_raise":"RAISE","_lower":"LOWER","windowShade":"ST","ST:open":"OPEN","ST:close":"CLOSE"}, "2.3" ],
"valve": [{"GV0": "status_", "BATLVL": "battery", "ST": "valve"}, {"valve":"ST","ST:open":"OPEN","ST:close":"CLOSE","_query":"QUERY","_open":"OPEN","_close":"CLOSE"}, "2.4" ],
"lock": [{"GV0": "status_", "BATLVL": "battery", "ST": "lock"}, {"_query":"QUERY","_unlock":"UNLOCK","_lock":"LOCK","lock":"ST","ST:lock":"UNLOCK","ST:unlock":"LOCK"}, "8.1" ],
"mode": [{"GV0": "status_", "MODE": "themode"}, {"_query":"QUERY","_Day":"SET_DAY", "_Evening":"SET_EVENING", "_Night":"SET_NIGHT", "_Away":"SET_AWAY",
"themode":"MODE","MODE:Day":"SET_EVENING","MODE:Evening":"SET_NIGHT","MODE:Night":"SET_AWAY","MODE:Away":"SET_DAY"}, "8.2" ],
"hsm": [{"GV0": "status_", "ST": "status"}, {"_query":"QUERY","_armaway":"armAway","_armHome":"armHome","_armNight":"armNight",
"_disarm":"disarm","_disarmAll":"disarmAll","_cancelAlerts":"cancelAlerts"}, "8.3" ],
"thermostat": [{"GV0": "status_", "BATLVL": "battery", "ST": "switch",
"CLITEMP":"temperature", "CLIHUM": "humidity",
"CLISPH": "heatingSetpoint", "CLISPC": "coolingSetpoint",
"CLIMD": "thermostatMode", "CLIHCS": "thermostatOperatingState",
"CLIFS": "thermostatFanMode", "CLIFRS": "thermostatFanSetting",
"CLISMD": "thermostatHold"}, {"_query":"QUERY", "heatingSetpoint":"SET_HEAT", "heatingSetpoint-up":"HUP", "heatingSetpoint-dn":"HDN",
"coolingSetpoint":"SET_COOL", "coolingSetpoint-up":"CUP", "coolingSetpoint-dn":"CDN",
"_setThermostatMode":"SET_TMODE", "_setThermostatFanMode":"SET_FMODE"}, "5.1" ],
"temperature": [{"GV0": "status_", "BATLVL": "battery", "TEMPOUT": "temperature",
"CLIHUM":"humidity"}, {"_query":"QUERY"}, "5.2" ],
"illuminance": [{"GV0": "status_", "BATLVL": "battery", "LUMIN": "illuminance"}, {"_query":"QUERY"}, "5.3" ],
"other": [{"GV0":"status_","BATLVL":"battery","ST":"switch","OL":"level"}, {"_query":"QUERY"}, "9.1" ],
"actuator": [{"GV0":"status_","BATLVL":"battery","ST":"switch","OL":"level"}, {"_query":"QUERY","_docmd":"DOCMD"}, "9.2" ],
"music": [ {"GV0": "status_", "ST": "status", "OL":"level", "SVOL":"mute"}, {"_previousTrack":"previousTrack","_pause":"pause","_play":"play","_stop":"stop","_nextTrack":"nextTrack",
"_volumeDown":"volumeDown","_volumeUp":"volumeUp","_mute":"mute","_unmute":"unmute","level":"SETVOL"}, "3.1" ]
};
// these are maps of index 25 integers to names
// they should match the en_us.txt file contents
GLB.indexMap = {
"switch": {"RR": {1:"9.0 min", 2:"8.0 min", 3:"7.0 min", 4:"6.0 min", 5:"5.0 min", 6:"4.5 min", 7:"4.0 min", 8:"3.5 min",
9:"3.0 min", 10:"2.5 min", 11:"2.0 min", 12:"1.5 min", 13:"1.0 min", 14:"47.0 sec", 15:"43.0 sec", 16:"38.5 sec",
17:"34.0 sec", 18:"32.0 sec", 19:"30.0 sec", 20:"28.0 sec", 21:"26.0 sec", 22:"23.5 sec", 23:"21.5 sec", 24:"19.0 sec",
25:"8.5 sec", 26:"6.5 sec", 27:"4.5 sec", 28:"2.0 sec", 29:"0.5 sec", 30:"0.3 sec", 31:"0.2 sec", 32:"0.1 sec"} },
"switchlevel": {"RR": {1:"9.0 min", 2:"8.0 min", 3:"7.0 min", 4:"6.0 min", 5:"5.0 min", 6:"4.5 min", 7:"4.0 min", 8:"3.5 min",
9:"3.0 min", 10:"2.5 min", 11:"2.0 min", 12:"1.5 min", 13:"1.0 min", 14:"47.0 sec", 15:"43.0 sec", 16:"38.5 sec",
17:"34.0 sec", 18:"32.0 sec", 19:"30.0 sec", 20:"28.0 sec", 21:"26.0 sec", 22:"23.5 sec", 23:"21.5 sec", 24:"19.0 sec",
25:"8.5 sec", 26:"6.5 sec", 27:"4.5 sec", 28:"2.0 sec", 29:"0.5 sec", 30:"0.3 sec", 31:"0.2 sec", 32:"0.1 sec"} },
"bulb": {"RR": {1:"9.0 min", 2:"8.0 min", 3:"7.0 min", 4:"6.0 min", 5:"5.0 min", 6:"4.5 min", 7:"4.0 min", 8:"3.5 min",
9:"3.0 min", 10:"2.5 min", 11:"2.0 min", 12:"1.5 min", 13:"1.0 min", 14:"47.0 sec", 15:"43.0 sec", 16:"38.5 sec",
17:"34.0 sec", 18:"32.0 sec", 19:"30.0 sec", 20:"28.0 sec", 21:"26.0 sec", 22:"23.5 sec", 23:"21.5 sec", 24:"19.0 sec",
25:"8.5 sec", 26:"6.5 sec", 27:"4.5 sec", 28:"2.0 sec", 29:"0.5 sec", 30:"0.3 sec", 31:"0.2 sec", 32:"0.1 sec"} },
"water": {"ST": {1:"dry", 2: "wet", 3:"unknown"} },
"motion": {"ST": {1:"inactive", 2:"active", 3:"unknown"} },
"aqaramotion": {"ST": {1:"inactive", 2:"active", 3:"unknown"},
"GV1": {1:"present", 2:"absent", 3:"unknown"},
"GV2": {1:"enter", 2:"leave", 3:"approach", 4:"away", 5:"left_enter", 6:"left_leave", 7:"right_enter", 8:"right_leave", 9:"unknown"},
"GV3": {1:"occupied", 2:"unoccupied", 3:"unknown"},
"GV4": {1:"enter", 2:"leave", 3:"towards", 4:"away", 5:"enter (left)", 6:"leave (left)", 7:"enter (right)", 8:"leave (right)", 9:"unknown"} },
"presence": {"ST": {1:"present", 2:"absent", 3:"unknown"} },
"door": {"ST": {1:"open", 2:"closed", 3:"opening", 4:"closing", 5:"unknown"} },
"garage": {"ST": {1:"open", 2:"closed", 3:"opening", 4:"closing", 5:"unknown"} },
"shade": {"ST": {1:"open", 2:"closed", 3:"partially_open", 4:"unknown"} },
"cosensor": {"ST": {1:"clear", 2:"tested", 3:"detected", 4:"unknown"} },
"smoke": {"ST": {1:"clear", 2:"tested", 3:"detected", 4:"unknown"} },
"hsm": {"ST": {1:"disarmed", 2:"armedAway", 3:"armedHome", 4:"armedNight", 5:"armingAway", 6:"armingHome", 7:"armingNight", 8:"allDisarmed", 9:"Not Installed", 10:"unknown"} },
"music": {"ST": {1:"stopped", 2:"playing", 3:"paused", 4:"unknown"} },
"mode": {"MODE": {1:"Day", 2:"Evening", 3:"Night", 4:"Away", 3:"unknown"} }
};
GLB.ISYcolors = [
'Black', 'White', 'Azure', 'Beige', 'Blue', 'Coral',
'Crimson','Forest', 'Fuchsia', 'Golden', 'Gray', 'Green',
'Pink', 'Indigo', 'Lavender', 'Lime', 'Maroon', 'Navy',
'Olive', 'Red', 'Royal', 'Tan', 'Teal', 'Purple',
'Yellow', 'Orange', 'Brown', 'Silver', 'Cyan', 'Custom'
];
// this map is for all supported uom values that use an array of settings
// all others just capture the numerica value into the field
// any uom = 25 must have the values specified in the above mapping object
// color is a special case
// refer to url: https://wiki.universal-devices.com/index.php?title=Polisy_Developers:ISY:API:Appendix:Units_of_Measure
GLB.uomMap = {
2 : {0:"false", 1:"true"},
11 : {0:"unlocked", 100: "locked", 101: "unknown", 102: "jammed"},
66 : {0:"idle", 1:"heating", 2:"cooling", 3:"fan_only", 4:"pending_heat", 5:"pending_cool",
6:"vent", 7:"emergency", 8:"2nd_stage_heating", 9:"2nd_stage_cooling", 10:"2nd_stage_aux_heat", 11:"3rd_stage_aux_heat"},
67 : {0:"off", 1:"heat", 2:"cool", 3:"auto", 4:"emergency", 5:"resume", 6:"fan_only",
7:"furnace", 8:"dry_air", 9:"moist_air", 10:"auto_changeover", 11:"energy_save_heat",
12:"energy_save_cool", 13:"away", 14:"program_auto", 15:"program_heat", 16:"program_cool"},
68 : {0:"auto", 1:"on", 2:"auto_high", 3:"high", 4:"auto_medium", 5:"medium", 6:"circulation", 7:"humidity_circulation",
8:"left_right_circulation", 9:"up_down_circulation", 10:"quiet"},
75 : {0:"Sunday", 1:"Monday", 2: "Tuesday", 3:"Wednesday", 4:"Thursday", 5:"Friday", 6:"Saturday"},
78 : {0:"off", 100: "on", 101:"unknown"},
79 : {0:"open", 100: "closed", 101:"unknown"},
97 : {0:"closed",100:"open",101:"unknown",102:"stopped",103:"closing",104:"opening"}
};
// this map contains the base capability for each type and all valid commands for that capability
// the keys here are unique to HousePanel and are used to define the type of thing on the panel
// the third entry is for mapping ISY hints to our types
// irrigation set same as switches and use pool hint for valves
GLB.capabilities = {
other: [ [], ["_on","_off"] ],
actuator: [ [], ["_on","_off"] ],
switch: [ ["switch"], ["_on","_off"] ],
switchlevel: [ ["switch","switchLevel"], ["_on","_off"] ],
bulb: [ ["colorControl","switch"],["_on","_off","color"] ],
button: [ ["button"],["_pushed","_held"] ],
power: [ ["powerMeter","energyMeter"],null ],
door: [ ["doorControl"],["_open","_close"] ],
garage: [ ["garageDoorControl"],["_open","_close"] ],
shade: [ ["windowShade","switchLevel"],["_open","_close","_pause","_presetPosition"] ],
vacuum: [ ["robotCleanerCleaningMode"],["_auto","_part","_repeat","_manual","_stop"] ],
washer: [ ["washerOperatingState","washerMode","switch"],["_on","_off","_pause","_run","_stop","_setWasherMode"] ],
valve: [ ["valve"],["_open","_close"] ],
contact: [ ["contactSensor"],null ],
motion: [ ["motionSensor"],null ],
presence: [ ["presenceSensor"],null ],
acceleration: [ ["accelerationSensor"],null ],
voltage: [["voltageMeasurement"],null ],
cosensor: [ ["carbonMonoxideMeasurement"],null ],
co2sensor: [ ["carbonDioxideMeasurement"],null ],
dust: [ ["dustSensor"],null ],
water: [ ["waterSensor"],null ],
smoke: [ ["smokeDetector"],null ],
alarm: [ ["alarm"],["_both","_off","_siren","_strobe"] ],
sound: [ ["soundSensor"],null ],
tamper: [ ["tamperAlert"],null ],
tone: [ ["tone"],["_beep"] ],
thermostat: [ ["temperatureMeasurement","thermostatMode","thermostatHeatingSetpoint","thermostatCoolingSetpoint","thermostatOperatingState"],null ],
temperature: [ ["temperatureMeasurement"],null ],
illuminance: [ ["illuminanceMeasurement"],null ],
fan: [ ["fanSpeed"],null ],
weather: [ ["temperatureMeasurement","relativeHumidityMeasurement","illuminanceMeasurement"],["_refresh"] ],
airquality: [ ["airQualitySensor"],null ],
uvindex: [["ultravioletIndex"],null ],
lock: [ ["lock"],["_unlock","_lock"] ],
music: [ ["mediaPlayback"], ["_previousTrack","_pause","_play","_stop","_nextTrack","_volumeDown","_volumeUp","_mute","_unmute","_refresh"] ],
audio: [ ["mediaPlayback","audioVolume","audioMute"],["_previousTrack","_pause","_play","_stop","_nextTrack","_volumeDown","_volumeUp","_mute","_unmute","_refresh"] ],
location: [ ["location"],["_refresh"],null ]
};
// list of capabilities that generate an event
GLB.trigger1 = [
"audioMute", "audioVolume", "battery", "colorControl", "colorTemperature", "contactSensor", "doorControl",
"lock", "motionSensor", "mediaInputSource",
"powerMeter", "presence", "switch", "switchLevel",
"windowShade", "windowShadeLevel"
];
GLB.trigger2 = [
"thermostat", "thermostatSetpoint", "robotCleanerMovement", "robotCleanerCleaningMode",
"temperatureMeasurement", "thermostatMode", "thermostatHeatingSetpoint", "thermostatCoolingSetpoint",
"mediaTrackControl", "mediaPlayback", "audioTrackData",
"illuminanceMeasurement", "smokeDetector", "valve", "waterSensor", "tvChannel"
];
// list of currently connected clients (users)
var clients = {};
// server variables
var app;
var applistening = false;
function setCookie(res, avar, theval, days) {
var options = {SameSite: "lax"};
if ( !days ) {
days = 365;
}
options.maxAge = days*24*3600*1000;
// modified cookie routines to tack on port to ensure we match this instance of the app
var thevar = avar + GLB.port;
res.cookie(thevar, theval, options);
}
function getCookie(req, avar) {
// modified cookie routines to tack on port to ensure we match this instance of the app
var cookies = req.cookies;
var thevar = avar + GLB.port;
if ( is_object(cookies) && typeof cookies[thevar]==="string" ) {
var val = cookies[thevar];
} else {
val = null;
}
return val;
}
function delCookie(res, avar) {
// modified cookie routines to tack on port to ensure we match this instance of the app
var thevar = avar + GLB.port;
res.clearCookie(thevar);
}
function hidden(pname, pvalue, id) {
var inpstr = "<input type='hidden' name='" + pname + "' value='" + pvalue + "'";
if (id) { inpstr += " id='" + id + "'"; }
inpstr += " />";
return inpstr;
}
// this makes Insteon ID's look good but it messes up the hub calls
// which oddly enough expect the id in the mangled form that does not match
// the way the id is written on the Insteon device so I disabled doing anything here
function fixISYid(id) {
// if ( id.indexOf(" ") !== -1 ) {
// var idparts = id.split(" ");
// if ( idparts.length===4 ) {
// idparts.forEach(function(idp, i) {
// if ( idp.length===1 ) {
// idparts[i] = "0" + idp;
// }
// });
// id = idparts.join(".");
// }
// }
return id;
}
function getHeader(userid, pname, skin, skip) {
var $tc = '<!DOCTYPE html>';
$tc += '<html lang="en"><head>';
$tc += '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
// specify icon and color for windows machines
$tc += '<meta name="msapplication-TileColor" content="#2b5797">';
$tc += '<meta name="msapplication-TileImage" content="media/mstile-144x144.png">';
$tc += "<title>HousePanel</title>";
// specify icons for browsers and apple
$tc += '<link rel="icon" type="image/png" href="media/favicon-16x16.png" sizes="16x16"> ';
$tc += '<link rel="icon" type="image/png" href="media/favicon-32x32.png" sizes="32x32"> ';
$tc += '<link rel="icon" type="image/png" href="media/favicon-96x96.png" sizes="96x96"> ';
$tc += '<link rel="apple-touch-icon" href="media/apple-touch-icon.png">';
// load jQuery and themes
$tc += '<link rel="stylesheet" type="text/css" href="jquery-ui.css">';
// $tc += '<script src="jquery-1.12.4.min.js"></script>';
$tc += '<script src="jquery-3.7.1.min.js"></script>';
$tc += '<script src="jquery-ui.min.js"></script>';
$tc += '<link rel="stylesheet" type="text/css" href="jquery-ui.theme.css">';
// $tc += '<script src="jquery-migrate-3.4.0.js"></script>';
// include hack from touchpunch.furf.com to enable touch punch through for tablets
// $tc += '<script src="jquery.ui.touch-punch.min.js"></script>';
$tc += '<script type="text/javascript" src="jquery.mobile-events.js"></script>';
if ( !skip ) {
// minicolors library
$tc += '<script src="jquery.minicolors.min.js"></script>';
$tc += '<link rel="stylesheet" href="jquery.minicolors.css">';
// analog clock support
$tc += '<!--[if IE]><script type="text/javascript" src="excanvas.js"></script><![endif]-->';
$tc += '<script type="text/javascript" src="coolclock.js"></script>';
}
// chart capability loaded here
$tc += '<script type="text/javascript" src="node_modules/chart.js/dist/chart.umd.js"></script>';
// $tc += '<script type="text/javascript" src="node_modules/chart.js/dist/chart.js"></script>';
// load main script file
var customhash = "js001_" + GLB.HPVERSION;
// $tc.= "<link id=\"customtiles\" rel=\"stylesheet\" type=\"text/css\" href=\"$skin/customtiles.css?v=". $customhash ."\">";
$tc += '<script type="text/javascript" src="housepanel.js?v='+customhash+'"></script>';
if ( !skip ) {
// load tile editor and customizer
$tc += "<script type='text/javascript' src='tileeditor.js'></script>";
$tc += '<script type="text/javascript" src="customize.js"></script>';
}
// load fixed css file with cutomization helpers
$tc += "<link id='tileeditor' rel='stylesheet' type='text/css' href='tileeditor.css'>";
// load the main css file - first check for valid skin folder
if (!skin) {
skin = "skin-housepanel";
}
$tc += "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + skin + "/housepanel.css\">";
if ( userid && pname && !skip ) {
// load the custom tile sheet for this user if it exists
// replaced logic to make customizations skin specific
var userfn = "user" + userid + "/" + pname + "/customtiles.css";
// var userfn = "user" + userid + "/" + skin + "/customtiles.css";
if ( fs.existsSync(userfn ) ) {
$tc += "<link id=\"customtiles\" rel=\"stylesheet\" type=\"text/css\" href=\"" + userfn + "\">";
}
}
// begin creating the main page
$tc += '</head><body>';
$tc += '<div class="maintable">';
return $tc;
}
function getFooter() {
return "</div></body></html>";
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function jsonshow(obj) {
return UTIL.inspect(obj, false, null, false);
}
// trying to not use encoding - but we have to replace single quotes to avoid messing up the DB
function encodeURI2(obj) {
var str = JSON.stringify(obj)
str = str.replace(/'/g,"%27");
// return encodeURI(str);
return str;
}
function decodeURI2(str) {
if ( !str || typeof str !== "string" ) {
return null;
}
var obj;
var decodestr = str;
decodestr = decodestr.replace(/%27/g,"'");
// if ( str.startsWith("%7B") ) {
// decodestr = decodeURI(str);
// }
try {
obj = JSON.parse(decodestr);
} catch(e) {
// console.log( (ddbg()),"error parsing existing string into an object, string: ", decodestr);
obj = decodestr;
}
return obj;
}
function hsv2rgb(h, s, v) {
var r, g, b;
function toHex(thenumber) {
var hex = parseInt(thenumber.toString(),16);
if (hex.length === 1) {
hex = "0" + hex;
}
return hex;
}
h = Math.round(h);
s /= 100.0;
v /= 100.0;
if ( h == 360 ) {
h = 0;
} else {
h = h / 60;
}
var i = parseInt(Math.floor(h));
var f = h - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
r = Math.round(r*255);
g = Math.round(g*255);
b = Math.round(b*255);
var rhex = toHex(r);
var ghex = toHex(g);
var bhex = toHex(b);
return "#"+rhex+ghex+bhex;
}
function rgb2hsv(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
var max = Math.max(r, Math.max(g, b));
var min = Math.min(r, Math.min(g, b));
var h = max;
var v = max;
var d = max - min;
var s = max == 0 ? 0 : d / max;
if (max === min) {
h = 0
} else {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
var h100 = h*100;
h100 = h100.toInteger();
h = h*360;
h = h.toInteger();
// h = mapMinMax(h,0,1,0,360);
s *= 100;
s = s.toInteger();
v *= 100;
v = v.toInteger();
return [h, s, v, h100]
}
function objCount(obj) {
if ( typeof obj === "object" ) {
return Object.keys(obj).length;
} else {
return 0;
}
}
function findDevice(bid, swtype, devices) {
for (var id in devices) {
var device = devices[id];
if ( device["devices_deviceid"] === bid && device["devices_devicetype"] === swtype ) {
return device["devices_id"];
} else if ( device["deviceid"] === bid && device["devicetype"] === swtype ) {
return device["id"];
}
}
return null;
}
// updated this function to work with array of configs or a single config
// if a single config is sent the tag should match the configkey of that object
function getConfigItem(configoptions, tag) {
// skip everything if configs are not defined or bogus tag given
if ( !configoptions || !tag ) {
return null;
}
var result = null;
if ( is_array(configoptions) ) {
try {
configoptions.forEach(function(opt) {
if ( opt.configkey === tag ) {
result = opt.configval;
}
});
} catch(e) {
result = null;
}
} else {
try {
if ( tag === configoptions.configkey ) {
result = configoptions.configval;
}
} catch(e) {
result = null;
}
}
// try converting to object
if ( (result && typeof result === "string") &&
(tag==="useroptions" || tag==="usroptions" || tag==="specialtiles" || tag.startsWith("user_") || tag==="clipboard" || result.startsWith("[") || result.startsWith("{")) ) {
var original = result;
try {
result = JSON.parse(result);
} catch (e) {
result = original;
}
}
return result;
}
// this function gets the user name and panel name that matches the hashes
async function getUserName(req) {
var uhash = getCookie(req, "uname");
var phash = getCookie(req, "pname");
if ( phash && phash.substr(1,1) === ":" ) {
phash = phash.substr(2);
}
// get all the users and check for one that matches the hashed email address
// emails for all users must be unique
// *** note *** changed to map userid to the usertype to support multiple logins mapped to same user
var joinstr = mydb.getJoinStr("panels", "userid", "users", "usertype");
var fields = "users.id as users_id, users.email as users_email, users.uname as users_uname, users.mobile as users_mobile, users.password as users_password, " +
"users.usertype as users_usertype, users.defhub as users_defhub, users.hpcode as users_hpcode, " +
"panels.id as panels_id, panels.userid as panels_userid, panels.pname as panels_pname, panels.password as panels_password, panels.skin as panels_skin";
var result = mydb.getRows("panels", fields, "", joinstr)
.then(rows => {
var therow = null;
if ( uhash && rows ) {
for ( var i=0; i<rows.length; i++ ) {
var row = rows[i];
var emailname = row["users_email"];
// if username hash matches uname hash or email hash and if panel name hash match then proceed
if ( ( pw_hash(emailname) === uhash || pw_hash(row["users_uname"]) === uhash ) &&
( pw_hash(row["panels_pname"]) === phash ) ) {
therow = row;
// fix legacy logins that don't have a hpcode security check
if ( !therow["users_hpcode"] ) {
var permcode = getNewCode(emailname);
therow["users_hpcode"] = permcode;
mydb.updateRow("users",{hpcode: permcode},"id = "+ row["users_id"])
.then( () => {
console.log( (ddbg()), "Added missing API security code to login user:", emailname,
"For security purposes, all API calls will use hpcode="+permcode);
})
.catch( reason => {
console.log( (ddbg()), reason);
});
}
break;
}
}
}
return therow;
})
.catch(reason => {
console.log( (ddbg()), "user not found, returning null. reason: ", reason);
return null;
});
return result;
}
// TODO - use DB query on devices table
function getTypes() {
// all sessions have these types
var hubtypes = Object.keys(GLB.mainISYMap);
var blanktypes = ["blank", "custom", "frame", "image", "piston",
"video", "control", "variables", "clock"];
var thingtypes = hubtypes.concat(blanktypes);
// add hubitat specific types
if ( array_key_exists("Hubitat", GLB.dbinfo.hubs) ) {
var hetypes = ["hsm","piston","music","audio","weather","actuator","other"];
hetypes.forEach( key => {
if ( !thingtypes.includes(key) ) {
thingtypes.push(key);
}
});
}
if ( array_key_exists("ISY", GLB.dbinfo.hubs) ) {
if ( !thingtypes.includes("isy") ) {
thingtypes.push("isy");
}
}
thingtypes.sort();
return thingtypes;
}
function writeCustomCss(userid, pname, str) {
if ( typeof str === "undefined" ) {
str = "";
} else if ( typeof str !== "string" ) {
str = str.toString();
}
// proceed only if there is a custom css file in this skin folder
if ( userid && pname ) {
// check for custom directory and if not there make it
var panelfolder = "user" + userid + "/" + pname;
if ( !fs.existsSync(panelfolder) ) {
makeDefaultFolder(userid, pname);
}
var fname = panelfolder + "/customtiles.css";
var d = new Date();
var today = d.toLocaleString();
var fixstr = "";
var opts = "w";
// preserve the header info and update it with date
var ipos = str.indexOf("*---*/");
if ( ipos=== -1 ) {
fixstr += "/* HousePanel Generated Tile Customization File */\n";
if ( str.length ) {
fixstr += "/* Updated: " + today + " *---*/\n";
} else {
fixstr += "/* Created: " + today + " *---*/\n";
}
fixstr += "/* ********************************************* */\n";
fixstr += "/* ****** DO NOT EDIT THIS FILE DIRECTLY ****** */\n";
fixstr += "/* ****** EDITS MADE MAY BE REPLACED ****** */\n";
fixstr += "/* ****** WHENEVER TILE EDITOR IS USED ****** */\n";
fixstr += "/* ********************************************* */\n";
} else {
fixstr += "/* HousePanel Generated Tile Customization File */\n";
fixstr += "/* Updated: " + today + " *---*/\n";
ipos = ipos + 7;
str = str.substring(ipos);
}
// fix addition of backslashes before quotes on some servers
// this changes all instances of \" to just "
if ( str.length ) {
str= str.replace("\\\"","\"");
}
// add the content to the header
fixstr += str;
// write to specific skin folder if the location is valid
try {
fs.writeFileSync(fname, fixstr, {encoding: "utf8", flag: opts});
} catch (e) {
console.log( (ddbg()), e);
}
} else {
console.log( (ddbg()), "custom CSS file not saved to file:", fname);
}
}
function sendText(phone, msg) {
// if a twilio account was provided, text the code
if ( twilioClient && phone && phone.length>=7 ) {
twilioClient.messages.create({
messagingServiceSid: twilioService,
to: phone,
body: msg
})
.then(message => {
console.log( (ddbg()), "Sent txt: ", msg," to: ", phone, " SID: ", message.sid);
});
}
}
function sendEmail(emailname, msg, addinfo = "") {
if ( emailname ) {
var transporter = nodemailer.createTransport({
secure: false,
host: GLB.dbinfo.emailhost,
port: GLB.dbinfo.emailport,
auth: {
user: GLB.dbinfo.emailuser,
pass: GLB.dbinfo.emailpass
},
tls: {rejectUnauthorized: false}
});
// setup the message
var textmsg = "";
var htmlmsg = "";
if ( addinfo ) {
textmsg+= "If you did not request " + addinfo + " for user [" + emailname + "] please ignore this email.\n\n";
textmsg+= "If there is an active link below, click it to proceed, or paste it into a browser window.\n\n";
htmlmsg = "<strong>If you did not request " + addinfo + " for user [" + emailname + "] please ignore this email.</strong><br><br>";
}
textmsg+= msg;
htmlmsg+= msg;
var message = {
from: GLB.dbinfo.emailuser,
to: emailname,
subject: "HousePanel administration message",
text: textmsg,
html: htmlmsg
};
// send the email
transporter.sendMail(message, function(err, info) {
if ( err ) {
console.log( (ddbg()), "error sending email to: ", emailname, " error: ", err);
} else {
console.log( (ddbg()), "email successfully sent to: ", emailname, " response: ", info.response);
}
});
}
}
// function json2query(params) {
// var queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&');
// return queryString;
// }
// this curl function uses promises so it can be chained with .then() like any other promise
function _curl(host, headers, nvpstr, calltype, callback) {
var promise = new Promise(function(resolve, reject) {
// var myURL = url.parse(host);
if ( ! host.startsWith("http") ) {
host = "http://" + host;
}
var myURL = new URL(host);
var path = myURL.pathname;
if ( DEBUGcurl ) {
console.log( (ddbg()),"myURL: ", myURL );
}
// add query string if given separately
var formbuff;
if ( nvpstr ) {
if ( calltype!=="GET" ) {
if ( typeof nvpstr === "object" ) {
formbuff = Buffer.from(JSON.stringify(nvpstr));
} else {
formbuff = nvpstr.toString(); // Buffer.from(nvpstr);
}
} else {
if ( typeof nvpstr === "object" ) {
nvpstr = json2query(nvpstr);
} else {
nvpstr = nvpstr.toString();
}
path = path + "?" + nvpstr;
}
}
if ( DEBUGcurl ) {
console.log((ddbg()), "_curl buffer: ", formbuff, " path: ", path);
}
if ( formbuff ) {
if ( !headers ) {
headers = {};
}
if ( typeof nvpstr === "string" ) {
headers['Content-Length'] = nvpstr.length;
} else {
headers['Content-Length'] = Buffer.byteLength(formbuff);
}
}
var myport = myURL.port;
const opts = {
hostname: myURL.hostname,
port: myport,
path: path,
rejectUnauthorized: false,
method: calltype,
};
if ( headers ) {
opts.headers = headers;
}
if ( myURL.auth ) {
opts.auth = myURL.auth;
}
// get the request
if ( DEBUGcurl ) {
console.log((ddbg()), "_curl opts: ", opts);
}
var totalbody = "";
if ( myURL.protocol === "https:" ) {
var req = https.request(opts, curlResponse);
} else {
req = http.request(opts, curlResponse);
}
// put any form data
if ( formbuff ) {
req.write(formbuff);
}
req.on("error", (e) => {
reject(e);
if ( callback && typeof callback === "function" ) {
if ( typeof e === "object" && e.message ) {
var errmsg = e.message;
} else {
errmsg = e;
}
callback(errmsg, null);
}
});
req.end();
function curlResponse(res) {
res.setEncoding('utf8');
var statusCode = res.statusCode;
var statusMsg = res.statusMessage;
res.on("data", (body) => {
totalbody+= body;
if ( DEBUGcurl ) {
console.log((ddbg()), "_curl debug: body: ", body, " totalbody: ", totalbody);
}
});
res.on("end", ()=> {
resolve(totalbody);
if ( callback && typeof callback === "function" ) {
if ( statusCode === 200 ) {
callback(statusCode, totalbody);
} else {
callback(statusCode, null);
}
}
if ( DEBUGcurl ) {
console.log((ddbg()), "end of _curl message. status: ", statusCode, statusMsg, " body: ", totalbody);
}
});
}
});
return promise;
}
// Made this into a promise function
function curl_call(host, headertype, nvpstr, formdata, calltype, callback = null) {
var promise = new Promise(function(resolve, reject) {
var opts = {url: host, rejectUnauthorized: false};
if ( calltype!=="GET" && calltype!=="POST" ) {
reject("invalid calltype " + calltype);
return;
}
opts.method = calltype;
if ( nvpstr && typeof nvpstr === "object" ) {
opts.form = nvpstr;
} else if ( nvpstr && typeof nvpstr === "string" ) {
opts.url = host + "?" + nvpstr;
}
if (formdata || formdata==="") {
opts.formData = formdata;
}
if ( headertype ) {
opts.headers = headertype;
}
request(opts, curlCallback);
function curlCallback(err, res, body) {
if ( err ) {
console.error( (ddbg()), "curl call error: ", err);
reject(err);
} else {
if ( DEBUGcurl ) {
console.log((ddbg()), "curl_call resolved, body: ", body, " \n res: ", res);
}
// process the callback if one provided
// but typically this isn't done with promises
if ( callback && typeof callback === "function" ) {
callback(err, res, body);
}
// resolve(body);
// // convert body to json or xml depending on what it looks like
var jsonbody;
if ( typeof body==="object" || !body ) {
resolve(body);
} else if ( typeof body==="string" && (body.startsWith("{") || body.startsWith("[")) ) {
try {
jsonbody = JSON.parse(body);
} catch (e) {
jsonbody = body;
}
resolve(jsonbody);
} else if ( typeof body==="string" && body.startsWith("<") ) {
xml2js(body, function(xmlerr, result) {
if ( xmlerr ) {
reject(xmlerr);