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.
- package/.eslintrc.json +26 -35
- package/Changelog +22 -0
- package/README.md +7 -3
- package/anslq25/MessagesMock.ts +218 -0
- package/anslq25/boards/MockBoardFactory.ts +50 -0
- package/anslq25/boards/MockEasyTouchBoard.ts +696 -0
- package/anslq25/boards/MockSystemBoard.ts +217 -0
- package/anslq25/chemistry/MockChlorinator.ts +75 -0
- package/anslq25/pumps/MockPump.ts +84 -0
- package/app.ts +10 -14
- package/config/Config.ts +13 -9
- package/config/VersionCheck.ts +6 -2
- package/controller/Constants.ts +58 -25
- package/controller/Equipment.ts +224 -41
- package/controller/Errors.ts +2 -1
- package/controller/Lockouts.ts +34 -2
- package/controller/State.ts +491 -48
- package/controller/boards/AquaLinkBoard.ts +6 -3
- package/controller/boards/BoardFactory.ts +5 -1
- package/controller/boards/EasyTouchBoard.ts +1971 -1751
- package/controller/boards/IntelliCenterBoard.ts +1311 -1688
- package/controller/boards/IntelliComBoard.ts +7 -1
- package/controller/boards/IntelliTouchBoard.ts +153 -42
- package/controller/boards/NixieBoard.ts +209 -66
- package/controller/boards/SunTouchBoard.ts +393 -0
- package/controller/boards/SystemBoard.ts +1862 -1543
- package/controller/comms/Comms.ts +539 -138
- package/controller/comms/ScreenLogic.ts +1663 -0
- package/controller/comms/messages/Messages.ts +242 -60
- package/controller/comms/messages/config/ChlorinatorMessage.ts +4 -3
- package/controller/comms/messages/config/CircuitGroupMessage.ts +5 -2
- package/controller/comms/messages/config/CircuitMessage.ts +81 -13
- package/controller/comms/messages/config/ConfigMessage.ts +3 -1
- package/controller/comms/messages/config/CoverMessage.ts +2 -1
- package/controller/comms/messages/config/CustomNameMessage.ts +2 -1
- package/controller/comms/messages/config/EquipmentMessage.ts +5 -1
- package/controller/comms/messages/config/ExternalMessage.ts +33 -3
- package/controller/comms/messages/config/FeatureMessage.ts +2 -1
- package/controller/comms/messages/config/GeneralMessage.ts +2 -1
- package/controller/comms/messages/config/HeaterMessage.ts +3 -1
- package/controller/comms/messages/config/IntellichemMessage.ts +2 -1
- package/controller/comms/messages/config/OptionsMessage.ts +12 -6
- package/controller/comms/messages/config/PumpMessage.ts +9 -12
- package/controller/comms/messages/config/RemoteMessage.ts +80 -13
- package/controller/comms/messages/config/ScheduleMessage.ts +43 -3
- package/controller/comms/messages/config/SecurityMessage.ts +2 -1
- package/controller/comms/messages/config/ValveMessage.ts +43 -26
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +8 -7
- package/controller/comms/messages/status/EquipmentStateMessage.ts +93 -20
- package/controller/comms/messages/status/HeaterStateMessage.ts +24 -5
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +7 -4
- package/controller/comms/messages/status/IntelliValveStateMessage.ts +2 -1
- package/controller/comms/messages/status/PumpStateMessage.ts +72 -4
- package/controller/comms/messages/status/VersionMessage.ts +2 -1
- package/controller/nixie/Nixie.ts +15 -4
- package/controller/nixie/NixieEquipment.ts +1 -0
- package/controller/nixie/chemistry/ChemController.ts +300 -129
- package/controller/nixie/chemistry/ChemDoser.ts +806 -0
- package/controller/nixie/chemistry/Chlorinator.ts +133 -129
- package/controller/nixie/circuits/Circuit.ts +171 -30
- package/controller/nixie/heaters/Heater.ts +337 -173
- package/controller/nixie/pumps/Pump.ts +264 -236
- package/controller/nixie/schedules/Schedule.ts +9 -3
- package/defaultConfig.json +45 -5
- package/logger/Logger.ts +38 -9
- package/package.json +13 -9
- package/web/Server.ts +235 -122
- package/web/bindings/aqualinkD.json +114 -59
- package/web/bindings/homeassistant.json +437 -0
- package/web/bindings/influxDB.json +15 -0
- package/web/bindings/mqtt.json +28 -9
- package/web/bindings/mqttAlt.json +15 -0
- package/web/interfaces/baseInterface.ts +58 -7
- package/web/interfaces/httpInterface.ts +5 -2
- package/web/interfaces/influxInterface.ts +9 -2
- package/web/interfaces/mqttInterface.ts +234 -74
- package/web/interfaces/ruleInterface.ts +87 -0
- package/web/services/config/Config.ts +140 -33
- package/web/services/config/ConfigSocket.ts +2 -1
- package/web/services/state/State.ts +144 -3
- package/web/services/state/StateSocket.ts +65 -14
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
651
|
-
console.log(`sendRS485PortStats set to ${
|
|
652
|
-
if (!
|
|
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
|
|
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:
|
|
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() + ':' +
|
|
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
|
-
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
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
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1463
|
+
setTimeoutSync(async () => {
|
|
1355
1464
|
try {
|
|
1356
|
-
let _tmr =
|
|
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(
|
|
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,
|
|
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); }
|