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
@@ -36,6 +36,7 @@ export class State implements IState {
36
36
  private _isDirty: boolean;
37
37
  private _timerDirty: NodeJS.Timeout;
38
38
  protected _dt: Timestamp;
39
+ protected _startTime: Timestamp;
39
40
  protected _controllerType: ControllerType;
40
41
  protected onchange = (obj, fn) => {
41
42
  const handler = {
@@ -191,6 +192,8 @@ export class State implements IState {
191
192
  public get controllerState() {
192
193
  var self = this;
193
194
  return {
195
+ systemUnits: sys.board.valueMaps.systemUnits.transform(sys.general.options.units),
196
+ startTime: self.data.startTime || '',
194
197
  time: self.data.time || '',
195
198
  // body: self.data.body || {},
196
199
  valve: self.data.valve || 0,
@@ -200,7 +203,6 @@ export class State implements IState {
200
203
  batteryVoltage: self.data.batteryVoltage || 0,
201
204
  status: self.data.status || {},
202
205
  mode: self.data.mode || {},
203
- // freeze: self.data.freeze || false,
204
206
  appVersion: sys.appVersion || '',
205
207
  appVersionState: self.appVersion.get(true) || {},
206
208
  clockMode: sys.board.valueMaps.clockModes.transform(sys.general.options.clockMode) || {},
@@ -209,7 +211,8 @@ export class State implements IState {
209
211
  model: sys.equipment.model,
210
212
  sunrise: self.data.sunrise || '',
211
213
  sunset: self.data.sunset || '',
212
- alias: sys.general.alias
214
+ alias: sys.general.alias,
215
+ freeze: utils.makeBool(self.data.freeze)
213
216
  };
214
217
  }
215
218
  public emitAllEquipmentChanges() {
@@ -305,7 +308,6 @@ export class State implements IState {
305
308
  this.hasChanged = true;
306
309
  }
307
310
  }
308
-
309
311
  }
310
312
  public get valve(): number { return this.data.valve; }
311
313
  public set valve(val: number) {
@@ -330,14 +332,30 @@ export class State implements IState {
330
332
  public get isInitialized(): boolean { return typeof (this.data.status) !== 'undefined' && this.data.status.val !== 0; }
331
333
  public init() {
332
334
  console.log(`Init state for Pool Controller`);
333
- var state = this.loadFile(this.statePath, {});
334
- state = extend(true, { mode: { val: -1 }, temps: { units: { val: 0, name: 'F', desc: 'Fahrenheit' } } }, state);
335
+ var sdata = this.loadFile(this.statePath, {});
336
+ sdata = extend(true, { mode: { val: -1 }, temps: { units: { val: 0, name: 'F', desc: 'Fahrenheit' } } }, sdata);
337
+ if (typeof sdata.temps !== 'undefined' && typeof sdata.temps.bodies !== 'undefined') {
338
+ EqStateCollection.removeNullIds(sdata.temps.bodies);
339
+ }
340
+ EqStateCollection.removeNullIds(sdata.schedules);
341
+ EqStateCollection.removeNullIds(sdata.features);
342
+ EqStateCollection.removeNullIds(sdata.circuits);
343
+ EqStateCollection.removeNullIds(sdata.pumps);
344
+ EqStateCollection.removeNullIds(sdata.chlorinators);
345
+ EqStateCollection.removeNullIds(sdata.valves);
346
+ EqStateCollection.removeNullIds(sdata.heaters);
347
+ EqStateCollection.removeNullIds(sdata.covers);
348
+ EqStateCollection.removeNullIds(sdata.circuitGroups);
349
+ EqStateCollection.removeNullIds(sdata.lightGroups);
350
+ EqStateCollection.removeNullIds(sdata.remotes);
351
+ EqStateCollection.removeNullIds(sdata.chemControllers);
352
+ EqStateCollection.removeNullIds(sdata.filters);
335
353
  var self = this;
336
- let pnlTime = typeof state.time !== 'undefined' ? new Date(state.time) : new Date();
354
+ let pnlTime = typeof sdata.time !== 'undefined' ? new Date(sdata.time) : new Date();
337
355
  if (isNaN(pnlTime.getTime())) pnlTime = new Date();
338
356
  this._dt = new Timestamp(pnlTime);
339
357
  this._dt.milliseconds = 0;
340
- this.data = state;
358
+ this.data = sdata;
341
359
  //this.onchange(state, function () { self.dirty = true; });
342
360
  this._dt.emitter.on('change', function () {
343
361
  self.data.time = self._dt.format();
@@ -370,6 +388,7 @@ export class State implements IState {
370
388
  this.comms = new CommsState();
371
389
  this.heliotrope = new Heliotrope();
372
390
  this.appVersion = new AppVersionState(this.data, 'appVersion');
391
+ this.data.startTime = Timestamp.toISOLocal(new Date());
373
392
  versionCheck.checkGitLocal();
374
393
  }
375
394
  public resetData() {
@@ -472,6 +491,7 @@ export interface ICircuitState {
472
491
  emitEquipmentChange();
473
492
  get(bCopy?: boolean);
474
493
  showInFeatures?: boolean;
494
+ isActive?: boolean;
475
495
  }
476
496
 
477
497
  interface IEqStateCreator<T> { ctor(data: any, name: string, parent?): T; }
@@ -571,6 +591,20 @@ class EqStateCollection<T> {
571
591
  if (typeof (data[name]) === 'undefined') data[name] = [];
572
592
  this.data = data[name];
573
593
  }
594
+ public static removeNullIds(data: any) {
595
+ if (typeof data !== 'undefined' && Array.isArray(data) && typeof data.length === 'number') {
596
+ for (let i = data.length - 1; i >= 0; i--) {
597
+ if (typeof data[i].id !== 'number') {
598
+ console.log(`Removing ${data[i].id}-${data[i].name}`);
599
+ data.splice(i, 1);
600
+ }
601
+ else if (typeof data[i].id === 'undefined' || isNaN(data[i].id)) {
602
+ console.log(`Removing isNaN ${data[i].id}-${data[i].name}`);
603
+ data.splice(i, 1);
604
+ }
605
+ }
606
+ }
607
+ }
574
608
  public getItemById(id: number, add?: boolean, data?: any): T {
575
609
  for (let i = 0; i < this.data.length; i++)
576
610
  if (typeof this.data[i].id !== 'undefined' && this.data[i].id === id) {
@@ -779,6 +813,12 @@ export class EquipmentMessages extends EqStateCollection<EquipmentMessage> {
779
813
  }
780
814
  return rem;
781
815
  }
816
+ public setMessageByCode(code: string, severity: string | number, message: string): EquipmentMessage {
817
+ let msg = this.getItemByCode(code, true);
818
+ msg.severity = sys.board.valueMaps.eqMessageSeverities.encode(severity, 0);
819
+ msg.message = message;
820
+ return msg;
821
+ }
782
822
  }
783
823
  export class EquipmentMessage extends ChildEqState {
784
824
  public initData() {
@@ -821,7 +861,10 @@ export class PumpStateCollection extends EqStateCollection<PumpState> {
821
861
  }
822
862
  public cleanupState() {
823
863
  for (let i = this.data.length - 1; i >= 0; i--) {
824
- if (typeof sys.pumps.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
864
+ if (isNaN(this.data[i].id)) this.data.splice(i, 1);
865
+ else {
866
+ if (typeof sys.pumps.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
867
+ }
825
868
  }
826
869
  let cfg = sys.pumps.toArray();
827
870
  for (let i = 0; i < cfg.length; i++) {
@@ -836,6 +879,11 @@ export class PumpStateCollection extends EqStateCollection<PumpState> {
836
879
  }
837
880
  export class PumpState extends EqState {
838
881
  public dataName: string = 'pump';
882
+ public initData() {
883
+ if (typeof this.data.status === 'undefined') {
884
+ this.data.status = { name: 'ok', desc: 'Ok', val: 0 };
885
+ }
886
+ }
839
887
  private _threshold = 0.05;
840
888
  private exceedsThreshold(origVal: number, newVal: number) {
841
889
  return Math.abs((newVal - origVal) / origVal) > this._threshold;
@@ -869,7 +917,7 @@ export class PumpState extends EqState {
869
917
  // quick fix for #172
870
918
  if (this.status !== val) {
871
919
  if (sys.board.valueMaps.pumpTypes.getName(this.type) === 'vsf' && val === 0) {
872
- this.data.status = { name: 'ok', desc: 'Ok', val };
920
+ this.data.status = { name: 'ok', desc: 'Ok', val: 0 };
873
921
  }
874
922
  else this.data.status = sys.board.valueMaps.pumpStatus.transform(val);
875
923
  this.hasChanged = true;
@@ -951,6 +999,7 @@ export class ScheduleState extends EqState {
951
999
  public get startDate(): Date { return this._startDate; }
952
1000
  public set startDate(val: Date) { this._startDate = val; this._saveStartDate(); }
953
1001
  private _saveStartDate() {
1002
+ if (typeof this._startDate === 'undefined') this._startDate = new Date();
954
1003
  this.startDate.setHours(0, 0, 0, 0);
955
1004
  this.setDataVal('startDate', Timestamp.toISOLocal(this.startDate));
956
1005
  }
@@ -1055,7 +1104,10 @@ export class CircuitGroupStateCollection extends EqStateCollection<CircuitGroupS
1055
1104
  }
1056
1105
  public cleanupState() {
1057
1106
  for (let i = this.data.length - 1; i >= 0; i--) {
1058
- if (typeof sys.circuitGroups.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1107
+ if (isNaN(this.data[i].id)) this.data.splice(i, 1);
1108
+ else {
1109
+ if (typeof sys.circuitGroups.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1110
+ }
1059
1111
  }
1060
1112
  let cfg = sys.circuitGroups.toArray();
1061
1113
  for (let i = 0; i < cfg.length; i++) {
@@ -1091,7 +1143,7 @@ export class CircuitGroupState extends EqState implements ICircuitGroupState, IC
1091
1143
  if (typeof this.data.endTime === 'undefined') return undefined;
1092
1144
  return new Timestamp(this.data.endTime);
1093
1145
  }
1094
- public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate()), false) : this.setDataVal('endTime', undefined); }
1146
+ public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
1095
1147
  public get isActive(): boolean { return this.data.isActive; }
1096
1148
  public set isActive(val: boolean) { this.setDataVal('isActive', val); }
1097
1149
  public get showInFeatures(): boolean { return typeof this.data.showInFeatures === 'undefined' ? true : this.data.showInFeatures; }
@@ -1115,12 +1167,23 @@ export class CircuitGroupState extends EqState implements ICircuitGroupState, IC
1115
1167
  state._dirtyList.removeEqState(this);
1116
1168
  }
1117
1169
  }
1170
+ public get(bcopy?: boolean): any {
1171
+ let d = super.get(bcopy);
1172
+ let cg = sys.circuitGroups.getItemById(this.id);
1173
+ if (!cg.isActive) d.isActive = false;
1174
+ else d.isActive = undefined;
1175
+ return d;
1176
+ }
1177
+
1118
1178
  }
1119
1179
  export class LightGroupStateCollection extends EqStateCollection<LightGroupState> {
1120
1180
  public createItem(data: any): LightGroupState { return new LightGroupState(data); }
1121
1181
  public cleanupState() {
1122
1182
  for (let i = this.data.length - 1; i >= 0; i--) {
1123
- if (typeof sys.lightGroups.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1183
+ if (isNaN(this.data[i].id)) this.data.splice(i, 1);
1184
+ else {
1185
+ if (typeof sys.lightGroups.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1186
+ }
1124
1187
  }
1125
1188
  let cfg = sys.lightGroups.toArray();
1126
1189
  for (let i = 0; i < cfg.length; i++) {
@@ -1164,7 +1227,7 @@ export class LightGroupState extends EqState implements ICircuitGroupState, ICir
1164
1227
  if (typeof this.data.endTime === 'undefined') return undefined;
1165
1228
  return new Timestamp(this.data.endTime);
1166
1229
  }
1167
- public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate()), false) : this.setDataVal('endTime', undefined); }
1230
+ public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
1168
1231
  public get isOn(): boolean { return this.data.isOn; }
1169
1232
  public set isOn(val: boolean) { this.setDataVal('isOn', val); }
1170
1233
  public get isActive(): boolean { return this.data.isActive; }
@@ -1201,8 +1264,12 @@ export class BodyTempStateCollection extends EqStateCollection<BodyTempState> {
1201
1264
  }
1202
1265
  public cleanupState() {
1203
1266
  for (let i = this.data.length - 1; i >= 0; i--) {
1204
- if (typeof sys.bodies.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1267
+ if (isNaN(this.data[i].id)) this.data.splice(i, 1);
1268
+ else {
1269
+ if (typeof sys.bodies.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1270
+ }
1205
1271
  }
1272
+ this.sortById();
1206
1273
  }
1207
1274
 
1208
1275
  }
@@ -1221,6 +1288,7 @@ export class BodyTempState extends EqState {
1221
1288
  public dataName = 'bodyTempState';
1222
1289
  public initData() {
1223
1290
  if (typeof this.data.heaterOptions === 'undefined') this.data.heaterOptions = { total: 0 };
1291
+ if (typeof this.data.isCovered === 'undefined') this.data.isCovered = false;
1224
1292
  }
1225
1293
  public get id(): number { return this.data.id; }
1226
1294
  public set id(val: number) { this.setDataVal('id', val); }
@@ -1259,6 +1327,8 @@ export class BodyTempState extends EqState {
1259
1327
  public set coolSetpoint(val: number) { this.setDataVal('coolSetpoint', val); }
1260
1328
  public get isOn(): boolean { return this.data.isOn; }
1261
1329
  public set isOn(val: boolean) { this.setDataVal('isOn', val); }
1330
+ public get isCovered(): boolean { return this.data.isCovered; }
1331
+ public set isCovered(val: boolean) { this.setDataVal('isCovered', val); }
1262
1332
  public emitData(name: string, data: any) { webApp.emitToClients('body', this.data); }
1263
1333
  // RKS: This is a very interesting object because we have a varied object. Type safety rules should not apply
1264
1334
  // here as the heater types are specific to the installed equipment. The reason is because it has no meaning without the body and the calculation of it should
@@ -1325,7 +1395,13 @@ export class HeaterStateCollection extends EqStateCollection<HeaterState> {
1325
1395
  public createItem(data: any): HeaterState { return new HeaterState(data); }
1326
1396
  public cleanupState() {
1327
1397
  for (let i = this.data.length - 1; i >= 0; i--) {
1328
- if (typeof sys.heaters.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1398
+ if (isNaN(this.data[i].id)) {
1399
+ logger.info(`Removed Invalid Heater ${this.data[i].id}-${this.data[i].name}`);
1400
+ this.data.splice(i, 1);
1401
+ }
1402
+ else {
1403
+ if (typeof sys.heaters.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1404
+ }
1329
1405
  }
1330
1406
  let cfg = sys.heaters.toArray();
1331
1407
  for (let i = 0; i < cfg.length; i++) {
@@ -1344,8 +1420,10 @@ export class HeaterState extends EqState {
1344
1420
  public set name(val: string) { this.setDataVal('name', val); }
1345
1421
  public get isOn(): boolean { return this.data.isOn; }
1346
1422
  public set isOn(val: boolean) { this.setDataVal('isOn', val); }
1347
- public get isVirtual(): boolean { return this.data.isVirtual; }
1348
- public set isVirtual(val: boolean) { this.setDataVal('isVirtual', val); }
1423
+ public get isCooling(): boolean { return this.data.isCooling; }
1424
+ public set isCooling(val: boolean) { this.setDataVal('isCooling', val); }
1425
+ //public get isVirtual(): boolean { return this.data.isVirtual; }
1426
+ //public set isVirtual(val: boolean) { this.setDataVal('isVirtual', val); }
1349
1427
  public get type(): number | any { return typeof this.data.type !== 'undefined' ? this.data.type.val : 0; }
1350
1428
  public set type(val: number | any) {
1351
1429
  if (this.type !== val) {
@@ -1367,7 +1445,10 @@ export class FeatureStateCollection extends EqStateCollection<FeatureState> {
1367
1445
  public async toggleFeatureStateAsync(id: number) { return sys.board.features.toggleFeatureStateAsync(id); }
1368
1446
  public cleanupState() {
1369
1447
  for (let i = this.data.length - 1; i >= 0; i--) {
1370
- if (typeof sys.features.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1448
+ if (isNaN(this.data[i].id)) this.data.splice(i, 1);
1449
+ else {
1450
+ if (typeof sys.features.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1451
+ }
1371
1452
  }
1372
1453
  let cfg = sys.features.toArray();
1373
1454
  for (let i = 0; i < cfg.length; i++) {
@@ -1383,6 +1464,9 @@ export class FeatureStateCollection extends EqStateCollection<FeatureState> {
1383
1464
 
1384
1465
  export class FeatureState extends EqState implements ICircuitState {
1385
1466
  public dataName: string = 'feature';
1467
+ public initData() {
1468
+ if (typeof this.data.freezeProtect === 'undefined') this.data.freezeProtect = false;
1469
+ }
1386
1470
  public get id(): number { return this.data.id; }
1387
1471
  public set id(val: number) { this.data.id = val; }
1388
1472
  public get name(): string { return this.data.name; }
@@ -1404,7 +1488,13 @@ export class FeatureState extends EqState implements ICircuitState {
1404
1488
  if (typeof this.data.endTime === 'undefined') return undefined;
1405
1489
  return new Timestamp(this.data.endTime);
1406
1490
  }
1407
- public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate()), false) : this.setDataVal('endTime', undefined); }
1491
+ public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
1492
+ // This property will be set if the system has turn this feature on for freeze protection reasons. We have no way of knowing when Pentair does this but
1493
+ // need to know (so we can shut it off) if we have done this.
1494
+ public get freezeProtect(): boolean { return this.data.freezeProtect; }
1495
+ public set freezeProtect(val: boolean) { this.setDataVal('freezeProtect', val); }
1496
+ public get isActive(): boolean { return this.data.isActive; }
1497
+ public set isActive(val: boolean) { this.setDataVal('isActive', val); }
1408
1498
  }
1409
1499
  export class VirtualCircuitState extends EqState implements ICircuitState {
1410
1500
  public dataName: string = 'virtualCircuit';
@@ -1427,7 +1517,7 @@ export class VirtualCircuitState extends EqState implements ICircuitState {
1427
1517
  if (typeof this.data.endTime === 'undefined') return undefined;
1428
1518
  return new Timestamp(this.data.endTime);
1429
1519
  }
1430
- public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate()), false) : this.setDataVal('endTime', undefined); }
1520
+ public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
1431
1521
  }
1432
1522
  export class VirtualCircuitStateCollection extends EqStateCollection<VirtualCircuitState> {
1433
1523
  public createItem(data: any): VirtualCircuitState { return new VirtualCircuitState(data); }
@@ -1452,7 +1542,10 @@ export class CircuitStateCollection extends EqStateCollection<CircuitState> {
1452
1542
  }
1453
1543
  public cleanupState() {
1454
1544
  for (let i = this.data.length - 1; i >= 0; i--) {
1455
- if (typeof sys.circuits.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1545
+ if (isNaN(this.data[i].id)) this.data.splice(i, 1);
1546
+ else {
1547
+ if (typeof sys.circuits.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1548
+ }
1456
1549
  }
1457
1550
  let cfg = sys.circuits.toArray();
1458
1551
  for (let i = 0; i < cfg.length; i++) {
@@ -1468,6 +1561,9 @@ export class CircuitStateCollection extends EqStateCollection<CircuitState> {
1468
1561
  }
1469
1562
  export class CircuitState extends EqState implements ICircuitState {
1470
1563
  public dataName = 'circuit';
1564
+ public initData() {
1565
+ if (typeof this.data.freezeProtect === 'undefined') this.data.freezeProtect = false;
1566
+ }
1471
1567
  public get id(): number { return this.data.id; }
1472
1568
  public set id(val: number) { this.data.id = val; }
1473
1569
  public get name(): string { return this.data.name; }
@@ -1485,10 +1581,12 @@ export class CircuitState extends EqState implements ICircuitState {
1485
1581
  this.hasChanged = true;
1486
1582
  }
1487
1583
  }
1488
- public get lightingTheme(): number { return typeof (this.data.lightingTheme) !== 'undefined' ? this.data.lightingTheme.val : 255; }
1584
+ public get lightingTheme(): number { return typeof this.data.lightingTheme !== 'undefined' ? this.data.lightingTheme.val : 255; }
1489
1585
  public set lightingTheme(val: number) {
1490
1586
  if (this.lightingTheme !== val) {
1491
- this.data.lightingTheme = sys.board.valueMaps.lightThemes.transform(val);
1587
+ // Force this to undefined when we are a circuit without a theme.
1588
+ if (typeof val === 'undefined') this.data.lightingTheme = undefined;
1589
+ else this.data.lightingTheme = sys.board.valueMaps.lightThemes.transform(val);
1492
1590
  this.hasChanged = true;
1493
1591
  }
1494
1592
  }
@@ -1505,13 +1603,22 @@ export class CircuitState extends EqState implements ICircuitState {
1505
1603
  if (typeof this.data.endTime === 'undefined') return undefined;
1506
1604
  return new Timestamp(this.data.endTime);
1507
1605
  }
1508
- public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate()), false) : this.setDataVal('endTime', undefined); }
1606
+ public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
1607
+ // This property will be set if the system has turn this circuit on for freeze protection reasons. We have no way of knowing when Pentair does this but
1608
+ // need to know (so we can shut it off) if we have done this.
1609
+ public get freezeProtect(): boolean { return this.data.freezeProtect; }
1610
+ public set freezeProtect(val: boolean) { this.setDataVal('freezeProtect', val); }
1611
+ public get isActive(): boolean { return this.data.isActive; }
1612
+ public set isActive(val: boolean) { this.setDataVal('isActive', val); }
1509
1613
  }
1510
1614
  export class ValveStateCollection extends EqStateCollection<ValveState> {
1511
1615
  public createItem(data: any): ValveState { return new ValveState(data); }
1512
1616
  public cleanupState() {
1513
1617
  for (let i = this.data.length - 1; i >= 0; i--) {
1514
- if (typeof sys.valves.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1618
+ if (isNaN(this.data[i].id)) this.data.splice(i, 1);
1619
+ else {
1620
+ if (typeof sys.valves.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1621
+ }
1515
1622
  }
1516
1623
  let cfg = sys.valves.toArray();
1517
1624
  for (let i = 0; i < cfg.length; i++) {
@@ -1550,7 +1657,7 @@ export class ValveState extends EqState {
1550
1657
  if (valve.circuit !== 256) vstate.circuit = state.circuits.getInterfaceById(valve.circuit).get(true);
1551
1658
  vstate.isIntake = utils.makeBool(valve.isIntake);
1552
1659
  vstate.isReturn = utils.makeBool(valve.isReturn);
1553
- vstate.isVirtual = utils.makeBool(valve.isVirtual);
1660
+ // vstate.isVirtual = utils.makeBool(valve.isVirtual);
1554
1661
  vstate.isActive = utils.makeBool(valve.isActive);
1555
1662
  vstate.pinId = valve.pinId;
1556
1663
  return vstate;
@@ -1568,12 +1675,15 @@ export class CoverStateCollection extends EqStateCollection<CoverState> {
1568
1675
  }
1569
1676
  export class CoverState extends EqState {
1570
1677
  public dataName: string = 'cover';
1678
+ public initData() {
1679
+ if (typeof this.data.isClosed === 'undefined') this.data.isClosed = false;
1680
+ }
1571
1681
  public get id(): number { return this.data.id; }
1572
1682
  public set id(val: number) { this.data.id = val; }
1573
1683
  public get name(): string { return this.data.name; }
1574
1684
  public set name(val: string) { this.setDataVal('name', val); }
1575
- public get isOpen(): boolean { return this.data.isOpen; }
1576
- public set isOpen(val: boolean) { this.setDataVal('isOpen', val); }
1685
+ public get isClosed(): boolean { return this.data.isClosed; }
1686
+ public set isClosed(val: boolean) { this.setDataVal('isClosed', val); }
1577
1687
  }
1578
1688
  export class ChlorinatorStateCollection extends EqStateCollection<ChlorinatorState> {
1579
1689
  public createItem(data: any): ChlorinatorState { return new ChlorinatorState(data); }
@@ -1581,7 +1691,10 @@ export class ChlorinatorStateCollection extends EqStateCollection<ChlorinatorSta
1581
1691
  public lastDispatchSuperChlor: number = 0;
1582
1692
  public cleanupState() {
1583
1693
  for (let i = this.data.length - 1; i >= 0; i--) {
1584
- if (typeof sys.chlorinators.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1694
+ if (isNaN(this.data[i].id)) this.data.splice(i, 1);
1695
+ else {
1696
+ if (typeof sys.chlorinators.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1697
+ }
1585
1698
  }
1586
1699
  let cfg = sys.chlorinators.toArray();
1587
1700
  for (let i = 0; i < cfg.length; i++) {
@@ -1634,16 +1747,6 @@ export class ChlorinatorState extends EqState {
1634
1747
  this.hasChanged = true;
1635
1748
  }
1636
1749
  }
1637
- //public get virtualControllerStatus(): number {
1638
- // return typeof (this.data.virtualControllerStatus) !== 'undefined' ? this.data.virtualControllerStatus.val : -1;
1639
- //}
1640
- //public set virtualControllerStatus(val: number) {
1641
- // if (this.virtualControllerStatus !== val) {
1642
- // this.data.virtualControllerStatus = sys.board.valueMaps.virtualControllerStatus.transform(val);
1643
- // this.hasChanged = true;
1644
-
1645
- // }
1646
- //}
1647
1750
  public get type(): number { return typeof (this.data.type) !== 'undefined' ? this.data.type.val : -1; }
1648
1751
  public set type(val: number) {
1649
1752
  if (this.type !== val) {
@@ -1730,12 +1833,21 @@ export class ChlorinatorState extends EqState {
1730
1833
  else
1731
1834
  this.setDataVal('superChlor', false);
1732
1835
  }
1836
+ public getExtended(): any {
1837
+ let schlor = this.get(true);
1838
+ let chlor = sys.chlorinators.getItemById(this.id, false);
1839
+ schlor.lockSetpoints = chlor.disabled || chlor.isDosing;
1840
+ return schlor;
1841
+ }
1733
1842
  }
1734
1843
  export class ChemControllerStateCollection extends EqStateCollection<ChemControllerState> {
1735
1844
  public createItem(data: any): ChemControllerState { return new ChemControllerState(data); }
1736
1845
  public cleanupState() {
1737
1846
  for (let i = this.data.length - 1; i >= 0; i--) {
1738
- if (typeof sys.chemControllers.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1847
+ if (isNaN(this.data[i].id)) this.data.splice(i, 1);
1848
+ else {
1849
+ if (typeof sys.chemControllers.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
1850
+ }
1739
1851
  }
1740
1852
  // Make sure we have at least the items that exist in the config.
1741
1853
  let cfg = sys.chemControllers.toArray();
@@ -1775,7 +1887,7 @@ export class ChemControllerState extends EqState {
1775
1887
  //var chemControllerState = {
1776
1888
  // lastComm: 'number', // The unix time the chem controller sent its status.
1777
1889
  // id: 'number', // Id of the chemController.
1778
- // type: 'valueMap', // intellichem, homegrown, rem.
1890
+ // type: 'valueMap', // intellichem, rem.
1779
1891
  // address: 'number', // Assigned address if IntelliChem.
1780
1892
  // name: 'string', // Name assigned to the controller.
1781
1893
  // status: 'valueMap', // ok, nocomms, setupError
@@ -1998,21 +2110,22 @@ export class ChemControllerState extends EqState {
1998
2110
  export class ChemicalState extends ChildEqState {
1999
2111
  public initData() {
2000
2112
  if (typeof this.data.probe === 'undefined') this.data.probe = {};
2001
- if (typeof this.data.tank == 'undefined') this.data.tank = { capacity: 0, level: 0, units: 0 };
2113
+ if (typeof this.data.tank === 'undefined') this.data.tank = { capacity: 0, level: 0, units: 0 };
2114
+ if (typeof this.data.pump === 'undefined') this.data.pump = { isDosing: false };
2002
2115
  if (typeof this.data.dosingTimeRemaining === 'undefined') this.data.dosingTimeRemaining = 0;
2003
2116
  if (typeof this.data.delayTimeRemaining === 'undefined') this.data.delayTimeRemaining = 0;
2004
2117
  if (typeof this.data.dosingVolumeRemaining === 'undefined') this.data.dosingVolumeRemaining = 0;
2005
2118
  if (typeof this.data.doseVolume === 'undefined') this.data.doseVolume = 0;
2006
2119
  if (typeof this.data.doseTime === 'undefined') this.data.doseTime = 0;
2007
2120
  if (typeof this.data.lockout === 'undefined') this.data.lockout = false;
2008
- if (typeof this.data.level == 'undefined') this.data.level = 0;
2121
+ if (typeof this.data.level === 'undefined') this.data.level = 0;
2009
2122
  if (typeof this.data.mixTimeRemaining === 'undefined') this.data.mixTimeRemaining = 0;
2010
2123
  if (typeof this.data.dailyLimitReached === 'undefined') this.data.dailyLimitReached = false;
2011
2124
  if (typeof this.data.manualDosing === 'undefined') this.data.manualDosing = false;
2125
+ if (typeof this.data.manualMixing === 'undefined') this.data.manualMixing = false;
2012
2126
  if (typeof this.data.flowDelay === 'undefined') this.data.flowDelay = false;
2013
2127
  if (typeof this.data.dosingStatus === 'undefined') this.dosingStatus = 2;
2014
2128
  if (typeof this.data.enabled === 'undefined') this.data.enabled = true;
2015
- if (typeof this.data.level === 'undefined') this.data.level = 0;
2016
2129
  }
2017
2130
  public getConfig(): Chemical { return; }
2018
2131
  public calcDoseHistory(): number {
@@ -2073,7 +2186,7 @@ export class ChemicalState extends ChildEqState {
2073
2186
  this.doseHistory.unshift(dose);
2074
2187
  this.dailyVolumeDosed = this.calcDoseHistory();
2075
2188
  DataLogger.writeEnd(`chemDosage_${this.chemType}.log`, dose);
2076
- webApp.emitToClients(`chemicalDose`, dose);
2189
+ setImmediate(() => { webApp.emitToClients(`chemicalDose`, dose); });
2077
2190
  }
2078
2191
  this.currentDose = undefined;
2079
2192
  }
@@ -2088,7 +2201,7 @@ export class ChemicalState extends ChildEqState {
2088
2201
  this.timeDosed = Math.round(dose._timeDosed / 1000);
2089
2202
  this.dosingTimeRemaining = dose.timeRemaining;
2090
2203
  this.dosingVolumeRemaining = dose.volumeRemaining;
2091
- if (dose.volumeDosed > 0) webApp.emitToClients(`chemicalDose`, dose);
2204
+ if (dose.volumeDosed > 0) setImmediate(() => { webApp.emitToClients(`chemicalDose`, dose); });
2092
2205
  return dose;
2093
2206
  }
2094
2207
  public get currentDose(): ChemicalDoseState {
@@ -2112,6 +2225,11 @@ export class ChemicalState extends ChildEqState {
2112
2225
  return this.data.doseHistory;
2113
2226
  }
2114
2227
  public set doseHistory(val: ChemicalDoseState[]) { this.setDataVal('doseHistory', val); }
2228
+ public appendDemand(time: number, val: number) {
2229
+ let dH = this.demandHistory;
2230
+ dH.appendDemand(time, val);
2231
+ }
2232
+ public get demandHistory() { return new ChemicalDemandState(this.data, 'demandHistory', this) };
2115
2233
  public get enabled(): boolean { return this.data.enabled; }
2116
2234
  public set enabled(val: boolean) { this.data.enabled = val; }
2117
2235
  public get level(): number { return this.data.level; }
@@ -2143,6 +2261,7 @@ export class ChemicalState extends ChildEqState {
2143
2261
  public get dosingStatus(): number { return typeof (this.data.dosingStatus) !== 'undefined' ? this.data.dosingStatus.val : undefined; }
2144
2262
  public set dosingStatus(val: number) {
2145
2263
  if (this.dosingStatus !== val) {
2264
+ logger.debug(`${this.chemType} dosing status changed from ${sys.board.valueMaps.chemControllerDosingStatus.getName(this.dosingStatus)} (${this.dosingStatus}) to ${sys.board.valueMaps.chemControllerDosingStatus.getName(val)}(${val})`);
2146
2265
  this.data.dosingStatus = sys.board.valueMaps.chemControllerDosingStatus.transform(val);
2147
2266
  this.hasChanged = true;
2148
2267
  }
@@ -2153,10 +2272,13 @@ export class ChemicalState extends ChildEqState {
2153
2272
  public set flowDelay(val: boolean) { this.data.flowDelay = val; }
2154
2273
  public get manualDosing(): boolean { return utils.makeBool(this.data.manualDosing); }
2155
2274
  public set manualDosing(val: boolean) { this.setDataVal('manualDosing', val); }
2275
+ public get manualMixing(): boolean { return utils.makeBool(this.data.manualMixing); }
2276
+ public set manualMixing(val: boolean) { this.setDataVal('manualMixing', val); }
2156
2277
  public get dailyLimitReached(): boolean { return utils.makeBool(this.data.dailyLimitReached); }
2157
2278
  public set dailyLimitReached(val: boolean) { this.data.dailyLimitReached = val; }
2158
2279
  public get tank(): ChemicalTankState { return new ChemicalTankState(this.data, 'tank', this); }
2159
2280
  public get pump(): ChemicalPumpState { return new ChemicalPumpState(this.data, 'pump', this); }
2281
+ public get chlor(): ChemicalChlorState { return new ChemicalChlorState(this.data, 'chlor', this); }
2160
2282
  public calcDemand(chem?: ChemController): number { return 0; }
2161
2283
  public getExtended() {
2162
2284
  let chem = this.get(true);
@@ -2167,7 +2289,7 @@ export class ChemicalState extends ChildEqState {
2167
2289
  }
2168
2290
  export class ChemicalPhState extends ChemicalState {
2169
2291
  public initData() {
2170
- if (typeof this.data.chemType === 'undefined') this.data.chemType === 'acid';
2292
+ // if (typeof this.data.chemType === 'undefined') this.data.chemType === 'acid'; // RSG 10-23-21 - Only a getter; don't need to set this.
2171
2293
  super.initData();
2172
2294
  }
2173
2295
  public getConfig() {
@@ -2200,7 +2322,7 @@ export class ChemicalPhState extends ChemicalState {
2200
2322
  if (chem.body === 1 || chem.body === 32 || sys.equipment.shared) totalGallons += sys.bodies.getItemById(2).capacity;
2201
2323
  if (chem.body === 2) totalGallons += sys.bodies.getItemById(3).capacity;
2202
2324
  if (chem.body === 3) totalGallons += sys.bodies.getItemById(4).capacity;
2203
- logger.verbose(`Chem begin calculating demand: ${this.level} setpoint: ${this.setpoint} body: ${totalGallons}`);
2325
+ logger.verbose(`Chem begin calculating ${this.chemType} demand: ${this.level} setpoint: ${this.setpoint} body: ${totalGallons}`);
2204
2326
  let chg = this.setpoint - this.level;
2205
2327
  let delta = chg * totalGallons;
2206
2328
  let temp = (this.level + this.setpoint) / 2;
@@ -2227,6 +2349,7 @@ export class ChemicalORPState extends ChemicalState {
2227
2349
  public initData() {
2228
2350
  if (typeof this.data.probe === 'undefined') this.data.probe = {};
2229
2351
  if (typeof this.data.chemType === 'undefined') this.data.chemType === 'orp';
2352
+ if (typeof this.data.useChlorinator === 'undefined') this.data.useChlorinator = false;
2230
2353
  super.initData();
2231
2354
  // Load up the 24 hours doseHistory.
2232
2355
  //this.doseHistory = DataLogger.readFromEnd(`chemDosage_${this.chemType}.log`, ChemicalDoseState, (lineNumber: number, entry: ChemicalDoseState): boolean => {
@@ -2239,6 +2362,8 @@ export class ChemicalORPState extends ChemicalState {
2239
2362
  }
2240
2363
  public get chemType() { return 'orp'; }
2241
2364
  public get probe() { return new ChemicalProbeORPState(this.data, 'probe', this); }
2365
+ public get useChlorinator(): boolean { return utils.makeBool(this.data.useChlorinator); }
2366
+ public set useChlorinator(val: boolean) { this.setDataVal('useChlorinator', val); }
2242
2367
  public get suspendDosing(): boolean {
2243
2368
  let cc = this.chemController;
2244
2369
  return cc.alarms.comms !== 0 || cc.alarms.orpProbeFault !== 0 || cc.alarms.orpPumpFault !== 0 || cc.alarms.bodyFault !== 0;
@@ -2280,6 +2405,22 @@ export class ChemicalPumpState extends ChildEqState {
2280
2405
  return pump;
2281
2406
  }
2282
2407
  }
2408
+ export class ChemicalChlorState extends ChildEqState {
2409
+ public initData() {
2410
+ if (typeof this.data.isDosing === 'undefined') this.data.isDosing = false;
2411
+ }
2412
+ public get chemical(): ChemicalState { return this.getParent() as ChemicalState; }
2413
+ public get chemController(): ChemControllerState {
2414
+ let p = this.chemical;
2415
+ return typeof p !== 'undefined' ? p.getParent() as ChemControllerState : undefined;
2416
+ }
2417
+ public get isDosing(): boolean { return utils.makeBool(this.data.isDosing); }
2418
+ public set isDosing(val: boolean) { this.setDataVal('isDosing', val); }
2419
+ public getExtended() {
2420
+ let chlor = this.get(true);
2421
+ return chlor;
2422
+ }
2423
+ }
2283
2424
  export class ChemicalProbeState extends ChildEqState {
2284
2425
  public initData() {
2285
2426
  if (typeof this.data.level === 'undefined') this.data.level = null;
@@ -2353,14 +2494,16 @@ export class ChemicalDoseState extends DataLoggerEntry {
2353
2494
  public _isManual: boolean;
2354
2495
 
2355
2496
  constructor(entry?: string | object) {
2356
- super(entry);
2497
+ super();
2498
+ if (typeof entry === 'object') entry = JSON.stringify(entry);
2499
+ if (typeof entry === 'string') this.parse(entry);
2357
2500
  // Javascript is idiotic in that the initialization of variables
2358
2501
  // do not happen before the assignment so some of the values can be undefined.
2359
2502
  if (typeof this.volumeDosed === 'undefined' || !this.volumeDosed) this.volumeDosed = 0;
2360
2503
  if (typeof this.volume === 'undefined' || !this.volume) this.volume = 0;
2361
2504
  if (typeof this._isManual === 'undefined') this._isManual = this.method === 'manual';
2362
2505
  if (typeof this.timeDosed === 'undefined' || !this.timeDosed) this.timeDosed = 0;
2363
- if (typeof this._timeDosed === 'undefined') this.timeDosed * 1000;
2506
+ if (typeof this._timeDosed === 'undefined') this._timeDosed = this.timeDosed * 1000;
2364
2507
  if (typeof this.time === 'undefined' || !this.time) this.time = 0;
2365
2508
  }
2366
2509
  public id: number;
@@ -2377,12 +2520,65 @@ export class ChemicalDoseState extends DataLoggerEntry {
2377
2520
  public time: number;
2378
2521
  public timeDosed: number;
2379
2522
 
2380
- public createInstance(entry?: string): ChemicalDoseState { return new ChemicalDoseState(entry); }
2523
+ public static createInstance(entry?: string): ChemicalDoseState { return new ChemicalDoseState(entry); }
2381
2524
  public save() { DataLogger.writeEnd(`chemDosage_${this.chem}.log`, this); }
2382
2525
  public get timeRemaining(): number { return Math.floor(Math.max(0, this.time - (this._timeDosed / 1000))); }
2383
2526
  public get volumeRemaining(): number { return Math.max(0, this.volume - this.volumeDosed); }
2527
+ public parse(entry: string) {
2528
+ // let obj = typeof entry !== 'undefined' ? JSON.parse(entry, this.dateParser) : {};
2529
+ let obj = typeof entry !== 'undefined' ? JSON.parse(entry) : {};
2530
+ for (const prop in obj) {obj[prop] = this.dateParser(prop, obj[prop])}
2531
+ if (typeof obj.setpoint !== 'undefined') this.setpoint = obj.setpoint;
2532
+ if (typeof obj.method !== 'undefined') this.method = obj.method;
2533
+ if (typeof obj.start !== 'undefined') this.start = obj.start;
2534
+ if (typeof obj.end !== 'undefined') this.end = obj.end;
2535
+ if (typeof obj.chem !== 'undefined') this.chem = obj.chem;
2536
+ if (typeof obj.demand !== 'undefined') this.demand = obj.demand;
2537
+ if (typeof obj.id !== 'undefined') this.id = obj.id;
2538
+ if (typeof obj.level !== 'undefined') this.level = obj.level;
2539
+ if (typeof obj.volume !== 'undefined') this.volume = obj.volume;
2540
+ if (typeof obj.status !== 'undefined') this.status = obj.status;
2541
+ if (typeof obj.volumeDosed !== 'undefined') this.volumeDosed = obj.volumeDosed;
2542
+ if (typeof obj.time !== 'undefined') this.time = obj.time;
2543
+ if (typeof obj.timeDosed !== 'undefined') this.timeDosed = obj.timeDosed;
2544
+ // this.setProperties(obj);
2545
+ }
2546
+ protected setProperties(data: any) {
2547
+ let op = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
2548
+ for (let i in op) {
2549
+ let prop = op[i];
2550
+ if (typeof this[prop] === 'function') continue;
2551
+ if (typeof data[prop] !== 'undefined') {
2552
+ if (typeof this[prop] === null || typeof data[prop] === null) continue;
2553
+ this[prop] = data[prop];
2554
+ }
2555
+ }
2556
+ }
2384
2557
  }
2385
2558
 
2559
+ export class ChemicalDemandState extends ChildEqState {
2560
+ public initData() {
2561
+ if (typeof this.data.time === 'undefined') this.data.time = [];
2562
+ if (typeof this.data.value === 'undefined') this.data.value = [];
2563
+ }
2564
+
2565
+ public appendDemand(time: number, val: number) {
2566
+ while (this.data.time.length > 99) {
2567
+ this.data.time.pop();
2568
+ this.data.value.pop();
2569
+ }
2570
+ this.data.time.unshift(Math.round(time / 1000));
2571
+ this.data.value.unshift(val);
2572
+ // calculate the slope with each save
2573
+ let slope = utils.slopeOfLeastSquares(this.data.time, this.data.value);
2574
+ this.setDataVal('slope', slope); // will act as hasChanged=true;
2575
+ }
2576
+ public get demandHistory(): {} { return [this.data.time, this.data.value]; }
2577
+ public get times(): number[] { return this.data.time; }
2578
+ public get values(): number[] { return this.data.value; }
2579
+ public set slope(val: number) { this.setDataVal('slope', val); }
2580
+ public get slope():number { return this.data.slope; }
2581
+ }
2386
2582
 
2387
2583
  export class ChemControllerStateWarnings extends ChildEqState {
2388
2584
  ///ctor(data): ChemControllerStateWarnings { return new ChemControllerStateWarnings(data, name || 'warnings'); }
@@ -2605,15 +2801,35 @@ export class FilterState extends EqState {
2605
2801
  this.hasChanged = true;
2606
2802
  }
2607
2803
  }
2608
- public get psi(): number { return this.data.psi; }
2609
- public set psi(val: number) { this.setDataVal('psi', val); }
2610
- public get filterPsi(): number { return this.data.filterPsi; } // do not exceed value.
2611
- public set filterPsi(val: number) { this.setDataVal('filterPsi', val); }
2804
+ public get pressureUnits(): number { return this.data.pressureUnits; }
2805
+ public set pressureUnits(val: number) {
2806
+ if (this.pressureUnits !== val) {
2807
+ this.setDataVal('pressureUnits', sys.board.valueMaps.pressureUnits.transform(val));
2808
+ }
2809
+ }
2810
+ public get pressure(): number { return this.data.pressure; }
2811
+ public set pressure(val: number) { this.setDataVal('pressure', val); }
2812
+ public get refPressure(): number { return this.data.refPressure; }
2813
+ public set refPressure(val: number) {
2814
+ if (val !== this.refPressure) {
2815
+ this.setDataVal('refPressure', val);
2816
+ this.calcCleanPercentage();
2817
+ }
2818
+ else { this.setDataVal('refPressure', val); }
2819
+ }
2820
+ public get cleanPercentage(): number { return this.data.cleanPercentage; }
2821
+ public set cleanPercentage(val: number) { this.setDataVal('cleanPercentage', val); }
2612
2822
  public get lastCleanDate(): Timestamp { return this.data.lastCleanDate; }
2613
2823
  public set lastCleanDate(val: Timestamp) { this.setDataVal('lastCleanDate', val); }
2614
- public get needsCleaning(): number { return this.data.needsCleaning; }
2615
- public set needsCleaning(val: number) { this.setDataVal('needsCleaning', val); }
2616
2824
  public get isOn(): boolean { return utils.makeBool(this.data.isOn); }
2617
2825
  public set isOn(val: boolean) { this.setDataVal('isOn', val); }
2826
+ public calcCleanPercentage() {
2827
+ if (typeof this.refPressure === 'undefined') return;
2828
+ let filter = sys.filters.find(elem => elem.id == this.id);
2829
+ // 8 to 10
2830
+ let cp = filter.cleanPressure || 0;
2831
+ let dp = filter.dirtyPressure || 1;
2832
+ this.cleanPercentage = (cp - dp != 0) ? Math.round(Math.max(0, (1 - (this.refPressure - cp) / (dp - cp)) * 100) * 100)/100 : 0;
2833
+ }
2618
2834
  }
2619
2835
  export var state = new State();