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.
- package/.eslintrc.json +36 -45
- package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -0
- package/.github/ISSUE_TEMPLATE/2-docs.md +12 -0
- package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/CONTRIBUTING.md +74 -74
- package/Changelog +242 -215
- package/Dockerfile +17 -17
- package/Gruntfile.js +40 -40
- package/LICENSE +661 -661
- package/README.md +195 -191
- 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 +26 -8
- package/config/VersionCheck.ts +8 -4
- package/controller/Constants.ts +59 -25
- package/controller/Equipment.ts +2667 -2459
- package/controller/Errors.ts +181 -180
- package/controller/Lockouts.ts +534 -436
- package/controller/State.ts +596 -77
- package/controller/boards/AquaLinkBoard.ts +1003 -0
- package/controller/boards/BoardFactory.ts +53 -45
- package/controller/boards/EasyTouchBoard.ts +3079 -2653
- package/controller/boards/IntelliCenterBoard.ts +3821 -4230
- package/controller/boards/IntelliComBoard.ts +69 -63
- package/controller/boards/IntelliTouchBoard.ts +384 -241
- package/controller/boards/NixieBoard.ts +1871 -1675
- package/controller/boards/SunTouchBoard.ts +393 -0
- package/controller/boards/SystemBoard.ts +5244 -4697
- package/controller/comms/Comms.ts +905 -541
- package/controller/comms/ScreenLogic.ts +1663 -0
- package/controller/comms/messages/Messages.ts +382 -54
- package/controller/comms/messages/config/ChlorinatorMessage.ts +8 -4
- package/controller/comms/messages/config/CircuitGroupMessage.ts +5 -2
- package/controller/comms/messages/config/CircuitMessage.ts +82 -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 +31 -30
- 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 +145 -11
- package/controller/comms/messages/config/IntellichemMessage.ts +2 -1
- package/controller/comms/messages/config/OptionsMessage.ts +16 -27
- package/controller/comms/messages/config/PumpMessage.ts +62 -47
- package/controller/comms/messages/config/RemoteMessage.ts +80 -13
- package/controller/comms/messages/config/ScheduleMessage.ts +390 -347
- package/controller/comms/messages/config/SecurityMessage.ts +2 -1
- package/controller/comms/messages/config/ValveMessage.ts +44 -27
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +44 -91
- package/controller/comms/messages/status/EquipmentStateMessage.ts +139 -30
- package/controller/comms/messages/status/HeaterStateMessage.ts +135 -86
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -445
- package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -35
- package/controller/comms/messages/status/PumpStateMessage.ts +92 -2
- package/controller/comms/messages/status/VersionMessage.ts +2 -1
- package/controller/nixie/Nixie.ts +173 -162
- package/controller/nixie/NixieEquipment.ts +104 -103
- package/controller/nixie/bodies/Body.ts +120 -120
- package/controller/nixie/bodies/Filter.ts +135 -135
- package/controller/nixie/chemistry/ChemController.ts +2682 -2498
- package/controller/nixie/chemistry/ChemDoser.ts +806 -0
- package/controller/nixie/chemistry/Chlorinator.ts +367 -314
- package/controller/nixie/circuits/Circuit.ts +402 -248
- package/controller/nixie/heaters/Heater.ts +815 -649
- package/controller/nixie/pumps/Pump.ts +934 -661
- package/controller/nixie/schedules/Schedule.ts +319 -257
- package/controller/nixie/valves/Valve.ts +170 -170
- package/defaultConfig.json +346 -286
- package/logger/DataLogger.ts +448 -448
- package/logger/Logger.ts +38 -9
- package/package.json +60 -56
- package/tsconfig.json +25 -25
- package/web/Server.ts +275 -117
- package/web/bindings/aqualinkD.json +560 -0
- package/web/bindings/homeassistant.json +437 -0
- package/web/bindings/influxDB.json +1066 -1021
- package/web/bindings/mqtt.json +721 -654
- package/web/bindings/mqttAlt.json +746 -684
- package/web/bindings/rulesManager.json +54 -54
- package/web/bindings/smartThings-Hubitat.json +31 -31
- package/web/bindings/valveRelays.json +20 -20
- package/web/bindings/vera.json +25 -25
- package/web/interfaces/baseInterface.ts +188 -136
- package/web/interfaces/httpInterface.ts +148 -124
- package/web/interfaces/influxInterface.ts +283 -245
- package/web/interfaces/mqttInterface.ts +695 -475
- package/web/interfaces/ruleInterface.ts +87 -0
- package/web/services/config/Config.ts +177 -49
- package/web/services/config/ConfigSocket.ts +2 -1
- package/web/services/state/State.ts +154 -3
- package/web/services/state/StateSocket.ts +69 -18
- package/web/services/utilities/Utilities.ts +232 -42
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
- package/config copy.json +0 -300
- 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
|
|
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
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
686
|
-
console.log(`sendRS485PortStats set to ${
|
|
687
|
-
if (!
|
|
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 !== '/
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
let
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1463
|
+
setTimeoutSync(async () => {
|
|
1310
1464
|
try {
|
|
1311
|
-
let _tmr =
|
|
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(
|
|
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 (
|
|
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,
|
|
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); }
|