nodejs-poolcontroller 8.3.0 → 8.4.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 (107) hide show
  1. package/.eslintrc.json +36 -36
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/copilot-instructions.md +63 -63
  7. package/.github/workflows/ghcr-publish.yml +67 -67
  8. package/157_issues.md +101 -0
  9. package/AGENTS.md +613 -0
  10. package/CONTRIBUTING.md +74 -74
  11. package/Changelog +292 -284
  12. package/Dockerfile +62 -62
  13. package/Gruntfile.js +40 -40
  14. package/LICENSE +661 -661
  15. package/README.md +329 -309
  16. package/anslq25/MessagesMock.ts +221 -221
  17. package/anslq25/boards/MockBoardFactory.ts +49 -49
  18. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  19. package/anslq25/boards/MockSystemBoard.ts +216 -216
  20. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  21. package/anslq25/pumps/MockPump.ts +83 -83
  22. package/app.ts +115 -115
  23. package/config/Config.ts +0 -0
  24. package/config/VersionCheck.ts +0 -0
  25. package/controller/Constants.ts +809 -805
  26. package/controller/Equipment.ts +2737 -2664
  27. package/controller/Errors.ts +181 -181
  28. package/controller/Lockouts.ts +549 -549
  29. package/controller/State.ts +3746 -3701
  30. package/controller/boards/AquaLinkBoard.ts +1175 -1003
  31. package/controller/boards/BoardFactory.ts +53 -53
  32. package/controller/boards/EasyTouchBoard.ts +3246 -3202
  33. package/controller/boards/IntelliCenterBoard.ts +4581 -3899
  34. package/controller/boards/IntelliComBoard.ts +69 -69
  35. package/controller/boards/IntelliTouchBoard.ts +382 -382
  36. package/controller/boards/NixieBoard.ts +1947 -1944
  37. package/controller/boards/SunTouchBoard.ts +401 -400
  38. package/controller/boards/SystemBoard.ts +5303 -5268
  39. package/controller/comms/Comms.ts +1278 -1255
  40. package/controller/comms/ScreenLogic.ts +1665 -1665
  41. package/controller/comms/messages/Messages.ts +1627 -1406
  42. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  43. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  44. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  45. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  46. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  47. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  48. package/controller/comms/messages/config/EquipmentMessage.ts +250 -210
  49. package/controller/comms/messages/config/ExternalMessage.ts +1051 -903
  50. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  51. package/controller/comms/messages/config/GeneralMessage.ts +65 -0
  52. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  53. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  54. package/controller/comms/messages/config/OptionsMessage.ts +207 -174
  55. package/controller/comms/messages/config/PumpMessage.ts +427 -421
  56. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  57. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  58. package/controller/comms/messages/config/SecurityMessage.ts +37 -13
  59. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  60. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  61. package/controller/comms/messages/status/EquipmentStateMessage.ts +940 -822
  62. package/controller/comms/messages/status/HeaterStateMessage.ts +147 -135
  63. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  64. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  65. package/controller/comms/messages/status/NeptuneModbusStateMessage.ts +217 -0
  66. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  67. package/controller/comms/messages/status/RegalModbusStateMessage.ts +410 -410
  68. package/controller/comms/messages/status/VersionMessage.ts +152 -41
  69. package/controller/nixie/Nixie.ts +173 -173
  70. package/controller/nixie/NixieEquipment.ts +104 -104
  71. package/controller/nixie/bodies/Body.ts +120 -120
  72. package/controller/nixie/bodies/Filter.ts +135 -135
  73. package/controller/nixie/chemistry/ChemController.ts +2756 -2724
  74. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  75. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  76. package/controller/nixie/circuits/Circuit.ts +478 -478
  77. package/controller/nixie/heaters/Heater.ts +843 -834
  78. package/controller/nixie/pumps/Pump.ts +1336 -1193
  79. package/controller/nixie/schedules/Schedule.ts +401 -401
  80. package/controller/nixie/valves/Valve.ts +170 -170
  81. package/defaultConfig.json +352 -352
  82. package/docker-compose.yml +32 -31
  83. package/logger/DataLogger.ts +448 -448
  84. package/logger/Logger.ts +459 -436
  85. package/package.json +58 -58
  86. package/sendSocket.js +32 -32
  87. package/tsconfig.json +26 -25
  88. package/types/express-multer.d.ts +32 -32
  89. package/web/Server.ts +1939 -1927
  90. package/web/bindings/aqualinkD.json +559 -559
  91. package/web/bindings/influxDB.json +1066 -1066
  92. package/web/bindings/mqtt.json +721 -721
  93. package/web/bindings/mqttAlt.json +746 -746
  94. package/web/bindings/rulesManager.json +54 -54
  95. package/web/bindings/smartThings-Hubitat.json +31 -31
  96. package/web/bindings/valveRelays.json +20 -20
  97. package/web/bindings/vera.json +25 -25
  98. package/web/interfaces/baseInterface.ts +188 -188
  99. package/web/interfaces/httpInterface.ts +148 -148
  100. package/web/interfaces/influxInterface.ts +283 -283
  101. package/web/interfaces/mqttInterface.ts +695 -695
  102. package/web/interfaces/ruleInterface.ts +101 -87
  103. package/web/services/config/Config.ts +1212 -1053
  104. package/web/services/config/ConfigSocket.ts +0 -0
  105. package/web/services/state/State.ts +21 -0
  106. package/web/services/state/StateSocket.ts +28 -0
  107. package/web/services/utilities/Utilities.ts +233 -233
package/logger/Logger.ts CHANGED
@@ -1,436 +1,459 @@
1
- /* nodejs-poolController. An application to control pool equipment.
2
- Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
- Russell Goldin, tagyoureit. russ.goldin@gmail.com
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as
7
- published by the Free Software Foundation, either version 3 of the
8
- License, or (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <http://www.gnu.org/licenses/>.
17
- */
18
- import * as path from 'path';
19
- import * as fs from 'fs';
20
- import * as winston from 'winston';
21
- import * as os from 'os';
22
- import { utils } from "../controller/Constants";
23
- import { Message } from '../controller/comms/messages/Messages';
24
- import { config } from '../config/Config';
25
- import { webApp } from '../web/Server';
26
- import { sl } from '../controller/comms/ScreenLogic';
27
-
28
- const extend = require("extend");
29
-
30
- class Logger {
31
- constructor() {
32
- if (!fs.existsSync(path.join(process.cwd(), '/logs'))) fs.mkdirSync(path.join(process.cwd(), '/logs'));
33
- this.pktPath = path.join(process.cwd(), '/logs', this.getPacketPath());
34
- this.captureForReplayBaseDir = path.join(process.cwd(), '/logs/', this.getLogTimestamp());
35
- /* this.captureForReplayPath = path.join(this.captureForReplayBaseDir, '/packetCapture.json'); */
36
- this.pkts = [];
37
- this.slMessages = [];
38
- }
39
- private cfg;
40
- private pkts: Message[];
41
- private slMessages: any[];
42
- private pktPath: string;
43
- private consoleToFilePath: string;
44
- private transports: { console: winston.transports.ConsoleTransportInstance, file?: winston.transports.FileTransportInstance, consoleFile?: winston.transports.FileTransportInstance } = {
45
- console: new winston.transports.Console({ level: 'silly' })
46
- };
47
- private captureForReplayBaseDir: string;
48
- private captureForReplayPath: string;
49
- private pktTimer: NodeJS.Timeout;
50
- private currentTimestamp: string;
51
- private getPacketPath(): string {
52
- // changed this to remove spaces from the name
53
- return 'packetLog(' + this.getLogTimestamp() + ').log';
54
- }
55
- private getConsoleToFilePath(): string {
56
- return 'consoleLog(' + this.getLogTimestamp() + ').log';
57
- }
58
- public getLogTimestamp(bNew: boolean = false): string {
59
- if (!bNew && typeof this.currentTimestamp !== 'undefined') { return this.currentTimestamp; }
60
- var ts = new Date();
61
- function pad(n) { return (n < 10 ? '0' : '') + n; }
62
- this.currentTimestamp = ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds());
63
- return this.currentTimestamp;
64
- }
65
-
66
- private myFormat = winston.format.printf(({ level, message, label }) => {
67
- return `[${new Date().toLocaleString()}] ${level}: ${message}`;
68
- });
69
-
70
- private _logger: winston.Logger;
71
- public init() {
72
- this.cfg = config.getSection('log');
73
- logger._logger = winston.createLogger({
74
- format: winston.format.combine(winston.format.timestamp({format: 'MMMM DD YYYY'}), winston.format.colorize(), winston.format.splat(), this.myFormat),
75
- transports: [this.transports.console]
76
- });
77
- this.transports.console.level = this.cfg.app.level;
78
- if (this.cfg.app.captureForReplay) this.startCaptureForReplay(false);
79
- if (this.cfg.app.logToFile) {
80
- this.transports.consoleFile = new winston.transports.File({
81
- filename: path.join(process.cwd(), '/logs', this.getConsoleToFilePath()),
82
- level: 'silly',
83
- format: winston.format.combine(winston.format.splat(), winston.format.uncolorize(), this.myFormat)
84
- });
85
- this.transports.consoleFile.level = this.cfg.app.level;
86
- this._logger.add(this.transports.consoleFile);
87
- }
88
- }
89
- public async stopAsync() {
90
- try {
91
- this.info(`Stopping logger Process.`);
92
- if (this.cfg.app.captureForReplay) {
93
- return await this.stopCaptureForReplayAsync();
94
- }
95
- // Free up the file handles. This is yet another goofiness with winston. Not sure why they
96
- // need to exclusively lock the file handles when the process always appends. Just stupid.
97
- if (typeof this.transports.consoleFile !== 'undefined') {
98
- this._logger.remove(this.transports.consoleFile);
99
- this.transports.consoleFile.close();
100
- this.transports.consoleFile = undefined;
101
- }
102
- console.log(`Logger Process Stopped`);
103
- } catch (err) { console.log(`Error shutting down logger: ${err.message}`); }
104
- }
105
- public get options(): any { return this.cfg; }
106
- public info(...args: any[]) { logger._logger.info.apply(logger._logger, arguments); }
107
- public debug(...args: any[]) { logger._logger.debug.apply(logger._logger, arguments); }
108
- public warn(...args: any[]) { logger._logger.warn.apply(logger._logger, arguments); }
109
- public verbose(...args: any[]) { logger._logger.verbose.apply(logger._logger, arguments); }
110
- public error(...args: any[]): Error { logger._logger.error.apply(logger._logger, arguments); return new Error(arguments[0]); }
111
- public silly(...args: any[]) { logger._logger.silly.apply(logger._logger, arguments); }
112
- public reject(sError: string): Promise<Error> {
113
- logger.error(sError);
114
- return Promise.reject(new Error(sError));
115
- }
116
- private isIncluded(byte: number, arr: number[]): boolean {
117
- if (typeof (arr) === "undefined" || !arr || arr.length === 0) return true;
118
- if (arr.indexOf(byte) !== -1) return true;
119
- return false;
120
- }
121
- private isExcluded(byte: number, arr: number[]): boolean {
122
- if (typeof (arr) === "undefined" || !arr) return false;
123
- if (arr && arr.length === 0) return false;
124
- if (arr.indexOf(byte) !== -1) return true;
125
- return false;
126
- }
127
- public packet(msg: Message) {
128
- if (logger.cfg.packet.enabled || logger.cfg.app.captureForReplay) {
129
- // Filter out the messages we do not want.
130
- var bLog: boolean = true;
131
- // A random packet may actually find its way into the throws should the bytes get messed up
132
- // in a fashion where the header byte is 255, 0, 255 but we have not identified the channel.
133
- // Thus far we have seen 165 and 166.
134
- var cfgPacket = logger.cfg.packet[msg.protocol] || logger.cfg.packet['unidentified'];
135
- if (!logger.cfg.app.captureForReplay) {
136
- // Log invalid messages no matter what if the user has selected invalid message logging.
137
- if (bLog && !msg.isValid) {
138
- if (!logger.cfg.packet.invalid) bLog = false;
139
- }
140
- else {
141
- if (bLog && !cfgPacket.enabled) bLog = false;
142
- if (bLog && !logger.isIncluded(msg.source, cfgPacket.includeSouce)) bLog = false;
143
- if (bLog && !logger.isIncluded(msg.dest, cfgPacket.includeDest)) bLog = false;
144
- if (bLog && !logger.isIncluded(msg.action, cfgPacket.includeActions)) bLog = false;
145
- if (bLog && logger.isExcluded(msg.source, cfgPacket.excludeSource)) bLog = false;
146
- if (bLog && logger.isExcluded(msg.dest, cfgPacket.excludeDest)) bLog = false;
147
- if (bLog && logger.isExcluded(msg.action, cfgPacket.excludeActions)) bLog = false;
148
- }
149
- }
150
-
151
- if (bLog) {
152
- if (logger.cfg.packet.logToFile) {
153
- logger.pkts.push(msg);
154
- if (logger.pkts.length > 5)
155
- logger.flushLogs();
156
- else {
157
- // Attempt to ease up on the writes if we are logging a bunch of packets.
158
- if (logger.pktTimer) clearTimeout(logger.pktTimer);
159
- logger.pktTimer = setTimeout(logger.flushLogs, 1000);
160
- }
161
- }
162
- webApp.emitToChannel('msgLogger', 'logMessage', msg);
163
- }
164
- }
165
- if (logger.cfg.packet.logToConsole) {
166
- if (msg.isValid && bLog) logger._logger.info(msg.toLog());
167
- else if (!msg.isValid) logger._logger.warn(msg.toLog());
168
- }
169
- }
170
- public screenlogic(data: any){
171
- if (logger.cfg.screenlogic.enabled || logger.cfg.app.captureForReplay){
172
- if (logger.cfg.screenlogic.logToFile) {
173
- logger.slMessages.push(data);
174
- if (logger.slMessages.length > 5)
175
- logger.flushSLLogs();
176
- else {
177
- // Attempt to ease up on the writes if we are logging a bunch of packets.
178
- if (logger.pktTimer) clearTimeout(logger.pktTimer);
179
- logger.pktTimer = setTimeout(logger.flushSLLogs, 1000);
180
- }
181
- }
182
- webApp.emitToChannel('msgLogger', 'logMessage', data);
183
-
184
- }
185
- if (logger.cfg.screenlogic.logToConsole){
186
- logger._logger.info(sl.toLog(data));
187
- }
188
- }
189
- public logAPI(apiReq:string){
190
- if (logger.cfg.app.captureForReplay){
191
- // TODO: buffer this
192
- fs.appendFile(logger.pktPath, apiReq, function(err) {
193
- if (err) logger.error('Error writing packet to %s: %s', logger.pktPath, err);
194
- });
195
- }
196
-
197
- }
198
- public clearMessages() {
199
- if (fs.existsSync(logger.pktPath)) {
200
- logger.info(`Clearing message log: ${ logger.pktPath }`);
201
- fs.truncateSync(logger.pktPath, 0);
202
- }
203
- }
204
- public flushLogs() {
205
- var p: Message[] = logger.pkts.splice(0, logger.pkts.length);
206
- var buf: string = '';
207
- if (logger.cfg.packet.enabled) {
208
- for (let i = 0; i < p.length; i++) {
209
-
210
- buf += (p[i].toLog() + os.EOL);
211
- }
212
- fs.appendFile(logger.pktPath, buf, function(err) {
213
- if (err) logger.error('Error writing packet to %s: %s', logger.pktPath, err);
214
- });
215
- }
216
- buf = '';
217
- }
218
- public flushSLLogs() {
219
- var p: any[] = logger.slMessages.splice(0, logger.slMessages.length);
220
- var buf: string = '';
221
-
222
- for (let i = 0; i < p.length; i++) {
223
- buf += (p[i] + os.EOL);
224
- }
225
- fs.appendFile(logger.pktPath, buf, function(err) {
226
- if (err) logger.error(`Error writing screenlogic message to ${logger.pktPath}: ${err.message}`);
227
- });
228
-
229
- buf = '';
230
- }
231
- public setOptions(opts, c?: any) {
232
- c = typeof c === 'undefined' ? this.cfg : c;
233
- for (let prop in opts) {
234
- let o = opts[prop];
235
- if (o instanceof Array) {
236
- //console.log({ o: o, c: c, prop: prop });
237
- c[prop] = o; // Stop here we are replacing the array.
238
- }
239
- else if (typeof o === 'object') {
240
- if (typeof c[prop] === 'undefined') c[prop] = {};
241
- this.setOptions(o, c[prop]); // Use recursion here. Harder to follow but much less code.
242
- }
243
- else
244
- c[prop] = opts[prop];
245
- }
246
- config.setSection('log', this.cfg);
247
- if (utils.makeBool(this.cfg.app.logToFile)) {
248
- if (typeof this.transports.consoleFile === 'undefined') {
249
- this.transports.consoleFile = new winston.transports.File({
250
- filename: path.join(process.cwd(), '/logs', this.getConsoleToFilePath()),
251
- level: 'silly',
252
- format: winston.format.combine(winston.format.splat(), winston.format.uncolorize(), this.myFormat)
253
- });
254
- this._logger.add(this.transports.consoleFile);
255
- }
256
- }
257
- else {
258
- if (typeof this.transports.consoleFile !== 'undefined') {
259
- this._logger.remove(this.transports.consoleFile);
260
- this.transports.consoleFile.close();
261
- this.transports.consoleFile = undefined;
262
- }
263
- }
264
- for (let [key, transport] of Object.entries(this.transports)) {
265
- if(typeof transport !== 'undefined') transport.level = this.cfg.app.level;
266
- }
267
- }
268
- public startCaptureForReplay(bResetLogs:boolean) {
269
- logger.info(`Starting Replay Capture.`);
270
- // start new replay directory
271
-
272
- if (!fs.existsSync(this.captureForReplayPath)) fs.mkdirSync(this.captureForReplayBaseDir, { recursive: true });
273
-
274
- // Create logs subdirectory for additional log files
275
- let logsSubDir = path.join(this.captureForReplayBaseDir, 'logs');
276
- if (!fs.existsSync(logsSubDir)) fs.mkdirSync(logsSubDir, { recursive: true });
277
- if (bResetLogs){
278
- if (fs.existsSync(path.join(process.cwd(), 'data/poolConfig.json'))) {
279
- fs.copyFileSync(path.join(process.cwd(), 'data/poolConfig.json'), path.join(process.cwd(),'data/', `poolConfig-${this.getLogTimestamp()}.json`));
280
- fs.unlinkSync((path.join(process.cwd(), 'data/poolConfig.json')));
281
- }
282
- if (fs.existsSync(path.join(process.cwd(), 'data/poolState.json'))) {
283
- fs.copyFileSync(path.join(process.cwd(), 'data/poolState.json'), path.join(process.cwd(),'data/', `poolState-${this.getLogTimestamp()}.json`));
284
- fs.unlinkSync((path.join(process.cwd(), 'data/poolState.json')));
285
- }
286
- this.clearMessages();
287
- }
288
- logger.cfg = extend(true, {}, logger.cfg, {
289
- "packet": {
290
- "enabled": true,
291
- "logToConsole": true,
292
- "logToFile": true,
293
- "invalid": true,
294
- "broadcast": {
295
- "enabled": true,
296
- "includeActions": [],
297
- "includeSource": [],
298
- "includeDest": [],
299
- "excludeActions": [],
300
- "excludeSource": [],
301
- "excludeDest": []
302
- },
303
- "pump": {
304
- "enabled": true,
305
- "includeActions": [],
306
- "includeSource": [],
307
- "includeDest": [],
308
- "excludeActions": [],
309
- "excludeSource": [],
310
- "excludeDest": []
311
- },
312
- "chlorinator": {
313
- "enabled": true,
314
- "includeSource": [],
315
- "includeDest": [],
316
- "excludeSource": [],
317
- "excludeDest": []
318
- },
319
- "intellichem": {
320
- "enabled": true,
321
- "includeActions": [],
322
- "exclueActions": [],
323
- "includeSource": [],
324
- "includeDest": [],
325
- "excludeSource": [],
326
- "excludeDest": []
327
- },
328
- "intellivalve": {
329
- "enabled": true,
330
- "includeActions": [],
331
- "exclueActions": [],
332
- "includeSource": [],
333
- "includeDest": [],
334
- "excludeSource": [],
335
- "excludeDest": []
336
- },
337
- "unidentified": {
338
- "enabled": true,
339
- "includeSource": [],
340
- "includeDest": [],
341
- "excludeSource": [],
342
- "excludeDest": []
343
- },
344
- "unknown": {
345
- "enabled": true,
346
- "includeSource": [],
347
- "includeDest": [],
348
- "excludeSource": [],
349
- "excludeDest": []
350
- }
351
- },
352
- "app": {
353
- "enabled": true,
354
- "level": "silly",
355
- "captureForReplay": true
356
- }
357
- });
358
- this.consoleToFilePath = path.join(this.captureForReplayBaseDir, this.getConsoleToFilePath());
359
- this.transports.file = new winston.transports.File({
360
- filename: this.consoleToFilePath,
361
- level: 'silly',
362
- format: winston.format.combine(winston.format.splat(), winston.format.uncolorize(), this.myFormat)
363
- });
364
- logger._logger.add(this.transports.file);
365
- this.transports.console.level = 'silly';
366
- }
367
- public async stopCaptureForReplayAsync(remLogs?: any[]):Promise<string> {
368
- return new Promise<string>(async (resolve, reject) => {
369
- try {
370
- // Get REM server configurations from config
371
- let configData = config.getSection();
372
- let remServers = [];
373
- if (configData.web && configData.web.interfaces) {
374
- for (let interfaceName in configData.web.interfaces) {
375
- let interfaceConfig = configData.web.interfaces[interfaceName];
376
- if (interfaceConfig.type === 'rem' && interfaceConfig.enabled) {
377
- remServers.push({
378
- name: interfaceConfig.name || interfaceName,
379
- uuid: interfaceConfig.uuid,
380
- host: interfaceConfig.options?.host || '',
381
- backup: true
382
- });
383
- }
384
- }
385
- }
386
-
387
- // Use the existing backup logic to create the base backup
388
- let backupOptions = {
389
- njsPC: true,
390
- servers: remServers,
391
- name: `Packet Capture ${this.currentTimestamp}`,
392
- automatic: false
393
- };
394
-
395
- let backupFile = await webApp.backupServer(backupOptions);
396
-
397
- // Add packet capture logs to the existing backup zip
398
- let jszip = require("jszip");
399
- let zip = await jszip.loadAsync(fs.readFileSync(backupFile.filePath));
400
-
401
- // Add packet capture logs to the njsPC/logs directory
402
- zip.file(`njsPC/logs/${this.getPacketPath()}`, fs.readFileSync(logger.pktPath));
403
- zip.file(`njsPC/logs/${this.getConsoleToFilePath()}`, fs.readFileSync(this.consoleToFilePath));
404
-
405
- // Add REM server logs if provided
406
- if (remLogs && remLogs.length > 0) {
407
- logger.info(`Adding ${remLogs.length} REM logs to backup`);
408
- for (let remLog of remLogs) {
409
- // Create logs directory for the REM server using the hardcoded name
410
- let logPath = `Relay Equipment Manager/logs/${remLog.logFileName}`;
411
- logger.info(`Adding REM log to backup: ${logPath} (size: ${remLog.logData.length} characters)`);
412
- zip.file(logPath, remLog.logData);
413
- }
414
- } else {
415
- logger.info(`No REM logs provided to add to backup`);
416
- }
417
-
418
- // Generate the updated zip
419
- await zip.generateAsync({type:'nodebuffer'}).then(content => {
420
- fs.writeFileSync(backupFile.filePath, content);
421
- });
422
-
423
- // Restore original logging configuration
424
- this.cfg = config.getSection('log');
425
- logger._logger.remove(this.transports.file);
426
- this.transports.console.level = this.cfg.app.level;
427
-
428
- resolve(backupFile.filePath);
429
- }
430
- catch (err) {
431
- reject(err.message);
432
- }
433
- });
434
- }
435
- }
436
- export var logger = new Logger();
1
+ /* nodejs-poolController. An application to control pool equipment.
2
+ Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
+ Russell Goldin, tagyoureit. russ.goldin@gmail.com
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as
7
+ published by the Free Software Foundation, either version 3 of the
8
+ License, or (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+ import * as path from 'path';
19
+ import * as fs from 'fs';
20
+ import * as winston from 'winston';
21
+ import * as os from 'os';
22
+ import { utils } from "../controller/Constants";
23
+ import { Message } from '../controller/comms/messages/Messages';
24
+ import { config } from '../config/Config';
25
+ import { webApp } from '../web/Server';
26
+ import { sl } from '../controller/comms/ScreenLogic';
27
+
28
+ const extend = require("extend");
29
+
30
+ class Logger {
31
+ constructor() {
32
+ if (!fs.existsSync(path.join(process.cwd(), '/logs'))) fs.mkdirSync(path.join(process.cwd(), '/logs'));
33
+ this.pktPath = path.join(process.cwd(), '/logs', this.getPacketPath());
34
+ this.captureForReplayBaseDir = path.join(process.cwd(), '/logs/', this.getLogTimestamp());
35
+ this.captureForReplayPath = this.captureForReplayBaseDir;
36
+ this.pkts = [];
37
+ this.slMessages = [];
38
+ }
39
+ private cfg;
40
+ private pkts: Message[];
41
+ private slMessages: any[];
42
+ private pktPath: string;
43
+ private consoleToFilePath: string;
44
+ private transports: { console: winston.transports.ConsoleTransportInstance, file?: winston.transports.FileTransportInstance, consoleFile?: winston.transports.FileTransportInstance } = {
45
+ console: new winston.transports.Console({ level: 'silly' })
46
+ };
47
+ private captureForReplayBaseDir: string;
48
+ private captureForReplayPath: string;
49
+ private pktTimer: NodeJS.Timeout;
50
+ private currentTimestamp: string;
51
+ private _captureInProgress: boolean = false;
52
+ public get captureInProgress(): boolean { return this._captureInProgress; }
53
+ private getPacketPath(): string {
54
+ // changed this to remove spaces from the name
55
+ return 'packetLog(' + this.getLogTimestamp() + ').log';
56
+ }
57
+ private getConsoleToFilePath(): string {
58
+ return 'consoleLog(' + this.getLogTimestamp() + ').log';
59
+ }
60
+ public getLogTimestamp(bNew: boolean = false): string {
61
+ if (!bNew && typeof this.currentTimestamp !== 'undefined') { return this.currentTimestamp; }
62
+ var ts = new Date();
63
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
64
+ this.currentTimestamp = ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds());
65
+ return this.currentTimestamp;
66
+ }
67
+
68
+ private myFormat = winston.format.printf(({ level, message, label }) => {
69
+ return `[${new Date().toLocaleString()}] ${level}: ${message}`;
70
+ });
71
+
72
+ private _logger: winston.Logger;
73
+ public init() {
74
+ this.cfg = config.getSection('log');
75
+ logger._logger = winston.createLogger({
76
+ format: winston.format.combine(winston.format.timestamp({format: 'MMMM DD YYYY'}), winston.format.colorize(), winston.format.splat(), this.myFormat),
77
+ transports: [this.transports.console]
78
+ });
79
+ this.transports.console.level = this.cfg.app.level;
80
+ // Only start capture if not already capturing (prevents duplicate transports when config watcher triggers init())
81
+ if (this.cfg.app.captureForReplay && !this._captureInProgress) {
82
+ this.startCaptureForReplay(false);
83
+ }
84
+ if (this.cfg.app.logToFile) {
85
+ this.transports.consoleFile = new winston.transports.File({
86
+ filename: path.join(process.cwd(), '/logs', this.getConsoleToFilePath()),
87
+ level: 'silly',
88
+ format: winston.format.combine(winston.format.splat(), winston.format.uncolorize(), this.myFormat)
89
+ });
90
+ this.transports.consoleFile.level = this.cfg.app.level;
91
+ this._logger.add(this.transports.consoleFile);
92
+ }
93
+ }
94
+ public async stopAsync() {
95
+ try {
96
+ this.info(`Stopping logger Process.`);
97
+ if (this.cfg.app.captureForReplay) {
98
+ return await this.stopCaptureForReplayAsync();
99
+ }
100
+ // Free up the file handles. This is yet another goofiness with winston. Not sure why they
101
+ // need to exclusively lock the file handles when the process always appends. Just stupid.
102
+ if (typeof this.transports.consoleFile !== 'undefined') {
103
+ this._logger.remove(this.transports.consoleFile);
104
+ this.transports.consoleFile.close();
105
+ this.transports.consoleFile = undefined;
106
+ }
107
+ console.log(`Logger Process Stopped`);
108
+ } catch (err) { console.log(`Error shutting down logger: ${err.message}`); }
109
+ }
110
+ public get options(): any { return this.cfg; }
111
+ public info(...args: any[]) { logger._logger.info.apply(logger._logger, arguments); }
112
+ public debug(...args: any[]) { logger._logger.debug.apply(logger._logger, arguments); }
113
+ public warn(...args: any[]) { logger._logger.warn.apply(logger._logger, arguments); }
114
+ public verbose(...args: any[]) { logger._logger.verbose.apply(logger._logger, arguments); }
115
+ public error(...args: any[]): Error { logger._logger.error.apply(logger._logger, arguments); return new Error(arguments[0]); }
116
+ public silly(...args: any[]) { logger._logger.silly.apply(logger._logger, arguments); }
117
+ public reject(sError: string): Promise<Error> {
118
+ logger.error(sError);
119
+ return Promise.reject(new Error(sError));
120
+ }
121
+ private isIncluded(byte: number, arr: number[]): boolean {
122
+ if (typeof (arr) === "undefined" || !arr || arr.length === 0) return true;
123
+ if (arr.indexOf(byte) !== -1) return true;
124
+ return false;
125
+ }
126
+ private isExcluded(byte: number, arr: number[]): boolean {
127
+ if (typeof (arr) === "undefined" || !arr) return false;
128
+ if (arr && arr.length === 0) return false;
129
+ if (arr.indexOf(byte) !== -1) return true;
130
+ return false;
131
+ }
132
+ public packet(msg: Message) {
133
+ // Emits to clients should not be gated by logging settings.
134
+ // Logging-to-file/console remains gated by packet config and filters.
135
+ webApp.emitToChannel('msgLogger', 'logMessage', msg);
136
+
137
+ // Filter out the messages we do not want to *log*.
138
+ let bLog: boolean = true;
139
+ if (logger.cfg.packet.enabled || logger.cfg.app.captureForReplay) {
140
+ // A random packet may actually find its way into the throws should the bytes get messed up
141
+ // in a fashion where the header byte is 255, 0, 255 but we have not identified the channel.
142
+ // Thus far we have seen 165 and 166.
143
+ const cfgPacket = logger.cfg.packet[msg.protocol] || logger.cfg.packet['unidentified'];
144
+ if (!logger.cfg.app.captureForReplay) {
145
+ // Log invalid messages no matter what if the user has selected invalid message logging.
146
+ if (bLog && !msg.isValid) {
147
+ if (!logger.cfg.packet.invalid) bLog = false;
148
+ }
149
+ else {
150
+ if (bLog && !cfgPacket.enabled) bLog = false;
151
+ if (bLog && !logger.isIncluded(msg.source, cfgPacket.includeSouce)) bLog = false;
152
+ if (bLog && !logger.isIncluded(msg.dest, cfgPacket.includeDest)) bLog = false;
153
+ if (bLog && !logger.isIncluded(msg.action, cfgPacket.includeActions)) bLog = false;
154
+ if (bLog && logger.isExcluded(msg.source, cfgPacket.excludeSource)) bLog = false;
155
+ if (bLog && logger.isExcluded(msg.dest, cfgPacket.excludeDest)) bLog = false;
156
+ if (bLog && logger.isExcluded(msg.action, cfgPacket.excludeActions)) bLog = false;
157
+ }
158
+ }
159
+
160
+ if (bLog && logger.cfg.packet.logToFile) {
161
+ logger.pkts.push(msg);
162
+ if (logger.pkts.length > 5)
163
+ logger.flushLogs();
164
+ else {
165
+ // Attempt to ease up on the writes if we are logging a bunch of packets.
166
+ if (logger.pktTimer) clearTimeout(logger.pktTimer);
167
+ logger.pktTimer = setTimeout(logger.flushLogs, 1000);
168
+ }
169
+ }
170
+ }
171
+ else {
172
+ bLog = false;
173
+ }
174
+
175
+ if (logger.cfg.packet.logToConsole && bLog) {
176
+ if (msg.isValid) logger._logger.info(msg.toLog());
177
+ else logger._logger.warn(msg.toLog());
178
+ }
179
+ }
180
+ public screenlogic(data: any){
181
+ if (logger.cfg.screenlogic.enabled || logger.cfg.app.captureForReplay){
182
+ if (logger.cfg.screenlogic.logToFile) {
183
+ logger.slMessages.push(data);
184
+ if (logger.slMessages.length > 5)
185
+ logger.flushSLLogs();
186
+ else {
187
+ // Attempt to ease up on the writes if we are logging a bunch of packets.
188
+ if (logger.pktTimer) clearTimeout(logger.pktTimer);
189
+ logger.pktTimer = setTimeout(logger.flushSLLogs, 1000);
190
+ }
191
+ }
192
+ webApp.emitToChannel('msgLogger', 'logMessage', data);
193
+
194
+ }
195
+ if (logger.cfg.screenlogic.logToConsole){
196
+ logger._logger.info(sl.toLog(data));
197
+ }
198
+ }
199
+ public logAPI(apiReq:string){
200
+ if (logger.cfg.app.captureForReplay){
201
+ // TODO: buffer this
202
+ fs.appendFile(logger.pktPath, apiReq, function(err) {
203
+ if (err) logger.error('Error writing packet to %s: %s', logger.pktPath, err);
204
+ });
205
+ }
206
+
207
+ }
208
+ public clearMessages() {
209
+ if (fs.existsSync(logger.pktPath)) {
210
+ logger.info(`Clearing message log: ${ logger.pktPath }`);
211
+ fs.truncateSync(logger.pktPath, 0);
212
+ }
213
+ }
214
+ public flushLogs() {
215
+ var p: Message[] = logger.pkts.splice(0, logger.pkts.length);
216
+ var buf: string = '';
217
+ if (logger.cfg.packet.enabled) {
218
+ for (let i = 0; i < p.length; i++) {
219
+
220
+ buf += (p[i].toLog() + os.EOL);
221
+ }
222
+ fs.appendFile(logger.pktPath, buf, function(err) {
223
+ if (err) logger.error('Error writing packet to %s: %s', logger.pktPath, err);
224
+ });
225
+ }
226
+ buf = '';
227
+ }
228
+ public flushSLLogs() {
229
+ var p: any[] = logger.slMessages.splice(0, logger.slMessages.length);
230
+ var buf: string = '';
231
+
232
+ for (let i = 0; i < p.length; i++) {
233
+ buf += (p[i] + os.EOL);
234
+ }
235
+ fs.appendFile(logger.pktPath, buf, function(err) {
236
+ if (err) logger.error(`Error writing screenlogic message to ${logger.pktPath}: ${err.message}`);
237
+ });
238
+
239
+ buf = '';
240
+ }
241
+ public setOptions(opts, c?: any) {
242
+ c = typeof c === 'undefined' ? this.cfg : c;
243
+ for (let prop in opts) {
244
+ let o = opts[prop];
245
+ if (o instanceof Array) {
246
+ //console.log({ o: o, c: c, prop: prop });
247
+ c[prop] = o; // Stop here we are replacing the array.
248
+ }
249
+ else if (typeof o === 'object') {
250
+ if (typeof c[prop] === 'undefined') c[prop] = {};
251
+ this.setOptions(o, c[prop]); // Use recursion here. Harder to follow but much less code.
252
+ }
253
+ else
254
+ c[prop] = opts[prop];
255
+ }
256
+ config.setSection('log', this.cfg);
257
+ if (utils.makeBool(this.cfg.app.logToFile)) {
258
+ if (typeof this.transports.consoleFile === 'undefined') {
259
+ this.transports.consoleFile = new winston.transports.File({
260
+ filename: path.join(process.cwd(), '/logs', this.getConsoleToFilePath()),
261
+ level: 'silly',
262
+ format: winston.format.combine(winston.format.splat(), winston.format.uncolorize(), this.myFormat)
263
+ });
264
+ this._logger.add(this.transports.consoleFile);
265
+ }
266
+ }
267
+ else {
268
+ if (typeof this.transports.consoleFile !== 'undefined') {
269
+ this._logger.remove(this.transports.consoleFile);
270
+ this.transports.consoleFile.close();
271
+ this.transports.consoleFile = undefined;
272
+ }
273
+ }
274
+ for (let [key, transport] of Object.entries(this.transports)) {
275
+ if(typeof transport !== 'undefined') transport.level = this.cfg.app.level;
276
+ }
277
+ }
278
+ public startCaptureForReplay(bResetLogs:boolean) {
279
+ this._captureInProgress = true;
280
+ logger.info(`Starting Replay Capture.`);
281
+ // start new replay directory
282
+
283
+ if (!fs.existsSync(this.captureForReplayBaseDir)) fs.mkdirSync(this.captureForReplayBaseDir, { recursive: true });
284
+
285
+ // Create logs subdirectory for additional log files
286
+ let logsSubDir = path.join(this.captureForReplayBaseDir, 'logs');
287
+ if (!fs.existsSync(logsSubDir)) fs.mkdirSync(logsSubDir, { recursive: true });
288
+ if (bResetLogs){
289
+ if (fs.existsSync(path.join(process.cwd(), 'data/poolConfig.json'))) {
290
+ fs.copyFileSync(path.join(process.cwd(), 'data/poolConfig.json'), path.join(process.cwd(),'data/', `poolConfig-${this.getLogTimestamp()}.json`));
291
+ fs.unlinkSync((path.join(process.cwd(), 'data/poolConfig.json')));
292
+ }
293
+ if (fs.existsSync(path.join(process.cwd(), 'data/poolState.json'))) {
294
+ fs.copyFileSync(path.join(process.cwd(), 'data/poolState.json'), path.join(process.cwd(),'data/', `poolState-${this.getLogTimestamp()}.json`));
295
+ fs.unlinkSync((path.join(process.cwd(), 'data/poolState.json')));
296
+ }
297
+ this.clearMessages();
298
+ }
299
+ logger.cfg = extend(true, {}, logger.cfg, {
300
+ "packet": {
301
+ "enabled": true,
302
+ "logToConsole": true,
303
+ "logToFile": true,
304
+ "invalid": true,
305
+ "broadcast": {
306
+ "enabled": true,
307
+ "includeActions": [],
308
+ "includeSource": [],
309
+ "includeDest": [],
310
+ "excludeActions": [],
311
+ "excludeSource": [],
312
+ "excludeDest": []
313
+ },
314
+ "pump": {
315
+ "enabled": true,
316
+ "includeActions": [],
317
+ "includeSource": [],
318
+ "includeDest": [],
319
+ "excludeActions": [],
320
+ "excludeSource": [],
321
+ "excludeDest": []
322
+ },
323
+ "chlorinator": {
324
+ "enabled": true,
325
+ "includeSource": [],
326
+ "includeDest": [],
327
+ "excludeSource": [],
328
+ "excludeDest": []
329
+ },
330
+ "intellichem": {
331
+ "enabled": true,
332
+ "includeActions": [],
333
+ "exclueActions": [],
334
+ "includeSource": [],
335
+ "includeDest": [],
336
+ "excludeSource": [],
337
+ "excludeDest": []
338
+ },
339
+ "intellivalve": {
340
+ "enabled": true,
341
+ "includeActions": [],
342
+ "exclueActions": [],
343
+ "includeSource": [],
344
+ "includeDest": [],
345
+ "excludeSource": [],
346
+ "excludeDest": []
347
+ },
348
+ "unidentified": {
349
+ "enabled": true,
350
+ "includeSource": [],
351
+ "includeDest": [],
352
+ "excludeSource": [],
353
+ "excludeDest": []
354
+ },
355
+ "unknown": {
356
+ "enabled": true,
357
+ "includeSource": [],
358
+ "includeDest": [],
359
+ "excludeSource": [],
360
+ "excludeDest": []
361
+ }
362
+ },
363
+ "app": {
364
+ "enabled": true,
365
+ "level": "silly",
366
+ "captureForReplay": true
367
+ }
368
+ });
369
+ this.consoleToFilePath = path.join(this.captureForReplayBaseDir, this.getConsoleToFilePath());
370
+ this.transports.file = new winston.transports.File({
371
+ filename: this.consoleToFilePath,
372
+ level: 'silly',
373
+ format: winston.format.combine(winston.format.splat(), winston.format.uncolorize(), this.myFormat)
374
+ });
375
+ logger._logger.add(this.transports.file);
376
+ this.transports.console.level = 'silly';
377
+ }
378
+ public async stopCaptureForReplayAsync(remLogs?: any[]):Promise<string> {
379
+ try {
380
+ if (!this._captureInProgress) {
381
+ logger.warn(`stopCaptureForReplayAsync called with no active capture session; creating backup without capture logs`);
382
+ }
383
+ // Get REM server configurations from config
384
+ let configData = config.getSection();
385
+ let remServers = [];
386
+ if (configData.web && configData.web.interfaces) {
387
+ for (let interfaceName in configData.web.interfaces) {
388
+ let interfaceConfig = configData.web.interfaces[interfaceName];
389
+ if (interfaceConfig.type === 'rem' && interfaceConfig.enabled) {
390
+ remServers.push({
391
+ name: interfaceConfig.name || interfaceName,
392
+ uuid: interfaceConfig.uuid,
393
+ host: interfaceConfig.options?.host || '',
394
+ backup: true
395
+ });
396
+ }
397
+ }
398
+ }
399
+
400
+ // Use the existing backup logic to create the base backup.
401
+ const ts = this.currentTimestamp || this.getLogTimestamp();
402
+ let backupOptions = {
403
+ njsPC: true,
404
+ servers: remServers,
405
+ name: `Packet Capture ${ts}`,
406
+ automatic: false
407
+ };
408
+ let backupFile = await webApp.backupServer(backupOptions);
409
+ // Add packet capture logs to the existing backup zip
410
+ let jszip = require("jszip");
411
+ let zip = await jszip.loadAsync(fs.readFileSync(backupFile.filePath));
412
+
413
+ // Add packet capture logs to the njsPC/logs directory if present.
414
+ if (typeof logger.pktPath === 'string' && logger.pktPath.length > 0 && fs.existsSync(logger.pktPath)) {
415
+ zip.file(`njsPC/logs/${path.basename(logger.pktPath)}`, fs.readFileSync(logger.pktPath));
416
+ } else {
417
+ logger.warn(`Packet capture log file unavailable during stopCaptureForReplayAsync; skipping packet log attachment`);
418
+ }
419
+ if (typeof this.consoleToFilePath === 'string' && this.consoleToFilePath.length > 0 && fs.existsSync(this.consoleToFilePath)) {
420
+ zip.file(`njsPC/logs/${path.basename(this.consoleToFilePath)}`, fs.readFileSync(this.consoleToFilePath));
421
+ } else {
422
+ logger.warn(`Console capture log file unavailable during stopCaptureForReplayAsync; skipping console log attachment`);
423
+ }
424
+
425
+ // Add REM server logs if provided.
426
+ if (remLogs && remLogs.length > 0) {
427
+ logger.info(`Adding ${remLogs.length} REM logs to backup`);
428
+ for (let remLog of remLogs) {
429
+ // Create logs directory for the REM server using the hardcoded name.
430
+ let logPath = `Relay Equipment Manager/logs/${remLog.logFileName}`;
431
+ logger.info(`Adding REM log to backup: ${logPath} (size: ${remLog.logData.length} characters)`);
432
+ zip.file(logPath, remLog.logData);
433
+ }
434
+ } else {
435
+ logger.info(`No REM logs provided to add to backup`);
436
+ }
437
+
438
+ // Generate the updated zip.
439
+ await zip.generateAsync({ type: 'nodebuffer' }).then(content => {
440
+ fs.writeFileSync(backupFile.filePath, content);
441
+ });
442
+ // Restore original logging configuration.
443
+ this.cfg = config.getSection('log');
444
+ if (typeof this.transports.file !== 'undefined') {
445
+ logger._logger.remove(this.transports.file);
446
+ this.transports.file.close();
447
+ this.transports.file = undefined;
448
+ }
449
+ this.transports.console.level = this.cfg.app.level;
450
+ this._captureInProgress = false;
451
+ return backupFile.filePath;
452
+ }
453
+ catch (err) {
454
+ this._captureInProgress = false;
455
+ return Promise.reject(err instanceof Error ? err.message : `${err}`);
456
+ }
457
+ }
458
+ }
459
+ export var logger = new Logger();