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
@@ -0,0 +1,806 @@
1
+ import { clearTimeout, setTimeout } from 'timers';
2
+ import { conn } from '../../../controller/comms/Comms';
3
+ import { Outbound, Protocol, Response } from '../../../controller/comms/messages/Messages';
4
+ import { ChemDoser, ChemDoserCollection, ChemFlowSensor, ChemicalPump, ChemicalTank, sys } from "../../../controller/Equipment";
5
+ import { logger } from '../../../logger/Logger';
6
+ import { InterfaceServerResponse, webApp } from "../../../web/Server";
7
+ import { Timestamp, utils } from '../../Constants';
8
+ import { EquipmentNotFoundError, EquipmentTimeoutError, InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError } from '../../Errors';
9
+ import { ChemDoserState, ChemicalChlorState, ChemicalDoseState, ChemicalPumpState, ChemicalState, ChemicalTankState, ChlorinatorState, state } from "../../State";
10
+ import { ncp } from '../Nixie';
11
+ import { INixieControlPanel, NixieChildEquipment, NixieEquipment, NixieEquipmentCollection } from "../NixieEquipment";
12
+ import { INixieChemController, NixieChemFlowSensor, NixieChemPump, NixieChemTank, NixieChemMix, INixieChemical } from "./ChemController";
13
+ export class NixieChemDoserCollection extends NixieEquipmentCollection<NixieChemDoserBase> {
14
+ public async manualDoseAsync(id: number, data: any) {
15
+ try {
16
+ let c: NixieChemDoser = this.find(elem => elem.id === id) as NixieChemDoser;
17
+ if (typeof c === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Nixie could not find a chem doser at id ${id}`, id, 'chemDoser'));
18
+ await c.manualDoseAsync(data);
19
+ } catch (err) { logger.error(`manualDoseAysnc: ${err.message}`); return Promise.reject(err); }
20
+ }
21
+ public async calibrateDoseAsync(id: number, data: any) {
22
+ try {
23
+ let c: NixieChemDoser = this.find(elem => elem.id === id) as NixieChemDoser;
24
+ if (typeof c === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Nixie could not find a chem doser at id ${id}`, id, 'chemDoser'));
25
+ await c.calibrateDoseAsync(data);
26
+ } catch (err) { logger.error(`calibrateDoseAysnc: ${err.message}`); return Promise.reject(err); }
27
+ }
28
+ public async cancelDoseAsync(id: number, data: any) {
29
+ try {
30
+ let c: NixieChemDoser = this.find(elem => elem.id === id) as NixieChemDoser;
31
+ if (typeof c === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Nixie could not find a chem doser at id ${id}`, id, 'chemDoser'));
32
+ await c.cancelDosingAsync(data);
33
+ } catch (err) { logger.error(`cancelDoseAsync: ${err.message}`); return Promise.reject(err); }
34
+ }
35
+ public async manualMixAsync(id: number, data: any) {
36
+ try {
37
+ let c: NixieChemDoser = this.find(elem => elem.id === id) as NixieChemDoser;
38
+ if (typeof c === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Nixie could not find a chem doser at id ${id}`, id, 'chemDoser'));
39
+ await c.manualMixAsync(data);
40
+ } catch (err) { logger.error(`manualMixAysnc: ${err.message}`); return Promise.reject(err); }
41
+ }
42
+ public async cancelMixingAsync(id: number, data: any) {
43
+ try {
44
+ let c: NixieChemDoser = this.find(elem => elem.id === id) as NixieChemDoser;
45
+ if (typeof c === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Nixie could not find a chem doser at id ${id}`, id, 'chemDoser'));
46
+ await c.cancelMixingAsync(data);
47
+ } catch (err) { logger.error(`cancelMixingAsync: ${err.message}`); return Promise.reject(err); }
48
+ }
49
+ public async setDoserAsync(chem: ChemDoser, data: any) {
50
+ // By the time we get here we know that we are in control and this REM Chem or IntelliChem.
51
+ try {
52
+ let ncc: NixieChemDoserBase = this.find(elem => elem.id === chem.id) as NixieChemDoserBase;
53
+ if (typeof ncc === 'undefined') {
54
+ chem.master = 1;
55
+ ncc = NixieChemDoserBase.create(this.controlPanel, chem);
56
+ this.push(ncc);
57
+ logger.info(`Nixie Chem Doser was created at id #${chem.id}`);
58
+ await ncc.setDoserAsync(data);
59
+ }
60
+ else {
61
+ await ncc.setDoserAsync(data);
62
+ }
63
+ // Now go back through the array and undo anything that is in need of pruning.
64
+ }
65
+ catch (err) { logger.error(`setControllerAsync: ${err.message}`); return Promise.reject(err); }
66
+ }
67
+ public async syncRemoteREMFeeds(servers) {
68
+ for (let i = 0; i < this.length; i++) {
69
+ let ncc = this[i] as NixieChemDoserBase;
70
+ ncc.syncRemoteREMFeeds(servers);
71
+ }
72
+ }
73
+ public async initAsync(controllers: ChemDoserCollection) {
74
+ try {
75
+ for (let i = 0; i < controllers.length; i++) {
76
+ let cc = controllers.getItemByIndex(i);
77
+ if (cc.master === 1) {
78
+ logger.info(`Initializing chemDoser ${cc.name}`);
79
+ // First check to make sure it isnt already there.
80
+ if (typeof this.find(elem => elem.id === cc.id) === 'undefined') {
81
+ let ncc = NixieChemDoserBase.create(this.controlPanel, cc);
82
+ this.push(ncc);
83
+ }
84
+ else {
85
+ logger.info(`chemDoser ${cc.name} has already been initialized`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ catch (err) { logger.error(`initAsync: ${err.message}`); return Promise.reject(err); }
91
+ }
92
+ public async closeAsync() {
93
+ try {
94
+ for (let i = this.length - 1; i >= 0; i--) {
95
+ try {
96
+ logger.info(`Closing chemDoser ${this[i].id}`);
97
+ await this[i].closeAsync();
98
+ this.splice(i, 1);
99
+ } catch (err) { logger.error(`Error stopping Nixie Chem Doser ${err}`); return Promise.reject(err); }
100
+ }
101
+
102
+ } catch (err) { } // Don't bail if we have an error
103
+ }
104
+ public async setServiceModeAsync() {
105
+ try {
106
+ for (let i = this.length - 1; i >= 0; i--) {
107
+ try {
108
+ let cc = this[i] as NixieChemDoserBase;
109
+ await cc.setServiceModeAsync();
110
+ } catch (err) { logger.error(`Error setting Chem Doser to service mode ${err}`); return Promise.reject(err); }
111
+ }
112
+ } catch (err) { } // Don't bail if we have an error
113
+ }
114
+ }
115
+ export class NixieChemDoserBase extends NixieEquipment implements INixieChemController {
116
+ public pollingInterval: number = 10000;
117
+ protected _suspendPolling: number = 0;
118
+ public get suspendPolling(): boolean { return this._suspendPolling > 0; }
119
+ public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
120
+ public _ispolling = false;
121
+ protected _pollTimer: NodeJS.Timeout = null;
122
+ protected closing = false;
123
+ public flowSensor: NixieChemFlowSensor;
124
+ public bodyOnTime: number;
125
+ public flowDetected: boolean = false;
126
+ public get id() { return typeof this.chem !== 'undefined' ? this.chem.id : -1; }
127
+ public pump: NixieChemPump;
128
+ public tank: NixieChemTank;
129
+ public _lastOnStatus: number;
130
+ protected _stoppingMix = false;
131
+ protected _processingMix = false;
132
+ public chemType: string;
133
+ public _currentMix: NixieChemMix;
134
+ protected _mixTimer: NodeJS.Timeout;
135
+ public get currentMix(): NixieChemMix { return this._currentMix; }
136
+ public set currentMix(val: NixieChemMix) {
137
+ if (typeof val === 'undefined' && typeof this._currentMix !== 'undefined') logger.debug(`${this.chem.chemType} mix set to undefined`);
138
+ else logger.debug(`Set new current mix ${this.chem.chemType}`)
139
+ this._currentMix = val;
140
+ }
141
+ constructor(ncp: INixieControlPanel, chem: ChemDoser) {
142
+ super(ncp);
143
+ this.chem = chem;
144
+ }
145
+ public chem: ChemDoser;
146
+ public syncRemoteREMFeeds(servers) { }
147
+ public async setServiceModeAsync() {}
148
+ public static create(ncp: INixieControlPanel, chem: ChemDoser): NixieChemDoserBase {
149
+ return new NixieChemDoser(ncp, chem);
150
+ }
151
+ public isBodyOn() {
152
+ let isOn = sys.board.bodies.isBodyOn(this.chem.body);
153
+ if (isOn && typeof this.bodyOnTime === 'undefined') {
154
+ this.bodyOnTime = new Date().getTime();
155
+ }
156
+ else if (!isOn) this.bodyOnTime = undefined;
157
+ return isOn;
158
+ }
159
+ public async setDoserAsync(data: any) { } // This is meant to be abstract override this value
160
+ protected async cancelMixing(schem: ChemDoserState): Promise<void> {
161
+ try {
162
+ logger.verbose(`Cancelling ${this.chemType} Mix`);
163
+ await this.stopMixing(schem);
164
+ } catch (err) { logger.error(`cancelMixing ${this.chemType}: ${err.message}`); return Promise.reject(err); }
165
+ }
166
+ protected async stopMixing(schem: ChemDoserState): Promise<void> {
167
+ try {
168
+ this._stoppingMix = true;
169
+ this.suspendPolling = true;
170
+ if (typeof this.currentMix !== 'undefined') logger.debug(`Stopping ${schem.chemType} mix and clearing the current mix object.`);
171
+ if (typeof this.currentMix !== 'undefined' || typeof this._mixTimer !== 'undefined' || this._mixTimer) {
172
+ if (this._mixTimer || typeof this._mixTimer !== 'undefined') {
173
+ clearInterval(this._mixTimer);
174
+ this._mixTimer = undefined;
175
+ logger.verbose(`Cleared ${schem.chemType} mix timer`);
176
+ }
177
+ else
178
+ logger.warn(`${schem.chemType} did not have a mix timer set when cancelling.`);
179
+ if (typeof this.currentMix !== 'undefined') {
180
+ this.currentMix = undefined;
181
+ logger.verbose(`Cleared ${schem.chemType} mix object`);
182
+ }
183
+ else
184
+ logger.warn(`${schem.chemType} did not have a currentMix object set when cancelling.`);
185
+ schem.dosingStatus = sys.board.valueMaps.chemDoserDosingStatus.getValue('monitoring');
186
+ schem.mixTimeRemaining = 0;
187
+ schem.manualMixing = false;
188
+ }
189
+ } catch (err) { logger.error(`Error stopping chemical mix`); return Promise.reject(err); }
190
+ finally { this._stoppingMix = false; this.suspendPolling = false; }
191
+ }
192
+ protected async initMixChemicals(schem: ChemDoserState, mixingTime?: number): Promise<void> {
193
+ try {
194
+ if (this._stoppingMix) return;
195
+ if (typeof this.currentMix === 'undefined') {
196
+ if (typeof mixingTime !== 'undefined') {
197
+ // This is a manual mix so we need to make sure the pump is not dosing.
198
+ logger.info(`Clearing any possible ${schem.chemType} dosing or existing mix for mixingTime: ${mixingTime}`);
199
+ await this.pump.stopDosing(schem, 'mix override');
200
+ await this.stopMixing(schem);
201
+ }
202
+ this.currentMix = new NixieChemMix();
203
+ if (typeof mixingTime !== 'undefined' && !isNaN(mixingTime)) {
204
+ this.currentMix.set({ time: mixingTime, timeMixed: 0, isManual: true });
205
+ schem.manualMixing = true;
206
+ }
207
+ else if (schem.mixTimeRemaining > 0) {
208
+ if (schem.manualMixing) {
209
+ this.currentMix.set({ time: schem.mixTimeRemaining, timeMixed: 0, isManual: true });
210
+ }
211
+ else
212
+
213
+ this.currentMix.set({ time: this.chem.mixingTime, timeMixed: Math.max(0, this.chem.mixingTime - schem.mixTimeRemaining) });
214
+ }
215
+
216
+ else
217
+ this.currentMix.set({ time: this.chem.mixingTime, timeMixed: 0 });
218
+ logger.info(`Chem Doser begin mixing ${schem.chemType} for ${utils.formatDuration(this.currentMix.timeRemaining)} of ${utils.formatDuration(this.currentMix.time)}`)
219
+ schem.mixTimeRemaining = this.currentMix.timeRemaining;
220
+ }
221
+ if (typeof this._mixTimer === 'undefined' || !this._mixTimer) {
222
+ let self = this;
223
+ this._mixTimer = setInterval(async () => { await self.mixChemicals(schem); }, 1000);
224
+ logger.verbose(`Set ${schem.chemType} mix timer`);
225
+ }
226
+ } catch (err) { logger.error(`Error initializing ${schem.chemType} mix: ${err.message}`); }
227
+ }
228
+ public async mixChemicals(schem: ChemDoserState, mixingTime?: number): Promise<void> {
229
+ try {
230
+ if (this._stoppingMix) {
231
+ logger.verbose(`${schem.chemType} is currently stopping mixChemicals ignored.`)
232
+ return;
233
+ }
234
+ if (this._processingMix) {
235
+ logger.verbose(`${schem.chemType} is already processing mixChemicals ignored.`);
236
+ return;
237
+ }
238
+ this._processingMix = true;
239
+ if (!this.chem.enabled) {
240
+ // The chemical is not enabled so we need to ditch the mixing if it is currently underway.
241
+ await this.stopMixing(schem);
242
+ return;
243
+ }
244
+
245
+ let dt = new Date().getTime();
246
+ await this.initMixChemicals(schem, mixingTime);
247
+ if (this._stoppingMix) return;
248
+ if (!this.chem.flowOnlyMixing || (schem.chemController.isBodyOn && this.flowDetected && !schem.freezeProtect)) {
249
+ this.currentMix.timeMixed += Math.round((dt - this.currentMix.lastChecked) / 1000);
250
+ logger.verbose(`Chem ${schem.chemType} mixing paused because body is not on.`);
251
+ // Reflect any changes to the configuration.
252
+ if (!this.currentMix.isManual) { this.currentMix.time = this.chem.mixingTime; }
253
+ schem.mixTimeRemaining = Math.round(this.currentMix.timeRemaining);
254
+ logger.verbose(`Chem mixing ${schem.chemType} remaining: ${utils.formatDuration(schem.mixTimeRemaining)}`);
255
+ }
256
+ else
257
+ logger.verbose(`Chem ${schem.chemType} mixing paused because body is not on.`);
258
+
259
+ this.currentMix.lastChecked = dt;
260
+ if (schem.mixTimeRemaining <= 0) {
261
+ logger.info(`Chem Doser ${schem.chemType} mixing Complete after ${utils.formatDuration(this.currentMix.timeMixed)}`);
262
+ await this.stopMixing(schem);
263
+ }
264
+ else {
265
+ schem.dosingStatus = sys.board.valueMaps.chemDoserDosingStatus.getValue('mixing');
266
+ }
267
+ } catch (err) { logger.error(`Error mixing chemicals: ${err.message}`); }
268
+ finally {
269
+ this._processingMix = false;
270
+ setImmediate(() => { schem.emitEquipmentChange(); });
271
+ }
272
+ }
273
+ protected async setHardware(chem: ChemDoser, data: any) {
274
+ try {
275
+
276
+ }
277
+ catch (err) { return Promise.reject(err); }
278
+ }
279
+ public processAlarms(schem: ChemDoserState) {
280
+ try {
281
+ // Calculate all the alarms. These are only informational at this point.
282
+ let setupValid = true;
283
+ if (this.flowSensor.sensor.type === 0) {
284
+ // When there is no flow sensor we always use the body to determine flow. This means that the
285
+ // flow alarm can never be triggered.
286
+ schem.alarms.flow = 0;
287
+ }
288
+ else {
289
+ // If the body is on and there is no flow detected then we need
290
+ // to indicate this to the user.
291
+ schem.alarms.flow = schem.isBodyOn && !schem.flowDetected ? 1 : 0;
292
+ }
293
+ schem.dailyVolumeDosed = schem.calcDoseHistory();
294
+ let chem = this.chem;
295
+ schem.enabled = this.chem.enabled;
296
+ if (this.chem.enabled) {
297
+ let pumpType = chem.pump.type;
298
+ let currLevelPercent = schem.tank.level / schem.tank.capacity * 100;
299
+ if (pumpType !== 0) {
300
+ if (currLevelPercent <= 0) schem.alarms.tank = 32;
301
+ else schem.alarms.tank = schem.tank.alarmEmptyEnabled && currLevelPercent <= schem.tank.alarmEmptyLevel ? 129 : 0;
302
+ }
303
+ else schem.alarms.tank = 0;
304
+ schem.warnings.dailyLimitReached = 0;
305
+ // Alright we need to determine whether we need to adjust the volume any so that we get at least 3 seconds out of the pump.
306
+ let padj = this.chem.pump.type > 0 ? (this.chem.pump.ratedFlow / 60) * 3 : 0;
307
+ if (this.chem.maxDailyVolume <= schem.dailyVolumeDosed + padj) {
308
+ schem.warnings.dailyLimitReached = 2;
309
+ schem.dailyLimitReached = true;
310
+ }
311
+ else {
312
+ schem.warnings.dailyLimitReached = 0;
313
+ schem.dailyLimitReached = false;
314
+ }
315
+ schem.freezeProtect = (state.freeze && chem.disableOnFreeze && schem.isBodyOn);
316
+ }
317
+ else {
318
+ schem.alarms.tank = 0;
319
+ schem.warnings.dailyLimitReached = 0;
320
+ schem.freezeProtect = false;
321
+ }
322
+ schem.alarms.freezeProtect = (schem.freezeProtect) ? sys.board.valueMaps.chemDoserAlarms.getValue('freezeprotect') : 0;
323
+ } catch (err) { logger.error(`Error processing chem doser ${this.chem.name} alarms: ${err.message}`); }
324
+ }
325
+ }
326
+ export class NixieChemDoser extends NixieChemDoserBase implements INixieChemical {
327
+ private ver = 1.0;
328
+ constructor(ncp: INixieControlPanel, chem: ChemDoser) {
329
+ super(ncp, chem);
330
+ this.flowSensor = new NixieChemFlowSensor(this, chem.flowSensor);
331
+ this.pollEquipmentAsync();
332
+ this.pump = new NixieChemPump(this, chem.pump);
333
+ this.tank = new NixieChemTank(this, chem.tank);
334
+ }
335
+ public get chemical() { return this.chem; }
336
+ public get chemController() { return this; }
337
+ public async setServiceModeAsync() {
338
+ let schem = state.chemDosers.getItemById(this.chem.id);
339
+ this.cancelDosing(schem, 'service mode');
340
+ }
341
+ public async calibrateDoseAsync(data: any) {
342
+ try {
343
+ this.suspendPolling = true;
344
+ // Check to see that we are a rem chem.
345
+ let time = parseInt(data.time, 10);
346
+ if (isNaN(time)) return Promise.reject(new InvalidEquipmentDataError(`Time was not supplied for the calibration chem dose`, 'chemDoser', data.time));
347
+ // Now we can tell the chemical to dose.
348
+ let schem = state.chemDosers.getItemById(this.chem.id);
349
+ await this.calibratePumpAsync(schem, time);
350
+ }
351
+ catch (err) { logger.error(`calibrateDoseAsync: ${err.message}`); return Promise.reject(err); }
352
+ finally { this.suspendPolling = false; }
353
+
354
+ }
355
+ public async manualDoseAsync(data: any) {
356
+ try {
357
+ this.suspendPolling = true;
358
+ // Check to see that we are a rem chem.
359
+ let vol = parseInt(data.volume, 10);
360
+ if (isNaN(vol)) return Promise.reject(new InvalidEquipmentDataError(`Volume was not supplied for the manual chem dose`, 'chemDoser', data.volume));
361
+ let schem = state.chemDosers.getItemById(this.chem.id, true);
362
+ // Determine which chemical we are dosing. This will be ph or orp.
363
+ await this.manualDoseVolumeAsync(schem, vol);
364
+ }
365
+ catch (err) { logger.error(`manualDoseAsync: ${err.message}`); return Promise.reject(err); }
366
+ finally { this.suspendPolling = false; }
367
+ }
368
+ public async manualMixAsync(data: any) {
369
+ try {
370
+ this.suspendPolling = true;
371
+ // Check to see that we are a rem chem.
372
+ let time = 0;
373
+ if (typeof data.hours !== 'undefined') time += parseInt(data.hours, 10) * 3600;
374
+ if (typeof data.minutes !== 'undefined') time += parseInt(data.minutes, 10) * 60;
375
+ if (typeof data.seconds !== 'undefined') time += parseInt(data.seconds, 10);
376
+ if (isNaN(time) || time <= 0) return Promise.reject(new InvalidEquipmentDataError(`Mix time was not supplied for the manual chem mix`, 'chemDoser', time));
377
+ // Determine which chemical we are dosing. This will be ph or orp.
378
+ let schem = state.chemDosers.getItemById(this.chem.id, true);
379
+ if (typeof schem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Could not initiate ${data.chemType} manual mix state not found.`, 'chemDoser', data.chemType));
380
+ // Now we can tell the chemical to dose.
381
+ await this.mixChemicals(schem, time);
382
+ }
383
+ catch (err) { logger.error(`manualMixAsync: ${err.message}`); return Promise.reject(err); }
384
+ finally { this.suspendPolling = false; }
385
+ }
386
+ public async cancelDosingAsync(data: any) {
387
+ try {
388
+ this.suspendPolling = true;
389
+ let schem = state.chemDosers.getItemById(this.chem.id, true);
390
+ if (typeof schem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Could not cancel ${data.chemType} dose state not found.`, 'chemDoser', data.chemType));
391
+ // Now we can tell the chemical to dose.
392
+ await this.cancelDosing(schem, 'cancelled');
393
+ }
394
+ catch (err) { logger.error(`cancelDosingAsync: ${err.message}`); return Promise.reject(err); }
395
+ finally { this.suspendPolling = false; }
396
+ }
397
+ public async cancelMixingAsync(data: any) {
398
+ try {
399
+ this.suspendPolling = true;
400
+ // Determine which chemical we are cancelling. This will be ph or orp.
401
+ let schem = state.chemDosers.getItemById(this.chem.id);
402
+ await this.cancelMixing(schem);
403
+ }
404
+ catch (err) { logger.error(`cancelMixingAsync: ${err.message}`); return Promise.reject(err); }
405
+ finally { this.suspendPolling = false; }
406
+ }
407
+ public async setDoserAsync(data: any) {
408
+ try {
409
+ this.suspendPolling = true;
410
+ let chem = this.chem;
411
+ // So now we are down to the nitty gritty setting the data for the REM or Homegrown Chem controller.
412
+ let body = sys.board.bodies.mapBodyAssociation(typeof data.body === 'undefined' ? chem.body : data.body);
413
+ if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid body assignment`, 'chemDoser', data.body || chem.body));
414
+ // Do a final validation pass so we dont send this off in a mess.
415
+ let schem = state.chemDosers.getItemById(chem.id, true);
416
+ chem.body = body;
417
+ schem.name = chem.name = data.name || chem.name || `Chem Doser ${chem.id}`;
418
+ schem.isActive = chem.isActive = true;
419
+ schem.enabled = chem.enabled = typeof data.enabled !== 'undefined' ? utils.makeBool(data.enabled) : chem.enabled;
420
+ chem.dosingMethod = typeof data.dosingMethod !== 'undefined' ? data.dosingMethod : chem.dosingMethod;
421
+ chem.dosingVolume = typeof data.dosingVolume !== 'undefined' ? parseInt(data.dosingVolume, 10) : chem.dosingVolume;
422
+ chem.startDelay = typeof data.startDelay !== 'undefined' ? parseFloat(data.startDelay) : chem.startDelay;
423
+ chem.maxDailyVolume = typeof data.maxDailyVolume !== 'undefined' ? typeof data.maxDailyVolume === 'number' ? data.maxDailyVolume : parseInt(data.maxDailyVolume, 10) : chem.maxDailyVolume;
424
+ chem.flowOnlyMixing = typeof data.flowOnlyMixing !== 'undefined' ? utils.makeBool(data.flowOnlyMixing) : chem.flowOnlyMixing;
425
+ schem.type = chem.type = typeof data.type !== 'undefined' ? sys.board.valueMaps.chemDoserTypes.encode(data.type, 0) : chem.type;
426
+ if (typeof data.mixingTimeHours !== 'undefined' || typeof data.mixingTimeMinutes !== 'undefined') {
427
+ data.mixingTime = (typeof data.mixingTimeHours !== 'undefined' ? parseInt(data.mixingTimeHours, 10) * 3600 : 0) +
428
+ (typeof data.mixingTimeMinutes !== 'undefined' ? parseInt(data.mixingTimeMinutes, 10) * 60 : 0) +
429
+ (typeof data.mixingTimeSeconds !== 'undefined' ? parseInt(data.mixingTimeSeconds, 10) : 0);
430
+ }
431
+ chem.mixingTime = typeof data.mixingTime !== 'undefined' ? parseInt(data.mixingTime, 10) : chem.mixingTime;
432
+ await this.flowSensor.setSensorAsync(data.flowSensor);
433
+ await this.tank.setTankAsync(schem.tank, data.tank);
434
+ await this.pump.setPumpAsync(schem.pump, data.pump);
435
+ await this.processAlarms(schem);
436
+ }
437
+ catch (err) { logger.error(`setDoserAsync: ${err.message}`); return Promise.reject(err); }
438
+ finally { this.suspendPolling = false; }
439
+ }
440
+ public async checkFlowAsync(schem: ChemDoserState): Promise<boolean> {
441
+ try {
442
+ this.suspendPolling = true;
443
+ schem.isBodyOn = this.isBodyOn();
444
+ // rsg - we were not returning the flow sensor state when the body was off.
445
+ // first, this would not allow us to retrieve a pressure of 0 to update flowSensor.state
446
+ // second, we can set a flow alarm if the expected flow doesn't match actual flow
447
+ if (this.flowSensor.sensor.type === 0) {
448
+ this.flowDetected = schem.flowDetected = true;
449
+ schem.alarms.flowSensorFault = 0;
450
+ }
451
+ else {
452
+ logger.verbose(`Begin getting flow sensor state`);
453
+ let ret = await this.flowSensor.getState();
454
+ schem.flowSensor.state = ret.obj.state;
455
+ // Call out to REM to see if we have flow.
456
+
457
+ // We should have state from the sensor but we want to keep this somewhat generic.
458
+ //[1, { name: 'switch', desc: 'Flow Switch', remAddress: true }],
459
+ //[2, { name: 'rate', desc: 'Rate Sensor', remAddress: true }],
460
+ //[4, { name: 'pressure', desc: 'Pressure Sensor', remAddress: true }],
461
+ if (this.flowSensor.sensor.type === 1) {
462
+ // This is a flow switch. The expectation is that it should be 0 or 1.
463
+ let v;
464
+ if (typeof ret.obj.state.boolean !== 'undefined') v = utils.makeBool(ret.obj.state.boolean);
465
+ else if (typeof ret.obj.state === 'string') v = utils.makeBool(ret.obj.state);
466
+ else if (typeof ret.obj.state === 'boolean') v = ret.obj.state;
467
+ else if (typeof ret.obj.state === 'number') v = utils.makeBool(ret.obj.state);
468
+ else if (typeof ret.obj.state.val === 'number') v = utils.makeBool(ret.obj.state.val);
469
+ else if (typeof ret.obj.state.value === 'number') v = utils.makeBool(ret.obj.state.value);
470
+ else v = false;
471
+ this.flowDetected = schem.flowDetected = v;
472
+ }
473
+ else if (this.flowSensor.sensor.type == 2) {
474
+ this.flowDetected = schem.flowDetected = ret.obj.state > this.flowSensor.sensor.minimumFlow;
475
+ }
476
+ else if (this.flowSensor.sensor.type == 4) {
477
+ this.flowDetected = schem.flowDetected = ret.obj.state > this.flowSensor.sensor.minimumPressure;
478
+ }
479
+ else
480
+ this.flowDetected = schem.flowDetected = false;
481
+ schem.alarms.flowSensorFault = 0;
482
+ }
483
+ if (!schem.flowDetected) this.bodyOnTime = undefined;
484
+ else if (typeof this.bodyOnTime === 'undefined') this.bodyOnTime = new Date().getTime();
485
+ logger.verbose(`End getting flow sensor state`);
486
+ return schem.flowDetected;
487
+ }
488
+ catch (err) { logger.error(`checkFlowAsync: ${err.message}`); schem.alarms.flowSensorFault = 7; this.flowDetected = schem.flowDetected = false; return Promise.reject(err); }
489
+ finally { this.suspendPolling = false; }
490
+ }
491
+ public async pollEquipmentAsync() {
492
+ let self = this;
493
+ try {
494
+ logger.verbose(`Begin polling Chem Doser ${this.id}`);
495
+ if (this._suspendPolling > 0) logger.warn(`Suspend polling for ${this.chem.name} -> ${this._suspendPolling}`);
496
+ if (this.suspendPolling) return;
497
+ if (this._ispolling) return;
498
+ this._ispolling = true;
499
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
500
+ this._pollTimer = null;
501
+ let schem = state.chemDosers.getItemById(this.chem.id, !this.closing);
502
+ // We need to check on the equipment to make sure it is solid.
503
+ if (NixieEquipment.isConnected) {
504
+ schem.alarms.comms = 0;
505
+ schem.status = 0;
506
+ schem.lastComm = new Date().getTime();
507
+ await this.checkFlowAsync(schem);
508
+ await this.validateSetupAsync(this.chem, schem);
509
+ this.processAlarms(schem);
510
+ await this.checkDosing(this.chem, schem);
511
+ if (state.mode === 0 && this.chem.enabled) {
512
+ }
513
+ }
514
+ else
515
+ logger.warn('REM Server not Connected');
516
+ this._ispolling = false;
517
+ }
518
+ catch (err) { this._ispolling = false; logger.error(`Error polling Chem Doser - ${err}`); }
519
+ finally {
520
+ if (!this.closing && !this._ispolling)
521
+ this._pollTimer = setTimeout(() => { self.pollEquipmentAsync(); }, this.pollingInterval || 10000);
522
+ logger.verbose(`End polling Chem Doser ${this.id}`);
523
+ }
524
+ }
525
+ public async checkDosing(doser: ChemDoser, sd: ChemDoserState) {
526
+ try {
527
+ let status = sys.board.valueMaps.chemControllerDosingStatus.getName(sd.dosingStatus);
528
+ logger.debug(`Begin check dosing status = ${status}`);
529
+ if (!doser.enabled) {
530
+ await this.cancelDosing(sd, 'disabled');
531
+ return;
532
+ }
533
+ if (sd.suspendDosing) {
534
+ // Kill off the dosing and make sure the pump isn't running. Let's force the issue here.
535
+ await this.cancelDosing(sd, 'suspended');
536
+ return;
537
+ }
538
+ if (status === 'monitoring') {
539
+ // Alright our mixing and dosing have either been cancelled or we fininsed a mixing cycle. Either way
540
+ // let the system clean these up.
541
+ if (typeof sd.currentDose !== 'undefined') logger.error('Somehow we made it to monitoring and still have a current dose');
542
+ sd.currentDose = undefined;
543
+ sd.manualDosing = false;
544
+ sd.dosingVolumeRemaining = 0;
545
+ sd.dosingTimeRemaining = 0;
546
+ if (typeof this.currentMix !== 'undefined') {
547
+ if (ncp.chemDosers.length > 1) {
548
+ let arrIds = [];
549
+ for (let i = 0; i < ncp.chemDosers.length; i++) {
550
+ arrIds.push(ncp[i].id);
551
+ }
552
+ logger.info(`More than one NixieChemDoser object was found ${JSON.stringify(arrIds)}`);
553
+ }
554
+ logger.debug(`We are now monitoring and have a mixing object`);
555
+ await this.stopMixing(sd);
556
+ }
557
+ await this.cancelDosing(sd, 'monitoring');
558
+ }
559
+ if (status === 'mixing') {
560
+ await this.cancelDosing(sd, 'mixing');
561
+ if (typeof this.currentMix === 'undefined') {
562
+ logger.info(`Current ${sd.chemType} mix object not defined initializing mix`);
563
+ await this.mixChemicals(sd);
564
+ }
565
+ }
566
+ else if (sd.manualDosing) {
567
+ // We are manually dosing. We are not going to dynamically change the dose.
568
+ if (typeof sd.currentDose === 'undefined') {
569
+ // This will only happen when njspc is killed in the middle of a dose. Unlike IntelliChem we will pick that back up.
570
+ // Unfortunately we will lose the original start date but who cares as the volumes should remain the same.
571
+ let volume = sd.volumeDosed + sd.dosingVolumeRemaining;
572
+ let time = sd.timeDosed + sd.dosingTimeRemaining;
573
+ sd.startDose(new Timestamp().addSeconds(-sd.doseTime).toDate(), 'manual', volume, sd.dosingVolumeRemaining, time * 1000, sd.doseTime * 1000);
574
+ }
575
+ if (sd.tank.level > 0) {
576
+ logger.verbose(`Chem ${sd.chemType} dose activate pump ${ this.pump.pump.ratedFlow }mL / min`);
577
+ await this.stopMixing(sd);
578
+ await this.pump.dose(sd);
579
+ }
580
+ else {
581
+ if (typeof sd.currentDose !== 'undefined' && sd.currentDose.method === 'calibration') { }
582
+ else await this.cancelDosing(sd, 'empty tank');
583
+ }
584
+ }
585
+ else if (sd.dailyLimitReached) {
586
+ await this.cancelDosing(sd, 'daily limit');
587
+ }
588
+ else if (status === 'monitoring' || status === 'dosing') {
589
+ // Figure out what mode we are in and what mode we should be in.
590
+ //sph.level = 7.61;
591
+ // Check the setpoint and the current level to see if we need to dose.
592
+ if (!sd.chemController.isBodyOn)
593
+ await this.cancelDosing(sd, 'body off');
594
+ else if (sd.freezeProtect)
595
+ await this.cancelDosing(sd, 'freeze');
596
+ else if (!sd.chemController.flowDetected)
597
+ await this.cancelDosing(sd, 'no flow');
598
+ else {
599
+ logger.info(`Starting dose calculation`);
600
+ let pump = this.pump.pump;
601
+ let dose = Math.max(0, Math.min(this.chem.maxDailyVolume - sd.dailyVolumeDosed, doser.dosingVolume));
602
+ let time = typeof pump.ratedFlow === 'undefined' || pump.ratedFlow <= 0 ? 0 : Math.round(dose / (pump.ratedFlow / 60));
603
+ logger.info(`Chem ${sd.chemType} calculated ${dose}mL for ${utils.formatDuration(time)} Tank Level: ${sd.tank.level}`);
604
+ if (typeof sd.currentDose === 'undefined' && sd.tank.level > 0) {
605
+ // We will include this with the dose demand because our limits may reduce it.
606
+ //dosage.demand = demand;
607
+ if (sd.dosingStatus === 0) { // 0 is dosing.
608
+ // We need to finish off a dose that was interrupted by regular programming. This occurs
609
+ // when for instance njspc is interrupted and restarted in the middle of a dose. If we were
610
+ // mixing before we will never get here.
611
+ logger.info(`Continuing a previous new ${sd.chemType} dose ${sd.doseVolume}mL`);
612
+ sd.startDose(new Timestamp().addSeconds(-sd.doseTime).toDate(), 'auto', sd.doseVolume + sd.dosingVolumeRemaining, sd.doseVolume, (sd.doseTime + sd.dosingTimeRemaining) * 1000, sd.doseTime * 1000);
613
+ }
614
+ else {
615
+ logger.info(`Starting a new ${sd.chemType} dose ${dose}mL`);
616
+ sd.startDose(new Date(), 'auto', dose, 0, time, 0);
617
+ }
618
+ }
619
+ // Now let's determine what we need to do with our pump to satisfy our acid demand.
620
+ if (sd.tank.level > 0) {
621
+ logger.verbose(`Chem ${sd.chemType} dose activate pump ${this.pump.pump.ratedFlow}mL/min`);
622
+ await this.pump.dose(sd);
623
+ }
624
+ else {
625
+ logger.warn(`Chem ${sd.chemType} NOT dosed because tank level is level ${sd.tank.level}.`);
626
+ await this.cancelDosing(sd, 'empty tank');
627
+ }
628
+ }
629
+ }
630
+ }
631
+ catch (err) { logger.error(err); return Promise.reject(err); }
632
+ finally {
633
+ logger.debug(`End check ${sd.chemType} dosing status = ${sys.board.valueMaps.chemControllerDosingStatus.getName(sd.dosingStatus)}`);
634
+ }
635
+ }
636
+ private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
637
+ try {
638
+ let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
639
+ return dev;
640
+ } catch (err) { logger.error(`checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
641
+ }
642
+ public async validateSetupAsync(chem: ChemDoser, schem: ChemDoserState) {
643
+ try {
644
+ // The validation will be different if the body is on or not. So lets get that information.
645
+ logger.verbose(`Begin validating ${chem.id} - ${chem.name} setup`);
646
+ if (chem.enabled) {
647
+ if (chem.pump.type !== 0) {
648
+ let type = sys.board.valueMaps.chemPumpTypes.transform(chem.pump.type);
649
+ if (type.remAddress) {
650
+ let dev = await this.checkHardwareStatusAsync(chem.pump.connectionId, chem.pump.deviceBinding);
651
+ schem.alarms.pumpFault = dev.hasFault ? 2 : 0;
652
+ }
653
+ else schem.alarms.pumpFault = 0;
654
+ }
655
+ else schem.alarms.pumpFault = 0;
656
+ }
657
+ else schem.alarms.pumpFault = schem.alarms.pumpFault = 0;
658
+ schem.alarms.comms = 0;
659
+ logger.verbose(`End validating ${chem.id} - ${chem.name} setup`);
660
+ } catch (err) { logger.error(`Error checking Chem Doser Hardware ${this.chem.name}: ${err.message}`); schem.alarms.comms = 2; return Promise.reject(err); }
661
+ }
662
+ public async closeAsync() {
663
+ try {
664
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
665
+ this._pollTimer = null;
666
+ if (typeof this._mixTimer !== 'undefined' || this._mixTimer) clearTimeout(this._mixTimer);
667
+ this._mixTimer = null;
668
+ this.currentMix = null;
669
+ this.closing = true;
670
+ let schem = state.chemDosers.getItemById(this.chem.id);
671
+ await this.cancelDosing(schem, 'closing');
672
+ logger.info(`Closed chem doser ${schem.id} ${schem.name}`);
673
+ schem.emitEquipmentChange();
674
+ }
675
+ catch (err) { logger.error(`ChemDoser closeAsync: ${err.message}`); return Promise.reject(err); }
676
+ }
677
+ public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
678
+ public syncRemoteREMFeeds(servers) { }
679
+ protected async initMixChemicals(schem: ChemDoserState, mixingTime?: number): Promise<void> {
680
+ try {
681
+ if (this._stoppingMix) return;
682
+ if (typeof this.currentMix === 'undefined') {
683
+ if (typeof mixingTime !== 'undefined') {
684
+ // This is a manual mix so we need to make sure the pump is not dosing.
685
+ logger.info(`Clearing any possible ${schem.chemType} dosing or existing mix for mixingTime: ${mixingTime}`);
686
+ await this.pump.stopDosing(schem, 'mix override');
687
+ await this.stopMixing(schem);
688
+ }
689
+ this.currentMix = new NixieChemMix();
690
+ if (typeof mixingTime !== 'undefined' && !isNaN(mixingTime)) {
691
+ this.currentMix.set({ time: mixingTime, timeMixed: 0, isManual: true });
692
+ schem.manualMixing = true;
693
+ }
694
+ else if (schem.mixTimeRemaining > 0) {
695
+ if (schem.manualMixing) {
696
+ this.currentMix.set({ time: schem.mixTimeRemaining, timeMixed: 0, isManual: true });
697
+ }
698
+ else
699
+ this.currentMix.set({ time: this.chem.mixingTime, timeMixed: Math.max(0, this.chem.mixingTime - schem.mixTimeRemaining) });
700
+ }
701
+ else
702
+ this.currentMix.set({ time: this.chem.mixingTime, timeMixed: 0 });
703
+ logger.info(`Chem Doser begin mixing ${schem.chemType} for ${utils.formatDuration(this.currentMix.timeRemaining)} of ${utils.formatDuration(this.currentMix.time)}`)
704
+ schem.mixTimeRemaining = this.currentMix.timeRemaining;
705
+ }
706
+ if (typeof this._mixTimer === 'undefined' || !this._mixTimer) {
707
+ let self = this;
708
+ this._mixTimer = setInterval(async () => { await self.mixChemicals(schem); }, 1000);
709
+ logger.verbose(`Set ${schem.chemType} mix timer`);
710
+ }
711
+ } catch (err) { logger.error(`Error initializing ${schem.chemType} mix: ${err.message}`); }
712
+ }
713
+ public async cancelDosing(schem: ChemDoserState, reason: string) {
714
+ try {
715
+ // Just stop the pump for now but we will do some logging later.
716
+ await this.pump.stopDosing(schem, reason);
717
+ if (schem.dosingStatus === 0) {
718
+ await this.mixChemicals(schem);
719
+ // Set the setpoints back to the original.
720
+ if (this.chem.disableChlorinator) {
721
+ let chlors = sys.chlorinators.getByBody(this.chem.body);
722
+ for (let i = 0; i < chlors.length; i++) {
723
+ let chlor = chlors.getItemByIndex(i);
724
+ if (chlor.disabled) await sys.board.chlorinator.setChlorAsync({ id: chlor.id, disabled: false });
725
+ }
726
+ }
727
+ }
728
+ if (typeof schem.currentDose !== 'undefined') schem.endDose(new Date(), 'cancelled');
729
+ } catch (err) { logger.error(`cancelDosing ${this.chem.chemType}: ${ err.message }`); return Promise.reject(err); }
730
+ }
731
+ public async calibratePumpAsync(schem: ChemDoserState, time: number) {
732
+ try {
733
+ logger.debug(`Starting manual ${schem.chemType} dose for ${time}seconds`);
734
+ let status = sys.board.valueMaps.chemDoserDosingStatus.getName(schem.dosingStatus);
735
+ if (status === 'monitoring') {
736
+ // Alright our mixing and dosing have either been cancelled or we fininsed a mixing cycle. Either way
737
+ // let the system clean these up.
738
+ if (typeof schem.currentDose !== 'undefined') await this.cancelDosing(schem, 'manual cancel');
739
+ if (typeof this.currentMix !== 'undefined') await this.stopMixing(schem);
740
+ }
741
+ if (status === 'mixing') {
742
+ // We are mixing so we need to stop that.
743
+ await this.stopMixing(schem);
744
+ }
745
+ else if (status === 'dosing') {
746
+ // We are dosing so we need to stop that.
747
+ await this.cancelDosing(schem, 'manual cancel');
748
+ }
749
+ //if (sph.tank.level <= 0) return Promise.reject(new InvalidEquipmentDataError(`The ${sph.chemType} tank is empty`, 'chemical', sph));
750
+ let pump = this.pump.pump;
751
+ let volume = typeof pump.ratedFlow === 'undefined' || pump.ratedFlow <= 0 ? 0 : time * (pump.ratedFlow / 60);
752
+ // We should now be monitoring.
753
+ logger.verbose(`Chem begin calculating calibration dose time:${time} seconds`);
754
+ schem.manualDosing = true;
755
+ schem.startDose(new Date(), 'calibration', -1, 0, time);
756
+ logger.verbose(`Chem ${this.chemType} manual calibration dose activate pump`);
757
+ await this.pump.dose(schem);
758
+ }
759
+ catch (err) { logger.error(`calibrateDoseAsync: ${err.message}`); logger.error(err); return Promise.reject(err); }
760
+ }
761
+ public async manualDoseVolumeAsync(schem: ChemDoserState, volume: number) {
762
+ try {
763
+ logger.debug(`Starting manual ${schem.chemType} dose of ${volume}mL`);
764
+ let status = sys.board.valueMaps.chemDoserDosingStatus.getName(schem.dosingStatus);
765
+ if (status === 'monitoring') {
766
+ // Alright our mixing and dosing have either been cancelled or we fininsed a mixing cycle. Either way
767
+ // let the system clean these up.
768
+ if (typeof schem.currentDose !== 'undefined') await this.cancelDosing(schem, 'manual cancel');
769
+ if (typeof this.currentMix !== 'undefined') await this.stopMixing(schem);
770
+ }
771
+ if (status === 'mixing') {
772
+ // We are mixing so we need to stop that.
773
+ await this.stopMixing(schem);
774
+ }
775
+ else if (status === 'dosing') {
776
+ // We are dosing so we need to stop that.
777
+ await this.cancelDosing(schem, 'manual cancel');
778
+ }
779
+ if (schem.tank.level <= 0) return Promise.reject(new InvalidEquipmentDataError(`The ${schem.chemType} tank is empty`, 'chemical', schem));
780
+ let pump = this.pump.pump;
781
+ let time = typeof pump.ratedFlow === 'undefined' || pump.ratedFlow <= 0 ? 0 : Math.round(volume / (pump.ratedFlow / 60));
782
+ // We should now be monitoring.
783
+ logger.verbose(`Chem begin calculating manual dose volume:${volume}`);
784
+ schem.manualDosing = true;
785
+ schem.startDose(new Date(), 'manual', volume, 0, time);
786
+ if (schem.tank.level > 0) {
787
+ logger.verbose(`Chem ${schem.chemType} manual dose activate pump ${ this.pump.pump.ratedFlow }mL / min`);
788
+ await this.pump.dose(schem);
789
+ }
790
+ }
791
+ catch (err) { logger.error(`manualDoseVolumeAsync: ${err.message}`); logger.error(err); return Promise.reject(err); }
792
+ }
793
+ public async initDose(schem: ChemDoserState) {
794
+ try {
795
+ // We need to do a couple of things here. First we should disable the chlorinator.
796
+ if (this.chem.disableChlorinator) {
797
+ let chlors = sys.chlorinators.getByBody(this.chem.body);
798
+ for (let i = 0; i < chlors.length; i++) {
799
+ let chlor = chlors.getItemByIndex(i);
800
+ if (!chlor.disabled) await sys.board.chlorinator.setChlorAsync({ id: chlor.id, disabled: true });
801
+ }
802
+ }
803
+ }
804
+ catch (err) { logger.error(`initDose: ${err.message}`); return Promise.reject(err); }
805
+ }
806
+ }