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
@@ -22,13 +22,16 @@ import { logger } from '../../logger/Logger';
22
22
  import * as net from 'net';
23
23
  import { setTimeout, setInterval } from 'timers';
24
24
  import { Message, Outbound, Inbound, Response } from './messages/Messages';
25
- import { MessageError, OutboundMessageError } from '../Errors';
25
+ import { InvalidOperationError, MessageError, OutboundMessageError } from '../Errors';
26
+ import { utils } from "../Constants";
27
+ import { webApp } from "../../web/Server";
26
28
  const extend = require("extend");
27
29
  export class Connection {
28
30
  constructor() {
29
31
  this.emitter = new EventEmitter();
30
32
  }
31
33
  public isOpen: boolean = false;
34
+ private _closing: boolean = false;
32
35
  private _cfg: any;
33
36
  private _port: any;
34
37
  public mockPort: boolean = false;
@@ -38,19 +41,49 @@ export class Connection {
38
41
  protected resetConnTimer(...args) {
39
42
  //console.log(`resetting connection timer`);
40
43
  if (conn.connTimer !== null) clearTimeout(conn.connTimer);
41
- if (!conn._cfg.mockPort && conn._cfg.inactivityRetry > 0) conn.connTimer = setTimeout(async () => {
44
+ if (!conn._cfg.mockPort && conn._cfg.inactivityRetry > 0 && !conn._closing) conn.connTimer = setTimeout(async () => {
42
45
  try {
43
- await conn.openAsync()
46
+ await conn.openAsync();
44
47
  }
45
- catch (err) {};
46
- }
47
- , conn._cfg.inactivityRetry * 1000);
48
+ catch (err) { logger.error(`Error resetting RS485 port on inactivity: ${err.message}`); };
49
+ }, conn._cfg.inactivityRetry * 1000);
48
50
  }
49
51
  public isRTS: boolean = true;
50
52
  public emitter: EventEmitter;
51
53
  public get enabled(): boolean { return typeof this._cfg !== 'undefined' && this._cfg.enabled; }
54
+ public async setPortAsync(data: any) : Promise<any> {
55
+ try {
56
+ // Lets set the config data.
57
+ let pdata = config.getSection('controller.comms', {});
58
+ pdata.enabled = typeof data.enabled !== 'undefined' ? utils.makeBool(data.enabled) : utils.makeBool(pdata.enabled);
59
+ pdata.netConnect = typeof data.netConnect !== 'undefined' ? utils.makeBool(data.netConnect) : utils.makeBool(pdata.netConnect);
60
+ pdata.rs485Port = typeof data.rs485Port !== 'undefined' ? data.rs485Port : pdata.rs485Port;
61
+ pdata.inactivityRetry = typeof data.inactivityRetry === 'number' ? data.inactivityRetry : pdata.inactivityRetry;
62
+ if (pdata.netConnect) {
63
+ pdata.netHost = typeof data.netHost !== 'undefined' ? data.netHost : pdata.netHost;
64
+ pdata.netPort = typeof data.netPort === 'number' ? data.netPort : pdata.netPort;
65
+ }
66
+ if (!await this.closeAsync()) {
67
+ return Promise.reject(new InvalidOperationError(`Unable to close the current RS485 port`, 'setPortAsync'));
68
+ }
69
+ config.setSection('controller.comms', pdata);
70
+ this._cfg = config.getSection('controller.comms', {
71
+ rs485Port: "/dev/ttyUSB0",
72
+ portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
73
+ mockPort: false,
74
+ netConnect: false,
75
+ netHost: "raspberrypi",
76
+ netPort: 9801,
77
+ inactivityRetry: 10
78
+ });
79
+ if (!await this.openAsync()) {
80
+ return Promise.reject(new InvalidOperationError(`Unable to open RS485 port ${pdata.rs485Port}`, 'setPortAsync'));
81
+ }
82
+ return this._cfg;
83
+ } catch (err) { return Promise.reject(err); }
84
+ }
52
85
  public async openAsync(): Promise<boolean> {
53
- if (typeof (this.buffer) === 'undefined') {
86
+ if (typeof this.buffer === 'undefined') {
54
87
  this.buffer = new SendRecieveBuffer();
55
88
  this.emitter.on('packetread', (pkt) => { this.buffer.pushIn(pkt); });
56
89
  this.emitter.on('messagewrite', (msg) => { this.buffer.pushOut(msg); });
@@ -64,18 +97,24 @@ export class Connection {
64
97
  let nc: net.Socket = new net.Socket();
65
98
  nc.on('connect', () => { logger.info(`Net connect (socat) connected to: ${this._cfg.netHost}:${this._cfg.netPort}`); }); // Socket is opened but not yet ready.
66
99
  nc.on('ready', () => {
100
+ this.isOpen = true;
101
+ this.isRTS = true;
67
102
  logger.info(`Net connect (socat) ready and communicating: ${this._cfg.netHost}:${this._cfg.netPort}`);
68
- nc.on('data', (data) => { if (data.length > 0 && !this.isPaused) this.emitter.emit('packetread', data); });
103
+ nc.on('data', (data) => {
104
+ //this.resetConnTimer();
105
+ if (data.length > 0 && !this.isPaused) this.emitter.emit('packetread', data);
106
+ });
69
107
  });
70
108
  nc.on('close', (p) => {
71
109
  this.isOpen = false;
72
110
  if (typeof this._port !== 'undefined') this._port.destroy();
73
111
  this._port = undefined;
112
+ this.buffer.clearOutbound();
74
113
  logger.info(`Net connect (socat) closed ${p === true ? 'due to error' : ''}: ${this._cfg.netHost}:${this._cfg.netPort}`);
75
114
  });
76
115
  nc.on('end', () => { // Happens when the other end of the socket closes.
77
116
  this.isOpen = false;
78
- this.resetConnTimer();
117
+ //this.resetConnTimer();
79
118
  logger.info(`Net connect (socat) end event was fired`);
80
119
  });
81
120
  //nc.on('drain', () => { logger.info(`The drain event was fired.`); });
@@ -83,18 +122,32 @@ export class Connection {
83
122
  // Occurs when there is no activity. This should not reset the connection, the previous implementation did so and
84
123
  // left the connection in a weird state where the previous connection was processing events and the new connection was
85
124
  // doing so as well. This isn't an error it is a warning as the RS485 bus will most likely be communicating at all times.
86
- nc.on('timeout', () => { logger.warn(`Net connect (socat) Connection Idle: ${this._cfg.netHost}:${this._cfg.netPort}`); });
87
- return new Promise<boolean>((resolve, _) => {
125
+ //nc.on('timeout', () => { logger.warn(`Net connect (socat) Connection Idle: ${this._cfg.netHost}:${this._cfg.netPort}`); });
126
+ nc.setTimeout(Math.max(this._cfg.inactivityRetry, 10) * 1000, async () => {
127
+ logger.warn(`Net connect (socat) connection idle: ${this._cfg.netHost}:${this._cfg.netPort} retrying connection.`);
128
+ try {
129
+ await conn.endAsync();
130
+ await conn.openAsync();
131
+ } catch (err) { logger.error(`Net connect (socat) error retrying connection ${err.message}`); }
132
+ });
133
+
134
+ return await new Promise<boolean>((resolve, _) => {
88
135
  // We only connect an error once as we will destroy this connection on error then recreate a new socket on failure.
89
136
  nc.once('error', (err) => {
90
- logger.error(`Net connect (socat) Connection: ${err}. ${this._cfg.inactivityRetry > 0 ? `Retry in ${this._cfg.inactivityRetry} seconds` : `Never retrying; inactivityRetry set to ${this._cfg.inactivityRetry}`}`);
91
- this.resetConnTimer();
137
+ //logger.error(`Net connect (socat) Connection: ${err}. ${this._cfg.inactivityRetry > 0 ? `Retry in ${this._cfg.inactivityRetry} seconds` : `Never retrying; inactivityRetry set to ${this._cfg.inactivityRetry}`}`);
138
+ //this.resetConnTimer();
92
139
  this.isOpen = false;
93
140
  // if the promise has already been fulfilled, but the error happens later, we don't want to call the promise again.
94
141
  if (typeof resolve !== 'undefined') { resolve(false); }
142
+ if (this._cfg.inactivityRetry > 0) {
143
+ logger.error(`Net connect (socat) connection error: ${err}. Retry in ${this._cfg.inactivityRetry} seconds`);
144
+ setTimeout(async () => { try { await conn.openAsync(); } catch (err) { } }, this._cfg.inactivityRetry * 1000);
145
+ }
146
+ else logger.error(`Net connect (socat) connection error: ${err}. Never retrying -- No retry time set`);
95
147
  });
96
148
  nc.connect(conn._cfg.netPort, conn._cfg.netHost, () => {
97
149
  if (typeof this._port !== 'undefined') logger.warn('Net connect (socat) recovered from lost connection.');
150
+ logger.info(`Net connect (socat) Connection connected`);
98
151
  this._port = nc;
99
152
  this.isOpen = true;
100
153
  resolve(true);
@@ -159,19 +212,117 @@ export class Connection {
159
212
  });
160
213
  }
161
214
  }
162
- public closeAsync() {
215
+ public async closeAsync(): Promise<boolean> {
163
216
  try {
164
- if (conn.connTimer) clearTimeout(conn.connTimer);
165
- if (typeof (conn._port) !== 'undefined' && conn._cfg.netConnect) {
166
- if (typeof (conn._port.destroy) !== 'function')
167
- conn._port.close(function (err) {
168
- if (err) logger.error('Error closing %s:%s', conn._cfg.netHost, conn._cfg.netPort);
169
- });
170
- else
171
- conn._port.destroy();
217
+ this._closing = true;
218
+ if (this.connTimer) clearTimeout(this.connTimer);
219
+ if (typeof this._port !== 'undefined' && this.isOpen) {
220
+ let success = await new Promise<boolean>((resolve, reject) => {
221
+ if (this._cfg.netConnect) {
222
+ this._port.removeAllListeners();
223
+ this._port.once('error', (err) => {
224
+ if (err) {
225
+ logger.error(`Error closing ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port}: ${err}`);
226
+ resolve(false);
227
+ }
228
+ else {
229
+ conn._port = undefined;
230
+ this.isOpen = false;
231
+ logger.info(`Successfully closed (socat) port ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port}`);
232
+ resolve(true);
233
+ }
234
+ });
235
+ this._port.once('close', (p) => {
236
+ this.isOpen = false;
237
+ this._port = undefined;
238
+ logger.info(`Net connect (socat) successfully closed: ${this._cfg.netHost}:${this._cfg.netPort}`);
239
+ resolve(true);
240
+ });
241
+ this._port.destroy();
242
+ }
243
+ else if (typeof conn._port.close === 'function') {
244
+ conn._port.close((err) => {
245
+ if (err) {
246
+ logger.error(`Error closing ${this._cfg.rs485Port}: ${err}`);
247
+ resolve(false);
248
+ }
249
+ else {
250
+ conn._port = undefined;
251
+ logger.info(`Successfully closed seral port ${this._cfg.rs485Port}`);
252
+ resolve(true);
253
+ this.isOpen = false;
254
+ }
255
+ });
256
+ }
257
+ else {
258
+ resolve(true);
259
+ conn._port = undefined;
260
+ }
261
+ });
262
+ if (success) {
263
+ if (typeof conn.buffer !== 'undefined') conn.buffer.close();
264
+ }
265
+
266
+ return success;
172
267
  }
173
- if (typeof conn.buffer !== 'undefined') conn.buffer.close();
174
- } catch (err) { logger.error(`Error closing comms connection: ${err.message}`); }
268
+ return true;
269
+ } catch (err) { logger.error(`Error closing comms connection: ${err.message}`); return Promise.resolve(false); }
270
+ }
271
+ public async endAsync(): Promise<boolean> {
272
+ try {
273
+ this._closing = true;
274
+ if (this.connTimer) clearTimeout(this.connTimer);
275
+ if (typeof this._port !== 'undefined' && this.isOpen) {
276
+ let success = await new Promise<boolean>((resolve, reject) => {
277
+ if (this._cfg.netConnect) {
278
+ this._port.removeAllListeners();
279
+ this._port.once('error', (err) => {
280
+ if (err) {
281
+ logger.error(`Error closing ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port}: ${err}`);
282
+ resolve(false);
283
+ }
284
+ else {
285
+ conn._port = undefined;
286
+ this.isOpen = false;
287
+ logger.info(`Successfully closed (socat) port ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port}`);
288
+ resolve(true);
289
+ }
290
+ });
291
+ this._port.once('close', (p) => {
292
+ this.isOpen = false;
293
+ this._port = undefined;
294
+ logger.info(`Net connect (socat) successfully closed: ${this._cfg.netHost}:${this._cfg.netPort}`);
295
+ resolve(true);
296
+ });
297
+ this._port.destroy();
298
+ }
299
+ else if (typeof conn._port.close === 'function') {
300
+ conn._port.close((err) => {
301
+ if (err) {
302
+ logger.error(`Error closing ${this._cfg.rs485Port}: ${err}`);
303
+ resolve(false);
304
+ }
305
+ else {
306
+ conn._port = undefined;
307
+ logger.info(`Successfully closed seral port ${this._cfg.rs485Port}`);
308
+ resolve(true);
309
+ this.isOpen = false;
310
+ }
311
+ });
312
+ }
313
+ else {
314
+ resolve(true);
315
+ conn._port = undefined;
316
+ }
317
+ });
318
+ if (success) {
319
+ if (typeof conn.buffer !== 'undefined') conn.buffer.close();
320
+ }
321
+ return success;
322
+ }
323
+ return true;
324
+ } catch (err) { logger.error(`Error closing comms connection: ${err.message}`); return Promise.resolve(false); }
325
+ finally { this._closing = false; }
175
326
  }
176
327
  public drain(cb: Function) {
177
328
  if (typeof (conn._port.drain) === 'function')
@@ -265,12 +416,13 @@ export class SendRecieveBuffer {
265
416
  private _waitingPacket: Outbound;
266
417
  private _msg: Inbound;
267
418
  public pushIn(pkt) {
268
- conn.buffer._inBuffer.push.apply(conn.buffer._inBuffer, pkt.toJSON().data); setTimeout(() => { this.processPackets(); }, 0);
419
+ let self = this;
420
+ conn.buffer._inBuffer.push.apply(conn.buffer._inBuffer, pkt.toJSON().data); setTimeout(() => { self.processPackets(); }, 0);
269
421
  }
270
422
  public pushOut(msg) { conn.buffer._outBuffer.push(msg); setTimeout(() => { this.processPackets(); }, 0); }
271
423
  public clear() { conn.buffer._inBuffer.length = 0; conn.buffer._outBuffer.length = 0; }
272
424
  public close() { clearTimeout(conn.buffer.procTimer); conn.buffer.clear(); this._msg = undefined; }
273
-
425
+ public clearOutbound() { conn.buffer._outBuffer.length = 0; conn.buffer._waitingPacket = undefined; }
274
426
  /********************************************************************
275
427
  * RKS: 06-06-20
276
428
  * This used to process every 175ms. While the processing was light
@@ -303,6 +455,7 @@ export class SendRecieveBuffer {
303
455
  let dt = new Date();
304
456
  if (conn.buffer._waitingPacket.timestamp.getTime() + timeout < dt.getTime()) {
305
457
  logger.silly(`Retrying outbound message after ${(dt.getTime() - conn.buffer._waitingPacket.timestamp.getTime()) / 1000} secs with ${conn.buffer._waitingPacket.remainingTries} attempt(s) left. - ${conn.buffer._waitingPacket.toShortPacket()} `);
458
+ conn.buffer.counter.sndRetries++;
306
459
  conn.buffer.writeMessage(conn.buffer._waitingPacket);
307
460
  }
308
461
  return true;
@@ -339,7 +492,8 @@ export class SendRecieveBuffer {
339
492
  // would be sitting idle for eternity.
340
493
  if (conn.buffer._outBuffer.length > 0 || typeof conn.buffer._waitingPacket !== 'undefined' || conn.buffer._waitingPacket || typeof msg !== 'undefined') {
341
494
  // Come back later as we still have items to send.
342
- conn.buffer.procTimer = setTimeout(() => this.processPackets(), 100);
495
+ let self = this;
496
+ conn.buffer.procTimer = setTimeout(() => self.processPackets(), 100);
343
497
  }
344
498
  }
345
499
  /*
@@ -375,6 +529,7 @@ export class SendRecieveBuffer {
375
529
  setTimeout(msg.response.callback, 100, msg);
376
530
  }
377
531
  }
532
+ conn.buffer.counter.sndAborted++;
378
533
  conn.isRTS = true;
379
534
  return;
380
535
  }
@@ -396,21 +551,26 @@ export class SendRecieveBuffer {
396
551
  let error = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${err} `);
397
552
  if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
398
553
  conn.buffer._waitingPacket = null;
554
+ conn.buffer.counter.sndAborted++;
399
555
  }
400
556
  }
401
557
  else {
402
558
  logger.verbose(`Wrote packet[${bytes}].Retries remaining: ${msg.remainingTries} `);
403
559
  // We have all the success we are going to get so if the call succeeded then
404
560
  // don't set the waiting packet when we aren't actually waiting for a response.
561
+ conn.buffer.counter.sndSuccess++;
405
562
  if (!msg.requiresResponse) {
406
563
  // As far as we know the message made it to OCP.
407
564
  conn.buffer._waitingPacket = null;
408
565
  if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
566
+
409
567
  }
410
568
  else if (msg.remainingTries >= 0) {
411
569
  conn.buffer._waitingPacket = msg;
412
570
  }
413
571
  }
572
+ conn.buffer.counter.updatefailureRate();
573
+ webApp.emitToChannel('rs485PortStats', 'rs485Stats', conn.buffer.counter);
414
574
  });
415
575
  }
416
576
  }
@@ -439,7 +599,9 @@ export class SendRecieveBuffer {
439
599
  let out = conn.buffer._outBuffer[i--];
440
600
  if (typeof out === 'undefined') continue;
441
601
  let resp = out.response;
442
- if (out.requiresResponse) {
602
+ // RG - added check for msgOut because the *Touch chlor packet 153 adds an status packet 217
603
+ // but if it is the only packet on the queue the outbound will have been cleared out already.
604
+ if (out.requiresResponse && msgOut !== null) {
443
605
  if (resp instanceof Response && resp.isResponse(msgIn, out) && (typeof out.scope === 'undefined' || out.scope === msgOut.scope)) {
444
606
  resp.message = msgIn;
445
607
  if (typeof (resp.callback) === 'function' && resp.callback) callback = resp.callback;
@@ -456,20 +618,21 @@ export class SendRecieveBuffer {
456
618
  private processCompletedMessage(msg: Inbound, ndx): number {
457
619
  msg.timestamp = new Date();
458
620
  msg.id = Message.nextMessageId;
459
- conn.buffer.counter.collisions += msg.collisions;
621
+ conn.buffer.counter.recCollisions += msg.collisions;
622
+ logger.packet(msg);
623
+ webApp.emitToChannel('rs485PortStats', 'rs485Stats', conn.buffer.counter);
460
624
  if (msg.isValid) {
461
- conn.buffer.counter.success++;
625
+ conn.buffer.counter.recSuccess++;
462
626
  conn.buffer.counter.updatefailureRate();
463
627
  msg.process();
464
628
  conn.buffer.clearResponses(msg);
465
629
  }
466
630
  else {
467
- conn.buffer.counter.failed++;
631
+ conn.buffer.counter.recFailed++;
468
632
  conn.buffer.counter.updatefailureRate();
469
- console.log('RS485 Stats:' + JSON.stringify(conn.buffer.counter));
633
+ console.log('RS485 Stats:' + conn.buffer.counter.toLog());
470
634
  ndx = this.rewindFailedMessage(msg, ndx);
471
635
  }
472
- logger.packet(msg);
473
636
  return ndx;
474
637
  }
475
638
  private rewindFailedMessage(msg: Inbound, ndx: number): number {
@@ -513,20 +676,34 @@ export class SendRecieveBuffer {
513
676
  export class Counter {
514
677
  constructor() {
515
678
  this.bytesReceived = 0;
516
- this.success = 0;
517
- this.failed = 0;
679
+ this.recSuccess = 0;
680
+ this.recFailed = 0;
681
+ this.recCollisions = 0;
518
682
  this.bytesSent = 0;
519
- this.collisions = 0;
520
- this.failureRate = '0.00%';
683
+ this.sndAborted = 0;
684
+ this.sndRetries = 0;
685
+ this.sndSuccess = 0;
686
+ this.recFailureRate = 0;
687
+ this.sndFailureRate = 0;
521
688
  }
522
689
  public bytesReceived: number;
523
- public success: number;
524
- public failed: number;
525
690
  public bytesSent: number;
526
- public collisions: number;
527
- public failureRate: string;
691
+ public recSuccess: number;
692
+ public recFailed: number;
693
+ public recCollisions: number;
694
+ public recFailureRate: number;
695
+ public sndSuccess: number;
696
+ public sndAborted: number;
697
+ public sndRetries: number;
698
+ public sndFailureRate: number;
528
699
  public updatefailureRate(): void {
529
- conn.buffer.counter.failureRate = `${(conn.buffer.counter.failed / (conn.buffer.counter.failed + conn.buffer.counter.success) * 100).toFixed(2)}% `;
700
+ conn.buffer.counter.recFailureRate = (this.recFailed + this.recSuccess) !== 0 ? (this.recFailed / (this.recFailed + this.recSuccess) * 100) : 0;
701
+ conn.buffer.counter.sndFailureRate = (this.sndAborted + this.sndSuccess) !== 0 ? (this.sndAborted / (this.sndAborted + this.sndSuccess) * 100) : 0;
702
+ //conn.buffer.counter.recFailureRate = `${(conn.buffer.counter.recFailed / (conn.buffer.counter.recFailed + conn.buffer.counter.recSuccess) * 100).toFixed(2)}% `;
703
+ //conn.buffer.counter.sndFailureRate = `${(conn.buffer.counter.sndAborted / (conn.buffer.counter.sndAborted + conn.buffer.counter.sndSuccess) * 100).toFixed(2)}% `;
704
+ }
705
+ public toLog(): string {
706
+ return `{ "bytesReceived": ${this.bytesReceived} "success": ${this.recSuccess}, "failed": ${this.recFailed}, "bytesSent": ${this.bytesSent}, "collisions": ${this.recCollisions}, "failureRate": ${this.recFailureRate.toFixed(2)}% }`;
530
707
  }
531
708
  }
532
709
  export var conn: Connection = new Connection();
@@ -168,7 +168,13 @@ export class Inbound extends Message {
168
168
  return `{"id":${this.id},"valid":${this.isValid},"dir":"${this.direction}","proto":"${this.protocol}","for":${JSON.stringify(this.responseFor)},"pkt":[${JSON.stringify(this.padding)},${JSON.stringify(this.preamble)},${JSON.stringify(this.header)},${JSON.stringify(this.payload)},${JSON.stringify(this.term)}],"ts": "${Timestamp.toISOLocal(this.timestamp)}"}`;
169
169
  return `{"id":${this.id},"valid":${this.isValid},"dir":"${this.direction}","proto":"${this.protocol}","pkt":[${JSON.stringify(this.padding)},${JSON.stringify(this.preamble)},${JSON.stringify(this.header)},${JSON.stringify(this.payload)},${JSON.stringify(this.term)}],"ts": "${Timestamp.toISOLocal(this.timestamp)}"}`;
170
170
  }
171
- private testChlorHeader(bytes: number[], ndx: number): boolean { return (ndx + 1 < bytes.length && bytes[ndx] === 16 && bytes[ndx + 1] === 2); }
171
+ private testChlorHeader(bytes: number[], ndx: number): boolean {
172
+ // if packets have 16,2 (eg status=16,2,29) in them and they come as partial packets, they would have
173
+ // prev been detected as chlor packets;
174
+ // valid chlor packets should have 16,2,0 or 16,2,[80-96];
175
+ // this should reduce the number of false chlor packets
176
+ return (ndx + 2 < bytes.length && bytes[ndx] === 16 && bytes[ndx + 1] === 2 && (bytes[ndx + 2] === 0 || (bytes[ndx + 2] >= 80 && bytes[ndx + 2] <= 96)))
177
+ }
172
178
  private testBroadcastHeader(bytes: number[], ndx: number): boolean { return ndx < bytes.length - 3 && bytes[ndx] === 255 && bytes[ndx + 1] === 0 && bytes[ndx + 2] === 255 && bytes[ndx + 3] === 165; }
173
179
  private testUnidentifiedHeader(bytes: number[], ndx: number): boolean { return ndx < bytes.length - 3 && bytes[ndx] === 255 && bytes[ndx + 1] === 0 && bytes[ndx + 2] === 255 && bytes[ndx + 3] !== 165; }
174
180
  private testChlorTerm(bytes: number[], ndx: number): boolean { return ndx < bytes.length - 2 && bytes[ndx + 1] === 16 && bytes[ndx + 2] === 3; }
@@ -399,6 +405,7 @@ export class Inbound extends Message {
399
405
  case ControllerType.IntelliCenter:
400
406
  switch (this.action) {
401
407
  case 1: // ACK
408
+ this.isProcessed = true;
402
409
  break;
403
410
  case 2:
404
411
  case 204:
@@ -418,12 +425,14 @@ export class Inbound extends Message {
418
425
  break;
419
426
  case 222: // A panel is asking for action 30s
420
427
  case 228: // A panel is asking for the current version
428
+ this.isProcessed = true;
421
429
  break;
422
430
  default:
423
431
  logger.info(`An unprocessed message was received ${this.toPacket()}`)
424
432
  break;
425
433
 
426
434
  }
435
+ if (!this.isProcessed) logger.info(`The message was not processed ${this.action} - ${this.toPacket()}`);
427
436
  break;
428
437
  default:
429
438
  switch (this.action) {
@@ -495,17 +504,19 @@ export class Inbound extends Message {
495
504
  case 9:
496
505
  case 16:
497
506
  case 34:
498
- case 114:
499
507
  case 137:
500
508
  case 144:
501
509
  case 162:
502
510
  HeaterMessage.process(this);
503
511
  break;
512
+ case 114:
513
+ case 115:
514
+ HeaterStateMessage.process(this);
515
+ break
504
516
  case 147:
505
517
  IntellichemMessage.process(this);
506
518
  break;
507
519
  default:
508
- // take these out...
509
520
  if (this.action === 109 && this.payload[1] === 3) break;
510
521
  if (this.source === 17 && this.payload[0] === 109) break;
511
522
  logger.debug(`Packet not processed: ${this.toPacket()}`);
@@ -568,6 +579,7 @@ class OutboundCommon extends Message {
568
579
  case Protocol.IntelliValve:
569
580
  case Protocol.Unidentified:
570
581
  case Protocol.IntelliChem:
582
+ case Protocol.Heater:
571
583
  this.chkHi = Math.floor(sum / 256);
572
584
  this.chkLo = (sum - (super.chkHi * 256));
573
585
  break;
@@ -597,7 +609,7 @@ export class Outbound extends OutboundCommon {
597
609
  this.header.push.apply(this.header, [165, Message.headerSubByte, 15, Message.pluginAddress, 0, 0]);
598
610
  this.term.push.apply(this.term, [0, 0]);
599
611
  }
600
- else if (proto === Protocol.Pump || proto === Protocol.IntelliValve || proto === Protocol.IntelliChem) {
612
+ else if (proto === Protocol.Pump || proto === Protocol.IntelliValve || proto === Protocol.IntelliChem || proto === Protocol.Heater) {
601
613
  this.preamble.push.apply(this.preamble, [255, 0, 255]);
602
614
  this.header.push.apply(this.header, [165, 0, 15, Message.pluginAddress, 0, 0]);
603
615
  this.term.push.apply(this.term, [0, 0]);
@@ -27,15 +27,19 @@ export class ChlorinatorMessage {
27
27
  chlorId = 1;
28
28
  for (let i = 0; i < 4 && i + 30 < msg.payload.length; i++) {
29
29
  let isActive = msg.extractPayloadByte(i + 22) === 1;
30
+ chlor = sys.chlorinators.getItemById(chlorId);
31
+ if (chlor.master === 1) continue; // RSG: probably never need this. See Touch chlor below.
30
32
  if (isActive) {
31
- let chlor = sys.chlorinators.getItemById(chlorId, true);
33
+ chlor = sys.chlorinators.getItemById(chlorId, true);
32
34
  let schlor = state.chlorinators.getItemById(chlor.id, true);
33
35
  chlor.isActive = schlor.isActive = true;
34
36
  chlor.body = msg.extractPayloadByte(i + 2);
35
37
  chlor.type = msg.extractPayloadByte(i + 6);
36
- if (!chlor.disabled) {
38
+ if (!chlor.disabled && !chlor.isDosing) {
37
39
  // RKS: We don't want to change the setpoints if our chem controller disabled
38
40
  // the chlorinator. These should be 0.
41
+ if (msg.extractPayloadByte(i + 10) === 0) logger.info(`Changing pool setpoint to 0 ${msg.extractPayloadByte(i + 10)}`);
42
+
39
43
  chlor.poolSetpoint = msg.extractPayloadByte(i + 10);
40
44
  chlor.spaSetpoint = msg.extractPayloadByte(i + 14);
41
45
  }
@@ -57,6 +61,7 @@ export class ChlorinatorMessage {
57
61
  }
58
62
  chlorId++;
59
63
  }
64
+ msg.isProcessed = true;
60
65
  break;
61
66
  default:
62
67
  logger.debug(`Unprocessed Config Message ${msg.toPacket()}`)
@@ -65,6 +70,8 @@ export class ChlorinatorMessage {
65
70
  }
66
71
  public static processTouch(msg: Inbound) {
67
72
  // This is for the 25 message that is broadcast from the OCP.
73
+ let chlor = sys.chlorinators.getItemById(1);
74
+ if (chlor.master === 1) return; // Some Aquarite chlors need more frequent control (via Nixie) but will be disabled via Touch. https://github.com/tagyoureit/nodejs-poolController/issues/349
68
75
  let isActive = (msg.extractPayloadByte(0) & 0x01) === 1;
69
76
  if (isActive) {
70
77
  let chlor = sys.chlorinators.getItemById(1, true);
@@ -78,7 +85,9 @@ export class ChlorinatorMessage {
78
85
  chlor.address = chlor.id + 79;
79
86
  schlor.body = chlor.body = sys.equipment.maxBodies >= 1 || sys.equipment.shared === true ? 32 : 0;
80
87
  }
81
- schlor.name = chlor.name = msg.extractPayloadString(6, 16);
88
+ if (typeof chlor.name === 'undefined') schlor.name = chlor.name = msg.extractPayloadString(6, 16);
89
+ if (typeof chlor.model === 'undefined') chlor.model = sys.board.valueMaps.chlorinatorModel.getValue(schlor.name.toLowerCase());
90
+ if (typeof chlor.type === 'undefined') chlor.type = schlor.type = 0;
82
91
  schlor.saltLevel = msg.extractPayloadByte(3) * 50 || schlor.saltLevel;
83
92
  schlor.status = msg.extractPayloadByte(4) & 0x007F; // Strip off the high bit. The chlorinator does not actually report this.;
84
93
  // Pull the hours from the 25 message.
@@ -109,5 +118,6 @@ export class ChlorinatorMessage {
109
118
  sys.chlorinators.removeItemById(1);
110
119
  state.chlorinators.removeItemById(1);
111
120
  }
121
+ msg.isProcessed = true;
112
122
  }
113
123
  }
@@ -80,6 +80,7 @@ export class CircuitGroupMessage {
80
80
  group.circuits.removeItemByIndex(i);
81
81
  }
82
82
  }
83
+ msg.isProcessed = true;
83
84
  }
84
85
  else if (msgId >= 16 && msgId <= 31) {
85
86
  groupId = msgId - 16 + sys.board.equipmentIds.circuitGroups.start;
@@ -90,6 +91,7 @@ export class CircuitGroupMessage {
90
91
  group.name = msg.extractPayloadString(2, 16);
91
92
  sgroup.name = group.name;
92
93
  }
94
+ msg.isProcessed = true;
93
95
  }
94
96
  }
95
97
  }
@@ -143,6 +145,7 @@ export class CircuitGroupMessage {
143
145
  state.circuitGroups.removeItemById(groupId);
144
146
  }
145
147
  state.emitEquipmentChanges();
148
+ msg.isProcessed = true;
146
149
  }
147
150
  private static processGroupType(msg: Inbound) {
148
151
  var groupId = ((msg.extractPayloadByte(1) - 32) * 16) + sys.board.equipmentIds.circuitGroups.start;
@@ -188,6 +191,7 @@ export class CircuitGroupMessage {
188
191
  sgroup.type = group.type;
189
192
  }
190
193
  state.emitEquipmentChanges();
194
+ msg.isProcessed = true;
191
195
  }
192
196
  private static processColor(msg: Inbound) {
193
197
  var groupId = ((msg.extractPayloadByte(1) - 35)) + sys.board.equipmentIds.circuitGroups.start;
@@ -208,6 +212,7 @@ export class CircuitGroupMessage {
208
212
  }
209
213
 
210
214
  }
215
+ msg.isProcessed = true;
211
216
  }
212
217
  private static processEggTimer(msg: Inbound) {
213
218
  var groupId = ((msg.extractPayloadByte(1) - 34) * 16) + sys.board.equipmentIds.circuitGroups.start;
@@ -220,5 +225,6 @@ export class CircuitGroupMessage {
220
225
  // sgroup.eggTimer = group.eggTimer;
221
226
  }
222
227
  }
228
+ msg.isProcessed = true;
223
229
  }
224
230
  }
@@ -250,7 +250,7 @@ export class CircuitMessage {
250
250
  circuit.freeze = (functionId & 64) === 64;
251
251
  circuit.showInFeatures = typeof circuit.showInFeatures === 'undefined' ? true : circuit.showInFeatures;
252
252
  circuit.isActive = _isActive;
253
- if (typeof circuit.eggTimer === 'undefined') circuit.eggTimer = 720;
253
+ if (typeof circuit.eggTimer === 'undefined' || circuit.eggTimer === 0) circuit.eggTimer = 720;
254
254
  if (typeof circuit.dontStop === 'undefined') circuit.dontStop = circuit.eggTimer === 1620;
255
255
  if ([9, 10, 16, 17].includes(circuit.type)) {
256
256
  const lg = sys.lightGroups.getItemById(sys.board.equipmentIds.circuitGroups.start, true);
@@ -32,6 +32,7 @@ export class CoverMessage {
32
32
  for (let i = 1; i < 10; i++) {
33
33
  if (msg.extractPayloadByte(i + 18) !== 255) cover.circuits.push(msg.extractPayloadByte(i + 18));
34
34
  }
35
+ msg.isProcessed = true;
35
36
  break;
36
37
  default:
37
38
  logger.debug(`Unprocessed Config Message ${msg.toPacket()}`)
@@ -48,6 +48,7 @@ export class EquipmentMessage {
48
48
  body.capacity = msg.extractPayloadByte(34) * 1000;
49
49
  if (body.isActive && sys.equipment.maxBodies === 0) sys.bodies.removeItemById(1);
50
50
  body.isActive = sys.equipment.maxBodies > 0;
51
+ msg.isProcessed = true;
51
52
  break;
52
53
  case 1:
53
54
  pnl = sys.equipment.expansions.getItemById(2);
@@ -66,6 +67,7 @@ export class EquipmentMessage {
66
67
  }
67
68
  pnl = sys.equipment.expansions.getItemById(3);
68
69
  pnl.name = msg.extractPayloadString(18, 16);
70
+ msg.isProcessed = true;
69
71
  break;
70
72
  case 2:
71
73
  // The first name is the first body in this packet and the second is the third. Go figure.
@@ -87,6 +89,7 @@ export class EquipmentMessage {
87
89
  sys.bodies.removeItemById(bodyId);
88
90
  state.temps.bodies.removeItemById(bodyId);
89
91
  }
92
+ msg.isProcessed = true;
90
93
  break;
91
94
  case 3:
92
95
  // The first name is the second body and the 2nd is the 4th. This packet also contains
@@ -136,6 +139,7 @@ export class EquipmentMessage {
136
139
  state.equipment.maxValves = sys.equipment.maxValves;
137
140
  state.equipment.maxSchedules = sys.equipment.maxSchedules;
138
141
  state.equipment.maxPumps = sys.equipment.maxPumps;
142
+ msg.isProcessed = true;
139
143
  break;
140
144
  default:
141
145
  logger.debug(`Unprocessed Config Message ${msg.toPacket()}`)