nodejs-poolcontroller 8.3.0 → 8.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.eslintrc.json +36 -36
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/copilot-instructions.md +63 -63
  7. package/.github/workflows/ghcr-publish.yml +67 -67
  8. package/157_issues.md +101 -0
  9. package/AGENTS.md +613 -0
  10. package/CONTRIBUTING.md +74 -74
  11. package/Changelog +292 -284
  12. package/Dockerfile +62 -62
  13. package/Gruntfile.js +40 -40
  14. package/LICENSE +661 -661
  15. package/README.md +329 -309
  16. package/anslq25/MessagesMock.ts +221 -221
  17. package/anslq25/boards/MockBoardFactory.ts +49 -49
  18. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  19. package/anslq25/boards/MockSystemBoard.ts +216 -216
  20. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  21. package/anslq25/pumps/MockPump.ts +83 -83
  22. package/app.ts +115 -115
  23. package/config/Config.ts +0 -0
  24. package/config/VersionCheck.ts +0 -0
  25. package/controller/Constants.ts +809 -805
  26. package/controller/Equipment.ts +2737 -2664
  27. package/controller/Errors.ts +181 -181
  28. package/controller/Lockouts.ts +549 -549
  29. package/controller/State.ts +3746 -3701
  30. package/controller/boards/AquaLinkBoard.ts +1175 -1003
  31. package/controller/boards/BoardFactory.ts +53 -53
  32. package/controller/boards/EasyTouchBoard.ts +3246 -3202
  33. package/controller/boards/IntelliCenterBoard.ts +4581 -3899
  34. package/controller/boards/IntelliComBoard.ts +69 -69
  35. package/controller/boards/IntelliTouchBoard.ts +382 -382
  36. package/controller/boards/NixieBoard.ts +1947 -1944
  37. package/controller/boards/SunTouchBoard.ts +401 -400
  38. package/controller/boards/SystemBoard.ts +5303 -5268
  39. package/controller/comms/Comms.ts +1278 -1255
  40. package/controller/comms/ScreenLogic.ts +1665 -1665
  41. package/controller/comms/messages/Messages.ts +1627 -1406
  42. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  43. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  44. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  45. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  46. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  47. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  48. package/controller/comms/messages/config/EquipmentMessage.ts +250 -210
  49. package/controller/comms/messages/config/ExternalMessage.ts +1051 -903
  50. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  51. package/controller/comms/messages/config/GeneralMessage.ts +65 -0
  52. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  53. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  54. package/controller/comms/messages/config/OptionsMessage.ts +207 -174
  55. package/controller/comms/messages/config/PumpMessage.ts +427 -421
  56. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  57. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  58. package/controller/comms/messages/config/SecurityMessage.ts +37 -13
  59. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  60. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  61. package/controller/comms/messages/status/EquipmentStateMessage.ts +940 -822
  62. package/controller/comms/messages/status/HeaterStateMessage.ts +147 -135
  63. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  64. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  65. package/controller/comms/messages/status/NeptuneModbusStateMessage.ts +217 -0
  66. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  67. package/controller/comms/messages/status/RegalModbusStateMessage.ts +410 -410
  68. package/controller/comms/messages/status/VersionMessage.ts +152 -41
  69. package/controller/nixie/Nixie.ts +173 -173
  70. package/controller/nixie/NixieEquipment.ts +104 -104
  71. package/controller/nixie/bodies/Body.ts +120 -120
  72. package/controller/nixie/bodies/Filter.ts +135 -135
  73. package/controller/nixie/chemistry/ChemController.ts +2756 -2724
  74. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  75. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  76. package/controller/nixie/circuits/Circuit.ts +478 -478
  77. package/controller/nixie/heaters/Heater.ts +843 -834
  78. package/controller/nixie/pumps/Pump.ts +1336 -1193
  79. package/controller/nixie/schedules/Schedule.ts +401 -401
  80. package/controller/nixie/valves/Valve.ts +170 -170
  81. package/defaultConfig.json +352 -352
  82. package/docker-compose.yml +32 -31
  83. package/logger/DataLogger.ts +448 -448
  84. package/logger/Logger.ts +459 -436
  85. package/package.json +58 -58
  86. package/sendSocket.js +32 -32
  87. package/tsconfig.json +26 -25
  88. package/types/express-multer.d.ts +32 -32
  89. package/web/Server.ts +1939 -1927
  90. package/web/bindings/aqualinkD.json +559 -559
  91. package/web/bindings/influxDB.json +1066 -1066
  92. package/web/bindings/mqtt.json +721 -721
  93. package/web/bindings/mqttAlt.json +746 -746
  94. package/web/bindings/rulesManager.json +54 -54
  95. package/web/bindings/smartThings-Hubitat.json +31 -31
  96. package/web/bindings/valveRelays.json +20 -20
  97. package/web/bindings/vera.json +25 -25
  98. package/web/interfaces/baseInterface.ts +188 -188
  99. package/web/interfaces/httpInterface.ts +148 -148
  100. package/web/interfaces/influxInterface.ts +283 -283
  101. package/web/interfaces/mqttInterface.ts +695 -695
  102. package/web/interfaces/ruleInterface.ts +101 -87
  103. package/web/services/config/Config.ts +1212 -1053
  104. package/web/services/config/ConfigSocket.ts +0 -0
  105. package/web/services/state/State.ts +21 -0
  106. package/web/services/state/StateSocket.ts +28 -0
  107. package/web/services/utilities/Utilities.ts +233 -233
@@ -1,834 +1,843 @@
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 { Body, 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 { Inbound, 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
- public async setServiceModeAsync() {
90
- try {
91
- for (let i = this.length - 1; i >= 0; i--) {
92
- let heater = this[i] as NixieHeaterBase;
93
- await heater.setServiceModeAsync();
94
- }
95
- } catch (err) { return Promise.reject(`Nixie Control Panel setServiceMode ${err.message}`); }
96
- }
97
- }
98
- export class NixieHeaterBase extends NixieEquipment {
99
- protected _suspendPolling: number = 0;
100
- public pollingInterval: number = 10000;
101
- public heater: Heater;
102
- protected _pollTimer: NodeJS.Timeout = null;
103
- protected _lastState;
104
- protected closing = false;
105
- protected bodyOnTime: number;
106
- protected isOn: boolean = false;
107
- protected isCooling: boolean = false;
108
- protected lastHeatCycle: Date;
109
- protected lastCoolCycle: Date;
110
- constructor(ncp: INixieControlPanel, heater: Heater) {
111
- super(ncp);
112
- this.heater = heater;
113
- }
114
- public get suspendPolling(): boolean { return this._suspendPolling > 0; }
115
- public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
116
- public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
117
- public getCooldownTime() { return 0; }
118
- public static create(ncp: INixieControlPanel, heater: Heater): NixieHeaterBase {
119
- let type = sys.board.valueMaps.heaterTypes.transform(heater.type);
120
- switch (type.name) {
121
- case 'heatpump':
122
- return new NixieHeatpump(ncp, heater);
123
- case 'ultratemp':
124
- return new NixieUltratemp(ncp, heater);
125
- case 'gas':
126
- return new NixieGasHeater(ncp, heater);
127
- case 'mastertemp':
128
- return new NixieMastertemp(ncp, heater);
129
- case 'solar':
130
- return new NixieSolarHeater(ncp, heater);
131
- case 'hybrid':
132
- return new NixieUltraTempETi(ncp, heater);
133
- default:
134
- return new NixieHeaterBase(ncp, heater);
135
- }
136
- }
137
- public isBodyOn() {
138
- let isOn = sys.board.bodies.isBodyOn(this.heater.body);
139
- if (isOn && typeof this.bodyOnTime === 'undefined') {
140
- this.bodyOnTime = new Date().getTime();
141
- }
142
- else if (!isOn) this.bodyOnTime = undefined;
143
- return isOn;
144
- }
145
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
146
- try {
147
- return Promise.reject(new InvalidOperationError(`You cannot change the state on this type of heater ${hstate.name}`, 'setHeaterStateAsync'));
148
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
149
- }
150
- public async setHeaterAsync(data: any) {
151
- try {
152
- let heater = this.heater;
153
-
154
- }
155
- catch (err) { logger.error(`Nixie setHeaterAsync: ${err.message}`); return Promise.reject(err); }
156
- }
157
- public async closeAsync() { }
158
- public async setServiceModeAsync() {
159
- let hstate = state.heaters.getItemById(this.heater.id);
160
- await this.setHeaterStateAsync(hstate, false, false);
161
- }
162
- }
163
- export class NixieGasHeater extends NixieHeaterBase {
164
- public pollingInterval: number = 10000;
165
- //declare heater: Heater;
166
- constructor(ncp: INixieControlPanel, heater: Heater) {
167
- super(ncp, heater);
168
- this.heater = heater;
169
- if (typeof this.heater.stopTempDelta === 'undefined') this.heater.stopTempDelta = 1;
170
- if (typeof this.heater.minCycleTime === 'undefined') this.heater.minCycleTime = 2;
171
- this.pollEquipmentAsync();
172
- }
173
- public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
174
- public getCooldownTime(): number {
175
- // Delays are always in terms of seconds so convert the minute to seconds.
176
- if (this.heater.cooldownDelay === 0 || typeof this.lastHeatCycle === 'undefined') return 0;
177
- let now = new Date().getTime();
178
- let cooldown = this.isOn ? this.heater.cooldownDelay * 60000 : Math.round(((this.lastHeatCycle.getDate() + this.heater.cooldownDelay * 60000) - now) / 1000);
179
- return Math.min(Math.max(0, cooldown), this.heater.cooldownDelay * 60);
180
- }
181
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
182
- try {
183
- // Initialize the desired state.
184
- this.isOn = isOn;
185
- this.isCooling = false;
186
- let target = hstate.startupDelay === false && isOn;
187
- if (target && typeof hstate.endTime !== 'undefined') {
188
- // Calculate a short cycle time so that the gas heater does not cycle
189
- // too often. For gas heaters this is 60 seconds. This gives enough time
190
- // for the heater control circuit to make a full cycle.
191
- if (new Date().getTime() - hstate.endTime.getTime() < this.heater.minCycleTime * 60000) {
192
- logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
193
- target = false;
194
- }
195
- }
196
- // Here we go we need to set the firemans switch state.
197
- if (hstate.isOn !== target) {
198
- logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
199
- }
200
- if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
201
- if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
202
- this._lastState = hstate.isOn = target;
203
- }
204
- else {
205
- let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
206
- { isOn: target, latch: target ? 10000 : undefined });
207
- if (res.status.code === 200) this._lastState = hstate.isOn = target;
208
- else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
209
- }
210
- if (target) this.lastHeatCycle = new Date();
211
- }
212
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
213
- }
214
- public async pollEquipmentAsync() {
215
- let self = this;
216
- try {
217
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
218
- this._pollTimer = null;
219
- let success = false;
220
- }
221
- catch (err) { logger.error(`Nixie Error polling Heater - ${err}`); }
222
- finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
223
- }
224
- private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
225
- try {
226
- let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
227
- return dev;
228
- } catch (err) { logger.error(`Nixie Heater Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
229
- }
230
- public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
231
- try {
232
- if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
233
- && typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
234
- try {
235
- let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
236
- // If we have a status check the return.
237
- hstate.commStatus = stat.hasFault ? 1 : 0;
238
- } catch (err) { hstate.commStatus = 1; }
239
- }
240
- else
241
- hstate.commStatus = 0;
242
- } catch (err) { logger.error(`Nixie Error checking heater Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
243
- }
244
- public async closeAsync() {
245
- try {
246
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
247
- this._pollTimer = null;
248
- let hstate = state.heaters.getItemById(this.heater.id);
249
- await this.setHeaterStateAsync(hstate, false);
250
- hstate.emitEquipmentChange();
251
- }
252
- catch (err) { logger.error(`Nixie Heater closeAsync: ${err.message}`); return Promise.reject(err); }
253
- }
254
- public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
255
- }
256
- export class NixieSolarHeater extends NixieHeaterBase {
257
- public pollingInterval: number = 10000;
258
- declare heater: Heater;
259
- constructor(ncp: INixieControlPanel, heater: Heater) {
260
- super(ncp, heater);
261
- this.heater = heater;
262
- this.pollEquipmentAsync();
263
- }
264
- public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
265
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
266
- try {
267
- let origState = hstate.isOn;
268
- // Initialize the desired state.
269
- this.isOn = isOn;
270
- this.isCooling = isCooling;
271
- let target = hstate.startupDelay === false && isOn;
272
- if (target && typeof hstate.endTime !== 'undefined') {
273
- // Calculate a short cycle time so that the solar heater does not cycle
274
- // too often. For solar heaters this is 60 seconds. This gives enough time
275
- // for the valve to rotate and start heating. If the solar and water sensors are
276
- // not having issues this should be plenty of time.
277
- if (new Date().getTime() - hstate.endTime.getTime() < 60000) {
278
- logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
279
- target = false;
280
- }
281
- }
282
-
283
- // Here we go we need to set the valve status that is attached to solar.
284
- if (hstate.isOn !== target) {
285
- logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
286
- }
287
- if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
288
- if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
289
- this._lastState = hstate.isOn = target;
290
- hstate.isCooling = target && isCooling;
291
- }
292
- else {
293
- let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
294
- { isOn: target, latch: target ? 10000 : undefined });
295
- if (res.status.code === 200) {
296
- this._lastState = hstate.isOn = target;
297
- hstate.isCooling = target && isCooling;
298
- }
299
- else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
300
- }
301
- if (target) {
302
- if (isCooling) this.lastCoolCycle = new Date();
303
- else if (isOn) this.lastHeatCycle = new Date();
304
- }
305
- }
306
- // In this instance we need to see if there are cleaner circuits that we need to turn off
307
- // then delay for the current body because the solar just came on.
308
- if (hstate.isOn && sys.general.options.cleanerSolarDelay && !origState) {
309
- let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('cleaner') !== -1 && x.body === hstate.bodyId });
310
- let cleaners = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
311
- // Turn off all the cleaner circuits and set an on delay if they are on.
312
- for (let i = 0; i < cleaners.length; i++) {
313
- let cleaner = cleaners.getItemByIndex(i);
314
- if (cleaner.isActive) {
315
- let cstate = state.circuits.getItemById(cleaner.id);
316
- if (cstate.isOn && sys.general.options.cleanerSolarDelayTime > 0) {
317
- // Turn off the circuit then set a delay.
318
- logger.info(`Setting cleaner solar delay for ${cleaner.name} to ${sys.general.options.cleanerSolarDelayTime}`);
319
- await sys.board.circuits.setCircuitStateAsync(cstate.id, false);
320
- delayMgr.setCleanerStartDelay(cstate, hstate.bodyId, sys.general.options.cleanerSolarDelayTime);
321
- }
322
- }
323
- }
324
- }
325
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
326
- }
327
- public async pollEquipmentAsync() {
328
- let self = this;
329
- try {
330
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
331
- this._pollTimer = null;
332
- let success = false;
333
- }
334
- catch (err) { logger.error(`Nixie Error polling Heater - ${err}`); }
335
- finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
336
- }
337
- private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
338
- try {
339
- let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
340
- return dev;
341
- } catch (err) { logger.error(`Nixie Heater Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
342
- }
343
- public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
344
- try {
345
- if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
346
- && typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
347
- try {
348
- let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
349
- // If we have a status check the return.
350
- hstate.commStatus = stat.hasFault ? 1 : 0;
351
- } catch (err) { hstate.commStatus = 1; }
352
- }
353
- else
354
- hstate.commStatus = 0;
355
- } catch (err) { logger.error(`Nixie Error checking heater Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
356
- }
357
- public async closeAsync() {
358
- try {
359
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
360
- this._pollTimer = null;
361
- let hstate = state.heaters.getItemById(this.heater.id);
362
- await this.setHeaterStateAsync(hstate, false, false);
363
- hstate.emitEquipmentChange();
364
- }
365
- catch (err) { logger.error(`Nixie Heater closeAsync: ${err.message}`); return Promise.reject(err); }
366
- }
367
- public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
368
- }
369
- export class NixieHeatpump extends NixieHeaterBase {
370
- public pollingInterval: number = 10000;
371
- //declare heater: Heater;
372
- constructor(ncp: INixieControlPanel, heater: Heater) {
373
- super(ncp, heater);
374
- this.heater = heater;
375
- if (typeof this.heater.stopTempDelta === 'undefined') this.heater.stopTempDelta = 1;
376
- if (typeof this.heater.minCycleTime === 'undefined') this.heater.minCycleTime = 2;
377
- this.pollEquipmentAsync();
378
- }
379
- public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
380
- public getCooldownTime(): number { return 0; } // There is no cooldown delay at this time for a heatpump
381
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
382
- try {
383
- // Initialize the desired state.
384
- this.isOn = isOn;
385
- this.isCooling = false;
386
- let target = hstate.startupDelay === false && isOn;
387
- if (target && typeof hstate.endTime !== 'undefined') {
388
- // Calculate a short cycle time so that the gas heater does not cycle
389
- // too often. For gas heaters this is 60 seconds. This gives enough time
390
- // for the heater control circuit to make a full cycle.
391
- if (new Date().getTime() - hstate.endTime.getTime() < this.heater.minCycleTime * 60000) {
392
- logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
393
- target = false;
394
- }
395
- }
396
- // Here we go we need to set the firemans switch state.
397
- if (hstate.isOn !== target) {
398
- logger.info(`Nixie: Set Heatpump ${hstate.id}-${hstate.name} to ${isOn}`);
399
- }
400
- if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
401
- if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
402
- this._lastState = hstate.isOn = target;
403
- }
404
- else {
405
- let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
406
- { isOn: target, latch: target ? 10000 : undefined });
407
- if (res.status.code === 200) this._lastState = hstate.isOn = target;
408
- else logger.error(`Nixie Error setting heatpump state: ${res.status.code} -${res.status.message} ${res.error.message}`);
409
- }
410
- if (target) this.lastHeatCycle = new Date();
411
- }
412
- } catch (err) { return logger.error(`Nixie Error setting heatpump state ${hstate.id}-${hstate.name}: ${err.message}`); }
413
- }
414
- public async pollEquipmentAsync() {
415
- let self = this;
416
- try {
417
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
418
- this._pollTimer = null;
419
- let success = false;
420
- }
421
- catch (err) { logger.error(`Nixie Error polling Heatpump - ${err}`); }
422
- finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
423
- }
424
- private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
425
- try {
426
- let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
427
- return dev;
428
- } catch (err) { logger.error(`Nixie Heatpump Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
429
- }
430
- public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
431
- try {
432
- if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
433
- && typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
434
- try {
435
- let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
436
- // If we have a status check the return.
437
- hstate.commStatus = stat.hasFault ? 1 : 0;
438
- } catch (err) { hstate.commStatus = 1; }
439
- }
440
- else
441
- hstate.commStatus = 0;
442
- } catch (err) { logger.error(`Nixie Error checking heatpump Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
443
- }
444
- public async closeAsync() {
445
- try {
446
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
447
- this._pollTimer = null;
448
- let hstate = state.heaters.getItemById(this.heater.id);
449
- await this.setHeaterStateAsync(hstate, false);
450
- hstate.emitEquipmentChange();
451
- }
452
- catch (err) { logger.error(`Nixie Heatpump closeAsync: ${err.message}`); return Promise.reject(err); }
453
- }
454
- public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
455
- }
456
- export class NixieUltratemp extends NixieHeaterBase {
457
- constructor(ncp: INixieControlPanel, heater: Heater) {
458
- super(ncp, heater);
459
- // Set the polling interval to 3 seconds.
460
- this.pollEquipmentAsync();
461
- }
462
- public async setServiceModeAsync() {
463
- let hstate = state.heaters.getItemById(this.heater.id);
464
- await this.setHeaterStateAsync(hstate, false, false);
465
- await this.releaseHeater(hstate);
466
- }
467
-
468
- public async pollEquipmentAsync() {
469
- let self = this;
470
- try {
471
- this.suspendPolling = true;
472
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
473
- this._pollTimer = null;
474
- if (this._suspendPolling > 1) return;
475
- let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
476
- // If the body isn't on then we won't communicate with the chem controller. There is no need
477
- // since most of the time these are attached to the filter relay.
478
- if (!this.closing) {
479
- await this.setStatus(sheater);
480
- }
481
- }
482
- catch (err) { logger.error(`Error polling UltraTemp heater - ${err}`); }
483
- finally {
484
- this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
485
- try { await self.pollEquipmentAsync() } catch (err) { }
486
- }, this.pollingInterval || 10000);
487
- }
488
- }
489
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
490
- try {
491
- // Initialize the desired state.
492
- this.isCooling = isCooling;
493
- if (hstate.isOn !== isOn) {
494
- logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isCooling ? 'cooling' : isOn ? 'heating' : 'off'}`);
495
-
496
- }
497
- if (isOn && !hstate.startupDelay) this.lastHeatCycle = new Date();
498
- this.isOn = hstate.isOn = isOn;
499
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
500
- }
501
- public async releaseHeater(sheater: HeaterState): Promise<boolean> {
502
- try {
503
- let out = Outbound.create({
504
- portId: this.heater.portId || 0,
505
- protocol: Protocol.Heater,
506
- source: 16,
507
- dest: this.heater.address,
508
- action: 114,
509
- payload: [],
510
- retries: 3, // We are going to try 4 times.
511
- response: Response.create({ protocol: Protocol.Heater, action: 115 }),
512
- onAbort: () => { }
513
- });
514
- out.appendPayloadBytes(0, 10);
515
- out.setPayloadByte(0, 144);
516
- out.setPayloadByte(1, 0, 0);
517
- await out.sendAsync();
518
- return true;
519
-
520
- } catch (err) {
521
- // 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
522
- // come across this will be cleared by the processing of that message.
523
- sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
524
- state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
525
- logger.error(`Communication error with Ultratemp : ${err.message}`);
526
- return false;
527
- }
528
- }
529
- public async setStatus(sheater: HeaterState): Promise<boolean> {
530
- try {
531
- let out = Outbound.create({
532
- portId: this.heater.portId || 0,
533
- protocol: Protocol.Heater,
534
- source: 16,
535
- dest: this.heater.address,
536
- action: 114,
537
- payload: [],
538
- retries: 3, // We are going to try 4 times.
539
- response: Response.create({ protocol: Protocol.Heater, action: 115 }),
540
- onAbort: () => { }
541
- });
542
- out.appendPayloadBytes(0, 10);
543
- out.setPayloadByte(0, 144);
544
- // If we are in startup delay simply tell the heater that it is off.
545
- if (sheater.startupDelay || this.closing)
546
- out.setPayloadByte(1, 0, 0);
547
- else {
548
- if (this.isOn) {
549
- if (!this.isCooling) this.lastHeatCycle = new Date();
550
- else this.lastCoolCycle = new Date();
551
- }
552
- //console.log(`Setting the heater byte ${this.isOn} ${sheater.isOn} to ${this.isOn ? (this.isCooling ? 2 : 1) : 0}`);
553
- out.setPayloadByte(1, this.isOn ? (this.isCooling ? 2 : 1) : 0, 0);
554
- }
555
- let success = await out.sendAsync();
556
- return success;
557
- } catch (err) {
558
- // 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
559
- // come across this will be cleared by the processing of that message.
560
- sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
561
- state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
562
- logger.error(`Communication error with Ultratemp : ${err.message}`);
563
- return false;
564
- }
565
- }
566
- public async closeAsync() {
567
- try {
568
- this.suspendPolling = true;
569
- this.closing = true;
570
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
571
- this._pollTimer = null;
572
- let sheater = state.heaters.getItemById(this.id);
573
- await this.releaseHeater(sheater);
574
- logger.info(`Closing Heater ${this.heater.name}`);
575
- }
576
- catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
577
- }
578
- }
579
- export class NixieMastertemp extends NixieGasHeater {
580
- constructor(ncp: INixieControlPanel, heater: Heater) {
581
- super(ncp, heater);
582
- // Set the polling interval to 3 seconds.
583
- this.pollEquipmentAsync();
584
- this.pollingInterval = 3000;
585
-
586
- }
587
- /* public getCooldownTime(): number {
588
- // Delays are always in terms of seconds so convert the minute to seconds.
589
- if (this.heater.cooldownDelay === 0 || typeof this.lastHeatCycle === 'undefined') return 0;
590
- let now = new Date().getTime();
591
- let cooldown = this.isOn ? this.heater.cooldownDelay * 60000 : Math.round(((this.lastHeatCycle.getDate() + this.heater.cooldownDelay * 60000) - now) / 1000);
592
- return Math.min(Math.max(0, cooldown), this.heater.cooldownDelay * 60);
593
- } */
594
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
595
- try {
596
- // Initialize the desired state.
597
- this.isOn = isOn;
598
- this.isCooling = false;
599
- let target = hstate.startupDelay === false && isOn;
600
- if (target && typeof hstate.endTime !== 'undefined') {
601
- // Calculate a short cycle time so that the gas heater does not cycle
602
- // too often. For gas heaters this is 60 seconds. This gives enough time
603
- // for the heater control circuit to make a full cycle.
604
- if (new Date().getTime() - hstate.endTime.getTime() < this.heater.minCycleTime * 60000) {
605
- logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
606
- target = false;
607
- }
608
- }
609
- // Here we go we need to set the state.
610
- if (hstate.isOn !== target) {
611
- logger.info(`Nixie: Set Mastertemp ${hstate.id}-${hstate.name} to ${isOn}`);
612
- }
613
- if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
614
- this._lastState = hstate.isOn = target;
615
- if (target) this.lastHeatCycle = new Date();
616
- }
617
- hstate.isOn = isOn;
618
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
619
- }
620
- public async pollEquipmentAsync() {
621
- let self = this;
622
- try {
623
- this.suspendPolling = true;
624
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
625
- this._pollTimer = null;
626
- if (this._suspendPolling > 1) return;
627
- let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
628
- if (!this.closing) await this.setStatus(sheater);
629
- }
630
- catch (err) { logger.error(`Error polling MasterTemp heater - ${err}`); }
631
- finally {
632
- this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
633
- try { await self.pollEquipmentAsync() } catch (err) { }
634
- }, this.pollingInterval || 3000);
635
- }
636
- }
637
- public async setStatus(sheater: HeaterState): Promise<boolean> {
638
- try {
639
- let out = Outbound.create({
640
- portId: this.heater.portId || 0,
641
- protocol: Protocol.Heater,
642
- source: 16,
643
- dest: this.heater.address,
644
- action: 112,
645
- payload: [],
646
- retries: 3, // We are going to try 4 times.
647
- response: Response.create({ protocol: Protocol.Heater, action: 116 }),
648
- onAbort: () => { }
649
- });
650
- out.appendPayloadBytes(0, 11);
651
- // If we have a startup delay we need to simply send 0 to the heater to make sure that it is off.
652
- if (sheater.startupDelay)
653
- out.setPayloadByte(0, 0);
654
- else {
655
- // The cooldown delay is a bit hard to figure out here since I think the heater does it on its own.
656
- out.setPayloadByte(0, sheater.bodyId <= 2 ? sheater.bodyId : 0);
657
- }
658
- out.setPayloadByte(1, sys.bodies.getItemById(1).heatSetpoint || 0);
659
- out.setPayloadByte(2, sys.bodies.getItemById(2).heatSetpoint || 0);
660
- let success = await out.sendAsync();
661
- return success;
662
- } catch (err) {
663
- // 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
664
- // come across this will be cleared by the processing of that message.
665
- sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
666
- state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
667
- logger.error(`Communication error with MasterTemp : ${err.message}`);
668
- return false;
669
- }
670
- }
671
- public async setServiceModeAsync() {
672
- let hstate = state.heaters.getItemById(this.heater.id);
673
- await this.setHeaterStateAsync(hstate, false);
674
- }
675
- public async closeAsync() {
676
- try {
677
- this.suspendPolling = true;
678
- this.closing = true;
679
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
680
- this._pollTimer = null;
681
- logger.info(`Closing Heater ${this.heater.name}`);
682
-
683
- }
684
- catch (err) { logger.error(`MasterTemp closeAsync: ${err.message}`); return Promise.reject(err); }
685
- }
686
- }
687
- export class NixieUltraTempETi extends NixieHeaterBase {
688
- constructor(ncp: INixieControlPanel, heater: Heater) {
689
- super(ncp, heater);
690
- // Set the polling interval to 3 seconds.
691
- this.pollEquipmentAsync();
692
- }
693
- public async pollEquipmentAsync() {
694
- let self = this;
695
- try {
696
- this.suspendPolling = true;
697
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
698
- this._pollTimer = null;
699
- if (this._suspendPolling > 1) return;
700
- let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
701
- // If the body isn't on then we won't communicate with the chem controller. There is no need
702
- // since most of the time these are attached to the filter relay.
703
- if (!this.closing) {
704
- await this.setStatus(sheater);
705
- }
706
- }
707
- catch (err) { logger.error(`Error polling UltraTemp ETi heater - ${err}`); }
708
- finally {
709
- this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
710
- try { await self.pollEquipmentAsync() } catch (err) { }
711
- }, this.pollingInterval || 10000);
712
- }
713
- }
714
- public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
715
- try {
716
- // Initialize the desired state.
717
- this.isCooling = isCooling;
718
- if (hstate.isOn !== isOn) {
719
- logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isCooling ? 'cooling' : isOn ? 'heating' : 'off'}`);
720
-
721
- }
722
- if (isOn && !hstate.startupDelay) this.lastHeatCycle = new Date();
723
- this.isOn = hstate.isOn = isOn;
724
- } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
725
- }
726
- public async releaseHeater(sheater: HeaterState): Promise<boolean> {
727
- try {
728
- let out = Outbound.create({
729
- portId: this.heater.portId || 0,
730
- protocol: Protocol.Heater,
731
- source: 16,
732
- dest: this.heater.address,
733
- action: 112,
734
- payload: [],
735
- retries: 3, // We are going to try 4 times.
736
- response: Response.create({ protocol: Protocol.Heater, action: 113 }),
737
- onAbort: () => { }
738
- });
739
- out.appendPayloadBytes(0, 10);
740
- out.setPayloadByte(0, 0);
741
- out.setPayloadByte(1, 0);
742
- out.setPayloadByte(2, 78);
743
- out.setPayloadByte(3, 1);
744
- out.setPayloadByte(4, 5);
745
- let success = await out.sendAsync();
746
- return success;
747
- } catch (err) {
748
- // If the Ultratemp is not responding we need to store that off but at this point we know none of the codes. If a 113 does
749
- // come across this will be cleared by the processing of that message.
750
- sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
751
- state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
752
- logger.error(`Communication error with Ultratemp : ${err.message}`);
753
- return false;
754
- }
755
- }
756
- protected calcHeatModeByte(body: Body): number {
757
- let byte = 0;
758
- if (this.closing) return 0; // We are closing so just set the heat mode to off.
759
- let mode = sys.board.valueMaps.heatModes.transform(body.heatMode || 0);
760
- switch (mode.name) {
761
- case 'hpump':
762
- case 'heatpump':
763
- byte = 1;
764
- break;
765
- case 'heater':
766
- byte = 2;
767
- break;
768
- case 'heatpumpref':
769
- case 'heatpumppref':
770
- case 'hybrid':
771
- byte = 3;
772
- break;
773
- case 'dual':
774
- byte = 4;
775
- break;
776
- }
777
- return byte;
778
- }
779
- public async setServiceModeAsync() {
780
- let hstate = state.heaters.getItemById(this.heater.id);
781
- await this.setHeaterStateAsync(hstate, false, false);
782
- await this.releaseHeater(hstate);
783
- }
784
- public async setStatus(sheater: HeaterState): Promise<boolean> {
785
- try {
786
- let out = Outbound.create({
787
- portId: this.heater.portId || 0,
788
- protocol: Protocol.Heater,
789
- source: 16,
790
- dest: this.heater.address,
791
- action: 112,
792
- payload: [],
793
- retries: 3, // We are going to try 4 times.
794
- response: Response.create({ protocol: Protocol.Heater, action: 113 }),
795
- onAbort: () => { }
796
- });
797
- out.appendPayloadBytes(0, 10);
798
- out.setPayloadByte(0, this.isOn && !sheater.startupDelay && !this.closing ? 1 : 0);
799
- if (sheater.bodyId > 0) {
800
- let body = sys.bodies.getItemById(sheater.bodyId);
801
- out.setPayloadByte(1, this.calcHeatModeByte(body));
802
- out.setPayloadByte(2, body.setPoint);
803
- }
804
- else out.setPayloadByte(2, utils.convert.temperature.convertUnits(78, 'F', sys.board.valueMaps.tempUnits.getName(state.temps.units) || 'F')); // Just set it to a valid setpoint and call it a day.
805
- out.setPayloadByte(3, this.heater.economyTime, 1);
806
- out.setPayloadByte(4, this.heater.maxBoostTemp, 5);
807
- if (this.isOn) {
808
- if (!this.isCooling) this.lastHeatCycle = new Date();
809
- else this.lastCoolCycle = new Date();
810
- }
811
- let success = await out.sendAsync();
812
- return success;
813
- } catch (err) {
814
- // If the Ultratemp ETi is not responding we need to store that off but at this point we know none of the codes. If a 113 does
815
- // come across this will be cleared by the processing of that message.
816
- sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
817
- state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
818
- logger.error(`Communication error with Ultratemp ETi : ${err.message}`);
819
- return false;
820
- }
821
- }
822
- public async closeAsync() {
823
- try {
824
- this.suspendPolling = true;
825
- this.closing = true;
826
- if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
827
- this._pollTimer = null;
828
- let sheater = state.heaters.getItemById(this.id);
829
- await this.releaseHeater(sheater);
830
- logger.info(`Closing Heater ${this.heater.name}`);
831
- }
832
- catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
833
- }
834
- }
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 { Body, 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 { Inbound, 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
+ // Auto-initialize if heater exists in config with master=1 but hasn't been
32
+ // added to NCP yet (e.g. syncHeaterStates runs before ncp.initAsync completes).
33
+ let heater = sys.heaters.getItemById(hstate.id);
34
+ if (typeof heater !== 'undefined' && heater.master === 1 && heater.isActive !== false) {
35
+ logger.info(`NCP: Auto-initializing heater ${hstate.id}-${heater.name}`);
36
+ h = await this.initHeaterAsync(heater);
37
+ }
38
+ if (typeof h === 'undefined') {
39
+ return Promise.reject(new Error(`NCP: Heater ${hstate.id}-${hstate.name} could not be found to set the state to ${val}.`));
40
+ }
41
+ }
42
+ await h.setHeaterStateAsync(hstate, val, isCooling);
43
+ }
44
+ catch (err) { return logger.error(`NCP: setHeaterStateAsync ${hstate.id}-${hstate.name}: ${err.message}`); }
45
+ }
46
+ public async setHeaterAsync(heater: Heater, data: any) {
47
+ // By the time we get here we know that we are in control and this is a Nixie heater.
48
+ try {
49
+ let h: NixieHeaterBase = this.find(elem => elem.id === heater.id) as NixieHeaterBase;
50
+ if (typeof h === 'undefined') {
51
+ heater.master = 1;
52
+ h = NixieHeaterBase.create(this.controlPanel, heater);
53
+ this.push(h);
54
+ await h.setHeaterAsync(data);
55
+ logger.info(`A Heater was not found for id #${heater.id} creating Heater`);
56
+ }
57
+ else {
58
+ await h.setHeaterAsync(data);
59
+ }
60
+ }
61
+ catch (err) { logger.error(`setHeaterAsync: ${err.message}`); return Promise.reject(err); }
62
+ }
63
+ public async initAsync(heaters: HeaterCollection) {
64
+ try {
65
+ for (let i = 0; i < heaters.length; i++) {
66
+ let heater = heaters.getItemByIndex(i);
67
+ if (heater.master === 1) {
68
+ if (typeof this.find(elem => elem.id === heater.id) === 'undefined') {
69
+ logger.info(`Initializing Heater ${heater.name}`);
70
+ let nHeater = NixieHeaterBase.create(this.controlPanel, heater);
71
+ this.push(nHeater);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ catch (err) { logger.error(`Nixie Heater initAsync: ${err.message}`); return Promise.reject(err); }
77
+ }
78
+ public async closeAsync() {
79
+ try {
80
+ for (let i = this.length - 1; i >= 0; i--) {
81
+ try {
82
+ await this[i].closeAsync();
83
+ this.splice(i, 1);
84
+ } catch (err) { logger.error(`Error stopping Nixie Heater ${err}`); }
85
+ }
86
+ } catch (err) { } // Don't bail if we have an errror.
87
+ }
88
+ public async initHeaterAsync(heater: Heater): Promise<NixieHeaterBase> {
89
+ try {
90
+ let c: NixieHeaterBase = this.find(elem => elem.id === heater.id) as NixieHeaterBase;
91
+ if (typeof c === 'undefined') {
92
+ c = NixieHeaterBase.create(this.controlPanel, heater);
93
+ this.push(c);
94
+ }
95
+ return c;
96
+ } catch (err) { logger.error(`initHeaterAsync: ${err.message}`); return Promise.reject(err); }
97
+ }
98
+ public async setServiceModeAsync() {
99
+ try {
100
+ for (let i = this.length - 1; i >= 0; i--) {
101
+ let heater = this[i] as NixieHeaterBase;
102
+ await heater.setServiceModeAsync();
103
+ }
104
+ } catch (err) { return Promise.reject(`Nixie Control Panel setServiceMode ${err.message}`); }
105
+ }
106
+ }
107
+ export class NixieHeaterBase extends NixieEquipment {
108
+ protected _suspendPolling: number = 0;
109
+ public pollingInterval: number = 10000;
110
+ public heater: Heater;
111
+ protected _pollTimer: NodeJS.Timeout = null;
112
+ protected _lastState;
113
+ protected closing = false;
114
+ protected bodyOnTime: number;
115
+ protected isOn: boolean = false;
116
+ protected isCooling: boolean = false;
117
+ protected lastHeatCycle: Date;
118
+ protected lastCoolCycle: Date;
119
+ constructor(ncp: INixieControlPanel, heater: Heater) {
120
+ super(ncp);
121
+ this.heater = heater;
122
+ }
123
+ public get suspendPolling(): boolean { return this._suspendPolling > 0; }
124
+ public set suspendPolling(val: boolean) { this._suspendPolling = Math.max(0, this._suspendPolling + (val ? 1 : -1)); }
125
+ public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
126
+ public getCooldownTime() { return 0; }
127
+ public static create(ncp: INixieControlPanel, heater: Heater): NixieHeaterBase {
128
+ let type = sys.board.valueMaps.heaterTypes.transform(heater.type);
129
+ switch (type.name) {
130
+ case 'heatpump':
131
+ return new NixieHeatpump(ncp, heater);
132
+ case 'ultratemp':
133
+ return new NixieUltratemp(ncp, heater);
134
+ case 'gas':
135
+ return new NixieGasHeater(ncp, heater);
136
+ case 'mastertemp':
137
+ return new NixieMastertemp(ncp, heater);
138
+ case 'solar':
139
+ return new NixieSolarHeater(ncp, heater);
140
+ case 'hybrid':
141
+ return new NixieUltraTempETi(ncp, heater);
142
+ default:
143
+ return new NixieHeaterBase(ncp, heater);
144
+ }
145
+ }
146
+ public isBodyOn() {
147
+ let isOn = sys.board.bodies.isBodyOn(this.heater.body);
148
+ if (isOn && typeof this.bodyOnTime === 'undefined') {
149
+ this.bodyOnTime = new Date().getTime();
150
+ }
151
+ else if (!isOn) this.bodyOnTime = undefined;
152
+ return isOn;
153
+ }
154
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
155
+ try {
156
+ return Promise.reject(new InvalidOperationError(`You cannot change the state on this type of heater ${hstate.name}`, 'setHeaterStateAsync'));
157
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
158
+ }
159
+ public async setHeaterAsync(data: any) {
160
+ try {
161
+ let heater = this.heater;
162
+
163
+ }
164
+ catch (err) { logger.error(`Nixie setHeaterAsync: ${err.message}`); return Promise.reject(err); }
165
+ }
166
+ public async closeAsync() { }
167
+ public async setServiceModeAsync() {
168
+ let hstate = state.heaters.getItemById(this.heater.id);
169
+ await this.setHeaterStateAsync(hstate, false, false);
170
+ }
171
+ }
172
+ export class NixieGasHeater extends NixieHeaterBase {
173
+ public pollingInterval: number = 10000;
174
+ //declare heater: Heater;
175
+ constructor(ncp: INixieControlPanel, heater: Heater) {
176
+ super(ncp, heater);
177
+ this.heater = heater;
178
+ if (typeof this.heater.stopTempDelta === 'undefined') this.heater.stopTempDelta = 1;
179
+ if (typeof this.heater.minCycleTime === 'undefined') this.heater.minCycleTime = 2;
180
+ this.pollEquipmentAsync();
181
+ }
182
+ public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
183
+ public getCooldownTime(): number {
184
+ // Delays are always in terms of seconds so convert the minute to seconds.
185
+ if (this.heater.cooldownDelay === 0 || typeof this.lastHeatCycle === 'undefined') return 0;
186
+ let now = new Date().getTime();
187
+ let cooldown = this.isOn ? this.heater.cooldownDelay * 60000 : Math.round(((this.lastHeatCycle.getDate() + this.heater.cooldownDelay * 60000) - now) / 1000);
188
+ return Math.min(Math.max(0, cooldown), this.heater.cooldownDelay * 60);
189
+ }
190
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
191
+ try {
192
+ // Initialize the desired state.
193
+ this.isOn = isOn;
194
+ this.isCooling = false;
195
+ let target = hstate.startupDelay === false && isOn;
196
+ if (target && typeof hstate.endTime !== 'undefined') {
197
+ // Calculate a short cycle time so that the gas heater does not cycle
198
+ // too often. For gas heaters this is 60 seconds. This gives enough time
199
+ // for the heater control circuit to make a full cycle.
200
+ if (new Date().getTime() - hstate.endTime.getTime() < this.heater.minCycleTime * 60000) {
201
+ logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
202
+ target = false;
203
+ }
204
+ }
205
+ // Here we go we need to set the firemans switch state.
206
+ if (hstate.isOn !== target) {
207
+ logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
208
+ }
209
+ if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
210
+ if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
211
+ this._lastState = hstate.isOn = target;
212
+ }
213
+ else {
214
+ let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
215
+ { isOn: target, latch: target ? 10000 : undefined });
216
+ if (res.status.code === 200) this._lastState = hstate.isOn = target;
217
+ else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
218
+ }
219
+ if (target) this.lastHeatCycle = new Date();
220
+ }
221
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
222
+ }
223
+ public async pollEquipmentAsync() {
224
+ let self = this;
225
+ try {
226
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
227
+ this._pollTimer = null;
228
+ let success = false;
229
+ }
230
+ catch (err) { logger.error(`Nixie Error polling Heater - ${err}`); }
231
+ finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
232
+ }
233
+ private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
234
+ try {
235
+ let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
236
+ return dev;
237
+ } catch (err) { logger.error(`Nixie Heater Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
238
+ }
239
+ public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
240
+ try {
241
+ if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
242
+ && typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
243
+ try {
244
+ let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
245
+ // If we have a status check the return.
246
+ hstate.commStatus = stat.hasFault ? 1 : 0;
247
+ } catch (err) { hstate.commStatus = 1; }
248
+ }
249
+ else
250
+ hstate.commStatus = 0;
251
+ } catch (err) { logger.error(`Nixie Error checking heater Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
252
+ }
253
+ public async closeAsync() {
254
+ try {
255
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
256
+ this._pollTimer = null;
257
+ let hstate = state.heaters.getItemById(this.heater.id);
258
+ await this.setHeaterStateAsync(hstate, false);
259
+ hstate.emitEquipmentChange();
260
+ }
261
+ catch (err) { logger.error(`Nixie Heater closeAsync: ${err.message}`); return Promise.reject(err); }
262
+ }
263
+ public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
264
+ }
265
+ export class NixieSolarHeater extends NixieHeaterBase {
266
+ public pollingInterval: number = 10000;
267
+ declare heater: Heater;
268
+ constructor(ncp: INixieControlPanel, heater: Heater) {
269
+ super(ncp, heater);
270
+ this.heater = heater;
271
+ this.pollEquipmentAsync();
272
+ }
273
+ public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
274
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
275
+ try {
276
+ let origState = hstate.isOn;
277
+ // Initialize the desired state.
278
+ this.isOn = isOn;
279
+ this.isCooling = isCooling;
280
+ let target = hstate.startupDelay === false && isOn;
281
+ if (target && typeof hstate.endTime !== 'undefined') {
282
+ // Calculate a short cycle time so that the solar heater does not cycle
283
+ // too often. For solar heaters this is 60 seconds. This gives enough time
284
+ // for the valve to rotate and start heating. If the solar and water sensors are
285
+ // not having issues this should be plenty of time.
286
+ if (new Date().getTime() - hstate.endTime.getTime() < 60000) {
287
+ logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
288
+ target = false;
289
+ }
290
+ }
291
+
292
+ // Here we go we need to set the valve status that is attached to solar.
293
+ if (hstate.isOn !== target) {
294
+ logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`);
295
+ }
296
+ if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
297
+ if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
298
+ this._lastState = hstate.isOn = target;
299
+ hstate.isCooling = target && isCooling;
300
+ }
301
+ else {
302
+ let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
303
+ { isOn: target, latch: target ? 10000 : undefined });
304
+ if (res.status.code === 200) {
305
+ this._lastState = hstate.isOn = target;
306
+ hstate.isCooling = target && isCooling;
307
+ }
308
+ else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`);
309
+ }
310
+ if (target) {
311
+ if (isCooling) this.lastCoolCycle = new Date();
312
+ else if (isOn) this.lastHeatCycle = new Date();
313
+ }
314
+ }
315
+ // In this instance we need to see if there are cleaner circuits that we need to turn off
316
+ // then delay for the current body because the solar just came on.
317
+ if (hstate.isOn && sys.general.options.cleanerSolarDelay && !origState) {
318
+ let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('cleaner') !== -1 && x.body === hstate.bodyId });
319
+ let cleaners = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
320
+ // Turn off all the cleaner circuits and set an on delay if they are on.
321
+ for (let i = 0; i < cleaners.length; i++) {
322
+ let cleaner = cleaners.getItemByIndex(i);
323
+ if (cleaner.isActive) {
324
+ let cstate = state.circuits.getItemById(cleaner.id);
325
+ if (cstate.isOn && sys.general.options.cleanerSolarDelayTime > 0) {
326
+ // Turn off the circuit then set a delay.
327
+ logger.info(`Setting cleaner solar delay for ${cleaner.name} to ${sys.general.options.cleanerSolarDelayTime}`);
328
+ await sys.board.circuits.setCircuitStateAsync(cstate.id, false);
329
+ delayMgr.setCleanerStartDelay(cstate, hstate.bodyId, sys.general.options.cleanerSolarDelayTime);
330
+ }
331
+ }
332
+ }
333
+ }
334
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
335
+ }
336
+ public async pollEquipmentAsync() {
337
+ let self = this;
338
+ try {
339
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
340
+ this._pollTimer = null;
341
+ let success = false;
342
+ }
343
+ catch (err) { logger.error(`Nixie Error polling Heater - ${err}`); }
344
+ finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
345
+ }
346
+ private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
347
+ try {
348
+ let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
349
+ return dev;
350
+ } catch (err) { logger.error(`Nixie Heater Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
351
+ }
352
+ public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
353
+ try {
354
+ if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
355
+ && typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
356
+ try {
357
+ let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
358
+ // If we have a status check the return.
359
+ hstate.commStatus = stat.hasFault ? 1 : 0;
360
+ } catch (err) { hstate.commStatus = 1; }
361
+ }
362
+ else
363
+ hstate.commStatus = 0;
364
+ } catch (err) { logger.error(`Nixie Error checking heater Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
365
+ }
366
+ public async closeAsync() {
367
+ try {
368
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
369
+ this._pollTimer = null;
370
+ let hstate = state.heaters.getItemById(this.heater.id);
371
+ await this.setHeaterStateAsync(hstate, false, false);
372
+ hstate.emitEquipmentChange();
373
+ }
374
+ catch (err) { logger.error(`Nixie Heater closeAsync: ${err.message}`); return Promise.reject(err); }
375
+ }
376
+ public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
377
+ }
378
+ export class NixieHeatpump extends NixieHeaterBase {
379
+ public pollingInterval: number = 10000;
380
+ //declare heater: Heater;
381
+ constructor(ncp: INixieControlPanel, heater: Heater) {
382
+ super(ncp, heater);
383
+ this.heater = heater;
384
+ if (typeof this.heater.stopTempDelta === 'undefined') this.heater.stopTempDelta = 1;
385
+ if (typeof this.heater.minCycleTime === 'undefined') this.heater.minCycleTime = 2;
386
+ this.pollEquipmentAsync();
387
+ }
388
+ public get id(): number { return typeof this.heater !== 'undefined' ? this.heater.id : -1; }
389
+ public getCooldownTime(): number { return 0; } // There is no cooldown delay at this time for a heatpump
390
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
391
+ try {
392
+ // Initialize the desired state.
393
+ this.isOn = isOn;
394
+ this.isCooling = false;
395
+ let target = hstate.startupDelay === false && isOn;
396
+ if (target && typeof hstate.endTime !== 'undefined') {
397
+ // Calculate a short cycle time so that the gas heater does not cycle
398
+ // too often. For gas heaters this is 60 seconds. This gives enough time
399
+ // for the heater control circuit to make a full cycle.
400
+ if (new Date().getTime() - hstate.endTime.getTime() < this.heater.minCycleTime * 60000) {
401
+ logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
402
+ target = false;
403
+ }
404
+ }
405
+ // Here we go we need to set the firemans switch state.
406
+ if (hstate.isOn !== target) {
407
+ logger.info(`Nixie: Set Heatpump ${hstate.id}-${hstate.name} to ${isOn}`);
408
+ }
409
+ if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
410
+ if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) {
411
+ this._lastState = hstate.isOn = target;
412
+ }
413
+ else {
414
+ let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`,
415
+ { isOn: target, latch: target ? 10000 : undefined });
416
+ if (res.status.code === 200) this._lastState = hstate.isOn = target;
417
+ else logger.error(`Nixie Error setting heatpump state: ${res.status.code} -${res.status.message} ${res.error.message}`);
418
+ }
419
+ if (target) this.lastHeatCycle = new Date();
420
+ }
421
+ } catch (err) { return logger.error(`Nixie Error setting heatpump state ${hstate.id}-${hstate.name}: ${err.message}`); }
422
+ }
423
+ public async pollEquipmentAsync() {
424
+ let self = this;
425
+ try {
426
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
427
+ this._pollTimer = null;
428
+ let success = false;
429
+ }
430
+ catch (err) { logger.error(`Nixie Error polling Heatpump - ${err}`); }
431
+ finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
432
+ }
433
+ private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
434
+ try {
435
+ let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
436
+ return dev;
437
+ } catch (err) { logger.error(`Nixie Heatpump Error checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
438
+ }
439
+ public async validateSetupAsync(heater: Heater, hstate: HeaterState) {
440
+ try {
441
+ if (typeof heater.connectionId !== 'undefined' && heater.connectionId !== ''
442
+ && typeof heater.deviceBinding !== 'undefined' && heater.deviceBinding !== '') {
443
+ try {
444
+ let stat = await this.checkHardwareStatusAsync(heater.connectionId, heater.deviceBinding);
445
+ // If we have a status check the return.
446
+ hstate.commStatus = stat.hasFault ? 1 : 0;
447
+ } catch (err) { hstate.commStatus = 1; }
448
+ }
449
+ else
450
+ hstate.commStatus = 0;
451
+ } catch (err) { logger.error(`Nixie Error checking heatpump Hardware ${this.heater.name}: ${err.message}`); hstate.commStatus = 1; return Promise.reject(err); }
452
+ }
453
+ public async closeAsync() {
454
+ try {
455
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
456
+ this._pollTimer = null;
457
+ let hstate = state.heaters.getItemById(this.heater.id);
458
+ await this.setHeaterStateAsync(hstate, false);
459
+ hstate.emitEquipmentChange();
460
+ }
461
+ catch (err) { logger.error(`Nixie Heatpump closeAsync: ${err.message}`); return Promise.reject(err); }
462
+ }
463
+ public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
464
+ }
465
+ export class NixieUltratemp extends NixieHeaterBase {
466
+ constructor(ncp: INixieControlPanel, heater: Heater) {
467
+ super(ncp, heater);
468
+ // Set the polling interval to 3 seconds.
469
+ this.pollEquipmentAsync();
470
+ }
471
+ public async setServiceModeAsync() {
472
+ let hstate = state.heaters.getItemById(this.heater.id);
473
+ await this.setHeaterStateAsync(hstate, false, false);
474
+ await this.releaseHeater(hstate);
475
+ }
476
+
477
+ public async pollEquipmentAsync() {
478
+ let self = this;
479
+ try {
480
+ this.suspendPolling = true;
481
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
482
+ this._pollTimer = null;
483
+ if (this._suspendPolling > 1) return;
484
+ let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
485
+ // If the body isn't on then we won't communicate with the chem controller. There is no need
486
+ // since most of the time these are attached to the filter relay.
487
+ if (!this.closing) {
488
+ await this.setStatus(sheater);
489
+ }
490
+ }
491
+ catch (err) { logger.error(`Error polling UltraTemp heater - ${err}`); }
492
+ finally {
493
+ this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
494
+ try { await self.pollEquipmentAsync() } catch (err) { }
495
+ }, this.pollingInterval || 10000);
496
+ }
497
+ }
498
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
499
+ try {
500
+ // Initialize the desired state.
501
+ this.isCooling = isCooling;
502
+ if (hstate.isOn !== isOn) {
503
+ logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isCooling ? 'cooling' : isOn ? 'heating' : 'off'}`);
504
+
505
+ }
506
+ if (isOn && !hstate.startupDelay) this.lastHeatCycle = new Date();
507
+ this.isOn = hstate.isOn = isOn;
508
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
509
+ }
510
+ public async releaseHeater(sheater: HeaterState): Promise<boolean> {
511
+ try {
512
+ let out = Outbound.create({
513
+ portId: this.heater.portId || 0,
514
+ protocol: Protocol.Heater,
515
+ source: 16,
516
+ dest: this.heater.address,
517
+ action: 114,
518
+ payload: [],
519
+ retries: 3, // We are going to try 4 times.
520
+ response: Response.create({ protocol: Protocol.Heater, action: 115 }),
521
+ onAbort: () => { }
522
+ });
523
+ out.appendPayloadBytes(0, 10);
524
+ out.setPayloadByte(0, 144);
525
+ out.setPayloadByte(1, 0, 0);
526
+ await out.sendAsync();
527
+ return true;
528
+
529
+ } catch (err) {
530
+ // 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
531
+ // come across this will be cleared by the processing of that message.
532
+ sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
533
+ state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
534
+ logger.error(`Communication error with Ultratemp : ${err.message}`);
535
+ return false;
536
+ }
537
+ }
538
+ public async setStatus(sheater: HeaterState): Promise<boolean> {
539
+ try {
540
+ let out = Outbound.create({
541
+ portId: this.heater.portId || 0,
542
+ protocol: Protocol.Heater,
543
+ source: 16,
544
+ dest: this.heater.address,
545
+ action: 114,
546
+ payload: [],
547
+ retries: 3, // We are going to try 4 times.
548
+ response: Response.create({ protocol: Protocol.Heater, action: 115 }),
549
+ onAbort: () => { }
550
+ });
551
+ out.appendPayloadBytes(0, 10);
552
+ out.setPayloadByte(0, 144);
553
+ // If we are in startup delay simply tell the heater that it is off.
554
+ if (sheater.startupDelay || this.closing)
555
+ out.setPayloadByte(1, 0, 0);
556
+ else {
557
+ if (this.isOn) {
558
+ if (!this.isCooling) this.lastHeatCycle = new Date();
559
+ else this.lastCoolCycle = new Date();
560
+ }
561
+ //console.log(`Setting the heater byte ${this.isOn} ${sheater.isOn} to ${this.isOn ? (this.isCooling ? 2 : 1) : 0}`);
562
+ out.setPayloadByte(1, this.isOn ? (this.isCooling ? 2 : 1) : 0, 0);
563
+ }
564
+ let success = await out.sendAsync();
565
+ return success;
566
+ } catch (err) {
567
+ // 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
568
+ // come across this will be cleared by the processing of that message.
569
+ sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
570
+ state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
571
+ logger.error(`Communication error with Ultratemp : ${err.message}`);
572
+ return false;
573
+ }
574
+ }
575
+ public async closeAsync() {
576
+ try {
577
+ this.suspendPolling = true;
578
+ this.closing = true;
579
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
580
+ this._pollTimer = null;
581
+ let sheater = state.heaters.getItemById(this.id);
582
+ await this.releaseHeater(sheater);
583
+ logger.info(`Closing Heater ${this.heater.name}`);
584
+ }
585
+ catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
586
+ }
587
+ }
588
+ export class NixieMastertemp extends NixieGasHeater {
589
+ constructor(ncp: INixieControlPanel, heater: Heater) {
590
+ super(ncp, heater);
591
+ // Set the polling interval to 3 seconds.
592
+ this.pollEquipmentAsync();
593
+ this.pollingInterval = 3000;
594
+
595
+ }
596
+ /* public getCooldownTime(): number {
597
+ // Delays are always in terms of seconds so convert the minute to seconds.
598
+ if (this.heater.cooldownDelay === 0 || typeof this.lastHeatCycle === 'undefined') return 0;
599
+ let now = new Date().getTime();
600
+ let cooldown = this.isOn ? this.heater.cooldownDelay * 60000 : Math.round(((this.lastHeatCycle.getDate() + this.heater.cooldownDelay * 60000) - now) / 1000);
601
+ return Math.min(Math.max(0, cooldown), this.heater.cooldownDelay * 60);
602
+ } */
603
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) {
604
+ try {
605
+ // Initialize the desired state.
606
+ this.isOn = isOn;
607
+ this.isCooling = false;
608
+ let target = hstate.startupDelay === false && isOn;
609
+ if (target && typeof hstate.endTime !== 'undefined') {
610
+ // Calculate a short cycle time so that the gas heater does not cycle
611
+ // too often. For gas heaters this is 60 seconds. This gives enough time
612
+ // for the heater control circuit to make a full cycle.
613
+ if (new Date().getTime() - hstate.endTime.getTime() < this.heater.minCycleTime * 60000) {
614
+ logger.verbose(`${hstate.name} short cycle detected deferring turn on state`);
615
+ target = false;
616
+ }
617
+ }
618
+ // Here we go we need to set the state.
619
+ if (hstate.isOn !== target) {
620
+ logger.info(`Nixie: Set Mastertemp ${hstate.id}-${hstate.name} to ${isOn}`);
621
+ }
622
+ if (typeof this._lastState === 'undefined' || target || this._lastState !== target) {
623
+ this._lastState = hstate.isOn = target;
624
+ if (target) this.lastHeatCycle = new Date();
625
+ }
626
+ hstate.isOn = isOn;
627
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
628
+ }
629
+ public async pollEquipmentAsync() {
630
+ let self = this;
631
+ try {
632
+ this.suspendPolling = true;
633
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
634
+ this._pollTimer = null;
635
+ if (this._suspendPolling > 1) return;
636
+ let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
637
+ if (!this.closing) await this.setStatus(sheater);
638
+ }
639
+ catch (err) { logger.error(`Error polling MasterTemp heater - ${err}`); }
640
+ finally {
641
+ this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
642
+ try { await self.pollEquipmentAsync() } catch (err) { }
643
+ }, this.pollingInterval || 3000);
644
+ }
645
+ }
646
+ public async setStatus(sheater: HeaterState): Promise<boolean> {
647
+ try {
648
+ let out = Outbound.create({
649
+ portId: this.heater.portId || 0,
650
+ protocol: Protocol.Heater,
651
+ source: 16,
652
+ dest: this.heater.address,
653
+ action: 112,
654
+ payload: [],
655
+ retries: 3, // We are going to try 4 times.
656
+ response: Response.create({ protocol: Protocol.Heater, action: 116 }),
657
+ onAbort: () => { }
658
+ });
659
+ out.appendPayloadBytes(0, 11);
660
+ // If we have a startup delay we need to simply send 0 to the heater to make sure that it is off.
661
+ if (sheater.startupDelay)
662
+ out.setPayloadByte(0, 0);
663
+ else {
664
+ // The cooldown delay is a bit hard to figure out here since I think the heater does it on its own.
665
+ out.setPayloadByte(0, sheater.bodyId <= 2 ? sheater.bodyId : 0);
666
+ }
667
+ out.setPayloadByte(1, sys.bodies.getItemById(1).heatSetpoint || 0);
668
+ out.setPayloadByte(2, sys.bodies.getItemById(2).heatSetpoint || 0);
669
+ let success = await out.sendAsync();
670
+ return success;
671
+ } catch (err) {
672
+ // 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
673
+ // come across this will be cleared by the processing of that message.
674
+ sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
675
+ state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
676
+ logger.error(`Communication error with MasterTemp : ${err.message}`);
677
+ return false;
678
+ }
679
+ }
680
+ public async setServiceModeAsync() {
681
+ let hstate = state.heaters.getItemById(this.heater.id);
682
+ await this.setHeaterStateAsync(hstate, false);
683
+ }
684
+ public async closeAsync() {
685
+ try {
686
+ this.suspendPolling = true;
687
+ this.closing = true;
688
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
689
+ this._pollTimer = null;
690
+ logger.info(`Closing Heater ${this.heater.name}`);
691
+
692
+ }
693
+ catch (err) { logger.error(`MasterTemp closeAsync: ${err.message}`); return Promise.reject(err); }
694
+ }
695
+ }
696
+ export class NixieUltraTempETi extends NixieHeaterBase {
697
+ constructor(ncp: INixieControlPanel, heater: Heater) {
698
+ super(ncp, heater);
699
+ // Set the polling interval to 3 seconds.
700
+ this.pollEquipmentAsync();
701
+ }
702
+ public async pollEquipmentAsync() {
703
+ let self = this;
704
+ try {
705
+ this.suspendPolling = true;
706
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
707
+ this._pollTimer = null;
708
+ if (this._suspendPolling > 1) return;
709
+ let sheater = state.heaters.getItemById(this.heater.id, !this.closing);
710
+ // If the body isn't on then we won't communicate with the chem controller. There is no need
711
+ // since most of the time these are attached to the filter relay.
712
+ if (!this.closing) {
713
+ await this.setStatus(sheater);
714
+ }
715
+ }
716
+ catch (err) { logger.error(`Error polling UltraTemp ETi heater - ${err}`); }
717
+ finally {
718
+ this.suspendPolling = false; if (!this.closing) this._pollTimer = setTimeout(async () => {
719
+ try { await self.pollEquipmentAsync() } catch (err) { }
720
+ }, this.pollingInterval || 10000);
721
+ }
722
+ }
723
+ public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean, isCooling: boolean) {
724
+ try {
725
+ // Initialize the desired state.
726
+ this.isCooling = isCooling;
727
+ if (hstate.isOn !== isOn) {
728
+ logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isCooling ? 'cooling' : isOn ? 'heating' : 'off'}`);
729
+
730
+ }
731
+ if (isOn && !hstate.startupDelay) this.lastHeatCycle = new Date();
732
+ this.isOn = hstate.isOn = isOn;
733
+ } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); }
734
+ }
735
+ public async releaseHeater(sheater: HeaterState): Promise<boolean> {
736
+ try {
737
+ let out = Outbound.create({
738
+ portId: this.heater.portId || 0,
739
+ protocol: Protocol.Heater,
740
+ source: 16,
741
+ dest: this.heater.address,
742
+ action: 112,
743
+ payload: [],
744
+ retries: 3, // We are going to try 4 times.
745
+ response: Response.create({ protocol: Protocol.Heater, action: 113 }),
746
+ onAbort: () => { }
747
+ });
748
+ out.appendPayloadBytes(0, 10);
749
+ out.setPayloadByte(0, 0);
750
+ out.setPayloadByte(1, 0);
751
+ out.setPayloadByte(2, 78);
752
+ out.setPayloadByte(3, 1);
753
+ out.setPayloadByte(4, 5);
754
+ let success = await out.sendAsync();
755
+ return success;
756
+ } catch (err) {
757
+ // If the Ultratemp is not responding we need to store that off but at this point we know none of the codes. If a 113 does
758
+ // come across this will be cleared by the processing of that message.
759
+ sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
760
+ state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
761
+ logger.error(`Communication error with Ultratemp : ${err.message}`);
762
+ return false;
763
+ }
764
+ }
765
+ protected calcHeatModeByte(body: Body): number {
766
+ let byte = 0;
767
+ if (this.closing) return 0; // We are closing so just set the heat mode to off.
768
+ let mode = sys.board.valueMaps.heatModes.transform(body.heatMode || 0);
769
+ switch (mode.name) {
770
+ case 'hpump':
771
+ case 'heatpump':
772
+ byte = 1;
773
+ break;
774
+ case 'heater':
775
+ byte = 2;
776
+ break;
777
+ case 'heatpumpref':
778
+ case 'heatpumppref':
779
+ case 'hybrid':
780
+ byte = 3;
781
+ break;
782
+ case 'dual':
783
+ byte = 4;
784
+ break;
785
+ }
786
+ return byte;
787
+ }
788
+ public async setServiceModeAsync() {
789
+ let hstate = state.heaters.getItemById(this.heater.id);
790
+ await this.setHeaterStateAsync(hstate, false, false);
791
+ await this.releaseHeater(hstate);
792
+ }
793
+ public async setStatus(sheater: HeaterState): Promise<boolean> {
794
+ try {
795
+ let out = Outbound.create({
796
+ portId: this.heater.portId || 0,
797
+ protocol: Protocol.Heater,
798
+ source: 16,
799
+ dest: this.heater.address,
800
+ action: 112,
801
+ payload: [],
802
+ retries: 3, // We are going to try 4 times.
803
+ response: Response.create({ protocol: Protocol.Heater, action: 113 }),
804
+ onAbort: () => { }
805
+ });
806
+ out.appendPayloadBytes(0, 10);
807
+ out.setPayloadByte(0, this.isOn && !sheater.startupDelay && !this.closing ? 1 : 0);
808
+ if (sheater.bodyId > 0) {
809
+ let body = sys.bodies.getItemById(sheater.bodyId);
810
+ out.setPayloadByte(1, this.calcHeatModeByte(body));
811
+ out.setPayloadByte(2, body.setPoint);
812
+ }
813
+ else out.setPayloadByte(2, utils.convert.temperature.convertUnits(78, 'F', sys.board.valueMaps.tempUnits.getName(state.temps.units) || 'F')); // Just set it to a valid setpoint and call it a day.
814
+ out.setPayloadByte(3, this.heater.economyTime, 1);
815
+ out.setPayloadByte(4, this.heater.maxBoostTemp, 5);
816
+ if (this.isOn) {
817
+ if (!this.isCooling) this.lastHeatCycle = new Date();
818
+ else this.lastCoolCycle = new Date();
819
+ }
820
+ let success = await out.sendAsync();
821
+ return success;
822
+ } catch (err) {
823
+ // If the Ultratemp ETi is not responding we need to store that off but at this point we know none of the codes. If a 113 does
824
+ // come across this will be cleared by the processing of that message.
825
+ sheater.commStatus = sys.board.valueMaps.equipmentCommStatus.getValue('commerr');
826
+ state.equipment.messages.setMessageByCode(`heater:${sheater.id}:comms`, 'error', `Communication error with ${sheater.name}`);
827
+ logger.error(`Communication error with Ultratemp ETi : ${err.message}`);
828
+ return false;
829
+ }
830
+ }
831
+ public async closeAsync() {
832
+ try {
833
+ this.suspendPolling = true;
834
+ this.closing = true;
835
+ if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
836
+ this._pollTimer = null;
837
+ let sheater = state.heaters.getItemById(this.id);
838
+ await this.releaseHeater(sheater);
839
+ logger.info(`Closing Heater ${this.heater.name}`);
840
+ }
841
+ catch (err) { logger.error(`Ultratemp closeAsync: ${err.message}`); return Promise.reject(err); }
842
+ }
843
+ }