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