nodejs-poolcontroller 7.6.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/.eslintrc.json +36 -45
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -0
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -0
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -0
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  6. package/CONTRIBUTING.md +74 -74
  7. package/Changelog +242 -215
  8. package/Dockerfile +17 -17
  9. package/Gruntfile.js +40 -40
  10. package/LICENSE +661 -661
  11. package/README.md +195 -191
  12. package/anslq25/MessagesMock.ts +218 -0
  13. package/anslq25/boards/MockBoardFactory.ts +50 -0
  14. package/anslq25/boards/MockEasyTouchBoard.ts +696 -0
  15. package/anslq25/boards/MockSystemBoard.ts +217 -0
  16. package/anslq25/chemistry/MockChlorinator.ts +75 -0
  17. package/anslq25/pumps/MockPump.ts +84 -0
  18. package/app.ts +10 -14
  19. package/config/Config.ts +26 -8
  20. package/config/VersionCheck.ts +8 -4
  21. package/controller/Constants.ts +59 -25
  22. package/controller/Equipment.ts +2667 -2459
  23. package/controller/Errors.ts +181 -180
  24. package/controller/Lockouts.ts +534 -436
  25. package/controller/State.ts +596 -77
  26. package/controller/boards/AquaLinkBoard.ts +1003 -0
  27. package/controller/boards/BoardFactory.ts +53 -45
  28. package/controller/boards/EasyTouchBoard.ts +3079 -2653
  29. package/controller/boards/IntelliCenterBoard.ts +3821 -4230
  30. package/controller/boards/IntelliComBoard.ts +69 -63
  31. package/controller/boards/IntelliTouchBoard.ts +384 -241
  32. package/controller/boards/NixieBoard.ts +1871 -1675
  33. package/controller/boards/SunTouchBoard.ts +393 -0
  34. package/controller/boards/SystemBoard.ts +5244 -4697
  35. package/controller/comms/Comms.ts +905 -541
  36. package/controller/comms/ScreenLogic.ts +1663 -0
  37. package/controller/comms/messages/Messages.ts +382 -54
  38. package/controller/comms/messages/config/ChlorinatorMessage.ts +8 -4
  39. package/controller/comms/messages/config/CircuitGroupMessage.ts +5 -2
  40. package/controller/comms/messages/config/CircuitMessage.ts +82 -13
  41. package/controller/comms/messages/config/ConfigMessage.ts +3 -1
  42. package/controller/comms/messages/config/CoverMessage.ts +2 -1
  43. package/controller/comms/messages/config/CustomNameMessage.ts +31 -30
  44. package/controller/comms/messages/config/EquipmentMessage.ts +5 -1
  45. package/controller/comms/messages/config/ExternalMessage.ts +33 -3
  46. package/controller/comms/messages/config/FeatureMessage.ts +2 -1
  47. package/controller/comms/messages/config/GeneralMessage.ts +2 -1
  48. package/controller/comms/messages/config/HeaterMessage.ts +145 -11
  49. package/controller/comms/messages/config/IntellichemMessage.ts +2 -1
  50. package/controller/comms/messages/config/OptionsMessage.ts +16 -27
  51. package/controller/comms/messages/config/PumpMessage.ts +62 -47
  52. package/controller/comms/messages/config/RemoteMessage.ts +80 -13
  53. package/controller/comms/messages/config/ScheduleMessage.ts +390 -347
  54. package/controller/comms/messages/config/SecurityMessage.ts +2 -1
  55. package/controller/comms/messages/config/ValveMessage.ts +44 -27
  56. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +44 -91
  57. package/controller/comms/messages/status/EquipmentStateMessage.ts +139 -30
  58. package/controller/comms/messages/status/HeaterStateMessage.ts +135 -86
  59. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -445
  60. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -35
  61. package/controller/comms/messages/status/PumpStateMessage.ts +92 -2
  62. package/controller/comms/messages/status/VersionMessage.ts +2 -1
  63. package/controller/nixie/Nixie.ts +173 -162
  64. package/controller/nixie/NixieEquipment.ts +104 -103
  65. package/controller/nixie/bodies/Body.ts +120 -120
  66. package/controller/nixie/bodies/Filter.ts +135 -135
  67. package/controller/nixie/chemistry/ChemController.ts +2682 -2498
  68. package/controller/nixie/chemistry/ChemDoser.ts +806 -0
  69. package/controller/nixie/chemistry/Chlorinator.ts +367 -314
  70. package/controller/nixie/circuits/Circuit.ts +402 -248
  71. package/controller/nixie/heaters/Heater.ts +815 -649
  72. package/controller/nixie/pumps/Pump.ts +934 -661
  73. package/controller/nixie/schedules/Schedule.ts +319 -257
  74. package/controller/nixie/valves/Valve.ts +170 -170
  75. package/defaultConfig.json +346 -286
  76. package/logger/DataLogger.ts +448 -448
  77. package/logger/Logger.ts +38 -9
  78. package/package.json +60 -56
  79. package/tsconfig.json +25 -25
  80. package/web/Server.ts +275 -117
  81. package/web/bindings/aqualinkD.json +560 -0
  82. package/web/bindings/homeassistant.json +437 -0
  83. package/web/bindings/influxDB.json +1066 -1021
  84. package/web/bindings/mqtt.json +721 -654
  85. package/web/bindings/mqttAlt.json +746 -684
  86. package/web/bindings/rulesManager.json +54 -54
  87. package/web/bindings/smartThings-Hubitat.json +31 -31
  88. package/web/bindings/valveRelays.json +20 -20
  89. package/web/bindings/vera.json +25 -25
  90. package/web/interfaces/baseInterface.ts +188 -136
  91. package/web/interfaces/httpInterface.ts +148 -124
  92. package/web/interfaces/influxInterface.ts +283 -245
  93. package/web/interfaces/mqttInterface.ts +695 -475
  94. package/web/interfaces/ruleInterface.ts +87 -0
  95. package/web/services/config/Config.ts +177 -49
  96. package/web/services/config/ConfigSocket.ts +2 -1
  97. package/web/services/state/State.ts +154 -3
  98. package/web/services/state/StateSocket.ts +69 -18
  99. package/web/services/utilities/Utilities.ts +232 -42
  100. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
  101. package/config copy.json +0 -300
  102. package/issue_template.md +0 -52
package/web/Server.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /* nodejs-poolController. An application to control pool equipment.
2
- Copyright (C) 2016, 2017, 2018, 2019, 2020. Russell Goldin, tagyoureit. russ.goldin@gmail.com
2
+ Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
+ Russell Goldin, tagyoureit. russ.goldin@gmail.com
3
4
 
4
5
  This program is free software: you can redistribute it and/or modify
5
6
  it under the terms of the GNU Affero General Public License as
@@ -37,6 +38,7 @@ import { logger } from "../logger/Logger";
37
38
  import { HttpInterfaceBindings } from './interfaces/httpInterface';
38
39
  import { InfluxInterfaceBindings } from './interfaces/influxInterface';
39
40
  import { MqttInterfaceBindings } from './interfaces/mqttInterface';
41
+ import { RuleInterfaceBindings } from "./interfaces/ruleInterface";
40
42
  import { ConfigRoute } from "./services/config/Config";
41
43
  import { ConfigSocket } from "./services/config/ConfigSocket";
42
44
  import { StateRoute } from "./services/state/State";
@@ -44,7 +46,8 @@ import { StateSocket } from "./services/state/StateSocket";
44
46
  import { UtilitiesRoute } from "./services/utilities/Utilities";
45
47
  import express = require('express');
46
48
  import extend = require("extend");
47
-
49
+ import { setTimeout as setTimeoutSync } from 'timers';
50
+ import { setTimeout } from 'timers/promises';
48
51
 
49
52
  // This class serves data and pages for
50
53
  // external interfaces as well as an internal dashboard.
@@ -54,6 +57,7 @@ export class WebServer {
54
57
  private _servers: ProtoServer[] = [];
55
58
  private family = 'IPv4';
56
59
  private _autoBackupTimer: NodeJS.Timeout;
60
+ private _httpPort: number;
57
61
  constructor() { }
58
62
  public async init() {
59
63
  try {
@@ -68,12 +72,15 @@ export class WebServer {
68
72
  switch (s) {
69
73
  case 'http':
70
74
  srv = new HttpServer(s, s);
75
+ if (c.enabled !== false) this._httpPort = c.port;
71
76
  break;
72
77
  case 'http2':
73
78
  srv = new Http2Server(s, s);
79
+ if (c.enabled !== false) this._httpPort = c.port;
74
80
  break;
75
81
  case 'https':
76
82
  srv = new HttpsServer(s, s);
83
+ if (c.enabled !== false) this._httpPort = c.port;
77
84
  break;
78
85
  case 'mdns':
79
86
  srv = new MdnsServer(s, s);
@@ -111,7 +118,13 @@ export class WebServer {
111
118
  int.init(c);
112
119
  this._servers.push(int);
113
120
  break;
121
+ case 'rule':
122
+ int = new RuleInterfaceServer(c.name, type);
123
+ int.init(c);
124
+ this._servers.push(int);
125
+ break;
114
126
  case 'influx':
127
+ case 'influxdb2':
115
128
  int = new InfluxInterfaceServer(c.name, type);
116
129
  int.init(c);
117
130
  this._servers.push(int);
@@ -161,6 +174,7 @@ export class WebServer {
161
174
  // RKS: We need to get the scope-local nic. This has nothing to do with IP4/6 and is not necessarily named en0 or specific to a particular nic. We are
162
175
  // looking for the first IPv4 interface that has a mac address which will be the scope-local address. However, in the future we can simply use the IPv6 interface
163
176
  // if that is returned on the local scope but I don't know if the node ssdp server supports it on all platforms.
177
+ let fallback; // Use this for WSL adapters.
164
178
  for (let name in networkInterfaces) {
165
179
  let nic = networkInterfaces[name];
166
180
  for (let ndx in nic) {
@@ -168,17 +182,43 @@ export class WebServer {
168
182
  // All scope-local addresses will have a mac. In a multi-nic scenario we are simply grabbing
169
183
  // the first one we come across.
170
184
  if (!addr.internal && addr.mac.indexOf('00:00:00:') < 0 && addr.family === this.family) {
171
- return addr;
185
+ if (!addr.mac.startsWith('00:'))
186
+ return addr;
187
+ else if (typeof fallback === 'undefined') fallback = addr;
172
188
  }
173
189
  }
174
190
  }
191
+ return fallback;
175
192
  }
176
- public ip() {
177
- return typeof this.getInterface() === 'undefined' ? '0.0.0.0' : this.getInterface().address;
178
- }
179
- public mac() {
180
- return typeof this.getInterface() === 'undefined' ? '00:00:00:00' : this.getInterface().mac;
193
+ public getNetworkInterfaces() {
194
+ const networkInterfaces = os.networkInterfaces();
195
+ // RKS: We need to get the scope-local nics. This has nothing to do with IP4/6 and is not necessarily named en0 or specific to a particular nic. We are
196
+ // looking for the first IPv4 interface that has a mac address which will be the scope-local address. However, in the future we can simply use the IPv6 interface
197
+ // if that is returned on the local scope but I don't know if the node ssdp server supports it on all platforms.
198
+ let ips = [];
199
+ let nics = { physical: [], virtual: [] }
200
+ for (let name in networkInterfaces) {
201
+ let nic = networkInterfaces[name];
202
+ for (let ndx in nic) {
203
+ let addr = nic[ndx];
204
+ // All scope-local addresses will have a mac. In a multi-nic scenario we are simply grabbing
205
+ // the first one we come across.
206
+ if (!addr.internal && addr.mac.indexOf('00:00:00:') < 0 && addr.family === this.family) {
207
+ if (typeof ips.find((x) => x === addr.address) === 'undefined') {
208
+ ips.push(addr.address);
209
+ if (!addr.mac.startsWith('00:'))
210
+ nics.physical.push(extend(true, { name: name }, addr));
211
+ else
212
+ nics.virtual.push(extend(true, { name: name }, addr));
213
+ }
214
+ }
215
+ }
216
+ }
217
+ return nics;
181
218
  }
219
+ public ip() { return typeof this.getInterface() === 'undefined' ? '0.0.0.0' : this.getInterface().address; }
220
+ public mac() { return typeof this.getInterface() === 'undefined' ? '00:00:00:00' : this.getInterface().mac; }
221
+ public httpPort(): number { return this._httpPort }
182
222
  public findServer(name: string): ProtoServer { return this._servers.find(elem => elem.name === name); }
183
223
  public findServersByType(type: string) { return this._servers.filter(elem => elem.type === type); }
184
224
  public findServerByGuid(uuid: string) { return this._servers.find(elem => elem.uuid === uuid); }
@@ -221,7 +261,7 @@ export class WebServer {
221
261
  else
222
262
  logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))}`);
223
263
  // Lets wait a good 20 seconds before we auto-backup anything. Now that we are initialized let the OCP have its way with everything.
224
- setTimeout(() => { this.checkAutoBackup(); }, 20000);
264
+ setTimeoutSync(()=>{this.checkAutoBackup();}, 20000);
225
265
  }
226
266
  catch (err) { logger.error(`Error initializing auto-backup: ${err.message}`); }
227
267
  }
@@ -372,7 +412,7 @@ export class WebServer {
372
412
  if (this.autoBackup) {
373
413
  await this.pruneAutoBackups(bu.keepCount);
374
414
  let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
375
- setTimeout(async () => {
415
+ setTimeoutSync(async () => {
376
416
  try {
377
417
  await this.checkAutoBackup();
378
418
  } catch (err) { logger.error(`Error checking auto-backup: ${err.message}`); }
@@ -480,7 +520,7 @@ export class WebServer {
480
520
  }
481
521
  }
482
522
  stats.servers.push(ctx);
483
- if (!srv.isConnected) await utils.sleep(6000); // rem server waits to connect 5s before isConnected will be true. Server.ts#1256 = REMInterfaceServer.init(); What's a better way to do this?
523
+ if (!srv.isConnected) await setTimeout(6000); // rem server waits to connect 5s before isConnected will be true. Server.ts#1256 = REMInterfaceServer.init(); What's a better way to do this?
484
524
  if (typeof cfg === 'undefined' || typeof cfg.controllerConfig === 'undefined') ctx.server.errors.push(`Server configuration not found in zip file`);
485
525
  else if (typeof srv === 'undefined') ctx.server.errors.push(`Server ${s.name} is not enabled in njsPC cannot restore.`);
486
526
  else if (!srv.isConnected) ctx.server.errors.push(`Server ${s.name} is not connected cannot restore.`);
@@ -605,7 +645,7 @@ export class HttpServer extends ProtoServer {
605
645
  private socketHandler(sock: Socket) {
606
646
  let self = this;
607
647
  // this._sockets.push(sock);
608
- setTimeout(async () => {
648
+ setTimeoutSync(async () => {
609
649
  // refresh socket list with every new socket
610
650
  self._sockets = await self.sockServer.fetchSockets();
611
651
  }, 100)
@@ -618,42 +658,6 @@ export class HttpServer extends ProtoServer {
618
658
  self._sockets = await self.sockServer.fetchSockets();
619
659
  });
620
660
  sock.on('echo', (msg) => { sock.emit('echo', msg); });
621
- /* sock.on('receivePacketRaw', function (incomingPacket: any[]) {
622
- //var str = 'Add packet(s) to incoming buffer: ';
623
- logger.silly('User request (replay.html) to RECEIVE packet: %s', JSON.stringify(incomingPacket));
624
- for (var i = 0; i < incomingPacket.length; i++) {
625
- conn.buffer.pushIn(Buffer.from(incomingPacket[i]));
626
- // str += JSON.stringify(incomingPacket[i]) + ' ';
627
- }
628
- //logger.info(str);
629
- });
630
- sock.on('replayPackets', function (inboundPkts: number[][]) {
631
- // used for replay
632
- logger.debug(`Received replayPackets: ${inboundPkts}`);
633
- inboundPkts.forEach(inbound => {
634
- conn.buffer.pushIn(Buffer.from([].concat.apply([], inbound)));
635
- // conn.queueInboundMessage([].concat.apply([], inbound));
636
- });
637
- });
638
- sock.on('sendPackets', function (bytesToProcessArr: number[][]) {
639
- // takes an input of bytes (src/dest/action/payload) and sends
640
- if (!bytesToProcessArr.length) return;
641
- logger.silly('User request (replay.html) to SEND packet: %s', JSON.stringify(bytesToProcessArr));
642
-
643
- do {
644
- let bytesToProcess: number[] = bytesToProcessArr.shift();
645
-
646
- // todo: logic for chlor packets
647
- let out = Outbound.create({
648
- source: bytesToProcess.shift(),
649
- dest: bytesToProcess.shift(),
650
- action: bytesToProcess.shift(),
651
- payload: bytesToProcess.splice(1, bytesToProcess[0])
652
- });
653
- conn.queueSendMessage(out);
654
- } while (bytesToProcessArr.length > 0);
655
-
656
- }); */
657
661
  sock.on('sendOutboundMessage', (mdata) => {
658
662
  let msg: Outbound = Outbound.create({});
659
663
  Object.assign(msg, mdata);
@@ -682,11 +686,16 @@ export class HttpServer extends ProtoServer {
682
686
  if (!sendMessages) sock.leave('msgLogger');
683
687
  else sock.join('msgLogger');
684
688
  });
685
- sock.on('sendRS485PortStats', function (sendPortStatus: boolean) {
686
- console.log(`sendRS485PortStats set to ${sendPortStatus}`);
687
- if (!sendPortStatus) sock.leave('rs485PortStats');
689
+ sock.on('sendRS485PortStats', function (sendPortStats: boolean) {
690
+ console.log(`sendRS485PortStats set to ${sendPortStats}`);
691
+ if (!sendPortStats) sock.leave('rs485PortStats');
688
692
  else sock.join('rs485PortStats');
689
693
  });
694
+ sock.on('sendScreenlogicStats', function (sendScreenlogicStats: boolean) {
695
+ console.log(`sendScreenlogicStats set to ${sendScreenlogicStats}`);
696
+ if (!sendScreenlogicStats) sock.leave('screenlogicStats');
697
+ else sock.join('screenlogicStats');
698
+ });
690
699
  StateSocket.initSockets(sock);
691
700
  ConfigSocket.initSockets(sock);
692
701
  }
@@ -733,7 +742,7 @@ export class HttpServer extends ProtoServer {
733
742
  res.header('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, DELETE');
734
743
  if ('OPTIONS' === req.method) { res.sendStatus(200); }
735
744
  else {
736
- if (req.url !== '/device') {
745
+ if (req.url !== '/upnp.xml') {
737
746
  logger.info(`[${new Date().toLocaleTimeString()}] ${req.ip} ${req.method} ${req.url} ${typeof req.body === 'undefined' ? '' : JSON.stringify(req.body)}`);
738
747
  logger.logAPI(`{"dir":"in","proto":"api","requestor":"${req.ip}","method":"${req.method}","path":"${req.url}",${typeof req.body === 'undefined' ? '' : `"body":${JSON.stringify(req.body)},`}"ts":"${Timestamp.toISOLocal(new Date())}"}${os.EOL}`);
739
748
  }
@@ -773,7 +782,7 @@ export class HttpServer extends ProtoServer {
773
782
 
774
783
  // start our server on port
775
784
  this.server.listen(cfg.port, cfg.ip, function () {
776
- logger.info('Server is now listening on %s:%s', cfg.ip, cfg.port);
785
+ logger.info('Server is now listening on %s:%s - %s:%s', cfg.ip, cfg.port, webApp.ip(), webApp.httpPort());
777
786
  });
778
787
  this.isRunning = true;
779
788
  }
@@ -824,7 +833,7 @@ export class HttpsServer extends HttpServer {
824
833
  res.header('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, DELETE');
825
834
  if ('OPTIONS' === req.method) { res.sendStatus(200); }
826
835
  else {
827
- if (req.url !== '/device') {
836
+ if (!req.url.startsWith('/upnp.xml')) {
828
837
  logger.info(`[${new Date().toLocaleString()}] ${req.ip} ${req.method} ${req.url} ${typeof req.body === 'undefined' ? '' : JSON.stringify(req.body)}`);
829
838
  logger.logAPI(`{"dir":"in","proto":"api","requestor":"${req.ip}","method":"${req.method}","path":"${req.url}",${typeof req.body === 'undefined' ? '' : `"body":${JSON.stringify(req.body)},`}"ts":"${Timestamp.toISOLocal(new Date())}"}${os.EOL}`);
830
839
  }
@@ -875,27 +884,57 @@ export class HttpsServer extends HttpServer {
875
884
  }
876
885
  export class SsdpServer extends ProtoServer {
877
886
  // Simple service discovery protocol
878
- public server: any; //node-ssdp;
887
+ public server: ssdp.Server; //node-ssdp;
888
+ public deviceUUID: string;
889
+ public upnpPath: string;
890
+ public modelName: string;
891
+ public modelNumber: string;
892
+ public serialNumber: string;
893
+ public deviceType = 'urn:schemas-tagyoureit-org:device:PoolController:1';
879
894
  public async init(cfg) {
880
895
  this.uuid = cfg.uuid;
881
896
  if (cfg.enabled) {
882
897
  let self = this;
883
-
884
898
  logger.info('Starting up SSDP server');
885
- var udn = 'uuid:806f52f4-1f35-4e33-9299-' + webApp.mac();
899
+ let ver = JSON.parse(fs.readFileSync(path.posix.join(process.cwd(), '/package.json'), 'utf8')).version || '0.0.0';
900
+ this.deviceUUID = 'uuid:806f52f4-1f35-4e33-9299-' + webApp.mac().replace(/:/g, '');
901
+ this.serialNumber = webApp.mac();
902
+ this.modelName = `njsPC v${ver}`;
903
+ this.modelNumber = `njsPC${ver.replace(/\./g, '-')}`;
886
904
  // todo: should probably check if http/https is enabled at this point
887
- var port = config.getSection('web').servers.http.port || 4200;
888
- //console.log(port);
889
- let location = 'http://' + webApp.ip() + ':' + port + '/device';
890
- var SSDP = ssdp.Server;
891
- this.server = new SSDP({
892
- logLevel: 'INFO',
893
- udn: udn,
894
- location: location,
895
- sourcePort: 1900
896
- });
897
- this.server.addUSN('urn:schemas-upnp-org:device:PoolController:1');
905
+ //let port = config.getSection('web').servers.http.port || 7777;
906
+ this.upnpPath = 'http://' + webApp.ip() + ':' + webApp.httpPort() + '/upnp.xml';
907
+ let nics = webApp.getNetworkInterfaces();
908
+ let SSDP = ssdp.Server;
909
+ if (nics.physical.length + nics.virtual.length > 1) {
910
+ // If there are multiple nics (docker...etc) then
911
+ // this will bind on all of them.
912
+ this.server = new SSDP({
913
+ //customLogger: (...args) => console.log.apply(null, args),
914
+ logLevel: 'INFO',
915
+ udn: this.deviceUUID,
916
+ location: {
917
+ protocol: 'http://',
918
+ port: webApp.httpPort(),
919
+ path: '/upnp.xml'
920
+ },
921
+ explicitSocketBind: true,
922
+ sourcePort: 1900
923
+ });
924
+ }
925
+ else {
926
+ this.server = new SSDP({
927
+ //customLogger: (...args) => console.log.apply(null, args),
928
+ logLevel: 'INFO',
929
+ udn: this.deviceUUID,
930
+ location: this.upnpPath,
931
+ sourcePort: 1900
932
+ });
933
+
898
934
 
935
+ }
936
+ this.server.addUSN('upnp:rootdevice'); // This line will make the server show up in windows.
937
+ this.server.addUSN(this.deviceType);
899
938
  // start the server
900
939
  this.server.start()
901
940
  .then(function () {
@@ -908,26 +947,39 @@ export class SsdpServer extends ProtoServer {
908
947
  });
909
948
  }
910
949
  }
911
- public static deviceXML() {
912
- let ver = sys.appVersion;
950
+ public deviceXML(): string {
951
+ let ver = sys.appVersion.split('.');
952
+ let friendlyName = 'njsPC: unknown model';
953
+ if (typeof sys !== 'undefined' && typeof sys.equipment !== 'undefined' && typeof sys.equipment.model !== 'undefined') friendlyName = `${sys.equipment.model}`
913
954
  let XML = `<?xml version="1.0"?>
914
- <root xmlns="urn:schemas-upnp-org:PoolController-1-0">
915
- <specVersion>
916
- <major>${ver.split('.')[0]}</major>
917
- <minor>${ver.split('.')[1]}</minor>
918
- <patch>${ver.split('.')[2]}</patch>
919
- </specVersion>
920
- <device>
921
- <deviceType>urn:echo:device:PoolController:1</deviceType>
922
- <friendlyName>NodeJS Pool Controller</friendlyName>
923
- <manufacturer>tagyoureit</manufacturer>
924
- <manufacturerURL>https://github.com/tagyoureit/nodejs-poolController</manufacturerURL>
925
- <modelDescription>An application to control pool equipment.</modelDescription>
926
- <serialNumber>0</serialNumber>
927
- <UDN>uuid:806f52f4-1f35-4e33-9299-${webApp.mac()}</UDN>
928
- <serviceList></serviceList>
929
- </device>
930
- </root>`;
955
+ <root xmlns="urn:schemas-upnp-org:device-1-0">
956
+ <specVersion>
957
+ <major>1</major>
958
+ <minor>0</minor>
959
+ </specVersion>
960
+ <device>
961
+ <deviceType>${this.deviceType}</deviceType>
962
+ <friendlyName>${friendlyName}</friendlyName>
963
+ <manufacturer>tagyoureit</manufacturer>
964
+ <manufacturerURL>https://github.com/tagyoureit/nodejs-poolController</manufacturerURL>
965
+ <presentationURL>http://${webApp.ip()}:${webApp.httpPort()}/state/all</presentationURL>
966
+ <appVersion>
967
+ <major>${ver[0] || 1}</major>
968
+ <minor>${ver[1] || 0}</minor>
969
+ <patch>${ver[2] || 0}</patch>
970
+ </appVersion>
971
+ <modelName>${this.modelName}</modelName>
972
+ <modelNumber>${this.modelNumber}</modelNumber>
973
+ <modelDescription>An application to control pool equipment.</modelDescription>
974
+ <serialNumber>${this.serialNumber}</serialNumber>
975
+ <UDN>${this.deviceUUID}::${this.deviceType}</UDN>
976
+ <serviceList></serviceList>
977
+ <deviceList></deviceList>
978
+ </device>
979
+ </root>`;
980
+ //console.log(XML.match(/<device>[\s|\S]+<appVersion>[\s|\S]+<major>(\d+)<\/major>/)[1]);
981
+ //console.log(XML.match(/<device>[\s|\S]+<appVersion>[\s|\S]+<minor>(\d+)<\/minor>/)[1]);
982
+ //console.log(XML.match(/<device>[\s|\S]+<appVersion>[\s|\S]+<patch>(\d+)<\/patch>/)[1]);
931
983
  return XML;
932
984
  }
933
985
  public async stopAsync() {
@@ -977,22 +1029,29 @@ export class MdnsServer extends ProtoServer {
977
1029
  if (question.name === '_poolcontroller._tcp.local') {
978
1030
  logger.info(`received mdns query for nodejs_poolController`);
979
1031
  self.server.respond({
980
- answers: [{
981
- name: '_poolcontroller._tcp.local',
982
- type: 'A',
983
- ttl: 300,
984
- data: webApp.ip()
985
- },
986
- {
987
- name: 'api._poolcontroller._tcp.local',
988
- type: 'SRV',
989
- data: {
990
- port: '4200',
991
- target: '_poolcontroller._tcp.local',
992
- weight: 0,
993
- priority: 10
994
- }
995
- }]
1032
+ answers: [
1033
+ {
1034
+ name: '_poolcontroller._tcp.local',
1035
+ type: 'A',
1036
+ ttl: 300,
1037
+ data: webApp.ip()
1038
+ },
1039
+ {
1040
+ name: '_poolcontroller._tcp.local',
1041
+ type: 'SRV',
1042
+ data: {
1043
+ port: webApp.httpPort().toString(),
1044
+ target: '_poolcontroller._tcp.local',
1045
+ weight: 0,
1046
+ priority: 10
1047
+ }
1048
+ },
1049
+ {
1050
+ name: 'model',
1051
+ type: 'TXT',
1052
+ data: 'njsPC'
1053
+ },
1054
+ ]
996
1055
  });
997
1056
  }
998
1057
  });
@@ -1102,6 +1161,86 @@ export class HttpInterfaceServer extends ProtoServer {
1102
1161
  catch (err) { }
1103
1162
  }
1104
1163
  }
1164
+ export class RuleInterfaceServer extends ProtoServer {
1165
+ public bindingsPath: string;
1166
+ public bindings: RuleInterfaceBindings;
1167
+ private _fileTime: Date = new Date(0);
1168
+ private _isLoading: boolean = false;
1169
+ public async init(cfg) {
1170
+ this.uuid = cfg.uuid;
1171
+ if (cfg.enabled) {
1172
+ if (cfg.fileName && this.initBindings(cfg)) this.isRunning = true;
1173
+ }
1174
+ }
1175
+ public loadBindings(cfg): boolean {
1176
+ this._isLoading = true;
1177
+ if (fs.existsSync(this.bindingsPath)) {
1178
+ try {
1179
+ let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1180
+ let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1181
+ this.bindings = Object.assign<RuleInterfaceBindings, any>(new RuleInterfaceBindings(cfg), ext);
1182
+ this.isRunning = true;
1183
+ this._isLoading = false;
1184
+ const stats = fs.statSync(this.bindingsPath);
1185
+ this._fileTime = stats.mtime;
1186
+ return true;
1187
+ }
1188
+ catch (err) {
1189
+ logger.error(`Error reading interface bindings file: ${this.bindingsPath}. ${err}`);
1190
+ this.isRunning = false;
1191
+ this._isLoading = false;
1192
+ }
1193
+ }
1194
+ return false;
1195
+ }
1196
+ public initBindings(cfg): boolean {
1197
+ let self = this;
1198
+ try {
1199
+ this.bindingsPath = path.posix.join(process.cwd(), "/web/bindings") + '/' + cfg.fileName;
1200
+ let fileTime = new Date(0).valueOf();
1201
+ fs.watch(this.bindingsPath, (event, fileName) => {
1202
+ if (fileName && event === 'change') {
1203
+ if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
1204
+ const stats = fs.statSync(self.bindingsPath);
1205
+ if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
1206
+ self.loadBindings(cfg);
1207
+ logger.info(`Reloading ${cfg.name || ''} interface config: ${fileName}`);
1208
+ }
1209
+ });
1210
+ this.loadBindings(cfg);
1211
+ if (this.bindings.context.mdnsDiscovery) {
1212
+ let srv = webApp.mdnsServer;
1213
+ let qry = typeof this.bindings.context.mdnsDiscovery === 'string' ? { name: this.bindings.context.mdnsDiscovery, type: 'A' } : this.bindings.context.mdnsDiscovery;
1214
+ if (typeof srv !== 'undefined') {
1215
+ srv.queryMdns(qry);
1216
+ srv.mdnsEmitter.on('mdnsResponse', (response) => {
1217
+ let url: URL;
1218
+ url = new URL(response);
1219
+ this.bindings.context.options.host = url.host;
1220
+ this.bindings.context.options.port = url.port || 80;
1221
+ });
1222
+ }
1223
+ }
1224
+ return true;
1225
+ }
1226
+ catch (err) {
1227
+ logger.error(`Error initializing interface bindings: ${err}`);
1228
+ }
1229
+ return false;
1230
+ }
1231
+ public emitToClients(evt: string, ...data: any) {
1232
+ if (this.isRunning) {
1233
+ // Take the bindings and map them to the appropriate http GET, PUT, DELETE, and POST.
1234
+ this.bindings.bindEvent(evt, ...data);
1235
+ }
1236
+ }
1237
+ public async stopAsync() {
1238
+ try {
1239
+ logger.info(`${this.name} Interface Server Shut down`);
1240
+ }
1241
+ catch (err) { }
1242
+ }
1243
+ }
1105
1244
 
1106
1245
  export class InfluxInterfaceServer extends ProtoServer {
1107
1246
  public bindingsPath: string;
@@ -1163,10 +1302,9 @@ export class InfluxInterfaceServer extends ProtoServer {
1163
1302
  }
1164
1303
  }
1165
1304
  }
1166
-
1167
1305
  export class MqttInterfaceServer extends ProtoServer {
1168
1306
  public bindingsPath: string;
1169
- public bindings: HttpInterfaceBindings;
1307
+ public bindings: MqttInterfaceBindings;
1170
1308
  private _fileTime: Date = new Date(0);
1171
1309
  private _isLoading: boolean = false;
1172
1310
  public get isConnected() { return this.isRunning && this.bindings.events.length > 0; }
@@ -1182,7 +1320,20 @@ export class MqttInterfaceServer extends ProtoServer {
1182
1320
  try {
1183
1321
  let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1184
1322
  let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1185
- this.bindings = Object.assign<MqttInterfaceBindings, any>(new MqttInterfaceBindings(cfg), ext);
1323
+ if (this.bindings && this.bindings.client) {
1324
+ // RKS: 05-29-22 - This was actually orphaning the subscriptions and event processors. Instead of simply doing
1325
+ // an assign we ned to assign the underlying data and clear the old info out. The reload method takes care of the
1326
+ // bindings for us.
1327
+ (async () => {
1328
+ await this.bindings.reload(ext);
1329
+ })();
1330
+ }
1331
+ else {
1332
+ this.bindings = Object.assign<MqttInterfaceBindings, any>(new MqttInterfaceBindings(cfg), ext);
1333
+ (async () => {
1334
+ await this.bindings.initAsync();
1335
+ })();
1336
+ }
1186
1337
  this.isRunning = true;
1187
1338
  this._isLoading = false;
1188
1339
  const stats = fs.statSync(this.bindingsPath);
@@ -1227,7 +1378,8 @@ export class MqttInterfaceServer extends ProtoServer {
1227
1378
  }
1228
1379
  public async stopAsync() {
1229
1380
  try {
1230
- if (typeof this.bindings !== 'undefined') await this.bindings.stopAsync();
1381
+ fs.unwatchFile(this.bindingsPath);
1382
+ if (this.bindings) await this.bindings.stopAsync();
1231
1383
  } catch (err) { logger.error(`Error shutting down MQTT Server ${this.name}: ${err.message}`); }
1232
1384
  }
1233
1385
  }
@@ -1253,7 +1405,7 @@ export class REMInterfaceServer extends ProtoServer {
1253
1405
  this.uuid = cfg.uuid;
1254
1406
  if (cfg.enabled) {
1255
1407
  this.initSockets();
1256
- setTimeout(async () => {
1408
+ setTimeoutSync(async () => {
1257
1409
  try {
1258
1410
  await self.initConnection();
1259
1411
  }
@@ -1267,18 +1419,18 @@ export class REMInterfaceServer extends ProtoServer {
1267
1419
  try {
1268
1420
  let response = await this.sendClientRequest('GET', '/config/backup/controller', undefined, 10000);
1269
1421
  return response;
1270
- } catch (err) { logger.error(err); }
1422
+ } catch (err) { logger.error(`Error requesting GET /config/backup/controller: ${err.message}`); }
1271
1423
  }
1272
1424
  public async validateRestore(cfg): Promise<InterfaceServerResponse> {
1273
1425
  try {
1274
1426
  let response = await this.sendClientRequest('PUT', '/config/restore/validate', cfg, 10000);
1275
1427
  return response;
1276
- } catch (err) { logger.error(err); }
1428
+ } catch (err) { logger.error(`Error requesting PUT /config/restore/validate ${err.message}`); }
1277
1429
  }
1278
1430
  public async restoreConfig(cfg): Promise<InterfaceServerResponse> {
1279
1431
  try {
1280
1432
  return await this.sendClientRequest('PUT', '/config/restore/file', cfg, 20000);
1281
- } catch (err) { logger.error(err); }
1433
+ } catch (err) { logger.error(`Error requesting PUT /config/restore/file ${err.message}`); }
1282
1434
  }
1283
1435
  private async initConnection() {
1284
1436
  try {
@@ -1289,6 +1441,8 @@ export class REMInterfaceServer extends ProtoServer {
1289
1441
  let url = '/config/checkconnection/';
1290
1442
  // can & should extend for https/username-password/ssl
1291
1443
  let data: any = { type: "njspc", isActive: true, id: null, name: "njsPC - automatic", protocol: "http:", ipAddress: webApp.ip(), port: config.getSection('web').servers.http.port || 4200, userName: "", password: "", sslKeyFile: "", sslCertFile: "", hostnames: [] }
1444
+ if (typeof this.cfg.options !== 'undefined' && this.cfg.options.host !== 'undefined' &&
1445
+ this.cfg.options.host.toLowerCase() === 'localhost' || this.cfg.options.host === '127.0.0.1') data.loopback = true;
1292
1446
  logger.info(`Checking REM Connection ${data.name} ${data.ipAddress}:${data.port}`);
1293
1447
  try {
1294
1448
  data.hostnames = await dns.promises.reverse(data.ipAddress);
@@ -1306,15 +1460,15 @@ export class REMInterfaceServer extends ProtoServer {
1306
1460
  url = '/config/checkemit'
1307
1461
  data = { eventName: "checkemit", property: "result", value: 'success', connectionId: result.obj.id }
1308
1462
  // wait for REM server to finish resetting
1309
- setTimeout(async () => {
1463
+ setTimeoutSync(async () => {
1310
1464
  try {
1311
- let _tmr = setTimeout(() => { return reject(new Error(`initConnection: No socket response received. Check REM→njsPC communications.`)) }, 5000);
1465
+ let _tmr = setTimeoutSync(() => { return reject(new Error(`initConnection: No socket response received. Check REM→njsPC communications.`)) }, 5000);
1312
1466
  let srv: HttpServer = webApp.findServer('http') as HttpServer;
1313
1467
  srv.addListenerOnce('/checkemit', (data: any) => {
1314
1468
  // if we receive the emit, data will work both ways.
1315
1469
  // console.log(data);
1316
1470
  clearTimeout(_tmr);
1317
- logger.info(`REM bi-directional communications established.`)
1471
+ logger.info(`${this.name} bi-directional communications established.`)
1318
1472
  resolve();
1319
1473
  });
1320
1474
  result = await self.putApiService(url, data);
@@ -1446,11 +1600,11 @@ export class REMInterfaceServer extends ProtoServer {
1446
1600
  });
1447
1601
  this.isRunning = true;
1448
1602
  }
1449
- catch (err) { logger.error(err); }
1603
+ catch (err) { logger.error(`Error Initializing Sockets: ${err.message}`); }
1450
1604
  }
1451
1605
  private isJSONString(s: string): boolean {
1452
1606
  if (typeof s !== 'string') return false;
1453
- if (typeof s.startsWith('{') || typeof s.startsWith('[')) return true;
1607
+ if (s.startsWith('{') || s.startsWith('[')) return true;
1454
1608
  return false;
1455
1609
  }
1456
1610
  public async getApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
@@ -1475,7 +1629,11 @@ export class REMInterfaceServer extends ProtoServer {
1475
1629
  }
1476
1630
  public async getDevices() {
1477
1631
  try {
1478
- let response = await this.sendClientRequest('GET', '/devices/all', undefined, 10000);
1632
+ let response = await this.sendClientRequest('GET', '/devices/all', undefined, 3000);
1633
+ if (response.status.code !== 200) {
1634
+ // Let's try again. Sometimes the resolver for calls like this are stupid.
1635
+ response = await this.sendClientRequest('GET', '/devices/all', undefined, 10000);
1636
+ }
1479
1637
  return (response.status.code === 200) ? JSON.parse(response.data) : [];
1480
1638
  }
1481
1639
  catch (err) { logger.error(err); }