nodejs-poolcontroller 7.3.1 → 7.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.eslintrc.json +44 -44
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +52 -52
  3. package/CONTRIBUTING.md +74 -74
  4. package/Changelog +215 -195
  5. package/Dockerfile +17 -17
  6. package/Gruntfile.js +40 -40
  7. package/LICENSE +661 -661
  8. package/README.md +191 -186
  9. package/app.ts +2 -0
  10. package/config/Config.ts +27 -2
  11. package/config/VersionCheck.ts +33 -14
  12. package/config copy.json +299 -299
  13. package/controller/Constants.ts +88 -0
  14. package/controller/Equipment.ts +2459 -2225
  15. package/controller/Errors.ts +180 -157
  16. package/controller/Lockouts.ts +437 -0
  17. package/controller/State.ts +364 -79
  18. package/controller/boards/BoardFactory.ts +45 -45
  19. package/controller/boards/EasyTouchBoard.ts +2653 -2489
  20. package/controller/boards/IntelliCenterBoard.ts +4230 -3973
  21. package/controller/boards/IntelliComBoard.ts +63 -63
  22. package/controller/boards/IntelliTouchBoard.ts +241 -167
  23. package/controller/boards/NixieBoard.ts +1675 -1105
  24. package/controller/boards/SystemBoard.ts +4697 -3201
  25. package/controller/comms/Comms.ts +222 -10
  26. package/controller/comms/messages/Messages.ts +13 -9
  27. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -4
  28. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  29. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  30. package/controller/comms/messages/config/ConfigMessage.ts +0 -0
  31. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  32. package/controller/comms/messages/config/CustomNameMessage.ts +30 -30
  33. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  34. package/controller/comms/messages/config/ExternalMessage.ts +53 -33
  35. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  36. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  37. package/controller/comms/messages/config/HeaterMessage.ts +14 -28
  38. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  39. package/controller/comms/messages/config/OptionsMessage.ts +38 -2
  40. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  41. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  42. package/controller/comms/messages/config/ScheduleMessage.ts +347 -331
  43. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  44. package/controller/comms/messages/config/ValveMessage.ts +13 -3
  45. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
  46. package/controller/comms/messages/status/EquipmentStateMessage.ts +79 -25
  47. package/controller/comms/messages/status/HeaterStateMessage.ts +86 -53
  48. package/controller/comms/messages/status/IntelliChemStateMessage.ts +445 -386
  49. package/controller/comms/messages/status/IntelliValveStateMessage.ts +35 -35
  50. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  51. package/controller/comms/messages/status/VersionMessage.ts +0 -0
  52. package/controller/nixie/Nixie.ts +162 -160
  53. package/controller/nixie/NixieEquipment.ts +103 -103
  54. package/controller/nixie/bodies/Body.ts +120 -117
  55. package/controller/nixie/bodies/Filter.ts +135 -135
  56. package/controller/nixie/chemistry/ChemController.ts +2498 -2395
  57. package/controller/nixie/chemistry/Chlorinator.ts +314 -313
  58. package/controller/nixie/circuits/Circuit.ts +248 -210
  59. package/controller/nixie/heaters/Heater.ts +649 -441
  60. package/controller/nixie/pumps/Pump.ts +661 -599
  61. package/controller/nixie/schedules/Schedule.ts +257 -256
  62. package/controller/nixie/valves/Valve.ts +170 -170
  63. package/defaultConfig.json +286 -271
  64. package/issue_template.md +51 -51
  65. package/logger/DataLogger.ts +448 -433
  66. package/logger/Logger.ts +0 -0
  67. package/package.json +56 -54
  68. package/tsconfig.json +25 -25
  69. package/web/Server.ts +522 -31
  70. package/web/bindings/influxDB.json +1022 -894
  71. package/web/bindings/mqtt.json +654 -543
  72. package/web/bindings/mqttAlt.json +684 -574
  73. package/web/bindings/rulesManager.json +54 -54
  74. package/web/bindings/smartThings-Hubitat.json +31 -31
  75. package/web/bindings/valveRelays.json +20 -20
  76. package/web/bindings/vera.json +25 -25
  77. package/web/interfaces/baseInterface.ts +136 -136
  78. package/web/interfaces/httpInterface.ts +124 -122
  79. package/web/interfaces/influxInterface.ts +245 -240
  80. package/web/interfaces/mqttInterface.ts +475 -464
  81. package/web/services/config/Config.ts +181 -152
  82. package/web/services/config/ConfigSocket.ts +0 -0
  83. package/web/services/state/State.ts +118 -7
  84. package/web/services/state/StateSocket.ts +18 -1
  85. package/web/services/utilities/Utilities.ts +42 -42
@@ -1,313 +1,314 @@
1
- import { InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError } from '../../Errors';
2
- import { utils, Timestamp } 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, 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
-
13
- export class NixieChlorinatorCollection extends NixieEquipmentCollection<NixieChlorinator> {
14
- public async deleteChlorinatorAsync(id: number) {
15
- try {
16
- // Since we don't have hash tables per se in TS go through all the entries and remove every one that
17
- // matches the id. This will ensure cleanup of a dirty array.
18
- for (let i = this.length - 1; i >= 0; i--) {
19
- let c = this[i];
20
- if (c.id === id) {
21
- await c.closeAsync();
22
- this.splice(i, 1);
23
- }
24
- }
25
- } catch (err) { logger.error(`NCP: Error removing chlorinator`); }
26
-
27
- }
28
- public async setChlorinatorAsync(chlor: Chlorinator, data: any) {
29
- // By the time we get here we know that we are in control and this is a REMChem.
30
- try {
31
- let c: NixieChlorinator = this.find(elem => elem.id === chlor.id) as NixieChlorinator;
32
- if (typeof c === 'undefined') {
33
- chlor.master = 1;
34
- c = new NixieChlorinator(this.controlPanel, chlor);
35
- this.push(c);
36
- await c.setChlorinatorAsync(data);
37
- logger.info(`A Chlorinator was not found for id #${chlor.id} starting Nixie Chlorinator`);
38
- await c.initAsync();
39
- }
40
- else {
41
- await c.setChlorinatorAsync(data);
42
- }
43
- }
44
- catch (err) { logger.error(`setChlorinatorAsync: ${err.message}`); return Promise.reject(err); }
45
- }
46
- public async initAsync(chlorinators: ChlorinatorCollection) {
47
- try {
48
- for (let i = 0; i < chlorinators.length; i++) {
49
- let cc = chlorinators.getItemByIndex(i);
50
- if (cc.master === 1) {
51
- if (typeof this.find(elem => elem.id === cc.id) === 'undefined') {
52
- logger.info(`Initializing Nixie chlorinator ${cc.name}`);
53
- let ncc = new NixieChlorinator(this.controlPanel, cc);
54
- this.push(ncc);
55
- await ncc.initAsync();
56
- }
57
- }
58
- }
59
- }
60
- catch (err) { logger.error(`initAsync: ${err.message}`); return Promise.reject(err); }
61
- }
62
- public async closeAsync() {
63
- try {
64
- for (let i = this.length - 1; i >= 0; i--) {
65
- try {
66
- await this[i].closeAsync();
67
- this.splice(i, 1);
68
- } catch (err) { logger.error(`Error stopping Nixie Chlorinator ${err}`); }
69
- }
70
- } catch (err) { } // Don't bail if we have an error
71
- }
72
- }
73
- export class NixieChlorinator extends NixieEquipment {
74
- public pollingInterval: number = 3000;
75
- private _pollTimer: NodeJS.Timeout = null;
76
- private superChlorinating: boolean = false;
77
- private superChlorStart: number = 0;
78
- private chlorinating: boolean = false;
79
- public chlor: Chlorinator;
80
- public bodyOnTime: number;
81
- protected _suspendPolling: number = 0;
82
- public closing = false;
83
- constructor(ncp: INixieControlPanel, chlor: Chlorinator) {
84
- super(ncp);
85
- this.chlor = chlor;
86
- }
87
- public get id(): number { return typeof this.chlor !== 'undefined' ? this.chlor.id : -1; }
88
- public get suspendPolling(): boolean { return this._suspendPolling > 0; }
89
- public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
90
- public async setChlorinatorAsync(data: any) {
91
- try {
92
- let chlor = this.chlor;
93
- if (chlor.type === sys.board.valueMaps.chlorinatorType.getValue('intellichlor')) {
94
-
95
- }
96
- let poolSetpoint = typeof data.poolSetpoint !== 'undefined' ? parseInt(data.poolSetpoint, 10) : chlor.poolSetpoint;
97
- let spaSetpoint = typeof data.spaSetpoint !== 'undefined' ? parseInt(data.spaSetpoint, 10) : chlor.spaSetpoint;
98
- let body = sys.board.bodies.mapBodyAssociation(typeof data.body === 'undefined' ? chlor.body : data.body);
99
- let superChlor = typeof data.superChlor !== 'undefined' ? utils.makeBool(data.superChlor) : chlor.superChlor;
100
- let chlorType = typeof data.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(data.type) : chlor.type || 0;
101
- let superChlorHours = typeof data.superChlorHours !== 'undefined' ? parseInt(data.superChlorHours, 10) : chlor.superChlorHours;
102
- let disabled = typeof data.disabled !== 'undefined' ? utils.makeBool(data.disabled) : chlor.disabled;
103
- let isDosing = typeof data.isDosing !== 'undefined' ? utils.makeBool(data.isDosing) : chlor.isDosing;
104
- let model = typeof data.model !== 'undefined' ? data.model : chlor.model;
105
- if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid body assignment`, 'chlorinator', data.body || chlor.body));
106
- if (isNaN(poolSetpoint)) poolSetpoint = 0;
107
- if (isNaN(spaSetpoint)) spaSetpoint = 0;
108
- chlor.ignoreSaltReading = (typeof data.ignoreSaltReading !== 'undefined') ? utils.makeBool(data.ignoreSaltReading) : utils.makeBool(chlor.ignoreSaltReading);
109
- // Do a final validation pass so we dont send this off in a mess.
110
- let schlor = state.chlorinators.getItemById(chlor.id, true);
111
- schlor.poolSetpoint = chlor.poolSetpoint = poolSetpoint;
112
- schlor.spaSetpoint = chlor.spaSetpoint = spaSetpoint;
113
- schlor.superChlor = chlor.superChlor = superChlor;
114
- schlor.superChlorHours = chlor.superChlorHours = superChlorHours;
115
- schlor.type = chlor.type = chlorType;
116
- chlor.body = body;
117
- chlor.model = model;
118
- schlor.body = chlor.body;
119
- chlor.disabled = disabled;
120
- chlor.isDosing = isDosing;
121
- schlor.name = chlor.name = data.name || chlor.name || `Chlorinator ${chlor.id}`;
122
- schlor.isActive = chlor.isActive = true;
123
- }
124
- catch (err) { logger.error(`setChlorinatorAsync: ${err.message}`); return Promise.reject(err); }
125
- }
126
- public async closeAsync() {
127
- try {
128
- this.closing = true; // This will tell the polling cycle to stop what it is doing and don't restart.
129
- if (this._pollTimer) {
130
- clearTimeout(this._pollTimer);
131
- this._pollTimer = undefined;
132
- }
133
- // Let the Nixie equipment do its thing if it needs to.
134
- await super.closeAsync();
135
- }
136
- catch (err) { logger.error(`Chlorinator closeAsync: ${err.message}`); return Promise.reject(err); }
137
- }
138
- public async initAsync() {
139
- try {
140
- // Start our polling but only after we clean up any other polling going on.
141
- if (this._pollTimer) {
142
- clearTimeout(this._pollTimer);
143
- this._pollTimer = undefined;
144
- }
145
- this.closing = false;
146
- this._suspendPolling = 0;
147
- // 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.
148
- this.pollEquipment();
149
- } catch (err) { logger.error(`Error initializing ${this.chlor.name} : ${err.message}`); }
150
- }
151
- public isBodyOn() {
152
- let isOn = sys.board.bodies.isBodyOn(this.chlor.body);
153
- return isOn;
154
- }
155
- public async pollEquipment() {
156
- let self = this;
157
- try {
158
- if (this._pollTimer) {
159
- clearTimeout(this._pollTimer);
160
- this._pollTimer = undefined;
161
- }
162
- if (!this.suspendPolling) {
163
- logger.debug(`Begin sending chlorinator messages ${this.chlor.name}`);
164
- try {
165
- this.suspendPolling = true;
166
- if (!this.closing) await this.takeControl();
167
- if (!this.closing) await utils.sleep(300);
168
- if (!this.closing) await this.setOutput();
169
- if (!this.closing) await utils.sleep(300);
170
- if (!this.closing) await this.getModel();
171
- } catch (err) {
172
- // We only display an error here if the body is on. The chlorinator should be powered down when it is not.
173
- if (this.isBodyOn()) logger.error(`Chlorinator ${this.chlor.name} comms failure: ${err.message}`);
174
- }
175
- finally { this.suspendPolling = false; }
176
- }
177
- } catch (err) {
178
- // Comms failure will be handeled by the message processor.
179
- logger.error(`Chlorinator ${this.chlor.name} comms failure: ${err.message}`);
180
- }
181
- finally { if(!this.closing) this._pollTimer = setTimeout(async () => { await self.pollEquipment(); }, this.pollingInterval); }
182
- }
183
- public async takeControl(): Promise<boolean> {
184
- try {
185
- let cstate = state.chlorinators.getItemById(this.chlor.id, true);
186
- // The sequence is as follows.
187
- // 0 = Disabled control panel by taking control of it.
188
- // 17 = Set the current setpoint
189
- // 20 = Request the status
190
- // Disable the control panel by sending an action 0 the chlorinator should respond with an action 1.
191
- //[16, 2, 80, 0][0][98, 16, 3]
192
- let success = await new Promise<boolean>((resolve, reject) => {
193
- let out = Outbound.create({
194
- protocol: Protocol.Chlorinator,
195
- dest: this.chlor.id,
196
- action: 0,
197
- payload: [0],
198
- retries: 3, // IntelliCenter tries 4 times to get a response.
199
- response: Response.create({ protocol: Protocol.Chlorinator, action: 1 }),
200
- onAbort: () => {},
201
- onComplete: (err) => {
202
- if (err) {
203
- // This flag is cleared in ChlorinatorStateMessage
204
- cstate.status = 128;
205
- resolve(false);
206
- }
207
- else {
208
- // If this is successful the action 1 message will have been
209
- // digested by ChlorinatorStateMessage and the lastComm will have been set clearing the
210
- // communication lost flag.
211
- resolve(true);
212
- }
213
- }
214
- });
215
- conn.queueSendMessage(out);
216
- });
217
- return success;
218
- } catch (err) { logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`); }
219
- }
220
- public async setOutput() {
221
- try {
222
- // A couple of things need to be in place before setting the output.
223
- // 1. The chlorinator will have to have responded to the takeControl message.
224
- // 2. If the body is not on then we need to send it a 0 output. This is just in case the
225
- // chlorinator is not wired into the filter relay. The current output should be 0 if no body is on.
226
- // 3. If we are superchlorinating and the remaing superChlor time is > 0 then we need to keep it at 100%.
227
- // 4. If the chlorinator disabled flag is set then we need to make sure the setpoint is 0.
228
- let cstate = state.chlorinators.getItemById(this.chlor.id, true);
229
- let body = state.temps.bodies.getBodyIsOn();
230
- let setpoint = 0;
231
- if (typeof body !== 'undefined') {
232
- setpoint = (body.id === 1) ? this.chlor.poolSetpoint : this.chlor.spaSetpoint;
233
- if (this.chlor.superChlor === true) setpoint = 100;
234
- if (this.chlor.disabled === true) setpoint = 0; // Our target should be 0 because we have other things going on. For instance,
235
- // we may be dosing acid which will cause the disabled flag to be true.
236
- }
237
- // RKS: Not sure if it needs to be smart enough to force an off message when the comms die.
238
- //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.
239
- // Perhaps we will be luckier on the next poll cycle.
240
- // Tell the chlorinator that we are to use the current output.
241
- //[16, 2, 80, 17][0][115, 16, 3]
242
- cstate.targetOutput = setpoint;
243
- let success = await new Promise<boolean>((resolve, reject) => {
244
- let out = Outbound.create({
245
- protocol: Protocol.Chlorinator,
246
- dest: this.chlor.id,
247
- action: 17,
248
- payload: [setpoint],
249
- retries: 7, // IntelliCenter tries 8 times to make this happen.
250
- response: Response.create({ protocol: Protocol.Chlorinator, action: 18 }),
251
- onAbort: () => {},
252
- onComplete: (err) => {
253
- if (err) {
254
- this.chlorinating = false;
255
- cstate.currentOutput = 0;
256
- cstate.status = 128;
257
- resolve(false);
258
- }
259
- else {
260
- // The action:17 message originated from us so we will not see it in the
261
- // ChlorinatorStateMessage module.
262
- cstate.currentOutput = setpoint;
263
- this.chlorinating = true;
264
- if (!this.superChlorinating && cstate.superChlor) {
265
- cstate.superChlorRemaining = cstate.superChlorHours * 3600;
266
- this.superChlorStart = Math.floor(new Date().getTime() / 1000) * 1000;
267
- }
268
- else if (cstate.superChlor) {
269
- cstate.superChlorRemaining = cstate.superChlorHours * 3600 - ((Math.floor(new Date().getTime() / 1000) * 1000) - this.superChlorStart);
270
- }
271
- else if (!cstate.superChlor)
272
- cstate.superChlorRemaining = 0;
273
- resolve(true);
274
- }
275
- }
276
- });
277
- conn.queueSendMessage(out);
278
- });
279
-
280
- } catch (err) { logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`); return Promise.reject(err);}
281
-
282
- }
283
- public async getModel() {
284
- try {
285
- // We only need to ask for this if we can communicate with the chlorinator. IntelliCenter
286
- // asks for this anyway but it really is gratuitous. If the setOutput and takeControl fail
287
- // then this will too.
288
- let cstate = state.chlorinators.getItemById(this.chlor.id, true);
289
- if (cstate.status !== 128) {
290
- // Ask the chlorinator for its model.
291
- //[16, 2, 80, 20][0][118, 16, 3]
292
- let success = await new Promise<boolean>((resolve, reject) => {
293
- let out = Outbound.create({
294
- protocol: Protocol.Chlorinator,
295
- dest: this.chlor.id,
296
- action: 20,
297
- payload: [0],
298
- retries: 3, // IntelliCenter tries 4 times to get a response.
299
- response: Response.create({ protocol: Protocol.Chlorinator, action: 3 }),
300
- onAbort: () => { },
301
- onComplete: (err) => {
302
- if (err) resolve(false);
303
- else resolve(true);
304
- }
305
- });
306
- conn.queueSendMessage(out);
307
- });
308
- }
309
- } catch (err) { logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`); return Promise.reject(err);}
310
-
311
- }
312
-
313
- }
1
+ import { InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError } from '../../Errors';
2
+ import { utils, Timestamp } 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, 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
+
14
+ export class NixieChlorinatorCollection extends NixieEquipmentCollection<NixieChlorinator> {
15
+ public async deleteChlorinatorAsync(id: number) {
16
+ try {
17
+ // Since we don't have hash tables per se in TS go through all the entries and remove every one that
18
+ // matches the id. This will ensure cleanup of a dirty array.
19
+ for (let i = this.length - 1; i >= 0; i--) {
20
+ let c = this[i];
21
+ if (c.id === id) {
22
+ await ncp.chemControllers.deleteChlorAsync(c as NixieChlorinator);
23
+ await c.closeAsync();
24
+ this.splice(i, 1);
25
+ }
26
+ }
27
+ } catch (err) { logger.error(`NCP: Error removing chlorinator`); }
28
+
29
+ }
30
+ public async setChlorinatorAsync(chlor: Chlorinator, data: any) {
31
+ // By the time we get here we know that we are in control and this is a REMChem.
32
+ try {
33
+ let c: NixieChlorinator = this.find(elem => elem.id === chlor.id) as NixieChlorinator;
34
+ if (typeof c === 'undefined') {
35
+ chlor.master = 1;
36
+ c = new NixieChlorinator(this.controlPanel, chlor);
37
+ this.push(c);
38
+ await c.setChlorinatorAsync(data);
39
+ logger.info(`A Chlorinator was not found for id #${chlor.id} starting Nixie Chlorinator`);
40
+ await c.initAsync();
41
+ }
42
+ else {
43
+ await c.setChlorinatorAsync(data);
44
+ }
45
+ }
46
+ catch (err) { logger.error(`setChlorinatorAsync: ${err.message}`); return Promise.reject(err); }
47
+ }
48
+ public async initAsync(chlorinators: ChlorinatorCollection) {
49
+ try {
50
+ for (let i = 0; i < chlorinators.length; i++) {
51
+ let cc = chlorinators.getItemByIndex(i);
52
+ if (cc.master === 1) {
53
+ if (typeof this.find(elem => elem.id === cc.id) === 'undefined') {
54
+ logger.info(`Initializing Nixie chlorinator ${cc.name}`);
55
+ let ncc = new NixieChlorinator(this.controlPanel, cc);
56
+ this.push(ncc);
57
+ await ncc.initAsync();
58
+ }
59
+ }
60
+ }
61
+ }
62
+ catch (err) { logger.error(`initAsync: ${err.message}`); return Promise.reject(err); }
63
+ }
64
+ public async closeAsync() {
65
+ try {
66
+ for (let i = this.length - 1; i >= 0; i--) {
67
+ try {
68
+ await this[i].closeAsync();
69
+ this.splice(i, 1);
70
+ } catch (err) { logger.error(`Error stopping Nixie Chlorinator ${err}`); }
71
+ }
72
+ } catch (err) { } // Don't bail if we have an error
73
+ }
74
+ }
75
+ export class NixieChlorinator extends NixieEquipment {
76
+ public pollingInterval: number = 3000;
77
+ private _pollTimer: NodeJS.Timeout = null;
78
+ private superChlorinating: boolean = false;
79
+ private superChlorStart: number = 0;
80
+ public chlor: Chlorinator;
81
+ public bodyOnTime: number;
82
+ protected _suspendPolling: number = 0;
83
+ public closing = false;
84
+ constructor(ncp: INixieControlPanel, chlor: Chlorinator) {
85
+ super(ncp);
86
+ this.chlor = chlor;
87
+ }
88
+ public get id(): number { return typeof this.chlor !== 'undefined' ? this.chlor.id : -1; }
89
+ public get suspendPolling(): boolean { return this._suspendPolling > 0; }
90
+ public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
91
+ public async setChlorinatorAsync(data: any) {
92
+ try {
93
+ let chlor = this.chlor;
94
+ if (chlor.type === sys.board.valueMaps.chlorinatorType.getValue('intellichlor')) {
95
+
96
+ }
97
+ let poolSetpoint = typeof data.poolSetpoint !== 'undefined' ? parseInt(data.poolSetpoint, 10) : chlor.poolSetpoint;
98
+ let spaSetpoint = typeof data.spaSetpoint !== 'undefined' ? parseInt(data.spaSetpoint, 10) : chlor.spaSetpoint;
99
+ let body = sys.board.bodies.mapBodyAssociation(typeof data.body === 'undefined' ? chlor.body : data.body);
100
+ let superChlor = typeof data.superChlor !== 'undefined' ? utils.makeBool(data.superChlor) : chlor.superChlor;
101
+ let chlorType = typeof data.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(data.type) : chlor.type || 0;
102
+ let superChlorHours = typeof data.superChlorHours !== 'undefined' ? parseInt(data.superChlorHours, 10) : chlor.superChlorHours;
103
+ let disabled = typeof data.disabled !== 'undefined' ? utils.makeBool(data.disabled) : chlor.disabled;
104
+ let isDosing = typeof data.isDosing !== 'undefined' ? utils.makeBool(data.isDosing) : chlor.isDosing;
105
+ let model = typeof data.model !== 'undefined' ? data.model : chlor.model || 0;
106
+ if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid body assignment`, 'chlorinator', data.body || chlor.body));
107
+ if (isNaN(poolSetpoint)) poolSetpoint = 0;
108
+ if (isNaN(spaSetpoint)) spaSetpoint = 0;
109
+ chlor.ignoreSaltReading = (typeof data.ignoreSaltReading !== 'undefined') ? utils.makeBool(data.ignoreSaltReading) : utils.makeBool(chlor.ignoreSaltReading);
110
+ // Do a final validation pass so we dont send this off in a mess.
111
+ let schlor = state.chlorinators.getItemById(chlor.id, true);
112
+ schlor.poolSetpoint = chlor.poolSetpoint = poolSetpoint;
113
+ schlor.spaSetpoint = chlor.spaSetpoint = spaSetpoint;
114
+ schlor.superChlor = chlor.superChlor = superChlor;
115
+ schlor.superChlorHours = chlor.superChlorHours = superChlorHours;
116
+ schlor.type = chlor.type = chlorType;
117
+ chlor.model = model;
118
+ schlor.body = chlor.body = body.val;
119
+ chlor.disabled = disabled;
120
+ chlor.isDosing = isDosing;
121
+ schlor.name = chlor.name = data.name || chlor.name || `Chlorinator ${chlor.id}`;
122
+ schlor.isActive = chlor.isActive = true;
123
+ }
124
+ catch (err) { logger.error(`setChlorinatorAsync: ${err.message}`); return Promise.reject(err); }
125
+ }
126
+ public async closeAsync() {
127
+ try {
128
+ this.closing = true; // This will tell the polling cycle to stop what it is doing and don't restart.
129
+ if (this._pollTimer) {
130
+ clearTimeout(this._pollTimer);
131
+ this._pollTimer = undefined;
132
+ }
133
+ // Let the Nixie equipment do its thing if it needs to.
134
+ await super.closeAsync();
135
+ }
136
+ catch (err) { logger.error(`Chlorinator closeAsync: ${err.message}`); return Promise.reject(err); }
137
+ }
138
+ public async initAsync() {
139
+ try {
140
+ // Start our polling but only after we clean up any other polling going on.
141
+ if (this._pollTimer) {
142
+ clearTimeout(this._pollTimer);
143
+ this._pollTimer = undefined;
144
+ }
145
+ this.closing = false;
146
+ this._suspendPolling = 0;
147
+ // 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.
148
+ this.pollEquipment();
149
+ } catch (err) { logger.error(`Error initializing ${this.chlor.name} : ${err.message}`); }
150
+ }
151
+ public isBodyOn() {
152
+ let isOn = sys.board.bodies.isBodyOn(this.chlor.body);
153
+ return isOn;
154
+ }
155
+ public async pollEquipment() {
156
+ let self = this;
157
+ try {
158
+ if (this._pollTimer) {
159
+ clearTimeout(this._pollTimer);
160
+ this._pollTimer = undefined;
161
+ }
162
+ if (!this.suspendPolling) {
163
+ logger.debug(`Begin sending chlorinator messages ${this.chlor.name}`);
164
+ try {
165
+ this.suspendPolling = true;
166
+ if (!this.closing) await this.takeControl();
167
+ if (!this.closing) await utils.sleep(300);
168
+ if (!this.closing) await this.setOutput();
169
+ if (!this.closing) await utils.sleep(300);
170
+ if (!this.closing) await this.getModel();
171
+ } catch (err) {
172
+ // We only display an error here if the body is on. The chlorinator should be powered down when it is not.
173
+ if (this.isBodyOn()) logger.error(`Chlorinator ${this.chlor.name} comms failure: ${err.message}`);
174
+ }
175
+ finally { this.suspendPolling = false; }
176
+ }
177
+ } catch (err) {
178
+ // Comms failure will be handeled by the message processor.
179
+ logger.error(`Chlorinator ${this.chlor.name} comms failure: ${err.message}`);
180
+ }
181
+ finally { if(!this.closing) this._pollTimer = setTimeout(() => {self.pollEquipment();}, this.pollingInterval); }
182
+ }
183
+ public async takeControl(): Promise<boolean> {
184
+ try {
185
+ let cstate = state.chlorinators.getItemById(this.chlor.id, true);
186
+ // The sequence is as follows.
187
+ // 0 = Disabled control panel by taking control of it.
188
+ // 17 = Set the current setpoint
189
+ // 20 = Request the status
190
+ // Disable the control panel by sending an action 0 the chlorinator should respond with an action 1.
191
+ //[16, 2, 80, 0][0][98, 16, 3]
192
+ let success = await new Promise<boolean>((resolve, reject) => {
193
+ let out = Outbound.create({
194
+ protocol: Protocol.Chlorinator,
195
+ dest: this.chlor.id,
196
+ action: 0,
197
+ payload: [0],
198
+ retries: 3, // IntelliCenter tries 4 times to get a response.
199
+ response: Response.create({ protocol: Protocol.Chlorinator, action: 1 }),
200
+ onAbort: () => {},
201
+ onComplete: (err) => {
202
+ if (err) {
203
+ // This flag is cleared in ChlorinatorStateMessage
204
+ cstate.status = 128;
205
+ resolve(false);
206
+ }
207
+ else {
208
+ // If this is successful the action 1 message will have been
209
+ // digested by ChlorinatorStateMessage and the lastComm will have been set clearing the
210
+ // communication lost flag.
211
+ resolve(true);
212
+ }
213
+ }
214
+ });
215
+ conn.queueSendMessage(out);
216
+ });
217
+ return success;
218
+ } catch (err) { logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`); }
219
+ }
220
+ public async setOutput() {
221
+ try {
222
+ // A couple of things need to be in place before setting the output.
223
+ // 1. The chlorinator will have to have responded to the takeControl message.
224
+ // 2. If the body is not on then we need to send it a 0 output. This is just in case the
225
+ // chlorinator is not wired into the filter relay. The current output should be 0 if no body is on.
226
+ // 3. If we are superchlorinating and the remaing superChlor time is > 0 then we need to keep it at 100%.
227
+ // 4. If the chlorinator disabled flag is set then we need to make sure the setpoint is 0.
228
+ let cstate = state.chlorinators.getItemById(this.chlor.id, true);
229
+ let body = state.temps.bodies.getBodyIsOn();
230
+ let setpoint = 0;
231
+ if (typeof body !== 'undefined') {
232
+ setpoint = (body.id === 1) ? this.chlor.poolSetpoint : this.chlor.spaSetpoint;
233
+ if (this.chlor.superChlor === true || this.chlor.isDosing) setpoint = 100;
234
+ if (this.chlor.disabled === true) setpoint = 0; // Our target should be 0 because we have other things going on. For instance,
235
+ // we may be dosing acid which will cause the disabled flag to be true.
236
+ }
237
+ // RKS: Not sure if it needs to be smart enough to force an off message when the comms die.
238
+ //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.
239
+ // Perhaps we will be luckier on the next poll cycle.
240
+ // Tell the chlorinator that we are to use the current output.
241
+ //[16, 2, 80, 17][0][115, 16, 3]
242
+ cstate.targetOutput = setpoint;
243
+ let success = await new Promise<boolean>((resolve, reject) => {
244
+ let out = Outbound.create({
245
+ protocol: Protocol.Chlorinator,
246
+ dest: this.chlor.id,
247
+ action: 17,
248
+ payload: [setpoint],
249
+ retries: 7, // IntelliCenter tries 8 times to make this happen.
250
+ response: Response.create({ protocol: Protocol.Chlorinator, action: 18 }),
251
+ onAbort: () => {},
252
+ onComplete: (err) => {
253
+ if (err) {
254
+ cstate.currentOutput = 0;
255
+ cstate.status = 128;
256
+ resolve(false);
257
+ }
258
+ else {
259
+ // The action:17 message originated from us so we will not see it in the
260
+ // ChlorinatorStateMessage module.
261
+ cstate.currentOutput = setpoint;
262
+ if (!this.superChlorinating && cstate.superChlor) {
263
+ cstate.superChlorRemaining = cstate.superChlorHours * 3600;
264
+ this.superChlorStart = Math.floor(new Date().getTime() / 1000) * 1000;
265
+ }
266
+ else if (cstate.superChlor) {
267
+ cstate.superChlorRemaining = cstate.superChlorHours * 3600 - ((Math.floor(new Date().getTime() / 1000) * 1000) - this.superChlorStart);
268
+ }
269
+ else if (!cstate.superChlor)
270
+ cstate.superChlorRemaining = 0;
271
+ resolve(true);
272
+ }
273
+ }
274
+ });
275
+ // #338
276
+ if (setpoint === 16) { out.appendPayloadByte(0); }
277
+ conn.queueSendMessage(out);
278
+ });
279
+
280
+ } catch (err) { logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`); return Promise.reject(err);}
281
+
282
+ }
283
+ public async getModel() {
284
+ try {
285
+ // We only need to ask for this if we can communicate with the chlorinator. IntelliCenter
286
+ // asks for this anyway but it really is gratuitous. If the setOutput and takeControl fail
287
+ // then this will too.
288
+ let cstate = state.chlorinators.getItemById(this.chlor.id, true);
289
+ if (cstate.status !== 128) {
290
+ // Ask the chlorinator for its model.
291
+ //[16, 2, 80, 20][0][118, 16, 3]
292
+ let success = await new Promise<boolean>((resolve, reject) => {
293
+ let out = Outbound.create({
294
+ protocol: Protocol.Chlorinator,
295
+ dest: this.chlor.id,
296
+ action: 20,
297
+ payload: [0],
298
+ retries: 3, // IntelliCenter tries 4 times to get a response.
299
+ response: Response.create({ protocol: Protocol.Chlorinator, action: 3 }),
300
+ onAbort: () => { },
301
+ onComplete: (err) => {
302
+ if (err) resolve(false);
303
+ else resolve(true);
304
+ }
305
+ });
306
+ conn.queueSendMessage(out);
307
+ });
308
+ }
309
+ else return Promise.resolve(false);
310
+ } catch (err) { logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`); return Promise.reject(err);}
311
+
312
+ }
313
+
314
+ }