nodejs-poolcontroller 7.6.1 → 8.0.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 (102) hide show
  1. package/.eslintrc.json +36 -45
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -0
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -0
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -0
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  6. package/CONTRIBUTING.md +74 -74
  7. package/Changelog +242 -215
  8. package/Dockerfile +17 -17
  9. package/Gruntfile.js +40 -40
  10. package/LICENSE +661 -661
  11. package/README.md +195 -191
  12. package/anslq25/MessagesMock.ts +218 -0
  13. package/anslq25/boards/MockBoardFactory.ts +50 -0
  14. package/anslq25/boards/MockEasyTouchBoard.ts +696 -0
  15. package/anslq25/boards/MockSystemBoard.ts +217 -0
  16. package/anslq25/chemistry/MockChlorinator.ts +75 -0
  17. package/anslq25/pumps/MockPump.ts +84 -0
  18. package/app.ts +10 -14
  19. package/config/Config.ts +26 -8
  20. package/config/VersionCheck.ts +8 -4
  21. package/controller/Constants.ts +59 -25
  22. package/controller/Equipment.ts +2667 -2459
  23. package/controller/Errors.ts +181 -180
  24. package/controller/Lockouts.ts +534 -436
  25. package/controller/State.ts +596 -77
  26. package/controller/boards/AquaLinkBoard.ts +1003 -0
  27. package/controller/boards/BoardFactory.ts +53 -45
  28. package/controller/boards/EasyTouchBoard.ts +3079 -2653
  29. package/controller/boards/IntelliCenterBoard.ts +3821 -4230
  30. package/controller/boards/IntelliComBoard.ts +69 -63
  31. package/controller/boards/IntelliTouchBoard.ts +384 -241
  32. package/controller/boards/NixieBoard.ts +1871 -1675
  33. package/controller/boards/SunTouchBoard.ts +393 -0
  34. package/controller/boards/SystemBoard.ts +5244 -4697
  35. package/controller/comms/Comms.ts +905 -541
  36. package/controller/comms/ScreenLogic.ts +1663 -0
  37. package/controller/comms/messages/Messages.ts +382 -54
  38. package/controller/comms/messages/config/ChlorinatorMessage.ts +8 -4
  39. package/controller/comms/messages/config/CircuitGroupMessage.ts +5 -2
  40. package/controller/comms/messages/config/CircuitMessage.ts +82 -13
  41. package/controller/comms/messages/config/ConfigMessage.ts +3 -1
  42. package/controller/comms/messages/config/CoverMessage.ts +2 -1
  43. package/controller/comms/messages/config/CustomNameMessage.ts +31 -30
  44. package/controller/comms/messages/config/EquipmentMessage.ts +5 -1
  45. package/controller/comms/messages/config/ExternalMessage.ts +33 -3
  46. package/controller/comms/messages/config/FeatureMessage.ts +2 -1
  47. package/controller/comms/messages/config/GeneralMessage.ts +2 -1
  48. package/controller/comms/messages/config/HeaterMessage.ts +145 -11
  49. package/controller/comms/messages/config/IntellichemMessage.ts +2 -1
  50. package/controller/comms/messages/config/OptionsMessage.ts +16 -27
  51. package/controller/comms/messages/config/PumpMessage.ts +62 -47
  52. package/controller/comms/messages/config/RemoteMessage.ts +80 -13
  53. package/controller/comms/messages/config/ScheduleMessage.ts +390 -347
  54. package/controller/comms/messages/config/SecurityMessage.ts +2 -1
  55. package/controller/comms/messages/config/ValveMessage.ts +44 -27
  56. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +44 -91
  57. package/controller/comms/messages/status/EquipmentStateMessage.ts +139 -30
  58. package/controller/comms/messages/status/HeaterStateMessage.ts +135 -86
  59. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -445
  60. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -35
  61. package/controller/comms/messages/status/PumpStateMessage.ts +92 -2
  62. package/controller/comms/messages/status/VersionMessage.ts +2 -1
  63. package/controller/nixie/Nixie.ts +173 -162
  64. package/controller/nixie/NixieEquipment.ts +104 -103
  65. package/controller/nixie/bodies/Body.ts +120 -120
  66. package/controller/nixie/bodies/Filter.ts +135 -135
  67. package/controller/nixie/chemistry/ChemController.ts +2682 -2498
  68. package/controller/nixie/chemistry/ChemDoser.ts +806 -0
  69. package/controller/nixie/chemistry/Chlorinator.ts +367 -314
  70. package/controller/nixie/circuits/Circuit.ts +402 -248
  71. package/controller/nixie/heaters/Heater.ts +815 -649
  72. package/controller/nixie/pumps/Pump.ts +934 -661
  73. package/controller/nixie/schedules/Schedule.ts +319 -257
  74. package/controller/nixie/valves/Valve.ts +170 -170
  75. package/defaultConfig.json +346 -286
  76. package/logger/DataLogger.ts +448 -448
  77. package/logger/Logger.ts +38 -9
  78. package/package.json +60 -56
  79. package/tsconfig.json +25 -25
  80. package/web/Server.ts +275 -117
  81. package/web/bindings/aqualinkD.json +560 -0
  82. package/web/bindings/homeassistant.json +437 -0
  83. package/web/bindings/influxDB.json +1066 -1021
  84. package/web/bindings/mqtt.json +721 -654
  85. package/web/bindings/mqttAlt.json +746 -684
  86. package/web/bindings/rulesManager.json +54 -54
  87. package/web/bindings/smartThings-Hubitat.json +31 -31
  88. package/web/bindings/valveRelays.json +20 -20
  89. package/web/bindings/vera.json +25 -25
  90. package/web/interfaces/baseInterface.ts +188 -136
  91. package/web/interfaces/httpInterface.ts +148 -124
  92. package/web/interfaces/influxInterface.ts +283 -245
  93. package/web/interfaces/mqttInterface.ts +695 -475
  94. package/web/interfaces/ruleInterface.ts +87 -0
  95. package/web/services/config/Config.ts +177 -49
  96. package/web/services/config/ConfigSocket.ts +2 -1
  97. package/web/services/state/State.ts +154 -3
  98. package/web/services/state/StateSocket.ts +69 -18
  99. package/web/services/utilities/Utilities.ts +232 -42
  100. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
  101. package/config copy.json +0 -300
  102. package/issue_template.md +0 -52
@@ -1,314 +1,367 @@
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
- }
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
+ }