nodejs-poolcontroller 8.1.2 → 8.4.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 (106) 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 -0
  7. package/.github/workflows/ghcr-publish.yml +67 -0
  8. package/AGENTS.md +597 -0
  9. package/CONTRIBUTING.md +74 -74
  10. package/Changelog +292 -257
  11. package/Dockerfile +62 -19
  12. package/Gruntfile.js +40 -40
  13. package/LICENSE +661 -661
  14. package/README.md +318 -191
  15. package/anslq25/MessagesMock.ts +221 -221
  16. package/anslq25/boards/MockBoardFactory.ts +49 -49
  17. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  18. package/anslq25/boards/MockSystemBoard.ts +216 -216
  19. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  20. package/anslq25/pumps/MockPump.ts +83 -83
  21. package/app.ts +115 -115
  22. package/config/Config.ts +57 -7
  23. package/config/VersionCheck.ts +63 -35
  24. package/controller/Constants.ts +809 -805
  25. package/controller/Equipment.ts +2688 -2664
  26. package/controller/Errors.ts +181 -181
  27. package/controller/Lockouts.ts +549 -549
  28. package/controller/State.ts +3738 -3690
  29. package/controller/boards/AquaLinkBoard.ts +1003 -1003
  30. package/controller/boards/BoardFactory.ts +53 -53
  31. package/controller/boards/EasyTouchBoard.ts +3202 -3202
  32. package/controller/boards/IntelliCenterBoard.ts +4393 -3899
  33. package/controller/boards/IntelliComBoard.ts +69 -69
  34. package/controller/boards/IntelliTouchBoard.ts +382 -382
  35. package/controller/boards/NixieBoard.ts +1944 -1929
  36. package/controller/boards/SunTouchBoard.ts +400 -400
  37. package/controller/boards/SystemBoard.ts +5268 -5268
  38. package/controller/comms/Comms.ts +1272 -1214
  39. package/controller/comms/ScreenLogic.ts +1665 -1665
  40. package/controller/comms/messages/Messages.ts +1433 -1243
  41. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  42. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  43. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  44. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  45. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  46. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  47. package/controller/comms/messages/config/EquipmentMessage.ts +216 -210
  48. package/controller/comms/messages/config/ExternalMessage.ts +96 -10
  49. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  50. package/controller/comms/messages/config/GeneralMessage.ts +0 -0
  51. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  52. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  53. package/controller/comms/messages/config/OptionsMessage.ts +194 -174
  54. package/controller/comms/messages/config/PumpMessage.ts +0 -0
  55. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  56. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  57. package/controller/comms/messages/config/SecurityMessage.ts +0 -0
  58. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  59. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  60. package/controller/comms/messages/status/EquipmentStateMessage.ts +1158 -822
  61. package/controller/comms/messages/status/HeaterStateMessage.ts +135 -135
  62. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  63. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  64. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  65. package/controller/comms/messages/status/RegalModbusStateMessage.ts +411 -0
  66. package/controller/comms/messages/status/VersionMessage.ts +103 -41
  67. package/controller/nixie/Nixie.ts +173 -173
  68. package/controller/nixie/NixieEquipment.ts +104 -104
  69. package/controller/nixie/bodies/Body.ts +120 -120
  70. package/controller/nixie/bodies/Filter.ts +135 -135
  71. package/controller/nixie/chemistry/ChemController.ts +2724 -2724
  72. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  73. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  74. package/controller/nixie/circuits/Circuit.ts +478 -478
  75. package/controller/nixie/heaters/Heater.ts +834 -834
  76. package/controller/nixie/pumps/Pump.ts +1194 -996
  77. package/controller/nixie/schedules/Schedule.ts +401 -401
  78. package/controller/nixie/valves/Valve.ts +170 -170
  79. package/defaultConfig.json +352 -347
  80. package/docker-compose.yml +32 -0
  81. package/logger/DataLogger.ts +448 -448
  82. package/logger/Logger.ts +448 -436
  83. package/package.json +58 -60
  84. package/sendSocket.js +32 -32
  85. package/tsconfig.json +25 -25
  86. package/types/express-multer.d.ts +32 -0
  87. package/web/Server.ts +1937 -1927
  88. package/web/bindings/aqualinkD.json +559 -559
  89. package/web/bindings/influxDB.json +1066 -1066
  90. package/web/bindings/mqtt.json +721 -721
  91. package/web/bindings/mqttAlt.json +746 -746
  92. package/web/bindings/rulesManager.json +54 -54
  93. package/web/bindings/smartThings-Hubitat.json +31 -31
  94. package/web/bindings/valveRelays.json +20 -20
  95. package/web/bindings/vera.json +25 -25
  96. package/web/interfaces/baseInterface.ts +188 -188
  97. package/web/interfaces/httpInterface.ts +148 -148
  98. package/web/interfaces/influxInterface.ts +283 -283
  99. package/web/interfaces/mqttInterface.ts +695 -695
  100. package/web/interfaces/ruleInterface.ts +101 -87
  101. package/web/services/config/Config.ts +1063 -1053
  102. package/web/services/config/ConfigSocket.ts +0 -0
  103. package/web/services/state/State.ts +0 -0
  104. package/web/services/state/StateSocket.ts +0 -0
  105. package/web/services/utilities/Utilities.ts +233 -233
  106. package/.github/workflows/docker-publish-njsPC-linux.yml +0 -50
@@ -1,550 +1,550 @@
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 { PumpState, HeaterState, BodyTempState, ICircuitState, state } from "./State";
19
- import { Equipment, sys } from "./Equipment";
20
- import { Timestamp, utils } from "./Constants";
21
- import { logger } from "../logger/Logger";
22
- import { webApp } from "../web/Server";
23
- // LOCKOUT PRIMER
24
- // Lockouts are either time based (Delays) or based upon the current state configuration for
25
- // the system. So in some cases circuits can only be engaged in pool mode or in spa mode. In
26
- // others a period of time must occur before a particular action can continue. Delays can typically
27
- // be cancelled manually while lockouts can only be cancelled when the condition required for the lockout
28
- // is changed.
29
-
30
- // DELAYS:
31
- // Pump Off During Valve Rotation (30 sec): This turns any pump associated with the body being turned on to
32
- // so that is is off. This gives the valves time to rotate so that cold water from the pool does not cycle into
33
- // the spa and hot water from the spa does not escape into the pool. This has nothing to do with
34
- // water hammer or anything else.
35
- //
36
- // Heater Cooldown Delay (based on max heater time): When the system is heating and an event is occurring
37
- // that will cause the heater to be turned off, the current mode will be retained until the delay is either
38
- // cancelled or expired.
39
- // Delay Conditions:
40
- // 1. Being in either pool or spa mode and simply turning off that mode where the heater will be turned off.
41
- // 2. Switching between pool and spa when the target mode does not use the identified heater.
42
- // Exceptions:
43
- // 1. The last call for heat was earlier than the current time minus the cooldown delay defined for the heater.
44
- // 2. The heater mode is in a cooling mode.
45
- //
46
- // Heater Startup: When a body is first turned on the heater will not be engaged for 10 seconds after any pump delay
47
- // or the time that the body is engaged.
48
- //
49
- // Cleaner Circuit Start Delay: Delays turning on any circuit with a cleaner function until the delay expires. This is
50
- // so booster pumps can be assured of sufficient forward pressure prior to turning on. These pumps often require sufficient
51
- // pressure before engaging and will cavitate if they do not have it. The Pentair default is 5min.
52
- //
53
- // Cleaner Circuit Solar Delay: This only exists with Pentair panels. This shuts off any circuit
54
- // designated as a pool cleaner circuit if it is on and delays turning it on for 5min after the solar starts. The assumption
55
- // here is that pressure reduction that can occur when the solar kicks on can cavitate the pump.
56
- //
57
- // Manual Operation Priority Delay:
58
- // From the manual:
59
- // Manual OP Priority: ON: This feature allows for a circuit to be manually switched OFF and switched ON within
60
- // a scheduled program, the circuit will continue to run for a maximum of 12 hours or whatever that circuit Egg
61
- // Timer is set to, after which the scheduled program will resume. This feature will turn off any scheduled
62
- // program to allow manual pump override. The Default setting is OFF.
63
- //
64
- // ## When on
65
- // 1. If a schedule should be on and the user turns the schedule off then the schedule expires until such time
66
- // as the time off has expired. When that occurs the schedule should be reset to run at the designated time.
67
- // If the user resets the schedule by turning the circuit back on again then the schedule will be ignored and
68
- // the circuit will run until the egg timer expires or the circuit/feature is manually turned off. This setting
69
- // WILL affect other schedules that may impact this circuit.
70
- //
71
- // ## When off
72
- // 1. "Normal" = If a schedule should be on and the user turns the schedule off then the schedule expires until
73
- // such time as the time off has expired. When that occurs the schedule should be reset to run at the designated
74
- // time. If the user resets the schedule by turning the circuit back on again then the schedule will resume and
75
- // turn off at the specified time.
76
- //
77
- // LOCKOUTS (Proposed):
78
- // Spillway Lockout: This locks out any circuit or feature that is marked with a Spillway circuit function (type) whenever
79
- // whenever the pool circuit is not engaged. This should mark the spillway circuit as a delayStart then release it when the
80
- // pool body starts.
81
- interface ILockout {
82
- type: string
83
- }
84
- export class EquipmentLockout implements ILockout {
85
- public id = utils.uuid();
86
- public create() { }
87
- public startTime: Date;
88
- public type: string = 'lockout';
89
- public message: string = '';
90
- }
91
- export class EquipmentDelay implements ILockout {
92
- public constructor() { this.id = delayMgr.getNextId(); }
93
- public id;
94
- public type: string = 'delay';
95
- public startTime: Date;
96
- public endTime: Date;
97
- public canCancel: boolean = true;
98
- public cancelDelay() { };
99
- public reset() { };
100
- public clearDelay() { };
101
- public message;
102
- protected _delayTimer: NodeJS.Timeout;
103
- public serialize(): any {
104
- return {
105
- id: this.id,
106
- type: this.type,
107
- canCancel: this.canCancel,
108
- message: this.message,
109
- startTime: typeof this.startTime !== 'undefined' ? Timestamp.toISOLocal(this.startTime) : undefined,
110
- endTime: typeof this.endTime !== 'undefined' ? Timestamp.toISOLocal(this.endTime) : undefined,
111
- duration: typeof this.startTime !== 'undefined' && typeof this.endTime !== 'undefined' ? (this.endTime.getTime() - this.startTime.getTime()) / 1000 : 0
112
- };
113
- }
114
- }
115
- export class ManualPriorityDelay extends EquipmentDelay {
116
- public constructor(cs: ICircuitState) {
117
- super();
118
- this.type = 'manualOperationPriorityDelay';
119
- this.message = `${cs.name} will override future schedules until expired/cancelled.`;
120
- this.circuitState = cs;
121
- this.circuitState.manualPriorityActive = true;
122
- this.startTime = new Date();
123
- this.endTime = cs.endTime.clone().toDate();
124
- this._delayTimer = setTimeout(() => {
125
- logger.info(`Manual Operation Priority expired for ${this.circuitState.name}`);
126
- this.circuitState.manualPriorityActive = false;
127
- delayMgr.deleteDelay(this.id);
128
- }, this.endTime.getTime() - new Date().getTime());
129
- logger.info(`Manual Operation Priority delay in effect until ${this.circuitState.name} - ${cs.endTime.toDate()}`);
130
- }
131
- public circuitState: ICircuitState;
132
- public cancelDelay() {
133
- this.circuitState.manualPriorityActive = false;
134
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
135
- logger.info(`Manual Operation Priority cancelled for ${this.circuitState.name}`);
136
- this._delayTimer = undefined;
137
- this.circuitState.manualPriorityActive = false;
138
- // Rip through all the schedules and clear the manual priority.
139
- let sscheds = state.schedules.getActiveSchedules();
140
- let circIds = [];
141
- for (let i = 0; i < sscheds.length; i++) {
142
- let ssched = sscheds[i];
143
- ssched.manualPriorityActive = false;
144
- if (!circIds.includes(ssched.circuit)) circIds.push(ssched.circuit);
145
- }
146
- for (let i = 0; i < circIds.length; i++) {
147
- let circ = sys.circuits.getInterfaceById(circIds[i]);
148
- if (!circ.isActive) continue;
149
- let cstate = state.circuits.getInterfaceById(circ.id);
150
- sys.board.circuits.setEndTime(circ, cstate, cstate.isOn, true);
151
- }
152
-
153
- delayMgr.deleteDelay(this.id);
154
- }
155
- public clearDelay() {
156
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
157
- logger.info(`Manual Operation Priority cleared for ${this.circuitState.name}`);
158
- this._delayTimer = undefined;
159
- delayMgr.deleteDelay(this.id);
160
- }
161
- }
162
- export class PumpValveDelay extends EquipmentDelay {
163
- public constructor(ps: PumpState, delay?: number) {
164
- super();
165
- this.type = 'pumpValveDelay';
166
- this.message = `${ps.name} will start after valve delay`;
167
- this.pumpState = ps;
168
- this.pumpState.pumpOnDelay = true;
169
- this.startTime = new Date();
170
- this.endTime = new Date(this.startTime.getTime() + (delay * 1000 || sys.general.options.valveDelayTime * 1000));
171
- this._delayTimer = setTimeout(() => {
172
- logger.info(`Valve delay expired for ${this.pumpState.name}`);
173
- this.pumpState.pumpOnDelay = false;
174
- delayMgr.deleteDelay(this.id);
175
- }, delay * 1000 || sys.general.options.valveDelayTime * 1000);
176
- logger.info(`Valve delay started for ${this.pumpState.name} - ${delay || sys.general.options.valveDelayTime}sec`);
177
- }
178
- public pumpState: PumpState;
179
- public cancelDelay() {
180
- this.pumpState.pumpOnDelay = false;
181
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
182
- logger.info(`Valve delay cancelled for ${this.pumpState.name}`);
183
- this._delayTimer = undefined;
184
- delayMgr.deleteDelay(this.id);
185
- }
186
- public clearDelay() {
187
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
188
- logger.info(`Valve delay cleared for ${this.pumpState.name}`);
189
- this._delayTimer = undefined;
190
- delayMgr.deleteDelay(this.id);
191
- }
192
- }
193
- export class HeaterStartupDelay extends EquipmentDelay {
194
- public constructor(hs: HeaterState, delay?: number) {
195
- super();
196
- this.type = 'heaterStartupDelay';
197
- this.message = `${hs.name} will start after delay`;
198
- this.heaterState = hs;
199
- this.heaterState.startupDelay = true;
200
- this.startTime = new Date();
201
- this.endTime = new Date(this.startTime.getTime() + (delay * 1000 || sys.general.options.heaterStartDelayTime * 1000));
202
- this._delayTimer = setTimeout(() => {
203
- logger.info(`Heater Startup delay expired for ${this.heaterState.name}`);
204
- this.heaterState.startupDelay = false;
205
- delayMgr.deleteDelay(this.id);
206
- }, delay * 1000 || sys.general.options.heaterStartDelayTime * 1000);
207
- logger.info(`Heater delay started for ${this.heaterState.name} - ${delay || sys.general.options.heaterStartDelayTime}sec`);
208
- }
209
- public heaterState: HeaterState;
210
- public cancelDelay() {
211
- this.heaterState.startupDelay = false;
212
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
213
- logger.info(`Heater Startup delay cancelled for ${this.heaterState.name}`);
214
- this._delayTimer = undefined;
215
- delayMgr.deleteDelay(this.id);
216
- }
217
- public clearDelay() {
218
- this.heaterState.startupDelay = false;
219
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
220
- logger.info(`Heater Startup delay cancelled for ${this.heaterState.name}`);
221
- this._delayTimer = undefined;
222
- delayMgr.deleteDelay(this.id);
223
- }
224
- }
225
- export class HeaterCooldownDelay extends EquipmentDelay {
226
- public constructor(bsoff: BodyTempState, bson?: BodyTempState, delay?: number) {
227
- super();
228
- this.type = 'heaterCooldownDelay';
229
- this.message = `${bsoff.name} Heater Cooldown in progress`;
230
- this.bodyStateOff = bsoff;
231
- this.bodyStateOff.heaterCooldownDelay = true;
232
- this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooldown');
233
- let cstateOff = state.circuits.getItemById(bsoff.circuit);
234
- this.bodyStateOn = bson;
235
- this.bodyStateOff.stopDelay = cstateOff.stopDelay = true;
236
- let cstateOn = (typeof bson !== 'undefined') ? state.circuits.getItemById(bson.circuit) : undefined;
237
- if (typeof cstateOn !== 'undefined') {
238
- this.bodyStateOn.startDelay = cstateOn.startDelay = true;
239
- }
240
- logger.verbose(`Heater Cooldown Delay started for ${this.bodyStateOff.name} - ${delay/1000}sec`);
241
- this.startTime = new Date();
242
- this.endTime = new Date(this.startTime.getTime() + (delay * 1000));
243
- this._delayTimer = setTimeout(() => {
244
- logger.verbose(`Heater Cooldown delay expired for ${this.bodyStateOff.name}`);
245
- this.bodyStateOff.stopDelay = state.circuits.getItemById(this.bodyStateOff.circuit).stopDelay = false;
246
- // Now that the startup delay expired cancel the delay and shut off the circuit.
247
- (async () => {
248
- try {
249
- await sys.board.circuits.setCircuitStateAsync(cstateOff.id, false, true);
250
- if (typeof this.bodyStateOn !== 'undefined') {
251
- this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
252
- await sys.board.circuits.setCircuitStateAsync(this.bodyStateOn.circuit, true);
253
- }
254
- } catch (err) { logger.error(`Error executing Cooldown Delay completion: ${err}`); }
255
- })();
256
- this.bodyStateOff.heaterCooldownDelay = false;
257
- this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
258
- delayMgr.deleteDelay(this.id);
259
- }, delay);
260
- state.emitEquipmentChanges();
261
- }
262
- public bodyStateOff: BodyTempState;
263
- public bodyStateOn: BodyTempState;
264
- public setBodyStateOn(bson?: BodyTempState) {
265
- if (typeof this.bodyStateOn !== 'undefined' && (typeof bson === 'undefined' || this.bodyStateOn.id !== bson.id))
266
- this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
267
- if (typeof bson !== 'undefined') {
268
- if (typeof this.bodyStateOn === 'undefined' || this.bodyStateOn.id !== bson.id) {
269
- bson.startDelay = state.circuits.getItemById(bson.circuit).startDelay = true;
270
- logger.info(`${bson.name} will Start After Cooldown Delay`);
271
- this.bodyStateOn = bson;
272
- }
273
- }
274
- else this.bodyStateOn = undefined;
275
- }
276
- public cancelDelay() {
277
- let cstateOff = state.circuits.getItemById(this.bodyStateOff.circuit);
278
- cstateOff.stopDelay = false;
279
- (async () => {
280
- await sys.board.circuits.setCircuitStateAsync(cstateOff.id, false);
281
- if (typeof this.bodyStateOn !== 'undefined') {
282
- this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
283
- await sys.board.circuits.setCircuitStateAsync(this.bodyStateOn.circuit, true);
284
- }
285
- })();
286
- this.bodyStateOff.stopDelay = this.bodyStateOff.heaterCooldownDelay = false;
287
- this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
288
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
289
- logger.info(`Heater Cooldown delay cancelled for ${this.bodyStateOff.name}`);
290
- this._delayTimer = undefined;
291
- delayMgr.deleteDelay(this.id);
292
- state.emitEquipmentChanges();
293
- }
294
- public clearDelay() {
295
- let cstateOff = state.circuits.getItemById(this.bodyStateOff.circuit);
296
- cstateOff.stopDelay = false;
297
- (async () => {
298
- await sys.board.circuits.setCircuitStateAsync(cstateOff.id, false);
299
- if (typeof this.bodyStateOn !== 'undefined') {
300
- this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
301
- await sys.board.circuits.setCircuitStateAsync(this.bodyStateOn.circuit, true);
302
- }
303
- })();
304
- this.bodyStateOff.stopDelay = this.bodyStateOff.heaterCooldownDelay = false;
305
- this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
306
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
307
- logger.info(`Heater Cooldown delay cleared for ${this.bodyStateOff.name}`);
308
- this._delayTimer = undefined;
309
- delayMgr.deleteDelay(this.id);
310
- state.emitEquipmentChanges();
311
- }
312
- }
313
- interface ICleanerDelay {
314
- cleanerState: ICircuitState,
315
- bodyId: number
316
- }
317
- export class CleanerStartDelay extends EquipmentDelay implements ICleanerDelay {
318
- constructor(cs: ICircuitState, bodyId: number, delay?: number) {
319
- super();
320
- this.type = 'cleanerStartDelay';
321
- this.message = `${cs.name} will start after delay`;
322
- this.bodyId = bodyId;
323
- this.cleanerState = cs;
324
- cs.startDelay = true;
325
- this.startTime = new Date();
326
- this.endTime = new Date(this.startTime.getTime() + (delay * 1000 || sys.general.options.cleanerStartDelayTime * 1000));
327
- this._delayTimer = setTimeout(() => {
328
- logger.info(`Cleaner delay expired for ${this.cleanerState.name}`);
329
- this.cleanerState.startDelay = false;
330
- (async () => {
331
- try {
332
- await sys.board.circuits.setCircuitStateAsync(this.cleanerState.id, true, true);
333
- this.cleanerState.startDelay = false;
334
- } catch (err) { logger.error(`Error executing Cleaner Start Delay completion: ${err}`); }
335
- })();
336
- delayMgr.deleteDelay(this.id);
337
- }, delay * 1000 || sys.general.options.cleanerStartDelayTime * 1000);
338
- logger.info(`Cleaner delay started for ${this.cleanerState.name} - ${delay || sys.general.options.cleanerStartDelayTime}sec`);
339
- }
340
- public cleanerState: ICircuitState;
341
- public bodyId: number;
342
- public cancelDelay() {
343
- this.cleanerState.startDelay = false;
344
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
345
- logger.info(`Cleaner Start delay cancelled for ${this.cleanerState.name}`);
346
- this._delayTimer = undefined;
347
- this.cleanerState.startDelay = false;
348
- (async () => {
349
- try {
350
- await sys.board.circuits.setCircuitStateAsync(this.cleanerState.id, true, true);
351
- } catch (err) { logger.error(`Error executing Cleaner Start Delay completion: ${err}`); }
352
- })();
353
- delayMgr.deleteDelay(this.id);
354
- }
355
- public clearDelay() {
356
- this.cleanerState.startDelay = false;
357
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
358
- logger.info(`Cleaner Start delay cleared for ${this.cleanerState.name}`);
359
- this._delayTimer = undefined;
360
- this.cleanerState.startDelay = false;
361
- delayMgr.deleteDelay(this.id);
362
- }
363
-
364
- public reset(delay?: number) {
365
- if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
366
- this.cleanerState.startDelay = true;
367
- logger.info(`Cleaner Start delay reset for ${this.cleanerState.name}`);
368
- this.startTime = new Date();
369
- this.endTime = new Date(this.startTime.getTime() + (delay * 1000 || sys.general.options.cleanerStartDelayTime * 1000));
370
- this._delayTimer = setTimeout(() => {
371
- logger.info(`Cleaner delay expired for ${this.cleanerState.name}`);
372
- this.cleanerState.startDelay = false;
373
- (async () => {
374
- try {
375
- await sys.board.circuits.setCircuitStateAsync(this.cleanerState.id, true);
376
- this.cleanerState.startDelay = false;
377
- } catch (err) { logger.error(`Error executing Cleaner Start Delay completion: ${err}`); }
378
- })();
379
- delayMgr.deleteDelay(this.id);
380
- }, delay * 1000 || sys.general.options.cleanerStartDelayTime * 1000);
381
- }
382
- }
383
- export class DelayManager extends Array<EquipmentDelay> {
384
- protected _id = 1;
385
- private _emitTimer: NodeJS.Timeout;
386
- public setDirty() {
387
- if (typeof this._emitTimer) clearTimeout(this._emitTimer);
388
- this._emitTimer = setTimeout(() => this.emitDelayState(), 1000);
389
- }
390
- public getNextId() { return this._id++; }
391
- public cancelDelay(id: number) {
392
- let del = this.find(x => x.id === id);
393
- if (typeof del !== 'undefined') del.cancelDelay();
394
- }
395
- public clearAllDelays() {
396
- for (let i = this.length - 1; i >= 0; i--) {
397
- let del = this[i];
398
- del.clearDelay();
399
- }
400
- }
401
- public setManualPriorityDelay(cs: ICircuitState) {
402
- let cds = this.filter(x => x.type === 'manualOperationPriorityDelay');
403
- for (let i = 0; i < cds.length; i++) {
404
- let delay = cds[i] as ManualPriorityDelay;
405
- if (delay.circuitState.id === cs.id) delay.clearDelay();
406
- }
407
- this.push(new ManualPriorityDelay(cs)); this.setDirty();
408
- }
409
- public cancelManualPriorityDelays() { this.cancelDelaysByType('manualOperationPriorityDelay'); this.setDirty(); }
410
- public cancelManualPriorityDelay(id: number){
411
- let delays = this.filter(x => x.type === 'manualOperationPriorityDelay');
412
- for (let i = 0; i < delays.length; i++) {
413
- if((delays[i] as ManualPriorityDelay).circuitState.id === id) delays[i].cancelDelay();
414
- }
415
- }
416
- public setPumpValveDelay(ps: PumpState, delay?: number) {
417
- let cds = this.filter(x => x.type === 'pumpValveDelay');
418
- for (let i = 0; i < cds.length; i++) {
419
- let delay = cds[i] as PumpValveDelay;
420
- if (delay.pumpState.id === ps.id) delay.clearDelay();
421
- }
422
- this.push(new PumpValveDelay(ps, delay)); this.setDirty();
423
- }
424
- public cancelPumpValveDelays() { this.cancelDelaysByType('pumpValveDelay'); this.setDirty(); }
425
- public setHeaterStartupDelay(hs: HeaterState, delay?: number) {
426
- let cds = this.filter(x => x.type === 'heaterStartupDelay');
427
- for (let i = 0; i < cds.length; i++) {
428
- let delay = cds[i] as HeaterStartupDelay;
429
- if (delay.heaterState.id === hs.id) delay.cancelDelay();
430
- }
431
- this.push(new HeaterStartupDelay(hs, delay)); this.setDirty();
432
- }
433
- public cancelHeaterStartupDelays() {
434
- this.cancelDelaysByType('heaterStartupDelay');
435
- }
436
- public setHeaterCooldownDelay(bsOff: BodyTempState, bsOn?: BodyTempState, delay?: number) {
437
- logger.info(`Setting Heater Cooldown Delay for ${bsOff.name}`);
438
- let cds = this.filter(x => x.type === 'heaterCooldownDelay');
439
- for (let i = 0; i < cds.length; i++) {
440
- let delay = cds[i] as HeaterCooldownDelay;
441
- if (delay.bodyStateOff.id === bsOff.id) {
442
- if(typeof bsOn !== 'undefined') logger.info(`Found Cooldown Delay adding on circuit ${bsOn.name}`);
443
- delay.setBodyStateOn(bsOn);
444
- this.setDirty();
445
- return;
446
- }
447
- }
448
- this.push(new HeaterCooldownDelay(bsOff, bsOn, delay));
449
- this.setDirty();
450
- }
451
- public clearBodyStartupDelay(bs: BodyTempState) {
452
- logger.info(`Clearing startup delays for ${bs.name}`);
453
- // We are doing this non type safety thing below so that
454
- // we can only emit when the body is cleared.
455
- let cds = this.filter(x => {
456
- return x.type === 'heaterCooldownDelay' &&
457
- typeof x['bodyStateOn'] !== 'undefined' &&
458
- x['bodyStateOn'].id === bs.id;
459
- });
460
- for (let i = 0; i < cds.length; i++) {
461
- let delay = cds[i] as HeaterCooldownDelay;
462
- logger.info(`Clearing ${bs.name} from Cooldown Delay`);
463
- delay.setBodyStateOn();
464
- }
465
- if (cds.length) this.setDirty();
466
- }
467
- public cancelHeaterCooldownDelays() { this.cancelDelaysByType('heaterCooldownDelay'); }
468
- public setCleanerStartDelay(cs: ICircuitState, bodyId: number, delay?: number) {
469
- let cds = this.filter(x => x.type === ('cleanerStartDelay' || 'cleanerSolarDelay'));
470
- let startDelay: CleanerStartDelay;
471
- for (let i = 0; i < cds.length; i++) {
472
- let delay = cds[i] as unknown as ICleanerDelay;
473
- if (delay.cleanerState.id === cs.id) {
474
- if (delay.bodyId !== bodyId || cds[i].type !== 'cleanerStartDelay') cds[i].cancelDelay();
475
- else if (typeof startDelay !== 'undefined') {
476
- startDelay.cancelDelay();
477
- startDelay = cds[i] as CleanerStartDelay;
478
- }
479
- else startDelay = cds[i] as CleanerStartDelay;
480
- }
481
- }
482
- if (typeof startDelay !== 'undefined') {
483
- startDelay.reset(delay);
484
- this.setDirty();
485
- }
486
- else {
487
- this.push(new CleanerStartDelay(cs, bodyId, delay));
488
- this.setDirty();
489
- }
490
- }
491
- public cancelCleanerStartDelays(bodyId?: number) {
492
- if (typeof bodyId === 'undefined') this.cancelDelaysByType('cleanerStartDelay');
493
- else {
494
- let delays = this.filter(x => x.type === 'cleanerStartDelay' && x['bodyId'] === bodyId);
495
- for (let i = 0; i < delays.length; i++) {
496
- delays[i].cancelDelay();
497
- }
498
- if (delays.length > 0) this.setDirty();
499
- }
500
- }
501
- public clearCleanerStartDelays(bodyId?: number) {
502
- if (typeof bodyId === 'undefined') this.clearDelaysByType('cleanerStartDelay');
503
- else {
504
- let delays = this.filter(x => x.type === 'cleanerStartDelay' && x['bodyId'] === bodyId);
505
- for (let i = 0; i < delays.length; i++) {
506
- delays[i].clearDelay();
507
- }
508
- if (delays.length > 0) this.setDirty();
509
- }
510
- }
511
- public deleteDelay(id: number) {
512
- for (let i = this.length - 1; i >= 0; i--) {
513
- if (this[i].id === id) {
514
- this.splice(i, 1);
515
- this.setDirty();
516
- }
517
- }
518
- }
519
- public setSolarStartupDelay
520
- protected cancelDelaysByType(type: string) {
521
- let delays = this.filter(x => x.type === type);
522
- for (let i = 0; i < delays.length; i++) {
523
- delays[i].cancelDelay();
524
- }
525
- }
526
- protected clearDelaysByType(type: string) {
527
- let delays = this.filter(x => x.type === type);
528
- for (let i = 0; i < delays.length; i++) {
529
- delays[i].clearDelay();
530
- }
531
- if (delays.length > 0) this.setDirty();
532
- }
533
- public serialize() {
534
- try {
535
- let delays = [];
536
- for (let i = 0; i < this.length; i++) {
537
- delays.push(this[i].serialize());
538
- }
539
- return delays;
540
- } catch (err) { logger.error(`Error serializing delays: ${err.message}`); }
541
- }
542
- public emitDelayState() {
543
- try {
544
- // We have to use a custom serializer because the properties of
545
- // our delays will create a circular reference due to the timers and state references.
546
- webApp.emitToClients('delays', this.serialize());
547
- } catch (err) { logger.error(`Error emitting delay states ${err.message}`); }
548
- }
549
- }
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 { PumpState, HeaterState, BodyTempState, ICircuitState, state } from "./State";
19
+ import { Equipment, sys } from "./Equipment";
20
+ import { Timestamp, utils } from "./Constants";
21
+ import { logger } from "../logger/Logger";
22
+ import { webApp } from "../web/Server";
23
+ // LOCKOUT PRIMER
24
+ // Lockouts are either time based (Delays) or based upon the current state configuration for
25
+ // the system. So in some cases circuits can only be engaged in pool mode or in spa mode. In
26
+ // others a period of time must occur before a particular action can continue. Delays can typically
27
+ // be cancelled manually while lockouts can only be cancelled when the condition required for the lockout
28
+ // is changed.
29
+
30
+ // DELAYS:
31
+ // Pump Off During Valve Rotation (30 sec): This turns any pump associated with the body being turned on to
32
+ // so that is is off. This gives the valves time to rotate so that cold water from the pool does not cycle into
33
+ // the spa and hot water from the spa does not escape into the pool. This has nothing to do with
34
+ // water hammer or anything else.
35
+ //
36
+ // Heater Cooldown Delay (based on max heater time): When the system is heating and an event is occurring
37
+ // that will cause the heater to be turned off, the current mode will be retained until the delay is either
38
+ // cancelled or expired.
39
+ // Delay Conditions:
40
+ // 1. Being in either pool or spa mode and simply turning off that mode where the heater will be turned off.
41
+ // 2. Switching between pool and spa when the target mode does not use the identified heater.
42
+ // Exceptions:
43
+ // 1. The last call for heat was earlier than the current time minus the cooldown delay defined for the heater.
44
+ // 2. The heater mode is in a cooling mode.
45
+ //
46
+ // Heater Startup: When a body is first turned on the heater will not be engaged for 10 seconds after any pump delay
47
+ // or the time that the body is engaged.
48
+ //
49
+ // Cleaner Circuit Start Delay: Delays turning on any circuit with a cleaner function until the delay expires. This is
50
+ // so booster pumps can be assured of sufficient forward pressure prior to turning on. These pumps often require sufficient
51
+ // pressure before engaging and will cavitate if they do not have it. The Pentair default is 5min.
52
+ //
53
+ // Cleaner Circuit Solar Delay: This only exists with Pentair panels. This shuts off any circuit
54
+ // designated as a pool cleaner circuit if it is on and delays turning it on for 5min after the solar starts. The assumption
55
+ // here is that pressure reduction that can occur when the solar kicks on can cavitate the pump.
56
+ //
57
+ // Manual Operation Priority Delay:
58
+ // From the manual:
59
+ // Manual OP Priority: ON: This feature allows for a circuit to be manually switched OFF and switched ON within
60
+ // a scheduled program, the circuit will continue to run for a maximum of 12 hours or whatever that circuit Egg
61
+ // Timer is set to, after which the scheduled program will resume. This feature will turn off any scheduled
62
+ // program to allow manual pump override. The Default setting is OFF.
63
+ //
64
+ // ## When on
65
+ // 1. If a schedule should be on and the user turns the schedule off then the schedule expires until such time
66
+ // as the time off has expired. When that occurs the schedule should be reset to run at the designated time.
67
+ // If the user resets the schedule by turning the circuit back on again then the schedule will be ignored and
68
+ // the circuit will run until the egg timer expires or the circuit/feature is manually turned off. This setting
69
+ // WILL affect other schedules that may impact this circuit.
70
+ //
71
+ // ## When off
72
+ // 1. "Normal" = If a schedule should be on and the user turns the schedule off then the schedule expires until
73
+ // such time as the time off has expired. When that occurs the schedule should be reset to run at the designated
74
+ // time. If the user resets the schedule by turning the circuit back on again then the schedule will resume and
75
+ // turn off at the specified time.
76
+ //
77
+ // LOCKOUTS (Proposed):
78
+ // Spillway Lockout: This locks out any circuit or feature that is marked with a Spillway circuit function (type) whenever
79
+ // whenever the pool circuit is not engaged. This should mark the spillway circuit as a delayStart then release it when the
80
+ // pool body starts.
81
+ interface ILockout {
82
+ type: string
83
+ }
84
+ export class EquipmentLockout implements ILockout {
85
+ public id = utils.uuid();
86
+ public create() { }
87
+ public startTime: Date;
88
+ public type: string = 'lockout';
89
+ public message: string = '';
90
+ }
91
+ export class EquipmentDelay implements ILockout {
92
+ public constructor() { this.id = delayMgr.getNextId(); }
93
+ public id;
94
+ public type: string = 'delay';
95
+ public startTime: Date;
96
+ public endTime: Date;
97
+ public canCancel: boolean = true;
98
+ public cancelDelay() { };
99
+ public reset() { };
100
+ public clearDelay() { };
101
+ public message;
102
+ protected _delayTimer: NodeJS.Timeout;
103
+ public serialize(): any {
104
+ return {
105
+ id: this.id,
106
+ type: this.type,
107
+ canCancel: this.canCancel,
108
+ message: this.message,
109
+ startTime: typeof this.startTime !== 'undefined' ? Timestamp.toISOLocal(this.startTime) : undefined,
110
+ endTime: typeof this.endTime !== 'undefined' ? Timestamp.toISOLocal(this.endTime) : undefined,
111
+ duration: typeof this.startTime !== 'undefined' && typeof this.endTime !== 'undefined' ? (this.endTime.getTime() - this.startTime.getTime()) / 1000 : 0
112
+ };
113
+ }
114
+ }
115
+ export class ManualPriorityDelay extends EquipmentDelay {
116
+ public constructor(cs: ICircuitState) {
117
+ super();
118
+ this.type = 'manualOperationPriorityDelay';
119
+ this.message = `${cs.name} will override future schedules until expired/cancelled.`;
120
+ this.circuitState = cs;
121
+ this.circuitState.manualPriorityActive = true;
122
+ this.startTime = new Date();
123
+ this.endTime = cs.endTime.clone().toDate();
124
+ this._delayTimer = setTimeout(() => {
125
+ logger.info(`Manual Operation Priority expired for ${this.circuitState.name}`);
126
+ this.circuitState.manualPriorityActive = false;
127
+ delayMgr.deleteDelay(this.id);
128
+ }, this.endTime.getTime() - new Date().getTime());
129
+ logger.info(`Manual Operation Priority delay in effect until ${this.circuitState.name} - ${cs.endTime.toDate()}`);
130
+ }
131
+ public circuitState: ICircuitState;
132
+ public cancelDelay() {
133
+ this.circuitState.manualPriorityActive = false;
134
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
135
+ logger.info(`Manual Operation Priority cancelled for ${this.circuitState.name}`);
136
+ this._delayTimer = undefined;
137
+ this.circuitState.manualPriorityActive = false;
138
+ // Rip through all the schedules and clear the manual priority.
139
+ let sscheds = state.schedules.getActiveSchedules();
140
+ let circIds = [];
141
+ for (let i = 0; i < sscheds.length; i++) {
142
+ let ssched = sscheds[i];
143
+ ssched.manualPriorityActive = false;
144
+ if (!circIds.includes(ssched.circuit)) circIds.push(ssched.circuit);
145
+ }
146
+ for (let i = 0; i < circIds.length; i++) {
147
+ let circ = sys.circuits.getInterfaceById(circIds[i]);
148
+ if (!circ.isActive) continue;
149
+ let cstate = state.circuits.getInterfaceById(circ.id);
150
+ sys.board.circuits.setEndTime(circ, cstate, cstate.isOn, true);
151
+ }
152
+
153
+ delayMgr.deleteDelay(this.id);
154
+ }
155
+ public clearDelay() {
156
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
157
+ logger.info(`Manual Operation Priority cleared for ${this.circuitState.name}`);
158
+ this._delayTimer = undefined;
159
+ delayMgr.deleteDelay(this.id);
160
+ }
161
+ }
162
+ export class PumpValveDelay extends EquipmentDelay {
163
+ public constructor(ps: PumpState, delay?: number) {
164
+ super();
165
+ this.type = 'pumpValveDelay';
166
+ this.message = `${ps.name} will start after valve delay`;
167
+ this.pumpState = ps;
168
+ this.pumpState.pumpOnDelay = true;
169
+ this.startTime = new Date();
170
+ this.endTime = new Date(this.startTime.getTime() + (delay * 1000 || sys.general.options.valveDelayTime * 1000));
171
+ this._delayTimer = setTimeout(() => {
172
+ logger.info(`Valve delay expired for ${this.pumpState.name}`);
173
+ this.pumpState.pumpOnDelay = false;
174
+ delayMgr.deleteDelay(this.id);
175
+ }, delay * 1000 || sys.general.options.valveDelayTime * 1000);
176
+ logger.info(`Valve delay started for ${this.pumpState.name} - ${delay || sys.general.options.valveDelayTime}sec`);
177
+ }
178
+ public pumpState: PumpState;
179
+ public cancelDelay() {
180
+ this.pumpState.pumpOnDelay = false;
181
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
182
+ logger.info(`Valve delay cancelled for ${this.pumpState.name}`);
183
+ this._delayTimer = undefined;
184
+ delayMgr.deleteDelay(this.id);
185
+ }
186
+ public clearDelay() {
187
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
188
+ logger.info(`Valve delay cleared for ${this.pumpState.name}`);
189
+ this._delayTimer = undefined;
190
+ delayMgr.deleteDelay(this.id);
191
+ }
192
+ }
193
+ export class HeaterStartupDelay extends EquipmentDelay {
194
+ public constructor(hs: HeaterState, delay?: number) {
195
+ super();
196
+ this.type = 'heaterStartupDelay';
197
+ this.message = `${hs.name} will start after delay`;
198
+ this.heaterState = hs;
199
+ this.heaterState.startupDelay = true;
200
+ this.startTime = new Date();
201
+ this.endTime = new Date(this.startTime.getTime() + (delay * 1000 || sys.general.options.heaterStartDelayTime * 1000));
202
+ this._delayTimer = setTimeout(() => {
203
+ logger.info(`Heater Startup delay expired for ${this.heaterState.name}`);
204
+ this.heaterState.startupDelay = false;
205
+ delayMgr.deleteDelay(this.id);
206
+ }, delay * 1000 || sys.general.options.heaterStartDelayTime * 1000);
207
+ logger.info(`Heater delay started for ${this.heaterState.name} - ${delay || sys.general.options.heaterStartDelayTime}sec`);
208
+ }
209
+ public heaterState: HeaterState;
210
+ public cancelDelay() {
211
+ this.heaterState.startupDelay = false;
212
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
213
+ logger.info(`Heater Startup delay cancelled for ${this.heaterState.name}`);
214
+ this._delayTimer = undefined;
215
+ delayMgr.deleteDelay(this.id);
216
+ }
217
+ public clearDelay() {
218
+ this.heaterState.startupDelay = false;
219
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
220
+ logger.info(`Heater Startup delay cancelled for ${this.heaterState.name}`);
221
+ this._delayTimer = undefined;
222
+ delayMgr.deleteDelay(this.id);
223
+ }
224
+ }
225
+ export class HeaterCooldownDelay extends EquipmentDelay {
226
+ public constructor(bsoff: BodyTempState, bson?: BodyTempState, delay?: number) {
227
+ super();
228
+ this.type = 'heaterCooldownDelay';
229
+ this.message = `${bsoff.name} Heater Cooldown in progress`;
230
+ this.bodyStateOff = bsoff;
231
+ this.bodyStateOff.heaterCooldownDelay = true;
232
+ this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooldown');
233
+ let cstateOff = state.circuits.getItemById(bsoff.circuit);
234
+ this.bodyStateOn = bson;
235
+ this.bodyStateOff.stopDelay = cstateOff.stopDelay = true;
236
+ let cstateOn = (typeof bson !== 'undefined') ? state.circuits.getItemById(bson.circuit) : undefined;
237
+ if (typeof cstateOn !== 'undefined') {
238
+ this.bodyStateOn.startDelay = cstateOn.startDelay = true;
239
+ }
240
+ logger.verbose(`Heater Cooldown Delay started for ${this.bodyStateOff.name} - ${delay/1000}sec`);
241
+ this.startTime = new Date();
242
+ this.endTime = new Date(this.startTime.getTime() + (delay * 1000));
243
+ this._delayTimer = setTimeout(() => {
244
+ logger.verbose(`Heater Cooldown delay expired for ${this.bodyStateOff.name}`);
245
+ this.bodyStateOff.stopDelay = state.circuits.getItemById(this.bodyStateOff.circuit).stopDelay = false;
246
+ // Now that the startup delay expired cancel the delay and shut off the circuit.
247
+ (async () => {
248
+ try {
249
+ await sys.board.circuits.setCircuitStateAsync(cstateOff.id, false, true);
250
+ if (typeof this.bodyStateOn !== 'undefined') {
251
+ this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
252
+ await sys.board.circuits.setCircuitStateAsync(this.bodyStateOn.circuit, true);
253
+ }
254
+ } catch (err) { logger.error(`Error executing Cooldown Delay completion: ${err}`); }
255
+ })();
256
+ this.bodyStateOff.heaterCooldownDelay = false;
257
+ this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
258
+ delayMgr.deleteDelay(this.id);
259
+ }, delay);
260
+ state.emitEquipmentChanges();
261
+ }
262
+ public bodyStateOff: BodyTempState;
263
+ public bodyStateOn: BodyTempState;
264
+ public setBodyStateOn(bson?: BodyTempState) {
265
+ if (typeof this.bodyStateOn !== 'undefined' && (typeof bson === 'undefined' || this.bodyStateOn.id !== bson.id))
266
+ this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
267
+ if (typeof bson !== 'undefined') {
268
+ if (typeof this.bodyStateOn === 'undefined' || this.bodyStateOn.id !== bson.id) {
269
+ bson.startDelay = state.circuits.getItemById(bson.circuit).startDelay = true;
270
+ logger.info(`${bson.name} will Start After Cooldown Delay`);
271
+ this.bodyStateOn = bson;
272
+ }
273
+ }
274
+ else this.bodyStateOn = undefined;
275
+ }
276
+ public cancelDelay() {
277
+ let cstateOff = state.circuits.getItemById(this.bodyStateOff.circuit);
278
+ cstateOff.stopDelay = false;
279
+ (async () => {
280
+ await sys.board.circuits.setCircuitStateAsync(cstateOff.id, false);
281
+ if (typeof this.bodyStateOn !== 'undefined') {
282
+ this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
283
+ await sys.board.circuits.setCircuitStateAsync(this.bodyStateOn.circuit, true);
284
+ }
285
+ })();
286
+ this.bodyStateOff.stopDelay = this.bodyStateOff.heaterCooldownDelay = false;
287
+ this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
288
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
289
+ logger.info(`Heater Cooldown delay cancelled for ${this.bodyStateOff.name}`);
290
+ this._delayTimer = undefined;
291
+ delayMgr.deleteDelay(this.id);
292
+ state.emitEquipmentChanges();
293
+ }
294
+ public clearDelay() {
295
+ let cstateOff = state.circuits.getItemById(this.bodyStateOff.circuit);
296
+ cstateOff.stopDelay = false;
297
+ (async () => {
298
+ await sys.board.circuits.setCircuitStateAsync(cstateOff.id, false);
299
+ if (typeof this.bodyStateOn !== 'undefined') {
300
+ this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
301
+ await sys.board.circuits.setCircuitStateAsync(this.bodyStateOn.circuit, true);
302
+ }
303
+ })();
304
+ this.bodyStateOff.stopDelay = this.bodyStateOff.heaterCooldownDelay = false;
305
+ this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
306
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
307
+ logger.info(`Heater Cooldown delay cleared for ${this.bodyStateOff.name}`);
308
+ this._delayTimer = undefined;
309
+ delayMgr.deleteDelay(this.id);
310
+ state.emitEquipmentChanges();
311
+ }
312
+ }
313
+ interface ICleanerDelay {
314
+ cleanerState: ICircuitState,
315
+ bodyId: number
316
+ }
317
+ export class CleanerStartDelay extends EquipmentDelay implements ICleanerDelay {
318
+ constructor(cs: ICircuitState, bodyId: number, delay?: number) {
319
+ super();
320
+ this.type = 'cleanerStartDelay';
321
+ this.message = `${cs.name} will start after delay`;
322
+ this.bodyId = bodyId;
323
+ this.cleanerState = cs;
324
+ cs.startDelay = true;
325
+ this.startTime = new Date();
326
+ this.endTime = new Date(this.startTime.getTime() + (delay * 1000 || sys.general.options.cleanerStartDelayTime * 1000));
327
+ this._delayTimer = setTimeout(() => {
328
+ logger.info(`Cleaner delay expired for ${this.cleanerState.name}`);
329
+ this.cleanerState.startDelay = false;
330
+ (async () => {
331
+ try {
332
+ await sys.board.circuits.setCircuitStateAsync(this.cleanerState.id, true, true);
333
+ this.cleanerState.startDelay = false;
334
+ } catch (err) { logger.error(`Error executing Cleaner Start Delay completion: ${err}`); }
335
+ })();
336
+ delayMgr.deleteDelay(this.id);
337
+ }, delay * 1000 || sys.general.options.cleanerStartDelayTime * 1000);
338
+ logger.info(`Cleaner delay started for ${this.cleanerState.name} - ${delay || sys.general.options.cleanerStartDelayTime}sec`);
339
+ }
340
+ public cleanerState: ICircuitState;
341
+ public bodyId: number;
342
+ public cancelDelay() {
343
+ this.cleanerState.startDelay = false;
344
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
345
+ logger.info(`Cleaner Start delay cancelled for ${this.cleanerState.name}`);
346
+ this._delayTimer = undefined;
347
+ this.cleanerState.startDelay = false;
348
+ (async () => {
349
+ try {
350
+ await sys.board.circuits.setCircuitStateAsync(this.cleanerState.id, true, true);
351
+ } catch (err) { logger.error(`Error executing Cleaner Start Delay completion: ${err}`); }
352
+ })();
353
+ delayMgr.deleteDelay(this.id);
354
+ }
355
+ public clearDelay() {
356
+ this.cleanerState.startDelay = false;
357
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
358
+ logger.info(`Cleaner Start delay cleared for ${this.cleanerState.name}`);
359
+ this._delayTimer = undefined;
360
+ this.cleanerState.startDelay = false;
361
+ delayMgr.deleteDelay(this.id);
362
+ }
363
+
364
+ public reset(delay?: number) {
365
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
366
+ this.cleanerState.startDelay = true;
367
+ logger.info(`Cleaner Start delay reset for ${this.cleanerState.name}`);
368
+ this.startTime = new Date();
369
+ this.endTime = new Date(this.startTime.getTime() + (delay * 1000 || sys.general.options.cleanerStartDelayTime * 1000));
370
+ this._delayTimer = setTimeout(() => {
371
+ logger.info(`Cleaner delay expired for ${this.cleanerState.name}`);
372
+ this.cleanerState.startDelay = false;
373
+ (async () => {
374
+ try {
375
+ await sys.board.circuits.setCircuitStateAsync(this.cleanerState.id, true);
376
+ this.cleanerState.startDelay = false;
377
+ } catch (err) { logger.error(`Error executing Cleaner Start Delay completion: ${err}`); }
378
+ })();
379
+ delayMgr.deleteDelay(this.id);
380
+ }, delay * 1000 || sys.general.options.cleanerStartDelayTime * 1000);
381
+ }
382
+ }
383
+ export class DelayManager extends Array<EquipmentDelay> {
384
+ protected _id = 1;
385
+ private _emitTimer: NodeJS.Timeout;
386
+ public setDirty() {
387
+ if (typeof this._emitTimer) clearTimeout(this._emitTimer);
388
+ this._emitTimer = setTimeout(() => this.emitDelayState(), 1000);
389
+ }
390
+ public getNextId() { return this._id++; }
391
+ public cancelDelay(id: number) {
392
+ let del = this.find(x => x.id === id);
393
+ if (typeof del !== 'undefined') del.cancelDelay();
394
+ }
395
+ public clearAllDelays() {
396
+ for (let i = this.length - 1; i >= 0; i--) {
397
+ let del = this[i];
398
+ del.clearDelay();
399
+ }
400
+ }
401
+ public setManualPriorityDelay(cs: ICircuitState) {
402
+ let cds = this.filter(x => x.type === 'manualOperationPriorityDelay');
403
+ for (let i = 0; i < cds.length; i++) {
404
+ let delay = cds[i] as ManualPriorityDelay;
405
+ if (delay.circuitState.id === cs.id) delay.clearDelay();
406
+ }
407
+ this.push(new ManualPriorityDelay(cs)); this.setDirty();
408
+ }
409
+ public cancelManualPriorityDelays() { this.cancelDelaysByType('manualOperationPriorityDelay'); this.setDirty(); }
410
+ public cancelManualPriorityDelay(id: number){
411
+ let delays = this.filter(x => x.type === 'manualOperationPriorityDelay');
412
+ for (let i = 0; i < delays.length; i++) {
413
+ if((delays[i] as ManualPriorityDelay).circuitState.id === id) delays[i].cancelDelay();
414
+ }
415
+ }
416
+ public setPumpValveDelay(ps: PumpState, delay?: number) {
417
+ let cds = this.filter(x => x.type === 'pumpValveDelay');
418
+ for (let i = 0; i < cds.length; i++) {
419
+ let delay = cds[i] as PumpValveDelay;
420
+ if (delay.pumpState.id === ps.id) delay.clearDelay();
421
+ }
422
+ this.push(new PumpValveDelay(ps, delay)); this.setDirty();
423
+ }
424
+ public cancelPumpValveDelays() { this.cancelDelaysByType('pumpValveDelay'); this.setDirty(); }
425
+ public setHeaterStartupDelay(hs: HeaterState, delay?: number) {
426
+ let cds = this.filter(x => x.type === 'heaterStartupDelay');
427
+ for (let i = 0; i < cds.length; i++) {
428
+ let delay = cds[i] as HeaterStartupDelay;
429
+ if (delay.heaterState.id === hs.id) delay.cancelDelay();
430
+ }
431
+ this.push(new HeaterStartupDelay(hs, delay)); this.setDirty();
432
+ }
433
+ public cancelHeaterStartupDelays() {
434
+ this.cancelDelaysByType('heaterStartupDelay');
435
+ }
436
+ public setHeaterCooldownDelay(bsOff: BodyTempState, bsOn?: BodyTempState, delay?: number) {
437
+ logger.info(`Setting Heater Cooldown Delay for ${bsOff.name}`);
438
+ let cds = this.filter(x => x.type === 'heaterCooldownDelay');
439
+ for (let i = 0; i < cds.length; i++) {
440
+ let delay = cds[i] as HeaterCooldownDelay;
441
+ if (delay.bodyStateOff.id === bsOff.id) {
442
+ if(typeof bsOn !== 'undefined') logger.info(`Found Cooldown Delay adding on circuit ${bsOn.name}`);
443
+ delay.setBodyStateOn(bsOn);
444
+ this.setDirty();
445
+ return;
446
+ }
447
+ }
448
+ this.push(new HeaterCooldownDelay(bsOff, bsOn, delay));
449
+ this.setDirty();
450
+ }
451
+ public clearBodyStartupDelay(bs: BodyTempState) {
452
+ logger.info(`Clearing startup delays for ${bs.name}`);
453
+ // We are doing this non type safety thing below so that
454
+ // we can only emit when the body is cleared.
455
+ let cds = this.filter(x => {
456
+ return x.type === 'heaterCooldownDelay' &&
457
+ typeof x['bodyStateOn'] !== 'undefined' &&
458
+ x['bodyStateOn'].id === bs.id;
459
+ });
460
+ for (let i = 0; i < cds.length; i++) {
461
+ let delay = cds[i] as HeaterCooldownDelay;
462
+ logger.info(`Clearing ${bs.name} from Cooldown Delay`);
463
+ delay.setBodyStateOn();
464
+ }
465
+ if (cds.length) this.setDirty();
466
+ }
467
+ public cancelHeaterCooldownDelays() { this.cancelDelaysByType('heaterCooldownDelay'); }
468
+ public setCleanerStartDelay(cs: ICircuitState, bodyId: number, delay?: number) {
469
+ let cds = this.filter(x => x.type === ('cleanerStartDelay' || 'cleanerSolarDelay'));
470
+ let startDelay: CleanerStartDelay;
471
+ for (let i = 0; i < cds.length; i++) {
472
+ let delay = cds[i] as unknown as ICleanerDelay;
473
+ if (delay.cleanerState.id === cs.id) {
474
+ if (delay.bodyId !== bodyId || cds[i].type !== 'cleanerStartDelay') cds[i].cancelDelay();
475
+ else if (typeof startDelay !== 'undefined') {
476
+ startDelay.cancelDelay();
477
+ startDelay = cds[i] as CleanerStartDelay;
478
+ }
479
+ else startDelay = cds[i] as CleanerStartDelay;
480
+ }
481
+ }
482
+ if (typeof startDelay !== 'undefined') {
483
+ startDelay.reset(delay);
484
+ this.setDirty();
485
+ }
486
+ else {
487
+ this.push(new CleanerStartDelay(cs, bodyId, delay));
488
+ this.setDirty();
489
+ }
490
+ }
491
+ public cancelCleanerStartDelays(bodyId?: number) {
492
+ if (typeof bodyId === 'undefined') this.cancelDelaysByType('cleanerStartDelay');
493
+ else {
494
+ let delays = this.filter(x => x.type === 'cleanerStartDelay' && x['bodyId'] === bodyId);
495
+ for (let i = 0; i < delays.length; i++) {
496
+ delays[i].cancelDelay();
497
+ }
498
+ if (delays.length > 0) this.setDirty();
499
+ }
500
+ }
501
+ public clearCleanerStartDelays(bodyId?: number) {
502
+ if (typeof bodyId === 'undefined') this.clearDelaysByType('cleanerStartDelay');
503
+ else {
504
+ let delays = this.filter(x => x.type === 'cleanerStartDelay' && x['bodyId'] === bodyId);
505
+ for (let i = 0; i < delays.length; i++) {
506
+ delays[i].clearDelay();
507
+ }
508
+ if (delays.length > 0) this.setDirty();
509
+ }
510
+ }
511
+ public deleteDelay(id: number) {
512
+ for (let i = this.length - 1; i >= 0; i--) {
513
+ if (this[i].id === id) {
514
+ this.splice(i, 1);
515
+ this.setDirty();
516
+ }
517
+ }
518
+ }
519
+ public setSolarStartupDelay
520
+ protected cancelDelaysByType(type: string) {
521
+ let delays = this.filter(x => x.type === type);
522
+ for (let i = 0; i < delays.length; i++) {
523
+ delays[i].cancelDelay();
524
+ }
525
+ }
526
+ protected clearDelaysByType(type: string) {
527
+ let delays = this.filter(x => x.type === type);
528
+ for (let i = 0; i < delays.length; i++) {
529
+ delays[i].clearDelay();
530
+ }
531
+ if (delays.length > 0) this.setDirty();
532
+ }
533
+ public serialize() {
534
+ try {
535
+ let delays = [];
536
+ for (let i = 0; i < this.length; i++) {
537
+ delays.push(this[i].serialize());
538
+ }
539
+ return delays;
540
+ } catch (err) { logger.error(`Error serializing delays: ${err.message}`); }
541
+ }
542
+ public emitDelayState() {
543
+ try {
544
+ // We have to use a custom serializer because the properties of
545
+ // our delays will create a circular reference due to the timers and state references.
546
+ webApp.emitToClients('delays', this.serialize());
547
+ } catch (err) { logger.error(`Error emitting delay states ${err.message}`); }
548
+ }
549
+ }
550
550
  export let delayMgr = new DelayManager();