nodejs-poolcontroller 7.3.0 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/Changelog +23 -0
  3. package/README.md +5 -5
  4. package/app.ts +2 -0
  5. package/config/Config.ts +3 -0
  6. package/config/VersionCheck.ts +8 -4
  7. package/controller/Constants.ts +88 -0
  8. package/controller/Equipment.ts +246 -66
  9. package/controller/Errors.ts +24 -1
  10. package/controller/Lockouts.ts +423 -0
  11. package/controller/State.ts +314 -54
  12. package/controller/boards/EasyTouchBoard.ts +107 -59
  13. package/controller/boards/IntelliCenterBoard.ts +186 -125
  14. package/controller/boards/IntelliTouchBoard.ts +104 -30
  15. package/controller/boards/NixieBoard.ts +721 -159
  16. package/controller/boards/SystemBoard.ts +2370 -1108
  17. package/controller/comms/Comms.ts +85 -10
  18. package/controller/comms/messages/Messages.ts +10 -4
  19. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -4
  20. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  21. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  22. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  23. package/controller/comms/messages/config/ExternalMessage.ts +44 -26
  24. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  25. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  26. package/controller/comms/messages/config/HeaterMessage.ts +15 -9
  27. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  28. package/controller/comms/messages/config/OptionsMessage.ts +13 -1
  29. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  30. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  31. package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
  32. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  33. package/controller/comms/messages/config/ValveMessage.ts +13 -3
  34. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
  35. package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
  36. package/controller/comms/messages/status/HeaterStateMessage.ts +42 -9
  37. package/controller/comms/messages/status/IntelliChemStateMessage.ts +37 -26
  38. package/controller/nixie/Nixie.ts +18 -16
  39. package/controller/nixie/bodies/Body.ts +4 -1
  40. package/controller/nixie/chemistry/ChemController.ts +80 -77
  41. package/controller/nixie/chemistry/Chlorinator.ts +9 -8
  42. package/controller/nixie/circuits/Circuit.ts +55 -6
  43. package/controller/nixie/heaters/Heater.ts +192 -32
  44. package/controller/nixie/pumps/Pump.ts +146 -84
  45. package/controller/nixie/schedules/Schedule.ts +3 -2
  46. package/controller/nixie/valves/Valve.ts +1 -1
  47. package/defaultConfig.json +32 -1
  48. package/issue_template.md +1 -1
  49. package/logger/DataLogger.ts +37 -22
  50. package/package.json +20 -18
  51. package/web/Server.ts +520 -29
  52. package/web/bindings/influxDB.json +96 -8
  53. package/web/bindings/mqtt.json +151 -40
  54. package/web/bindings/mqttAlt.json +114 -4
  55. package/web/interfaces/httpInterface.ts +2 -0
  56. package/web/interfaces/influxInterface.ts +36 -19
  57. package/web/interfaces/mqttInterface.ts +14 -3
  58. package/web/services/config/Config.ts +171 -44
  59. package/web/services/state/State.ts +49 -5
  60. package/web/services/state/StateSocket.ts +18 -1
@@ -4,12 +4,13 @@ import { logger } from '../../../logger/Logger';
4
4
 
5
5
  import { NixieEquipment, NixieChildEquipment, NixieEquipmentCollection, INixieControlPanel } from "../NixieEquipment";
6
6
  import { Heater, HeaterCollection, sys } from "../../../controller/Equipment";
7
- import { HeaterState, state, } from "../../State";
7
+ import { BodyTempState, HeaterState, state, } from "../../State";
8
8
  import { setTimeout, clearTimeout } from 'timers';
9
9
  import { NixieControlPanel } from '../Nixie';
10
10
  import { webApp, InterfaceServerResponse } from "../../../web/Server";
11
11
  import { conn } from '../../../controller/comms/Comms';
12
12
  import { Outbound, Protocol, Response } from '../../../controller/comms/messages/Messages';
13
+ import { delayMgr } from '../../Lockouts';
13
14
 
14
15
  export class NixieHeaterCollection extends NixieEquipmentCollection<NixieHeaterBase> {
15
16
  public async deleteHeaterAsync(id: number) {
@@ -96,6 +97,8 @@ export class NixieHeaterBase extends NixieEquipment {
96
97
  protected bodyOnTime: number;
97
98
  protected isOn: boolean = false;
98
99
  protected isCooling: boolean = false;
100
+ protected lastHeatCycle: Date;
101
+ protected lastCoolCycle: Date;
99
102
  constructor(ncp: INixieControlPanel, heater: Heater) {
100
103
  super(ncp);
101
104
  this.heater = heater;
@@ -103,6 +106,7 @@ export class NixieHeaterBase extends NixieEquipment {
103
106
  public get suspendPolling(): boolean { return this._suspendPolling > 0; }
104
107
  public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
105
108
  public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
109
+ public getCooldownTime() { return 0; }
106
110
  public static create(ncp: INixieControlPanel, heater: Heater): NixieHeaterBase {
107
111
  let type = sys.board.valueMaps.heaterTypes.transform(heater.type);
108
112
  switch (type.name) {
@@ -144,34 +148,50 @@ export class NixieHeaterBase extends NixieEquipment {
144
148
  }
145
149
  export class NixieGasHeater extends NixieHeaterBase {
146
150
  public pollingInterval: number = 10000;
147
- public heater: Heater;
151
+ //declare heater: Heater;
148
152
  constructor(ncp: INixieControlPanel, heater: Heater) {
149
153
  super(ncp, heater);
150
154
  this.heater = heater;
151
155
  this.pollEquipmentAsync();
152
156
  }
153
157
  public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
158
+ public getCooldownTime(): number {
159
+ // Delays are always in terms of seconds so convert the minute to seconds.
160
+ if (this.heater.cooldownDelay === 0 || typeof this.lastHeatCycle === 'undefined') return 0;
161
+ let now = new Date().getTime();
162
+ let cooldown = this.isOn ? this.heater.cooldownDelay * 60000 : Math.round( ((this.lastHeatCycle.getDate() + this.heater.cooldownDelay * 60000) - now) / 1000);
163
+ return Math.min(Math.max(0, cooldown), this.heater.cooldownDelay * 60);
164
+ }
154
165
  public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
155
166
  try {
156
167
  // Initialize the desired state.
157
168
  this.isOn = isOn;
158
169
  this.isCooling = false;
170
+ let target = hstate.startupDelay === false && isOn;
171
+ if (target && typeof hstate.endTime !== 'undefined') {
172
+ // Calculate a short cycle time so that the solar heater does not cycle
173
+ // too often. For gas heaters this is 60 seconds. This gives enough time
174
+ // for the heater control circuit to make a full cycle.
175
+ if (new Date().getTime() - hstate.endTime.getTime() < 60000) {
176
+ logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
177
+ target = false;
178
+ }
179
+ }
159
180
  // Here we go we need to set the firemans switch state.
160
- if (hstate.isOn !== isOn) {
181
+ if (hstate.isOn !== target) {
161
182
  logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
162
183
  }
163
- if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
164
- hstate.isOn = isOn;
165
- return;
166
- }
167
- if (typeof this._lastState === 'undefined' || isOn || this._lastState !== isOn) {
168
- let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`, { isOn: isOn, latch: isOn ? 10000 : undefined });
169
- if (res.status.code === 200) this._lastState = hstate.isOn = isOn;
170
- else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
171
- }
172
- else {
173
- hstate.isOn = isOn;
174
- return;
184
+ if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
185
+ if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
186
+ this._lastState = hstate.isOn = target;
187
+ }
188
+ else {
189
+ let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
190
+ { isOn: target, latch: target ? 10000 : undefined });
191
+ if (res.status.code === 200) this._lastState = hstate.isOn = target;
192
+ else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
193
+ }
194
+ if (target) this.lastHeatCycle = new Date();
175
195
  }
176
196
  } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
177
197
  }
@@ -219,7 +239,7 @@ export class NixieGasHeater extends NixieHeaterBase {
219
239
  }
220
240
  export class NixieSolarHeater extends NixieHeaterBase {
221
241
  public pollingInterval: number = 10000;
222
- public heater: Heater;
242
+ declare heater: Heater;
223
243
  constructor(ncp: INixieControlPanel, heater: Heater) {
224
244
  super(ncp, heater);
225
245
  this.heater = heater;
@@ -228,25 +248,59 @@ export class NixieSolarHeater extends NixieHeaterBase {
228
248
  public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
229
249
  public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
230
250
  try {
251
+ let origState = hstate.isOn;
231
252
  // Initialize the desired state.
232
253
  this.isOn = isOn;
233
254
  this.isCooling = isCooling;
234
- // Here we go we need to set the firemans switch state.
235
- if (hstate.isOn !== isOn) {
236
- logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
255
+ let target = hstate.startupDelay === false && isOn;
256
+ if (target && typeof hstate.endTime !== 'undefined') {
257
+ // Calculate a short cycle time so that the solar heater does not cycle
258
+ // too often. For solar heaters this is 60 seconds. This gives enough time
259
+ // for the valve to rotate and start heating. If the solar and water sensors are
260
+ // not having issues this should be plenty of time.
261
+ if (new Date().getTime() - hstate.endTime.getTime() < 60000) {
262
+ logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
263
+ target = false;
264
+ }
237
265
  }
238
- if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
239
- hstate.isOn = isOn;
240
- return;
266
+
267
+ // Here we go we need to set the valve status that is attached to solar.
268
+ if (hstate.isOn !== target) {
269
+ logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
241
270
  }
242
- if (typeof this._lastState === 'undefined' || isOn || this._lastState !== isOn) {
243
- let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`, { isOn: isOn, latch: isOn ? 10000 : undefined });
244
- if (res.status.code === 200) this._lastState = hstate.isOn = isOn;
245
- else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
271
+ if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
272
+ if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
273
+ this._lastState = hstate.isOn = target;
274
+ }
275
+ else {
276
+ let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
277
+ { isOn: target, latch: target ? 10000 : undefined });
278
+ if (res.status.code === 200) this._lastState = hstate.isOn = target;
279
+ else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
280
+ }
281
+ if (target) {
282
+ if (isCooling) this.lastCoolCycle = new Date();
283
+ else if (isOn) this.lastHeatCycle = new Date();
284
+ }
246
285
  }
247
- else {
248
- hstate.isOn = isOn;
249
- return;
286
+ // In this instance we need to see if there are cleaner circuits that we need to turn off
287
+ // then delay for the current body because the solar just came on.
288
+ if (hstate.isOn && sys.general.options.cleanerSolarDelay && !origState) {
289
+ let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('cleaner') !== -1 && x.body === hstate.bodyId });
290
+ let cleaners = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
291
+ // Turn off all the cleaner circuits and set an on delay if they are on.
292
+ for (let i = 0; i < cleaners.length; i++) {
293
+ let cleaner = cleaners.getItemByIndex(i);
294
+ if (cleaner.isActive) {
295
+ let cstate = state.circuits.getItemById(cleaner.id);
296
+ if (cstate.isOn && sys.general.options.cleanerSolarDelayTime > 0) {
297
+ // Turn off the circuit then set a delay.
298
+ logger.info(`Setting cleaner solar delay for ${cleaner.name} to ${sys.general.options.cleanerSolarDelayTime}`);
299
+ await sys.board.circuits.setCircuitStateAsync(cstate.id, false);
300
+ delayMgr.setCleanerStartDelay(cstate, hstate.bodyId, sys.general.options.cleanerSolarDelayTime);
301
+ }
302
+ }
303
+ }
250
304
  }
251
305
  } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
252
306
  }
@@ -304,6 +358,11 @@ export class NixieHeatpump extends NixieHeaterBase {
304
358
  this.suspendPolling = true;
305
359
  this.isOn = isOn;
306
360
  this.isCooling = isCooling;
361
+ if (!hstate.startupDelay) {
362
+ if (isOn && !isCooling) this.lastHeatCycle = new Date();
363
+ else if (isCooling) this.lastCoolCycle = new Date();
364
+ }
365
+ // When this is implemented lets not forget to deal with the startup delay. See UltraTemp for implementation.
307
366
  } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
308
367
  finally { this.suspendPolling = false; }
309
368
  }
@@ -420,7 +479,16 @@ export class NixieUltratemp extends NixieHeatpump {
420
479
  });
421
480
  out.appendPayloadBytes(0, 10);
422
481
  out.setPayloadByte(0, 144);
423
- out.setPayloadByte(1, this.isOn ? (this.isCooling ? 2 : 1) : 0, 0);
482
+ // If we are in startup delay simply tell the heater that it is off.
483
+ if (sheater.startupDelay)
484
+ out.setPayloadByte(1, 0, 0);
485
+ else {
486
+ if (this.isOn) {
487
+ if (!this.isCooling) this.lastHeatCycle = new Date();
488
+ else this.lastCoolCycle = new Date();
489
+ }
490
+ out.setPayloadByte(1, this.isOn ? (this.isCooling ? 2 : 1) : 0, 0);
491
+ }
424
492
  conn.queueSendMessage(out);
425
493
  });
426
494
  return success;
@@ -433,9 +501,101 @@ export class NixieUltratemp extends NixieHeatpump {
433
501
  if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
434
502
  this._pollTimer = null;
435
503
  logger.info(`Closing Heater ${this.heater.name}`);
436
-
437
504
  }
438
505
  catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
439
506
  }
440
507
  }
441
- export class NixieMastertemp extends NixieGasHeater {}
508
+ export class NixieMastertemp extends NixieGasHeater {
509
+ constructor(ncp: INixieControlPanel, heater: Heater) {
510
+ super(ncp, heater);
511
+ // Set the polling interval to 3 seconds.
512
+ this.pollEquipmentAsync();
513
+ this.pollingInterval = 3000;
514
+ }
515
+ public getCooldownTime(): number {
516
+ // Delays are always in terms of seconds so convert the minute to seconds.
517
+ if (this.heater.cooldownDelay === 0 || typeof this.lastHeatCycle === 'undefined') return 0;
518
+ let now = new Date().getTime();
519
+ let cooldown = this.isOn ? this.heater.cooldownDelay * 60000 : Math.round(((this.lastHeatCycle.getDate() + this.heater.cooldownDelay * 60000) - now) / 1000);
520
+ return Math.min(Math.max(0, cooldown), this.heater.cooldownDelay * 60);
521
+ }
522
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
523
+ try {
524
+ // Initialize the desired state.
525
+ this.isOn = isOn;
526
+ this.isCooling = false;
527
+ // Here we go we need to set the firemans switch state.
528
+ if (hstate.isOn !== isOn) {
529
+ logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
530
+ }
531
+ if (isOn && !hstate.startupDelay) this.lastHeatCycle = new Date();
532
+ hstate.isOn = isOn;
533
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
534
+ }
535
+ public async pollEquipmentAsync() {
536
+ let self = this;
537
+ try {
538
+ this.suspendPolling = true;
539
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
540
+ this._pollTimer = null;
541
+ if (this._suspendPolling > 1) return;
542
+ let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
543
+ if (!this.closing) await this.setStatus(sheater);
544
+ }
545
+ catch (err) { logger.error(`Error polling MasterTemp heater - ${err}`); }
546
+ finally {
547
+ this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
548
+ try { await self.pollEquipmentAsync() } catch (err) { }
549
+ }, this.pollingInterval || 3000);
550
+ }
551
+ }
552
+ public async setStatus(sheater: HeaterState): Promise<boolean> {
553
+ try {
554
+ let success = await new Promise<boolean>((resolve, reject) => {
555
+ let out = Outbound.create({
556
+ protocol: Protocol.Heater,
557
+ source: 16,
558
+ dest: this.heater.address,
559
+ action: 112,
560
+ payload: [],
561
+ retries: 3, // We are going to try 4 times.
562
+ response: Response.create({ protocol: Protocol.Heater, action: 116 }),
563
+ onAbort: () => { },
564
+ onComplete: (err) => {
565
+ if (err) {
566
+ // If the MasterTemp is not responding we need to store that off but at this point we know none of the codes. If a 115 does
567
+ // come across this will be cleared by the processing of that message.
568
+ sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
569
+ state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
570
+ resolve(false);
571
+ }
572
+ else { resolve(true); }
573
+ }
574
+ });
575
+ out.appendPayloadBytes(0, 11);
576
+ // If we have a startup delay we need to simply send 0 to the heater to make sure that it is off.
577
+ if (sheater.startupDelay)
578
+ out.setPayloadByte(0, 0);
579
+ else {
580
+ // The cooldown delay is a bit hard to figure out here since I think the heater does it on its own.
581
+ out.setPayloadByte(0, sheater.bodyId <= 2 ? sheater.bodyId : 0);
582
+ }
583
+ out.setPayloadByte(1, sys.bodies.getItemById(1).heatSetpoint || 0);
584
+ out.setPayloadByte(2, sys.bodies.getItemById(2).heatSetpoint || 0);
585
+ conn.queueSendMessage(out);
586
+ });
587
+ return success;
588
+ } catch (err) { logger.error(`Communication error with MasterTemp : ${err.message}`); }
589
+ }
590
+ public async closeAsync() {
591
+ try {
592
+ this.suspendPolling = true;
593
+ this.closing = true;
594
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
595
+ this._pollTimer = null;
596
+ logger.info(`Closing Heater ${this.heater.name}`);
597
+
598
+ }
599
+ catch (err) { logger.error(`MasterTemp closeAsync: ${err.message}`); return Promise.reject(err); }
600
+ }
601
+ }