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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- package/Changelog +13 -0
- package/Dockerfile +1 -0
- package/README.md +5 -5
- package/app.ts +11 -0
- package/config/Config.ts +3 -0
- package/config/VersionCheck.ts +8 -4
- package/controller/Constants.ts +165 -9
- package/controller/Equipment.ts +186 -65
- package/controller/Errors.ts +22 -1
- package/controller/State.ts +273 -57
- package/controller/boards/EasyTouchBoard.ts +194 -95
- package/controller/boards/IntelliCenterBoard.ts +115 -42
- package/controller/boards/IntelliTouchBoard.ts +104 -30
- package/controller/boards/NixieBoard.ts +155 -53
- package/controller/boards/SystemBoard.ts +1529 -514
- package/controller/comms/Comms.ts +219 -42
- package/controller/comms/messages/Messages.ts +16 -4
- package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -3
- package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
- package/controller/comms/messages/config/CircuitMessage.ts +1 -1
- package/controller/comms/messages/config/CoverMessage.ts +1 -0
- package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
- package/controller/comms/messages/config/ExternalMessage.ts +43 -25
- package/controller/comms/messages/config/FeatureMessage.ts +8 -1
- package/controller/comms/messages/config/GeneralMessage.ts +8 -0
- package/controller/comms/messages/config/HeaterMessage.ts +15 -9
- package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
- package/controller/comms/messages/config/OptionsMessage.ts +13 -1
- package/controller/comms/messages/config/PumpMessage.ts +4 -20
- package/controller/comms/messages/config/RemoteMessage.ts +4 -0
- package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
- package/controller/comms/messages/config/SecurityMessage.ts +1 -0
- package/controller/comms/messages/config/ValveMessage.ts +12 -2
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +14 -6
- package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
- package/controller/comms/messages/status/HeaterStateMessage.ts +25 -5
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +55 -26
- package/controller/nixie/Nixie.ts +18 -16
- package/controller/nixie/NixieEquipment.ts +6 -6
- package/controller/nixie/bodies/Body.ts +7 -4
- package/controller/nixie/bodies/Filter.ts +7 -4
- package/controller/nixie/chemistry/ChemController.ts +800 -283
- package/controller/nixie/chemistry/Chlorinator.ts +22 -14
- package/controller/nixie/circuits/Circuit.ts +42 -7
- package/controller/nixie/heaters/Heater.ts +303 -30
- package/controller/nixie/pumps/Pump.ts +57 -30
- package/controller/nixie/schedules/Schedule.ts +10 -7
- package/controller/nixie/valves/Valve.ts +7 -5
- package/defaultConfig.json +32 -1
- package/issue_template.md +1 -1
- package/logger/DataLogger.ts +37 -22
- package/package.json +20 -18
- package/web/Server.ts +529 -31
- package/web/bindings/influxDB.json +157 -5
- package/web/bindings/mqtt.json +112 -13
- package/web/bindings/mqttAlt.json +109 -11
- package/web/interfaces/baseInterface.ts +2 -1
- package/web/interfaces/httpInterface.ts +2 -0
- package/web/interfaces/influxInterface.ts +103 -54
- package/web/interfaces/mqttInterface.ts +16 -5
- package/web/services/config/Config.ts +179 -43
- package/web/services/state/State.ts +51 -5
- package/web/services/state/StateSocket.ts +19 -2
|
@@ -9,6 +9,7 @@ import { setTimeout, clearTimeout } from 'timers';
|
|
|
9
9
|
import { webApp, InterfaceServerResponse } from "../../../web/Server";
|
|
10
10
|
import { Outbound, Protocol, Response } from '../../comms/messages/Messages';
|
|
11
11
|
import { conn } from '../../comms/Comms';
|
|
12
|
+
import { ncp } from '../Nixie';
|
|
12
13
|
|
|
13
14
|
export class NixieChlorinatorCollection extends NixieEquipmentCollection<NixieChlorinator> {
|
|
14
15
|
public async deleteChlorinatorAsync(id: number) {
|
|
@@ -18,6 +19,7 @@ export class NixieChlorinatorCollection extends NixieEquipmentCollection<NixieCh
|
|
|
18
19
|
for (let i = this.length - 1; i >= 0; i--) {
|
|
19
20
|
let c = this[i];
|
|
20
21
|
if (c.id === id) {
|
|
22
|
+
await ncp.chemControllers.deleteChlorAsync(c as NixieChlorinator);
|
|
21
23
|
await c.closeAsync();
|
|
22
24
|
this.splice(i, 1);
|
|
23
25
|
}
|
|
@@ -45,14 +47,15 @@ export class NixieChlorinatorCollection extends NixieEquipmentCollection<NixieCh
|
|
|
45
47
|
}
|
|
46
48
|
public async initAsync(chlorinators: ChlorinatorCollection) {
|
|
47
49
|
try {
|
|
48
|
-
this.length = 0;
|
|
49
50
|
for (let i = 0; i < chlorinators.length; i++) {
|
|
50
51
|
let cc = chlorinators.getItemByIndex(i);
|
|
51
52
|
if (cc.master === 1) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
if (typeof this.find(elem => elem.id === cc.id) === 'undefined') {
|
|
54
|
+
logger.info(`Initializing Nixie chlorinator ${cc.name}`);
|
|
55
|
+
let ncc = new NixieChlorinator(this.controlPanel, cc);
|
|
56
|
+
this.push(ncc);
|
|
57
|
+
await ncc.initAsync();
|
|
58
|
+
}
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -74,7 +77,6 @@ export class NixieChlorinator extends NixieEquipment {
|
|
|
74
77
|
private _pollTimer: NodeJS.Timeout = null;
|
|
75
78
|
private superChlorinating: boolean = false;
|
|
76
79
|
private superChlorStart: number = 0;
|
|
77
|
-
private chlorinating: boolean = false;
|
|
78
80
|
public chlor: Chlorinator;
|
|
79
81
|
public bodyOnTime: number;
|
|
80
82
|
protected _suspendPolling: number = 0;
|
|
@@ -96,12 +98,14 @@ export class NixieChlorinator extends NixieEquipment {
|
|
|
96
98
|
let spaSetpoint = typeof data.spaSetpoint !== 'undefined' ? parseInt(data.spaSetpoint, 10) : chlor.spaSetpoint;
|
|
97
99
|
let body = sys.board.bodies.mapBodyAssociation(typeof data.body === 'undefined' ? chlor.body : data.body);
|
|
98
100
|
let superChlor = typeof data.superChlor !== 'undefined' ? utils.makeBool(data.superChlor) : chlor.superChlor;
|
|
99
|
-
let chlorType = typeof data.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(data.type) : chlor.type;
|
|
101
|
+
let chlorType = typeof data.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(data.type) : chlor.type || 0;
|
|
100
102
|
let superChlorHours = typeof data.superChlorHours !== 'undefined' ? parseInt(data.superChlorHours, 10) : chlor.superChlorHours;
|
|
103
|
+
let disabled = typeof data.disabled !== 'undefined' ? utils.makeBool(data.disabled) : chlor.disabled;
|
|
104
|
+
let isDosing = typeof data.isDosing !== 'undefined' ? utils.makeBool(data.isDosing) : chlor.isDosing;
|
|
105
|
+
let model = typeof data.model !== 'undefined' ? data.model : chlor.model || 0;
|
|
101
106
|
if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid body assignment`, 'chlorinator', data.body || chlor.body));
|
|
102
107
|
if (isNaN(poolSetpoint)) poolSetpoint = 0;
|
|
103
108
|
if (isNaN(spaSetpoint)) spaSetpoint = 0;
|
|
104
|
-
if (isNaN(chlorType)) chlorType = sys.board.valueMaps.chlorinatorType.getValue('intellichlor');
|
|
105
109
|
chlor.ignoreSaltReading = (typeof data.ignoreSaltReading !== 'undefined') ? utils.makeBool(data.ignoreSaltReading) : utils.makeBool(chlor.ignoreSaltReading);
|
|
106
110
|
// Do a final validation pass so we dont send this off in a mess.
|
|
107
111
|
let schlor = state.chlorinators.getItemById(chlor.id, true);
|
|
@@ -110,8 +114,10 @@ export class NixieChlorinator extends NixieEquipment {
|
|
|
110
114
|
schlor.superChlor = chlor.superChlor = superChlor;
|
|
111
115
|
schlor.superChlorHours = chlor.superChlorHours = superChlorHours;
|
|
112
116
|
schlor.type = chlor.type = chlorType;
|
|
113
|
-
chlor.
|
|
114
|
-
schlor.body = chlor.body;
|
|
117
|
+
chlor.model = model;
|
|
118
|
+
schlor.body = chlor.body = body.val;
|
|
119
|
+
chlor.disabled = disabled;
|
|
120
|
+
chlor.isDosing = isDosing;
|
|
115
121
|
schlor.name = chlor.name = data.name || chlor.name || `Chlorinator ${chlor.id}`;
|
|
116
122
|
schlor.isActive = chlor.isActive = true;
|
|
117
123
|
}
|
|
@@ -147,6 +153,7 @@ export class NixieChlorinator extends NixieEquipment {
|
|
|
147
153
|
return isOn;
|
|
148
154
|
}
|
|
149
155
|
public async pollEquipment() {
|
|
156
|
+
let self = this;
|
|
150
157
|
try {
|
|
151
158
|
if (this._pollTimer) {
|
|
152
159
|
clearTimeout(this._pollTimer);
|
|
@@ -171,7 +178,7 @@ export class NixieChlorinator extends NixieEquipment {
|
|
|
171
178
|
// Comms failure will be handeled by the message processor.
|
|
172
179
|
logger.error(`Chlorinator ${this.chlor.name} comms failure: ${err.message}`);
|
|
173
180
|
}
|
|
174
|
-
finally { if(!this.closing) this._pollTimer = setTimeout(
|
|
181
|
+
finally { if(!this.closing) this._pollTimer = setTimeout(() => {self.pollEquipment();}, this.pollingInterval); }
|
|
175
182
|
}
|
|
176
183
|
public async takeControl(): Promise<boolean> {
|
|
177
184
|
try {
|
|
@@ -223,7 +230,7 @@ export class NixieChlorinator extends NixieEquipment {
|
|
|
223
230
|
let setpoint = 0;
|
|
224
231
|
if (typeof body !== 'undefined') {
|
|
225
232
|
setpoint = (body.id === 1) ? this.chlor.poolSetpoint : this.chlor.spaSetpoint;
|
|
226
|
-
if (this.chlor.superChlor === true) setpoint = 100;
|
|
233
|
+
if (this.chlor.superChlor === true || this.chlor.isDosing) setpoint = 100;
|
|
227
234
|
if (this.chlor.disabled === true) setpoint = 0; // Our target should be 0 because we have other things going on. For instance,
|
|
228
235
|
// we may be dosing acid which will cause the disabled flag to be true.
|
|
229
236
|
}
|
|
@@ -244,7 +251,6 @@ export class NixieChlorinator extends NixieEquipment {
|
|
|
244
251
|
onAbort: () => {},
|
|
245
252
|
onComplete: (err) => {
|
|
246
253
|
if (err) {
|
|
247
|
-
this.chlorinating = false;
|
|
248
254
|
cstate.currentOutput = 0;
|
|
249
255
|
cstate.status = 128;
|
|
250
256
|
resolve(false);
|
|
@@ -253,7 +259,6 @@ export class NixieChlorinator extends NixieEquipment {
|
|
|
253
259
|
// The action:17 message originated from us so we will not see it in the
|
|
254
260
|
// ChlorinatorStateMessage module.
|
|
255
261
|
cstate.currentOutput = setpoint;
|
|
256
|
-
this.chlorinating = true;
|
|
257
262
|
if (!this.superChlorinating && cstate.superChlor) {
|
|
258
263
|
cstate.superChlorRemaining = cstate.superChlorHours * 3600;
|
|
259
264
|
this.superChlorStart = Math.floor(new Date().getTime() / 1000) * 1000;
|
|
@@ -267,6 +272,8 @@ export class NixieChlorinator extends NixieEquipment {
|
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
274
|
});
|
|
275
|
+
// #338
|
|
276
|
+
if (setpoint === 16) { out.appendPayloadByte(0); }
|
|
270
277
|
conn.queueSendMessage(out);
|
|
271
278
|
});
|
|
272
279
|
|
|
@@ -299,6 +306,7 @@ export class NixieChlorinator extends NixieEquipment {
|
|
|
299
306
|
conn.queueSendMessage(out);
|
|
300
307
|
});
|
|
301
308
|
}
|
|
309
|
+
else return Promise.resolve(false);
|
|
302
310
|
} catch (err) { logger.error(`Communication error with Chlorinator ${this.chlor.name} : ${err.message}`); return Promise.reject(err);}
|
|
303
311
|
|
|
304
312
|
}
|
|
@@ -56,15 +56,22 @@ export class NixieCircuitCollection extends NixieEquipmentCollection<NixieCircui
|
|
|
56
56
|
}
|
|
57
57
|
catch (err) { logger.error(`setCircuitAsync: ${err.message}`); return Promise.reject(err); }
|
|
58
58
|
}
|
|
59
|
+
public async checkCircuitEggTimerExpirationAsync(cstate: ICircuitState) {
|
|
60
|
+
try {
|
|
61
|
+
let c: NixieCircuit = this.find(elem => elem.id === cstate.id) as NixieCircuit;
|
|
62
|
+
await c.checkCircuitEggTimerExpirationAsync(cstate);
|
|
63
|
+
} catch (err) { logger.error(`NCP: Error syncing circuit states: ${err}`); }
|
|
64
|
+
}
|
|
59
65
|
public async initAsync(circuits: CircuitCollection) {
|
|
60
66
|
try {
|
|
61
|
-
this.length = 0;
|
|
62
67
|
for (let i = 0; i < circuits.length; i++) {
|
|
63
68
|
let circuit = circuits.getItemByIndex(i);
|
|
64
69
|
if (circuit.master === 1) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
if (typeof this.find(elem => elem.id === circuit.id) === 'undefined') {
|
|
71
|
+
logger.info(`Initializing Nixie circuit ${circuit.name}`);
|
|
72
|
+
let ncircuit = new NixieCircuit(this.controlPanel, circuit);
|
|
73
|
+
this.push(ncircuit);
|
|
74
|
+
}
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
}
|
|
@@ -93,13 +100,14 @@ export class NixieCircuitCollection extends NixieEquipmentCollection<NixieCircui
|
|
|
93
100
|
} catch (err) { logger.error(`initCircuitAsync: ${err.message}`); return Promise.reject(err); }
|
|
94
101
|
}
|
|
95
102
|
public async pollCircuitsAsync() {
|
|
103
|
+
let self = this;
|
|
96
104
|
try {
|
|
97
105
|
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
98
106
|
this._pollTimer = null;
|
|
99
107
|
let success = false;
|
|
100
108
|
|
|
101
109
|
} catch (err) { logger.error(`Error polling circuits: ${err.message}`); return Promise.reject(err); }
|
|
102
|
-
finally { this._pollTimer = setTimeout(async () => await
|
|
110
|
+
finally { this._pollTimer = setTimeout(async () => await self.pollCircuitsAsync(), this.pollingInterval || 10000); }
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
113
|
export class NixieCircuit extends NixieEquipment {
|
|
@@ -124,12 +132,26 @@ export class NixieCircuit extends NixieEquipment {
|
|
|
124
132
|
this._sequencing = true;
|
|
125
133
|
let arr = [];
|
|
126
134
|
let t = typeof timeout === 'undefined' ? 100 : timeout;
|
|
127
|
-
arr.push({ isOn:
|
|
135
|
+
arr.push({ isOn: false, timeout: t }); // This may not be needed but we always need to start from off.
|
|
128
136
|
//[{ isOn: true, timeout: 1000 }, { isOn: false, timeout: 1000 }]
|
|
129
137
|
for (let i = 0; i < count; i++) {
|
|
130
|
-
arr.push({ isOn: false, timeout: t });
|
|
131
138
|
arr.push({ isOn: true, timeout: t });
|
|
139
|
+
if(i < count - 1) arr.push({ isOn: false, timeout: t });
|
|
132
140
|
}
|
|
141
|
+
// The documentation for IntelliBrite is incorrect. The sequence below will give us Party mode.
|
|
142
|
+
// Party mode:2
|
|
143
|
+
// Start: Off
|
|
144
|
+
// On
|
|
145
|
+
// Off
|
|
146
|
+
// On
|
|
147
|
+
// According to the docs this is the sequence they lay out.
|
|
148
|
+
// Party mode:2
|
|
149
|
+
// Start: On
|
|
150
|
+
// Off
|
|
151
|
+
// On
|
|
152
|
+
// Off
|
|
153
|
+
// On
|
|
154
|
+
|
|
133
155
|
let res = await NixieEquipment.putDeviceService(this.circuit.connectionId, `/state/device/${this.circuit.deviceBinding}`, arr, 60000);
|
|
134
156
|
return res;
|
|
135
157
|
} catch (err) { logger.error(`Nixie: Error sending circuit sequence ${this.id}: ${count}`); }
|
|
@@ -147,12 +169,14 @@ export class NixieCircuit extends NixieEquipment {
|
|
|
147
169
|
// Check to see if we should be on by poking the schedules.
|
|
148
170
|
}
|
|
149
171
|
if (utils.isNullOrEmpty(this.circuit.connectionId) || utils.isNullOrEmpty(this.circuit.deviceBinding)) {
|
|
172
|
+
sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(cstate.id), cstate, val);
|
|
150
173
|
cstate.isOn = val;
|
|
151
174
|
return new InterfaceServerResponse(200, 'Success');
|
|
152
175
|
}
|
|
153
176
|
if (this._sequencing) return new InterfaceServerResponse(200, 'Success');
|
|
154
177
|
let res = await NixieEquipment.putDeviceService(this.circuit.connectionId, `/state/device/${this.circuit.deviceBinding}`, { isOn: val, latch: val ? 10000 : undefined });
|
|
155
178
|
if (res.status.code === 200) {
|
|
179
|
+
sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(cstate.id), cstate, val);
|
|
156
180
|
cstate.isOn = val;
|
|
157
181
|
// Set this up so we can process our egg timer.
|
|
158
182
|
if (!cstate.isOn && val) { this.timeOn = new Timestamp(); }
|
|
@@ -161,6 +185,17 @@ export class NixieCircuit extends NixieEquipment {
|
|
|
161
185
|
return res;
|
|
162
186
|
} catch (err) { logger.error(`Nixie: Error setting circuit state ${cstate.id}-${cstate.name} to ${val}`); }
|
|
163
187
|
}
|
|
188
|
+
public async checkCircuitEggTimerExpirationAsync(cstate: ICircuitState) {
|
|
189
|
+
// if circuit end time is past current time, either the schedule is finished
|
|
190
|
+
// (this should already be turned off) or the egg timer has expired
|
|
191
|
+
try {
|
|
192
|
+
if (!cstate.isActive || !cstate.isOn) return;
|
|
193
|
+
if (cstate.endTime.toDate() < new Timestamp().toDate()) {
|
|
194
|
+
await sys.board.circuits.setCircuitStateAsync(cstate.id, false);
|
|
195
|
+
cstate.emitEquipmentChange();
|
|
196
|
+
}
|
|
197
|
+
} catch (err) { logger.error(`Error syncing circuit: ${err}`); }
|
|
198
|
+
}
|
|
164
199
|
private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
|
|
165
200
|
try {
|
|
166
201
|
let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ParameterOutOfRangeError } from '../../Errors';
|
|
1
|
+
import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError, ParameterOutOfRangeError } from '../../Errors';
|
|
2
2
|
import { utils, Timestamp } from '../../Constants';
|
|
3
3
|
import { logger } from '../../../logger/Logger';
|
|
4
4
|
|
|
@@ -8,8 +8,10 @@ import { HeaterState, state, } from "../../State";
|
|
|
8
8
|
import { setTimeout, clearTimeout } from 'timers';
|
|
9
9
|
import { NixieControlPanel } from '../Nixie';
|
|
10
10
|
import { webApp, InterfaceServerResponse } from "../../../web/Server";
|
|
11
|
+
import { conn } from '../../../controller/comms/Comms';
|
|
12
|
+
import { Outbound, Protocol, Response } from '../../../controller/comms/messages/Messages';
|
|
11
13
|
|
|
12
|
-
export class NixieHeaterCollection extends NixieEquipmentCollection<
|
|
14
|
+
export class NixieHeaterCollection extends NixieEquipmentCollection<NixieHeaterBase> {
|
|
13
15
|
public async deleteHeaterAsync(id: number) {
|
|
14
16
|
try {
|
|
15
17
|
for (let i = this.length - 1; i >= 0; i--) {
|
|
@@ -21,24 +23,23 @@ export class NixieHeaterCollection extends NixieEquipmentCollection<NixieHeater>
|
|
|
21
23
|
}
|
|
22
24
|
} catch (err) { return Promise.reject(`Nixie Control Panel deleteHeaterAsync ${err.message}`); }
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
public async setHeaterStateAsync(hstate: HeaterState, val: boolean) {
|
|
26
|
+
public async setHeaterStateAsync(hstate: HeaterState, val: boolean, isCooling: boolean) {
|
|
26
27
|
try {
|
|
27
|
-
let h:
|
|
28
|
+
let h: NixieHeaterBase = this.find(elem => elem.id === hstate.id) as NixieHeaterBase;
|
|
28
29
|
if (typeof h === 'undefined') {
|
|
29
30
|
return Promise.reject(new Error(`NCP: Heater ${hstate.id}-${hstate.name} could not be found to set the state to ${val}.`));
|
|
30
31
|
}
|
|
31
|
-
await h.setHeaterStateAsync(hstate, val);
|
|
32
|
+
await h.setHeaterStateAsync(hstate, val, isCooling);
|
|
32
33
|
}
|
|
33
34
|
catch (err) { return logger.error(`NCP: setHeaterStateAsync ${hstate.id}-${hstate.name}: ${err.message}`); }
|
|
34
35
|
}
|
|
35
36
|
public async setHeaterAsync(heater: Heater, data: any) {
|
|
36
37
|
// By the time we get here we know that we are in control and this is a Nixie heater.
|
|
37
38
|
try {
|
|
38
|
-
let h:
|
|
39
|
+
let h: NixieHeaterBase = this.find(elem => elem.id === heater.id) as NixieHeaterBase;
|
|
39
40
|
if (typeof h === 'undefined') {
|
|
40
41
|
heater.master = 1;
|
|
41
|
-
h =
|
|
42
|
+
h = NixieHeaterBase.create(this.controlPanel, heater);
|
|
42
43
|
this.push(h);
|
|
43
44
|
await h.setHeaterAsync(data);
|
|
44
45
|
logger.info(`A Heater was not found for id #${heater.id} creating Heater`);
|
|
@@ -51,13 +52,14 @@ export class NixieHeaterCollection extends NixieEquipmentCollection<NixieHeater>
|
|
|
51
52
|
}
|
|
52
53
|
public async initAsync(heaters: HeaterCollection) {
|
|
53
54
|
try {
|
|
54
|
-
this.length = 0;
|
|
55
55
|
for (let i = 0; i < heaters.length; i++) {
|
|
56
56
|
let heater = heaters.getItemByIndex(i);
|
|
57
57
|
if (heater.master === 1) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
if (typeof this.find(elem => elem.id === heater.id) === 'undefined') {
|
|
59
|
+
logger.info(`Initializing Heater ${heater.name}`);
|
|
60
|
+
let nHeater = NixieHeaterBase.create(this.controlPanel, heater);
|
|
61
|
+
this.push(nHeater);
|
|
62
|
+
}
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
}
|
|
@@ -73,66 +75,190 @@ export class NixieHeaterCollection extends NixieEquipmentCollection<NixieHeater>
|
|
|
73
75
|
}
|
|
74
76
|
} catch (err) { } // Don't bail if we have an errror.
|
|
75
77
|
}
|
|
76
|
-
public async initHeaterAsync(heater: Heater): Promise<
|
|
78
|
+
public async initHeaterAsync(heater: Heater): Promise<NixieHeaterBase> {
|
|
77
79
|
try {
|
|
78
|
-
let c:
|
|
80
|
+
let c: NixieHeaterBase = this.find(elem => elem.id === heater.id) as NixieHeaterBase;
|
|
79
81
|
if (typeof c === 'undefined') {
|
|
80
|
-
c =
|
|
82
|
+
c = NixieHeaterBase.create(this.controlPanel, heater);
|
|
81
83
|
this.push(c);
|
|
82
84
|
}
|
|
83
85
|
return c;
|
|
84
86
|
} catch (err) { logger.error(`initHeaterAsync: ${err.message}`); return Promise.reject(err); }
|
|
85
87
|
}
|
|
86
|
-
|
|
87
88
|
}
|
|
88
|
-
export class
|
|
89
|
+
export class NixieHeaterBase extends NixieEquipment {
|
|
90
|
+
protected _suspendPolling: number = 0;
|
|
89
91
|
public pollingInterval: number = 10000;
|
|
90
|
-
private _pollTimer: NodeJS.Timeout = null;
|
|
91
|
-
private _lastState;
|
|
92
92
|
public heater: Heater;
|
|
93
|
+
protected _pollTimer: NodeJS.Timeout = null;
|
|
94
|
+
protected _lastState;
|
|
95
|
+
protected closing = false;
|
|
96
|
+
protected bodyOnTime: number;
|
|
97
|
+
protected isOn: boolean = false;
|
|
98
|
+
protected isCooling: boolean = false;
|
|
93
99
|
constructor(ncp: INixieControlPanel, heater: Heater) {
|
|
94
100
|
super(ncp);
|
|
95
101
|
this.heater = heater;
|
|
102
|
+
}
|
|
103
|
+
public get suspendPolling(): boolean { return this._suspendPolling > 0; }
|
|
104
|
+
public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
|
|
105
|
+
public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
|
|
106
|
+
public static create(ncp: INixieControlPanel, heater: Heater): NixieHeaterBase {
|
|
107
|
+
let type = sys.board.valueMaps.heaterTypes.transform(heater.type);
|
|
108
|
+
switch (type.name) {
|
|
109
|
+
case 'heatpump':
|
|
110
|
+
return new NixieHeatpump(ncp, heater);
|
|
111
|
+
case 'ultratemp':
|
|
112
|
+
return new NixieUltratemp(ncp, heater);
|
|
113
|
+
case 'gas':
|
|
114
|
+
return new NixieGasHeater(ncp, heater);
|
|
115
|
+
case 'mastertemp':
|
|
116
|
+
return new NixieMastertemp(ncp, heater);
|
|
117
|
+
case 'solar':
|
|
118
|
+
return new NixieSolarHeater(ncp, heater);
|
|
119
|
+
default:
|
|
120
|
+
return new NixieHeaterBase(ncp, heater);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
public isBodyOn() {
|
|
124
|
+
let isOn = sys.board.bodies.isBodyOn(this.heater.body);
|
|
125
|
+
if (isOn && typeof this.bodyOnTime === 'undefined') {
|
|
126
|
+
this.bodyOnTime = new Date().getTime();
|
|
127
|
+
}
|
|
128
|
+
else if (!isOn) this.bodyOnTime = undefined;
|
|
129
|
+
return isOn;
|
|
130
|
+
}
|
|
131
|
+
public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
|
|
132
|
+
try {
|
|
133
|
+
return Promise.reject(new InvalidOperationError(`You cannot change the state on this type of heater ${hstate.name}`, 'setHeaterStateAsync'));
|
|
134
|
+
} catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
|
|
135
|
+
}
|
|
136
|
+
public async setHeaterAsync(data: any) {
|
|
137
|
+
try {
|
|
138
|
+
let heater = this.heater;
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
catch (err) { logger.error(`Nixie setHeaterAsync: ${err.message}`); return Promise.reject(err); }
|
|
142
|
+
}
|
|
143
|
+
public async closeAsync() {}
|
|
144
|
+
}
|
|
145
|
+
export class NixieGasHeater extends NixieHeaterBase {
|
|
146
|
+
public pollingInterval: number = 10000;
|
|
147
|
+
declare heater: Heater;
|
|
148
|
+
constructor(ncp: INixieControlPanel, heater: Heater) {
|
|
149
|
+
super(ncp, heater);
|
|
150
|
+
this.heater = heater;
|
|
96
151
|
this.pollEquipmentAsync();
|
|
97
152
|
}
|
|
98
153
|
public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
|
|
99
154
|
public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
|
|
100
155
|
try {
|
|
101
|
-
//
|
|
156
|
+
// Initialize the desired state.
|
|
157
|
+
this.isOn = isOn;
|
|
158
|
+
this.isCooling = false;
|
|
159
|
+
// Here we go we need to set the firemans switch state.
|
|
102
160
|
if (hstate.isOn !== isOn) {
|
|
103
161
|
logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
|
|
104
162
|
}
|
|
105
163
|
if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
|
|
106
164
|
hstate.isOn = isOn;
|
|
107
|
-
return
|
|
165
|
+
return;
|
|
108
166
|
}
|
|
109
167
|
if (typeof this._lastState === 'undefined' || isOn || this._lastState !== isOn) {
|
|
110
168
|
let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`, { isOn: isOn, latch: isOn ? 10000 : undefined });
|
|
111
169
|
if (res.status.code === 200) this._lastState = hstate.isOn = isOn;
|
|
112
|
-
|
|
170
|
+
else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
|
|
113
171
|
}
|
|
114
172
|
else {
|
|
115
173
|
hstate.isOn = isOn;
|
|
116
|
-
return
|
|
174
|
+
return;
|
|
117
175
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
176
|
} catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
|
|
121
177
|
}
|
|
122
|
-
public async
|
|
178
|
+
public async pollEquipmentAsync() {
|
|
179
|
+
let self = this;
|
|
123
180
|
try {
|
|
124
|
-
|
|
181
|
+
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
182
|
+
this._pollTimer = null;
|
|
183
|
+
let success = false;
|
|
125
184
|
}
|
|
126
|
-
catch (err) { logger.error(`Nixie
|
|
185
|
+
catch (err) { logger.error(`Nixie Error polling Heater - ${err}`); }
|
|
186
|
+
finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
|
|
187
|
+
}
|
|
188
|
+
private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
|
|
189
|
+
try {
|
|
190
|
+
let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
|
|
191
|
+
return dev;
|
|
192
|
+
} catch (err) { logger.error(`Nixie Heater Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
|
|
193
|
+
}
|
|
194
|
+
public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
|
|
195
|
+
try {
|
|
196
|
+
if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
|
|
197
|
+
&& typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
|
|
198
|
+
try {
|
|
199
|
+
let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
|
|
200
|
+
// If we have a status check the return.
|
|
201
|
+
hstate.commStatus = stat.hasFault ? 1 : 0;
|
|
202
|
+
} catch (err) { hstate.commStatus = 1; }
|
|
203
|
+
}
|
|
204
|
+
else
|
|
205
|
+
hstate.commStatus = 0;
|
|
206
|
+
} catch (err) { logger.error(`Nixie Error checking heater Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
|
|
207
|
+
}
|
|
208
|
+
public async closeAsync() {
|
|
209
|
+
try {
|
|
210
|
+
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
211
|
+
this._pollTimer = null;
|
|
212
|
+
let hstate = state.heaters.getItemById(this.heater.id);
|
|
213
|
+
await this.setHeaterStateAsync(hstate, false);
|
|
214
|
+
hstate.emitEquipmentChange();
|
|
215
|
+
}
|
|
216
|
+
catch (err) { logger.error(`Nixie Heater closeAsync: ${err.message}`); return Promise.reject(err); }
|
|
217
|
+
}
|
|
218
|
+
public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
|
|
219
|
+
}
|
|
220
|
+
export class NixieSolarHeater extends NixieHeaterBase {
|
|
221
|
+
public pollingInterval: number = 10000;
|
|
222
|
+
declare heater: Heater;
|
|
223
|
+
constructor(ncp: INixieControlPanel, heater: Heater) {
|
|
224
|
+
super(ncp, heater);
|
|
225
|
+
this.heater = heater;
|
|
226
|
+
this.pollEquipmentAsync();
|
|
227
|
+
}
|
|
228
|
+
public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
|
|
229
|
+
public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
|
|
230
|
+
try {
|
|
231
|
+
// Initialize the desired state.
|
|
232
|
+
this.isOn = isOn;
|
|
233
|
+
this.isCooling = isCooling;
|
|
234
|
+
// Here we go we need to set the firemans switch state.
|
|
235
|
+
if (hstate.isOn !== isOn) {
|
|
236
|
+
logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
|
|
237
|
+
}
|
|
238
|
+
if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
|
|
239
|
+
hstate.isOn = isOn;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (typeof this._lastState === 'undefined' || isOn || this._lastState !== isOn) {
|
|
243
|
+
let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`, { isOn: isOn, latch: isOn ? 10000 : undefined });
|
|
244
|
+
if (res.status.code === 200) this._lastState = hstate.isOn = isOn;
|
|
245
|
+
else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
hstate.isOn = isOn;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
} catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
|
|
127
252
|
}
|
|
128
253
|
public async pollEquipmentAsync() {
|
|
254
|
+
let self = this;
|
|
129
255
|
try {
|
|
130
256
|
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
131
257
|
this._pollTimer = null;
|
|
132
258
|
let success = false;
|
|
133
259
|
}
|
|
134
260
|
catch (err) { logger.error(`Nixie Error polling Heater - ${err}`); }
|
|
135
|
-
finally { this._pollTimer = setTimeout(async () => await
|
|
261
|
+
finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
|
|
136
262
|
}
|
|
137
263
|
private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
|
|
138
264
|
try {
|
|
@@ -159,10 +285,157 @@ export class NixieHeater extends NixieEquipment {
|
|
|
159
285
|
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
160
286
|
this._pollTimer = null;
|
|
161
287
|
let hstate = state.heaters.getItemById(this.heater.id);
|
|
162
|
-
await this.setHeaterStateAsync(hstate, false);
|
|
288
|
+
await this.setHeaterStateAsync(hstate, false, false);
|
|
163
289
|
hstate.emitEquipmentChange();
|
|
164
290
|
}
|
|
165
291
|
catch (err) { logger.error(`Nixie Heater closeAsync: ${err.message}`); return Promise.reject(err); }
|
|
166
292
|
}
|
|
167
293
|
public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
|
|
168
294
|
}
|
|
295
|
+
export class NixieHeatpump extends NixieHeaterBase {
|
|
296
|
+
public configSent: boolean = false;
|
|
297
|
+
constructor(ncp: INixieControlPanel, heater: Heater) {
|
|
298
|
+
super(ncp, heater);
|
|
299
|
+
// Set the polling interval to 3 seconds.
|
|
300
|
+
this.pollEquipmentAsync();
|
|
301
|
+
}
|
|
302
|
+
public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
|
|
303
|
+
try {
|
|
304
|
+
this.suspendPolling = true;
|
|
305
|
+
this.isOn = isOn;
|
|
306
|
+
this.isCooling = isCooling;
|
|
307
|
+
} catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
|
|
308
|
+
finally { this.suspendPolling = false; }
|
|
309
|
+
}
|
|
310
|
+
public async pollEquipmentAsync() {
|
|
311
|
+
let self = this;
|
|
312
|
+
try {
|
|
313
|
+
this.suspendPolling = true;
|
|
314
|
+
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
315
|
+
this._pollTimer = null;
|
|
316
|
+
if (this._suspendPolling > 1) return;
|
|
317
|
+
let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
|
|
318
|
+
// If the body isn't on then we won't communicate with the chem controller. There is no need
|
|
319
|
+
// since most of the time these are attached to the filter relay.
|
|
320
|
+
if (this.isBodyOn() && !this.closing) {
|
|
321
|
+
await this.sendState(sheater);
|
|
322
|
+
//if (!this.closing) await this.requestStatus(sheater);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (err) { logger.error(`Error polling Heat Pump - ${err}`); }
|
|
326
|
+
finally {
|
|
327
|
+
this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
|
|
328
|
+
try { await self.pollEquipmentAsync() } catch (err) { }
|
|
329
|
+
}, this.pollingInterval || 10000);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
public async sendState(sheater: HeaterState): Promise<boolean> {
|
|
333
|
+
try {
|
|
334
|
+
sheater.type = 2;
|
|
335
|
+
let success = await new Promise<boolean>((resolve, reject) => {
|
|
336
|
+
let out = Outbound.create({
|
|
337
|
+
protocol: Protocol.Heater,
|
|
338
|
+
source: 16,
|
|
339
|
+
dest: this.heater.address,
|
|
340
|
+
action: 210,
|
|
341
|
+
payload: [210],
|
|
342
|
+
retries: 3, // We are going to try 4 times.
|
|
343
|
+
response: Response.create({ protocol: Protocol.IntelliChem, action: 18 }),
|
|
344
|
+
onAbort: () => { },
|
|
345
|
+
onComplete: (err) => {
|
|
346
|
+
if (err) {
|
|
347
|
+
// If the IntelliChem is not responding we need to store that off. If an 18 does
|
|
348
|
+
// come across this will be cleared by the processing of that message.
|
|
349
|
+
resolve(false);
|
|
350
|
+
}
|
|
351
|
+
else { resolve(true); }
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
conn.queueSendMessage(out);
|
|
355
|
+
});
|
|
356
|
+
return success;
|
|
357
|
+
} catch (err) { logger.error(`Communication error with Ultratemp : ${err.message}`); }
|
|
358
|
+
}
|
|
359
|
+
public async closeAsync() {
|
|
360
|
+
try {
|
|
361
|
+
this.suspendPolling = true;
|
|
362
|
+
this.closing = true;
|
|
363
|
+
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
364
|
+
this._pollTimer = null;
|
|
365
|
+
logger.info(`Closing Heater ${this.heater.name}`);
|
|
366
|
+
|
|
367
|
+
}
|
|
368
|
+
catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
export class NixieUltratemp extends NixieHeatpump {
|
|
372
|
+
constructor(ncp: INixieControlPanel, heater: Heater) {
|
|
373
|
+
super(ncp, heater);
|
|
374
|
+
// Set the polling interval to 3 seconds.
|
|
375
|
+
this.pollEquipmentAsync();
|
|
376
|
+
}
|
|
377
|
+
public async pollEquipmentAsync() {
|
|
378
|
+
let self = this;
|
|
379
|
+
try {
|
|
380
|
+
this.suspendPolling = true;
|
|
381
|
+
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
382
|
+
this._pollTimer = null;
|
|
383
|
+
if (this._suspendPolling > 1) return;
|
|
384
|
+
let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
|
|
385
|
+
// If the body isn't on then we won't communicate with the chem controller. There is no need
|
|
386
|
+
// since most of the time these are attached to the filter relay.
|
|
387
|
+
if (!this.closing) {
|
|
388
|
+
await this.setStatus(sheater);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (err) { logger.error(`Error polling UltraTemp heater - ${err}`); }
|
|
392
|
+
finally {
|
|
393
|
+
this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
|
|
394
|
+
try { await self.pollEquipmentAsync() } catch (err) {}
|
|
395
|
+
}, this.pollingInterval || 10000);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
public async setStatus(sheater: HeaterState): Promise<boolean> {
|
|
399
|
+
try {
|
|
400
|
+
let success = await new Promise<boolean>((resolve, reject) => {
|
|
401
|
+
let out = Outbound.create({
|
|
402
|
+
protocol: Protocol.Heater,
|
|
403
|
+
source: 16,
|
|
404
|
+
dest: this.heater.address,
|
|
405
|
+
action: 114,
|
|
406
|
+
payload: [],
|
|
407
|
+
retries: 3, // We are going to try 4 times.
|
|
408
|
+
response: Response.create({ protocol: Protocol.Heater, action: 115 }),
|
|
409
|
+
onAbort: () => { },
|
|
410
|
+
onComplete: (err) => {
|
|
411
|
+
if (err) {
|
|
412
|
+
// If the Ultratemp is not responding we need to store that off but at this point we know none of the codes. If a 115 does
|
|
413
|
+
// come across this will be cleared by the processing of that message.
|
|
414
|
+
sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
|
|
415
|
+
state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
|
|
416
|
+
resolve(false);
|
|
417
|
+
}
|
|
418
|
+
else { resolve(true); }
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
out.appendPayloadBytes(0, 10);
|
|
422
|
+
out.setPayloadByte(0, 144);
|
|
423
|
+
out.setPayloadByte(1, this.isOn ? (this.isCooling ? 2 : 1) : 0, 0);
|
|
424
|
+
conn.queueSendMessage(out);
|
|
425
|
+
});
|
|
426
|
+
return success;
|
|
427
|
+
} catch (err) { logger.error(`Communication error with Ultratemp : ${err.message}`); }
|
|
428
|
+
}
|
|
429
|
+
public async closeAsync() {
|
|
430
|
+
try {
|
|
431
|
+
this.suspendPolling = true;
|
|
432
|
+
this.closing = true;
|
|
433
|
+
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
434
|
+
this._pollTimer = null;
|
|
435
|
+
logger.info(`Closing Heater ${this.heater.name}`);
|
|
436
|
+
|
|
437
|
+
}
|
|
438
|
+
catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
export class NixieMastertemp extends NixieGasHeater {}
|