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,367 +1,367 @@
1
- import { InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError } from '../../Errors';
2
- import { utils, Timestamp, ControllerType } from '../../Constants';
3
- import { logger } from '../../../logger/Logger';
4
-
5
- import { NixieEquipment, NixieChildEquipment, NixieEquipmentCollection, INixieControlPanel } from "../NixieEquipment";
6
- import { Chlorinator, sys, ChlorinatorCollection } from "../../../controller/Equipment";
7
- import { ChlorinatorState, state, } from "../../State";
8
- import { setTimeout as setTimeoutSync, clearTimeout } from 'timers';
9
- import { webApp, InterfaceServerResponse } from "../../../web/Server";
10
- import { Outbound, Protocol, Response } from '../../comms/messages/Messages';
11
- import { conn } from '../../comms/Comms';
12
- import { ncp } from '../Nixie';
13
- import { setTimeout } from 'timers/promises';
14
-
15
- export class NixieChlorinatorCollection extends NixieEquipmentCollection<NixieChlorinator> {
16
- public async deleteChlorinatorAsync(id: number) {
17
- try {
18
- // Since we don't have hash tables per se in TS go through all the entries and remove every one that
19
- // matches the id. This will ensure cleanup of a dirty array.
20
- for (let i = this.length - 1; i >= 0; i--) {
21
- let c = this[i];
22
- if (c.id === id) {
23
- await ncp.chemControllers.deleteChlorAsync(c as NixieChlorinator);
24
- await c.closeAsync();
25
- this.splice(i, 1);
26
- }
27
- }
28
- } catch (err) { logger.error(`NCP: Error removing chlorinator`); }
29
-
30
- }
31
- public async setChlorinatorAsync(chlor: Chlorinator, data: any) {
32
- // By the time we get here we know that we are in control and this is a REMChem.
33
- try {
34
- let c: NixieChlorinator = this.find(elem => elem.id === chlor.id) as NixieChlorinator;
35
- if (typeof c === 'undefined') {
36
- chlor.master = 1;
37
- c = new NixieChlorinator(this.controlPanel, chlor);
38
- this.push(c);
39
- await c.setChlorinatorAsync(data);
40
- logger.info(`A Chlorinator was not found for id #${chlor.id} starting Nixie Chlorinator`);
41
- await c.initAsync();
42
- }
43
- else {
44
- await c.setChlorinatorAsync(data);
45
- }
46
- }
47
- catch (err) { logger.error(`setChlorinatorAsync: ${err.message}`); return Promise.reject(err); }
48
- }
49
- public async setServiceModeAsync() {
50
- try {
51
- for (let i = this.length - 1; i >= 0; i--) {
52
- let c = this[i] as NixieChlorinator;
53
- await c.setServiceModeAsync();
54
- }
55
- } catch (err) { logger.error(`Nixie Chlorinator Error setServiceModeAsync: ${err.message}`) }
56
- }
57
- public async initAsync(chlorinators: ChlorinatorCollection) {
58
- try {
59
- for (let i = 0; i < chlorinators.length; i++) {
60
- let cc = chlorinators.getItemByIndex(i);
61
- if (cc.master === 1) {
62
- if (typeof this.find(elem => elem.id === cc.id) === 'undefined') {
63
- logger.info(`Initializing Nixie chlorinator ${cc.name}`);
64
- let ncc = new NixieChlorinator(this.controlPanel, cc);
65
- this.push(ncc);
66
- await ncc.initAsync();
67
- }
68
- }
69
- }
70
- }
71
- catch (err) { logger.error(`initAsync: ${err.message}`); return Promise.reject(err); }
72
- }
73
- public async closeAsync() {
74
- try {
75
- for (let i = this.length - 1; i >= 0; i--) {
76
- try {
77
- await this[i].closeAsync();
78
- this.splice(i, 1);
79
- } catch (err) { logger.error(`Error stopping Nixie Chlorinator ${err}`); }
80
- }
81
- } catch (err) { } // Don't bail if we have an error
82
- }
83
- }
84
- export class NixieChlorinator extends NixieEquipment {
85
- public pollingInterval: number = 3000;
86
- private _pollTimer: NodeJS.Timeout = null;
87
- private superChlorinating: boolean = false;
88
- private superChlorStart: number = 0;
89
- public chlor: Chlorinator;
90
- public bodyOnTime: number;
91
- protected _suspendPolling: number = 0;
92
- public closing = false;
93
- constructor(ncp: INixieControlPanel, chlor: Chlorinator) {
94
- super(ncp);
95
- this.chlor = chlor;
96
- }
97
- public get id(): number { return typeof this.chlor !== 'undefined' ? this.chlor.id : -1; }
98
- public get suspendPolling(): boolean { return this._suspendPolling > 0; }
99
- public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
100
- public get superChlorRemaining(): number {
101
- if (typeof this.superChlorStart === 'undefined' || this.superChlorStart === 0 || !this.chlor.superChlor) return 0;
102
- return Math.max(Math.floor(((this.chlor.superChlorHours * 3600 * 1000) - (new Date().getTime() - this.superChlorStart)) / 1000), 0);
103
- }
104
- public async setServiceModeAsync() {
105
- let cstate = state.chlorinators.getItemById(this.chlor.id);
106
- cstate.targetOutput = 0;
107
- cstate.superChlor = this.chlor.superChlor = false;
108
- await this.setSuperChlor(cstate);
109
- await this.sendOutputMessageAsync(cstate);
110
- }
111
- public async setChlorinatorAsync(data: any) {
112
- try {
113
- let chlor = this.chlor;
114
- if (chlor.type === sys.board.valueMaps.chlorinatorType.getValue('intellichlor')) {
115
-
116
- }
117
- let poolSetpoint = typeof data.poolSetpoint !== 'undefined' ? parseInt(data.poolSetpoint, 10) : chlor.poolSetpoint;
118
- let spaSetpoint = typeof data.spaSetpoint !== 'undefined' ? parseInt(data.spaSetpoint, 10) : chlor.spaSetpoint;
119
- let body = sys.board.bodies.mapBodyAssociation(typeof data.body === 'undefined' ? chlor.body : data.body);
120
- let superChlor = typeof data.superChlor !== 'undefined' ? utils.makeBool(data.superChlor) : typeof data.superChlorinate !== 'undefined' ? utils.makeBool(data.superChlorinate) : chlor.superChlor;
121
- let chlorType = typeof data.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(data.type) : chlor.type || 0;
122
- let superChlorHours = typeof data.superChlorHours !== 'undefined' ? parseInt(data.superChlorHours, 10) : chlor.superChlorHours;
123
- let disabled = typeof data.disabled !== 'undefined' ? utils.makeBool(data.disabled) : chlor.disabled;
124
- let isDosing = typeof data.isDosing !== 'undefined' ? utils.makeBool(data.isDosing) : chlor.isDosing;
125
- let model = typeof data.model !== 'undefined' ? sys.board.valueMaps.chlorinatorModel.encode(data.model) : chlor.model || 0;
126
- let saltTarget = typeof data.saltTarget === 'number' ? parseInt(data.saltTarget, 10) : chlor.saltTarget;
127
- let address = typeof data.address !== 'undefined' ? parseInt(data.address, 10) : chlor.address || 80;
128
- let portId = typeof data.portId !== 'undefined' ? parseInt(data.portId, 10) : chlor.portId;
129
- if (chlor.portId !== portId) {
130
- if (portId === 0 && sys.controllerType !== ControllerType.Nixie) return Promise.reject(new InvalidEquipmentDataError(`You may not install a chlorinator on an ${sys.controllerType} system that is assigned to the Primary Port that is under Nixe control`, 'Chlorinator', portId));
131
- }
132
- if (portId !== chlor.portId && sys.chlorinators.count(elem => elem.id !== this.chlor.id && elem.portId === portId && elem.master !== 2) > 0) return Promise.reject(new InvalidEquipmentDataError(`Another chlorinator is installed on port #${portId}. Only one chlorinator can be installed per port.`, 'Chlorinator', portId));
133
- if (isNaN(portId)) return Promise.reject(new InvalidEquipmentDataError(`Invalid port Id`, 'Chlorinator', data.portId));
134
- if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid body assignment`, 'Chlorinator', data.body || chlor.body));
135
- if (isNaN(poolSetpoint)) poolSetpoint = 0;
136
- if (isNaN(spaSetpoint)) spaSetpoint = 0;
137
-
138
- chlor.ignoreSaltReading = (typeof data.ignoreSaltReading !== 'undefined') ? utils.makeBool(data.ignoreSaltReading) : utils.makeBool(chlor.ignoreSaltReading);
139
- // Do a final validation pass so we dont send this off in a mess.
140
- let schlor = state.chlorinators.getItemById(chlor.id, true);
141
- schlor.poolSetpoint = chlor.poolSetpoint = poolSetpoint;
142
- schlor.spaSetpoint = chlor.spaSetpoint = spaSetpoint;
143
- schlor.superChlor = chlor.superChlor = superChlor;
144
- schlor.superChlorHours = chlor.superChlorHours = superChlorHours;
145
- schlor.type = chlor.type = chlorType;
146
- schlor.model = chlor.model = model;
147
- schlor.body = chlor.body = body.val;
148
- chlor.portId = portId;
149
- chlor.address = address;
150
- chlor.disabled = disabled;
151
- chlor.saltTarget = saltTarget;
152
- chlor.isDosing = isDosing;
153
- schlor.name = chlor.name = data.name || chlor.name || `Chlorinator ${chlor.id}`;
154
- schlor.isActive = chlor.isActive = true;
155
- chlor.hasChanged = true;
156
- if (!chlor.superChlor) {
157
- this.superChlorStart = 0;
158
- this.superChlorinating = false;
159
- }
160
- this.pollEquipmentAsync();
161
- }
162
- catch (err) { logger.error(`setChlorinatorAsync: ${err.message}`); return Promise.reject(err); }
163
- }
164
- public async closeAsync() {
165
- try {
166
- this.closing = true; // This will tell the polling cycle to stop what it is doing and don't restart.
167
- if (this._pollTimer) {
168
- clearTimeout(this._pollTimer);
169
- this._pollTimer = undefined;
170
- }
171
- // Let the Nixie equipment do its thing if it needs to.
172
- await super.closeAsync();
173
- }
174
- catch (err) { logger.error(`Chlorinator closeAsync: ${err.message}`); return Promise.reject(err); }
175
- }
176
- public async initAsync() {
177
- try {
178
- // Start our polling but only after we clean up any other polling going on.
179
- if (this._pollTimer) {
180
- clearTimeout(this._pollTimer);
181
- this._pollTimer = undefined;
182
- }
183
- this.closing = false;
184
- this._suspendPolling = 0;
185
- // During startup it won't be uncommon for the comms to be out. This will be because the body will be off so don't stress it so much.
186
- logger.debug(`Begin sending chlorinator messages ${this.chlor.name}`);
187
- this.pollEquipmentAsync();
188
- } catch (err) { logger.error(`Error initializing ${this.chlor.name} : ${err.message}`); }
189
- }
190
- public isBodyOn() { return sys.board.bodies.isBodyOn(this.chlor.body); }
191
- public setSuperChlor(cstate: ChlorinatorState) {
192
- if (this.chlor.superChlor) {
193
- if (!this.superChlorinating) {
194
- // Deal with the start time.
195
- let hours = this.chlor.superChlorHours * 3600;
196
- let offset = cstate.superChlorRemaining > 0 ? Math.max((hours - (hours - cstate.superChlorRemaining)), 0) : 0;
197
- this.superChlorStart = new Date().getTime() - offset;
198
- this.superChlorinating = true;
199
- }
200
- cstate.superChlorRemaining = this.superChlorRemaining;
201
- }
202
- else {
203
- this.superChlorStart = 0;
204
- this.superChlorinating = false;
205
- cstate.superChlorRemaining = 0;
206
- }
207
- }
208
- public async pollEquipmentAsync() {
209
- let self = this;
210
- try {
211
- if (this._pollTimer) {
212
- clearTimeout(this._pollTimer);
213
- this._pollTimer = undefined;
214
- }
215
- if (!this.suspendPolling) {
216
- try {
217
- this.suspendPolling = true;
218
- if (state.mode === 0) {
219
- if (!this.closing) await this.takeControlAsync();
220
- if (!this.closing) await setTimeout(300);
221
- if (!this.closing) await this.setOutputAsync();
222
- if (!this.closing) await setTimeout(300);
223
- if (!this.closing) await this.getModelAsync();
224
- }
225
- } catch (err) {
226
- // We only display an error here if the body is on. The chlorinator should be powered down when it is not.
227
- if (this.isBodyOn()) logger.error(`Chlorinator ${this.chlor.name} comms failure: ${err.message}`);
228
- }
229
- finally { this.suspendPolling = false; }
230
- }
231
- } catch (err) {
232
- // Comms failure will be handeled by the message processor.
233
- logger.error(`Chlorinator ${this.chlor.name} comms failure: ${err.message}`);
234
- }
235
- finally { if (!this.closing) this._pollTimer = setTimeoutSync(() => { self.pollEquipmentAsync(); }, this.pollingInterval); }
236
- }
237
- public async takeControlAsync(): Promise<void> {
238
- //try {
239
- let cstate = state.chlorinators.getItemById(this.chlor.id, true);
240
- // The sequence is as follows.
241
- // 0 = Disabled control panel by taking control of it.
242
- // 17 = Set the current setpoint
243
- // 20 = Request the status
244
- // Disable the control panel by sending an action 0 the chlorinator should respond with an action 1.
245
- //[16, 2, 80, 0][0][98, 16, 3]
246
- //let success = await new Promise<boolean>((resolve, reject) => {
247
- if (conn.isPortEnabled(this.chlor.portId || 0)) {
248
- let out = Outbound.create({
249
- portId: this.chlor.portId || 0,
250
- protocol: Protocol.Chlorinator,
251
- dest: this.chlor.address || 80,
252
- action: 0,
253
- payload: [0],
254
- retries: 3, // IntelliCenter tries 4 times to get a response.
255
- response: Response.create({ protocol: Protocol.Chlorinator, action: 1 }),
256
- onAbort: () => { this.chlor.superChlor = cstate.superChlor = false; this.setSuperChlor(cstate); },
257
- });
258
- try {
259
- // If this is successful the action 1 message will have been
260
- // digested by ChlorinatorStateMessage and the lastComm will have been set clearing the
261
- // communication lost flag.
262
- await out.sendAsync();
263
- cstate.emitEquipmentChange();
264
- }
265
- catch (err) {
266
- logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`);
267
- // This flag is cleared in ChlorinatorStateMessage
268
- this.chlor.superChlor = cstate.superChlor = false;
269
- this.setSuperChlor(cstate);
270
- cstate.status = 128;
271
- }
272
- }
273
- }
274
- public async setOutputAsync(): Promise<void> {
275
- try {
276
- // A couple of things need to be in place before setting the output.
277
- // 1. The chlorinator will have to have responded to the takeControl message.
278
- // 2. If the body is not on then we need to send it a 0 output. This is just in case the
279
- // chlorinator is not wired into the filter relay. The current output should be 0 if no body is on.
280
- // 3. If we are superchlorinating and the remaing superChlor time is > 0 then we need to keep it at 100%.
281
- // 4. If the chlorinator disabled flag is set then we need to make sure the setpoint is 0.
282
- let cstate = state.chlorinators.getItemById(this.chlor.id, true);
283
- let setpoint = 0;
284
- if (this.isBodyOn()) {
285
- if (sys.equipment.shared) {
286
- let body = state.temps.bodies.getBodyIsOn();
287
- setpoint = (body.id === 1) ? this.chlor.poolSetpoint : this.chlor.spaSetpoint;
288
- }
289
- else setpoint = this.chlor.body === 0 ? this.chlor.poolSetpoint : this.chlor.spaSetpoint;
290
- if (this.chlor.isDosing) setpoint = 100;
291
- }
292
- else {
293
- this.chlor.superChlor = cstate.superChlor = false;
294
- this.setSuperChlor(cstate);
295
- }
296
- if (this.chlor.disabled === true) setpoint = 0; // Our target should be 0 because we have other things going on. For instance,
297
- // RKS: Not sure if it needs to be smart enough to force an off message when the comms die.
298
- //if (cstate.status === 128) setpoint = 0; // If we haven't been able to get a response from the clorinator tell is to turn itself off.
299
- // Perhaps we will be luckier on the next poll cycle.
300
- // Tell the chlorinator that we are to use the current output.
301
- //[16, 2, 80, 17][0][115, 16, 3]
302
- cstate.targetOutput = cstate.superChlor ? 100 : setpoint;
303
- await this.sendOutputMessageAsync(cstate);
304
- cstate.emitEquipmentChange();
305
- } catch (err) { logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`); return Promise.reject(err); }
306
-
307
- }
308
- public async sendOutputMessageAsync(cstate: ChlorinatorState): Promise<void> {
309
- if (conn.isPortEnabled(this.chlor.portId || 0)) {
310
- let out = Outbound.create({
311
- portId: this.chlor.portId || 0,
312
- protocol: Protocol.Chlorinator,
313
- dest: this.chlor.address || 80,
314
- action: 17,
315
- payload: [cstate.targetOutput],
316
- retries: 3, // IntelliCenter tries 8 times to make this happen.
317
- response: Response.create({ protocol: Protocol.Chlorinator, action: 18 }),
318
- onAbort: () => { }
319
- });
320
- // #338
321
- if (cstate.targetOutput === 16) { out.appendPayloadByte(0); }
322
- try {
323
- await out.sendAsync();
324
- cstate.currentOutput = cstate.targetOutput;
325
- this.setSuperChlor(cstate);
326
- }
327
- catch (err) {
328
- cstate.currentOutput = 0;
329
- cstate.status = 128;
330
- }
331
- }
332
- else {
333
- cstate.currentOutput = cstate.targetOutput;
334
- this.setSuperChlor(cstate);
335
- }
336
-
337
- }
338
- public async getModelAsync() {
339
- // We only need to ask for this if we can communicate with the chlorinator. IntelliCenter
340
- // asks for this anyway but it really is gratuitous. If the setOutput and takeControl fail
341
- // then this will too.
342
- // RSG - Only need to ask if the model or name isn't defined. Added checks below.
343
- let cstate = state.chlorinators.getItemById(this.chlor.id, true);
344
- if (cstate.status !== 128 && (typeof cstate.model === 'undefined' || cstate.model === 0 || typeof cstate.name === 'undefined' || cstate.name === '')) {
345
- // Ask the chlorinator for its model.
346
- //[16, 2, 80, 20][0][118, 16, 3]
347
- if (conn.isPortEnabled(this.chlor.portId || 0)) {
348
- let out = Outbound.create({
349
- portId: this.chlor.portId || 0,
350
- protocol: Protocol.Chlorinator,
351
- dest: this.chlor.address || 80,
352
- action: 20,
353
- payload: [0],
354
- retries: 3, // IntelliCenter tries 4 times to get a response.
355
- response: Response.create({ protocol: Protocol.Chlorinator, action: 3 }),
356
- onAbort: () => { }
357
- });
358
- try {
359
- await out.sendAsync();
360
- }
361
- catch (err) {
362
- logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`);
363
- }
364
- }
365
- }
366
- }
367
- }
1
+ import { InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError } from '../../Errors';
2
+ import { utils, Timestamp, ControllerType } from '../../Constants';
3
+ import { logger } from '../../../logger/Logger';
4
+
5
+ import { NixieEquipment, NixieChildEquipment, NixieEquipmentCollection, INixieControlPanel } from "../NixieEquipment";
6
+ import { Chlorinator, sys, ChlorinatorCollection } from "../../../controller/Equipment";
7
+ import { ChlorinatorState, state, } from "../../State";
8
+ import { setTimeout as setTimeoutSync, clearTimeout } from 'timers';
9
+ import { webApp, InterfaceServerResponse } from "../../../web/Server";
10
+ import { Outbound, Protocol, Response } from '../../comms/messages/Messages';
11
+ import { conn } from '../../comms/Comms';
12
+ import { ncp } from '../Nixie';
13
+ import { setTimeout } from 'timers/promises';
14
+
15
+ export class NixieChlorinatorCollection extends NixieEquipmentCollection<NixieChlorinator> {
16
+ public async deleteChlorinatorAsync(id: number) {
17
+ try {
18
+ // Since we don't have hash tables per se in TS go through all the entries and remove every one that
19
+ // matches the id. This will ensure cleanup of a dirty array.
20
+ for (let i = this.length - 1; i >= 0; i--) {
21
+ let c = this[i];
22
+ if (c.id === id) {
23
+ await ncp.chemControllers.deleteChlorAsync(c as NixieChlorinator);
24
+ await c.closeAsync();
25
+ this.splice(i, 1);
26
+ }
27
+ }
28
+ } catch (err) { logger.error(`NCP: Error removing chlorinator`); }
29
+
30
+ }
31
+ public async setChlorinatorAsync(chlor: Chlorinator, data: any) {
32
+ // By the time we get here we know that we are in control and this is a REMChem.
33
+ try {
34
+ let c: NixieChlorinator = this.find(elem => elem.id === chlor.id) as NixieChlorinator;
35
+ if (typeof c === 'undefined') {
36
+ chlor.master = 1;
37
+ c = new NixieChlorinator(this.controlPanel, chlor);
38
+ this.push(c);
39
+ await c.setChlorinatorAsync(data);
40
+ logger.info(`A Chlorinator was not found for id #${chlor.id} starting Nixie Chlorinator`);
41
+ await c.initAsync();
42
+ }
43
+ else {
44
+ await c.setChlorinatorAsync(data);
45
+ }
46
+ }
47
+ catch (err) { logger.error(`setChlorinatorAsync: ${err.message}`); return Promise.reject(err); }
48
+ }
49
+ public async setServiceModeAsync() {
50
+ try {
51
+ for (let i = this.length - 1; i >= 0; i--) {
52
+ let c = this[i] as NixieChlorinator;
53
+ await c.setServiceModeAsync();
54
+ }
55
+ } catch (err) { logger.error(`Nixie Chlorinator Error setServiceModeAsync: ${err.message}`) }
56
+ }
57
+ public async initAsync(chlorinators: ChlorinatorCollection) {
58
+ try {
59
+ for (let i = 0; i < chlorinators.length; i++) {
60
+ let cc = chlorinators.getItemByIndex(i);
61
+ if (cc.master === 1) {
62
+ if (typeof this.find(elem => elem.id === cc.id) === 'undefined') {
63
+ logger.info(`Initializing Nixie chlorinator ${cc.name}`);
64
+ let ncc = new NixieChlorinator(this.controlPanel, cc);
65
+ this.push(ncc);
66
+ await ncc.initAsync();
67
+ }
68
+ }
69
+ }
70
+ }
71
+ catch (err) { logger.error(`initAsync: ${err.message}`); return Promise.reject(err); }
72
+ }
73
+ public async closeAsync() {
74
+ try {
75
+ for (let i = this.length - 1; i >= 0; i--) {
76
+ try {
77
+ await this[i].closeAsync();
78
+ this.splice(i, 1);
79
+ } catch (err) { logger.error(`Error stopping Nixie Chlorinator ${err}`); }
80
+ }
81
+ } catch (err) { } // Don't bail if we have an error
82
+ }
83
+ }
84
+ export class NixieChlorinator extends NixieEquipment {
85
+ public pollingInterval: number = 3000;
86
+ private _pollTimer: NodeJS.Timeout = null;
87
+ private superChlorinating: boolean = false;
88
+ private superChlorStart: number = 0;
89
+ public chlor: Chlorinator;
90
+ public bodyOnTime: number;
91
+ protected _suspendPolling: number = 0;
92
+ public closing = false;
93
+ constructor(ncp: INixieControlPanel, chlor: Chlorinator) {
94
+ super(ncp);
95
+ this.chlor = chlor;
96
+ }
97
+ public get id(): number { return typeof this.chlor !== 'undefined' ? this.chlor.id : -1; }
98
+ public get suspendPolling(): boolean { return this._suspendPolling > 0; }
99
+ public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
100
+ public get superChlorRemaining(): number {
101
+ if (typeof this.superChlorStart === 'undefined' || this.superChlorStart === 0 || !this.chlor.superChlor) return 0;
102
+ return Math.max(Math.floor(((this.chlor.superChlorHours * 3600 * 1000) - (new Date().getTime() - this.superChlorStart)) / 1000), 0);
103
+ }
104
+ public async setServiceModeAsync() {
105
+ let cstate = state.chlorinators.getItemById(this.chlor.id);
106
+ cstate.targetOutput = 0;
107
+ cstate.superChlor = this.chlor.superChlor = false;
108
+ await this.setSuperChlor(cstate);
109
+ await this.sendOutputMessageAsync(cstate);
110
+ }
111
+ public async setChlorinatorAsync(data: any) {
112
+ try {
113
+ let chlor = this.chlor;
114
+ if (chlor.type === sys.board.valueMaps.chlorinatorType.getValue('intellichlor')) {
115
+
116
+ }
117
+ let poolSetpoint = typeof data.poolSetpoint !== 'undefined' ? parseInt(data.poolSetpoint, 10) : chlor.poolSetpoint;
118
+ let spaSetpoint = typeof data.spaSetpoint !== 'undefined' ? parseInt(data.spaSetpoint, 10) : chlor.spaSetpoint;
119
+ let body = sys.board.bodies.mapBodyAssociation(typeof data.body === 'undefined' ? chlor.body : data.body);
120
+ let superChlor = typeof data.superChlor !== 'undefined' ? utils.makeBool(data.superChlor) : typeof data.superChlorinate !== 'undefined' ? utils.makeBool(data.superChlorinate) : chlor.superChlor;
121
+ let chlorType = typeof data.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(data.type) : chlor.type || 0;
122
+ let superChlorHours = typeof data.superChlorHours !== 'undefined' ? parseInt(data.superChlorHours, 10) : chlor.superChlorHours;
123
+ let disabled = typeof data.disabled !== 'undefined' ? utils.makeBool(data.disabled) : chlor.disabled;
124
+ let isDosing = typeof data.isDosing !== 'undefined' ? utils.makeBool(data.isDosing) : chlor.isDosing;
125
+ let model = typeof data.model !== 'undefined' ? sys.board.valueMaps.chlorinatorModel.encode(data.model) : chlor.model || 0;
126
+ let saltTarget = typeof data.saltTarget === 'number' ? parseInt(data.saltTarget, 10) : chlor.saltTarget;
127
+ let address = typeof data.address !== 'undefined' ? parseInt(data.address, 10) : chlor.address || 80;
128
+ let portId = typeof data.portId !== 'undefined' ? parseInt(data.portId, 10) : chlor.portId;
129
+ if (chlor.portId !== portId) {
130
+ if (portId === 0 && sys.controllerType !== ControllerType.Nixie) return Promise.reject(new InvalidEquipmentDataError(`You may not install a chlorinator on an ${sys.controllerType} system that is assigned to the Primary Port that is under Nixe control`, 'Chlorinator', portId));
131
+ }
132
+ if (portId !== chlor.portId && sys.chlorinators.count(elem => elem.id !== this.chlor.id && elem.portId === portId && elem.master !== 2) > 0) return Promise.reject(new InvalidEquipmentDataError(`Another chlorinator is installed on port #${portId}. Only one chlorinator can be installed per port.`, 'Chlorinator', portId));
133
+ if (isNaN(portId)) return Promise.reject(new InvalidEquipmentDataError(`Invalid port Id`, 'Chlorinator', data.portId));
134
+ if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid body assignment`, 'Chlorinator', data.body || chlor.body));
135
+ if (isNaN(poolSetpoint)) poolSetpoint = 0;
136
+ if (isNaN(spaSetpoint)) spaSetpoint = 0;
137
+
138
+ chlor.ignoreSaltReading = (typeof data.ignoreSaltReading !== 'undefined') ? utils.makeBool(data.ignoreSaltReading) : utils.makeBool(chlor.ignoreSaltReading);
139
+ // Do a final validation pass so we dont send this off in a mess.
140
+ let schlor = state.chlorinators.getItemById(chlor.id, true);
141
+ schlor.poolSetpoint = chlor.poolSetpoint = poolSetpoint;
142
+ schlor.spaSetpoint = chlor.spaSetpoint = spaSetpoint;
143
+ schlor.superChlor = chlor.superChlor = superChlor;
144
+ schlor.superChlorHours = chlor.superChlorHours = superChlorHours;
145
+ schlor.type = chlor.type = chlorType;
146
+ schlor.model = chlor.model = model;
147
+ schlor.body = chlor.body = body.val;
148
+ chlor.portId = portId;
149
+ chlor.address = address;
150
+ chlor.disabled = disabled;
151
+ chlor.saltTarget = saltTarget;
152
+ chlor.isDosing = isDosing;
153
+ schlor.name = chlor.name = data.name || chlor.name || `Chlorinator ${chlor.id}`;
154
+ schlor.isActive = chlor.isActive = true;
155
+ chlor.hasChanged = true;
156
+ if (!chlor.superChlor) {
157
+ this.superChlorStart = 0;
158
+ this.superChlorinating = false;
159
+ }
160
+ this.pollEquipmentAsync();
161
+ }
162
+ catch (err) { logger.error(`setChlorinatorAsync: ${err.message}`); return Promise.reject(err); }
163
+ }
164
+ public async closeAsync() {
165
+ try {
166
+ this.closing = true; // This will tell the polling cycle to stop what it is doing and don't restart.
167
+ if (this._pollTimer) {
168
+ clearTimeout(this._pollTimer);
169
+ this._pollTimer = undefined;
170
+ }
171
+ // Let the Nixie equipment do its thing if it needs to.
172
+ await super.closeAsync();
173
+ }
174
+ catch (err) { logger.error(`Chlorinator closeAsync: ${err.message}`); return Promise.reject(err); }
175
+ }
176
+ public async initAsync() {
177
+ try {
178
+ // Start our polling but only after we clean up any other polling going on.
179
+ if (this._pollTimer) {
180
+ clearTimeout(this._pollTimer);
181
+ this._pollTimer = undefined;
182
+ }
183
+ this.closing = false;
184
+ this._suspendPolling = 0;
185
+ // During startup it won't be uncommon for the comms to be out. This will be because the body will be off so don't stress it so much.
186
+ logger.debug(`Begin sending chlorinator messages ${this.chlor.name}`);
187
+ this.pollEquipmentAsync();
188
+ } catch (err) { logger.error(`Error initializing ${this.chlor.name} : ${err.message}`); }
189
+ }
190
+ public isBodyOn() { return sys.board.bodies.isBodyOn(this.chlor.body); }
191
+ public setSuperChlor(cstate: ChlorinatorState) {
192
+ if (this.chlor.superChlor) {
193
+ if (!this.superChlorinating) {
194
+ // Deal with the start time.
195
+ let hours = this.chlor.superChlorHours * 3600;
196
+ let offset = cstate.superChlorRemaining > 0 ? Math.max((hours - (hours - cstate.superChlorRemaining)), 0) : 0;
197
+ this.superChlorStart = new Date().getTime() - offset;
198
+ this.superChlorinating = true;
199
+ }
200
+ cstate.superChlorRemaining = this.superChlorRemaining;
201
+ }
202
+ else {
203
+ this.superChlorStart = 0;
204
+ this.superChlorinating = false;
205
+ cstate.superChlorRemaining = 0;
206
+ }
207
+ }
208
+ public async pollEquipmentAsync() {
209
+ let self = this;
210
+ try {
211
+ if (this._pollTimer) {
212
+ clearTimeout(this._pollTimer);
213
+ this._pollTimer = undefined;
214
+ }
215
+ if (!this.suspendPolling) {
216
+ try {
217
+ this.suspendPolling = true;
218
+ if (state.mode === 0) {
219
+ if (!this.closing) await this.takeControlAsync();
220
+ if (!this.closing) await setTimeout(300);
221
+ if (!this.closing) await this.setOutputAsync();
222
+ if (!this.closing) await setTimeout(300);
223
+ if (!this.closing) await this.getModelAsync();
224
+ }
225
+ } catch (err) {
226
+ // We only display an error here if the body is on. The chlorinator should be powered down when it is not.
227
+ if (this.isBodyOn()) logger.error(`Chlorinator ${this.chlor.name} comms failure: ${err.message}`);
228
+ }
229
+ finally { this.suspendPolling = false; }
230
+ }
231
+ } catch (err) {
232
+ // Comms failure will be handeled by the message processor.
233
+ logger.error(`Chlorinator ${this.chlor.name} comms failure: ${err.message}`);
234
+ }
235
+ finally { if (!this.closing) this._pollTimer = setTimeoutSync(() => { self.pollEquipmentAsync(); }, this.pollingInterval); }
236
+ }
237
+ public async takeControlAsync(): Promise<void> {
238
+ //try {
239
+ let cstate = state.chlorinators.getItemById(this.chlor.id, true);
240
+ // The sequence is as follows.
241
+ // 0 = Disabled control panel by taking control of it.
242
+ // 17 = Set the current setpoint
243
+ // 20 = Request the status
244
+ // Disable the control panel by sending an action 0 the chlorinator should respond with an action 1.
245
+ //[16, 2, 80, 0][0][98, 16, 3]
246
+ //let success = await new Promise<boolean>((resolve, reject) => {
247
+ if (conn.isPortEnabled(this.chlor.portId || 0)) {
248
+ let out = Outbound.create({
249
+ portId: this.chlor.portId || 0,
250
+ protocol: Protocol.Chlorinator,
251
+ dest: this.chlor.address || 80,
252
+ action: 0,
253
+ payload: [0],
254
+ retries: 3, // IntelliCenter tries 4 times to get a response.
255
+ response: Response.create({ protocol: Protocol.Chlorinator, action: 1 }),
256
+ onAbort: () => { this.chlor.superChlor = cstate.superChlor = false; this.setSuperChlor(cstate); },
257
+ });
258
+ try {
259
+ // If this is successful the action 1 message will have been
260
+ // digested by ChlorinatorStateMessage and the lastComm will have been set clearing the
261
+ // communication lost flag.
262
+ await out.sendAsync();
263
+ cstate.emitEquipmentChange();
264
+ }
265
+ catch (err) {
266
+ logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`);
267
+ // This flag is cleared in ChlorinatorStateMessage
268
+ this.chlor.superChlor = cstate.superChlor = false;
269
+ this.setSuperChlor(cstate);
270
+ cstate.status = 128;
271
+ }
272
+ }
273
+ }
274
+ public async setOutputAsync(): Promise<void> {
275
+ try {
276
+ // A couple of things need to be in place before setting the output.
277
+ // 1. The chlorinator will have to have responded to the takeControl message.
278
+ // 2. If the body is not on then we need to send it a 0 output. This is just in case the
279
+ // chlorinator is not wired into the filter relay. The current output should be 0 if no body is on.
280
+ // 3. If we are superchlorinating and the remaing superChlor time is > 0 then we need to keep it at 100%.
281
+ // 4. If the chlorinator disabled flag is set then we need to make sure the setpoint is 0.
282
+ let cstate = state.chlorinators.getItemById(this.chlor.id, true);
283
+ let setpoint = 0;
284
+ if (this.isBodyOn()) {
285
+ if (sys.equipment.shared) {
286
+ let body = state.temps.bodies.getBodyIsOn();
287
+ setpoint = (body.id === 1) ? this.chlor.poolSetpoint : this.chlor.spaSetpoint;
288
+ }
289
+ else setpoint = this.chlor.body === 0 ? this.chlor.poolSetpoint : this.chlor.spaSetpoint;
290
+ if (this.chlor.isDosing) setpoint = 100;
291
+ }
292
+ else {
293
+ this.chlor.superChlor = cstate.superChlor = false;
294
+ this.setSuperChlor(cstate);
295
+ }
296
+ if (this.chlor.disabled === true) setpoint = 0; // Our target should be 0 because we have other things going on. For instance,
297
+ // RKS: Not sure if it needs to be smart enough to force an off message when the comms die.
298
+ //if (cstate.status === 128) setpoint = 0; // If we haven't been able to get a response from the clorinator tell is to turn itself off.
299
+ // Perhaps we will be luckier on the next poll cycle.
300
+ // Tell the chlorinator that we are to use the current output.
301
+ //[16, 2, 80, 17][0][115, 16, 3]
302
+ cstate.targetOutput = cstate.superChlor ? 100 : setpoint;
303
+ await this.sendOutputMessageAsync(cstate);
304
+ cstate.emitEquipmentChange();
305
+ } catch (err) { logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`); return Promise.reject(err); }
306
+
307
+ }
308
+ public async sendOutputMessageAsync(cstate: ChlorinatorState): Promise<void> {
309
+ if (conn.isPortEnabled(this.chlor.portId || 0)) {
310
+ let out = Outbound.create({
311
+ portId: this.chlor.portId || 0,
312
+ protocol: Protocol.Chlorinator,
313
+ dest: this.chlor.address || 80,
314
+ action: 17,
315
+ payload: [cstate.targetOutput],
316
+ retries: 3, // IntelliCenter tries 8 times to make this happen.
317
+ response: Response.create({ protocol: Protocol.Chlorinator, action: 18 }),
318
+ onAbort: () => { }
319
+ });
320
+ // #338
321
+ if (cstate.targetOutput === 16) { out.appendPayloadByte(0); }
322
+ try {
323
+ await out.sendAsync();
324
+ cstate.currentOutput = cstate.targetOutput;
325
+ this.setSuperChlor(cstate);
326
+ }
327
+ catch (err) {
328
+ cstate.currentOutput = 0;
329
+ cstate.status = 128;
330
+ }
331
+ }
332
+ else {
333
+ cstate.currentOutput = cstate.targetOutput;
334
+ this.setSuperChlor(cstate);
335
+ }
336
+
337
+ }
338
+ public async getModelAsync() {
339
+ // We only need to ask for this if we can communicate with the chlorinator. IntelliCenter
340
+ // asks for this anyway but it really is gratuitous. If the setOutput and takeControl fail
341
+ // then this will too.
342
+ // RSG - Only need to ask if the model or name isn't defined. Added checks below.
343
+ let cstate = state.chlorinators.getItemById(this.chlor.id, true);
344
+ if (cstate.status !== 128 && (typeof cstate.model === 'undefined' || cstate.model === 0 || typeof cstate.name === 'undefined' || cstate.name === '')) {
345
+ // Ask the chlorinator for its model.
346
+ //[16, 2, 80, 20][0][118, 16, 3]
347
+ if (conn.isPortEnabled(this.chlor.portId || 0)) {
348
+ let out = Outbound.create({
349
+ portId: this.chlor.portId || 0,
350
+ protocol: Protocol.Chlorinator,
351
+ dest: this.chlor.address || 80,
352
+ action: 20,
353
+ payload: [0],
354
+ retries: 3, // IntelliCenter tries 4 times to get a response.
355
+ response: Response.create({ protocol: Protocol.Chlorinator, action: 3 }),
356
+ onAbort: () => { }
357
+ });
358
+ try {
359
+ await out.sendAsync();
360
+ }
361
+ catch (err) {
362
+ logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`);
363
+ }
364
+ }
365
+ }
366
+ }
367
+ }