nodejs-poolcontroller 7.5.1 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Changelog CHANGED
@@ -1,4 +1,18 @@
1
1
  # Changelog
2
+ ## 7.6
3
+ 1. MasterTemp RS485 support for Nixie and IntelliCenter
4
+ 2. Nixie Valve Rotation delay
5
+ 3. Nixie Heater Cooldown delay
6
+ 4. Nixie Cleaner Start delay
7
+ 5. Nixie Cleaner Shutdown on Solar
8
+ 6. Nixie Delay Cancel
9
+
10
+ ## 7.5.1
11
+ 1. Backup/restore fixes
12
+ 2. Egg timer expiration
13
+ 3. Bug Fixes
14
+ 4. dashPanel/messageManager Filter
15
+ 5. RS485 refactor
2
16
 
3
17
  ## 7.5
4
18
  1. Backup/restore
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # nodejs-poolController - Version 7.5
1
+ # nodejs-poolController - Version 7.6
2
2
 
3
3
  ## What is nodejs-poolController
4
4
 
@@ -724,6 +724,16 @@ export class Options extends EqItem {
724
724
  if (typeof this.data.clockMode === 'undefined') this.data.clockMode = 12;
725
725
  if (typeof this.data.adjustDST === 'undefined') this.data.adjustDST = true;
726
726
  if (typeof this.data.freezeThreshold === 'undefined') this.data.freezeThreshold = 35;
727
+ if (typeof this.data.pumpDelay === 'undefined') this.data.pumpDelay = false;
728
+ if (typeof this.data.valveDelayTime === 'undefined') this.data.valveDelayTime = 30;
729
+ // RKS: 12-04-21 If you are reading this in a few months delete the line below.
730
+ if (this.data.valveDelayTime > 1000) this.data.valveDelayTime = this.data.valveDelayTime / 1000;
731
+ if (typeof this.data.heaterStartDelay === 'undefined') this.data.heaterStartDelay = true;
732
+ if (typeof this.data.cleanerStartDelay === 'undefined') this.data.cleanerStartDelay = true;
733
+ if (typeof this.data.cleanerSolarDelay === 'undefined') this.data.cleanerSolarDelay = true;
734
+ if (typeof this.data.heaterStartDelayTime === 'undefined') this.data.heaterStartDelayTime = 10;
735
+ if (typeof this.data.cleanerStartDelayTime === 'undefined') this.data.cleanerStartDelayTime = 300; // 5min
736
+ if (typeof this.data.cleanerSolarDelayTime === 'undefined') this.data.cleanerSolarDelayTime = 300; // 5min
727
737
  }
728
738
  public get clockMode(): number | any { return this.data.clockMode; }
729
739
  public set clockMode(val: number | any) { this.setDataVal('clockMode', sys.board.valueMaps.clockModes.encode(val)); }
@@ -740,10 +750,25 @@ export class Options extends EqItem {
740
750
  public set manualHeat(val: boolean) { this.setDataVal('manualHeat', val); }
741
751
  public get pumpDelay(): boolean { return this.data.pumpDelay; }
742
752
  public set pumpDelay(val: boolean) { this.setDataVal('pumpDelay', val); }
753
+ public get valveDelayTime(): number { return this.data.valveDelayTime; }
754
+ public set valveDelayTime(val: number) { this.setDataVal('valveDelayTime', val); }
743
755
  public get cooldownDelay(): boolean { return this.data.cooldownDelay; }
744
756
  public set cooldownDelay(val: boolean) { this.setDataVal('cooldownDelay', val); }
745
757
  public get freezeThreshold(): number { return this.data.freezeThreshold; }
746
758
  public set freezeThreshold(val: number) { this.setDataVal('freezeThreshold', val); }
759
+ public get heaterStartDelay(): boolean { return this.data.heaterStartDelay; }
760
+ public set heaterStartDelay(val: boolean) { this.setDataVal('heaterStartDelay', val); }
761
+ public get heaterStartDelayTime(): number { return this.data.heaterStartDelayTime; }
762
+ public set heaterStartDelayTime(val: number) { this.setDataVal('heaterStartDelayTime', val); }
763
+
764
+ public get cleanerStartDelay(): boolean { return this.data.cleanerStartDelay; }
765
+ public set cleanerStartDelay(val: boolean) { this.setDataVal('cleanerStartDelay', val); }
766
+ public get cleanerStartDelayTime(): number { return this.data.cleanerStartDelayTime; }
767
+ public set cleanerStartDelayTime(val: number) { this.setDataVal('cleanerStartDelayTime', val); }
768
+ public get cleanerSolarDelay(): boolean { return this.data.cleanerSolarDelay; }
769
+ public set cleanerSolarDelay(val: boolean) { this.setDataVal('cleanerSolarDelay', val); }
770
+ public get cleanerSolarDelayTime(): number { return this.data.cleanerSolarDelayTime; }
771
+ public set cleanerSolarDelayTime(val: number) { this.setDataVal('cleanerSolarDelayTime', val); }
747
772
 
748
773
  //public get airTempAdj(): number { return typeof this.data.airTempAdj === 'undefined' ? 0 : this.data.airTempAdj; }
749
774
  //public set airTempAdj(val: number) { this.setDataVal('airTempAdj', val); }
@@ -1129,6 +1154,9 @@ export class EggTimer extends EqItem {
1129
1154
  }
1130
1155
  export class CircuitCollection extends EqItemCollection<Circuit> {
1131
1156
  constructor(data: any, name?: string) { super(data, name || "circuits"); }
1157
+ public filter(f: (value: Circuit, index?: any, array?: any[]) => boolean): CircuitCollection {
1158
+ return new CircuitCollection({ circuits: this.data.filter(f) });
1159
+ }
1132
1160
  public createItem(data: any): Circuit { return new Circuit(data); }
1133
1161
  public add(obj: any): Circuit {
1134
1162
  this.data.push(obj);
@@ -1185,7 +1213,22 @@ export class Circuit extends EqItem implements ICircuit {
1185
1213
  public get deviceBinding(): string { return this.data.deviceBinding; }
1186
1214
  public set deviceBinding(val: string) { this.setDataVal('deviceBinding', val); }
1187
1215
  public get hasHeatSource() { return typeof sys.board.valueMaps.circuitFunctions.get(this.type || 0).hasHeatSource !== 'undefined' ? sys.board.valueMaps.circuitFunctions.get(this.type || 0).hasHeatSource : false };
1188
- public getLightThemes() { return sys.board.circuits.getLightThemes(this.type); }
1216
+ public getLightThemes() {
1217
+ // Lets do this universally driven by the metadata.
1218
+ let cf = sys.board.valueMaps.circuitFunctions.transform(this.type);
1219
+ if (cf.isLight && typeof cf.theme !== 'undefined') {
1220
+ let arrThemes = sys.board.valueMaps.lightThemes.toArray();
1221
+ let themes = [];
1222
+ for (let i = 0; i < arrThemes.length; i++) {
1223
+ let thm = arrThemes[i];
1224
+ if (typeof thm.types !== 'undefined' && thm.types.length > 0 && thm.types.includes(cf.theme)) themes.push(thm);
1225
+ }
1226
+ return themes;
1227
+ }
1228
+ else return [];
1229
+
1230
+ //return sys.board.circuits.getLightThemes(this.type);
1231
+ }
1189
1232
  public static getIdName(id: number) {
1190
1233
  // todo: adjust for intellitouch
1191
1234
  let defName = "Aux" + (id + 1).toString();
@@ -1197,6 +1240,9 @@ export class Circuit extends EqItem implements ICircuit {
1197
1240
  }
1198
1241
  export class FeatureCollection extends EqItemCollection<Feature> {
1199
1242
  constructor(data: any, name?: string) { super(data, name || "features"); }
1243
+ public filter(f: (value: Circuit, index?: any, array?: any[]) => boolean): FeatureCollection {
1244
+ return new FeatureCollection({ features: this.data.filter(f) });
1245
+ }
1200
1246
  public createItem(data: any): Feature { return new Feature(data); }
1201
1247
  }
1202
1248
  export class Feature extends EqItem implements ICircuit {
@@ -1206,6 +1252,7 @@ export class Feature extends EqItem implements ICircuit {
1206
1252
  if (typeof this.data.isActive === 'undefined') this.data.isActive = true;
1207
1253
  if (typeof this.data.eggTimer === 'undefined') this.data.eggTimer = 720;
1208
1254
  if (typeof this.data.showInFeatures === 'undefined') this.data.showInFeatures = true;
1255
+ if (typeof this.data.master === 'undefined') this.data.master = sys.board.equipmentMaster;
1209
1256
  }
1210
1257
  public dataName = 'featureConfig';
1211
1258
  public get id(): number { return this.data.id; }
@@ -1454,6 +1501,22 @@ export class Chlorinator extends EqItem {
1454
1501
  export class ValveCollection extends EqItemCollection<Valve> {
1455
1502
  constructor(data: any, name?: string) { super(data, name || "valves"); }
1456
1503
  public createItem(data: any): Valve { return new Valve(data); }
1504
+ public getIntake(): Valve[] {
1505
+ let valves = this.data.filter(x => x.isIntake === true);
1506
+ let ret = [];
1507
+ for (let i = 0; i < valves.length; i++) {
1508
+ ret.push(this.getItemById(valves[i].id));
1509
+ }
1510
+ return ret;
1511
+ }
1512
+ public getReturn(): Valve[] {
1513
+ let valves = this.data.filter(x => x.isReturn === true);
1514
+ let ret = [];
1515
+ for (let i = 0; i < valves.length; i++) {
1516
+ ret.push(this.getItemById(valves[i].id));
1517
+ }
1518
+ return ret;
1519
+ }
1457
1520
  }
1458
1521
  export class Valve extends EqItem {
1459
1522
  public dataName = 'valveConfig';
@@ -1492,6 +1555,22 @@ export class HeaterCollection extends EqItemCollection<Heater> {
1492
1555
  if (typeof add !== 'undefined' && add) return this.add(data || { id: this.data.length + 1, address: address });
1493
1556
  return this.createItem(data || { id: this.data.length + 1, address: address });
1494
1557
  }
1558
+ public filter(f: (value: Heater, index?: any, array?: any[]) => boolean): HeaterCollection {
1559
+ return new HeaterCollection({ heaters: this.data.filter(f) });
1560
+ }
1561
+
1562
+ public getSolarHeaters(bodyId?: number): EqItemCollection<Heater> {
1563
+ let htype = sys.board.valueMaps.heaterTypes.getValue('solar');
1564
+ return new HeaterCollection(this.data.filter(x => {
1565
+ if (x.type === htype) {
1566
+ if (typeof bodyId !== 'undefined') {
1567
+ if (!x.isActive) return false;
1568
+ return (bodyId === x.body || (sys.equipment.shared && x.body === 32)) ? true : false;
1569
+ }
1570
+ }
1571
+ return false;
1572
+ }));
1573
+ }
1495
1574
  }
1496
1575
  export class Heater extends EqItem {
1497
1576
  public dataName = 'heaterConfig';
@@ -1664,7 +1743,33 @@ export class LightGroup extends EqItem implements ICircuitGroup, ICircuit {
1664
1743
  public get lightingTheme(): number | any { return this.data.lightingTheme; }
1665
1744
  public set lightingTheme(val: number | any) { this.setDataVal('lightingTheme', sys.board.valueMaps.lightThemes.encode(val)); }
1666
1745
  public get circuits(): LightGroupCircuitCollection { return new LightGroupCircuitCollection(this.data, "circuits"); }
1667
- public getLightThemes() { return sys.board.valueMaps.lightThemes.toArray(); }
1746
+ public getLightThemes() {
1747
+ // Go through the circuits and gather the themes.
1748
+ // This method first looks at the circuits to determine their type (function)
1749
+ // then it filters the list by the types associated with the circuits. It does this because
1750
+ // there can be combined ColorLogic and IntelliBrite lights. The themes array has
1751
+ // the circuit function.
1752
+ let arrThemes = [];
1753
+ for (let i = 0; i < this.circuits.length; i++) {
1754
+ let circ = this.circuits.getItemByIndex(i);
1755
+ let c = sys.circuits.getInterfaceById(circ.circuit);
1756
+ let cf = sys.board.valueMaps.circuitFunctions.transform(c.type);
1757
+ if (cf.isLight && typeof cf.theme !== 'undefined') {
1758
+ if (!arrThemes.includes(cf.theme)) arrThemes.push(cf.theme);
1759
+ }
1760
+ }
1761
+ // Alright now we need to get a listing of the themes.
1762
+ let t = sys.board.valueMaps.lightThemes.toArray();
1763
+ let ret = [];
1764
+ for (let i = 0; i < t.length; i++) {
1765
+ let thm = t[i];
1766
+ if (typeof thm.types !== 'undefined' && thm.types.length > 0) {
1767
+ // Look in the themes array of the theme.
1768
+ if (arrThemes.some(x => thm.types.includes(x))) ret.push(thm);
1769
+ }
1770
+ }
1771
+ return ret;
1772
+ }
1668
1773
  public getExtended() {
1669
1774
  let group = this.get(true);
1670
1775
  group.type = sys.board.valueMaps.circuitGroupTypes.transform(group.type);
@@ -144,6 +144,16 @@ export class ParameterOutOfRangeError extends InvalidOperationError {
144
144
  public value;
145
145
  public parameter: string;
146
146
  }
147
+ export class BoardProcessError extends ApiError {
148
+ constructor(message: string, process?: string) {
149
+ super(message, 300, 400);
150
+ this.name = 'ProcessingError';
151
+ this.process = process;
152
+ }
153
+ public process: string;
154
+
155
+ }
156
+
147
157
  export class MessageError extends ApiError {
148
158
  constructor(msg: Message, message: string, code?: number, httpCode?: number) {
149
159
  super(message, code, httpCode);
@@ -0,0 +1,423 @@
1
+ /* nodejs-poolController. An application to control pool equipment.
2
+ Copyright (C) 2016, 2017, 2018, 2019, 2020. Russell Goldin, tagyoureit. russ.goldin@gmail.com
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ */
17
+ import { PumpState, HeaterState, BodyTempState, ICircuitState, state } from "./State";
18
+ import { Equipment, sys } from "./Equipment";
19
+ import { utils } from "./Constants";
20
+ import { logger } from "../logger/Logger";
21
+ import { webApp } from "../web/Server";
22
+ // LOCKOUT PRIMER
23
+ // Lockouts are either time based (Delays) or based upon the current state configuration for
24
+ // the system. So in some cases circuits can only be engaged in pool mode or in spa mode. In
25
+ // others a period of time must occur before a particular action can continue. Delays can typically
26
+ // be cancelled manually while lockouts can only be cancelled when the condition required for the lockout
27
+ // is changed.
28
+
29
+ // DELAYS:
30
+ // Pump Off During Valve Rotation (30 sec): This turns any pump associated with the body being turned on to
31
+ // so that is is off. This gives the valves time to rotate so that cold water from the pool does not cycle into
32
+ // the spa and hot water from the spa does not escape into the pool. This has nothing to do with
33
+ // water hammer or anything else.
34
+ //
35
+ // Heater Cooldown Delay (based on max heater time): When the system is heating and an event is occurring
36
+ // that will cause the heater to be turned off, the current mode will be retained until the delay is either
37
+ // cancelled or expired.
38
+ // Delay Conditions:
39
+ // 1. Being in either pool or spa mode and simply turning off that mode where the heater will be turned off.
40
+ // 2. Switching between pool and spa when the target mode does not use the identified heater.
41
+ // Exceptions:
42
+ // 1. The last call for heat was earlier than the current time minus the cooldown delay defined for the heater.
43
+ // 2. The heater mode is in a cooling mode.
44
+ //
45
+ // Heater Startup: When a body is first turned on the heater will not be engaged for 10 seconds after any pump delay
46
+ // or the time that the body is engaged.
47
+ //
48
+ // Cleaner Circuit Start Delay: Delays turning on any circuit with a cleaner function until the delay expires. This is
49
+ // so booster pumps can be assured of sufficient forward pressure prior to turning on. These pumps often require sufficient
50
+ // pressure before engaging and will cavitate if they do not have it. The Pentair default is 5min.
51
+ //
52
+ // Cleaner Circuit Solar Delay: This only exists with Pentair panels. This shuts off any circuit
53
+ // designated as a pool cleaner circuit if it is on and delays turning it on for 5min after the solar starts. The assumption
54
+ // here is that pressure reduction that can occur when the solar kicks on can cavitate the pump.
55
+ //
56
+ // LOCKOUTS (Proposed):
57
+ // Spillway Lockout: This locks out any circuit or feature that is marked with a Spillway circuit function (type) whenever
58
+ // whenever the pool circuit is not engaged. This should mark the spillway circuit as a delayStart then release it when the
59
+ // pool body starts.
60
+ interface ILockout {
61
+ type: string
62
+ }
63
+ export class EquipmentLockout implements ILockout {
64
+ public id = utils.uuid();
65
+ public create() { }
66
+ public startTime: Date;
67
+ public type: string = 'lockout';
68
+ public message: string = '';
69
+ }
70
+ export class EquipmentDelay implements ILockout {
71
+ public constructor() { this.id = delayMgr.getNextId(); }
72
+ public id;
73
+ public type: string = 'delay';
74
+ public endTime: Date;
75
+ public canCancel: boolean = true;
76
+ public cancelDelay() { };
77
+ public reset() { };
78
+ public clearDelay() { };
79
+ public message;
80
+ protected _delayTimer: NodeJS.Timeout;
81
+ public serialize(): any {
82
+ return {
83
+ id: this.id,
84
+ type: this.type,
85
+ canCancel: this.canCancel,
86
+ message: this.message
87
+ };
88
+ }
89
+ }
90
+ export class PumpValveDelay extends EquipmentDelay {
91
+ public constructor(ps: PumpState, delay?: number) {
92
+ super();
93
+ this.type = 'pumpValveDelay';
94
+ this.message = `${ps.name} will start after valve Delay`;
95
+ this.pumpState = ps;
96
+ this.pumpState.pumpOnDelay = true;
97
+ this._delayTimer = setTimeout(() => {
98
+ logger.info(`Valve delay expired for ${this.pumpState.name}`);
99
+ this.pumpState.pumpOnDelay = false;
100
+ delayMgr.deleteDelay(this.id);
101
+ }, delay * 1000 || sys.general.options.valveDelayTime * 1000);
102
+ logger.info(`Valve delay started for ${this.pumpState.name} - ${delay || sys.general.options.valveDelayTime}sec`);
103
+ }
104
+ public pumpState: PumpState;
105
+ public cancelDelay() {
106
+ this.pumpState.pumpOnDelay = false;
107
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
108
+ logger.info(`Valve delay cancelled for ${this.pumpState.name}`);
109
+ this._delayTimer = undefined;
110
+ delayMgr.deleteDelay(this.id);
111
+ }
112
+ public clearDelay() {
113
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
114
+ logger.info(`Valve delay cleared for ${this.pumpState.name}`);
115
+ this._delayTimer = undefined;
116
+ delayMgr.deleteDelay(this.id);
117
+ }
118
+ }
119
+ export class HeaterStartupDelay extends EquipmentDelay {
120
+ public constructor(hs: HeaterState, delay?: number) {
121
+ super();
122
+ this.type = 'heaterStartupDelay';
123
+ this.message = `${hs.name} will start after delay`;
124
+ this.heaterState = hs;
125
+ this.heaterState.startupDelay = true;
126
+ this._delayTimer = setTimeout(() => {
127
+ logger.info(`Heater Startup delay expired for ${this.heaterState.name}`);
128
+ this.heaterState.startupDelay = false;
129
+ delayMgr.deleteDelay(this.id);
130
+ }, delay * 1000 || sys.general.options.valveDelayTime * 1000);
131
+ logger.info(`Heater delay started for ${this.heaterState.name} - ${delay || sys.general.options.heaterStartDelayTime}sec`);
132
+ }
133
+ public heaterState: HeaterState;
134
+ public cancelDelay() {
135
+ this.heaterState.startupDelay = false;
136
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
137
+ logger.info(`Heater Startup delay cancelled for ${this.heaterState.name}`);
138
+ this._delayTimer = undefined;
139
+ delayMgr.deleteDelay(this.id);
140
+ }
141
+ }
142
+ export class HeaterCooldownDelay extends EquipmentDelay {
143
+ public constructor(bsoff: BodyTempState, bson?: BodyTempState, delay?: number) {
144
+ super();
145
+ this.type = 'heaterCooldownDelay';
146
+ this.message = `${bsoff.name} Heater Cooldown in progress`;
147
+ this.bodyStateOff = bsoff;
148
+ this.bodyStateOff.heaterCooldownDelay = true;
149
+ this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooldown');
150
+ let cstateOff = state.circuits.getItemById(bsoff.circuit);
151
+ this.bodyStateOn = bson;
152
+ this.bodyStateOff.stopDelay = cstateOff.stopDelay = true;
153
+ let cstateOn = (typeof bson !== 'undefined') ? state.circuits.getItemById(bson.circuit) : undefined;
154
+ if (typeof cstateOn !== 'undefined') {
155
+ this.bodyStateOn.startDelay = cstateOn.startDelay = true;
156
+ }
157
+ logger.verbose(`Heater Cooldown Delay started for ${this.bodyStateOff.name} - ${delay/1000}sec`);
158
+ this._delayTimer = setTimeout(() => {
159
+ logger.verbose(`Heater Cooldown delay expired for ${this.bodyStateOff.name}`);
160
+ this.bodyStateOff.stopDelay = state.circuits.getItemById(this.bodyStateOff.circuit).stopDelay = false;
161
+ // Now that the startup delay expired cancel the delay and shut off the circuit.
162
+ (async () => {
163
+ try {
164
+ await sys.board.circuits.setCircuitStateAsync(cstateOff.id, false, true);
165
+ if (typeof this.bodyStateOn !== 'undefined') {
166
+ this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
167
+ await sys.board.circuits.setCircuitStateAsync(this.bodyStateOn.circuit, true);
168
+ }
169
+ } catch (err) { logger.error(`Error executing Cooldown Delay completion: ${err}`); }
170
+ })();
171
+ this.bodyStateOff.heaterCooldownDelay = false;
172
+ this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
173
+ delayMgr.deleteDelay(this.id);
174
+ }, delay);
175
+ state.emitEquipmentChanges();
176
+ }
177
+ public bodyStateOff: BodyTempState;
178
+ public bodyStateOn: BodyTempState;
179
+ public setBodyStateOn(bson?: BodyTempState) {
180
+ if (typeof this.bodyStateOn !== 'undefined' && (typeof bson === 'undefined' || this.bodyStateOn.id !== bson.id))
181
+ this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
182
+ if (typeof bson !== 'undefined') {
183
+ if (typeof this.bodyStateOn === 'undefined' || this.bodyStateOn.id !== bson.id) {
184
+ bson.startDelay = state.circuits.getItemById(bson.circuit).startDelay = true;
185
+ logger.info(`${bson.name} will Start After Cooldown Delay`);
186
+ this.bodyStateOn = bson;
187
+ }
188
+ }
189
+ else this.bodyStateOn = undefined;
190
+ }
191
+ public cancelDelay() {
192
+ let cstateOff = state.circuits.getItemById(this.bodyStateOff.circuit);
193
+ cstateOff.stopDelay = false;
194
+ (async () => {
195
+ await sys.board.circuits.setCircuitStateAsync(cstateOff.id, false);
196
+ if (typeof this.bodyStateOn !== 'undefined') {
197
+ this.bodyStateOn.startDelay = state.circuits.getItemById(this.bodyStateOn.circuit).startDelay = false;
198
+ await sys.board.circuits.setCircuitStateAsync(this.bodyStateOn.circuit, true);
199
+ }
200
+ })();
201
+ this.bodyStateOff.stopDelay = this.bodyStateOff.heaterCooldownDelay = false;
202
+ this.bodyStateOff.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
203
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
204
+ logger.info(`Heater Cooldown delay cancelled for ${this.bodyStateOff.name}`);
205
+ this._delayTimer = undefined;
206
+ delayMgr.deleteDelay(this.id);
207
+ state.emitEquipmentChanges();
208
+ }
209
+
210
+ }
211
+ interface ICleanerDelay {
212
+ cleanerState: ICircuitState,
213
+ bodyId: number
214
+ }
215
+ export class CleanerStartDelay extends EquipmentDelay implements ICleanerDelay {
216
+ constructor(cs: ICircuitState, bodyId: number, delay?: number) {
217
+ super();
218
+ this.type = 'cleanerStartDelay';
219
+ this.message = `${cs.name} will start after delay`;
220
+ this.bodyId = bodyId;
221
+ this.cleanerState = cs;
222
+ cs.startDelay = true;
223
+ this._delayTimer = setTimeout(() => {
224
+ logger.info(`Cleaner delay expired for ${this.cleanerState.name}`);
225
+ this.cleanerState.startDelay = false;
226
+ (async () => {
227
+ try {
228
+ await sys.board.circuits.setCircuitStateAsync(this.cleanerState.id, true, true);
229
+ this.cleanerState.startDelay = false;
230
+ } catch (err) { logger.error(`Error executing Cleaner Start Delay completion: ${err}`); }
231
+ })();
232
+ delayMgr.deleteDelay(this.id);
233
+ }, delay * 1000 || sys.general.options.cleanerStartDelayTime * 1000);
234
+ logger.info(`Cleaner delay started for ${this.cleanerState.name} - ${delay || sys.general.options.cleanerStartDelayTime}sec`);
235
+ }
236
+ public cleanerState: ICircuitState;
237
+ public bodyId: number;
238
+ public cancelDelay() {
239
+ this.cleanerState.startDelay = false;
240
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
241
+ logger.info(`Cleaner Start delay cancelled for ${this.cleanerState.name}`);
242
+ this._delayTimer = undefined;
243
+ this.cleanerState.startDelay = false;
244
+ (async () => {
245
+ try {
246
+ await sys.board.circuits.setCircuitStateAsync(this.cleanerState.id, true, true);
247
+ } catch (err) { logger.error(`Error executing Cleaner Start Delay completion: ${err}`); }
248
+ })();
249
+ delayMgr.deleteDelay(this.id);
250
+ }
251
+ public clearDelay() {
252
+ this.cleanerState.startDelay = false;
253
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
254
+ logger.info(`Cleaner Start delay cleared for ${this.cleanerState.name}`);
255
+ this._delayTimer = undefined;
256
+ this.cleanerState.startDelay = false;
257
+ delayMgr.deleteDelay(this.id);
258
+ }
259
+
260
+ public reset(delay?: number) {
261
+ if (typeof this._delayTimer !== 'undefined') clearTimeout(this._delayTimer);
262
+ this.cleanerState.startDelay = true;
263
+ logger.info(`Cleaner Start delay reset for ${this.cleanerState.name}`);
264
+ this._delayTimer = setTimeout(() => {
265
+ logger.info(`Cleaner delay expired for ${this.cleanerState.name}`);
266
+ this.cleanerState.startDelay = false;
267
+ (async () => {
268
+ try {
269
+ await sys.board.circuits.setCircuitStateAsync(this.cleanerState.id, true);
270
+ this.cleanerState.startDelay = false;
271
+ } catch (err) { logger.error(`Error executing Cleaner Start Delay completion: ${err}`); }
272
+ })();
273
+ delayMgr.deleteDelay(this.id);
274
+ }, delay * 1000 || sys.general.options.cleanerStartDelayTime * 1000);
275
+ }
276
+ }
277
+ export class DelayManager extends Array<EquipmentDelay> {
278
+ protected _id = 1;
279
+ private _emitTimer: NodeJS.Timeout;
280
+ public setDirty() {
281
+ if (typeof this._emitTimer) clearTimeout(this._emitTimer);
282
+ this._emitTimer = setTimeout(() => this.emitDelayState(), 1000);
283
+ }
284
+ public getNextId() { return this._id++; }
285
+ public cancelDelay(id: number) {
286
+ let del = this.find(x => x.id === id);
287
+ if (typeof del !== 'undefined') del.cancelDelay();
288
+ }
289
+ public setPumpValveDelay(ps: PumpState, delay?: number) {
290
+ let cds = this.filter(x => x.type === 'pumpValveDelay');
291
+ for (let i = 0; i < cds.length; i++) {
292
+ let delay = cds[i] as PumpValveDelay;
293
+ if (delay.pumpState.id === ps.id) delay.clearDelay();
294
+ }
295
+ this.push(new PumpValveDelay(ps, delay)); this.setDirty();
296
+ }
297
+ public cancelPumpValveDelays() { this.cancelDelaysByType('pumpValveDelay'); this.setDirty(); }
298
+ public setHeaterStartupDelay(hs: HeaterState, delay?: number) {
299
+ let cds = this.filter(x => x.type === 'heaterStartupDelay');
300
+ for (let i = 0; i < cds.length; i++) {
301
+ let delay = cds[i] as HeaterStartupDelay;
302
+ if (delay.heaterState.id === hs.id) delay.cancelDelay();
303
+ }
304
+ this.push(new HeaterStartupDelay(hs, delay)); this.setDirty();
305
+ }
306
+ public cancelHeaterStartupDelays() {
307
+ this.cancelDelaysByType('heaterStartupDelay');
308
+ }
309
+ public setHeaterCooldownDelay(bsOff: BodyTempState, bsOn?: BodyTempState, delay?: number) {
310
+ logger.info(`Setting Heater Cooldown Delay for ${bsOff.name}`);
311
+ let cds = this.filter(x => x.type === 'heaterCooldownDelay');
312
+ for (let i = 0; i < cds.length; i++) {
313
+ let delay = cds[i] as HeaterCooldownDelay;
314
+ if (delay.bodyStateOff.id === bsOff.id) {
315
+ if(typeof bsOn !== 'undefined') logger.info(`Found Cooldown Delay adding on circuit ${bsOn.name}`);
316
+ delay.setBodyStateOn(bsOn);
317
+ this.setDirty();
318
+ return;
319
+ }
320
+ }
321
+ this.push(new HeaterCooldownDelay(bsOff, bsOn, delay));
322
+ this.setDirty();
323
+ }
324
+ public clearBodyStartupDelay(bs: BodyTempState) {
325
+ logger.info(`Clearing startup delays for ${bs.name}`);
326
+ // We are doing this non type safety thing below so that
327
+ // we can only emit when the body is cleared.
328
+ let cds = this.filter(x => {
329
+ return x.type === 'heaterCooldownDelay' &&
330
+ typeof x['bodyStateOn'] !== 'undefined' &&
331
+ x['bodyStateOn'].id === bs.id;
332
+ });
333
+ for (let i = 0; i < cds.length; i++) {
334
+ let delay = cds[i] as HeaterCooldownDelay;
335
+ logger.info(`Clearing ${bs.name} from Cooldown Delay`);
336
+ delay.setBodyStateOn();
337
+ }
338
+ if (cds.length) this.setDirty();
339
+ }
340
+ public cancelHeaterCooldownDelays() { this.cancelDelaysByType('heaterCooldownDelay'); }
341
+ public setCleanerStartDelay(cs: ICircuitState, bodyId: number, delay?: number) {
342
+ let cds = this.filter(x => x.type === ('cleanerStartDelay' || 'cleanerSolarDelay'));
343
+ let startDelay: CleanerStartDelay;
344
+ for (let i = 0; i < cds.length; i++) {
345
+ let delay = cds[i] as unknown as ICleanerDelay;
346
+ if (delay.cleanerState.id === cs.id) {
347
+ if (delay.bodyId !== bodyId || cds[i].type !== 'cleanerStartDelay') cds[i].cancelDelay();
348
+ else if (typeof startDelay !== 'undefined') {
349
+ startDelay.cancelDelay();
350
+ startDelay = cds[i] as CleanerStartDelay;
351
+ }
352
+ else startDelay = cds[i] as CleanerStartDelay;
353
+ }
354
+ }
355
+ if (typeof startDelay !== 'undefined') {
356
+ startDelay.reset(delay);
357
+ this.setDirty();
358
+ }
359
+ else {
360
+ this.push(new CleanerStartDelay(cs, bodyId, delay));
361
+ this.setDirty();
362
+ }
363
+ }
364
+ public cancelCleanerStartDelays(bodyId?: number) {
365
+ if (typeof bodyId === 'undefined') this.cancelDelaysByType('cleanerStartDelay');
366
+ else {
367
+ let delays = this.filter(x => x.type === 'cleanerStartDelay' && x['bodyId'] === bodyId);
368
+ for (let i = 0; i < delays.length; i++) {
369
+ delays[i].cancelDelay();
370
+ }
371
+ if (delays.length > 0) this.setDirty();
372
+ }
373
+ }
374
+ public clearCleanerStartDelays(bodyId?: number) {
375
+ if (typeof bodyId === 'undefined') this.clearDelaysByType('cleanerStartDelay');
376
+ else {
377
+ let delays = this.filter(x => x.type === 'cleanerStartDelay' && x['bodyId'] === bodyId);
378
+ for (let i = 0; i < delays.length; i++) {
379
+ delays[i].clearDelay();
380
+ }
381
+ if (delays.length > 0) this.setDirty();
382
+ }
383
+ }
384
+ public deleteDelay(id: number) {
385
+ for (let i = this.length - 1; i >= 0; i--) {
386
+ if (this[i].id === id) {
387
+ this.splice(i, 1);
388
+ this.setDirty();
389
+ }
390
+ }
391
+ }
392
+ public setSolarStartupDelay
393
+ protected cancelDelaysByType(type: string) {
394
+ let delays = this.filter(x => x.type === type);
395
+ for (let i = 0; i < delays.length; i++) {
396
+ delays[i].cancelDelay();
397
+ }
398
+ }
399
+ protected clearDelaysByType(type: string) {
400
+ let delays = this.filter(x => x.type === type);
401
+ for (let i = 0; i < delays.length; i++) {
402
+ delays[i].clearDelay();
403
+ }
404
+ if (delays.length > 0) this.setDirty();
405
+ }
406
+ public serialize() {
407
+ try {
408
+ let delays = [];
409
+ for (let i = 0; i < this.length; i++) {
410
+ delays.push(this[i].serialize());
411
+ }
412
+ return delays;
413
+ } catch (err) { logger.error(`Error serializing delays: ${err.message}`); }
414
+ }
415
+ public emitDelayState() {
416
+ try {
417
+ // We have to use a custom serializer because the properties of
418
+ // our delays will create a circular reference due to the timers and state references.
419
+ webApp.emitToClients('delays', this.serialize());
420
+ } catch (err) { logger.error(`Error emitting delay states ${err.message}`); }
421
+ }
422
+ }
423
+ export let delayMgr = new DelayManager();