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 +14 -0
- package/README.md +1 -1
- package/controller/Equipment.ts +107 -2
- package/controller/Errors.ts +10 -0
- package/controller/Lockouts.ts +423 -0
- package/controller/State.ts +112 -8
- package/controller/boards/EasyTouchBoard.ts +15 -13
- package/controller/boards/IntelliCenterBoard.ts +82 -87
- package/controller/boards/NixieBoard.ts +590 -128
- package/controller/boards/SystemBoard.ts +1264 -970
- package/controller/comms/messages/config/ExternalMessage.ts +1 -1
- package/controller/comms/messages/config/ValveMessage.ts +1 -1
- package/controller/comms/messages/status/HeaterStateMessage.ts +27 -3
- package/controller/nixie/bodies/Body.ts +3 -0
- package/controller/nixie/circuits/Circuit.ts +24 -8
- package/controller/nixie/heaters/Heater.ts +191 -31
- package/controller/nixie/pumps/Pump.ts +97 -60
- package/controller/nixie/valves/Valve.ts +1 -1
- package/package.json +1 -1
- package/web/bindings/mqtt.json +49 -38
- package/web/bindings/mqttAlt.json +12 -1
- package/web/services/config/Config.ts +6 -6
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
package/controller/Equipment.ts
CHANGED
|
@@ -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() {
|
|
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() {
|
|
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);
|
package/controller/Errors.ts
CHANGED
|
@@ -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();
|