nodejs-poolcontroller 7.7.0 → 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 (82) hide show
  1. package/.eslintrc.json +26 -35
  2. package/Changelog +22 -0
  3. package/README.md +7 -3
  4. package/anslq25/MessagesMock.ts +218 -0
  5. package/anslq25/boards/MockBoardFactory.ts +50 -0
  6. package/anslq25/boards/MockEasyTouchBoard.ts +696 -0
  7. package/anslq25/boards/MockSystemBoard.ts +217 -0
  8. package/anslq25/chemistry/MockChlorinator.ts +75 -0
  9. package/anslq25/pumps/MockPump.ts +84 -0
  10. package/app.ts +10 -14
  11. package/config/Config.ts +13 -9
  12. package/config/VersionCheck.ts +6 -2
  13. package/controller/Constants.ts +58 -25
  14. package/controller/Equipment.ts +224 -41
  15. package/controller/Errors.ts +2 -1
  16. package/controller/Lockouts.ts +34 -2
  17. package/controller/State.ts +491 -48
  18. package/controller/boards/AquaLinkBoard.ts +6 -3
  19. package/controller/boards/BoardFactory.ts +5 -1
  20. package/controller/boards/EasyTouchBoard.ts +1971 -1751
  21. package/controller/boards/IntelliCenterBoard.ts +1311 -1688
  22. package/controller/boards/IntelliComBoard.ts +7 -1
  23. package/controller/boards/IntelliTouchBoard.ts +153 -42
  24. package/controller/boards/NixieBoard.ts +209 -66
  25. package/controller/boards/SunTouchBoard.ts +393 -0
  26. package/controller/boards/SystemBoard.ts +1862 -1543
  27. package/controller/comms/Comms.ts +539 -138
  28. package/controller/comms/ScreenLogic.ts +1663 -0
  29. package/controller/comms/messages/Messages.ts +242 -60
  30. package/controller/comms/messages/config/ChlorinatorMessage.ts +4 -3
  31. package/controller/comms/messages/config/CircuitGroupMessage.ts +5 -2
  32. package/controller/comms/messages/config/CircuitMessage.ts +81 -13
  33. package/controller/comms/messages/config/ConfigMessage.ts +3 -1
  34. package/controller/comms/messages/config/CoverMessage.ts +2 -1
  35. package/controller/comms/messages/config/CustomNameMessage.ts +2 -1
  36. package/controller/comms/messages/config/EquipmentMessage.ts +5 -1
  37. package/controller/comms/messages/config/ExternalMessage.ts +33 -3
  38. package/controller/comms/messages/config/FeatureMessage.ts +2 -1
  39. package/controller/comms/messages/config/GeneralMessage.ts +2 -1
  40. package/controller/comms/messages/config/HeaterMessage.ts +3 -1
  41. package/controller/comms/messages/config/IntellichemMessage.ts +2 -1
  42. package/controller/comms/messages/config/OptionsMessage.ts +12 -6
  43. package/controller/comms/messages/config/PumpMessage.ts +9 -12
  44. package/controller/comms/messages/config/RemoteMessage.ts +80 -13
  45. package/controller/comms/messages/config/ScheduleMessage.ts +43 -3
  46. package/controller/comms/messages/config/SecurityMessage.ts +2 -1
  47. package/controller/comms/messages/config/ValveMessage.ts +43 -26
  48. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +8 -7
  49. package/controller/comms/messages/status/EquipmentStateMessage.ts +93 -20
  50. package/controller/comms/messages/status/HeaterStateMessage.ts +24 -5
  51. package/controller/comms/messages/status/IntelliChemStateMessage.ts +7 -4
  52. package/controller/comms/messages/status/IntelliValveStateMessage.ts +2 -1
  53. package/controller/comms/messages/status/PumpStateMessage.ts +72 -4
  54. package/controller/comms/messages/status/VersionMessage.ts +2 -1
  55. package/controller/nixie/Nixie.ts +15 -4
  56. package/controller/nixie/NixieEquipment.ts +1 -0
  57. package/controller/nixie/chemistry/ChemController.ts +300 -129
  58. package/controller/nixie/chemistry/ChemDoser.ts +806 -0
  59. package/controller/nixie/chemistry/Chlorinator.ts +133 -129
  60. package/controller/nixie/circuits/Circuit.ts +171 -30
  61. package/controller/nixie/heaters/Heater.ts +337 -173
  62. package/controller/nixie/pumps/Pump.ts +264 -236
  63. package/controller/nixie/schedules/Schedule.ts +9 -3
  64. package/defaultConfig.json +45 -5
  65. package/logger/Logger.ts +38 -9
  66. package/package.json +13 -9
  67. package/web/Server.ts +235 -122
  68. package/web/bindings/aqualinkD.json +114 -59
  69. package/web/bindings/homeassistant.json +437 -0
  70. package/web/bindings/influxDB.json +15 -0
  71. package/web/bindings/mqtt.json +28 -9
  72. package/web/bindings/mqttAlt.json +15 -0
  73. package/web/interfaces/baseInterface.ts +58 -7
  74. package/web/interfaces/httpInterface.ts +5 -2
  75. package/web/interfaces/influxInterface.ts +9 -2
  76. package/web/interfaces/mqttInterface.ts +234 -74
  77. package/web/interfaces/ruleInterface.ts +87 -0
  78. package/web/services/config/Config.ts +140 -33
  79. package/web/services/config/ConfigSocket.ts +2 -1
  80. package/web/services/state/State.ts +144 -3
  81. package/web/services/state/StateSocket.ts +65 -14
  82. package/web/services/utilities/Utilities.ts +189 -1
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.
@@ -115,7 +118,13 @@ export class WebServer {
115
118
  int.init(c);
116
119
  this._servers.push(int);
117
120
  break;
121
+ case 'rule':
122
+ int = new RuleInterfaceServer(c.name, type);
123
+ int.init(c);
124
+ this._servers.push(int);
125
+ break;
118
126
  case 'influx':
127
+ case 'influxdb2':
119
128
  int = new InfluxInterfaceServer(c.name, type);
120
129
  int.init(c);
121
130
  this._servers.push(int);
@@ -165,6 +174,7 @@ export class WebServer {
165
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
166
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
167
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.
168
178
  for (let name in networkInterfaces) {
169
179
  let nic = networkInterfaces[name];
170
180
  for (let ndx in nic) {
@@ -172,10 +182,39 @@ export class WebServer {
172
182
  // All scope-local addresses will have a mac. In a multi-nic scenario we are simply grabbing
173
183
  // the first one we come across.
174
184
  if (!addr.internal && addr.mac.indexOf('00:00:00:') < 0 && addr.family === this.family) {
175
- return addr;
185
+ if (!addr.mac.startsWith('00:'))
186
+ return addr;
187
+ else if (typeof fallback === 'undefined') fallback = addr;
188
+ }
189
+ }
190
+ }
191
+ return fallback;
192
+ }
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
+ }
176
214
  }
177
215
  }
178
216
  }
217
+ return nics;
179
218
  }
180
219
  public ip() { return typeof this.getInterface() === 'undefined' ? '0.0.0.0' : this.getInterface().address; }
181
220
  public mac() { return typeof this.getInterface() === 'undefined' ? '00:00:00:00' : this.getInterface().mac; }
@@ -222,7 +261,7 @@ export class WebServer {
222
261
  else
223
262
  logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))}`);
224
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.
225
- setTimeout(() => { this.checkAutoBackup(); }, 20000);
264
+ setTimeoutSync(()=>{this.checkAutoBackup();}, 20000);
226
265
  }
227
266
  catch (err) { logger.error(`Error initializing auto-backup: ${err.message}`); }
228
267
  }
@@ -373,7 +412,7 @@ export class WebServer {
373
412
  if (this.autoBackup) {
374
413
  await this.pruneAutoBackups(bu.keepCount);
375
414
  let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
376
- setTimeout(async () => {
415
+ setTimeoutSync(async () => {
377
416
  try {
378
417
  await this.checkAutoBackup();
379
418
  } catch (err) { logger.error(`Error checking auto-backup: ${err.message}`); }
@@ -481,7 +520,7 @@ export class WebServer {
481
520
  }
482
521
  }
483
522
  stats.servers.push(ctx);
484
- 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?
485
524
  if (typeof cfg === 'undefined' || typeof cfg.controllerConfig === 'undefined') ctx.server.errors.push(`Server configuration not found in zip file`);
486
525
  else if (typeof srv === 'undefined') ctx.server.errors.push(`Server ${s.name} is not enabled in njsPC cannot restore.`);
487
526
  else if (!srv.isConnected) ctx.server.errors.push(`Server ${s.name} is not connected cannot restore.`);
@@ -606,7 +645,7 @@ export class HttpServer extends ProtoServer {
606
645
  private socketHandler(sock: Socket) {
607
646
  let self = this;
608
647
  // this._sockets.push(sock);
609
- setTimeout(async () => {
648
+ setTimeoutSync(async () => {
610
649
  // refresh socket list with every new socket
611
650
  self._sockets = await self.sockServer.fetchSockets();
612
651
  }, 100)
@@ -647,11 +686,16 @@ export class HttpServer extends ProtoServer {
647
686
  if (!sendMessages) sock.leave('msgLogger');
648
687
  else sock.join('msgLogger');
649
688
  });
650
- sock.on('sendRS485PortStats', function (sendPortStatus: boolean) {
651
- console.log(`sendRS485PortStats set to ${sendPortStatus}`);
652
- 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');
653
692
  else sock.join('rs485PortStats');
654
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
+ });
655
699
  StateSocket.initSockets(sock);
656
700
  ConfigSocket.initSockets(sock);
657
701
  }
@@ -738,7 +782,7 @@ export class HttpServer extends ProtoServer {
738
782
 
739
783
  // start our server on port
740
784
  this.server.listen(cfg.port, cfg.ip, function () {
741
- 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());
742
786
  });
743
787
  this.isRunning = true;
744
788
  }
@@ -789,7 +833,7 @@ export class HttpsServer extends HttpServer {
789
833
  res.header('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, DELETE');
790
834
  if ('OPTIONS' === req.method) { res.sendStatus(200); }
791
835
  else {
792
- if (req.url !== '/upnp.xml') {
836
+ if (!req.url.startsWith('/upnp.xml')) {
793
837
  logger.info(`[${new Date().toLocaleString()}] ${req.ip} ${req.method} ${req.url} ${typeof req.body === 'undefined' ? '' : JSON.stringify(req.body)}`);
794
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}`);
795
839
  }
@@ -840,7 +884,7 @@ export class HttpsServer extends HttpServer {
840
884
  }
841
885
  export class SsdpServer extends ProtoServer {
842
886
  // Simple service discovery protocol
843
- public server: any; //node-ssdp;
887
+ public server: ssdp.Server; //node-ssdp;
844
888
  public deviceUUID: string;
845
889
  public upnpPath: string;
846
890
  public modelName: string;
@@ -858,16 +902,37 @@ export class SsdpServer extends ProtoServer {
858
902
  this.modelName = `njsPC v${ver}`;
859
903
  this.modelNumber = `njsPC${ver.replace(/\./g, '-')}`;
860
904
  // todo: should probably check if http/https is enabled at this point
861
- let port = config.getSection('web').servers.http.port || 7777;
862
- this.upnpPath = 'http://' + webApp.ip() + ':' + port + '/upnp.xml';
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();
863
908
  let SSDP = ssdp.Server;
864
- this.server = new SSDP({
865
- //customLogger: (...args) => console.log.apply(null, args),
866
- logLevel: 'INFO',
867
- udn: this.deviceUUID,
868
- location: this.upnpPath,
869
- sourcePort: 1900
870
- });
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
+
934
+
935
+ }
871
936
  this.server.addUSN('upnp:rootdevice'); // This line will make the server show up in windows.
872
937
  this.server.addUSN(this.deviceType);
873
938
  // start the server
@@ -883,6 +948,9 @@ export class SsdpServer extends ProtoServer {
883
948
  }
884
949
  }
885
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}`
886
954
  let XML = `<?xml version="1.0"?>
887
955
  <root xmlns="urn:schemas-upnp-org:device-1-0">
888
956
  <specVersion>
@@ -891,10 +959,15 @@ export class SsdpServer extends ProtoServer {
891
959
  </specVersion>
892
960
  <device>
893
961
  <deviceType>${this.deviceType}</deviceType>
894
- <friendlyName>NodeJS Pool Controller</friendlyName>
962
+ <friendlyName>${friendlyName}</friendlyName>
895
963
  <manufacturer>tagyoureit</manufacturer>
896
964
  <manufacturerURL>https://github.com/tagyoureit/nodejs-poolController</manufacturerURL>
897
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>
898
971
  <modelName>${this.modelName}</modelName>
899
972
  <modelNumber>${this.modelNumber}</modelNumber>
900
973
  <modelDescription>An application to control pool equipment.</modelDescription>
@@ -904,6 +977,9 @@ export class SsdpServer extends ProtoServer {
904
977
  <deviceList></deviceList>
905
978
  </device>
906
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]);
907
983
  return XML;
908
984
  }
909
985
  public async stopAsync() {
@@ -915,77 +991,6 @@ export class SsdpServer extends ProtoServer {
915
991
  } catch (err) { logger.error(`Error stopping SSDP server ${err.message}`); }
916
992
  }
917
993
  }
918
- /* RKS DEPRECATED: 05-07-22 - This did not follow the upnp rules so it was not detecting properly. The entire class above emits the proper xml layout.
919
- export class SsdpServer1 extends ProtoServer {
920
- // Simple service discovery protocol
921
- public server: any; //node-ssdp;
922
- public async init(cfg) {
923
- this.uuid = cfg.uuid;
924
- if (cfg.enabled) {
925
- let self = this;
926
-
927
- logger.info('Starting up SSDP server');
928
- var udn = 'uuid:806f52f4-1f35-4e33-9299-' + webApp.mac();
929
- // todo: should probably check if http/https is enabled at this point
930
- var port = config.getSection('web').servers.http.port || 4200;
931
- //console.log(port);
932
- let location = 'http://' + webApp.ip() + ':' + port + '/device';
933
- var SSDP = ssdp.Server;
934
- this.server = new SSDP({
935
- logLevel: 'INFO',
936
- udn: udn,
937
- location: location,
938
- sourcePort: 1900
939
- });
940
- this.server.addUSN('urn:schemas-upnp-org:device:PoolController:1');
941
-
942
- // start the server
943
- this.server.start()
944
- .then(function () {
945
- logger.silly('SSDP/UPnP Server started.');
946
- self.isRunning = true;
947
- });
948
-
949
- this.server.on('error', function (e) {
950
- logger.error('error from SSDP:', e);
951
- });
952
- }
953
- }
954
- public static deviceXML() {
955
- let ver = sys.appVersion;
956
- let XML = `<?xml version="1.0"?>
957
- <root xmlns="urn:schemas-upnp-org:PoolController-1-0">
958
- <specVersion>
959
- <major>${ver.split('.')[0]}</major>
960
- <minor>${ver.split('.')[1]}</minor>
961
- <patch>${ver.split('.')[2]}</patch>
962
- </specVersion>
963
- <device>
964
- <deviceType>urn:echo:device:PoolController:1</deviceType>
965
- <friendlyName>NodeJS Pool Controller</friendlyName>
966
- <manufacturer>tagyoureit</manufacturer>
967
- <manufacturerURL>https://github.com/tagyoureit/nodejs-poolController</manufacturerURL>
968
- <modelDescription>An application to control pool equipment.</modelDescription>
969
- <serialNumber>0</serialNumber>
970
- <UDN>uuid:806f52f4-1f35-4e33-9299-${webApp.mac()}</UDN>
971
- <serviceList></serviceList>
972
- </device>
973
- </root>`;
974
- return XML;
975
- }
976
- public async stopAsync() {
977
- try {
978
- if (typeof this.server !== 'undefined') {
979
- this.server.stop();
980
- logger.info(`Stopped SSDP server: ${this.name}`);
981
- }
982
- } catch (err) { logger.error(`Error stopping SSDP server ${err.message}`); }
983
- }
984
- }
985
- */
986
-
987
-
988
-
989
994
  export class MdnsServer extends ProtoServer {
990
995
  // Multi-cast DNS server
991
996
  public server;
@@ -1024,22 +1029,29 @@ export class MdnsServer extends ProtoServer {
1024
1029
  if (question.name === '_poolcontroller._tcp.local') {
1025
1030
  logger.info(`received mdns query for nodejs_poolController`);
1026
1031
  self.server.respond({
1027
- answers: [{
1028
- name: '_poolcontroller._tcp.local',
1029
- type: 'A',
1030
- ttl: 300,
1031
- data: webApp.ip()
1032
- },
1033
- {
1034
- name: 'api._poolcontroller._tcp.local',
1035
- type: 'SRV',
1036
- data: {
1037
- port: '4200',
1038
- target: '_poolcontroller._tcp.local',
1039
- weight: 0,
1040
- priority: 10
1041
- }
1042
- }]
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
+ ]
1043
1055
  });
1044
1056
  }
1045
1057
  });
@@ -1149,6 +1161,87 @@ export class HttpInterfaceServer extends ProtoServer {
1149
1161
  catch (err) { }
1150
1162
  }
1151
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
+ }
1244
+
1152
1245
  export class InfluxInterfaceServer extends ProtoServer {
1153
1246
  public bindingsPath: string;
1154
1247
  public bindings: InfluxInterfaceBindings;
@@ -1211,7 +1304,7 @@ export class InfluxInterfaceServer extends ProtoServer {
1211
1304
  }
1212
1305
  export class MqttInterfaceServer extends ProtoServer {
1213
1306
  public bindingsPath: string;
1214
- public bindings: HttpInterfaceBindings;
1307
+ public bindings: MqttInterfaceBindings;
1215
1308
  private _fileTime: Date = new Date(0);
1216
1309
  private _isLoading: boolean = false;
1217
1310
  public get isConnected() { return this.isRunning && this.bindings.events.length > 0; }
@@ -1227,7 +1320,20 @@ export class MqttInterfaceServer extends ProtoServer {
1227
1320
  try {
1228
1321
  let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1229
1322
  let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1230
- 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
+ }
1231
1337
  this.isRunning = true;
1232
1338
  this._isLoading = false;
1233
1339
  const stats = fs.statSync(this.bindingsPath);
@@ -1272,7 +1378,8 @@ export class MqttInterfaceServer extends ProtoServer {
1272
1378
  }
1273
1379
  public async stopAsync() {
1274
1380
  try {
1275
- if (typeof this.bindings !== 'undefined') await this.bindings.stopAsync();
1381
+ fs.unwatchFile(this.bindingsPath);
1382
+ if (this.bindings) await this.bindings.stopAsync();
1276
1383
  } catch (err) { logger.error(`Error shutting down MQTT Server ${this.name}: ${err.message}`); }
1277
1384
  }
1278
1385
  }
@@ -1298,7 +1405,7 @@ export class REMInterfaceServer extends ProtoServer {
1298
1405
  this.uuid = cfg.uuid;
1299
1406
  if (cfg.enabled) {
1300
1407
  this.initSockets();
1301
- setTimeout(async () => {
1408
+ setTimeoutSync(async () => {
1302
1409
  try {
1303
1410
  await self.initConnection();
1304
1411
  }
@@ -1312,18 +1419,18 @@ export class REMInterfaceServer extends ProtoServer {
1312
1419
  try {
1313
1420
  let response = await this.sendClientRequest('GET', '/config/backup/controller', undefined, 10000);
1314
1421
  return response;
1315
- } catch (err) { logger.error(err); }
1422
+ } catch (err) { logger.error(`Error requesting GET /config/backup/controller: ${err.message}`); }
1316
1423
  }
1317
1424
  public async validateRestore(cfg): Promise<InterfaceServerResponse> {
1318
1425
  try {
1319
1426
  let response = await this.sendClientRequest('PUT', '/config/restore/validate', cfg, 10000);
1320
1427
  return response;
1321
- } catch (err) { logger.error(err); }
1428
+ } catch (err) { logger.error(`Error requesting PUT /config/restore/validate ${err.message}`); }
1322
1429
  }
1323
1430
  public async restoreConfig(cfg): Promise<InterfaceServerResponse> {
1324
1431
  try {
1325
1432
  return await this.sendClientRequest('PUT', '/config/restore/file', cfg, 20000);
1326
- } catch (err) { logger.error(err); }
1433
+ } catch (err) { logger.error(`Error requesting PUT /config/restore/file ${err.message}`); }
1327
1434
  }
1328
1435
  private async initConnection() {
1329
1436
  try {
@@ -1334,6 +1441,8 @@ export class REMInterfaceServer extends ProtoServer {
1334
1441
  let url = '/config/checkconnection/';
1335
1442
  // can & should extend for https/username-password/ssl
1336
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;
1337
1446
  logger.info(`Checking REM Connection ${data.name} ${data.ipAddress}:${data.port}`);
1338
1447
  try {
1339
1448
  data.hostnames = await dns.promises.reverse(data.ipAddress);
@@ -1351,15 +1460,15 @@ export class REMInterfaceServer extends ProtoServer {
1351
1460
  url = '/config/checkemit'
1352
1461
  data = { eventName: "checkemit", property: "result", value: 'success', connectionId: result.obj.id }
1353
1462
  // wait for REM server to finish resetting
1354
- setTimeout(async () => {
1463
+ setTimeoutSync(async () => {
1355
1464
  try {
1356
- 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);
1357
1466
  let srv: HttpServer = webApp.findServer('http') as HttpServer;
1358
1467
  srv.addListenerOnce('/checkemit', (data: any) => {
1359
1468
  // if we receive the emit, data will work both ways.
1360
1469
  // console.log(data);
1361
1470
  clearTimeout(_tmr);
1362
- logger.info(`REM bi-directional communications established.`)
1471
+ logger.info(`${this.name} bi-directional communications established.`)
1363
1472
  resolve();
1364
1473
  });
1365
1474
  result = await self.putApiService(url, data);
@@ -1491,7 +1600,7 @@ export class REMInterfaceServer extends ProtoServer {
1491
1600
  });
1492
1601
  this.isRunning = true;
1493
1602
  }
1494
- catch (err) { logger.error(err); }
1603
+ catch (err) { logger.error(`Error Initializing Sockets: ${err.message}`); }
1495
1604
  }
1496
1605
  private isJSONString(s: string): boolean {
1497
1606
  if (typeof s !== 'string') return false;
@@ -1520,7 +1629,11 @@ export class REMInterfaceServer extends ProtoServer {
1520
1629
  }
1521
1630
  public async getDevices() {
1522
1631
  try {
1523
- 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
+ }
1524
1637
  return (response.status.code === 200) ? JSON.parse(response.data) : [];
1525
1638
  }
1526
1639
  catch (err) { logger.error(err); }