nodejs-poolcontroller 7.2.0 → 7.5.1

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