nodejs-poolcontroller 7.6.1 → 7.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.eslintrc.json +44 -44
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -0
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -0
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -0
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  6. package/CONTRIBUTING.md +74 -74
  7. package/Changelog +220 -215
  8. package/Dockerfile +17 -17
  9. package/Gruntfile.js +40 -40
  10. package/LICENSE +661 -661
  11. package/README.md +191 -191
  12. package/app.ts +1 -1
  13. package/config/Config.ts +14 -0
  14. package/config/VersionCheck.ts +2 -2
  15. package/controller/Constants.ts +2 -1
  16. package/controller/Equipment.ts +2484 -2459
  17. package/controller/Errors.ts +180 -180
  18. package/controller/Lockouts.ts +502 -436
  19. package/controller/State.ts +106 -30
  20. package/controller/boards/AquaLinkBoard.ts +1000 -0
  21. package/controller/boards/BoardFactory.ts +49 -45
  22. package/controller/boards/EasyTouchBoard.ts +2859 -2653
  23. package/controller/boards/IntelliCenterBoard.ts +4198 -4230
  24. package/controller/boards/IntelliComBoard.ts +63 -63
  25. package/controller/boards/IntelliTouchBoard.ts +273 -241
  26. package/controller/boards/NixieBoard.ts +1728 -1675
  27. package/controller/boards/SystemBoard.ts +4925 -4697
  28. package/controller/comms/Comms.ts +442 -479
  29. package/controller/comms/messages/Messages.ts +171 -25
  30. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -2
  31. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  32. package/controller/comms/messages/config/CircuitMessage.ts +1 -0
  33. package/controller/comms/messages/config/ConfigMessage.ts +0 -0
  34. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  35. package/controller/comms/messages/config/CustomNameMessage.ts +30 -30
  36. package/controller/comms/messages/config/EquipmentMessage.ts +0 -0
  37. package/controller/comms/messages/config/ExternalMessage.ts +0 -0
  38. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  39. package/controller/comms/messages/config/GeneralMessage.ts +0 -0
  40. package/controller/comms/messages/config/HeaterMessage.ts +142 -10
  41. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  42. package/controller/comms/messages/config/OptionsMessage.ts +4 -21
  43. package/controller/comms/messages/config/PumpMessage.ts +53 -35
  44. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  45. package/controller/comms/messages/config/ScheduleMessage.ts +350 -347
  46. package/controller/comms/messages/config/SecurityMessage.ts +0 -0
  47. package/controller/comms/messages/config/ValveMessage.ts +1 -1
  48. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +38 -86
  49. package/controller/comms/messages/status/EquipmentStateMessage.ts +58 -22
  50. package/controller/comms/messages/status/HeaterStateMessage.ts +116 -86
  51. package/controller/comms/messages/status/IntelliChemStateMessage.ts +445 -445
  52. package/controller/comms/messages/status/IntelliValveStateMessage.ts +35 -35
  53. package/controller/comms/messages/status/PumpStateMessage.ts +23 -1
  54. package/controller/comms/messages/status/VersionMessage.ts +0 -0
  55. package/controller/nixie/Nixie.ts +162 -162
  56. package/controller/nixie/NixieEquipment.ts +103 -103
  57. package/controller/nixie/bodies/Body.ts +120 -120
  58. package/controller/nixie/bodies/Filter.ts +135 -135
  59. package/controller/nixie/chemistry/ChemController.ts +2511 -2498
  60. package/controller/nixie/chemistry/Chlorinator.ts +363 -314
  61. package/controller/nixie/circuits/Circuit.ts +261 -248
  62. package/controller/nixie/heaters/Heater.ts +650 -648
  63. package/controller/nixie/pumps/Pump.ts +906 -661
  64. package/controller/nixie/schedules/Schedule.ts +313 -257
  65. package/controller/nixie/valves/Valve.ts +170 -170
  66. package/defaultConfig.json +306 -286
  67. package/logger/DataLogger.ts +448 -448
  68. package/logger/Logger.ts +0 -0
  69. package/package.json +56 -56
  70. package/tsconfig.json +25 -25
  71. package/web/Server.ts +92 -47
  72. package/web/bindings/aqualinkD.json +505 -0
  73. package/web/bindings/influxDB.json +1051 -1021
  74. package/web/bindings/mqtt.json +702 -654
  75. package/web/bindings/mqttAlt.json +731 -684
  76. package/web/bindings/rulesManager.json +54 -54
  77. package/web/bindings/smartThings-Hubitat.json +31 -31
  78. package/web/bindings/valveRelays.json +20 -20
  79. package/web/bindings/vera.json +25 -25
  80. package/web/interfaces/baseInterface.ts +137 -136
  81. package/web/interfaces/httpInterface.ts +145 -124
  82. package/web/interfaces/influxInterface.ts +276 -245
  83. package/web/interfaces/mqttInterface.ts +535 -475
  84. package/web/services/config/Config.ts +39 -18
  85. package/web/services/config/ConfigSocket.ts +0 -0
  86. package/web/services/state/State.ts +10 -0
  87. package/web/services/state/StateSocket.ts +4 -4
  88. package/web/services/utilities/Utilities.ts +44 -42
  89. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
  90. package/config copy.json +0 -300
  91. package/issue_template.md +0 -52
@@ -1,649 +1,651 @@
1
- import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError, ParameterOutOfRangeError } 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 { Heater, HeaterCollection, sys } from "../../../controller/Equipment";
7
- import { BodyTempState, HeaterState, state, } from "../../State";
8
- import { setTimeout, clearTimeout } from 'timers';
9
- import { NixieControlPanel } from '../Nixie';
10
- import { webApp, InterfaceServerResponse } from "../../../web/Server";
11
- import { conn } from '../../../controller/comms/Comms';
12
- import { Outbound, Protocol, Response } from '../../../controller/comms/messages/Messages';
13
- import { delayMgr } from '../../Lockouts';
14
-
15
- export class NixieHeaterCollection extends NixieEquipmentCollection<NixieHeaterBase> {
16
- public async deleteHeaterAsync(id: number) {
17
- try {
18
- for (let i = this.length - 1; i >= 0; i--) {
19
- let heater = this[i];
20
- if (heater.id === id) {
21
- await heater.closeAsync();
22
- this.splice(i, 1);
23
- }
24
- }
25
- } catch (err) { return Promise.reject(`Nixie Control Panel deleteHeaterAsync ${err.message}`); }
26
- }
27
- public async setHeaterStateAsync(hstate: HeaterState, val: boolean, isCooling: boolean) {
28
- try {
29
- let h: NixieHeaterBase = this.find(elem => elem.id === hstate.id) as NixieHeaterBase;
30
- if (typeof h === 'undefined') {
31
- return Promise.reject(new Error(`NCP: Heater ${hstate.id}-${hstate.name} could not be found to set the state to ${val}.`));
32
- }
33
- await h.setHeaterStateAsync(hstate, val, isCooling);
34
- }
35
- catch (err) { return logger.error(`NCP: setHeaterStateAsync ${hstate.id}-${hstate.name}: ${err.message}`); }
36
- }
37
- public async setHeaterAsync(heater: Heater, data: any) {
38
- // By the time we get here we know that we are in control and this is a Nixie heater.
39
- try {
40
- let h: NixieHeaterBase = this.find(elem => elem.id === heater.id) as NixieHeaterBase;
41
- if (typeof h === 'undefined') {
42
- heater.master = 1;
43
- h = NixieHeaterBase.create(this.controlPanel, heater);
44
- this.push(h);
45
- await h.setHeaterAsync(data);
46
- logger.info(`A Heater was not found for id #${heater.id} creating Heater`);
47
- }
48
- else {
49
- await h.setHeaterAsync(data);
50
- }
51
- }
52
- catch (err) { logger.error(`setHeaterAsync: ${err.message}`); return Promise.reject(err); }
53
- }
54
- public async initAsync(heaters: HeaterCollection) {
55
- try {
56
- for (let i = 0; i < heaters.length; i++) {
57
- let heater = heaters.getItemByIndex(i);
58
- if (heater.master === 1) {
59
- if (typeof this.find(elem => elem.id === heater.id) === 'undefined') {
60
- logger.info(`Initializing Heater ${heater.name}`);
61
- let nHeater = NixieHeaterBase.create(this.controlPanel, heater);
62
- this.push(nHeater);
63
- }
64
- }
65
- }
66
- }
67
- catch (err) { logger.error(`Nixie Heater initAsync: ${err.message}`); return Promise.reject(err); }
68
- }
69
- public async closeAsync() {
70
- try {
71
- for (let i = this.length - 1; i >= 0; i--) {
72
- try {
73
- await this[i].closeAsync();
74
- this.splice(i, 1);
75
- } catch (err) { logger.error(`Error stopping Nixie Heater ${err}`); }
76
- }
77
- } catch (err) { } // Don't bail if we have an errror.
78
- }
79
- public async initHeaterAsync(heater: Heater): Promise<NixieHeaterBase> {
80
- try {
81
- let c: NixieHeaterBase = this.find(elem => elem.id === heater.id) as NixieHeaterBase;
82
- if (typeof c === 'undefined') {
83
- c = NixieHeaterBase.create(this.controlPanel, heater);
84
- this.push(c);
85
- }
86
- return c;
87
- } catch (err) { logger.error(`initHeaterAsync: ${err.message}`); return Promise.reject(err); }
88
- }
89
- }
90
- export class NixieHeaterBase extends NixieEquipment {
91
- protected _suspendPolling: number = 0;
92
- public pollingInterval: number = 10000;
93
- public heater: Heater;
94
- protected _pollTimer: NodeJS.Timeout = null;
95
- protected _lastState;
96
- protected closing = false;
97
- protected bodyOnTime: number;
98
- protected isOn: boolean = false;
99
- protected isCooling: boolean = false;
100
- protected lastHeatCycle: Date;
101
- protected lastCoolCycle: Date;
102
- constructor(ncp: INixieControlPanel, heater: Heater) {
103
- super(ncp);
104
- this.heater = heater;
105
- }
106
- public get suspendPolling(): boolean { return this._suspendPolling > 0; }
107
- public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
108
- public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
109
- public getCooldownTime() { return 0; }
110
- public static create(ncp: INixieControlPanel, heater: Heater): NixieHeaterBase {
111
- let type = sys.board.valueMaps.heaterTypes.transform(heater.type);
112
- switch (type.name) {
113
- case 'heatpump':
114
- return new NixieHeatpump(ncp, heater);
115
- case 'ultratemp':
116
- return new NixieUltratemp(ncp, heater);
117
- case 'gas':
118
- return new NixieGasHeater(ncp, heater);
119
- case 'mastertemp':
120
- return new NixieMastertemp(ncp, heater);
121
- case 'solar':
122
- return new NixieSolarHeater(ncp, heater);
123
- default:
124
- return new NixieHeaterBase(ncp, heater);
125
- }
126
- }
127
- public isBodyOn() {
128
- let isOn = sys.board.bodies.isBodyOn(this.heater.body);
129
- if (isOn && typeof this.bodyOnTime === 'undefined') {
130
- this.bodyOnTime = new Date().getTime();
131
- }
132
- else if (!isOn) this.bodyOnTime = undefined;
133
- return isOn;
134
- }
135
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
136
- try {
137
- return Promise.reject(new InvalidOperationError(`You cannot change the state on this type of heater ${hstate.name}`, 'setHeaterStateAsync'));
138
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
139
- }
140
- public async setHeaterAsync(data: any) {
141
- try {
142
- let heater = this.heater;
143
-
144
- }
145
- catch (err) { logger.error(`Nixie setHeaterAsync: ${err.message}`); return Promise.reject(err); }
146
- }
147
- public async closeAsync() {}
148
- }
149
- export class NixieGasHeater extends NixieHeaterBase {
150
- public pollingInterval: number = 10000;
151
- //declare heater: Heater;
152
- constructor(ncp: INixieControlPanel, heater: Heater) {
153
- super(ncp, heater);
154
- this.heater = heater;
155
- this.pollEquipmentAsync();
156
- }
157
- public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
158
- public getCooldownTime(): number {
159
- // Delays are always in terms of seconds so convert the minute to seconds.
160
- if (this.heater.cooldownDelay === 0 || typeof this.lastHeatCycle === 'undefined') return 0;
161
- let now = new Date().getTime();
162
- let cooldown = this.isOn ? this.heater.cooldownDelay * 60000 : Math.round( ((this.lastHeatCycle.getDate() + this.heater.cooldownDelay * 60000) - now) / 1000);
163
- return Math.min(Math.max(0, cooldown), this.heater.cooldownDelay * 60);
164
- }
165
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
166
- try {
167
- // Initialize the desired state.
168
- this.isOn = isOn;
169
- this.isCooling = false;
170
- let target = hstate.startupDelay === false && isOn;
171
- if (this.getCooldownTime() > 0) target = false;
172
- if (target && typeof hstate.endTime !== 'undefined') {
173
- // Calculate a short cycle time so that the gas heater does not cycle
174
- // too often. For gas heaters this is 60 seconds. This gives enough time
175
- // for the heater control circuit to make a full cycle.
176
- if (new Date().getTime() - hstate.endTime.getTime() < 60000) {
177
- logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
178
- target = false;
179
- }
180
- }
181
- // Here we go we need to set the firemans switch state.
182
- if (hstate.isOn !== target) {
183
- logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
184
- }
185
- if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
186
- if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
187
- this._lastState = hstate.isOn = target;
188
- }
189
- else {
190
- let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
191
- { isOn: target, latch: target ? 10000 : undefined });
192
- if (res.status.code === 200) this._lastState = hstate.isOn = target;
193
- else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
194
- }
195
- if (target) this.lastHeatCycle = new Date();
196
- }
197
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
198
- }
199
- public async pollEquipmentAsync() {
200
- let self = this;
201
- try {
202
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
203
- this._pollTimer = null;
204
- let success = false;
205
- }
206
- catch (err) { logger.error(`Nixie Error polling Heater - ${err}`); }
207
- finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
208
- }
209
- private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
210
- try {
211
- let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
212
- return dev;
213
- } catch (err) { logger.error(`Nixie Heater Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
214
- }
215
- public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
216
- try {
217
- if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
218
- && typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
219
- try {
220
- let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
221
- // If we have a status check the return.
222
- hstate.commStatus = stat.hasFault ? 1 : 0;
223
- } catch (err) { hstate.commStatus = 1; }
224
- }
225
- else
226
- hstate.commStatus = 0;
227
- } catch (err) { logger.error(`Nixie Error checking heater Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
228
- }
229
- public async closeAsync() {
230
- try {
231
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
232
- this._pollTimer = null;
233
- let hstate = state.heaters.getItemById(this.heater.id);
234
- await this.setHeaterStateAsync(hstate, false);
235
- hstate.emitEquipmentChange();
236
- }
237
- catch (err) { logger.error(`Nixie Heater closeAsync: ${err.message}`); return Promise.reject(err); }
238
- }
239
- public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
240
- }
241
- export class NixieSolarHeater extends NixieHeaterBase {
242
- public pollingInterval: number = 10000;
243
- declare heater: Heater;
244
- constructor(ncp: INixieControlPanel, heater: Heater) {
245
- super(ncp, heater);
246
- this.heater = heater;
247
- this.pollEquipmentAsync();
248
- }
249
- public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
250
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
251
- try {
252
- let origState = hstate.isOn;
253
- // Initialize the desired state.
254
- this.isOn = isOn;
255
- this.isCooling = isCooling;
256
- let target = hstate.startupDelay === false && isOn;
257
- if (this.getCooldownTime() > 0) target = false;
258
- if (target && typeof hstate.endTime !== 'undefined') {
259
- // Calculate a short cycle time so that the solar heater does not cycle
260
- // too often. For solar heaters this is 60 seconds. This gives enough time
261
- // for the valve to rotate and start heating. If the solar and water sensors are
262
- // not having issues this should be plenty of time.
263
- if (new Date().getTime() - hstate.endTime.getTime() < 60000) {
264
- logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
265
- target = false;
266
- }
267
- }
268
-
269
- // Here we go we need to set the valve status that is attached to solar.
270
- if (hstate.isOn !== target) {
271
- logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
272
- }
273
- if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
274
- if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
275
- this._lastState = hstate.isOn = target;
276
- }
277
- else {
278
- let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
279
- { isOn: target, latch: target ? 10000 : undefined });
280
- if (res.status.code === 200) this._lastState = hstate.isOn = target;
281
- else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
282
- }
283
- if (target) {
284
- if (isCooling) this.lastCoolCycle = new Date();
285
- else if (isOn) this.lastHeatCycle = new Date();
286
- }
287
- }
288
- // In this instance we need to see if there are cleaner circuits that we need to turn off
289
- // then delay for the current body because the solar just came on.
290
- if (hstate.isOn && sys.general.options.cleanerSolarDelay && !origState) {
291
- let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('cleaner') !== -1 && x.body === hstate.bodyId });
292
- let cleaners = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
293
- // Turn off all the cleaner circuits and set an on delay if they are on.
294
- for (let i = 0; i < cleaners.length; i++) {
295
- let cleaner = cleaners.getItemByIndex(i);
296
- if (cleaner.isActive) {
297
- let cstate = state.circuits.getItemById(cleaner.id);
298
- if (cstate.isOn && sys.general.options.cleanerSolarDelayTime > 0) {
299
- // Turn off the circuit then set a delay.
300
- logger.info(`Setting cleaner solar delay for ${cleaner.name} to ${sys.general.options.cleanerSolarDelayTime}`);
301
- await sys.board.circuits.setCircuitStateAsync(cstate.id, false);
302
- delayMgr.setCleanerStartDelay(cstate, hstate.bodyId, sys.general.options.cleanerSolarDelayTime);
303
- }
304
- }
305
- }
306
- }
307
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
308
- }
309
- public async pollEquipmentAsync() {
310
- let self = this;
311
- try {
312
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
313
- this._pollTimer = null;
314
- let success = false;
315
- }
316
- catch (err) { logger.error(`Nixie Error polling Heater - ${err}`); }
317
- finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
318
- }
319
- private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
320
- try {
321
- let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
322
- return dev;
323
- } catch (err) { logger.error(`Nixie Heater Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
324
- }
325
- public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
326
- try {
327
- if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
328
- && typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
329
- try {
330
- let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
331
- // If we have a status check the return.
332
- hstate.commStatus = stat.hasFault ? 1 : 0;
333
- } catch (err) { hstate.commStatus = 1; }
334
- }
335
- else
336
- hstate.commStatus = 0;
337
- } catch (err) { logger.error(`Nixie Error checking heater Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
338
- }
339
- public async closeAsync() {
340
- try {
341
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
342
- this._pollTimer = null;
343
- let hstate = state.heaters.getItemById(this.heater.id);
344
- await this.setHeaterStateAsync(hstate, false, false);
345
- hstate.emitEquipmentChange();
346
- }
347
- catch (err) { logger.error(`Nixie Heater closeAsync: ${err.message}`); return Promise.reject(err); }
348
- }
349
- public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
350
- }
351
- export class NixieHeatpump extends NixieHeaterBase {
352
- public configSent: boolean = false;
353
- constructor(ncp: INixieControlPanel, heater: Heater) {
354
- super(ncp, heater);
355
- // Set the polling interval to 3 seconds.
356
- this.pollEquipmentAsync();
357
- }
358
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
359
- try {
360
- this.suspendPolling = true;
361
- this.isOn = isOn;
362
- this.isCooling = isCooling;
363
- if (!hstate.startupDelay) {
364
- if (isOn && !isCooling) this.lastHeatCycle = new Date();
365
- else if (isCooling) this.lastCoolCycle = new Date();
366
- }
367
- // When this is implemented lets not forget to deal with the startup and any desired cool down delay. See UltraTemp for implementation.
368
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
369
- finally { this.suspendPolling = false; }
370
- }
371
- public async pollEquipmentAsync() {
372
- let self = this;
373
- try {
374
- this.suspendPolling = true;
375
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
376
- this._pollTimer = null;
377
- if (this._suspendPolling > 1) return;
378
- let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
379
- // If the body isn't on then we won't communicate with the chem controller. There is no need
380
- // since most of the time these are attached to the filter relay.
381
- if (this.isBodyOn() && !this.closing) {
382
- await this.sendState(sheater);
383
- //if (!this.closing) await this.requestStatus(sheater);
384
- }
385
- }
386
- catch (err) { logger.error(`Error polling Heat Pump - ${err}`); }
387
- finally {
388
- this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
389
- try { await self.pollEquipmentAsync() } catch (err) { }
390
- }, this.pollingInterval || 10000);
391
- }
392
- }
393
- public async sendState(sheater: HeaterState): Promise<boolean> {
394
- try {
395
- sheater.type = 2;
396
- let success = await new Promise<boolean>((resolve, reject) => {
397
- let out = Outbound.create({
398
- protocol: Protocol.Heater,
399
- source: 16,
400
- dest: this.heater.address,
401
- action: 210,
402
- payload: [210],
403
- retries: 3, // We are going to try 4 times.
404
- response: Response.create({ protocol: Protocol.IntelliChem, action: 18 }),
405
- onAbort: () => { },
406
- onComplete: (err) => {
407
- if (err) {
408
- // If the IntelliChem is not responding we need to store that off. If an 18 does
409
- // come across this will be cleared by the processing of that message.
410
- resolve(false);
411
- }
412
- else { resolve(true); }
413
- }
414
- });
415
- conn.queueSendMessage(out);
416
- });
417
- return success;
418
- } catch (err) { logger.error(`Communication error with Ultratemp : ${err.message}`); }
419
- }
420
- public async closeAsync() {
421
- try {
422
- this.suspendPolling = true;
423
- this.closing = true;
424
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
425
- this._pollTimer = null;
426
- logger.info(`Closing Heater ${this.heater.name}`);
427
-
428
- }
429
- catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
430
- }
431
- }
432
- export class NixieUltratemp extends NixieHeatpump {
433
- constructor(ncp: INixieControlPanel, heater: Heater) {
434
- super(ncp, heater);
435
- // Set the polling interval to 3 seconds.
436
- this.pollEquipmentAsync();
437
- }
438
- public async pollEquipmentAsync() {
439
- let self = this;
440
- try {
441
- this.suspendPolling = true;
442
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
443
- this._pollTimer = null;
444
- if (this._suspendPolling > 1) return;
445
- let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
446
- // If the body isn't on then we won't communicate with the chem controller. There is no need
447
- // since most of the time these are attached to the filter relay.
448
- if (!this.closing) {
449
- await this.setStatus(sheater);
450
- }
451
- }
452
- catch (err) { logger.error(`Error polling UltraTemp heater - ${err}`); }
453
- finally {
454
- this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
455
- try { await self.pollEquipmentAsync() } catch (err) {}
456
- }, this.pollingInterval || 10000);
457
- }
458
- }
459
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
460
- try {
461
- // Initialize the desired state.
462
- this.isOn = this.getCooldownTime() > 0 ? false : isOn;
463
- this.isCooling = isCooling;
464
- if (hstate.isOn !== isOn) {
465
- logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isCooling ? 'cooling' : isOn ? 'heating' : 'off'}`);
466
-
467
- }
468
- if (isOn && !hstate.startupDelay) this.lastHeatCycle = new Date();
469
- hstate.isOn = isOn;
470
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
471
- }
472
- public async releaseHeater(sheater: HeaterState): Promise<boolean> {
473
- try {
474
- let success = await new Promise<boolean>((resolve, reject) => {
475
- let out = Outbound.create({
476
- protocol: Protocol.Heater,
477
- source: 16,
478
- dest: this.heater.address,
479
- action: 114,
480
- payload: [],
481
- retries: 3, // We are going to try 4 times.
482
- response: Response.create({ protocol: Protocol.Heater, action: 115 }),
483
- onAbort: () => { },
484
- onComplete: (err) => {
485
- if (err) {
486
- // 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
487
- // come across this will be cleared by the processing of that message.
488
- sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
489
- state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
490
- resolve(false);
491
- }
492
- else { resolve(true); }
493
- }
494
- });
495
- out.appendPayloadBytes(0, 10);
496
- out.setPayloadByte(0, 144);
497
- out.setPayloadByte(1, 0, 0);
498
- conn.queueSendMessage(out);
499
- });
500
- return success;
501
- } catch (err) { logger.error(`Communication error with Ultratemp : ${err.message}`); }
502
- }
503
- public async setStatus(sheater: HeaterState): Promise<boolean> {
504
- try {
505
- let success = await new Promise<boolean>((resolve, reject) => {
506
- let out = Outbound.create({
507
- protocol: Protocol.Heater,
508
- source: 16,
509
- dest: this.heater.address,
510
- action: 114,
511
- payload: [],
512
- retries: 3, // We are going to try 4 times.
513
- response: Response.create({ protocol: Protocol.Heater, action: 115 }),
514
- onAbort: () => { },
515
- onComplete: (err) => {
516
- if (err) {
517
- // 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
518
- // come across this will be cleared by the processing of that message.
519
- sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
520
- state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
521
- resolve(false);
522
- }
523
- else { resolve(true); }
524
- }
525
- });
526
- out.appendPayloadBytes(0, 10);
527
- out.setPayloadByte(0, 144);
528
- // If we are in startup delay simply tell the heater that it is off.
529
- if (sheater.startupDelay || this.closing)
530
- out.setPayloadByte(1, 0, 0);
531
- else {
532
- if (this.getCooldownTime() > 0 ? false : this.isOn) {
533
- if (!this.isCooling) this.lastHeatCycle = new Date();
534
- else this.lastCoolCycle = new Date();
535
- }
536
- out.setPayloadByte(1, this.isOn ? (this.isCooling ? 2 : 1) : 0, 0);
537
- }
538
- conn.queueSendMessage(out);
539
- });
540
- return success;
541
- } catch (err) { logger.error(`Communication error with Ultratemp : ${err.message}`); }
542
- }
543
- public async closeAsync() {
544
- try {
545
- this.suspendPolling = true;
546
- this.closing = true;
547
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
548
- this._pollTimer = null;
549
- let sheater = state.heaters.getItemById(this.id);
550
- await this.releaseHeater(sheater);
551
- logger.info(`Closing Heater ${this.heater.name}`);
552
- }
553
- catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
554
- }
555
- }
556
- export class NixieMastertemp extends NixieGasHeater {
557
- constructor(ncp: INixieControlPanel, heater: Heater) {
558
- super(ncp, heater);
559
- // Set the polling interval to 3 seconds.
560
- this.pollEquipmentAsync();
561
- this.pollingInterval = 3000;
562
- }
563
- /* public getCooldownTime(): number {
564
- // Delays are always in terms of seconds so convert the minute to seconds.
565
- if (this.heater.cooldownDelay === 0 || typeof this.lastHeatCycle === 'undefined') return 0;
566
- let now = new Date().getTime();
567
- let cooldown = this.isOn ? this.heater.cooldownDelay * 60000 : Math.round(((this.lastHeatCycle.getDate() + this.heater.cooldownDelay * 60000) - now) / 1000);
568
- return Math.min(Math.max(0, cooldown), this.heater.cooldownDelay * 60);
569
- } */
570
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
571
- try {
572
- // Initialize the desired state.
573
- this.isOn = this.getCooldownTime() > 0 ? false : isOn;
574
- this.isCooling = false;
575
- // Here we go we need to set the firemans switch state.
576
- if (hstate.isOn !== isOn) {
577
- logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
578
- }
579
- if (isOn && !hstate.startupDelay) this.lastHeatCycle = new Date();
580
- hstate.isOn = isOn;
581
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
582
- }
583
- public async pollEquipmentAsync() {
584
- let self = this;
585
- try {
586
- this.suspendPolling = true;
587
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
588
- this._pollTimer = null;
589
- if (this._suspendPolling > 1) return;
590
- let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
591
- if (!this.closing) await this.setStatus(sheater);
592
- }
593
- catch (err) { logger.error(`Error polling MasterTemp heater - ${err}`); }
594
- finally {
595
- this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
596
- try { await self.pollEquipmentAsync() } catch (err) { }
597
- }, this.pollingInterval || 3000);
598
- }
599
- }
600
- public async setStatus(sheater: HeaterState): Promise<boolean> {
601
- try {
602
- let success = await new Promise<boolean>((resolve, reject) => {
603
- let out = Outbound.create({
604
- protocol: Protocol.Heater,
605
- source: 16,
606
- dest: this.heater.address,
607
- action: 112,
608
- payload: [],
609
- retries: 3, // We are going to try 4 times.
610
- response: Response.create({ protocol: Protocol.Heater, action: 116 }),
611
- onAbort: () => { },
612
- onComplete: (err) => {
613
- if (err) {
614
- // If the MasterTemp is not responding we need to store that off but at this point we know none of the codes. If a 115 does
615
- // come across this will be cleared by the processing of that message.
616
- sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
617
- state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
618
- resolve(false);
619
- }
620
- else { resolve(true); }
621
- }
622
- });
623
- out.appendPayloadBytes(0, 11);
624
- // If we have a startup delay we need to simply send 0 to the heater to make sure that it is off.
625
- if (sheater.startupDelay)
626
- out.setPayloadByte(0, 0);
627
- else {
628
- // The cooldown delay is a bit hard to figure out here since I think the heater does it on its own.
629
- out.setPayloadByte(0, sheater.bodyId <= 2 ? sheater.bodyId : 0);
630
- }
631
- out.setPayloadByte(1, sys.bodies.getItemById(1).heatSetpoint || 0);
632
- out.setPayloadByte(2, sys.bodies.getItemById(2).heatSetpoint || 0);
633
- conn.queueSendMessage(out);
634
- });
635
- return success;
636
- } catch (err) { logger.error(`Communication error with MasterTemp : ${err.message}`); }
637
- }
638
- public async closeAsync() {
639
- try {
640
- this.suspendPolling = true;
641
- this.closing = true;
642
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
643
- this._pollTimer = null;
644
- logger.info(`Closing Heater ${this.heater.name}`);
645
-
646
- }
647
- catch (err) { logger.error(`MasterTemp closeAsync: ${err.message}`); return Promise.reject(err); }
648
- }
1
+ import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError, ParameterOutOfRangeError } 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 { Heater, HeaterCollection, sys } from "../../../controller/Equipment";
7
+ import { BodyTempState, HeaterState, state, } from "../../State";
8
+ import { setTimeout, clearTimeout } from 'timers';
9
+ import { NixieControlPanel } from '../Nixie';
10
+ import { webApp, InterfaceServerResponse } from "../../../web/Server";
11
+ import { conn } from '../../../controller/comms/Comms';
12
+ import { Outbound, Protocol, Response } from '../../../controller/comms/messages/Messages';
13
+ import { delayMgr } from '../../Lockouts';
14
+
15
+ export class NixieHeaterCollection extends NixieEquipmentCollection<NixieHeaterBase> {
16
+ public async deleteHeaterAsync(id: number) {
17
+ try {
18
+ for (let i = this.length - 1; i >= 0; i--) {
19
+ let heater = this[i];
20
+ if (heater.id === id) {
21
+ await heater.closeAsync();
22
+ this.splice(i, 1);
23
+ }
24
+ }
25
+ } catch (err) { return Promise.reject(`Nixie Control Panel deleteHeaterAsync ${err.message}`); }
26
+ }
27
+ public async setHeaterStateAsync(hstate: HeaterState, val: boolean, isCooling: boolean) {
28
+ try {
29
+ let h: NixieHeaterBase = this.find(elem => elem.id === hstate.id) as NixieHeaterBase;
30
+ if (typeof h === 'undefined') {
31
+ return Promise.reject(new Error(`NCP: Heater ${hstate.id}-${hstate.name} could not be found to set the state to ${val}.`));
32
+ }
33
+ await h.setHeaterStateAsync(hstate, val, isCooling);
34
+ }
35
+ catch (err) { return logger.error(`NCP: setHeaterStateAsync ${hstate.id}-${hstate.name}: ${err.message}`); }
36
+ }
37
+ public async setHeaterAsync(heater: Heater, data: any) {
38
+ // By the time we get here we know that we are in control and this is a Nixie heater.
39
+ try {
40
+ let h: NixieHeaterBase = this.find(elem => elem.id === heater.id) as NixieHeaterBase;
41
+ if (typeof h === 'undefined') {
42
+ heater.master = 1;
43
+ h = NixieHeaterBase.create(this.controlPanel, heater);
44
+ this.push(h);
45
+ await h.setHeaterAsync(data);
46
+ logger.info(`A Heater was not found for id #${heater.id} creating Heater`);
47
+ }
48
+ else {
49
+ await h.setHeaterAsync(data);
50
+ }
51
+ }
52
+ catch (err) { logger.error(`setHeaterAsync: ${err.message}`); return Promise.reject(err); }
53
+ }
54
+ public async initAsync(heaters: HeaterCollection) {
55
+ try {
56
+ for (let i = 0; i < heaters.length; i++) {
57
+ let heater = heaters.getItemByIndex(i);
58
+ if (heater.master === 1) {
59
+ if (typeof this.find(elem => elem.id === heater.id) === 'undefined') {
60
+ logger.info(`Initializing Heater ${heater.name}`);
61
+ let nHeater = NixieHeaterBase.create(this.controlPanel, heater);
62
+ this.push(nHeater);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ catch (err) { logger.error(`Nixie Heater initAsync: ${err.message}`); return Promise.reject(err); }
68
+ }
69
+ public async closeAsync() {
70
+ try {
71
+ for (let i = this.length - 1; i >= 0; i--) {
72
+ try {
73
+ await this[i].closeAsync();
74
+ this.splice(i, 1);
75
+ } catch (err) { logger.error(`Error stopping Nixie Heater ${err}`); }
76
+ }
77
+ } catch (err) { } // Don't bail if we have an errror.
78
+ }
79
+ public async initHeaterAsync(heater: Heater): Promise<NixieHeaterBase> {
80
+ try {
81
+ let c: NixieHeaterBase = this.find(elem => elem.id === heater.id) as NixieHeaterBase;
82
+ if (typeof c === 'undefined') {
83
+ c = NixieHeaterBase.create(this.controlPanel, heater);
84
+ this.push(c);
85
+ }
86
+ return c;
87
+ } catch (err) { logger.error(`initHeaterAsync: ${err.message}`); return Promise.reject(err); }
88
+ }
89
+ }
90
+ export class NixieHeaterBase extends NixieEquipment {
91
+ protected _suspendPolling: number = 0;
92
+ public pollingInterval: number = 10000;
93
+ public heater: Heater;
94
+ protected _pollTimer: NodeJS.Timeout = null;
95
+ protected _lastState;
96
+ protected closing = false;
97
+ protected bodyOnTime: number;
98
+ protected isOn: boolean = false;
99
+ protected isCooling: boolean = false;
100
+ protected lastHeatCycle: Date;
101
+ protected lastCoolCycle: Date;
102
+ constructor(ncp: INixieControlPanel, heater: Heater) {
103
+ super(ncp);
104
+ this.heater = heater;
105
+ }
106
+ public get suspendPolling(): boolean { return this._suspendPolling > 0; }
107
+ public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
108
+ public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
109
+ public getCooldownTime() { return 0; }
110
+ public static create(ncp: INixieControlPanel, heater: Heater): NixieHeaterBase {
111
+ let type = sys.board.valueMaps.heaterTypes.transform(heater.type);
112
+ switch (type.name) {
113
+ case 'heatpump':
114
+ return new NixieHeatpump(ncp, heater);
115
+ case 'ultratemp':
116
+ return new NixieUltratemp(ncp, heater);
117
+ case 'gas':
118
+ return new NixieGasHeater(ncp, heater);
119
+ case 'mastertemp':
120
+ return new NixieMastertemp(ncp, heater);
121
+ case 'solar':
122
+ return new NixieSolarHeater(ncp, heater);
123
+ default:
124
+ return new NixieHeaterBase(ncp, heater);
125
+ }
126
+ }
127
+ public isBodyOn() {
128
+ let isOn = sys.board.bodies.isBodyOn(this.heater.body);
129
+ if (isOn && typeof this.bodyOnTime === 'undefined') {
130
+ this.bodyOnTime = new Date().getTime();
131
+ }
132
+ else if (!isOn) this.bodyOnTime = undefined;
133
+ return isOn;
134
+ }
135
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
136
+ try {
137
+ return Promise.reject(new InvalidOperationError(`You cannot change the state on this type of heater ${hstate.name}`, 'setHeaterStateAsync'));
138
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
139
+ }
140
+ public async setHeaterAsync(data: any) {
141
+ try {
142
+ let heater = this.heater;
143
+
144
+ }
145
+ catch (err) { logger.error(`Nixie setHeaterAsync: ${err.message}`); return Promise.reject(err); }
146
+ }
147
+ public async closeAsync() {}
148
+ }
149
+ export class NixieGasHeater extends NixieHeaterBase {
150
+ public pollingInterval: number = 10000;
151
+ //declare heater: Heater;
152
+ constructor(ncp: INixieControlPanel, heater: Heater) {
153
+ super(ncp, heater);
154
+ this.heater = heater;
155
+ if (typeof this.heater.stopTempDelta === 'undefined') this.heater.stopTempDelta = 1;
156
+ if (typeof this.heater.minCycleTime === 'undefined') this.heater.minCycleTime = 2;
157
+ this.pollEquipmentAsync();
158
+ }
159
+ public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
160
+ public getCooldownTime(): number {
161
+ // Delays are always in terms of seconds so convert the minute to seconds.
162
+ if (this.heater.cooldownDelay === 0 || typeof this.lastHeatCycle === 'undefined') return 0;
163
+ let now = new Date().getTime();
164
+ let cooldown = this.isOn ? this.heater.cooldownDelay * 60000 : Math.round( ((this.lastHeatCycle.getDate() + this.heater.cooldownDelay * 60000) - now) / 1000);
165
+ return Math.min(Math.max(0, cooldown), this.heater.cooldownDelay * 60);
166
+ }
167
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
168
+ try {
169
+ // Initialize the desired state.
170
+ this.isOn = isOn;
171
+ this.isCooling = false;
172
+ let target = hstate.startupDelay === false && isOn;
173
+ if (target && typeof hstate.endTime !== 'undefined') {
174
+ // Calculate a short cycle time so that the gas heater does not cycle
175
+ // too often. For gas heaters this is 60 seconds. This gives enough time
176
+ // for the heater control circuit to make a full cycle.
177
+ if (new Date().getTime() - hstate.endTime.getTime() < this.heater.minCycleTime * 60000) {
178
+ logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
179
+ target = false;
180
+ }
181
+ }
182
+ // Here we go we need to set the firemans switch state.
183
+ if (hstate.isOn !== target) {
184
+ logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
185
+ }
186
+ if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
187
+ if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
188
+ this._lastState = hstate.isOn = target;
189
+ }
190
+ else {
191
+ let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
192
+ { isOn: target, latch: target ? 10000 : undefined });
193
+ if (res.status.code === 200) this._lastState = hstate.isOn = target;
194
+ else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
195
+ }
196
+ if (target) this.lastHeatCycle = new Date();
197
+ }
198
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
199
+ }
200
+ public async pollEquipmentAsync() {
201
+ let self = this;
202
+ try {
203
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
204
+ this._pollTimer = null;
205
+ let success = false;
206
+ }
207
+ catch (err) { logger.error(`Nixie Error polling Heater - ${err}`); }
208
+ finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
209
+ }
210
+ private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
211
+ try {
212
+ let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
213
+ return dev;
214
+ } catch (err) { logger.error(`Nixie Heater Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
215
+ }
216
+ public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
217
+ try {
218
+ if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
219
+ && typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
220
+ try {
221
+ let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
222
+ // If we have a status check the return.
223
+ hstate.commStatus = stat.hasFault ? 1 : 0;
224
+ } catch (err) { hstate.commStatus = 1; }
225
+ }
226
+ else
227
+ hstate.commStatus = 0;
228
+ } catch (err) { logger.error(`Nixie Error checking heater Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
229
+ }
230
+ public async closeAsync() {
231
+ try {
232
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
233
+ this._pollTimer = null;
234
+ let hstate = state.heaters.getItemById(this.heater.id);
235
+ await this.setHeaterStateAsync(hstate, false);
236
+ hstate.emitEquipmentChange();
237
+ }
238
+ catch (err) { logger.error(`Nixie Heater closeAsync: ${err.message}`); return Promise.reject(err); }
239
+ }
240
+ public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
241
+ }
242
+ export class NixieSolarHeater extends NixieHeaterBase {
243
+ public pollingInterval: number = 10000;
244
+ declare heater: Heater;
245
+ constructor(ncp: INixieControlPanel, heater: Heater) {
246
+ super(ncp, heater);
247
+ this.heater = heater;
248
+ this.pollEquipmentAsync();
249
+ }
250
+ public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
251
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
252
+ try {
253
+ let origState = hstate.isOn;
254
+ // Initialize the desired state.
255
+ this.isOn = isOn;
256
+ this.isCooling = isCooling;
257
+ let target = hstate.startupDelay === false && isOn;
258
+ if (target && typeof hstate.endTime !== 'undefined') {
259
+ // Calculate a short cycle time so that the solar heater does not cycle
260
+ // too often. For solar heaters this is 60 seconds. This gives enough time
261
+ // for the valve to rotate and start heating. If the solar and water sensors are
262
+ // not having issues this should be plenty of time.
263
+ if (new Date().getTime() - hstate.endTime.getTime() < 60000) {
264
+ logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
265
+ target = false;
266
+ }
267
+ }
268
+
269
+ // Here we go we need to set the valve status that is attached to solar.
270
+ if (hstate.isOn !== target) {
271
+ logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
272
+ }
273
+ if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
274
+ if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
275
+ this._lastState = hstate.isOn = target;
276
+ }
277
+ else {
278
+ let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
279
+ { isOn: target, latch: target ? 10000 : undefined });
280
+ if (res.status.code === 200) this._lastState = hstate.isOn = target;
281
+ else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
282
+ }
283
+ if (target) {
284
+ if (isCooling) this.lastCoolCycle = new Date();
285
+ else if (isOn) this.lastHeatCycle = new Date();
286
+ }
287
+ }
288
+ // In this instance we need to see if there are cleaner circuits that we need to turn off
289
+ // then delay for the current body because the solar just came on.
290
+ if (hstate.isOn && sys.general.options.cleanerSolarDelay && !origState) {
291
+ let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('cleaner') !== -1 && x.body === hstate.bodyId });
292
+ let cleaners = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
293
+ // Turn off all the cleaner circuits and set an on delay if they are on.
294
+ for (let i = 0; i < cleaners.length; i++) {
295
+ let cleaner = cleaners.getItemByIndex(i);
296
+ if (cleaner.isActive) {
297
+ let cstate = state.circuits.getItemById(cleaner.id);
298
+ if (cstate.isOn && sys.general.options.cleanerSolarDelayTime > 0) {
299
+ // Turn off the circuit then set a delay.
300
+ logger.info(`Setting cleaner solar delay for ${cleaner.name} to ${sys.general.options.cleanerSolarDelayTime}`);
301
+ await sys.board.circuits.setCircuitStateAsync(cstate.id, false);
302
+ delayMgr.setCleanerStartDelay(cstate, hstate.bodyId, sys.general.options.cleanerSolarDelayTime);
303
+ }
304
+ }
305
+ }
306
+ }
307
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
308
+ }
309
+ public async pollEquipmentAsync() {
310
+ let self = this;
311
+ try {
312
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
313
+ this._pollTimer = null;
314
+ let success = false;
315
+ }
316
+ catch (err) { logger.error(`Nixie Error polling Heater - ${err}`); }
317
+ finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
318
+ }
319
+ private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
320
+ try {
321
+ let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
322
+ return dev;
323
+ } catch (err) { logger.error(`Nixie Heater Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
324
+ }
325
+ public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
326
+ try {
327
+ if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
328
+ && typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
329
+ try {
330
+ let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
331
+ // If we have a status check the return.
332
+ hstate.commStatus = stat.hasFault ? 1 : 0;
333
+ } catch (err) { hstate.commStatus = 1; }
334
+ }
335
+ else
336
+ hstate.commStatus = 0;
337
+ } catch (err) { logger.error(`Nixie Error checking heater Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
338
+ }
339
+ public async closeAsync() {
340
+ try {
341
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
342
+ this._pollTimer = null;
343
+ let hstate = state.heaters.getItemById(this.heater.id);
344
+ await this.setHeaterStateAsync(hstate, false, false);
345
+ hstate.emitEquipmentChange();
346
+ }
347
+ catch (err) { logger.error(`Nixie Heater closeAsync: ${err.message}`); return Promise.reject(err); }
348
+ }
349
+ public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
350
+ }
351
+ export class NixieHeatpump extends NixieHeaterBase {
352
+ public configSent: boolean = false;
353
+ constructor(ncp: INixieControlPanel, heater: Heater) {
354
+ super(ncp, heater);
355
+ // Set the polling interval to 3 seconds.
356
+ this.pollEquipmentAsync();
357
+ }
358
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
359
+ try {
360
+ this.suspendPolling = true;
361
+ this.isOn = isOn;
362
+ this.isCooling = isCooling;
363
+ if (!hstate.startupDelay) {
364
+ if (isOn && !isCooling) this.lastHeatCycle = new Date();
365
+ else if (isCooling) this.lastCoolCycle = new Date();
366
+ }
367
+ // When this is implemented lets not forget to deal with the startup and any desired cool down delay. See UltraTemp for implementation.
368
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
369
+ finally { this.suspendPolling = false; }
370
+ }
371
+ public async pollEquipmentAsync() {
372
+ let self = this;
373
+ try {
374
+ this.suspendPolling = true;
375
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
376
+ this._pollTimer = null;
377
+ if (this._suspendPolling > 1) return;
378
+ let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
379
+ // If the body isn't on then we won't communicate with the chem controller. There is no need
380
+ // since most of the time these are attached to the filter relay.
381
+ if (this.isBodyOn() && !this.closing) {
382
+ await this.sendState(sheater);
383
+ //if (!this.closing) await this.requestStatus(sheater);
384
+ }
385
+ }
386
+ catch (err) { logger.error(`Error polling Heat Pump - ${err}`); }
387
+ finally {
388
+ this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
389
+ try { await self.pollEquipmentAsync() } catch (err) { }
390
+ }, this.pollingInterval || 10000);
391
+ }
392
+ }
393
+ public async sendState(sheater: HeaterState): Promise<boolean> {
394
+ try {
395
+ sheater.type = 2;
396
+ let success = await new Promise<boolean>((resolve, reject) => {
397
+ let out = Outbound.create({
398
+ portId: this.heater.portId || 0,
399
+ protocol: Protocol.Heater,
400
+ source: 16,
401
+ dest: this.heater.address,
402
+ action: 210,
403
+ payload: [210],
404
+ retries: 3, // We are going to try 4 times.
405
+ response: Response.create({ protocol: Protocol.IntelliChem, action: 18 }),
406
+ onAbort: () => { },
407
+ onComplete: (err) => {
408
+ if (err) {
409
+ // If the IntelliChem is not responding we need to store that off. If an 18 does
410
+ // come across this will be cleared by the processing of that message.
411
+ resolve(false);
412
+ }
413
+ else { resolve(true); }
414
+ }
415
+ });
416
+ conn.queueSendMessage(out);
417
+ });
418
+ return success;
419
+ } catch (err) { logger.error(`Communication error with Ultratemp : ${err.message}`); }
420
+ }
421
+ public async closeAsync() {
422
+ try {
423
+ this.suspendPolling = true;
424
+ this.closing = true;
425
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
426
+ this._pollTimer = null;
427
+ logger.info(`Closing Heater ${this.heater.name}`);
428
+
429
+ }
430
+ catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
431
+ }
432
+ }
433
+ export class NixieUltratemp extends NixieHeatpump {
434
+ constructor(ncp: INixieControlPanel, heater: Heater) {
435
+ super(ncp, heater);
436
+ // Set the polling interval to 3 seconds.
437
+ this.pollEquipmentAsync();
438
+ }
439
+ public async pollEquipmentAsync() {
440
+ let self = this;
441
+ try {
442
+ this.suspendPolling = true;
443
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
444
+ this._pollTimer = null;
445
+ if (this._suspendPolling > 1) return;
446
+ let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
447
+ // If the body isn't on then we won't communicate with the chem controller. There is no need
448
+ // since most of the time these are attached to the filter relay.
449
+ if (!this.closing) {
450
+ await this.setStatus(sheater);
451
+ }
452
+ }
453
+ catch (err) { logger.error(`Error polling UltraTemp heater - ${err}`); }
454
+ finally {
455
+ this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
456
+ try { await self.pollEquipmentAsync() } catch (err) {}
457
+ }, this.pollingInterval || 10000);
458
+ }
459
+ }
460
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
461
+ try {
462
+ // Initialize the desired state.
463
+ this.isCooling = isCooling;
464
+ if (hstate.isOn !== isOn) {
465
+ logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isCooling ? 'cooling' : isOn ? 'heating' : 'off'}`);
466
+
467
+ }
468
+ if (isOn && !hstate.startupDelay) this.lastHeatCycle = new Date();
469
+ hstate.isOn = isOn;
470
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
471
+ }
472
+ public async releaseHeater(sheater: HeaterState): Promise<boolean> {
473
+ try {
474
+ let success = await new Promise<boolean>((resolve, reject) => {
475
+ let out = Outbound.create({
476
+ portId: this.heater.portId || 0,
477
+ protocol: Protocol.Heater,
478
+ source: 16,
479
+ dest: this.heater.address,
480
+ action: 114,
481
+ payload: [],
482
+ retries: 3, // We are going to try 4 times.
483
+ response: Response.create({ protocol: Protocol.Heater, action: 115 }),
484
+ onAbort: () => { },
485
+ onComplete: (err) => {
486
+ if (err) {
487
+ // 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
488
+ // come across this will be cleared by the processing of that message.
489
+ sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
490
+ state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
491
+ resolve(false);
492
+ }
493
+ else { resolve(true); }
494
+ }
495
+ });
496
+ out.appendPayloadBytes(0, 10);
497
+ out.setPayloadByte(0, 144);
498
+ out.setPayloadByte(1, 0, 0);
499
+ conn.queueSendMessage(out);
500
+ });
501
+ return success;
502
+ } catch (err) { logger.error(`Communication error with Ultratemp : ${err.message}`); }
503
+ }
504
+ public async setStatus(sheater: HeaterState): Promise<boolean> {
505
+ try {
506
+ let success = await new Promise<boolean>((resolve, reject) => {
507
+ let out = Outbound.create({
508
+ portId: this.heater.portId || 0,
509
+ protocol: Protocol.Heater,
510
+ source: 16,
511
+ dest: this.heater.address,
512
+ action: 114,
513
+ payload: [],
514
+ retries: 3, // We are going to try 4 times.
515
+ response: Response.create({ protocol: Protocol.Heater, action: 115 }),
516
+ onAbort: () => { },
517
+ onComplete: (err) => {
518
+ if (err) {
519
+ // 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
520
+ // come across this will be cleared by the processing of that message.
521
+ sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
522
+ state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
523
+ resolve(false);
524
+ }
525
+ else { resolve(true); }
526
+ }
527
+ });
528
+ out.appendPayloadBytes(0, 10);
529
+ out.setPayloadByte(0, 144);
530
+ // If we are in startup delay simply tell the heater that it is off.
531
+ if (sheater.startupDelay || this.closing)
532
+ out.setPayloadByte(1, 0, 0);
533
+ else {
534
+ if (this.isOn) {
535
+ if (!this.isCooling) this.lastHeatCycle = new Date();
536
+ else this.lastCoolCycle = new Date();
537
+ }
538
+ out.setPayloadByte(1, this.isOn ? (this.isCooling ? 2 : 1) : 0, 0);
539
+ }
540
+ conn.queueSendMessage(out);
541
+ });
542
+ return success;
543
+ } catch (err) { logger.error(`Communication error with Ultratemp : ${err.message}`); }
544
+ }
545
+ public async closeAsync() {
546
+ try {
547
+ this.suspendPolling = true;
548
+ this.closing = true;
549
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
550
+ this._pollTimer = null;
551
+ let sheater = state.heaters.getItemById(this.id);
552
+ await this.releaseHeater(sheater);
553
+ logger.info(`Closing Heater ${this.heater.name}`);
554
+ }
555
+ catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
556
+ }
557
+ }
558
+ export class NixieMastertemp extends NixieGasHeater {
559
+ constructor(ncp: INixieControlPanel, heater: Heater) {
560
+ super(ncp, heater);
561
+ // Set the polling interval to 3 seconds.
562
+ this.pollEquipmentAsync();
563
+ this.pollingInterval = 3000;
564
+ }
565
+ /* public getCooldownTime(): number {
566
+ // Delays are always in terms of seconds so convert the minute to seconds.
567
+ if (this.heater.cooldownDelay === 0 || typeof this.lastHeatCycle === 'undefined') return 0;
568
+ let now = new Date().getTime();
569
+ let cooldown = this.isOn ? this.heater.cooldownDelay * 60000 : Math.round(((this.lastHeatCycle.getDate() + this.heater.cooldownDelay * 60000) - now) / 1000);
570
+ return Math.min(Math.max(0, cooldown), this.heater.cooldownDelay * 60);
571
+ } */
572
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
573
+ try {
574
+ // Initialize the desired state.
575
+ this.isCooling = false;
576
+ // Here we go we need to set the firemans switch state.
577
+ if (hstate.isOn !== isOn) {
578
+ logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
579
+ }
580
+ if (isOn && !hstate.startupDelay) this.lastHeatCycle = new Date();
581
+ hstate.isOn = isOn;
582
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
583
+ }
584
+ public async pollEquipmentAsync() {
585
+ let self = this;
586
+ try {
587
+ this.suspendPolling = true;
588
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
589
+ this._pollTimer = null;
590
+ if (this._suspendPolling > 1) return;
591
+ let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
592
+ if (!this.closing) await this.setStatus(sheater);
593
+ }
594
+ catch (err) { logger.error(`Error polling MasterTemp heater - ${err}`); }
595
+ finally {
596
+ this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
597
+ try { await self.pollEquipmentAsync() } catch (err) { }
598
+ }, this.pollingInterval || 3000);
599
+ }
600
+ }
601
+ public async setStatus(sheater: HeaterState): Promise<boolean> {
602
+ try {
603
+ let success = await new Promise<boolean>((resolve, reject) => {
604
+ let out = Outbound.create({
605
+ portId: this.heater.portId || 0,
606
+ protocol: Protocol.Heater,
607
+ source: 16,
608
+ dest: this.heater.address,
609
+ action: 112,
610
+ payload: [],
611
+ retries: 3, // We are going to try 4 times.
612
+ response: Response.create({ protocol: Protocol.Heater, action: 116 }),
613
+ onAbort: () => { },
614
+ onComplete: (err) => {
615
+ if (err) {
616
+ // If the MasterTemp is not responding we need to store that off but at this point we know none of the codes. If a 115 does
617
+ // come across this will be cleared by the processing of that message.
618
+ sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
619
+ state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
620
+ resolve(false);
621
+ }
622
+ else { resolve(true); }
623
+ }
624
+ });
625
+ out.appendPayloadBytes(0, 11);
626
+ // If we have a startup delay we need to simply send 0 to the heater to make sure that it is off.
627
+ if (sheater.startupDelay)
628
+ out.setPayloadByte(0, 0);
629
+ else {
630
+ // The cooldown delay is a bit hard to figure out here since I think the heater does it on its own.
631
+ out.setPayloadByte(0, sheater.bodyId <= 2 ? sheater.bodyId : 0);
632
+ }
633
+ out.setPayloadByte(1, sys.bodies.getItemById(1).heatSetpoint || 0);
634
+ out.setPayloadByte(2, sys.bodies.getItemById(2).heatSetpoint || 0);
635
+ conn.queueSendMessage(out);
636
+ });
637
+ return success;
638
+ } catch (err) { logger.error(`Communication error with MasterTemp : ${err.message}`); }
639
+ }
640
+ public async closeAsync() {
641
+ try {
642
+ this.suspendPolling = true;
643
+ this.closing = true;
644
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
645
+ this._pollTimer = null;
646
+ logger.info(`Closing Heater ${this.heater.name}`);
647
+
648
+ }
649
+ catch (err) { logger.error(`MasterTemp closeAsync: ${err.message}`); return Promise.reject(err); }
650
+ }
649
651
  }