nodejs-poolcontroller 7.3.1 → 7.6.1

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 (85) hide show
  1. package/.eslintrc.json +44 -44
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +52 -52
  3. package/CONTRIBUTING.md +74 -74
  4. package/Changelog +215 -195
  5. package/Dockerfile +17 -17
  6. package/Gruntfile.js +40 -40
  7. package/LICENSE +661 -661
  8. package/README.md +191 -186
  9. package/app.ts +2 -0
  10. package/config/Config.ts +27 -2
  11. package/config/VersionCheck.ts +33 -14
  12. package/config copy.json +299 -299
  13. package/controller/Constants.ts +88 -0
  14. package/controller/Equipment.ts +2459 -2225
  15. package/controller/Errors.ts +180 -157
  16. package/controller/Lockouts.ts +437 -0
  17. package/controller/State.ts +364 -79
  18. package/controller/boards/BoardFactory.ts +45 -45
  19. package/controller/boards/EasyTouchBoard.ts +2653 -2489
  20. package/controller/boards/IntelliCenterBoard.ts +4230 -3973
  21. package/controller/boards/IntelliComBoard.ts +63 -63
  22. package/controller/boards/IntelliTouchBoard.ts +241 -167
  23. package/controller/boards/NixieBoard.ts +1675 -1105
  24. package/controller/boards/SystemBoard.ts +4697 -3201
  25. package/controller/comms/Comms.ts +222 -10
  26. package/controller/comms/messages/Messages.ts +13 -9
  27. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -4
  28. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  29. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  30. package/controller/comms/messages/config/ConfigMessage.ts +0 -0
  31. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  32. package/controller/comms/messages/config/CustomNameMessage.ts +30 -30
  33. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  34. package/controller/comms/messages/config/ExternalMessage.ts +53 -33
  35. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  36. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  37. package/controller/comms/messages/config/HeaterMessage.ts +14 -28
  38. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  39. package/controller/comms/messages/config/OptionsMessage.ts +38 -2
  40. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  41. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  42. package/controller/comms/messages/config/ScheduleMessage.ts +347 -331
  43. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  44. package/controller/comms/messages/config/ValveMessage.ts +13 -3
  45. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
  46. package/controller/comms/messages/status/EquipmentStateMessage.ts +79 -25
  47. package/controller/comms/messages/status/HeaterStateMessage.ts +86 -53
  48. package/controller/comms/messages/status/IntelliChemStateMessage.ts +445 -386
  49. package/controller/comms/messages/status/IntelliValveStateMessage.ts +35 -35
  50. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  51. package/controller/comms/messages/status/VersionMessage.ts +0 -0
  52. package/controller/nixie/Nixie.ts +162 -160
  53. package/controller/nixie/NixieEquipment.ts +103 -103
  54. package/controller/nixie/bodies/Body.ts +120 -117
  55. package/controller/nixie/bodies/Filter.ts +135 -135
  56. package/controller/nixie/chemistry/ChemController.ts +2498 -2395
  57. package/controller/nixie/chemistry/Chlorinator.ts +314 -313
  58. package/controller/nixie/circuits/Circuit.ts +248 -210
  59. package/controller/nixie/heaters/Heater.ts +649 -441
  60. package/controller/nixie/pumps/Pump.ts +661 -599
  61. package/controller/nixie/schedules/Schedule.ts +257 -256
  62. package/controller/nixie/valves/Valve.ts +170 -170
  63. package/defaultConfig.json +286 -271
  64. package/issue_template.md +51 -51
  65. package/logger/DataLogger.ts +448 -433
  66. package/logger/Logger.ts +0 -0
  67. package/package.json +56 -54
  68. package/tsconfig.json +25 -25
  69. package/web/Server.ts +522 -31
  70. package/web/bindings/influxDB.json +1022 -894
  71. package/web/bindings/mqtt.json +654 -543
  72. package/web/bindings/mqttAlt.json +684 -574
  73. package/web/bindings/rulesManager.json +54 -54
  74. package/web/bindings/smartThings-Hubitat.json +31 -31
  75. package/web/bindings/valveRelays.json +20 -20
  76. package/web/bindings/vera.json +25 -25
  77. package/web/interfaces/baseInterface.ts +136 -136
  78. package/web/interfaces/httpInterface.ts +124 -122
  79. package/web/interfaces/influxInterface.ts +245 -240
  80. package/web/interfaces/mqttInterface.ts +475 -464
  81. package/web/services/config/Config.ts +181 -152
  82. package/web/services/config/ConfigSocket.ts +0 -0
  83. package/web/services/state/State.ts +118 -7
  84. package/web/services/state/StateSocket.ts +18 -1
  85. package/web/services/utilities/Utilities.ts +42 -42
package/web/Server.ts CHANGED
@@ -14,43 +14,46 @@ GNU Affero General Public License for more details.
14
14
  You should have received a copy of the GNU Affero General Public License
15
15
  along with this program. If not, see <http://www.gnu.org/licenses/>.
16
16
  */
17
- import * as path from "path";
17
+ import * as dns from "dns";
18
+ import { EventEmitter } from 'events';
18
19
  import * as fs from "fs";
19
- import express = require('express');
20
- import { utils } from "../controller/Constants";
21
- import { config } from "../config/Config";
22
- import { logger } from "../logger/Logger";
23
- import { Namespace, RemoteSocket, Server as SocketIoServer, Socket } from "socket.io";
24
- import { io as sockClient } from "socket.io-client";
25
- import { ConfigRoute } from "./services/config/Config";
26
- import { StateRoute } from "./services/state/State";
27
- import { StateSocket } from "./services/state/StateSocket";
28
- import { UtilitiesRoute } from "./services/utilities/Utilities";
29
- import * as http2 from "http2";
30
20
  import * as http from "http";
21
+ import * as http2 from "http2";
31
22
  import * as https from "https";
32
- import { state } from "../controller/State";
33
- import { conn } from "../controller/comms/Comms";
34
- import { Inbound, Outbound } from "../controller/comms/messages/Messages";
35
- import { EventEmitter } from 'events';
36
- import { sys } from '../controller/Equipment';
37
23
  import * as multicastdns from 'multicast-dns';
38
24
  import * as ssdp from 'node-ssdp';
39
25
  import * as os from 'os';
26
+ import * as path from "path";
27
+ import { RemoteSocket, Server as SocketIoServer, Socket } from "socket.io";
28
+ import { io as sockClient } from "socket.io-client";
40
29
  import { URL } from "url";
30
+ import { config } from "../config/Config";
31
+ import { conn } from "../controller/comms/Comms";
32
+ import { Inbound, Outbound } from "../controller/comms/messages/Messages";
33
+ import { Timestamp, utils } from "../controller/Constants";
34
+ import { sys } from '../controller/Equipment';
35
+ import { state } from "../controller/State";
36
+ import { logger } from "../logger/Logger";
41
37
  import { HttpInterfaceBindings } from './interfaces/httpInterface';
42
38
  import { InfluxInterfaceBindings } from './interfaces/influxInterface';
43
39
  import { MqttInterfaceBindings } from './interfaces/mqttInterface';
44
- import { Timestamp } from '../controller/Constants';
45
- import extend = require("extend");
40
+ import { ConfigRoute } from "./services/config/Config";
46
41
  import { ConfigSocket } from "./services/config/ConfigSocket";
42
+ import { StateRoute } from "./services/state/State";
43
+ import { StateSocket } from "./services/state/StateSocket";
44
+ import { UtilitiesRoute } from "./services/utilities/Utilities";
45
+ import express = require('express');
46
+ import extend = require("extend");
47
47
 
48
48
 
49
49
  // This class serves data and pages for
50
50
  // external interfaces as well as an internal dashboard.
51
51
  export class WebServer {
52
+ public autoBackup = false;
53
+ public lastBackup;
52
54
  private _servers: ProtoServer[] = [];
53
55
  private family = 'IPv4';
56
+ private _autoBackupTimer: NodeJS.Timeout;
54
57
  constructor() { }
55
58
  public async init() {
56
59
  try {
@@ -86,6 +89,7 @@ export class WebServer {
86
89
  }
87
90
  }
88
91
  this.initInterfaces(cfg.interfaces);
92
+
89
93
  } catch (err) { logger.error(`Error initializing web server ${err.message}`) }
90
94
  }
91
95
  public async initInterfaces(interfaces: any) {
@@ -101,6 +105,7 @@ export class WebServer {
101
105
  let type = c.type || 'http';
102
106
  logger.info(`Init ${type} interface: ${c.name}`);
103
107
  switch (type) {
108
+ case 'rest':
104
109
  case 'http':
105
110
  int = new HttpInterfaceServer(c.name, type);
106
111
  int.init(c);
@@ -182,7 +187,7 @@ export class WebServer {
182
187
  if (this._servers[i].uuid === uuid) this._servers.splice(i, 1);
183
188
  }
184
189
  }
185
- public async updateServerInterface(obj: any) {
190
+ public async updateServerInterface(obj: any): Promise<any> {
186
191
  let int = config.setInterface(obj);
187
192
  let srv = this.findServerByGuid(obj.uuid);
188
193
  // if server is not enabled; stop & remove it from local storage
@@ -197,6 +202,321 @@ export class WebServer {
197
202
  }
198
203
  else srv.init(obj);
199
204
  }
205
+ return config.getInterfaceByUuid(obj.uuid);
206
+ }
207
+ public async initAutoBackup() {
208
+ try {
209
+ let bu = config.getSection('controller.backups');
210
+ this.autoBackup = false;
211
+ // These will be returned in reverse order with the newest backup first.
212
+ let files = await this.readBackupFiles();
213
+ let afiles = files.filter(elem => elem.options.automatic === true);
214
+ this.lastBackup = (afiles.length > 0) ? Date.parse(afiles[0].options.backupDate).valueOf() || 0 : 0;
215
+ // Set the last backup date.
216
+ this.autoBackup = utils.makeBool(bu.automatic);
217
+ if (this.autoBackup) {
218
+ let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
219
+ logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))} Next Backup: ${Timestamp.toISOLocal(new Date(nextBackup))}`);
220
+ }
221
+ else
222
+ logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))}`);
223
+ // Lets wait a good 20 seconds before we auto-backup anything. Now that we are initialized let the OCP have its way with everything.
224
+ setTimeout(() => { this.checkAutoBackup(); }, 20000);
225
+ }
226
+ catch (err) { logger.error(`Error initializing auto-backup: ${err.message}`); }
227
+ }
228
+ public async stopAutoBackup() {
229
+ this.autoBackup = false;
230
+ if (typeof this._autoBackupTimer !== 'undefined' || this._autoBackupTimer) clearTimeout(this._autoBackupTimer);
231
+ }
232
+ public async readBackupFiles(): Promise<BackupFile[]> {
233
+ try {
234
+ let backupDir = path.join(process.cwd(), 'backups');
235
+ let files = fs.readdirSync(backupDir);
236
+ let backups = [];
237
+ if (typeof files !== 'undefined') {
238
+ for (let i = 0; i < files.length; i++) {
239
+ let file = files[i];
240
+ if (path.extname(file) === '.zip') {
241
+ let bf = await BackupFile.fromFile(path.join(backupDir, file));
242
+ if (typeof bf !== 'undefined') backups.push(bf);
243
+ }
244
+ }
245
+ }
246
+ backups.sort((a, b) => { return Date.parse(b.options.backupDate) - Date.parse(a.options.backupDate) });
247
+ return backups;
248
+ }
249
+ catch (err) { logger.error(`Error reading backup file directory: ${err.message}`); }
250
+ }
251
+ protected async extractBackupOptions(file: string | Buffer): Promise<{ file: string, options: any }> {
252
+ try {
253
+ let opts = { file: Buffer.isBuffer(file) ? 'Buffer' : file, options: {} as any };
254
+ let jszip = require("jszip");
255
+ let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
256
+ await jszip.loadAsync(buff).then(async (zip) => {
257
+ await zip.file('options.json').async('string').then((data) => {
258
+ opts.options = JSON.parse(data);
259
+ if (typeof opts.options.backupDate === 'undefined' && typeof file === 'string') {
260
+ let name = path.parse(file).name;
261
+ if (name.length === 19) {
262
+ let date = name.substring(0, 10).replace(/-/g, '/');
263
+ let time = name.substring(11).replace(/-/g, ':');
264
+ let dt = Date.parse(`${date} ${time}`);
265
+ if (!isNaN(dt)) opts.options.backupDate = Timestamp.toISOLocal(new Date(dt));
266
+ }
267
+ }
268
+ });
269
+ });
270
+ return opts;
271
+ } catch (err) { logger.error(`Error extracting backup options from ${file}: ${err.message}`); }
272
+ }
273
+ public async pruneAutoBackups(keepCount: number) {
274
+ try {
275
+ // We only automatically prune backups that njsPC put there in the first place so only
276
+ // look at auto-backup files.
277
+ let files = await this.readBackupFiles();
278
+ let afiles = files.filter(elem => elem.options.automatic === true);
279
+ if (afiles.length > keepCount) {
280
+ // Prune off the oldest backups until we get to our keep count. When we read in the files
281
+ // these were sorted newest first.
282
+ while (afiles.length > keepCount) {
283
+ let afile = afiles.pop();
284
+ logger.info(`Pruning auto-backup file: ${afile.filePath}`);
285
+ try {
286
+ fs.unlinkSync(afile.filePath);
287
+ } catch (err) { logger.error(`Error deleting auto-backup file: ${afile.filePath}`); }
288
+ }
289
+ }
290
+ } catch (err) { logger.error(`Error pruning auto-backups: ${err.message}`); }
291
+ }
292
+ public async backupServer(opts: any): Promise<BackupFile> {
293
+ let ret = new BackupFile();
294
+ ret.options = extend(true, {}, opts, { version: 1.1, errors: [] });
295
+ //{ file: '', options: extend(true, {}, opts, { version: 1.0, errors: [] }) };
296
+ let jszip = require("jszip");
297
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
298
+ let zip = new jszip();
299
+ let ts = new Date();
300
+ let baseDir = process.cwd();
301
+ ret.filename = ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds()) + '.zip';
302
+ ret.filePath = path.join(baseDir, 'backups', ret.filename);
303
+ if (opts.njsPC === true) {
304
+ zip.folder('njsPC');
305
+ zip.folder('njsPC/data');
306
+ // Create the backup file and copy it into it.
307
+ zip.file('njsPC/config.json', fs.readFileSync(path.join(baseDir, 'config.json')));
308
+ zip.file('njsPC/data/poolConfig.json', fs.readFileSync(path.join(baseDir, 'data', 'poolConfig.json')));
309
+ zip.file('njsPC/data/poolState.json', fs.readFileSync(path.join(baseDir, 'data', 'poolState.json')));
310
+ }
311
+ if (typeof ret.options.servers !== 'undefined' && ret.options.servers.length > 0) {
312
+ // Back up all our servers.
313
+ for (let i = 0; i < ret.options.servers.length; i++) {
314
+ let srv = ret.options.servers[i];
315
+ if (typeof srv.errors === 'undefined') srv.errors = [];
316
+ if (srv.backup === false) continue;
317
+ let server = this.findServerByGuid(srv.uuid) as REMInterfaceServer;
318
+ if (typeof server === 'undefined') {
319
+ srv.errors.push(`Could not find server ${srv.name} : ${srv.uuid}`);
320
+ srv.success = false;
321
+ }
322
+ else if (!server.isConnected) {
323
+ srv.success = false;
324
+ srv.errors.push(`Server ${srv.name} : ${srv.uuid} not connected cannot back up`);
325
+ }
326
+ else {
327
+ // Try to get the data from the server.
328
+ zip.folder(server.name);
329
+ zip.file(`${server.name}/serverConfig.json`, JSON.stringify(server.cfg));
330
+ zip.folder(`${server.name}/data`);
331
+ try {
332
+ let resp = await server.getControllerConfig();
333
+ if (typeof resp !== 'undefined') {
334
+ if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
335
+ let ccfg = JSON.parse(resp.data);
336
+ zip.file(`${server.name}/data/controllerConfig.json`, JSON.stringify(ccfg));
337
+ srv.success = true;
338
+ }
339
+ else {
340
+ srv.errors.push(`Error getting controller configuration: ${resp.error.message}`);
341
+ srv.success = false;
342
+ }
343
+ }
344
+ else {
345
+ srv.success = false;
346
+ srv.errors.push(`No response from server`);
347
+ }
348
+ } catch (err) { srv.success = false; srv.errors.push(`Could not obtain server configuration`); }
349
+ }
350
+ }
351
+ }
352
+ ret.options.backupDate = Timestamp.toISOLocal(ts);
353
+ zip.file('options.json', JSON.stringify(ret.options));
354
+ await zip.generateAsync({ type: 'nodebuffer' }).then(content => {
355
+ fs.writeFileSync(ret.filePath, content);
356
+ this.lastBackup = ts.valueOf();
357
+ });
358
+ return ret;
359
+ }
360
+ public async checkAutoBackup() {
361
+ if (typeof this._autoBackupTimer !== 'undefined' || this._autoBackupTimer) clearTimeout(this._autoBackupTimer);
362
+ this._autoBackupTimer = undefined;
363
+ let bu = config.getSection('controller.backups');
364
+ if (bu.automatic === true) {
365
+ if (typeof this.lastBackup === 'undefined' ||
366
+ (this.lastBackup < new Date().valueOf() - (bu.interval.days * 86400000) - (bu.interval.hours * 3600000))) {
367
+ bu.name = 'Automatic Backup';
368
+ await this.backupServer(bu);
369
+ }
370
+ }
371
+ else this.autoBackup = false;
372
+ if (this.autoBackup) {
373
+ await this.pruneAutoBackups(bu.keepCount);
374
+ let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
375
+ setTimeout(async () => {
376
+ try {
377
+ await this.checkAutoBackup();
378
+ } catch (err) { logger.error(`Error checking auto-backup: ${err.message}`); }
379
+ }, Math.max(Math.min(nextBackup - new Date().valueOf(), 2147483647), 60000));
380
+ logger.info(`Last auto-backup ${Timestamp.toISOLocal(new Date(this.lastBackup))} Next auto - backup ${Timestamp.toISOLocal(new Date(nextBackup))}`);
381
+ }
382
+ }
383
+ public async validateRestore(opts): Promise<any> {
384
+ try {
385
+ let stats = { njsPC: {}, servers: [] };
386
+ // Step 1: Extract all the files from the zip file.
387
+ let rest = await RestoreFile.fromFile(opts.filePath);
388
+ // Step 2: Validate the njsPC data against the board. The return
389
+ // from here shoudld give a very detailed view of what it is about to do.
390
+ if (opts.options.njsPC === true) {
391
+ stats.njsPC = await sys.board.system.validateRestore(rest.njsPC);
392
+ }
393
+ // Step 3: For each REM server we need to validate the restore
394
+ // file.
395
+ if (typeof opts.options.servers !== 'undefined' && opts.options.servers.length > 0) {
396
+ for (let i = 0; i < opts.options.servers.length; i++) {
397
+ let s = opts.options.servers[i];
398
+ if (s.restore) {
399
+ let ctx: any = { server: { uuid: s.uuid, name: s.name, errors: [], warnings: [] } };
400
+ // Check to see if the server is on-line.
401
+ // First, try by UUID.
402
+ let srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
403
+ let cfg = rest.servers.find(elem => elem.uuid === s.uuid);
404
+ // Second, try by host
405
+ if (typeof srv === 'undefined' && parseFloat(opts.options.version) >= 1.1) {
406
+ let srvs = this.findServersByType('rem') as REMInterfaceServer[];
407
+ cfg = rest.servers.find(elem => elem.serverConfig.options.host === s.host);
408
+ for (let j = 0; j < srvs.length; j++){
409
+ if (srvs[j].cfg.options.host === cfg.serverConfig.options.host){
410
+ srv = srvs[j];
411
+ ctx.server.warnings.push(`REM Server from backup file (${srv.uuid}/${srv.cfg.options.host}) matched to current REM Server (${cfg.uuid}/${cfg.serverConfig.options.host}) by host name or IP and not UUID. UUID in current config.json for REM will be updated.`)
412
+ break;
413
+ }
414
+ }
415
+ }
416
+ stats.servers.push(ctx);
417
+ if (typeof cfg === 'undefined' || typeof cfg.controllerConfig === 'undefined') ctx.server.errors.push(`Server configuration not found in zip file`);
418
+ else if (typeof srv === 'undefined') ctx.server.errors.push(`Server ${s.name} is not enabled in njsPC cannot restore.`);
419
+ else if (!srv.isConnected) ctx.server.errors.push(`Server ${s.name} is not connected or cannot be found by UUID and cannot restore. If this is a version 1.0 file, update your current REM UUID to match the backup REM UUID.`);
420
+ else {
421
+ let resp = await srv.validateRestore(cfg.controllerConfig);
422
+ if (typeof resp !== 'undefined') {
423
+ if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
424
+ let cctx = JSON.parse(resp.data);
425
+ ctx = extend(true, ctx, cctx);
426
+ }
427
+ else
428
+ ctx.server.errors.push(`Error validating controller configuration: ${resp.error.message}`);
429
+ }
430
+ else
431
+ ctx.server.errors.push(`No response from server`);
432
+ }
433
+ }
434
+
435
+ }
436
+ }
437
+
438
+ return stats;
439
+ } catch (err) { logger.error(`Error validating restore options: ${err.message}`); return Promise.reject(err);}
440
+ }
441
+ public async restoreServers(opts): Promise<any> {
442
+ let stats: { backupOptions?: any, njsPC?: RestoreResults, servers: any[] } = { servers: [] };
443
+ try {
444
+ // Step 1: Extract all the files from the zip file.
445
+ let rest = await RestoreFile.fromFile(opts.filePath);
446
+ stats.backupOptions = rest.options;
447
+ // Step 2: Validate the njsPC data against the board. The return
448
+ // from here shoudld give a very detailed view of what it is about to do.
449
+ if (opts.options.njsPC === true) {
450
+ logger.info(`Begin Restore njsPC`);
451
+ stats.njsPC = await sys.board.system.restore(rest.njsPC);
452
+ logger.info(`End Restore njsPC`);
453
+ }
454
+ // Step 3: For each REM server we need to validate the restore
455
+ // file.
456
+ if (typeof opts.options.servers !== 'undefined' && opts.options.servers.length > 0) {
457
+ for (let i = 0; i < opts.options.servers.length; i++) {
458
+ let s = opts.options.servers[i];
459
+ if (s.restore) {
460
+ // Check to see if the server is on-line.
461
+ let srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
462
+ let cfg = rest.servers.find(elem => elem.uuid === s.uuid);
463
+ let ctx: any = { server: { uuid: s.uuid, name: s.name, errors: [], warnings: [] } };
464
+ if (typeof srv === 'undefined' && parseFloat(opts.options.version) >= 1.1) {
465
+ let srvs = this.findServersByType('rem') as REMInterfaceServer[];
466
+ cfg = rest.servers.find(elem => elem.serverConfig.options.host === s.host);
467
+ for (let j = 0; j < srvs.length; j++){
468
+ if (srvs[j].cfg.options.host === cfg.serverConfig.options.host){
469
+ srv = srvs[j];
470
+ let oldSrvCfg = srv.cfg;
471
+ oldSrvCfg.enabled = false;
472
+ await this.updateServerInterface(oldSrvCfg); // unload prev server interface
473
+ srv.uuid = srv.cfg.uuid = cfg.uuid;
474
+ config.setSection('web.interfaces.rem', cfg.serverConfig);
475
+ await this.updateServerInterface(cfg.serverConfig); // reset server interface
476
+ srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
477
+ logger.info(`Restore REM: Current UUID updated to UUID of backup.`);
478
+ break;
479
+ }
480
+ }
481
+ }
482
+ stats.servers.push(ctx);
483
+ if (!srv.isConnected) await utils.sleep(6000); // rem server waits to connect 5s before isConnected will be true. Server.ts#1256 = REMInterfaceServer.init(); What's a better way to do this?
484
+ if (typeof cfg === 'undefined' || typeof cfg.controllerConfig === 'undefined') ctx.server.errors.push(`Server configuration not found in zip file`);
485
+ else if (typeof srv === 'undefined') ctx.server.errors.push(`Server ${s.name} is not enabled in njsPC cannot restore.`);
486
+ else if (!srv.isConnected) ctx.server.errors.push(`Server ${s.name} is not connected cannot restore.`);
487
+ else {
488
+ let resp = await srv.validateRestore(cfg.controllerConfig);
489
+ if (typeof resp !== 'undefined') {
490
+ if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
491
+ let cctx = JSON.parse(resp.data);
492
+ ctx = extend(true, ctx, cctx);
493
+ // Ok so now here we are ready to restore the data.
494
+ let r = await srv.restoreConfig(cfg.controllerConfig);
495
+
496
+ }
497
+ else
498
+ ctx.server.errors.push(`Error validating controller configuration: ${resp.error.message}`);
499
+ }
500
+ else
501
+ ctx.server.errors.push(`No response from server`);
502
+ }
503
+ }
504
+
505
+ }
506
+ }
507
+
508
+ return stats;
509
+ } catch (err) { logger.error(`Error validating restore options: ${err.message}`); return Promise.reject(err); }
510
+ finally {
511
+ try {
512
+ let baseDir = process.cwd();
513
+ let ts = new Date();
514
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
515
+ let filename = 'restoreLog(' + ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds()) + ').log';
516
+ let filePath = path.join(baseDir, 'logs', filename);
517
+ fs.writeFileSync(filePath, JSON.stringify(stats, undefined, 3));
518
+ } catch (err) { logger.error(`Error writing restore log ${err.message}`); }
519
+ }
200
520
  }
201
521
  }
202
522
  class ProtoServer {
@@ -243,7 +563,7 @@ export class HttpServer extends ProtoServer {
243
563
  public app: express.Application;
244
564
  public server: http.Server;
245
565
  public sockServer: SocketIoServer<ClientToServerEvents, ServerToClientEvents>;
246
- private _sockets: RemoteSocket<ServerToClientEvents>[] = [];
566
+ private _sockets: RemoteSocket<ServerToClientEvents, any>[] = [];
247
567
  public emitToClients(evt: string, ...data: any) {
248
568
  if (this.isRunning) {
249
569
  this.sockServer.emit(evt, ...data);
@@ -298,7 +618,7 @@ export class HttpServer extends ProtoServer {
298
618
  self._sockets = await self.sockServer.fetchSockets();
299
619
  });
300
620
  sock.on('echo', (msg) => { sock.emit('echo', msg); });
301
- sock.on('receivePacketRaw', function (incomingPacket: any[]) {
621
+ /* sock.on('receivePacketRaw', function (incomingPacket: any[]) {
302
622
  //var str = 'Add packet(s) to incoming buffer: ';
303
623
  logger.silly('User request (replay.html) to RECEIVE packet: %s', JSON.stringify(incomingPacket));
304
624
  for (var i = 0; i < incomingPacket.length; i++) {
@@ -333,7 +653,7 @@ export class HttpServer extends ProtoServer {
333
653
  conn.queueSendMessage(out);
334
654
  } while (bytesToProcessArr.length > 0);
335
655
 
336
- });
656
+ }); */
337
657
  sock.on('sendOutboundMessage', (mdata) => {
338
658
  let msg: Outbound = Outbound.create({});
339
659
  Object.assign(msg, mdata);
@@ -341,6 +661,22 @@ export class HttpServer extends ProtoServer {
341
661
  logger.silly(`sendOutboundMessage ${msg.toLog()}`);
342
662
  conn.queueSendMessage(msg);
343
663
  });
664
+ sock.on('sendInboundMessage', (mdata) => {
665
+ try {
666
+
667
+ let msg: Inbound = new Inbound();
668
+ msg.direction = mdata.direction;
669
+ msg.header = mdata.header;
670
+ msg.payload = mdata.payload;
671
+ msg.preamble = mdata.preamble;
672
+ msg.protocol = mdata.protocol;
673
+ msg.term = mdata.term;
674
+ if (msg.isValid) msg.process();
675
+ }
676
+ catch (err){
677
+ logger.error(`Error replaying packet: ${err.message}`);
678
+ }
679
+ });
344
680
  sock.on('sendLogMessages', function (sendMessages: boolean) {
345
681
  console.log(`sendLogMessages set to ${sendMessages}`);
346
682
  if (!sendMessages) sock.leave('msgLogger');
@@ -451,7 +787,7 @@ export class HttpServer extends ProtoServer {
451
787
  }
452
788
  }
453
789
  export class HttpsServer extends HttpServer {
454
- public server: https.Server;
790
+ declare server: https.Server;
455
791
 
456
792
  public async init(cfg) {
457
793
  // const auth = require('http-auth');
@@ -927,6 +1263,23 @@ export class REMInterfaceServer extends ProtoServer {
927
1263
  }, 5000);
928
1264
  }
929
1265
  }
1266
+ public async getControllerConfig() : Promise<InterfaceServerResponse> {
1267
+ try {
1268
+ let response = await this.sendClientRequest('GET', '/config/backup/controller', undefined, 10000);
1269
+ return response;
1270
+ } catch (err) { logger.error(err); }
1271
+ }
1272
+ public async validateRestore(cfg): Promise<InterfaceServerResponse> {
1273
+ try {
1274
+ let response = await this.sendClientRequest('PUT', '/config/restore/validate', cfg, 10000);
1275
+ return response;
1276
+ } catch (err) { logger.error(err); }
1277
+ }
1278
+ public async restoreConfig(cfg): Promise<InterfaceServerResponse> {
1279
+ try {
1280
+ return await this.sendClientRequest('PUT', '/config/restore/file', cfg, 20000);
1281
+ } catch (err) { logger.error(err); }
1282
+ }
930
1283
  private async initConnection() {
931
1284
  try {
932
1285
  // find HTTP server
@@ -935,16 +1288,21 @@ export class REMInterfaceServer extends ProtoServer {
935
1288
  // First, send the connection info for njsPC and see if a connection exists.
936
1289
  let url = '/config/checkconnection/';
937
1290
  // can & should extend for https/username-password/ssl
938
- 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: "" }
1291
+ 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: [] }
1292
+ logger.info(`Checking REM Connection ${data.name} ${data.ipAddress}:${data.port}`);
1293
+ try {
1294
+ data.hostnames = await dns.promises.reverse(data.ipAddress);
1295
+ } catch (err) { logger.error(`Error getting hostnames for njsPC REM connection`); }
939
1296
  let result = await this.putApiService(url, data, 5000);
940
1297
  // If the result code is > 200 we have an issue. (-1 is for timeout)
941
1298
  if (result.status.code > 200 || result.status.code < 0) return reject(new Error(`initConnection: ${result.error.message}`));
942
- else { this.remoteConnectionId = result.obj.id };
1299
+ else {
1300
+ this.remoteConnectionId = result.obj.id;
1301
+ };
943
1302
 
944
1303
  // The passed connection has been setup/verified; now test for emit
945
1304
  // if this fails, it could be because the remote connection is disabled. We will not
946
1305
  // automatically re-enable it
947
-
948
1306
  url = '/config/checkemit'
949
1307
  data = { eventName: "checkemit", property: "result", value: 'success', connectionId: result.obj.id }
950
1308
  // wait for REM server to finish resetting
@@ -957,14 +1315,14 @@ export class REMInterfaceServer extends ProtoServer {
957
1315
  // console.log(data);
958
1316
  clearTimeout(_tmr);
959
1317
  logger.info(`REM bi-directional communications established.`)
960
- return resolve();
1318
+ resolve();
961
1319
  });
962
1320
  result = await self.putApiService(url, data);
963
1321
  // If the result code is > 200 or -1 we have an issue.
964
1322
  if (result.status.code > 200 || result.status.code === -1) return reject(new Error(`initConnection: ${result.error.message}`));
965
1323
  else {
966
1324
  clearTimeout(_tmr);
967
- return resolve();
1325
+ resolve();
968
1326
  }
969
1327
  }
970
1328
  catch (err) { reject(new Error(`initConnection setTimeout: ${result.error.message}`)); }
@@ -986,7 +1344,7 @@ export class REMInterfaceServer extends ProtoServer {
986
1344
  public sockClient;
987
1345
  protected agent: http.Agent = new http.Agent({ keepAlive: true });
988
1346
  public get isConnected() { return this.sockClient !== 'undefined' && this.sockClient.connected; };
989
- private _sockets: RemoteSocket<ServerToClientEvents>[] = [];
1347
+ private _sockets: RemoteSocket<ServerToClientEvents, any>[] = [];
990
1348
  private async sendClientRequest(method: string, url: string, data?: any, timeout: number = 10000): Promise<InterfaceServerResponse> {
991
1349
  try {
992
1350
 
@@ -1024,7 +1382,10 @@ export class REMInterfaceServer extends ProtoServer {
1024
1382
  req = http.request(opts, (response: http.IncomingMessage) => {
1025
1383
  ret.status.code = response.statusCode;
1026
1384
  ret.status.message = response.statusMessage;
1027
- response.on('error', (err) => { ret.error = err; resolve(); });
1385
+ response.on('error', (err) => {
1386
+ logger.error(`An error occurred with request: ${err}`);
1387
+ ret.error = err; resolve();
1388
+ });
1028
1389
  response.on('data', (data) => { ret.data += data; });
1029
1390
  response.on('end', () => { resolve(); });
1030
1391
  });
@@ -1120,4 +1481,134 @@ export class REMInterfaceServer extends ProtoServer {
1120
1481
  catch (err) { logger.error(err); }
1121
1482
  }
1122
1483
  }
1484
+ export class BackupFile {
1485
+ public static async fromBuffer(filename: string, buff: Buffer) {
1486
+ try {
1487
+ let bf = new BackupFile();
1488
+ bf.filename = filename;
1489
+ bf.filePath = path.join(process.cwd(), 'backups', bf.filename);
1490
+ await bf.extractBackupOptions(buff);
1491
+ return typeof bf.options !== 'undefined' ? bf : undefined;
1492
+ } catch (err) { logger.error(`Error creating buffered backup file: ${filename}`); }
1493
+ }
1494
+ public static async fromFile(filePath: string) {
1495
+ try {
1496
+ let bf = new BackupFile();
1497
+ bf.filePath = filePath;
1498
+ bf.filename = path.parse(filePath).base;
1499
+ await bf.extractBackupOptions(filePath);
1500
+ return typeof bf.options !== 'undefined' ? bf : undefined;
1501
+ } catch (err) { logger.error(`Error creating backup file from file ${filePath}`); }
1502
+ }
1503
+ public options: any;
1504
+ public filename: string;
1505
+ public filePath: string;
1506
+ public errors = [];
1507
+ protected async extractBackupOptions(file: string | Buffer) {
1508
+ try {
1509
+ let jszip = require("jszip");
1510
+ let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
1511
+ let zip = await jszip.loadAsync(buff);
1512
+ await zip.file('options.json').async('string').then((data) => {
1513
+ this.options = JSON.parse(data);
1514
+ if (typeof this.options.backupDate === 'undefined' && typeof file === 'string') {
1515
+ let name = path.parse(file).name;
1516
+ name = name.indexOf('(') !== -1 ? name.substring(0, name.indexOf('(')) : name;
1517
+ if (name.length === 19) {
1518
+ let date = name.substring(0, 10).replace(/-/g, '/');
1519
+ let time = name.substring(11).replace(/-/g, ':');
1520
+ let dt = Date.parse(`${date} ${time}`);
1521
+ if (!isNaN(dt)) this.options.backupDate = Timestamp.toISOLocal(new Date(dt));
1522
+ }
1523
+ }
1524
+ });
1525
+ } catch (err) { this.errors.push(err); logger.error(`Error extracting backup options from ${file}: ${err.message}`); }
1526
+ }
1527
+ }
1528
+ export class RestoreFile {
1529
+ public static async fromFile(filePath: string) {
1530
+ try {
1531
+ let rf = new RestoreFile();
1532
+ rf.filePath = filePath;
1533
+ rf.filename = path.parse(filePath).base;
1534
+ await rf.extractRestoreOptions(filePath);
1535
+ return rf;
1536
+ } catch (err) { logger.error(`Error created restore file options`); }
1537
+ }
1538
+ public filename: string;
1539
+ public filePath: string;
1540
+ public njsPC: { config:any, poolConfig: any, poolState: any };
1541
+ public servers: { name: string, uuid: string, serverConfig: any, controllerConfig: any }[] = [];
1542
+ public options: any;
1543
+ public errors = [];
1544
+ protected async extractFile(zip, path): Promise<any> {
1545
+ try {
1546
+ let obj;
1547
+ await zip.file(path).async('string').then((data) => { obj = JSON.parse(data); });
1548
+ return obj;
1549
+ } catch (err) { logger.error(`Error extracting restore data from ${this.filename}[${path}]: ${err.message}`); }
1550
+ }
1551
+ protected async extractRestoreOptions(file: string | Buffer) {
1552
+ try {
1553
+ let jszip = require("jszip");
1554
+ let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
1555
+ let zip = await jszip.loadAsync(buff);
1556
+ this.options = await this.extractFile(zip, 'options.json');
1557
+ // Now we need to extract all the servers from the file.
1558
+ if (this.options.njsPC) {
1559
+ this.njsPC = { config: {}, poolConfig: {}, poolState: {} };
1560
+ this.njsPC.config = await this.extractFile(zip, 'njsPC/config.json');
1561
+ this.njsPC.poolConfig = await this.extractFile(zip, 'njsPC/data/poolConfig.json');
1562
+ this.njsPC.poolState = await this.extractFile(zip, 'njsPC/data/poolState.json');
1563
+ }
1564
+ for (let i = 0; i < this.options.servers.length; i++) {
1565
+ // Extract each server from the file.
1566
+ let srv = this.options.servers[i];
1567
+ if (srv.backup && srv.success) {
1568
+ this.servers.push({
1569
+ name: srv.name,
1570
+ uuid: srv.uuid,
1571
+ serverConfig: await this.extractFile(zip, `${srv.name}/serverConfig.json`),
1572
+ controllerConfig: await this.extractFile(zip, `${srv.name}/data/controllerConfig.json`)
1573
+ });
1574
+ }
1575
+ }
1576
+ } catch(err) { this.errors.push(err); logger.error(`Error extracting restore options from ${file}: ${err.message}`); }
1577
+ }
1578
+ }
1579
+ export class RestoreResults {
1580
+ public errors = [];
1581
+ public warnings = [];
1582
+ public success = [];
1583
+ public modules: { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number }[] = [];
1584
+ protected getModule(name: string): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
1585
+ let mod = this.modules.find(elem => name === elem.name);
1586
+ if (typeof mod === 'undefined') {
1587
+ mod = { name: name, errors: [], warnings: [], success: [], restored: 0, ignored: 0 };
1588
+ this.modules.push(mod);
1589
+ }
1590
+ return mod;
1591
+ }
1592
+ public addModuleError(name: string, err: any): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
1593
+ let mod = this.getModule(name);
1594
+ mod.errors.push(err);
1595
+ mod.ignored++;
1596
+ logger.error(`Restore ${name} -> ${err}`);
1597
+ return mod;
1598
+ }
1599
+ public addModuleWarning(name: string, warn: any): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
1600
+ let mod = this.getModule(name);
1601
+ mod.warnings.push(warn);
1602
+ mod.restored++;
1603
+ logger.warn(`Restore ${name} -> ${warn}`);
1604
+ return mod;
1605
+ }
1606
+ public addModuleSuccess(name: string, success: any): { name: string, errors: any[], warnings: any[], success: any[], restored: number, ignored: number } {
1607
+ let mod = this.getModule(name);
1608
+ mod.success.push(success);
1609
+ mod.restored++;
1610
+ logger.info(`Restore ${name} -> ${success}`);
1611
+ return mod;
1612
+ }
1613
+ }
1123
1614
  export const webApp = new WebServer();