nodejs-poolcontroller 7.2.0 → 7.5.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 (64) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/Changelog +13 -0
  3. package/Dockerfile +1 -0
  4. package/README.md +5 -5
  5. package/app.ts +11 -0
  6. package/config/Config.ts +3 -0
  7. package/config/VersionCheck.ts +8 -4
  8. package/controller/Constants.ts +165 -9
  9. package/controller/Equipment.ts +186 -65
  10. package/controller/Errors.ts +22 -1
  11. package/controller/State.ts +273 -57
  12. package/controller/boards/EasyTouchBoard.ts +194 -95
  13. package/controller/boards/IntelliCenterBoard.ts +115 -42
  14. package/controller/boards/IntelliTouchBoard.ts +104 -30
  15. package/controller/boards/NixieBoard.ts +155 -53
  16. package/controller/boards/SystemBoard.ts +1529 -514
  17. package/controller/comms/Comms.ts +219 -42
  18. package/controller/comms/messages/Messages.ts +16 -4
  19. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -3
  20. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  21. package/controller/comms/messages/config/CircuitMessage.ts +1 -1
  22. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  23. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  24. package/controller/comms/messages/config/ExternalMessage.ts +43 -25
  25. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  26. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  27. package/controller/comms/messages/config/HeaterMessage.ts +15 -9
  28. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  29. package/controller/comms/messages/config/OptionsMessage.ts +13 -1
  30. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  31. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  32. package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
  33. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  34. package/controller/comms/messages/config/ValveMessage.ts +12 -2
  35. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +14 -6
  36. package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
  37. package/controller/comms/messages/status/HeaterStateMessage.ts +25 -5
  38. package/controller/comms/messages/status/IntelliChemStateMessage.ts +55 -26
  39. package/controller/nixie/Nixie.ts +18 -16
  40. package/controller/nixie/NixieEquipment.ts +6 -6
  41. package/controller/nixie/bodies/Body.ts +7 -4
  42. package/controller/nixie/bodies/Filter.ts +7 -4
  43. package/controller/nixie/chemistry/ChemController.ts +800 -283
  44. package/controller/nixie/chemistry/Chlorinator.ts +22 -14
  45. package/controller/nixie/circuits/Circuit.ts +42 -7
  46. package/controller/nixie/heaters/Heater.ts +303 -30
  47. package/controller/nixie/pumps/Pump.ts +57 -30
  48. package/controller/nixie/schedules/Schedule.ts +10 -7
  49. package/controller/nixie/valves/Valve.ts +7 -5
  50. package/defaultConfig.json +32 -1
  51. package/issue_template.md +1 -1
  52. package/logger/DataLogger.ts +37 -22
  53. package/package.json +20 -18
  54. package/web/Server.ts +529 -31
  55. package/web/bindings/influxDB.json +157 -5
  56. package/web/bindings/mqtt.json +112 -13
  57. package/web/bindings/mqttAlt.json +109 -11
  58. package/web/interfaces/baseInterface.ts +2 -1
  59. package/web/interfaces/httpInterface.ts +2 -0
  60. package/web/interfaces/influxInterface.ts +103 -54
  61. package/web/interfaces/mqttInterface.ts +16 -5
  62. package/web/services/config/Config.ts +179 -43
  63. package/web/services/state/State.ts +51 -5
  64. package/web/services/state/StateSocket.ts +19 -2
@@ -1,14 +1,16 @@
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 { ChemController, Chemical, ChemicalPh, ChemicalORP, ChemicalPhProbe, ChemicalORPProbe, ChemicalTank, ChemicalPump, sys, ChemicalProbe, ChemControllerCollection, ChemFlowSensor } from "../../../controller/Equipment";
7
- import { ChemControllerState, ChemicalState, ChemicalORPState, ChemicalPhState, state, ChemicalProbeState, ChemicalProbePHState, ChemicalProbeORPState, ChemicalTankState, ChemicalPumpState, ChemicalDoseState } from "../../State";
8
- import { setTimeout, clearTimeout } from 'timers';
9
- import { webApp, InterfaceServerResponse } from "../../../web/Server";
1
+ import { clearTimeout, setTimeout } from 'timers';
10
2
  import { conn } from '../../../controller/comms/Comms';
11
3
  import { Outbound, Protocol, Response } from '../../../controller/comms/messages/Messages';
4
+ import { ChemController, ChemControllerCollection, ChemFlowSensor, Chemical, ChemicalChlor, ChemicalORP, ChemicalORPProbe, ChemicalPh, ChemicalPhProbe, ChemicalProbe, 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 { ChemControllerState, ChemicalChlorState, ChemicalDoseState, ChemicalORPState, ChemicalPhState, ChemicalProbeORPState, ChemicalProbePHState, ChemicalProbeState, ChemicalPumpState, ChemicalState, ChemicalTankState, ChlorinatorState, state } from "../../State";
10
+ import { ncp } from '../Nixie';
11
+ import { INixieControlPanel, NixieChildEquipment, NixieEquipment, NixieEquipmentCollection } from "../NixieEquipment";
12
+ import { NixieChlorinator } from './Chlorinator';
13
+
12
14
 
13
15
  export class NixieChemControllerCollection extends NixieEquipmentCollection<NixieChemControllerBase> {
14
16
  public async manualDoseAsync(id: number, data: any) {
@@ -41,7 +43,7 @@ export class NixieChemControllerCollection extends NixieEquipmentCollection<Nixi
41
43
  }
42
44
 
43
45
  public async setControllerAsync(chem: ChemController, data: any) {
44
- // By the time we get here we know that we are in control and this is a REMChem.
46
+ // By the time we get here we know that we are in control and this REM Chem or IntelliChem.
45
47
  try {
46
48
  let ncc: NixieChemControllerBase = this.find(elem => elem.id === chem.id) as NixieChemControllerBase;
47
49
  if (typeof ncc === 'undefined') {
@@ -49,12 +51,13 @@ export class NixieChemControllerCollection extends NixieEquipmentCollection<Nixi
49
51
  ncc = NixieChemControllerBase.create(this.controlPanel, chem);
50
52
  this.push(ncc);
51
53
  let ctype = sys.board.valueMaps.chemControllerTypes.transform(chem.type);
52
- logger.info(`A Chem controller was not found for id #${chem.id} starting ${ctype.desc}`);
54
+ logger.info(`Nixie Chem Controller was created at id #${chem.id} for type ${ctype.desc}`);
53
55
  await ncc.setControllerAsync(data);
54
56
  }
55
57
  else {
56
58
  await ncc.setControllerAsync(data);
57
59
  }
60
+ // Now go back through the array and undo anything that is in need of pruning.
58
61
  }
59
62
  catch (err) { logger.error(`setControllerAsync: ${err.message}`); return Promise.reject(err); }
60
63
  }
@@ -67,13 +70,20 @@ export class NixieChemControllerCollection extends NixieEquipmentCollection<Nixi
67
70
  }
68
71
  public async initAsync(controllers: ChemControllerCollection) {
69
72
  try {
70
- this.length = 0;
71
73
  for (let i = 0; i < controllers.length; i++) {
72
74
  let cc = controllers.getItemByIndex(i);
73
75
  if (cc.master === 1) {
76
+ let type = sys.board.valueMaps.chemControllerTypes.transform(cc.type);
74
77
  logger.info(`Initializing chemController ${cc.name}`);
75
- let ncc = NixieChemControllerBase.create(this.controlPanel, cc);
76
- this.push(ncc);
78
+ // First check to make sure it isnt already there.
79
+ if (typeof this.find(elem => elem.id === cc.id) === 'undefined') {
80
+
81
+ let ncc = NixieChemControllerBase.create(this.controlPanel, cc);
82
+ this.push(ncc);
83
+ }
84
+ else {
85
+ logger.info(`chemController ${cc.name} has already been initialized`);
86
+ }
77
87
  }
78
88
  }
79
89
  }
@@ -86,39 +96,56 @@ export class NixieChemControllerCollection extends NixieEquipmentCollection<Nixi
86
96
  logger.info(`Closing chemController ${this[i].id}`);
87
97
  await this[i].closeAsync();
88
98
  this.splice(i, 1);
89
- } catch (err) { logger.error(`Error stopping Nixie Chem Controller ${err}`); return Promise.reject(err);}
99
+ } catch (err) { logger.error(`Error stopping Nixie Chem Controller ${err}`); return Promise.reject(err); }
90
100
  }
91
101
 
92
102
  } catch (err) { } // Don't bail if we have an error
93
103
  }
94
- // This is currently not used for anything.
95
- public async searchIntelliChem(): Promise<number[]> {
96
- let arr = [];
97
- try {
98
- for (let addr = 144; addr <= 152; addr++) {
99
- let success = await new Promise<boolean>((resolve, reject) => {
100
- let out = Outbound.create({
101
- protocol: Protocol.IntelliChem,
102
- dest: addr,
103
- action: 210,
104
- payload: [210],
105
- retries: 1, // We are going to try 2 times.
106
- response: Response.create({ protocol: Protocol.IntelliChem, action: 18 }),
107
- onAbort: () => { },
108
- onComplete: (err) => {
109
- if (err) resolve(false);
110
- else resolve(true);
111
- }
112
- });
113
- conn.queueSendMessage(out);
114
- });
115
- if (success) arr.push(addr)
104
+ public async deleteChlorAsync(chlor: NixieChlorinator) {
105
+ // if we delete the chlor, make sure it is removed from all REM Chem Controllers
106
+ try {
107
+ for (let i = this.length - 1; i >= 0; i--) {
108
+ try {
109
+ let ncc = this[i] as NixieChemControllerBase;;
110
+ ncc.orp.deleteChlorAsync(chlor);
111
+ } catch (err) { logger.error(`Error deleting chlor from Nixie Chem Controller ${err}`); return Promise.reject(err); }
116
112
  }
117
- } catch (err) { return arr; }
113
+
114
+ }
115
+ catch (err) { logger.error(`ncp.deleteChlorAsync: ${err.message}`); return Promise.reject(err); }
118
116
  }
117
+ // This is currently not used for anything.
118
+ /* public async searchIntelliChem(): Promise<number[]> {
119
+ let arr = [];
120
+ try {
121
+ for (let addr = 144; addr <= 152; addr++) {
122
+ let success = await new Promise<void>((resolve, reject) => {
123
+ let out = Outbound.create({
124
+ protocol: Protocol.IntelliChem,
125
+ dest: addr,
126
+ action: 210,
127
+ payload: [210],
128
+ retries: 1, // We are going to try 2 times.
129
+ response: Response.create({ protocol: Protocol.IntelliChem, action: 18 }),
130
+ onAbort: () => { },
131
+ onComplete: (err) => {
132
+ if (err) resolve(false);
133
+ else resolve(true);
134
+ }
135
+ });
136
+ conn.queueSendMessage(out);
137
+ });
138
+ if (success) arr.push(addr)
139
+ }
140
+ } catch (err) { return arr; }
141
+ } */
119
142
  }
120
143
  export class NixieChemControllerBase extends NixieEquipment {
121
144
  public pollingInterval: number = 10000;
145
+ protected _suspendPolling: number = 0;
146
+ public get suspendPolling(): boolean { return this._suspendPolling > 0; }
147
+ public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
148
+ public _ispolling = false;
122
149
  protected _pollTimer: NodeJS.Timeout = null;
123
150
  protected closing = false;
124
151
  public orp: NixieChemicalORP;
@@ -132,14 +159,12 @@ export class NixieChemControllerBase extends NixieEquipment {
132
159
  this.chem = chem;
133
160
  }
134
161
  public chem: ChemController;
135
- public syncRemoteREMFeeds(servers) {}
136
- public static create(ncp: INixieControlPanel, chem: ChemController): NixieChemControllerBase {
137
- // RKS: 06-25-21 - Keeping the homegrown around for now but I don't really know why we care.
162
+ public syncRemoteREMFeeds(servers) { }
163
+ public static create(ncp: INixieControlPanel, chem: ChemController): NixieChemControllerBase {
138
164
  let type = sys.board.valueMaps.chemControllerTypes.transform(chem.type);
139
165
  switch (type.name) {
140
166
  case 'intellichem':
141
167
  return new NixieIntelliChemController(ncp, chem);
142
- case 'homegrown':
143
168
  case 'rem':
144
169
  return new NixieChemController(ncp, chem);
145
170
  default:
@@ -155,12 +180,9 @@ export class NixieChemControllerBase extends NixieEquipment {
155
180
  else if (!isOn) this.bodyOnTime = undefined;
156
181
  return isOn;
157
182
  }
158
- public async setControllerAsync(data: any) {} // This is meant to be abstract override this value
183
+ public async setControllerAsync(data: any) { } // This is meant to be abstract override this value
159
184
  }
160
185
  export class NixieIntelliChemController extends NixieChemControllerBase {
161
- protected _suspendPolling: number = 0;
162
- public get suspendPolling(): boolean { return this._suspendPolling > 0; }
163
- public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
164
186
  public configSent: boolean = false;
165
187
  constructor(ncp: INixieControlPanel, chem: ChemController) {
166
188
  super(ncp, chem);
@@ -169,6 +191,7 @@ export class NixieIntelliChemController extends NixieChemControllerBase {
169
191
  this.pollEquipmentAsync();
170
192
  }
171
193
  public async pollEquipmentAsync() {
194
+ let self = this;
172
195
  try {
173
196
  this.suspendPolling = true;
174
197
  if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
@@ -181,11 +204,11 @@ export class NixieIntelliChemController extends NixieChemControllerBase {
181
204
  // since most of the time these are attached to the filter relay.
182
205
  if (this.isBodyOn() && !this.closing) {
183
206
  if (!this.configSent) await this.sendConfig(schem);
184
- if(!this.closing) await this.requestStatus(schem);
207
+ if (!this.closing) await this.requestStatus(schem);
185
208
  }
186
209
  }
187
210
  catch (err) { logger.error(`Error polling IntelliChem Controller - ${err}`); return Promise.reject(err); }
188
- finally { this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => { try { await this.pollEquipmentAsync() } catch (err) { return Promise.reject(err); } }, this.pollingInterval || 10000); }
211
+ finally { this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(() => { self.pollEquipmentAsync(); }, this.pollingInterval || 10000); }
189
212
  }
190
213
  public async setControllerAsync(data: any) {
191
214
  try {
@@ -194,7 +217,7 @@ export class NixieIntelliChemController extends NixieChemControllerBase {
194
217
  let address = typeof data.address !== 'undefined' ? parseInt(data.address) : chem.address;
195
218
  let name = typeof data.name !== 'undefined' ? data.name : chem.name || `IntelliChem - ${address - 143}`;
196
219
  let type = sys.board.valueMaps.chemControllerTypes.transformByName('intellichem');
197
- // So now we are down to the nitty gritty setting the data for the REM or Homegrown Chem controller.
220
+ // So now we are down to the nitty gritty setting the data for the REM Chem controller.
198
221
  let calciumHardness = typeof data.calciumHardness !== 'undefined' ? parseInt(data.calciumHardness, 10) : chem.calciumHardness;
199
222
  let cyanuricAcid = typeof data.cyanuricAcid !== 'undefined' ? parseInt(data.cyanuricAcid, 10) : chem.cyanuricAcid;
200
223
  let alkalinity = typeof data.alkalinity !== 'undefined' ? parseInt(data.alkalinity, 10) : chem.alkalinity;
@@ -247,7 +270,7 @@ export class NixieIntelliChemController extends NixieChemControllerBase {
247
270
  chem.borates = borates;
248
271
  chem.body = schem.body = body;
249
272
  chem.type = schem.type = type.val;
250
-
273
+
251
274
  let acidTankLevel = typeof data.ph !== 'undefined' && typeof data.ph.tank !== 'undefined' && typeof data.ph.tank.level !== 'undefined' ? parseInt(data.ph.tank.level, 10) : schem.ph.tank.level;
252
275
  let orpTankLevel = typeof data.orp !== 'undefined' && typeof data.orp.tank !== 'undefined' && typeof data.orp.tank.level !== 'undefined' ? parseInt(data.orp.tank.level, 10) : schem.orp.tank.level;
253
276
  // Copy the data back to the chem object.
@@ -270,7 +293,7 @@ export class NixieIntelliChemController extends NixieChemControllerBase {
270
293
  chem.orp.tolerance.high = orpTolerance.high;
271
294
  chem.ph.setpoint = pHSetpoint;
272
295
  chem.orp.setpoint = orpSetpoint;
273
- chem.siCalcType = siCalcType;
296
+ schem.siCalcType = chem.siCalcType = siCalcType;
274
297
  chem.address = schem.address = address;
275
298
  chem.name = schem.name = name;
276
299
  chem.flowSensor.enabled = false;
@@ -357,10 +380,10 @@ export class NixieIntelliChemController extends NixieChemControllerBase {
357
380
  if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
358
381
  this._pollTimer = null;
359
382
  let schem = state.chemControllers.getItemById(this.chem.id);
360
- if(typeof this.ph !== 'undefined') await this.ph.closeAsync();
383
+ if (typeof this.ph !== 'undefined') await this.ph.closeAsync();
361
384
  if (typeof this.orp !== 'undefined') await this.orp.closeAsync();
362
385
  logger.info(`Closing Chem Controller ${this.chem.name}`);
363
-
386
+
364
387
  }
365
388
  catch (err) { logger.error(`ChemController closeAsync: ${err.message}`); return Promise.reject(err); }
366
389
  }
@@ -382,6 +405,7 @@ export class NixieChemController extends NixieChemControllerBase {
382
405
  // not been set and we have a state obect defined.
383
406
  let cstate = state.chemControllers.find(x => x.id === chem.id);
384
407
  if (cstate && typeof cstate !== 'undefined') {
408
+ console.log(`Converting to v2.x data structure`);
385
409
  if (cstate.ph.dosingStatus === 1) cstate.ph.dosingStatus = 2;
386
410
  else if (cstate.ph.dosingStatus === 2) cstate.ph.dosingStatus = 1;
387
411
  if (cstate.orp.dosingStatus === 1) cstate.orp.dosingStatus = 2;
@@ -391,6 +415,7 @@ export class NixieChemController extends NixieChemControllerBase {
391
415
  }
392
416
  public async manualDoseAsync(data: any) {
393
417
  try {
418
+ this.suspendPolling = true;
394
419
  // Check to see that we are a rem chem.
395
420
  let vol = parseInt(data.volume, 10);
396
421
  if (isNaN(vol)) return Promise.reject(new InvalidEquipmentDataError(`Volume was not supplied for the manual chem dose`, 'chemController', data.volume));
@@ -406,9 +431,11 @@ export class NixieChemController extends NixieChemControllerBase {
406
431
  else if (chemType === 'orp') await this.orp.manualDoseAsync(schem, vol);
407
432
  }
408
433
  catch (err) { logger.error(`manualDoseAsync: ${err.message}`); return Promise.reject(err); }
434
+ finally { this.suspendPolling = false; }
409
435
  }
410
436
  public async manualMixAsync(data: any) {
411
437
  try {
438
+ this.suspendPolling = true;
412
439
  // Check to see that we are a rem chem.
413
440
  let time = 0;
414
441
  if (typeof data.hours !== 'undefined') time += parseInt(data.hours, 10) * 3600;
@@ -427,9 +454,11 @@ export class NixieChemController extends NixieChemControllerBase {
427
454
  else if (chemType === 'orp') await this.orp.mixChemicals(schem, time);
428
455
  }
429
456
  catch (err) { logger.error(`manualMixAsync: ${err.message}`); return Promise.reject(err); }
457
+ finally { this.suspendPolling = false; }
430
458
  }
431
459
  public async cancelDosingAsync(data: any) {
432
460
  try {
461
+ this.suspendPolling = true;
433
462
  // Determine which chemical we are cancelling. This will be ph or orp.
434
463
  let chemType = typeof data.chemType === 'string' ? data.chemType.toLowerCase() : '';
435
464
  if (typeof this[chemType] === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`A valid Chem type was not supplied for the manual chem dose ${data.chemType}`, 'chemController', data.chemType));
@@ -442,25 +471,24 @@ export class NixieChemController extends NixieChemControllerBase {
442
471
  else if (chemType === 'orp') await this.orp.cancelDosing(schem, 'cancelled');
443
472
  }
444
473
  catch (err) { logger.error(`cancelDosingAsync: ${err.message}`); return Promise.reject(err); }
474
+ finally { this.suspendPolling = false; }
445
475
  }
446
476
  public async cancelMixingAsync(data: any) {
447
477
  try {
478
+ this.suspendPolling = true;
448
479
  // Determine which chemical we are cancelling. This will be ph or orp.
449
480
  let chemType = typeof data.chemType === 'string' ? data.chemType.toLowerCase() : '';
450
- if (typeof this[chemType] === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`A valid Chem type was not supplied for mix chemical ${data.chemType}`, 'chemController', data.chemType));
451
- let chem = this.chem[chemType];
452
- if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Could not cancel ${data.chemType} mix config not found.`, 'chemController', data.chemType));
453
- let schem = state.chemControllers.getItemById(this.chem.id, true)[chemType];
454
- if (typeof schem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Could not cancel ${data.chemType} mix state not found.`, 'chemController', data.chemType));
455
- // Now we can tell the chemical to dose.
456
- if (chemType === 'ph') await this.ph.cancelMixing(schem);
457
- else if (chemType === 'orp') await this.orp.cancelMixing(schem);
458
- schem.dosingStatus = sys.board.valueMaps.chemControllerDosingStatus.getValue('monitoring');
481
+ let schem = state.chemControllers.getItemById(this.chem.id);
482
+ if (chemType === 'ph') await this.ph.cancelMixing(schem.ph);
483
+ else if (chemType === 'orp') await this.orp.cancelMixing(schem.orp);
484
+ else return Promise.reject(new InvalidEquipmentDataError(`A valid Chem type was not supplied for chemical ${data.chemType}`, 'chemController', data.chemType));
459
485
  }
460
- catch (err) { logger.error(`cancelDosingAsync: ${err.message}`); return Promise.reject(err); }
486
+ catch (err) { logger.error(`cancelMixingAsync: ${err.message}`); return Promise.reject(err); }
487
+ finally { this.suspendPolling = false; }
461
488
  }
462
489
  public async setControllerAsync(data: any) {
463
490
  try {
491
+ this.suspendPolling = true;
464
492
  let chem = this.chem;
465
493
  // So now we are down to the nitty gritty setting the data for the REM or Homegrown Chem controller.
466
494
  let calciumHardness = typeof data.calciumHardness !== 'undefined' ? parseInt(data.calciumHardness, 10) : chem.calciumHardness;
@@ -488,7 +516,7 @@ export class NixieChemController extends NixieChemControllerBase {
488
516
  if (typeof data.lsiRange.low === 'number') chem.lsiRange.low = data.lsiRange.low;
489
517
  if (typeof data.lsiRange.high === 'number') chem.lsiRange.high = data.lsiRange.high;
490
518
  }
491
- if (typeof data.siCalcType !== 'undefined') chem.siCalcType = data.siCalcType;
519
+ if (typeof data.siCalcType !== 'undefined') schem.siCalcType = chem.siCalcType = data.siCalcType;
492
520
  await this.flowSensor.setSensorAsync(data.flowSensor);
493
521
  // Alright we are down to the equipment items all validation should have been completed by now.
494
522
  // ORP Settings
@@ -498,9 +526,11 @@ export class NixieChemController extends NixieChemControllerBase {
498
526
  await this.processAlarms(schem);
499
527
  }
500
528
  catch (err) { logger.error(`setControllerAsync: ${err.message}`); return Promise.reject(err); }
529
+ finally { this.suspendPolling = false; }
501
530
  }
502
531
  public async checkFlowAsync(schem: ChemControllerState): Promise<boolean> {
503
532
  try {
533
+ this.suspendPolling = true;
504
534
  schem.isBodyOn = this.isBodyOn();
505
535
  // rsg - we were not returning the flow sensor state when the body was off.
506
536
  // first, this would not allow us to retrieve a pressure of 0 to update flowSensor.state
@@ -510,6 +540,7 @@ export class NixieChemController extends NixieChemControllerBase {
510
540
  schem.alarms.flowSensorFault = 0;
511
541
  }
512
542
  else {
543
+ logger.verbose(`Begin getting flow sensor state`);
513
544
  let ret = await this.flowSensor.getState();
514
545
  schem.flowSensor.state = ret.obj.state;
515
546
  // Call out to REM to see if we have flow.
@@ -526,6 +557,7 @@ export class NixieChemController extends NixieChemControllerBase {
526
557
  else if (typeof ret.obj.state === 'boolean') v = ret.obj.state;
527
558
  else if (typeof ret.obj.state === 'number') v = utils.makeBool(ret.obj.state);
528
559
  else if (typeof ret.obj.state.val === 'number') v = utils.makeBool(ret.obj.state.val);
560
+ else if (typeof ret.obj.state.value === 'number') v = utils.makeBool(ret.obj.state.value);
529
561
  else v = false;
530
562
  this.flowDetected = schem.flowDetected = v;
531
563
  }
@@ -541,15 +573,22 @@ export class NixieChemController extends NixieChemControllerBase {
541
573
  }
542
574
  if (!schem.flowDetected) this.bodyOnTime = undefined;
543
575
  else if (typeof this.bodyOnTime === 'undefined') this.bodyOnTime = new Date().getTime();
576
+ logger.verbose(`End getting flow sensor state`);
544
577
  return schem.flowDetected;
545
578
  }
546
- catch (err) { logger.error(`checkFlowAsync: ${err.message}`); schem.alarms.flowSensorFault = 7; this.flowDetected = schem.flowDetected = false; return Promise.reject(err);}
579
+ catch (err) { logger.error(`checkFlowAsync: ${err.message}`); schem.alarms.flowSensorFault = 7; this.flowDetected = schem.flowDetected = false; return Promise.reject(err); }
580
+ finally { this.suspendPolling = false; }
547
581
  }
548
582
  public async pollEquipmentAsync() {
583
+ let self = this;
549
584
  try {
585
+ logger.verbose(`Begin polling Chem Controller ${this.id}`);
586
+ if (this._suspendPolling > 0) logger.warn(`Suspend polling for ${this.chem.name} -> ${this._suspendPolling}`);
587
+ if (this.suspendPolling) return;
588
+ if (this._ispolling) return;
589
+ this._ispolling = true;
550
590
  if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
551
591
  this._pollTimer = null;
552
- let success = false;
553
592
  let schem = state.chemControllers.getItemById(this.chem.id, !this.closing);
554
593
  // We need to check on the equipment to make sure it is solid.
555
594
  if (sys.board.valueMaps.chemControllerTypes.getName(this.chem.type) === 'rem') {
@@ -560,10 +599,8 @@ export class NixieChemController extends NixieChemControllerBase {
560
599
  await this.checkFlowAsync(schem);
561
600
  await this.validateSetupAsync(this.chem, schem);
562
601
  if (this.chem.ph.enabled) await this.ph.probe.setTempCompensationAsync(schem.ph.probe);
563
- // We are not processing Homegrown at this point.
564
602
  // Check each piece of equipment to make sure it is doing its thing.
565
603
  schem.calculateSaturationIndex();
566
- //this.calculateSaturationIndex();
567
604
  this.processAlarms(schem);
568
605
  if (this.chem.ph.enabled) await this.ph.checkDosing(this.chem, schem.ph);
569
606
  if (this.chem.orp.enabled) await this.orp.checkDosing(this.chem, schem.orp);
@@ -571,9 +608,14 @@ export class NixieChemController extends NixieChemControllerBase {
571
608
  else
572
609
  logger.warn('REM Server not Connected');
573
610
  }
611
+ this._ispolling = false;
612
+ }
613
+ catch (err) { this._ispolling = false; logger.error(`Error polling Chem Controller - ${err}`); }
614
+ finally {
615
+ if (!this.closing && !this._ispolling)
616
+ this._pollTimer = setTimeout(() => { self.pollEquipmentAsync(); }, this.pollingInterval || 10000);
617
+ logger.verbose(`End polling Chem Controller ${this.id}`);
574
618
  }
575
- catch (err) { logger.error(`Error polling Chem Controller - ${err}`); return Promise.reject(err);}
576
- finally { if(!this.closing) this._pollTimer = setTimeout(async () => {try {await this.pollEquipmentAsync()} catch (err){return Promise.reject(err);}}, this.pollingInterval || 10000); }
577
619
  }
578
620
  public processAlarms(schem: ChemControllerState) {
579
621
  try {
@@ -588,7 +630,7 @@ export class NixieChemController extends NixieChemControllerBase {
588
630
  }
589
631
  else {
590
632
  // both flow and pressure sensors (type 2 & 4)
591
- if (schem.isBodyOn && schem.flowSensor.state === 0 || !schem.isBodyOn && schem.flowSensor.state > 0) {
633
+ if (schem.isBodyOn && !schem.flowDetected || !schem.isBodyOn && schem.flowDetected) {
592
634
  schem.alarms.flow = 1;
593
635
  }
594
636
  else schem.alarms.flow = 0;
@@ -612,7 +654,7 @@ export class NixieChemController extends NixieChemControllerBase {
612
654
  else schem.alarms.orpTank = 0;
613
655
  // 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.
614
656
  let padj = this.chem.orp.pump.type > 0 && !this.chem.orp.useChlorinator ? (this.chem.orp.pump.ratedFlow / 60) * 3 : 0;
615
- if (this.chem.orp.maxDailyVolume <= schem.orp.dailyVolumeDosed) {
657
+ if (this.chem.orp.maxDailyVolume <= schem.orp.dailyVolumeDosed && !this.chem.orp.useChlorinator) {
616
658
  schem.warnings.orpDailyLimitReached = 4;
617
659
  schem.orp.dailyLimitReached = true;
618
660
  }
@@ -671,6 +713,7 @@ export class NixieChemController extends NixieChemControllerBase {
671
713
  if (chem.lsiRange.enabled) {
672
714
  schem.warnings.waterChemistry = schem.saturationIndex < chem.lsiRange.low ? 1 : schem.saturationIndex > chem.lsiRange.high ? 2 : 0;
673
715
  }
716
+ else schem.warnings.waterChemistry = 0;
674
717
  } catch (err) { logger.error(`Error processing chem controller ${this.chem.name} alarms: ${err.message}`); }
675
718
  }
676
719
  private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
@@ -682,6 +725,7 @@ export class NixieChemController extends NixieChemControllerBase {
682
725
  public async validateSetupAsync(chem: ChemController, schem: ChemControllerState) {
683
726
  try {
684
727
  // The validation will be different if the body is on or not. So lets get that information.
728
+ logger.verbose(`Begin validating ${chem.id} - ${chem.name} setup`);
685
729
  if (chem.orp.enabled) {
686
730
  if (chem.orp.probe.type !== 0) {
687
731
  let type = sys.board.valueMaps.chemORPProbeTypes.transform(chem.orp.probe.type);
@@ -741,8 +785,11 @@ export class NixieChemController extends NixieChemControllerBase {
741
785
  if (chem.body === 2) totalGallons += sys.bodies.getItemById(3).capacity;
742
786
  if (chem.body === 3) totalGallons += sys.bodies.getItemById(4).capacity;
743
787
  schem.alarms.bodyFault = (isNaN(totalGallons) || totalGallons === 0) ? 6 : 0;
788
+ if (schem.alarms.bodyFault !== 0) logger.warn(`Chem controller body calculation invalid ${totalGallons} -> ${chem.body}`);
744
789
  }
745
790
  schem.alarms.comms = 0;
791
+ logger.verbose(`End validating ${chem.id} - ${chem.name} setup`);
792
+
746
793
  } catch (err) { logger.error(`Error checking Chem Controller Hardware ${this.chem.name}: ${err.message}`); schem.alarms.comms = 2; return Promise.reject(err); }
747
794
  }
748
795
  public async closeAsync() {
@@ -768,44 +815,40 @@ export class NixieChemController extends NixieChemControllerBase {
768
815
  class NixieChemical extends NixieChildEquipment {
769
816
  public chemical: Chemical;
770
817
  public pump: NixieChemPump;
818
+ public chlor: NixieChemChlor;
771
819
  public tank: NixieChemTank;
772
820
  public _lastOnStatus: number;
821
+ protected _stoppingMix = false;
822
+ protected _suspendPolling: number = 0;
823
+ public get suspendPolling(): boolean { return this._suspendPolling > 0; }
824
+ public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
825
+ protected _processingMix = false;
773
826
  //public currentDose: ChemicalDoseState;
774
827
  public chemType: string;
775
- public currentMix: NixieChemMix;
828
+ public _currentMix: NixieChemMix;
776
829
  //public doseHistory: NixieChemDoseLog[] = [];
777
830
  protected _mixTimer: NodeJS.Timeout;
778
831
  //public get logFilename() { return `chemDosage_unknown.log`; }
779
832
  public get chemController(): NixieChemController { return this.getParent() as NixieChemController; }
833
+ public get currentMix(): NixieChemMix { return this._currentMix; }
834
+ public set currentMix(val: NixieChemMix) {
835
+ if (typeof val === 'undefined' && typeof this._currentMix !== 'undefined') logger.debug(`${this.chemical.chemType} mix set to undefined`);
836
+ else logger.debug(`Set new current mix ${this.chemical.chemType}`)
837
+ this._currentMix = val;
838
+ }
780
839
  constructor(controller: NixieChemController, chemical: Chemical) {
781
840
  super(controller);
782
841
  chemical.master = 1;
783
842
  this.chemical = chemical;
784
843
  this.pump = new NixieChemPump(this, chemical.pump);
785
844
  this.tank = new NixieChemTank(this, chemical.tank);
786
- // RKS: We no longer need this as the chemicalState functions will take care of it all.
787
- //// Load up the dose history so we can do our 24 hour thingy.
788
- //(async () => {
789
- // let lines = await this.chemController.controlPanel.readLogFile(this.logFilename);
790
- // let dt = new Date().getTime() - 86400000;
791
- // let total = 0;
792
- // for (let i = 0; i < lines.length; i++) {
793
- // try {
794
- // let log = NixieChemDoseLog.fromLog(lines[i]);
795
- // if (log.end.getTime() > dt) {
796
- // this.doseHistory.push(log);
797
- // }
798
- // else break; // The file should be ordered where the latest dose is at the top.
799
- // } catch (err) { logger.error(`read chemController Dose History: ${err.message}`); }
800
- // }
801
- //})();
802
- }
803
- public async cancelMixing(schem: ChemicalState) {
845
+ logger.info(`Nixie Chemical ${chemical.chemType} object created`);
846
+ }
847
+ public async cancelMixing(schem: ChemicalState): Promise<void> {
804
848
  try {
805
- // Just stop the pump for now but we will do some logging later.
849
+ logger.verbose(`Cancelling ${this.chemType} Mix`);
806
850
  await this.stopMixing(schem);
807
- schem.mixTimeRemaining = 0;
808
- } catch (err) { logger.error(`cancelMixing pH: ${err.message}`); return Promise.reject(err); }
851
+ } catch (err) { logger.error(`cancelMixing ${this.chemType}: ${err.message}`); return Promise.reject(err); }
809
852
  }
810
853
  protected async setHardware(chemical: Chemical, data: any) {
811
854
  try {
@@ -813,7 +856,7 @@ class NixieChemical extends NixieChildEquipment {
813
856
  }
814
857
  catch (err) { return Promise.reject(err); }
815
858
  }
816
- protected async setDosing(chemical: Chemical, data: any) {
859
+ protected async setDosing(chemical: Chemical, data: any): Promise<void> {
817
860
  try {
818
861
  if (typeof data !== 'undefined') {
819
862
  chemical.enabled = typeof data.enabled !== 'undefined' ? utils.makeBool(data.enabled) : chemical.enabled;
@@ -830,8 +873,9 @@ class NixieChemical extends NixieChildEquipment {
830
873
  }
831
874
  } catch (err) { logger.error(`setDosing: ${err.message}`); return Promise.reject(err); }
832
875
  }
833
- protected async setMixing(chemical: Chemical, data: any) {
876
+ protected async setMixing(chemical: Chemical, data: any): Promise<void> {
834
877
  try {
878
+ this.suspendPolling = true;
835
879
  if (typeof data !== 'undefined') {
836
880
  if (typeof data.mixingTimeHours !== 'undefined' || typeof data.mixingTimeMinutes !== 'undefined') {
837
881
  data.mixingTime = (typeof data.mixingTimeHours !== 'undefined' ? parseInt(data.mixingTimeHours, 10) * 3600 : 0) +
@@ -842,103 +886,154 @@ class NixieChemical extends NixieChildEquipment {
842
886
  chemical.flowOnlyMixing = typeof data.flowOnlyMixing !== 'undefined' ? utils.makeBool(data.flowOnlyMixing) : chemical.flowOnlyMixing;
843
887
  }
844
888
  } catch (err) { logger.error(`setMixing: ${err.message}`); return Promise.reject(err); }
889
+ finally { this.suspendPolling = false; }
845
890
  }
846
- protected async stopMixing(schem: ChemicalState) {
891
+ protected async stopMixing(schem: ChemicalState): Promise<void> {
847
892
  try {
848
- let chem = this.chemController.chem;
849
- schem.pump.isDosing = false;
850
- if (typeof this._mixTimer !== 'undefined') {
851
- clearTimeout(this._mixTimer);
852
- this._mixTimer = undefined;
893
+ this._stoppingMix = true;
894
+ this.suspendPolling = true;
895
+ if (typeof this.currentMix !== 'undefined') logger.debug(`Stopping ${schem.chemType} mix and clearing the current mix object.`);
896
+ if (typeof this.chemController.orp.orp.useChlorinator !== 'undefined' && this.chemController.orp.orp.useChlorinator && this.chemController.orp.orp.dosingMethod > 0)
897
+ schem.chlor.isDosing = false;
898
+ else
899
+ schem.pump.isDosing = false;
900
+
901
+ if (typeof this.currentMix !== 'undefined' || typeof this._mixTimer !== 'undefined' || this._mixTimer) {
902
+ if (this._mixTimer || typeof this._mixTimer !== 'undefined') {
903
+ clearInterval(this._mixTimer);
904
+ this._mixTimer = undefined;
905
+ logger.verbose(`Cleared ${schem.chemType} mix timer`);
906
+ }
907
+ else
908
+ logger.warn(`${schem.chemType} did not have a mix timer set when cancelling.`);
909
+
910
+ if (typeof this.currentMix !== 'undefined') {
911
+ this.currentMix = undefined;
912
+ logger.verbose(`Cleared ${schem.chemType} mix object`);
913
+ }
914
+ else
915
+ logger.warn(`${schem.chemType} did not have a currentMix object set when cancelling.`);
916
+ schem.dosingStatus = sys.board.valueMaps.chemControllerDosingStatus.getValue('monitoring');
917
+ schem.mixTimeRemaining = 0;
918
+ schem.manualMixing = false;
853
919
  }
854
- schem.mixTimeRemaining = 0;
855
- this.currentMix = undefined;
856
- } catch (err) { logger.error(`Error stopping chemical mix`); return Promise.reject(err);}
920
+ } catch (err) { logger.error(`Error stopping chemical mix`); return Promise.reject(err); }
921
+ finally { this._stoppingMix = false; this.suspendPolling = false; }
857
922
  }
858
- public async mixChemicals(schem: ChemicalState, mixingTime?: number) {
923
+ protected async initMixChemicals(schem: ChemicalState, mixingTime?: number): Promise<void> {
859
924
  try {
860
- let chem = this.chemController.chem;
861
- let flowDetected = this.chemController.flowDetected;
862
- if (typeof this._mixTimer !== 'undefined') {
863
- clearTimeout(this._mixTimer);
864
- this._mixTimer = undefined;
865
- }
866
- let dt = new Date().getTime();
867
- if (typeof mixingTime !== 'undefined') {
868
- // This is a manual mix so we need to make sure the pump is not dosing.
869
- await this.pump.stopDosing(schem, 'completed');
870
- await this.stopMixing(schem);
871
- }
872
- schem.pump.isDosing = false;
925
+ if (this._stoppingMix) return;
873
926
  if (typeof this.currentMix === 'undefined') {
927
+ if (typeof mixingTime !== 'undefined') {
928
+ // This is a manual mix so we need to make sure the pump is not dosing.
929
+ logger.info(`Clearing any possible ${schem.chemType} dosing or existing mix for mixingTime: ${mixingTime}`);
930
+ await this.pump.stopDosing(schem, 'mix override');
931
+ await this.stopMixing(schem);
932
+ }
874
933
  this.currentMix = new NixieChemMix();
875
934
  if (typeof mixingTime !== 'undefined' && !isNaN(mixingTime)) {
876
935
  this.currentMix.set({ time: mixingTime, timeMixed: 0, isManual: true });
936
+ schem.manualMixing = true;
877
937
  }
878
938
  else if (schem.mixTimeRemaining > 0) {
879
- this.currentMix.set({ time: this.chemical.mixingTime, timeMixed: Math.max(0, this.chemical.mixingTime - schem.mixTimeRemaining) });
939
+ if (schem.manualMixing) {
940
+ this.currentMix.set({ time: schem.mixTimeRemaining, timeMixed: 0, isManual: true });
941
+ }
942
+ else
943
+
944
+ this.currentMix.set({ time: this.chemical.mixingTime, timeMixed: Math.max(0, this.chemical.mixingTime - schem.mixTimeRemaining) });
880
945
  }
946
+
881
947
  else
882
948
  this.currentMix.set({ time: this.chemical.mixingTime, timeMixed: 0 });
883
949
  logger.info(`Chem Controller begin mixing ${schem.chemType} for ${utils.formatDuration(this.currentMix.timeRemaining)} of ${utils.formatDuration(this.currentMix.time)}`)
884
- schem.dosingStatus = sys.board.valueMaps.chemControllerDosingStatus.getValue('mixing');
885
- this.currentMix.lastChecked = dt;
950
+ schem.mixTimeRemaining = this.currentMix.timeRemaining;
951
+ }
952
+ if (typeof this._mixTimer === 'undefined' || !this._mixTimer) {
953
+ let self = this;
954
+ this._mixTimer = setInterval(async () => { await self.mixChemicals(schem); }, 1000);
955
+ logger.verbose(`Set ${schem.chemType} mix timer`);
956
+ }
957
+ } catch (err) { logger.error(`Error initializing ${schem.chemType} mix: ${err.message}`); }
958
+ }
959
+ public async mixChemicals(schem: ChemicalState, mixingTime?: number): Promise<void> {
960
+ try {
961
+ if (this._stoppingMix) {
962
+ logger.verbose(`${schem.chemType} is currently stopping mixChemicals ignored.`)
963
+ return;
964
+ }
965
+ if (this._processingMix) {
966
+ logger.verbose(`${schem.chemType} is already processing mixChemicals ignored.`);
967
+ return;
886
968
  }
887
- // rsg - added isBodyOn check because flowDetected will be true if the spa is on but nixie is set to pool only
888
- if ((schem.chemController.isBodyOn && flowDetected) || !this.chemical.flowOnlyMixing) {
969
+ this._processingMix = true;
970
+ let dt = new Date().getTime();
971
+ await this.initMixChemicals(schem, mixingTime);
972
+ if (this._stoppingMix) return;
973
+ schem.chlor.isDosing = schem.pump.isDosing = false;
974
+ if (!this.chemical.flowOnlyMixing || (schem.chemController.isBodyOn && this.chemController.flowDetected)) {
975
+ if (this.chemType === 'orp' && typeof this.chemController.orp.orp.useChlorinator !== 'undefined' && this.chemController.orp.orp.useChlorinator && this.chemController.orp.orp.dosingMethod > 0) {
976
+ if (state.chlorinators.getItemById(1).currentOutput !== 0) {
977
+ logger.debug(`Chem mixing ORP (chlorinator) paused waiting for chlor current output to be 0%. Mix time remaining: ${utils.formatDuration(schem.mixTimeRemaining)} `);
978
+ return;
979
+ }
980
+ }
889
981
  this.currentMix.timeMixed += Math.round((dt - this.currentMix.lastChecked) / 1000);
890
982
  // Reflect any changes to the configuration.
891
- if (!this.currentMix.isManual) this.currentMix.time = this.chemical.mixingTime;
892
- schem.mixTimeRemaining = this.currentMix.timeRemaining;
983
+ if (!this.currentMix.isManual) { this.currentMix.time = this.chemical.mixingTime; }
984
+ schem.mixTimeRemaining = Math.round(this.currentMix.timeRemaining);
893
985
  logger.verbose(`Chem mixing ${schem.chemType} remaining: ${utils.formatDuration(schem.mixTimeRemaining)}`);
894
986
  }
895
- else {
896
- logger.verbose(`Chem mixing paused because body is not on.`);
897
- }
987
+ else
988
+ logger.verbose(`Chem ${schem.chemType} mixing paused because body is not on.`);
898
989
  this.currentMix.lastChecked = dt;
899
- if (schem.mixTimeRemaining === 0) {
900
- logger.info(`Chem Controller ${schem.chemType} mixing Complete after ${utils.formatDuration(this.currentMix.timeMixed)}`)
901
- schem.dosingStatus = sys.board.valueMaps.chemControllerDosingStatus.getValue('monitoring');
902
- this.currentMix = undefined;
990
+ if (schem.mixTimeRemaining <= 0) {
991
+ logger.info(`Chem Controller ${schem.chemType} mixing Complete after ${utils.formatDuration(this.currentMix.timeMixed)}`);
992
+ await this.stopMixing(schem);
903
993
  }
904
- else { schem.dosingStatus = sys.board.valueMaps.chemControllerDosingStatus.getValue('mixing'); }
905
- //state.emitEquipmentChanges();
906
- schem.chemController.emitEquipmentChange();
907
- } catch (err) { logger.error(`Error mixing chemicals.`) }
908
- finally { if (schem.mixTimeRemaining > 0) this._mixTimer = setTimeout(() => { this.mixChemicals(schem); }, 1000); }
994
+ else {
995
+ schem.dosingStatus = sys.board.valueMaps.chemControllerDosingStatus.getValue('mixing');
996
+ }
997
+ } catch (err) { logger.error(`Error mixing chemicals: ${err.message}`); }
998
+ finally {
999
+ this._processingMix = false;
1000
+ setImmediate(() => {
1001
+ schem.chemController.emitEquipmentChange();
1002
+ });
1003
+ }
909
1004
  }
910
1005
  public async initDose(schem: ChemicalState) { }
911
1006
  public async closeAsync() {
912
1007
  try {
913
- if (typeof this._mixTimer !== 'undefined') clearTimeout(this._mixTimer);
1008
+ // We are only killing the mix timer here so when njsPC is restarted it picks up where
1009
+ // it left off with mixing.
1010
+ if (typeof this._mixTimer !== 'undefined') clearInterval(this._mixTimer);
914
1011
  this._mixTimer = undefined;
915
1012
  await super.closeAsync();
916
1013
  }
917
- catch (err) { logger.error(`chemController closeAsync ${err.message}`); return Promise.reject(err);}
1014
+ catch (err) { logger.error(`chemController closeAsync ${err.message}`); return Promise.reject(err); }
918
1015
  }
919
- public async cancelDosing(schem: ChemicalState, reason: string) {
1016
+ public async cancelDosing(schem: ChemicalState, reason: string): Promise<void> {
920
1017
  try {
921
- // Just stop the pump for now but we will do some logging later.
922
- await this.pump.stopDosing(schem, reason);
923
- if (schem.dosingStatus === 0)
924
- await this.mixChemicals(schem);
1018
+ if (typeof this.chemController.orp.orp.useChlorinator !== 'undefined' && this.chemController.orp.orp.useChlorinator && this.chemController.orp.orp.dosingMethod > 0) {
1019
+ if (!this.chlor.chlor.superChlor) await this.chlor.stopDosing(schem, reason);
1020
+ // for chlor, we want 15 minute intervals
1021
+ if (schem.doseHistory.length) {
1022
+ // if last dose was within 15 minutes, set mix time to 15 mins-lastdose
1023
+ // if no dose in last 15, then we should be monitoring
1024
+ let lastDoseTime = schem.doseHistory[0].timeDosed;
1025
+ let mixTime = Math.min(Math.max(this.chlor.chlorInterval * 60 - lastDoseTime, 0), this.chlor.chlorInterval * 60);
1026
+ if (schem.dosingStatus === 0) await this.mixChemicals(schem, mixTime);
1027
+ }
1028
+ else
1029
+ if (schem.dosingStatus === 0) await this.mixChemicals(schem);
1030
+ }
1031
+ else {
1032
+ // Just stop the pump for now but we will do some logging later.
1033
+ if (schem.dosingStatus === 0) await this.mixChemicals(schem);
1034
+ }
925
1035
  } catch (err) { logger.error(`cancelDosing: ${err.message}`); return Promise.reject(err); }
926
1036
  }
927
- //public calcTotalDosed(hours: number, trim: boolean = false): number {
928
- // let total = 0;
929
- // let dt = new Date().getTime() - (hours * 3600000);
930
- // for (let i = this.doseHistory.length - 1; i >= 0; i--) {
931
- // let log = this.doseHistory[i];
932
- // if (log.end.getTime() > dt) total += log.volumeDosed;
933
- // else if (trim) {
934
- // this.doseHistory.splice(i, 1);
935
- // }
936
- // }
937
- // if (typeof this.currentDose !== 'undefined' && this.currentDose.volumeRemaining > 0 && this.currentDose.timeRemaining > 0) {
938
- // total += this.currentDose.volumeDosed;
939
- // }
940
- // return Math.round(total);
941
- //}
942
1037
  }
943
1038
  export class NixieChemTank extends NixieChildEquipment {
944
1039
  public tank: ChemicalTank;
@@ -982,7 +1077,7 @@ export class NixieChemPump extends NixieChildEquipment {
982
1077
  private _isStopping = false;
983
1078
  constructor(chemical: NixieChemical, pump: ChemicalPump) { super(chemical); this.pump = pump; }
984
1079
  public get chemical(): NixieChemical { return this.getParent() as NixieChemical; }
985
- public async setPumpAsync(spump: ChemicalPumpState, data: any) {
1080
+ public async setPumpAsync(spump: ChemicalPumpState, data: any): Promise<void> {
986
1081
  try {
987
1082
  if (typeof data !== 'undefined') {
988
1083
  this.pump.enabled = typeof data.enabled !== 'undefined' ? data.enabled : this.pump.enabled;
@@ -994,8 +1089,9 @@ export class NixieChemPump extends NixieChildEquipment {
994
1089
  } catch (err) { logger.error(`setPumpAsync: ${err.message}`); return Promise.reject(err); }
995
1090
 
996
1091
  }
997
- public async stopDosing(schem: ChemicalState, reason: string) {
1092
+ public async stopDosing(schem: ChemicalState, reason: string): Promise<void> {
998
1093
  try {
1094
+ logger.debug(`Stopping ${schem.chemType} pump: ${reason}`);
999
1095
  if (this._dosingTimer) {
1000
1096
  clearTimeout(this._dosingTimer);
1001
1097
  this._dosingTimer = undefined;
@@ -1006,20 +1102,20 @@ export class NixieChemPump extends NixieChildEquipment {
1006
1102
  }
1007
1103
  this._isStopping = true;
1008
1104
  let dose = schem.currentDose;
1009
- //let dose = this.chemical.currentDose;
1010
1105
  if (this.pump.type !== 0) await this.turnOff(schem);
1011
1106
  if (typeof dose !== 'undefined') {
1012
- //dose.log(this.chemical);
1013
- schem.endDose();
1107
+ schem.endDose(new Date(), reason);
1014
1108
  schem.manualDosing = false;
1015
1109
  schem.dosingTimeRemaining = 0;
1016
1110
  schem.dosingVolumeRemaining = 0;
1017
1111
  schem.volumeDosed = 0;
1112
+ schem.timeDosed = 0;
1018
1113
  }
1019
1114
  } catch (err) { logger.error(`Error stopping ${schem.chemType} dosing: ${err.message}`); return Promise.reject(err); }
1020
1115
  finally { this._isStopping = false; }
1021
1116
  }
1022
- public async dose(schem: ChemicalState) {
1117
+ public async dose(schem: ChemicalState): Promise<void> {
1118
+ let self = this;
1023
1119
  let dose: ChemicalDoseState = schem.currentDose;
1024
1120
  try {
1025
1121
  if (this._dosingTimer) {
@@ -1128,10 +1224,11 @@ export class NixieChemPump extends NixieChildEquipment {
1128
1224
  await this.chemical.cancelDosing(schem, 'empty tank');
1129
1225
  }
1130
1226
  //dosage.schem.dosingStatus = status;
1227
+ return;
1131
1228
  } catch (err) {
1132
1229
  // If we have an error then we want to clear the latch time. Theoretically we could add 3 seconds of latch time but who knows when the failure
1133
1230
  // actually occurred.
1134
- if(typeof dose !== 'undefined') dose._lastLatch = undefined;
1231
+ if (typeof dose !== 'undefined') dose._lastLatch = undefined;
1135
1232
  logger.error(`chemController.pump dose: ${err.message}`);
1136
1233
  return Promise.reject(err);
1137
1234
  }
@@ -1140,14 +1237,24 @@ export class NixieChemPump extends NixieChildEquipment {
1140
1237
  // Add a check to tell the chem when we are done.
1141
1238
  if (schem.dosingStatus === 0) {
1142
1239
  this._dosingTimer = setTimeout(async () => {
1143
- try { await this.dose(schem); }
1144
- catch (err) { logger.error(err); return Promise.reject(err);}
1240
+ try { await self.dose(schem); }
1241
+ catch (err) {
1242
+ logger.error(`self.dose error in finally:`);
1243
+ logger.error(err);
1244
+ //return Promise.reject(err); // this isn't a promise we should be returning
1245
+ }
1145
1246
  }, 1000);
1146
1247
  }
1147
1248
  else {
1148
1249
  // Tell whichever chemical we are dealing with to begin mixing.
1149
1250
  if (typeof dose !== 'undefined') {
1150
- await this.chemical.cancelDosing(schem, 'completed');
1251
+ try {
1252
+ await this.chemical.cancelDosing(schem, 'completed');
1253
+ }
1254
+ catch (err) {
1255
+ logger.error(`this.chemical.cancelDosing error in finally:`);
1256
+ logger.error(err);
1257
+ }
1151
1258
  schem.pump.isDosing = this.isOn = false;
1152
1259
  schem.manualDosing = false;
1153
1260
  }
@@ -1169,12 +1276,190 @@ export class NixieChemPump extends NixieChildEquipment {
1169
1276
  public async turnOn(schem: ChemicalState, latchTimeout?: number): Promise<InterfaceServerResponse> {
1170
1277
  try {
1171
1278
  let res = await NixieEquipment.putDeviceService(this.pump.connectionId, `/state/device/${this.pump.deviceBinding}`, typeof latchTimeout !== 'undefined' ? { isOn: true, latch: latchTimeout } : { isOn: true });
1172
- this.isOn = schem.pump.isDosing = false;
1279
+ this.isOn = schem.pump.isDosing = true;
1173
1280
  return res;
1174
1281
  }
1175
1282
  catch (err) { logger.error(`chemController.pump.turnOn: ${err.message}`); return Promise.reject(err); }
1176
1283
  }
1177
1284
  }
1285
+ export class NixieChemChlor extends NixieChildEquipment {
1286
+ public chlor: ChemicalChlor;
1287
+ public isOn: boolean;
1288
+ public _lastOnStatus: number;
1289
+ protected _dosingTimer: NodeJS.Timeout;
1290
+ private _isStopping = false;
1291
+ public chlorInterval = 15;
1292
+ constructor(chemical: NixieChemical, chlor: ChemicalChlor) { super(chemical); this.chlor = chlor; }
1293
+ public get chemical(): NixieChemical { return this.getParent() as NixieChemical; }
1294
+ public async setChlorAsync(schlor: ChemicalChlorState, data: any) {
1295
+ try {
1296
+ if (typeof data.chlorDosingMethod !== 'undefined' && data.chlorDosingMethod === 0) {
1297
+ if (schlor.chemical.dosingStatus === 0) { await this.chemical.cancelDosing(schlor.chemController.orp, 'dosing method changed'); }
1298
+ if (schlor.chemical.dosingStatus === 1) { await this.chemical.cancelMixing(schlor.chemController.orp); }
1299
+ let chlor = sys.chlorinators.getItemById(1);
1300
+ chlor.disabled = false;
1301
+ chlor.isDosing = false;
1302
+ }
1303
+ } catch (err) { logger.error(`setChlorAsync: ${err.message}`); return Promise.reject(err); }
1304
+ }
1305
+ public async stopDosing(schem: ChemicalState, reason: string): Promise<void> {
1306
+ try {
1307
+ if (this._dosingTimer) {
1308
+ clearTimeout(this._dosingTimer);
1309
+ this._dosingTimer = undefined;
1310
+ }
1311
+ if (this._isStopping) {
1312
+ logger.warn('Trying to stop dosing chlor but it has not yet responded.');
1313
+ return Promise.reject(new EquipmentTimeoutError(`Already trying to stop chlor dosing.`, `chlorStopDosingInProgress`));
1314
+ // return false; // We have to semaphore here just in case the chlor is not stopping as we would like.
1315
+ }
1316
+ logger.debug(`Stopping chlorinating: ${reason}`);
1317
+ this._isStopping = true;
1318
+ let dose = schem.currentDose;
1319
+ await this.turnOff(schem);
1320
+ if (typeof dose !== 'undefined') {
1321
+ schem.endDose();
1322
+ schem.manualDosing = false;
1323
+ schem.dosingTimeRemaining = 0;
1324
+ schem.dosingVolumeRemaining = 0;
1325
+ schem.volumeDosed = 0;
1326
+ }
1327
+ } catch (err) { logger.error(`Error stopping ${schem.chemType} dosing: ${err.message}`); return Promise.reject(err); }
1328
+ finally { this._isStopping = false; }
1329
+ }
1330
+ public async dose(schem: ChemicalState): Promise<void> {
1331
+ let self = this;
1332
+ let dose: ChemicalDoseState = schem.currentDose;
1333
+ try {
1334
+ if (this._dosingTimer) {
1335
+ clearTimeout(this._dosingTimer);
1336
+ this._dosingTimer = undefined;
1337
+ }
1338
+ if (typeof dose === 'undefined') {
1339
+ await this.chemical.cancelDosing(schem, 'undefined dose');
1340
+ return;
1341
+ }
1342
+ if (this.chlor.ratedLbs === 0) {
1343
+ // We aren't going to do anything.
1344
+ logger.verbose(`Chem dose ignore chlor because it doesn't have a dosing rating.`);
1345
+ }
1346
+ else {
1347
+ await this.chemical.chemController.processAlarms(schem.chemController);
1348
+ let isBodyOn = schem.chemController.flowDetected;
1349
+ await this.chemical.initDose(schem);
1350
+ let chemController = schem.getParent()
1351
+ let schlor = state.chlorinators.getItemById(1);
1352
+ if (!isBodyOn) {
1353
+ // Make sure the chlor is off.
1354
+ logger.info(`Chem chlor flow not detected. Body is not running.`);
1355
+ await this.chemical.cancelDosing(schem, 'no flow');
1356
+ }
1357
+
1358
+ else if (chemController.ph.enabled && chemController.ph.pump.isDosing && chemController.ph.dosePriority) {
1359
+ // If ph has dose priority and is dosing, we shouldn't be continuing here
1360
+ let chem = sys.chemControllers.getItemById(chemController.id, false);
1361
+ if (chem.ph.dosePriority)
1362
+ await this.chemical.cancelDosing(schem, 'ph dose priority');
1363
+ }
1364
+ else if (this.chlor.superChlor) {
1365
+ // if superchlor is active, it may be to boost the ORP and we should respect that
1366
+ await this.chemical.cancelDosing(schem, 'superchlor');
1367
+ }
1368
+ else if (dose.timeRemaining <= 0 || dose.volumeRemaining <= 0) {
1369
+ logger.info(`Dose completed ${dose.volumeDosed}lbs ${dose.timeRemaining} ${dose.volumeRemaining}`);
1370
+ await this.chemical.cancelDosing(schem, 'completed');
1371
+ }
1372
+ else if (dose.timeRemaining > 0 && dose.volumeRemaining > 0) { // We are actually dosing here
1373
+ try {
1374
+ await this.turnOn(schem);
1375
+ if (schlor.currentOutput !== 100) {
1376
+ logger.warn(`Chlor dose not added because current output is not 100%`);
1377
+ }
1378
+ else {
1379
+ if (typeof dose._lastLatch !== 'undefined') {
1380
+ let time = new Date().getTime() - (dose._lastLatch || new Date().getTime());
1381
+ let vol = this.chlor.ratedLbs * time / 1000;
1382
+ schem.appendDose(vol, time);
1383
+ }
1384
+ logger.info(`Chem Controller ${dose.chem} chlorinated ${Math.round(dose.volumeDosed * 1000000) / 1000000}lbs of ${Math.round(dose.volume * 1000000) / 1000000}lbs - ${utils.formatDuration(dose.timeRemaining)} remaining`);
1385
+ dose._lastLatch = new Date().getTime();
1386
+ }
1387
+ }
1388
+ catch (err) {
1389
+ logger.error(`Error starting chlorination: ${err}.`)
1390
+ }
1391
+ // if we don't reach the chlorinator, we still want to be in dosing status
1392
+ schem.dosingStatus = 0;
1393
+ }
1394
+ else {
1395
+ await this.chemical.cancelDosing(schem, 'unknown cancel');
1396
+ }
1397
+ return;
1398
+ }
1399
+ } catch (err) {
1400
+ logger.error(`chemController.chlor dose: ${err.message}`);
1401
+ return Promise.reject(err);
1402
+ }
1403
+ finally {
1404
+ schem.chemController.emitEquipmentChange();
1405
+ // Add a check to tell the chem when we are done.
1406
+ if (schem.dosingStatus === 0) {
1407
+ this._dosingTimer = setTimeout(async () => {
1408
+ try { await self.dose(schem); }
1409
+ catch (err) {
1410
+ logger.error(err);
1411
+ // return Promise.reject(err); // should not be returning a promise in a finally
1412
+ }
1413
+ }, 1000);
1414
+ }
1415
+ else {
1416
+ // Tell whichever chemical we are dealing with to begin mixing.
1417
+ if (typeof dose !== 'undefined') {
1418
+ await this.chemical.cancelDosing(schem, 'completed');
1419
+ schem.chlor.isDosing = this.isOn = false;
1420
+ // schem.manualDosing = false;
1421
+ }
1422
+ }
1423
+ }
1424
+ }
1425
+ public async turnOff(schem: ChemicalState): Promise<ChlorinatorState> {
1426
+ try {
1427
+ logger.info(`Turning off the chlorinator`);
1428
+ let chlor = sys.chlorinators.getItemById(1);
1429
+ let schlor = state.chlorinators.getItemById(1);
1430
+ if (schlor.currentOutput === 0 && schlor.targetOutput === 0 && !schlor.superChlor && chlor.disabled && !chlor.isDosing) {
1431
+ this.isOn = schem.chlor.isDosing = false;
1432
+ return schlor;
1433
+ }
1434
+ let cstate = await sys.board.chlorinator.setChlorAsync({
1435
+ id: 1,
1436
+ disabled: true,
1437
+ isDosing: false
1438
+ })
1439
+ this.isOn = schem.chlor.isDosing = false;
1440
+ return cstate;
1441
+ }
1442
+ catch (err) { logger.error(`chemController.chlor.turnOff: ${err.message}`); return Promise.reject(err); }
1443
+ }
1444
+ public async turnOn(schem: ChemicalState, latchTimeout?: number): Promise<ChlorinatorState> {
1445
+ try {
1446
+ let chlor = sys.chlorinators.getItemById(1);
1447
+ let schlor = state.chlorinators.getItemById(1);
1448
+ if (schlor.currentOutput === 100 && schlor.targetOutput === 100 && !schlor.superChlor && !chlor.disabled && chlor.isDosing) {
1449
+ this.isOn = schem.chlor.isDosing = true;
1450
+ return schlor;
1451
+ }
1452
+ let cstate = await sys.board.chlorinator.setChlorAsync({
1453
+ id: 1,
1454
+ disabled: false,
1455
+ isDosing: true
1456
+ })
1457
+ this.isOn = schem.chlor.isDosing = true;
1458
+ return cstate;
1459
+ }
1460
+ catch (err) { logger.error(`chemController.chlor.turnOn: ${err.message}`); return Promise.reject(err); }
1461
+ }
1462
+ }
1178
1463
  export class NixieChemicalPh extends NixieChemical {
1179
1464
  public get ph(): ChemicalPh { return this.chemical as ChemicalPh; }
1180
1465
  public probe: NixieChemProbePh;
@@ -1221,10 +1506,10 @@ export class NixieChemicalPh extends NixieChemical {
1221
1506
  let sorp = sph.chemController.orp;
1222
1507
  for (let i = 0; i < chlors.length; i++) {
1223
1508
  let chlor = chlors.getItemByIndex(i);
1224
- if (!chlor.disabled) await sys.board.chlorinator.setChlorAsync({ id: chlor.id, disabled: true });
1509
+ if (!chlor.disabled) await sys.board.chlorinator.setChlorAsync({ id: chlor.id, disabled: true, isDosing: false });
1225
1510
  }
1226
1511
  // If we are currently dosing ORP then we need to stop that because pH is currently dosing.
1227
- if (sorp.pump.isDosing) await this.chemController.orp.cancelDosing(sorp, 'pH priority');
1512
+ if (sorp.pump.isDosing || sorp.chlor.isDosing) await this.chemController.orp.cancelDosing(sorp, 'pH priority');
1228
1513
  }
1229
1514
  this.ph.dosePriority = b;
1230
1515
  }
@@ -1234,45 +1519,12 @@ export class NixieChemicalPh extends NixieChemical {
1234
1519
  }
1235
1520
  catch (err) { logger.error(`chemController setPhAysnc.: ${err.message}`); return Promise.reject(err); }
1236
1521
  }
1237
- //public calcDemand(sph: ChemicalPhState): number {
1238
- // let chem = this.chemController.chem;
1239
- // // Calculate how many mL are required to raise to our pH level.
1240
- // // 1. Get the total gallons of water that the chem controller is in
1241
- // // control of.
1242
- // let totalGallons = 0;
1243
-
1244
- // if (chem.body === 0 || chem.body === 32 || sys.equipment.shared) totalGallons += sys.bodies.getItemById(1).capacity;
1245
- // if (chem.body === 1 || chem.body === 32 || sys.equipment.shared) totalGallons += sys.bodies.getItemById(2).capacity;
1246
- // if (chem.body === 2) totalGallons += sys.bodies.getItemById(3).capacity;
1247
- // if (chem.body === 3) totalGallons += sys.bodies.getItemById(4).capacity;
1248
- // logger.verbose(`Chem begin calculating demand: ${sph.level} setpoint: ${this.ph.setpoint} body: ${totalGallons}`);
1249
- // let chg = this.ph.setpoint - sph.level;
1250
- // let delta = chg * totalGallons;
1251
- // let temp = (sph.level + this.ph.setpoint) / 2;
1252
- // let adj = (192.1626 + -60.1221 * temp + 6.0752 * temp * temp + -0.1943 * temp * temp * temp) * (chem.alkalinity + 13.91) / 114.6;
1253
- // let extra = (-5.476259 + 2.414292 * temp + -0.355882 * temp * temp + 0.01755 * temp * temp * temp) * (chem.borates || 0);
1254
- // extra *= delta;
1255
- // delta *= adj;
1256
- // let dose = 0;
1257
- // if (this.ph.phSupply === 0) { // We are dispensing base so we need to calculate the demand here.
1258
- // if (chg > 0) {
1259
-
1260
- // }
1261
- // }
1262
- // else {
1263
- // if (chg < 0) {
1264
- // let at = sys.board.valueMaps.acidTypes.transform(this.ph.acidType);
1265
- // dose = Math.round(utils.convert.volume.convertUnits((delta / -240.15 * at.dosingFactor) + (extra / -240.15 * at.dosingFactor), 'oz', 'mL'));
1266
- // }
1267
- // }
1268
- // sph.demand = dose;
1269
- // return dose;
1270
- //}
1271
1522
  public async checkDosing(chem: ChemController, sph: ChemicalPhState) {
1272
1523
  try {
1273
1524
  let status = sys.board.valueMaps.chemControllerDosingStatus.getName(sph.dosingStatus);
1525
+ logger.debug(`Begin check ${sph.chemType} dosing status = ${status}`);
1274
1526
  let demand = sph.calcDemand(chem);
1275
- //let demand = this.calcDemand(sph);
1527
+ sph.demand = Math.max(demand, 0);
1276
1528
  if (sph.suspendDosing) {
1277
1529
  // Kill off the dosing and make sure the pump isn't running. Let's force the issue here.
1278
1530
  await this.cancelDosing(sph, 'suspended');
@@ -1283,17 +1535,37 @@ export class NixieChemicalPh extends NixieChemical {
1283
1535
  // let the system clean these up.
1284
1536
  if (typeof sph.currentDose !== 'undefined') logger.error('Somehow we made it to monitoring and still have a current dose');
1285
1537
  sph.currentDose = undefined;
1286
- this.currentMix = undefined;
1287
1538
  sph.manualDosing = false;
1288
- sph.mixTimeRemaining = 0;
1289
1539
  sph.dosingVolumeRemaining = 0;
1290
1540
  sph.dosingTimeRemaining = 0;
1291
- await this.stopMixing(sph);
1292
- await this.cancelDosing(sph, 'completed');
1541
+ if (typeof this.currentMix !== 'undefined') {
1542
+ if (ncp.chemControllers.length > 1) {
1543
+ let arrIds = [];
1544
+ for (let i = 0; i < ncp.chemControllers.length; i++) {
1545
+ arrIds.push(ncp[i].id);
1546
+ }
1547
+ logger.info(`More than one NixieChemController object was found ${JSON.stringify(arrIds)}`);
1548
+ }
1549
+ logger.debug(`We are now monitoring and have a mixing object`);
1550
+ await this.stopMixing(sph);
1551
+ }
1552
+ await this.cancelDosing(sph, 'monitoring');
1293
1553
  }
1294
1554
  if (status === 'mixing') {
1295
- await this.cancelDosing(sph, 'completed');
1296
- await this.mixChemicals(sph);
1555
+ await this.cancelDosing(sph, 'mixing');
1556
+ if (typeof this.currentMix === 'undefined') {
1557
+ // First lets check to see how many chem controllers we have.
1558
+ // RKS: Keep this case around in case there is another Moby Dick and Nixie has an orphan out there.
1559
+ //if (ncp.chemControllers.length > 1) {
1560
+ // let arrIds = [];
1561
+ // for (let i = 0; i < ncp.chemControllers.length; i++) {
1562
+ // arrIds.push(ncp[i].id);
1563
+ // }
1564
+ // logger.info(`More than one NixieChemController object was found ${JSON.stringify(arrIds)}`);
1565
+ //}
1566
+ logger.info(`Current ${sph.chemType} mix object not defined initializing mix`);
1567
+ await this.mixChemicals(sph);
1568
+ }
1297
1569
  }
1298
1570
  else if (sph.manualDosing) {
1299
1571
  // We are manually dosing. We are not going to dynamically change the dose.
@@ -1302,7 +1574,6 @@ export class NixieChemicalPh extends NixieChemical {
1302
1574
  // Unfortunately we will lose the original start date but who cares as the volumes should remain the same.
1303
1575
  let volume = sph.volumeDosed + sph.dosingVolumeRemaining;
1304
1576
  let time = sph.timeDosed + sph.dosingTimeRemaining;
1305
- sph.demand = sph.calcDemand(this.chemController.chem);
1306
1577
  sph.startDose(new Timestamp().addSeconds(-sph.doseTime).toDate(), 'manual', volume, sph.dosingVolumeRemaining, time * 1000, sph.doseTime * 1000);
1307
1578
  }
1308
1579
  if (sph.tank.level > 0) {
@@ -1360,7 +1631,6 @@ export class NixieChemicalPh extends NixieChemical {
1360
1631
  break;
1361
1632
  }
1362
1633
  logger.verbose(`Chem acid dosing maximums applied ${dose}mL for ${utils.formatDuration(time)}`);
1363
- sph.demand = demand;
1364
1634
  if (typeof sph.currentDose === 'undefined' && sph.tank.level > 0) {
1365
1635
  // We will include this with the dose demand because our limits may reduce it.
1366
1636
  //dosage.demand = demand;
@@ -1386,10 +1656,12 @@ export class NixieChemicalPh extends NixieChemical {
1386
1656
  await this.cancelDosing(sph, 'empty tank');
1387
1657
  }
1388
1658
  }
1389
- return true;
1390
1659
  }
1391
1660
  }
1392
- catch (err) { logger.error(err); return Promise.reject(err);}
1661
+ catch (err) { logger.error(err); return Promise.reject(err); }
1662
+ finally {
1663
+ logger.debug(`End check ${sph.chemType} dosing status = ${sys.board.valueMaps.chemControllerDosingStatus.getName(sph.dosingStatus)}`);
1664
+ }
1393
1665
  }
1394
1666
  public async cancelDosing(sph: ChemicalPhState, reason: string) {
1395
1667
  try {
@@ -1406,11 +1678,12 @@ export class NixieChemicalPh extends NixieChemical {
1406
1678
  }
1407
1679
  }
1408
1680
  }
1409
- if(typeof sph.currentDose !== 'undefined') sph.endDose(new Date(), 'cancelled');
1681
+ if (typeof sph.currentDose !== 'undefined') sph.endDose(new Date(), 'cancelled');
1410
1682
  } catch (err) { logger.error(`cancelDosing pH: ${err.message}`); return Promise.reject(err); }
1411
1683
  }
1412
1684
  public async manualDoseAsync(sph: ChemicalPhState, volume: number) {
1413
1685
  try {
1686
+ logger.debug(`Starting manual ${sph.chemType} dose of ${volume}mL`);
1414
1687
  let status = sys.board.valueMaps.chemControllerDosingStatus.getName(sph.dosingStatus);
1415
1688
  if (status === 'monitoring') {
1416
1689
  // Alright our mixing and dosing have either been cancelled or we fininsed a mixing cycle. Either way
@@ -1439,7 +1712,7 @@ export class NixieChemicalPh extends NixieChemical {
1439
1712
  await this.pump.dose(sph);
1440
1713
  }
1441
1714
  }
1442
- catch (err) { logger.error(`manualDoseAsync: ${err.message}`); logger.error(err); return Promise.reject(err);}
1715
+ catch (err) { logger.error(`manualDoseAsync: ${err.message}`); logger.error(err); return Promise.reject(err); }
1443
1716
  }
1444
1717
  public async initDose(sph: ChemicalPhState) {
1445
1718
  try {
@@ -1466,21 +1739,24 @@ export class NixieChemicalORP extends NixieChemical {
1466
1739
  this.chemType = 'orp';
1467
1740
  this.orp = chemical;
1468
1741
  this.probe = new NixieChemProbeORP(this, chemical.probe);
1742
+ this.chlor = new NixieChemChlor(this, chemical.chlor);
1469
1743
  }
1470
1744
  public get logFilename() { return `chemDosage_orp.log`; }
1471
1745
  public async setORPAsync(sorp: ChemicalORPState, data: any) {
1472
1746
  try {
1473
1747
  if (typeof data !== 'undefined') {
1474
- this.orp.useChlorinator = typeof data.useChlorinator !== 'undefined' ? utils.makeBool(data.useChlorinator) : this.orp.useChlorinator;
1748
+ sorp.useChlorinator = this.orp.useChlorinator = typeof data.useChlorinator !== 'undefined' ? utils.makeBool(data.useChlorinator) : this.orp.useChlorinator;
1475
1749
  sorp.enabled = this.orp.enabled = typeof data.enabled !== 'undefined' ? utils.makeBool(data.enabled) : this.orp.enabled;
1476
1750
  sorp.level = typeof data.level !== 'undefined' && !isNaN(parseFloat(data.level)) ? parseFloat(data.level) : sorp.level;
1477
1751
  this.orp.phLockout = typeof data.phLockout !== 'undefined' && !isNaN(parseFloat(data.phLockout)) ? parseFloat(data.phLockout) : this.orp.phLockout;
1478
1752
  this.orp.flowReadingsOnly = typeof data.flowReadingsOnly !== 'undefined' ? utils.makeBool(data.flowReadingsOnly) : this.orp.flowReadingsOnly;
1753
+ if (typeof data.chlorDosingMethod !== 'undefined') { this.orp.chlorDosingMethod = data.chlorDosingMethod; }
1479
1754
  await this.setDosing(this.orp, data);
1480
1755
  await this.setMixing(this.orp, data);
1481
1756
  await this.probe.setProbeORPAsync(sorp.probe, data.probe);
1482
1757
  await this.tank.setTankAsync(sorp.tank, data.tank);
1483
1758
  await this.pump.setPumpAsync(sorp.pump, data.pump);
1759
+ await this.chlor.setChlorAsync(sorp.chlor, data);
1484
1760
  this.orp.setpoint = sorp.setpoint = typeof data.setpoint !== 'undefined' ? parseInt(data.setpoint, 10) : this.orp.setpoint;
1485
1761
  if (typeof data.tolerance !== 'undefined') {
1486
1762
  if (typeof data.tolerance.enabled !== 'undefined') this.orp.tolerance.enabled = utils.makeBool(data.tolerance.enabled);
@@ -1521,21 +1797,106 @@ export class NixieChemicalORP extends NixieChemical {
1521
1797
  await this.pump.dose(sorp);
1522
1798
  }
1523
1799
  }
1524
- catch (err) { logger.error(`manualDoseAsync ORP: ${err.message}`); logger.error(err); return Promise.reject(err);}
1800
+ catch (err) { logger.error(`manualDoseAsync ORP: ${err.message}`); logger.error(err); return Promise.reject(err); }
1525
1801
  }
1526
- public async cancelDosing(sorp: ChemicalORPState, reason: string) {
1802
+ public async cancelDosing(sorp: ChemicalORPState, reason: string): Promise<void> {
1527
1803
  try {
1528
- // Just stop the pump for now but we will do some logging later.
1529
- await this.pump.stopDosing(sorp, reason);
1804
+ if (typeof sorp.useChlorinator !== 'undefined' && sorp.useChlorinator && this.chemController.orp.orp.dosingMethod > 0) {
1805
+ await this.chlor.stopDosing(sorp, reason);
1806
+ // for chlor, we want 15 minute intervals
1807
+ if (sorp.doseHistory.length) {
1808
+ // if last dose was within 15 minutes, set mix time to 15 mins-lastdose
1809
+ // if no dose in last 15, then we should be monitoring
1810
+ if (new Date().getTime() - sorp.doseHistory[0].end.getTime() < this.chlor.chlorInterval * 60 * 1000){
1811
+ let lastDoseTime = sorp.doseHistory[0].timeDosed;
1812
+ let mixTime = Math.min(Math.max(this.chlor.chlorInterval * 60 - lastDoseTime, 0), this.chlor.chlorInterval * 60);
1813
+ // if (mixTime === 0) return; // due to delays with setting chlor, let the checkDosing pick up the cycle again with the chlor already on.
1814
+ if (sorp.dosingStatus === 0) await this.mixChemicals(sorp, mixTime);
1815
+ }
1816
+ }
1817
+ else{
1818
+ if (sorp.dosingStatus === 0) await this.mixChemicals(sorp);
1819
+ }
1820
+ return;
1821
+ }
1822
+ else {
1823
+ // Just stop the pump for now but we will do some logging later.
1824
+ await this.pump.stopDosing(sorp, reason);
1825
+ }
1530
1826
  if (sorp.dosingStatus === 0) {
1531
1827
  await this.mixChemicals(sorp);
1532
1828
  sorp.endDose(new Date(), 'cancelled');
1533
1829
  }
1534
1830
  } catch (err) { logger.error(`cancelDosing ORP: ${err.message}`); return Promise.reject(err); }
1535
1831
  }
1536
- public async checkDosing(chem: ChemController, sorp: ChemicalORPState) {
1832
+ protected async initMixChemicals(schem: ChemicalState, mixingTime?: number): Promise<void> {
1833
+ try {
1834
+ if (this._stoppingMix) return;
1835
+ if (typeof this.currentMix === 'undefined' || (typeof this.currentMix !== 'undefined' && isNaN(this.currentMix.timeMixed))) {
1836
+ if (typeof mixingTime !== 'undefined') {
1837
+ // This is a manual mix so we need to make sure the pump is not dosing.
1838
+ logger.info(`Clearing any possible ${schem.chemType} dosing or existing mix for mixingTime: ${mixingTime}`);
1839
+ if (schem.chemController.orp.useChlorinator) await this.chlor.stopDosing(schem, 'mix override');
1840
+ else await this.pump.stopDosing(schem, 'mix override');
1841
+ await this.stopMixing(schem);
1842
+ }
1843
+ this.currentMix = new NixieChemMix();
1844
+ if (typeof mixingTime !== 'undefined' && !isNaN(mixingTime)) {
1845
+ this.currentMix.set({ time: Math.round(mixingTime), timeMixed: 0, isManual: true });
1846
+ schem.manualMixing = true;
1847
+ }
1848
+ else if (schem.mixTimeRemaining > 0) {
1849
+ if (schem.manualMixing) {
1850
+ this.currentMix.set({ time: schem.mixTimeRemaining, timeMixed: 0, isManual: true });
1851
+ }
1852
+ else
1853
+ if (typeof this.chemController.orp.orp.useChlorinator !== 'undefined' && this.chemController.orp.orp.useChlorinator && this.chemController.orp.orp.dosingMethod > 0) {
1854
+ // if last dose was within 15 minutes, set mix time to 15 mins-(now-lastdose)
1855
+ // if no dose in last 15, then we should be monitoring
1856
+ await this.chlor.stopDosing(schem, 'mix override'); // ensure chlor has stopped
1857
+ if (schem.doseHistory.length) {
1858
+ // if last dose was within 15 minutes, set mix time to 15 mins-lastdose
1859
+ // if no dose in last 15, then we should be monitoring
1860
+ let lastDoseTime = schem.doseHistory[0].timeDosed;
1861
+ let mixTime = Math.min(Math.max(this.chlor.chlorInterval * 60 - lastDoseTime, 0), this.chlor.chlorInterval * 60);
1862
+ // if (mixTime === 0) return; // due to delays in the setting the chlor, if we had a full dose last time let the chlor continue
1863
+ this.currentMix.set({ time: this.chlor.chlorInterval, timeMixed: Math.max(0, mixTime - schem.mixTimeRemaining) });
1864
+ }
1865
+ else
1866
+ // if no dose history, mix for 0s
1867
+ // this.currentMix.set({ time: this.chemical.mixingTime, timeMixed: Math.max(0, (this.chlor.chlorInterval * 60) - schem.mixTimeRemaining) });
1868
+ this.currentMix.set({ time: 0, timeMixed: 0 });
1869
+ }
1870
+ else {
1871
+ this.currentMix.set({ time: this.chemical.mixingTime, timeMixed: Math.max(0, this.chemical.mixingTime - schem.mixTimeRemaining) });
1872
+ }
1873
+ }
1874
+ else
1875
+ if (typeof this.chemController.orp.orp.useChlorinator !== 'undefined' && this.chemController.orp.orp.useChlorinator && this.chemController.orp.orp.dosingMethod > 0)
1876
+ this.currentMix.set({ time: this.chlor.chlorInterval * 60, timeMixed: 0 });
1877
+ else
1878
+ this.currentMix.set({ time: this.chemical.mixingTime, timeMixed: 0 });
1879
+ logger.info(`Chem Controller begin mixing ${schem.chemType} for ${utils.formatDuration(this.currentMix.timeRemaining)} of ${utils.formatDuration(this.currentMix.time)}`)
1880
+ schem.mixTimeRemaining = this.currentMix.timeRemaining;
1881
+ }
1882
+ if (typeof this._mixTimer === 'undefined' || !this._mixTimer) {
1883
+ let self = this;
1884
+ this._mixTimer = setInterval(async () => { await self.mixChemicals(schem); }, 1000);
1885
+ logger.verbose(`Set ${schem.chemType} mix timer`);
1886
+ }
1887
+ } catch (err) { logger.error(`Error initializing ${schem.chemType} mix: ${err.message}`); }
1888
+ }
1889
+ public async checkDosing(chem: ChemController, sorp: ChemicalORPState): Promise<void> {
1537
1890
  try {
1538
1891
  let status = sys.board.valueMaps.chemControllerDosingStatus.getName(sorp.dosingStatus);
1892
+ if (!chem.orp.flowReadingsOnly || (chem.orp.flowReadingsOnly && sorp.chemController.flowDetected)) {
1893
+ // demand in raw mV
1894
+ sorp.demand = this.orp.setpoint - sorp.level;
1895
+ // log the demand. We'll store the last 100 data points.
1896
+ // With 1s intervals, this will only be 1m 40s. Likely should consider more... and def time to move this to an external file.
1897
+ sorp.appendDemand(new Date().valueOf(), sorp.demand);
1898
+ }
1899
+ if (chem.orp.useChlorinator && chem.orp.chlorDosingMethod === 0) return; // if chlor is managing itself, don't even cancel/stop as it will set the flags on the chlor
1539
1900
  if (sorp.suspendDosing) {
1540
1901
  // Kill off the dosing and make sure the pump isn't running. Let's force the issue here.
1541
1902
  await this.cancelDosing(sorp, 'suspended');
@@ -1550,10 +1911,10 @@ export class NixieChemicalORP extends NixieChemical {
1550
1911
  sorp.dosingVolumeRemaining = 0;
1551
1912
  sorp.dosingTimeRemaining = 0;
1552
1913
  await this.stopMixing(sorp);
1553
- await this.cancelDosing(sorp, 'unknown cancel');
1914
+ await this.cancelDosing(sorp, 'monitoring');
1554
1915
  }
1555
1916
  if (status === 'mixing') {
1556
- await this.cancelDosing(sorp, 'completed');
1917
+ await this.cancelDosing(sorp, 'mixing');
1557
1918
  await this.mixChemicals(sorp);
1558
1919
  }
1559
1920
  else if (sorp.manualDosing) {
@@ -1573,74 +1934,224 @@ export class NixieChemicalORP extends NixieChemical {
1573
1934
  }
1574
1935
  else await this.cancelDosing(sorp, 'empty tank');
1575
1936
  }
1576
- else if (sorp.dailyLimitReached) {
1937
+ else if (sorp.dailyLimitReached && !chem.orp.useChlorinator) {
1577
1938
  await this.cancelDosing(sorp, 'daily limit');
1578
1939
  }
1579
- else if (status === 'monitoring' || status === 'dosing' && !this.orp.useChlorinator) {
1580
- let dose = 0;
1581
- if (this.orp.setpoint > sorp.level && !sorp.lockout) {
1582
- // Calculate how many mL are required to raise to our ORP level.
1940
+ // if the ph pump is dosing and dosePriority is enabled, do not dose
1941
+ else if (sorp.chemController.ph.pump.isDosing && chem.ph.dosePriority) {
1942
+ await this.cancelDosing(sorp, 'ph pump dosing + dose priority');
1943
+ return;
1944
+ }
1945
+ else if (status === 'monitoring' || status === 'dosing') {
1946
+ // let _doseCalculatedSec = 0;
1947
+ if (!sorp.lockout) {
1948
+
1583
1949
  // 1. Get the total gallons of water that the chem controller is in control of.
1584
1950
  let totalGallons = 0;
1585
- if (chem.body === 0 || chem.body === 32) totalGallons += sys.bodies.getItemById(1).capacity;
1586
- if (chem.body === 1 || chem.body === 32) totalGallons += sys.bodies.getItemById(2).capacity;
1951
+ let body1 = sys.bodies.getItemById(1);
1952
+ let body2 = sys.bodies.getItemById(2);
1953
+ if (chem.body === 0 || chem.body === 32) totalGallons += body1.capacity;
1954
+ if (chem.body === 1 || chem.body === 32) totalGallons += body2.capacity;
1587
1955
  if (chem.body === 2) totalGallons += sys.bodies.getItemById(3).capacity;
1588
1956
  if (chem.body === 3) totalGallons += sys.bodies.getItemById(4).capacity;
1589
- let pump = this.pump.pump;
1590
- let demand = dose = Math.round(utils.convert.volume.convertUnits(0, 'oz', 'mL'));
1591
- let time = typeof pump.ratedFlow === 'undefined' || pump.ratedFlow <= 0 ? 0 : Math.round(dose / (pump.ratedFlow / 60));
1592
- let meth = sys.board.valueMaps.chemDosingMethods.getName(this.orp.dosingMethod);
1593
- // Now that we know our chlorine demand we need to adjust this dose based upon the limits provided in the setup.
1594
- switch (meth) {
1595
- case 'time':
1596
- time = this.orp.maxDosingTime;
1597
- dose = typeof pump.ratedFlow === 'undefined' ? 0 : Math.round(time * (this.pump.pump.ratedFlow / 60));
1598
- break;
1599
- case 'volume':
1600
- dose = this.orp.maxDosingVolume;
1601
- time = time = typeof pump.ratedFlow === 'undefined' || pump.ratedFlow <= 0 ? 0 : Math.round(dose / (pump.ratedFlow / 60));
1602
- break;
1603
- case 'volumeTime':
1604
- default:
1605
- // This is maybe a bit dumb as the volume and time should equal out for the rated flow. In other words
1606
- // you will never get to the volume limit if the rated flow can't keep up to the time.
1607
- if (dose > this.orp.maxDosingVolume) {
1608
- dose = this.orp.maxDosingVolume;
1609
- time = time = typeof pump.ratedFlow === 'undefined' || pump.ratedFlow <= 0 ? 0 : Math.round(dose / (pump.ratedFlow / 60));
1957
+
1958
+ if (chem.orp.useChlorinator) {
1959
+ /*
1960
+ Alright, here's the current thinking.
1961
+ 1. If the orp setpoint is > 50mV below the current orp, the chlor will
1962
+ be run at 100%.
1963
+ 2. At the other end, if the demand is < -20mV above the setpoint, chlor
1964
+ will be run at 0%.
1965
+ 3. This assumes a sliding scale where we will have an equilibrium point when
1966
+ setpoint = current orp and hopefully this will be somewhere near (50-20 / 100) = ~30% of time the chlor is on
1967
+
1968
+ Thoughts from @rstrouste
1969
+ Volume -- Check
1970
+ Delivery Rate -- IC40, IC60, IC20 and IC30 all have different production rates in pounds/day. The pounds are Sodium Hypochlorite which translates into Hypochlorous acid (HOCl) + Hypochlorite (OCI-). The former is stronger and the amount of this that is produced is based upon, temperature, pH, and CYA with pH within range being irrelevant (hence the reason for pH lockout).
1971
+
1972
+
1973
+ Additional future factors to consider-
1974
+ * If temp is below 65(?), the chlor won't be producing any chlorine. Throw a warning/error?
1975
+ * If salt level is too low/high it will cause issues. Warning/error?
1976
+ * Adjust chlor output if it is under/oversized for the total gallons
1977
+ */
1978
+ // if we are still mixing, return
1979
+ if (typeof sorp.mixTimeRemaining !== 'undefined' && sorp.mixTimeRemaining > 0) {
1980
+ await this.cancelDosing(sorp, 'still mixing');
1981
+ return;
1982
+ }
1983
+ // Old fashion method; let the setpoints on chlor be the master
1984
+ // if (chem.orp.chlorDosingMethod === 0) return;
1985
+ // if there is a current pending dose, finish it out
1986
+ if (typeof sorp.currentDose === 'undefined') {
1987
+ if (sorp.dosingStatus === 0) { // 0 is dosing
1988
+ // We need to finish off a dose that was interrupted by regular programming. This occurs
1989
+ // when for instance njspc is interrupted and restarted in the middle of a dose. If we were
1990
+ // mixing before we will never get here.
1991
+ if (typeof sorp.currentDose === 'undefined')
1992
+ sorp.startDose(new Timestamp().addSeconds(-sorp.doseTime).toDate(), 'auto', sorp.doseVolume + sorp.dosingVolumeRemaining, sorp.doseVolume, (sorp.doseTime + sorp.dosingTimeRemaining) * 1000, sorp.doseTime * 1000);
1993
+ await this.chlor.dose(sorp);
1994
+ return;
1610
1995
  }
1611
- if (time > this.orp.maxDosingTime) {
1612
- time = this.orp.maxDosingTime;
1613
- dose = typeof pump.ratedFlow === 'undefined' ? 0 : Math.round(time * (this.pump.pump.ratedFlow / 60));
1996
+ }
1997
+
1998
+
1999
+ let chlor = sys.chlorinators.getItemById(1); // Still haven't seen any systems with 2+ chlors
2000
+ let schlor = state.chlorinators.getItemById(1);
2001
+ // If someone or something is superchloring the pool, let it be
2002
+ if (schlor.superChlor) return;
2003
+ // Let's have some fun trying to figure out a dynamic approach to chlor management
2004
+ let body = sys.board.bodies.getBodyState(this.chemController.chem.body);
2005
+ let adj = 1;
2006
+ if (typeof body !== 'undefined' && totalGallons > 0 && sys.bodies.length > 1) {
2007
+ // Intellichem scales down dosing based on the spa being on
2008
+ // vs the pool. May be interesting to experiment with.
2009
+ let type = sys.board.valueMaps.bodyTypes.getName(body.type);
2010
+ switch (type) {
2011
+ case "pool":
2012
+ case "pool/spa":
2013
+ // normal dosing
2014
+ break;
2015
+ default:
2016
+ // case "spa":
2017
+ // adjust dosing down to the amount of the smaller body
2018
+ adj -= Math.abs((body1.capacity - body2.capacity) / Math.max(body1.capacity, body2.capacity));
2019
+ break;
1614
2020
  }
1615
- break;
1616
- }
1617
- logger.info(`Chem orp dose calculated ${dose}mL for ${utils.formatDuration(time)} Tank Level: ${sorp.tank.level} using ${meth}`);
2021
+ }
2022
+ let model = sys.board.valueMaps.chlorinatorModel.findItem(chlor.model);
2023
+ if (typeof model === 'undefined' || model === 0) return Promise.reject(new EquipmentNotFoundError(`Please specify a chlorinator model to allow Nixie to calculate chlorine demand`, `chlorinator`));
2024
+ // if we want to adjust for over/under sized chlorinator we can do so here
2025
+ // if (typeof model !== 'undefined' && model.capacity > 0) {
2026
+ // adj *= totalGallons / model.capacity;
2027
+ // }
1618
2028
 
1619
- sorp.demand = sorp.calcDemand(chem);
1620
- if (typeof sorp.currentDose === 'undefined') {
1621
- // We will include this with the dose demand because our limits may reduce it.
1622
- //dosage.demand = demand;
1623
- if (sorp.dosingStatus === 0) { // 0 is dosing.
1624
- // We need to finish off a dose that was interrupted by regular programming. This occurs
1625
- // when for instance njspc is interrupted and restarted in the middle of a dose. If we were
1626
- // mixing before we will never get here.
1627
- if (typeof sorp.currentDose === 'undefined')
1628
- sorp.startDose(new Timestamp().addSeconds(-sorp.doseTime).toDate(), 'auto', sorp.doseVolume + sorp.dosingVolumeRemaining, sorp.doseVolume, (sorp.doseTime + sorp.dosingTimeRemaining) * 1000, sorp.doseTime * 1000);
2029
+ // unlike ph/orp tank dosing, we are using 15 min intervals so if there is an existing dose then continue
2030
+ if (typeof sorp.currentDose !== 'undefined' && sorp.currentDose.volumeRemaining > 0) {
2031
+ await this.chlor.dose(sorp);
2032
+ return;
2033
+ }
2034
+
2035
+ // We could store these data points in a separate file like the dosing logs.
2036
+ let percentOfTime = 0;
2037
+ if (sorp.demand > 50) {
2038
+ logger.info(`Chlor demand ${sorp.demand} > 50; % of time set to 100%.`);
2039
+ percentOfTime = 1;
2040
+ }
2041
+ else if (sorp.demand < -20) {
2042
+ await this.cancelDosing(sorp, 'demand < -20');
2043
+ }
2044
+ else {
2045
+ // y=mx+b; m = 100/70; b = 100-(50*100/70) = 28.57
2046
+ // let's start with a straight line
2047
+ let b = 100 - (50 * 100 / 70);
2048
+ percentOfTime = ((100 / 70) * sorp.demand * adj + b) / 100;
2049
+ logger.info(`Chlor trend line is ${sorp.demandHistory.slope}.`);
2050
+ if (sorp.demandHistory.slope > 5 && sorp.demand < 0) {
2051
+ // need less chlorine, but we're getting there too fast
2052
+ // slope is high; turn down dose
2053
+ percentOfTime *= .5;
2054
+ }
2055
+ else if (sorp.demandHistory.slope < 5 && sorp.demand > 0) {
2056
+ // need more chlorine, but we aren't getting there fast enough
2057
+ // slope is too low, turn up dose
2058
+ percentOfTime *= 1.1;
2059
+ }
2060
+ else if (sorp.demandHistory.slope > 0 && sorp.demand > 0) {
2061
+ // chlorine is increasing, but we need less of it
2062
+ percentOfTime *= .5;
2063
+ }
2064
+ else if (sorp.demandHistory.slope < 0 && sorp.demand > 0) {
2065
+ // chlorine is decreasing, but we need more
2066
+ percentOfTime *= 1.1;
2067
+ }
2068
+ percentOfTime = Math.min(1, Math.max(0, percentOfTime));
2069
+ logger.info(`Chlor dosing % of time is ${Math.round(percentOfTime * 10000) / 100}%`)
1629
2070
  }
1630
- else
2071
+
2072
+ // convert the % of time back to an amount of chlorine over 15 minutes;
2073
+ let time = this.chlor.chlorInterval * 60 * percentOfTime;
2074
+ let dose = model.chlorinePerSec * time;
2075
+
2076
+ if (dose > 0) {
2077
+ logger.info(`Chem chlor calculated dosing at ${Math.round(percentOfTime * 10000) / 100}% and will dose ${Math.round(dose * 1000000) / 1000000}Lbs of chlorine over the next ${utils.formatDuration(time)}.`)
1631
2078
  sorp.startDose(new Date(), 'auto', dose, 0, time, 0);
2079
+ await this.chlor.dose(sorp);
2080
+ return;
2081
+ }
2082
+
2083
+ // if none of the other conditions are true, mix
2084
+ // await this.mixChemicals(sorp, this.chlor.chlorInterval * 60);
2085
+
1632
2086
  }
1633
- // Now let's determine what we need to do with our pump to satisfy our acid demand.
1634
- if (sorp.tank.level > 0) {
1635
- await this.pump.dose(sorp);
2087
+ else if (this.orp.setpoint > sorp.level) {
2088
+ let pump = this.pump.pump;
2089
+ // Calculate how many mL are required to raise to our ORP level.
2090
+ let demand = Math.round(utils.convert.volume.convertUnits(0, 'oz', 'mL'));
2091
+ let time = typeof pump.ratedFlow === 'undefined' || pump.ratedFlow <= 0 ? 0 : Math.round(demand / (pump.ratedFlow / 60));
2092
+ let meth = sys.board.valueMaps.chemDosingMethods.getName(this.orp.dosingMethod);
2093
+ // Now that we know our chlorine demand we need to adjust this dose based upon the limits provided in the setup.
2094
+ switch (meth) {
2095
+ case 'time':
2096
+ time = this.orp.maxDosingTime;
2097
+ demand = typeof pump.ratedFlow === 'undefined' ? 0 : Math.round(time * (this.pump.pump.ratedFlow / 60));
2098
+ break;
2099
+ case 'volume':
2100
+ demand = this.orp.maxDosingVolume;
2101
+ time = typeof pump.ratedFlow === 'undefined' || pump.ratedFlow <= 0 ? 0 : Math.round(demand / (pump.ratedFlow / 60));
2102
+ break;
2103
+ case 'volumeTime':
2104
+ default:
2105
+ // This is maybe a bit dumb as the volume and time should equal out for the rated flow. In other words
2106
+ // you will never get to the volume limit if the rated flow can't keep up to the time.
2107
+ if (demand > this.orp.maxDosingVolume) {
2108
+ demand = this.orp.maxDosingVolume;
2109
+ time = typeof pump.ratedFlow === 'undefined' || pump.ratedFlow <= 0 ? 0 : Math.round(demand / (pump.ratedFlow / 60));
2110
+ }
2111
+ if (time > this.orp.maxDosingTime) {
2112
+ time = this.orp.maxDosingTime;
2113
+ demand = typeof pump.ratedFlow === 'undefined' ? 0 : Math.round(time * (this.pump.pump.ratedFlow / 60));
2114
+ }
2115
+ break;
2116
+ }
2117
+ logger.info(`Chem orp dose calculated ${demand}mL for ${utils.formatDuration(time)} Tank Level: ${sorp.tank.level} using ${meth}`);
2118
+
2119
+ sorp.demand = sorp.calcDemand(chem);
2120
+ if (sorp.demand > 0) logger.info(`Chem orp dose calculated ${demand}mL for ${utils.formatDuration(time)} Tank Level: ${sorp.tank.level} using ${meth}`);
2121
+
2122
+ if (typeof sorp.currentDose === 'undefined') {
2123
+ // We will include this with the dose demand because our limits may reduce it.
2124
+ //dosage.demand = demand;
2125
+ if (sorp.dosingStatus === 0) { // 0 is dosing.
2126
+ // We need to finish off a dose that was interrupted by regular programming. This occurs
2127
+ // when for instance njspc is interrupted and restarted in the middle of a dose. If we were
2128
+ // mixing before we will never get here.
2129
+ if (typeof sorp.currentDose === 'undefined')
2130
+ sorp.startDose(new Timestamp().addSeconds(-sorp.doseTime).toDate(), 'auto', sorp.doseVolume + sorp.dosingVolumeRemaining, sorp.doseVolume, (sorp.doseTime + sorp.dosingTimeRemaining) * 1000, sorp.doseTime * 1000);
2131
+ }
2132
+ else
2133
+ sorp.startDose(new Date(), 'auto', demand, 0, time, 0);
2134
+ }
2135
+ // Now let's determine what we need to do with our pump to satisfy our acid demand.
2136
+ if (sorp.tank.level > 0) {
2137
+ await this.pump.dose(sorp);
2138
+ }
2139
+ else await this.cancelDosing(sorp, 'empty tank');
1636
2140
  }
1637
- else await this.cancelDosing(sorp, 'empty tank');
1638
2141
  }
1639
2142
  else
1640
2143
  await this.cancelDosing(sorp, 'unknown cancel');
1641
2144
  }
1642
2145
  }
1643
- catch (err) { logger.error(`checkDosing ORP: ${err.message}`); return Promise.reject(err);}
2146
+ catch (err) { logger.error(`checkDosing ORP: ${err.message}`); return Promise.reject(err); }
2147
+ }
2148
+ public async deleteChlorAsync(chlor: NixieChlorinator) {
2149
+ logger.info(`Removing chlor ${chlor.id} from Chem Controller ${this.getParent().id}`);
2150
+ let schem = state.chemControllers.getItemById(this.getParent().id);
2151
+ this.orp.useChlorinator = false;
2152
+ schem.orp.useChlorinator = false;
2153
+ if (schem.orp.dosingStatus === 0) { await this.cancelDosing(schem.orp, 'deleting chlorinator'); }
2154
+ if (schem.orp.dosingStatus === 1) { await this.cancelMixing(schem.orp); }
1644
2155
  }
1645
2156
  }
1646
2157
  class NixieChemProbe extends NixieChildEquipment {
@@ -1695,7 +2206,7 @@ export class NixieChemProbePh extends NixieChemProbe {
1695
2206
  // Set the current body so that it references the temperature of the current running body.
1696
2207
  let body = sys.board.bodies.getBodyState(this.chemical.chemController.chem.body);
1697
2208
  if (typeof body !== 'undefined' && body.isOn) {
1698
- let units = sys.board.valueMaps.tempUnits.transform(sys.general.options.units);
2209
+ let units = sys.board.valueMaps.tempUnits.transform(state.temps.units);
1699
2210
  let obj = {};
1700
2211
  obj[`temp${units.name.toUpperCase()}`] = body.temp;
1701
2212
  sprobe.tempUnits = units.val;
@@ -1721,7 +2232,7 @@ export class NixieChemProbePh extends NixieChemProbe {
1721
2232
  deviceBinding: this.probe.deviceBinding,
1722
2233
  eventName: "chemController",
1723
2234
  property: "pHLevel",
1724
- sendValue: 'pH',
2235
+ sendValue: 'all',
1725
2236
  isActive: data.remFeedEnabled,
1726
2237
  sampling: 1,
1727
2238
  changesOnly: false,
@@ -1729,7 +2240,7 @@ export class NixieChemProbePh extends NixieChemProbe {
1729
2240
  }
1730
2241
  let res = await NixieChemController.putDeviceService(this.probe.connectionId, '/config/feed', d);
1731
2242
  if (res.status.code === 200) { this.probe.remFeedEnabled = data.remFeedEnabled; }
1732
- else { logger.warn(`setRemoteREMFeed: Cannot set remote feed. Message:${JSON.stringify(res.status)} for feed: ${JSON.stringify(d)}.`); return Promise.reject(`Cannot set REM feed for pH probe: ${JSON.stringify(res)}.`); }
2243
+ else { logger.warn(`setRemoteREMFeed: Cannot set remote feed. Message:${JSON.stringify(res.status)} for feed: ${JSON.stringify(d)}.`); }
1733
2244
  }
1734
2245
  catch (err) { logger.error(`setRemoteREMFeed: ${err.message}`); return Promise.reject(err); }
1735
2246
  }
@@ -1812,9 +2323,15 @@ export class NixieChemProbeORP extends NixieChemProbe {
1812
2323
  }
1813
2324
  let res = await NixieChemController.putDeviceService(this.probe.connectionId, '/config/feed', d);
1814
2325
  if (res.status.code === 200) { this.probe.remFeedEnabled = data.remFeedEnabled; }
1815
- else { logger.warn(`setRemoteREMFeed: Cannot set remote feed. Message:${JSON.stringify(res.status)} for feed: ${JSON.stringify(d)}.`); return Promise.reject(new InvalidOperationError(`Nixie could not set remote REM feed for the ORP probe.`, this.probe.dataName)); }
2326
+ else {
2327
+ logger.warn(`setRemoteREMFeed: Cannot set remote feed. Message:${JSON.stringify(res.status)} for feed: ${JSON.stringify(d)}.`);
2328
+ // return Promise.reject(new InvalidOperationError(`Nixie could not set remote REM feed for the ORP probe.`, this.probe.dataName));
2329
+ }
2330
+ }
2331
+ catch (err) {
2332
+ logger.error(`setRemoteREMFeed: ${err.message}`);
2333
+ //return Promise.reject(err); // don't muck up chem controller if we can't set the feeds.
1816
2334
  }
1817
- catch (err) { logger.error(`setRemoteREMFeed: ${err.message}`); return Promise.reject(err); }
1818
2335
  }
1819
2336
  public syncRemoteREMFeeds(chem: ChemController, servers) {
1820
2337
  // match any feeds and store the id/statusf