nodejs-poolcontroller 7.2.0 → 7.5.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 (64) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/Changelog +13 -0
  3. package/Dockerfile +1 -0
  4. package/README.md +5 -5
  5. package/app.ts +11 -0
  6. package/config/Config.ts +3 -0
  7. package/config/VersionCheck.ts +8 -4
  8. package/controller/Constants.ts +165 -9
  9. package/controller/Equipment.ts +186 -65
  10. package/controller/Errors.ts +22 -1
  11. package/controller/State.ts +273 -57
  12. package/controller/boards/EasyTouchBoard.ts +194 -95
  13. package/controller/boards/IntelliCenterBoard.ts +115 -42
  14. package/controller/boards/IntelliTouchBoard.ts +104 -30
  15. package/controller/boards/NixieBoard.ts +155 -53
  16. package/controller/boards/SystemBoard.ts +1529 -514
  17. package/controller/comms/Comms.ts +219 -42
  18. package/controller/comms/messages/Messages.ts +16 -4
  19. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -3
  20. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  21. package/controller/comms/messages/config/CircuitMessage.ts +1 -1
  22. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  23. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  24. package/controller/comms/messages/config/ExternalMessage.ts +43 -25
  25. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  26. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  27. package/controller/comms/messages/config/HeaterMessage.ts +15 -9
  28. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  29. package/controller/comms/messages/config/OptionsMessage.ts +13 -1
  30. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  31. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  32. package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
  33. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  34. package/controller/comms/messages/config/ValveMessage.ts +12 -2
  35. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +14 -6
  36. package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
  37. package/controller/comms/messages/status/HeaterStateMessage.ts +25 -5
  38. package/controller/comms/messages/status/IntelliChemStateMessage.ts +55 -26
  39. package/controller/nixie/Nixie.ts +18 -16
  40. package/controller/nixie/NixieEquipment.ts +6 -6
  41. package/controller/nixie/bodies/Body.ts +7 -4
  42. package/controller/nixie/bodies/Filter.ts +7 -4
  43. package/controller/nixie/chemistry/ChemController.ts +800 -283
  44. package/controller/nixie/chemistry/Chlorinator.ts +22 -14
  45. package/controller/nixie/circuits/Circuit.ts +42 -7
  46. package/controller/nixie/heaters/Heater.ts +303 -30
  47. package/controller/nixie/pumps/Pump.ts +57 -30
  48. package/controller/nixie/schedules/Schedule.ts +10 -7
  49. package/controller/nixie/valves/Valve.ts +7 -5
  50. package/defaultConfig.json +32 -1
  51. package/issue_template.md +1 -1
  52. package/logger/DataLogger.ts +37 -22
  53. package/package.json +20 -18
  54. package/web/Server.ts +529 -31
  55. package/web/bindings/influxDB.json +157 -5
  56. package/web/bindings/mqtt.json +112 -13
  57. package/web/bindings/mqttAlt.json +109 -11
  58. package/web/interfaces/baseInterface.ts +2 -1
  59. package/web/interfaces/httpInterface.ts +2 -0
  60. package/web/interfaces/influxInterface.ts +103 -54
  61. package/web/interfaces/mqttInterface.ts +16 -5
  62. package/web/services/config/Config.ts +179 -43
  63. package/web/services/state/State.ts +51 -5
  64. package/web/services/state/StateSocket.ts +19 -2
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 {
@@ -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,11 +661,32 @@ 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');
347
683
  else sock.join('msgLogger');
348
684
  });
685
+ sock.on('sendRS485PortStats', function (sendPortStatus: boolean) {
686
+ console.log(`sendRS485PortStats set to ${sendPortStatus}`);
687
+ if (!sendPortStatus) sock.leave('rs485PortStats');
688
+ else sock.join('rs485PortStats');
689
+ });
349
690
  StateSocket.initSockets(sock);
350
691
  ConfigSocket.initSockets(sock);
351
692
  }
@@ -446,7 +787,7 @@ export class HttpServer extends ProtoServer {
446
787
  }
447
788
  }
448
789
  export class HttpsServer extends HttpServer {
449
- public server: https.Server;
790
+ declare server: https.Server;
450
791
 
451
792
  public async init(cfg) {
452
793
  // const auth = require('http-auth');
@@ -907,13 +1248,14 @@ export class InterfaceServerResponse {
907
1248
  }
908
1249
  export class REMInterfaceServer extends ProtoServer {
909
1250
  public async init(cfg) {
1251
+ let self = this;
910
1252
  this.cfg = cfg;
911
1253
  this.uuid = cfg.uuid;
912
1254
  if (cfg.enabled) {
913
1255
  this.initSockets();
914
1256
  setTimeout(async () => {
915
1257
  try {
916
- await this.initConnection();
1258
+ await self.initConnection();
917
1259
  }
918
1260
  catch (err) {
919
1261
  logger.error(`Error establishing bi-directional Nixie/REM connection: ${err}`)
@@ -921,23 +1263,46 @@ export class REMInterfaceServer extends ProtoServer {
921
1263
  }, 5000);
922
1264
  }
923
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
+ }
924
1283
  private async initConnection() {
925
1284
  try {
926
1285
  // find HTTP server
927
1286
  return new Promise<void>(async (resolve, reject) => {
1287
+ let self = this;
928
1288
  // First, send the connection info for njsPC and see if a connection exists.
929
1289
  let url = '/config/checkconnection/';
930
1290
  // can & should extend for https/username-password/ssl
931
- 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`); }
932
1296
  let result = await this.putApiService(url, data, 5000);
933
1297
  // If the result code is > 200 we have an issue. (-1 is for timeout)
934
1298
  if (result.status.code > 200 || result.status.code < 0) return reject(new Error(`initConnection: ${result.error.message}`));
935
- else { this.remoteConnectionId = result.obj.id };
1299
+ else {
1300
+ this.remoteConnectionId = result.obj.id;
1301
+ };
936
1302
 
937
1303
  // The passed connection has been setup/verified; now test for emit
938
1304
  // if this fails, it could be because the remote connection is disabled. We will not
939
1305
  // automatically re-enable it
940
-
941
1306
  url = '/config/checkemit'
942
1307
  data = { eventName: "checkemit", property: "result", value: 'success', connectionId: result.obj.id }
943
1308
  // wait for REM server to finish resetting
@@ -950,14 +1315,14 @@ export class REMInterfaceServer extends ProtoServer {
950
1315
  // console.log(data);
951
1316
  clearTimeout(_tmr);
952
1317
  logger.info(`REM bi-directional communications established.`)
953
- return resolve();
1318
+ resolve();
954
1319
  });
955
- result = await this.putApiService(url, data);
1320
+ result = await self.putApiService(url, data);
956
1321
  // If the result code is > 200 or -1 we have an issue.
957
1322
  if (result.status.code > 200 || result.status.code === -1) return reject(new Error(`initConnection: ${result.error.message}`));
958
1323
  else {
959
1324
  clearTimeout(_tmr);
960
- return resolve();
1325
+ resolve();
961
1326
  }
962
1327
  }
963
1328
  catch (err) { reject(new Error(`initConnection setTimeout: ${result.error.message}`)); }
@@ -1017,7 +1382,10 @@ export class REMInterfaceServer extends ProtoServer {
1017
1382
  req = http.request(opts, (response: http.IncomingMessage) => {
1018
1383
  ret.status.code = response.statusCode;
1019
1384
  ret.status.message = response.statusMessage;
1020
- 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
+ });
1021
1389
  response.on('data', (data) => { ret.data += data; });
1022
1390
  response.on('end', () => { resolve(); });
1023
1391
  });
@@ -1113,4 +1481,134 @@ export class REMInterfaceServer extends ProtoServer {
1113
1481
  catch (err) { logger.error(err); }
1114
1482
  }
1115
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
+ }
1116
1614
  export const webApp = new WebServer();