nodejs-poolcontroller 7.3.0 → 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/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- package/Changelog +23 -0
- package/README.md +5 -5
- package/app.ts +2 -0
- package/config/Config.ts +3 -0
- package/config/VersionCheck.ts +8 -4
- package/controller/Constants.ts +88 -0
- package/controller/Equipment.ts +246 -66
- package/controller/Errors.ts +24 -1
- package/controller/Lockouts.ts +423 -0
- package/controller/State.ts +314 -54
- package/controller/boards/EasyTouchBoard.ts +107 -59
- package/controller/boards/IntelliCenterBoard.ts +186 -125
- package/controller/boards/IntelliTouchBoard.ts +104 -30
- package/controller/boards/NixieBoard.ts +721 -159
- package/controller/boards/SystemBoard.ts +2370 -1108
- package/controller/comms/Comms.ts +85 -10
- package/controller/comms/messages/Messages.ts +10 -4
- package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -4
- package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
- package/controller/comms/messages/config/CoverMessage.ts +1 -0
- package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
- package/controller/comms/messages/config/ExternalMessage.ts +44 -26
- package/controller/comms/messages/config/FeatureMessage.ts +8 -1
- package/controller/comms/messages/config/GeneralMessage.ts +8 -0
- package/controller/comms/messages/config/HeaterMessage.ts +15 -9
- package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
- package/controller/comms/messages/config/OptionsMessage.ts +13 -1
- package/controller/comms/messages/config/PumpMessage.ts +4 -20
- package/controller/comms/messages/config/RemoteMessage.ts +4 -0
- package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
- package/controller/comms/messages/config/SecurityMessage.ts +1 -0
- package/controller/comms/messages/config/ValveMessage.ts +13 -3
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
- package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
- package/controller/comms/messages/status/HeaterStateMessage.ts +42 -9
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +37 -26
- package/controller/nixie/Nixie.ts +18 -16
- package/controller/nixie/bodies/Body.ts +4 -1
- package/controller/nixie/chemistry/ChemController.ts +80 -77
- package/controller/nixie/chemistry/Chlorinator.ts +9 -8
- package/controller/nixie/circuits/Circuit.ts +55 -6
- package/controller/nixie/heaters/Heater.ts +192 -32
- package/controller/nixie/pumps/Pump.ts +146 -84
- package/controller/nixie/schedules/Schedule.ts +3 -2
- package/controller/nixie/valves/Valve.ts +1 -1
- package/defaultConfig.json +32 -1
- package/issue_template.md +1 -1
- package/logger/DataLogger.ts +37 -22
- package/package.json +20 -18
- package/web/Server.ts +520 -29
- package/web/bindings/influxDB.json +96 -8
- package/web/bindings/mqtt.json +151 -40
- package/web/bindings/mqttAlt.json +114 -4
- package/web/interfaces/httpInterface.ts +2 -0
- package/web/interfaces/influxInterface.ts +36 -19
- package/web/interfaces/mqttInterface.ts +14 -3
- package/web/services/config/Config.ts +171 -44
- package/web/services/state/State.ts +49 -5
- package/web/services/state/StateSocket.ts +18 -1
|
@@ -19,9 +19,11 @@ import { logger } from '../../logger/Logger';
|
|
|
19
19
|
import { Message, Outbound } from '../comms/messages/Messages';
|
|
20
20
|
import { Timestamp, utils } from '../Constants';
|
|
21
21
|
import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, ControllerType, CustomName, CustomNameCollection, EggTimer, Equipment, Feature, Filter, General, Heater, ICircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, TempSensorCollection, Valve } from '../Equipment';
|
|
22
|
-
import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError } from '../Errors';
|
|
22
|
+
import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, BoardProcessError } from '../Errors';
|
|
23
23
|
import { ncp } from "../nixie/Nixie";
|
|
24
24
|
import { BodyTempState, ChemControllerState, ChlorinatorState, CircuitGroupState, FilterState, ICircuitGroupState, ICircuitState, LightGroupState, ScheduleState, state, TemperatureState, ValveState, VirtualCircuitState } from '../State';
|
|
25
|
+
import { RestoreResults } from '../../web/Server';
|
|
26
|
+
|
|
25
27
|
|
|
26
28
|
export class byteValueMap extends Map<number, any> {
|
|
27
29
|
public transform(byte: number, ext?: number) { return extend(true, { val: byte || 0 }, this.get(byte) || this.get(0)); }
|
|
@@ -115,362 +117,404 @@ export class EquipmentIds {
|
|
|
115
117
|
public invalidIds: InvalidEquipmentIdArray = new InvalidEquipmentIdArray([]);
|
|
116
118
|
}
|
|
117
119
|
export class byteValueMaps {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
120
|
+
constructor() {
|
|
121
|
+
this.pumpStatus.transform = function (byte) {
|
|
122
|
+
// if (byte === 0) return this.get(0);
|
|
123
|
+
if (byte === 0) return extend(true, {}, this.get(0), { val: byte });
|
|
124
|
+
for (let b = 16; b > 0; b--) {
|
|
125
|
+
let bit = (1 << (b - 1));
|
|
126
|
+
if ((byte & bit) > 0) {
|
|
127
|
+
let v = this.get(b);
|
|
128
|
+
if (typeof v !== 'undefined') {
|
|
129
|
+
return extend(true, {}, v, { val: byte });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { val: byte, name: 'error' + byte, desc: 'Unspecified Error ' + byte };
|
|
134
|
+
};
|
|
135
|
+
this.chlorinatorStatus.transform = function (byte) {
|
|
136
|
+
if (byte === 128) return { val: 128, name: 'commlost', desc: 'Communication Lost' };
|
|
137
|
+
else if (byte === 0) return { val: 0, name: 'ok', desc: 'Ok' };
|
|
138
|
+
for (let b = 8; b > 0; b--) {
|
|
139
|
+
let bit = (1 << (b - 1));
|
|
140
|
+
if ((byte & bit) > 0) {
|
|
141
|
+
let v = this.get(b);
|
|
142
|
+
if (typeof v !== "undefined") {
|
|
143
|
+
return extend(true, {}, v, { val: byte & 0x00FF });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { val: byte, name: 'unknown' + byte, desc: 'Unknown status ' + byte };
|
|
148
|
+
};
|
|
149
|
+
this.scheduleTypes.transform = function (byte) {
|
|
150
|
+
return (byte & 128) > 0 ? extend(true, { val: 128 }, this.get(128)) : extend(true, { val: 0 }, this.get(0));
|
|
151
|
+
};
|
|
152
|
+
this.scheduleDays.transform = function (byte) {
|
|
153
|
+
let days = [];
|
|
154
|
+
let b = byte & 0x007F;
|
|
155
|
+
for (let bit = 7; bit >= 0; bit--) {
|
|
156
|
+
if ((byte & (1 << (bit - 1))) > 0) days.push(extend(true, {}, this.get(bit)));
|
|
157
|
+
}
|
|
158
|
+
return { val: b, days: days };
|
|
159
|
+
};
|
|
160
|
+
this.scheduleDays.toArray = function () {
|
|
161
|
+
let arrKeys = Array.from(this.keys());
|
|
162
|
+
let arr = [];
|
|
163
|
+
for (let i = 0; i < arrKeys.length; i++) arr.push(extend(true, { val: arrKeys[i] }, this.get(arrKeys[i])));
|
|
164
|
+
return arr;
|
|
165
|
+
};
|
|
166
|
+
this.virtualCircuits.transform = function (byte) {
|
|
167
|
+
return extend(true, {}, { val: byte, name: 'Unknown ' + byte }, this.get(byte), { val: byte });
|
|
168
|
+
};
|
|
169
|
+
this.tempUnits.transform = function (byte) { return extend(true, {}, { val: byte & 0x04 }, this.get(byte & 0x04)); };
|
|
170
|
+
this.panelModes.transform = function (byte) { return extend(true, { val: byte & 0x83 }, this.get(byte & 0x83)); };
|
|
171
|
+
this.controllerStatus.transform = function (byte: number, percent?: number) {
|
|
172
|
+
let v = extend(true, {}, this.get(byte) || this.get(0));
|
|
173
|
+
if (typeof percent !== 'undefined') v.percent = percent;
|
|
174
|
+
return v;
|
|
175
|
+
};
|
|
176
|
+
this.lightThemes.transform = function (byte) { return typeof byte === 'undefined' ? this.get(255) : extend(true, { val: byte }, this.get(byte) || this.get(255)); };
|
|
177
|
+
this.timeZones.findItem = function (val: string | number | { val: any, name: string }) {
|
|
178
|
+
if (typeof val === null || typeof val === 'undefined') return;
|
|
179
|
+
else if (typeof val === 'number') {
|
|
180
|
+
if (val <= 12) { // We are looking for timezones based upon the utcOffset.
|
|
181
|
+
let arr = this.toArray();
|
|
182
|
+
let tz = arr.find(elem => elem.utcOffset === val);
|
|
183
|
+
return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined;
|
|
184
|
+
}
|
|
185
|
+
return this.transform(val);
|
|
186
|
+
}
|
|
187
|
+
else if (typeof val === 'string') {
|
|
188
|
+
let v = parseInt(val, 10);
|
|
189
|
+
if (!isNaN(v)) {
|
|
190
|
+
if (v <= 12) {
|
|
191
|
+
let arr = this.toArray();
|
|
192
|
+
let tz = arr.find(elem => elem.utcOffset === val);
|
|
193
|
+
return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined;
|
|
194
|
+
}
|
|
195
|
+
return this.transform(v);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
let arr = this.toArray();
|
|
199
|
+
let tz = arr.find(elem => elem.abbrev === val || elem.name === val);
|
|
200
|
+
return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (typeof val === 'object') {
|
|
204
|
+
if (typeof val.val !== 'undefined') return this.transform(parseInt(val.val, 10));
|
|
205
|
+
else if (typeof val.name !== 'undefined') return this.transformByName(val.name);
|
|
206
|
+
}
|
|
199
207
|
}
|
|
200
|
-
}
|
|
201
|
-
else if (typeof val === 'object') {
|
|
202
|
-
if (typeof val.val !== 'undefined') return this.transform(parseInt(val.val, 10));
|
|
203
|
-
else if (typeof val.name !== 'undefined') return this.transformByName(val.name);
|
|
204
|
-
}
|
|
205
208
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
]);
|
|
209
|
+
public expansionBoards: byteValueMap = new byteValueMap();
|
|
210
|
+
// Identifies which controller manages the underlying equipment.
|
|
211
|
+
public equipmentMaster: byteValueMap = new byteValueMap([
|
|
212
|
+
[0, { val: 0, name: 'ocp', desc: 'Outdoor Control Panel' }],
|
|
213
|
+
[1, { val: 1, name: 'ncp', desc: 'Nixie Control Panel' }]
|
|
214
|
+
]);
|
|
215
|
+
public equipmentCommStatus: byteValueMap = new byteValueMap([
|
|
216
|
+
[0, { val: 0, name: 'ready', desc: 'Ready' }],
|
|
217
|
+
[1, { val: 1, name: 'commerr', desc: 'Communication Error' }]
|
|
218
|
+
]);
|
|
219
|
+
public panelModes: byteValueMap = new byteValueMap([
|
|
220
|
+
[0, { val: 0, name: 'auto', desc: 'Auto' }],
|
|
221
|
+
// [1, { val: 1, name: 'service', desc: 'Service' }],
|
|
222
|
+
// [8, { val: 8, name: 'freeze', desc: 'Freeze' }],
|
|
223
|
+
// [128, { val: 128, name: 'timeout', desc: 'Timeout' }],
|
|
224
|
+
// [129, { val: 129, name: 'service-timeout', desc: 'Service/Timeout' }],
|
|
225
|
+
[255, { name: 'error', desc: 'System Error' }]
|
|
226
|
+
]);
|
|
227
|
+
public controllerStatus: byteValueMap = new byteValueMap([
|
|
228
|
+
[0, { val: 0, name: 'initializing', desc: 'Initializing', percent: 0 }],
|
|
229
|
+
[1, { val: 1, name: 'ready', desc: 'Ready', percent: 100 }],
|
|
230
|
+
[2, { val: 2, name: 'loading', desc: 'Loading', percent: 0 }],
|
|
231
|
+
[3, { val: 255, name: 'Error', desc: 'Error', percent: 0 }]
|
|
232
|
+
]);
|
|
231
233
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
234
|
+
public circuitFunctions: byteValueMap = new byteValueMap([
|
|
235
|
+
[0, { name: 'generic', desc: 'Generic' }],
|
|
236
|
+
[1, { name: 'spa', desc: 'Spa', hasHeatSource: true, body: 2 }],
|
|
237
|
+
[2, { name: 'pool', desc: 'Pool', hasHeatSource: true, body: 1 }],
|
|
238
|
+
[5, { name: 'mastercleaner', desc: 'Master Cleaner', body: 1 }],
|
|
239
|
+
[7, { name: 'light', desc: 'Light', isLight: true }],
|
|
240
|
+
[9, { name: 'samlight', desc: 'SAM Light', isLight: true }],
|
|
241
|
+
[10, { name: 'sallight', desc: 'SAL Light', isLight: true }],
|
|
242
|
+
[11, { name: 'photongen', desc: 'Photon Gen', isLight: true }],
|
|
243
|
+
[12, { name: 'colorwheel', desc: 'Color Wheel', isLight: true }],
|
|
244
|
+
[13, { name: 'valve', desc: 'Valve' }],
|
|
245
|
+
[14, { name: 'spillway', desc: 'Spillway' }],
|
|
246
|
+
[15, { name: 'floorcleaner', desc: 'Floor Cleaner', body: 1 }], // This circuit function does not seem to exist in IntelliTouch.
|
|
247
|
+
[16, { name: 'intellibrite', desc: 'Intellibrite', isLight: true, themes: 'intellibrite' }],
|
|
248
|
+
[17, { name: 'magicstream', desc: 'Magicstream', isLight: true, themes: 'magicstream' }],
|
|
249
|
+
[19, { name: 'notused', desc: 'Not Used' }],
|
|
250
|
+
[65, { name: 'lotemp', desc: 'Lo-Temp' }],
|
|
251
|
+
[66, { name: 'hightemp', desc: 'Hi-Temp' }]
|
|
252
|
+
]);
|
|
249
253
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
public scheduleTimeTypes: byteValueMap = new byteValueMap([
|
|
310
|
-
[0, { name: 'manual', desc: 'Manual' }]
|
|
311
|
-
]);
|
|
312
|
-
public scheduleDisplayTypes: byteValueMap = new byteValueMap([
|
|
313
|
-
[0, { name: 'always', desc: 'Always' }],
|
|
314
|
-
[1, { name: 'active', desc: 'When Active' }],
|
|
315
|
-
[2, { name: 'never', desc: 'Never' }]
|
|
316
|
-
]);
|
|
254
|
+
// Feature functions are used as the available options to define a circuit.
|
|
255
|
+
public featureFunctions: byteValueMap = new byteValueMap([[0, { name: 'generic', desc: 'Generic' }], [1, { name: 'spillway', desc: 'Spillway' }]]);
|
|
256
|
+
public virtualCircuits: byteValueMap = new byteValueMap([
|
|
257
|
+
[128, { name: 'solar', desc: 'Solar', assignableToPumpCircuit: true }],
|
|
258
|
+
[129, { name: 'heater', desc: 'Either Heater', assignableToPumpCircuit: true }],
|
|
259
|
+
[130, { name: 'poolHeater', desc: 'Pool Heater', assignableToPumpCircuit: true }],
|
|
260
|
+
[131, { name: 'spaHeater', desc: 'Spa Heater', assignableToPumpCircuit: true }],
|
|
261
|
+
[132, { name: 'freeze', desc: 'Freeze', assignableToPumpCircuit: true }],
|
|
262
|
+
[133, { name: 'heatBoost', desc: 'Heat Boost', assignableToPumpCircuit: false }],
|
|
263
|
+
[134, { name: 'heatEnable', desc: 'Heat Enable', assignableToPumpCircuit: false }],
|
|
264
|
+
[135, { name: 'pumpSpeedUp', desc: 'Pump Speed +', assignableToPumpCircuit: false }],
|
|
265
|
+
[136, { name: 'pumpSpeedDown', desc: 'Pump Speed -', assignableToPumpCircuit: false }],
|
|
266
|
+
[255, { name: 'notused', desc: 'NOT USED', assignableToPumpCircuit: true }]
|
|
267
|
+
]);
|
|
268
|
+
public lightThemes: byteValueMap = new byteValueMap([
|
|
269
|
+
[0, { name: 'off', desc: 'Off', types: ['intellibrite'] }],
|
|
270
|
+
[1, { name: 'on', desc: 'On', types: ['intellibrite'] }],
|
|
271
|
+
[128, { name: 'colorsync', desc: 'Color Sync', types: ['intellibrite'] }],
|
|
272
|
+
[144, { name: 'colorswim', desc: 'Color Swim', types: ['intellibrite'] }],
|
|
273
|
+
[160, { name: 'colorset', desc: 'Color Set', types: ['intellibrite'] }],
|
|
274
|
+
[177, { name: 'party', desc: 'Party', types: ['intellibrite'], sequence: 2 }],
|
|
275
|
+
[178, { name: 'romance', desc: 'Romance', types: ['intellibrite'], sequence: 3 }],
|
|
276
|
+
[179, { name: 'caribbean', desc: 'Caribbean', types: ['intellibrite'], sequence: 4 }],
|
|
277
|
+
[180, { name: 'american', desc: 'American', types: ['intellibrite'], sequence: 5 }],
|
|
278
|
+
[181, { name: 'sunset', desc: 'Sunset', types: ['intellibrite'], sequence: 6 }],
|
|
279
|
+
[182, { name: 'royal', desc: 'Royal', types: ['intellibrite'], sequence: 7 }],
|
|
280
|
+
[190, { name: 'save', desc: 'Save', types: ['intellibrite'], sequence: 13 }],
|
|
281
|
+
[191, { name: 'recall', desc: 'Recall', types: ['intellibrite'], sequence: 14 }],
|
|
282
|
+
[193, { name: 'blue', desc: 'Blue', types: ['intellibrite'], sequence: 8 }],
|
|
283
|
+
[194, { name: 'green', desc: 'Green', types: ['intellibrite'], sequence: 9 }],
|
|
284
|
+
[195, { name: 'red', desc: 'Red', types: ['intellibrite'], sequence: 10 }],
|
|
285
|
+
[196, { name: 'white', desc: 'White', types: ['intellibrite'], sequence: 11 }],
|
|
286
|
+
[197, { name: 'magenta', desc: 'Magenta', types: ['intellibrite'], sequence: 12 }],
|
|
287
|
+
[208, { name: 'thumper', desc: 'Thumper', types: ['magicstream'] }],
|
|
288
|
+
[209, { name: 'hold', desc: 'Hold', types: ['magicstream'] }],
|
|
289
|
+
[210, { name: 'reset', desc: 'Reset', types: ['magicstream'] }],
|
|
290
|
+
[211, { name: 'mode', desc: 'Mode', types: ['magicstream'] }],
|
|
291
|
+
[254, { name: 'unknown', desc: 'unknown' }],
|
|
292
|
+
[255, { name: 'none', desc: 'None' }]
|
|
293
|
+
]);
|
|
294
|
+
public colorLogicThemes = new byteValueMap([
|
|
295
|
+
[0, { name: 'cloudwhite', desc: 'Cloud White', types: ['colorlogic'], sequence: 7 }],
|
|
296
|
+
[1, { name: 'deepsea', desc: 'Deep Sea', types: ['colorlogic'], sequence: 2 }],
|
|
297
|
+
[2, { name: 'royalblue', desc: 'Royal Blue', types: ['colorlogic'], sequence: 3 }],
|
|
298
|
+
[3, { name: 'afernoonskies', desc: 'Afternoon Skies', types: ['colorlogic'], sequence: 4 }],
|
|
299
|
+
[4, { name: 'aquagreen', desc: 'Aqua Green', types: ['colorlogic'], sequence: 5 }],
|
|
300
|
+
[5, { name: 'emerald', desc: 'Emerald', types: ['colorlogic'], sequence: 6 }],
|
|
301
|
+
[6, { name: 'warmred', desc: 'Warm Red', types: ['colorlogic'], sequence: 8 }],
|
|
302
|
+
[7, { name: 'flamingo', desc: 'Flamingo', types: ['colorlogic'], sequence: 9 }],
|
|
303
|
+
[8, { name: 'vividviolet', desc: 'Vivid Violet', types: ['colorlogic'], sequence: 10 }],
|
|
304
|
+
[9, { name: 'sangria', desc: 'Sangria', types: ['colorlogic'], sequence: 11 }],
|
|
305
|
+
[10, { name: 'twilight', desc: 'Twilight', types: ['colorlogic'], sequence: 12 }],
|
|
306
|
+
[11, { name: 'tranquility', desc: 'Tranquility', types: ['colorlogic'], sequence: 13 }],
|
|
307
|
+
[12, { name: 'gemstone', desc: 'Gemstone', types: ['colorlogic'], sequence: 14 }],
|
|
308
|
+
[13, { name: 'usa', desc: 'USA', types: ['colorlogic'], sequence: 15 }],
|
|
309
|
+
[14, { name: 'mardigras', desc: 'Mardi Gras', types: ['colorlogic'], sequence: 16 }],
|
|
310
|
+
[15, { name: 'cabaret', desc: 'Cabaret', types: ['colorlogic'], sequence: 17 }],
|
|
311
|
+
[255, { name: 'none', desc: 'None' }]
|
|
312
|
+
]);
|
|
317
313
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
314
|
+
public lightColors: byteValueMap = new byteValueMap([
|
|
315
|
+
[0, { name: 'white', desc: 'White' }],
|
|
316
|
+
[2, { name: 'lightgreen', desc: 'Light Green' }],
|
|
317
|
+
[4, { name: 'green', desc: 'Green' }],
|
|
318
|
+
[6, { name: 'cyan', desc: 'Cyan' }],
|
|
319
|
+
[8, { name: 'blue', desc: 'Blue' }],
|
|
320
|
+
[10, { name: 'lavender', desc: 'Lavender' }],
|
|
321
|
+
[12, { name: 'magenta', desc: 'Magenta' }],
|
|
322
|
+
[14, { name: 'lightmagenta', desc: 'Light Magenta' }]
|
|
323
|
+
]);
|
|
324
|
+
public scheduleDays: byteValueMap = new byteValueMap([
|
|
325
|
+
[1, { name: 'sat', desc: 'Saturday', dow: 6 }],
|
|
326
|
+
[2, { name: 'fri', desc: 'Friday', dow: 5 }],
|
|
327
|
+
[3, { name: 'thu', desc: 'Thursday', dow: 4 }],
|
|
328
|
+
[4, { name: 'wed', desc: 'Wednesday', dow: 3 }],
|
|
329
|
+
[5, { name: 'tue', desc: 'Tuesday', dow: 2 }],
|
|
330
|
+
[6, { name: 'mon', desc: 'Monday', dow: 1 }],
|
|
331
|
+
[7, { name: 'sun', desc: 'Sunday', dow: 0 }]
|
|
332
|
+
]);
|
|
333
|
+
public scheduleTimeTypes: byteValueMap = new byteValueMap([
|
|
334
|
+
[0, { name: 'manual', desc: 'Manual' }]
|
|
335
|
+
]);
|
|
336
|
+
public scheduleDisplayTypes: byteValueMap = new byteValueMap([
|
|
337
|
+
[0, { name: 'always', desc: 'Always' }],
|
|
338
|
+
[1, { name: 'active', desc: 'When Active' }],
|
|
339
|
+
[2, { name: 'never', desc: 'Never' }]
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
public pumpTypes: byteValueMap = new byteValueMap([
|
|
343
|
+
[1, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }],
|
|
344
|
+
[64, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }],
|
|
345
|
+
[65, { name: 'ds', desc: 'Two-Speed', maxCircuits: 40, hasAddress: false, hasBody: true }],
|
|
346
|
+
[128, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, maxCircuits: 8, hasAddress: true }],
|
|
347
|
+
[169, { name: 'vssvrs', desc: 'IntelliFlo VS+SVRS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, maxCircuits: 8, hasAddress: true }]
|
|
348
|
+
]);
|
|
349
|
+
public pumpSSModels: byteValueMap = new byteValueMap([
|
|
350
|
+
[0, { name: 'unspecified', desc: 'Unspecified', amps: 0, pf: 0, volts: 0, watts: 0 }],
|
|
351
|
+
[1, { name: 'wf1hpE', desc: '1hp WhisperFlo E+', amps: 7.4, pf: .9, volts: 230, watts: 1532 }],
|
|
352
|
+
[2, { name: 'wf1hpMax', desc: '1hp WhisperFlo Max', amps: 9, pf: .87, volts: 230, watts: 1600 }],
|
|
353
|
+
[3, { name: 'generic15hp', desc: '1.5hp Pump', amps: 9.3, pf: .9, volts: 230, watts: 1925 }],
|
|
354
|
+
[4, { name: 'generic2hp', desc: '2hp Pump', amps: 12, pf: .9, volts: 230, watts: 2484 }],
|
|
355
|
+
[5, { name: 'generic25hp', desc: '2.5hp Pump', amps: 12.5, pf: .9, volts: 230, watts: 2587 }],
|
|
356
|
+
[6, { name: 'generic3hp', desc: '3hp Pump', amps: 13.5, pf: .9, volts: 230, watts: 2794 }]
|
|
357
|
+
]);
|
|
358
|
+
public pumpDSModels: byteValueMap = new byteValueMap([
|
|
359
|
+
[0, { name: 'unspecified', desc: 'Unspecified', loAmps: 0, hiAmps: 0, pf: 0, volts: 0, loWatts: 0, hiWatts: 0 }],
|
|
360
|
+
[1, { name: 'generic1hp', desc: '1hp Pump', loAmps: 2.4, hiAmps: 6.5, pf: .9, volts: 230, loWatts: 497, hiWatts: 1345 }],
|
|
361
|
+
[2, { name: 'generic15hp', desc: '1.5hp Pump', loAmps: 2.7, hiAmps: 9.3, pf: .9, volts: 230, loWatts: 558, hiWatts: 1925 }],
|
|
362
|
+
[3, { name: 'generic2hp', desc: '2hp Pump', loAmps: 2.9, hiAmps: 12, pf: .9, volts: 230, loWatts: 600, hiWatts: 2484 }],
|
|
363
|
+
[4, { name: 'generic25hp', desc: '2.5hp Pump', loAmps: 3.1, hiAmps: 12.5, pf: .9, volts: 230, loWatts: 642, hiWatts: 2587 }],
|
|
364
|
+
[5, { name: 'generic3hp', desc: '3hp Pump', loAmps: 3.3, hiAmps: 13.5, pf: .9, volts: 230, loWatts: 683, hiWatts: 2794 }]
|
|
365
|
+
]);
|
|
366
|
+
public pumpVSModels: byteValueMap = new byteValueMap([
|
|
367
|
+
[0, { name: 'intelliflovs', desc: 'IntelliFlo VS' }]
|
|
368
|
+
]);
|
|
369
|
+
public pumpVFModels: byteValueMap = new byteValueMap([
|
|
370
|
+
[0, { name: 'intelliflovf', desc: 'IntelliFlo VF' }]
|
|
371
|
+
]);
|
|
372
|
+
public pumpVSFModels: byteValueMap = new byteValueMap([
|
|
373
|
+
[0, { name: 'intelliflovsf', desc: 'IntelliFlo VSF' }]
|
|
374
|
+
]);
|
|
375
|
+
public pumpVSSVRSModels: byteValueMap = new byteValueMap([
|
|
376
|
+
[0, { name: 'intelliflovssvrs', desc: 'IntelliFlo VS+SVRS' }]
|
|
377
|
+
]);
|
|
378
|
+
// These are used for single-speed pump definitions. Essentially the way this works is that when
|
|
379
|
+
// the body circuit is running the single speed pump is on.
|
|
380
|
+
public pumpBodies: byteValueMap = new byteValueMap([
|
|
381
|
+
[0, { name: 'pool', desc: 'Pool' }],
|
|
382
|
+
[101, { name: 'spa', desc: 'Spa' }],
|
|
383
|
+
[255, { name: 'poolspa', desc: 'Pool/Spa' }]
|
|
384
|
+
]);
|
|
385
|
+
public heaterTypes: byteValueMap = new byteValueMap([
|
|
386
|
+
[1, { name: 'gas', desc: 'Gas Heater', hasAddress: false }],
|
|
387
|
+
[2, { name: 'solar', desc: 'Solar Heater', hasAddress: false, hasCoolSetpoint: true }],
|
|
388
|
+
[3, { name: 'heatpump', desc: 'Heat Pump', hasAddress: true }],
|
|
389
|
+
[4, { name: 'ultratemp', desc: 'UltraTemp', hasAddress: true, hasCoolSetpoint: true }],
|
|
390
|
+
[5, { name: 'hybrid', desc: 'Hybrid', hasAddress: true }],
|
|
391
|
+
[6, { name: 'mastertemp', desc: 'MasterTemp', hasAddress: true }],
|
|
392
|
+
[7, { name: 'maxetherm', desc: 'Max-E-Therm', hasAddress: true }],
|
|
393
|
+
]);
|
|
394
|
+
public heatModes: byteValueMap = new byteValueMap([
|
|
395
|
+
[0, { name: 'off', desc: 'Off' }],
|
|
396
|
+
[3, { name: 'heater', desc: 'Heater' }],
|
|
397
|
+
[5, { name: 'solar', desc: 'Solar Only' }],
|
|
398
|
+
[12, { name: 'solarpref', desc: 'Solar Preferred' }]
|
|
399
|
+
]);
|
|
400
|
+
public heatSources: byteValueMap = new byteValueMap([
|
|
401
|
+
[0, { name: 'off', desc: 'No Heater' }],
|
|
402
|
+
[3, { name: 'heater', desc: 'Heater' }],
|
|
403
|
+
[5, { name: 'solar', desc: 'Solar Only' }],
|
|
404
|
+
[21, { name: 'solarpref', desc: 'Solar Preferred' }],
|
|
405
|
+
[32, { name: 'nochange', desc: 'No Change' }]
|
|
406
|
+
]);
|
|
407
|
+
public heatStatus: byteValueMap = new byteValueMap([
|
|
408
|
+
[0, { name: 'off', desc: 'Off' }],
|
|
409
|
+
[1, { name: 'heater', desc: 'Heater' }],
|
|
410
|
+
[2, { name: 'solar', desc: 'Solar' }],
|
|
411
|
+
[3, { name: 'cooling', desc: 'Cooling' }],
|
|
412
|
+
[128, { name: 'cooldown', desc: 'Cooldown' }]
|
|
413
|
+
]);
|
|
414
|
+
public pumpStatus: byteValueMap = new byteValueMap([
|
|
415
|
+
[0, { name: 'off', desc: 'Off' }], // When the pump is disconnected or has no power then we simply report off as the status. This is not the recommended wiring
|
|
416
|
+
// for a VS/VF pump as is should be powered at all times. When it is, the status will always report a value > 0.
|
|
417
|
+
[1, { name: 'ok', desc: 'Ok' }], // Status is always reported when the pump is not wired to a relay regardless of whether it is on or not
|
|
418
|
+
// as is should be if this is a VS / VF pump. However if it is wired to a relay most often filter, the pump will report status
|
|
419
|
+
// 0 if it is not running. Essentially this is no error but it is not a status either.
|
|
420
|
+
[2, { name: 'filter', desc: 'Filter warning' }],
|
|
421
|
+
[3, { name: 'overcurrent', desc: 'Overcurrent condition' }],
|
|
422
|
+
[4, { name: 'priming', desc: 'Priming' }],
|
|
423
|
+
[5, { name: 'blocked', desc: 'System blocked' }],
|
|
424
|
+
[6, { name: 'general', desc: 'General alarm' }],
|
|
425
|
+
[7, { name: 'overtemp', desc: 'Overtemp condition' }],
|
|
426
|
+
[8, { name: 'power', dec: 'Power outage' }],
|
|
427
|
+
[9, { name: 'overcurrent2', desc: 'Overcurrent condition 2' }],
|
|
428
|
+
[10, { name: 'overvoltage', desc: 'Overvoltage condition' }],
|
|
429
|
+
[11, { name: 'error11', desc: 'Unspecified Error 11' }],
|
|
430
|
+
[12, { name: 'error12', desc: 'Unspecified Error 12' }],
|
|
431
|
+
[13, { name: 'error13', desc: 'Unspecified Error 13' }],
|
|
432
|
+
[14, { name: 'error14', desc: 'Unspecified Error 14' }],
|
|
433
|
+
[15, { name: 'error15', desc: 'Unspecified Error 15' }],
|
|
434
|
+
[16, { name: 'commfailure', desc: 'Communication failure' }]
|
|
435
|
+
]);
|
|
436
|
+
public pumpUnits: byteValueMap = new byteValueMap([
|
|
437
|
+
[0, { name: 'rpm', desc: 'RPM' }],
|
|
438
|
+
[1, { name: 'gpm', desc: 'GPM' }]
|
|
439
|
+
]);
|
|
440
|
+
public bodyTypes: byteValueMap = new byteValueMap([
|
|
441
|
+
[0, { name: 'pool', desc: 'Pool' }],
|
|
442
|
+
[1, { name: 'spa', desc: 'Spa' }],
|
|
443
|
+
[2, { name: 'spa', desc: 'Spa' }],
|
|
444
|
+
[3, { name: 'spa', desc: 'Spa' }]
|
|
445
|
+
]);
|
|
446
|
+
public bodies: byteValueMap = new byteValueMap([
|
|
447
|
+
[0, { name: 'pool', desc: 'Pool' }],
|
|
448
|
+
[1, { name: 'spa', desc: 'Spa' }],
|
|
449
|
+
[2, { name: 'body3', desc: 'Body 3' }],
|
|
450
|
+
[3, { name: 'body4', desc: 'Body 4' }],
|
|
451
|
+
[32, { name: 'poolspa', desc: 'Pool/Spa' }]
|
|
452
|
+
]);
|
|
453
|
+
public chlorinatorStatus: byteValueMap = new byteValueMap([
|
|
454
|
+
[0, { name: 'ok', desc: 'Ok' }],
|
|
455
|
+
[1, { name: 'lowflow', desc: 'Low Flow' }],
|
|
456
|
+
[2, { name: 'lowsalt', desc: 'Low Salt' }],
|
|
457
|
+
[3, { name: 'verylowsalt', desc: 'Very Low Salt' }],
|
|
458
|
+
[4, { name: 'highcurrent', desc: 'High Current' }],
|
|
459
|
+
[5, { name: 'clean', desc: 'Clean Cell' }],
|
|
460
|
+
[6, { name: 'lowvoltage', desc: 'Low Voltage' }],
|
|
461
|
+
[7, { name: 'lowtemp', desc: 'Water Temp Low' }],
|
|
462
|
+
[8, { name: 'commlost', desc: 'Communication Lost' }]
|
|
463
|
+
]);
|
|
464
|
+
public chlorinatorType: byteValueMap = new byteValueMap([
|
|
465
|
+
[0, { name: 'pentair', desc: 'Pentair' }],
|
|
466
|
+
[1, { name: 'unknown', desc: 'unknown' }],
|
|
467
|
+
[2, { name: 'aquarite', desc: 'Aquarite' }],
|
|
468
|
+
[3, { name: 'unknown', desc: 'unknown' }]
|
|
469
|
+
]);
|
|
470
|
+
public chlorinatorModel: byteValueMap = new byteValueMap([
|
|
471
|
+
[0, { name: 'unknown', desc: 'unknown', capacity: 0, chlorinePerDay: 0, chlorinePerSec: 0 }],
|
|
472
|
+
[1, { name: 'intellichlor--15', desc: 'IntelliChlor IC15', capacity: 15000, chlorinePerDay: 0.60, chlorinePerSec: 0.60 / 86400 }],
|
|
473
|
+
[2, { name: 'intellichlor--20', desc: 'IntelliChlor IC20', capacity: 20000, chlorinePerDay: 0.70, chlorinePerSec: 0.70 / 86400 }],
|
|
474
|
+
[3, { name: 'intellichlor--40', desc: 'IntelliChlor IC40', capacity: 40000, chlorinePerDay: 1.40, chlorinePerSec: 1.4 / 86400 }],
|
|
475
|
+
[4, { name: 'intellichlor--60', desc: 'IntelliChlor IC60', capacity: 60000, chlorinePerDay: 2, chlorinePerSec: 2 / 86400 }],
|
|
476
|
+
[5, { name: 'aquarite-t15', desc: 'AquaRite T15', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }],
|
|
477
|
+
[6, { name: 'aquarite-t9', desc: 'AquaRite T9', capacity: 30000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }],
|
|
478
|
+
[7, { name: 'aquarite-t5', desc: 'AquaRite T5', capacity: 20000, chlorinePerDay: 0.735, chlorinePerSec: 0.735 / 86400 }],
|
|
479
|
+
[8, { name: 'aquarite-t3', desc: 'AquaRite T3', capacity: 15000, chlorinePerDay: 0.53, chlorinePerSec: 0.53 / 86400 }],
|
|
480
|
+
[9, { name: 'aquarite-925', desc: 'AquaRite 925', capacity: 25000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }],
|
|
481
|
+
[10, { name: 'aquarite-940', desc: 'AquaRite 940', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }]
|
|
482
|
+
])
|
|
483
|
+
public customNames: byteValueMap = new byteValueMap();
|
|
484
|
+
public circuitNames: byteValueMap = new byteValueMap();
|
|
485
|
+
public scheduleTypes: byteValueMap = new byteValueMap([
|
|
486
|
+
[0, { name: 'runonce', desc: 'Run Once', startDate: true, startTime: true, endTime: true, days: false, heatSource: true, heatSetpoint: true }],
|
|
487
|
+
[128, { name: 'repeat', desc: 'Repeats', startDate: false, startTime: true, endTime: true, days: 'multi', heatSource: true, heatSetpoint: true }]
|
|
488
|
+
]);
|
|
489
|
+
public circuitGroupTypes: byteValueMap = new byteValueMap([
|
|
490
|
+
[0, { name: 'none', desc: 'Unspecified' }],
|
|
491
|
+
[1, { name: 'light', desc: 'Light' }],
|
|
492
|
+
[2, { name: 'circuit', desc: 'Circuit' }],
|
|
493
|
+
[3, { name: 'intellibrite', desc: 'IntelliBrite' }]
|
|
494
|
+
]);
|
|
495
|
+
public groupCircuitStates: byteValueMap = new byteValueMap([
|
|
496
|
+
[0, { name: 'off', desc: 'Off' }],
|
|
497
|
+
[1, { name: 'on', desc: 'On' }]
|
|
498
|
+
]);
|
|
499
|
+
public systemUnits: byteValueMap = new byteValueMap([
|
|
500
|
+
[0, { name: 'english', desc: 'English' }],
|
|
501
|
+
[4, { name: 'metric', desc: 'Metric' }]
|
|
502
|
+
]);
|
|
503
|
+
public tempUnits: byteValueMap = new byteValueMap([
|
|
504
|
+
[0, { name: 'F', desc: 'Fahrenheit' }],
|
|
505
|
+
[4, { name: 'C', desc: 'Celsius' }]
|
|
506
|
+
]);
|
|
507
|
+
public valveTypes: byteValueMap = new byteValueMap([
|
|
508
|
+
[0, { name: 'standard', desc: 'Standard' }],
|
|
509
|
+
[1, { name: 'intellivalve', desc: 'IntelliValve' }]
|
|
510
|
+
]);
|
|
511
|
+
public valveModes: byteValueMap = new byteValueMap([
|
|
512
|
+
[0, { name: 'off', desc: 'Off' }],
|
|
513
|
+
[1, { name: 'pool', desc: 'Pool' }],
|
|
514
|
+
[2, { name: 'spa', dest: 'Spa' }],
|
|
515
|
+
[3, { name: 'spillway', desc: 'Spillway' }],
|
|
516
|
+
[4, { name: 'spadrain', desc: 'Spa Drain' }]
|
|
517
|
+
]);
|
|
474
518
|
public intellibriteActions: byteValueMap = new byteValueMap([
|
|
475
519
|
[0, { name: 'ready', desc: 'Ready' }],
|
|
476
520
|
[1, { name: 'sync', desc: 'Synchronizing' }],
|
|
@@ -487,7 +531,7 @@ export class byteValueMaps {
|
|
|
487
531
|
[0, { name: 'none', desc: 'None', ph: { min: 6.8, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: false }],
|
|
488
532
|
[1, { name: 'unknown', desc: 'Unknown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }],
|
|
489
533
|
[2, { name: 'intellichem', desc: 'IntelliChem', ph: { min: 7.2, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: true }],
|
|
490
|
-
[3, { name: 'homegrown', desc: 'Homegrown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }],
|
|
534
|
+
// [3, { name: 'homegrown', desc: 'Homegrown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }],
|
|
491
535
|
[4, { name: 'rem', desc: 'REM Chem', ph: { min: 6.8, max: 8.0 }, hasAddress: false }]
|
|
492
536
|
]);
|
|
493
537
|
public siCalcTypes: byteValueMap = new byteValueMap([
|
|
@@ -538,6 +582,14 @@ export class byteValueMaps {
|
|
|
538
582
|
[6, { name: 'qt', desc: 'Quarts' }],
|
|
539
583
|
[7, { name: 'pt', desc: 'Pints' }]
|
|
540
584
|
]);
|
|
585
|
+
public pressureUnits: byteValueMap = new byteValueMap([
|
|
586
|
+
[0, { name: 'psi', desc: 'Pounds per Sqare Inch' }],
|
|
587
|
+
[1, { name: 'Pa', desc: 'Pascal' }],
|
|
588
|
+
[2, { name: 'kPa', desc: 'Kilo-pascals' }],
|
|
589
|
+
[3, { name: 'atm', desc: 'Atmospheres' }],
|
|
590
|
+
[4, { name: 'bar', desc: 'Barometric' }]
|
|
591
|
+
]);
|
|
592
|
+
|
|
541
593
|
public areaUnits: byteValueMap = new byteValueMap([
|
|
542
594
|
[0, { name: '', desc: 'No Units' }],
|
|
543
595
|
[1, { name: 'sqft', desc: 'Square Feet' }],
|
|
@@ -774,29 +826,37 @@ export class SystemBoard {
|
|
|
774
826
|
/// This method processes the status message periodically. The role of this method is to verify the circuit, valve, and heater
|
|
775
827
|
/// relays. This method does not control RS485 operations such as pumps and chlorinators. These are done through the respective
|
|
776
828
|
/// equipment polling functions.
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
829
|
+
public async processStatusAsync() {
|
|
830
|
+
let self = this;
|
|
831
|
+
try {
|
|
832
|
+
if (this._statusCheckRef > 0) return;
|
|
833
|
+
this.suspendStatus(true);
|
|
834
|
+
if (typeof this._statusTimer !== 'undefined' && this._statusTimer) clearTimeout(this._statusTimer);
|
|
835
|
+
// Go through all the assigned equipment and verify the current state.
|
|
836
|
+
sys.board.system.keepManualTime();
|
|
837
|
+
await sys.board.bodies.syncFreezeProtection();
|
|
838
|
+
await sys.board.syncEquipmentItems();
|
|
839
|
+
await sys.board.schedules.syncScheduleStates();
|
|
840
|
+
await sys.board.circuits.checkEggTimerExpirationAsync();
|
|
841
|
+
state.emitControllerChange();
|
|
842
|
+
state.emitEquipmentChanges();
|
|
843
|
+
} catch (err) { state.status = 255; logger.error(`Error performing processStatusAsync ${err.message}`); }
|
|
844
|
+
finally {
|
|
845
|
+
this.suspendStatus(false);
|
|
846
|
+
if (this.statusInterval > 0) this._statusTimer = setTimeout(async () => await self.processStatusAsync(), this.statusInterval);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
public async syncEquipmentItems() {
|
|
850
|
+
try {
|
|
851
|
+
await sys.board.circuits.syncCircuitRelayStates();
|
|
852
|
+
await sys.board.features.syncGroupStates();
|
|
853
|
+
await sys.board.circuits.syncVirtualCircuitStates();
|
|
854
|
+
await sys.board.valves.syncValveStates();
|
|
855
|
+
await sys.board.filters.syncFilterStates();
|
|
856
|
+
await sys.board.heaters.syncHeaterStates();
|
|
799
857
|
}
|
|
858
|
+
catch (err) { logger.error(`Error synchronizing equipment items: ${err.message}`); }
|
|
859
|
+
}
|
|
800
860
|
public async setControllerType(obj): Promise<Equipment> {
|
|
801
861
|
try {
|
|
802
862
|
if (obj.controllerType !== sys.controllerType)
|
|
@@ -874,6 +934,76 @@ export class BoardCommands {
|
|
|
874
934
|
constructor(parent: SystemBoard) { this.board = parent; }
|
|
875
935
|
}
|
|
876
936
|
export class SystemCommands extends BoardCommands {
|
|
937
|
+
public async restore(rest: { poolConfig: any, poolState: any }): Promise<RestoreResults> {
|
|
938
|
+
let res = new RestoreResults();
|
|
939
|
+
try {
|
|
940
|
+
let ctx = await sys.board.system.validateRestore(rest);
|
|
941
|
+
// Restore the general stuff.
|
|
942
|
+
if (ctx.general.update.length > 0) await sys.board.system.setGeneralAsync(ctx.general.update[0]);
|
|
943
|
+
for (let i = 0; i < ctx.customNames.update.length; i++) {
|
|
944
|
+
let cn = ctx.customNames.update[i];
|
|
945
|
+
try {
|
|
946
|
+
await sys.board.system.setCustomNameAsync(cn);
|
|
947
|
+
res.addModuleSuccess('customName', `Update: ${cn.id}-${cn.name}`);
|
|
948
|
+
} catch (err) { res.addModuleError('customName', `Update: ${cn.id}-${cn.name}: ${err.message}`); }
|
|
949
|
+
}
|
|
950
|
+
for (let i = 0; i < ctx.customNames.add.length; i++) {
|
|
951
|
+
let cn = ctx.customNames.add[i];
|
|
952
|
+
try {
|
|
953
|
+
await sys.board.system.setCustomNameAsync(cn);
|
|
954
|
+
res.addModuleSuccess('customName', `Add: ${cn.id}-${cn.name}`);
|
|
955
|
+
} catch (err) { res.addModuleError('customName', `Add: ${cn.id}-${cn.name}: ${err.message}`); }
|
|
956
|
+
}
|
|
957
|
+
await sys.board.bodies.restore(rest, ctx, res);
|
|
958
|
+
await sys.board.filters.restore(rest, ctx, res);
|
|
959
|
+
await sys.board.circuits.restore(rest, ctx, res);
|
|
960
|
+
await sys.board.heaters.restore(rest, ctx, res);
|
|
961
|
+
await sys.board.features.restore(rest, ctx, res);
|
|
962
|
+
await sys.board.pumps.restore(rest, ctx, res);
|
|
963
|
+
await sys.board.valves.restore(rest, ctx, res);
|
|
964
|
+
await sys.board.chlorinator.restore(rest, ctx, res);
|
|
965
|
+
await sys.board.chemControllers.restore(rest, ctx, res);
|
|
966
|
+
await sys.board.schedules.restore(rest, ctx, res);
|
|
967
|
+
return res;
|
|
968
|
+
//await sys.board.covers.restore(rest, ctx);
|
|
969
|
+
} catch (err) { logger.error(`Error restoring njsPC server: ${err.message}`); res.addModuleError('system', err.message); return Promise.reject(err);}
|
|
970
|
+
}
|
|
971
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<any> {
|
|
972
|
+
try {
|
|
973
|
+
let ctx: any = { board: { errors: [], warnings: [] } };
|
|
974
|
+
|
|
975
|
+
// Step 1 - Verify that the boards are the same. For instance you do not want to restore an IntelliTouch to an IntelliCenter.
|
|
976
|
+
let cfg = rest.poolConfig;
|
|
977
|
+
if (sys.controllerType === cfg.controllerType) {
|
|
978
|
+
ctx.customNames = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
979
|
+
let customNames = sys.customNames.get();
|
|
980
|
+
for (let i = 0; i < rest.poolConfig.customNames.length; i++) {
|
|
981
|
+
let cn = customNames.find(elem => elem.id === rest.poolConfig.customNames[i].id);
|
|
982
|
+
if (typeof cn === 'undefined') ctx.customNames.add.push(rest.poolConfig.customNames[i]);
|
|
983
|
+
else if (JSON.stringify(rest.poolConfig.customNames[i]) !== JSON.stringify(cn)) ctx.customNames.update.push(cn);
|
|
984
|
+
}
|
|
985
|
+
ctx.general = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
986
|
+
if (JSON.stringify(sys.general.get()) !== JSON.stringify(cfg.pool)) ctx.general.update.push(cfg.pool);
|
|
987
|
+
ctx.bodies = await sys.board.bodies.validateRestore(rest);
|
|
988
|
+
ctx.pumps = await sys.board.pumps.validateRestore(rest);
|
|
989
|
+
await sys.board.circuits.validateRestore(rest, ctx);
|
|
990
|
+
ctx.features = await sys.board.features.validateRestore(rest);
|
|
991
|
+
ctx.chlorinators = await sys.board.chlorinator.validateRestore(rest);
|
|
992
|
+
ctx.heaters = await sys.board.heaters.validateRestore(rest);
|
|
993
|
+
ctx.valves = await sys.board.valves.validateRestore(rest);
|
|
994
|
+
|
|
995
|
+
//ctx.covers = await sys.board.covers.validateRestore(rest);
|
|
996
|
+
ctx.chemControllers = await sys.board.chemControllers.validateRestore(rest);
|
|
997
|
+
ctx.filters = await sys.board.filters.validateRestore(rest);
|
|
998
|
+
ctx.schedules = await sys.board.schedules.validateRestore(rest);
|
|
999
|
+
}
|
|
1000
|
+
else ctx.board.errors.push(`Panel Types do not match cannot restore bakup from ${sys.controllerType} to ${rest.poolConfig.controllerType}`);
|
|
1001
|
+
|
|
1002
|
+
return ctx;
|
|
1003
|
+
|
|
1004
|
+
} catch (err) { logger.error(`Error validating restore file: ${err.message}`); return Promise.reject(err);}
|
|
1005
|
+
|
|
1006
|
+
}
|
|
877
1007
|
public cancelDelay(): Promise<any> { state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); }
|
|
878
1008
|
public setDateTimeAsync(obj: any): Promise<any> { return Promise.resolve(); }
|
|
879
1009
|
public keepManualTime() {
|
|
@@ -938,6 +1068,9 @@ export class SystemCommands extends BoardCommands {
|
|
|
938
1068
|
if (obj.clockSource === 'server') sys.board.system.setTZ();
|
|
939
1069
|
sys.board.system.setTempSensorsAsync(obj);
|
|
940
1070
|
sys.general.options.set(obj);
|
|
1071
|
+
let bodyUnits = sys.general.options.units === 0 ? 1 : 2;
|
|
1072
|
+
for (let i = 0; i < sys.bodies.length; i++) sys.bodies.getItemByIndex(i).capacityUnits = bodyUnits;
|
|
1073
|
+
state.temps.units = sys.general.options.units === 0 ? 1 : 4;
|
|
941
1074
|
return new Promise<Options>(function (resolve, reject) { resolve(sys.general.options); });
|
|
942
1075
|
}
|
|
943
1076
|
public async setLocationAsync(obj: any): Promise<Location> {
|
|
@@ -968,7 +1101,10 @@ export class SystemCommands extends BoardCommands {
|
|
|
968
1101
|
state.temps.waterSensor1 = sys.equipment.tempSensors.getCalibration('water1') + temp;
|
|
969
1102
|
let body = state.temps.bodies.getItemById(1);
|
|
970
1103
|
if (body.isOn) body.temp = state.temps.waterSensor1;
|
|
971
|
-
|
|
1104
|
+
else if (sys.equipment.shared) {
|
|
1105
|
+
body = state.temps.bodies.getItemById(2);
|
|
1106
|
+
if (body.isOn) body.temp = state.temps.waterSensor1;
|
|
1107
|
+
}
|
|
972
1108
|
}
|
|
973
1109
|
break;
|
|
974
1110
|
case 'waterSensor2':
|
|
@@ -1141,17 +1277,182 @@ export class SystemCommands extends BoardCommands {
|
|
|
1141
1277
|
}
|
|
1142
1278
|
}
|
|
1143
1279
|
export class BodyCommands extends BoardCommands {
|
|
1280
|
+
public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
|
|
1281
|
+
try {
|
|
1282
|
+
// First delete the bodies that should be removed.
|
|
1283
|
+
for (let i = 0; i < ctx.bodies.remove.length; i++) {
|
|
1284
|
+
let body = ctx.bodies.remove[i];
|
|
1285
|
+
try {
|
|
1286
|
+
sys.bodies.removeItemById(body.id);
|
|
1287
|
+
state.temps.bodies.removeItemById(body.id);
|
|
1288
|
+
res.addModuleSuccess('body', `Remove: ${body.id}-${body.name}`);
|
|
1289
|
+
} catch (err) { res.addModuleError('body', `Remove: ${body.id}-${body.name}: ${err.message}`); }
|
|
1290
|
+
}
|
|
1291
|
+
for (let i = 0; i < ctx.bodies.update.length; i++) {
|
|
1292
|
+
let body = ctx.bodies.update[i];
|
|
1293
|
+
try {
|
|
1294
|
+
await sys.board.bodies.setBodyAsync(body);
|
|
1295
|
+
res.addModuleSuccess('body', `Update: ${body.id}-${body.name}`);
|
|
1296
|
+
} catch (err) { res.addModuleError('body', `Update: ${body.id}-${body.name}: ${err.message}`); }
|
|
1297
|
+
}
|
|
1298
|
+
for (let i = 0; i < ctx.bodies.add.length; i++) {
|
|
1299
|
+
let body = ctx.bodies.add[i];
|
|
1300
|
+
try {
|
|
1301
|
+
// pull a little trick to first add the data then perform the update.
|
|
1302
|
+
sys.bodies.getItemById(body.id, true);
|
|
1303
|
+
await sys.board.bodies.setBodyAsync(body);
|
|
1304
|
+
} catch (err) { res.addModuleError('body', `Add: ${body.id}-${body.name}: ${err.message}`); }
|
|
1305
|
+
}
|
|
1306
|
+
return true;
|
|
1307
|
+
} catch (err) { logger.error(`Error restoring bodies: ${err.message}`); res.addModuleError('system', `Error restoring bodies: ${err.message}`); return false; }
|
|
1308
|
+
}
|
|
1309
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any}> {
|
|
1310
|
+
try {
|
|
1311
|
+
let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
1312
|
+
// Look at bodies.
|
|
1313
|
+
let cfg = rest.poolConfig;
|
|
1314
|
+
for (let i = 0; i < cfg.bodies.length; i++) {
|
|
1315
|
+
let r = cfg.bodies[i];
|
|
1316
|
+
let c = sys.bodies.find(elem => r.id === elem.id);
|
|
1317
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
1318
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
1319
|
+
}
|
|
1320
|
+
for (let i = 0; i < sys.bodies.length; i++) {
|
|
1321
|
+
let c = sys.bodies.getItemByIndex(i);
|
|
1322
|
+
let r = cfg.bodies.find(elem => elem.id == c.id);
|
|
1323
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
1324
|
+
}
|
|
1325
|
+
return ctx;
|
|
1326
|
+
} catch (err) { logger.error(`Error validating bodies for restore: ${err.message}`); }
|
|
1327
|
+
}
|
|
1328
|
+
public freezeProtectBodyOn: Date;
|
|
1329
|
+
public freezeProtectStart: Date;
|
|
1330
|
+
public async syncFreezeProtection() {
|
|
1331
|
+
try {
|
|
1332
|
+
// Go through all the features and circuits to make sure we have the freeze protect set appropriately. The freeze
|
|
1333
|
+
// flag will have already been set whether this is a Nixie setup or there is an OCP involved.
|
|
1334
|
+
|
|
1335
|
+
// First turn on/off any features that are in our control that should be under our control. If this is an OCP we
|
|
1336
|
+
// do not create features beyond those controlled by the OCP so we don't need to check these in that condition. That is
|
|
1337
|
+
// why it first checks the controller type.
|
|
1338
|
+
let freeze = utils.makeBool(state.freeze);
|
|
1339
|
+
if (sys.controllerType === ControllerType.Nixie) {
|
|
1340
|
+
// If we are a Nixie controller we need to evaluate the current freeze settings against the air temperature.
|
|
1341
|
+
if (typeof state.temps.air !== 'undefined') freeze = state.temps.air <= sys.general.options.freezeThreshold;
|
|
1342
|
+
else freeze = false;
|
|
1343
|
+
|
|
1344
|
+
// We need to know when we first turned the freeze protection on. This is because we will be rotating between pool and spa
|
|
1345
|
+
// on shared body systems when both pool and spa have freeze protection checked.
|
|
1346
|
+
if (state.freeze !== freeze) {
|
|
1347
|
+
this.freezeProtectStart = freeze ? new Date() : undefined;
|
|
1348
|
+
state.freeze = freeze;
|
|
1349
|
+
}
|
|
1350
|
+
for (let i = 0; i < sys.features.length; i++) {
|
|
1351
|
+
let feature = sys.features.getItemByIndex(i);
|
|
1352
|
+
let fstate = state.features.getItemById(feature.id, true);
|
|
1353
|
+
if (!feature.freeze || !feature.isActive === true || feature.master !== 1) {
|
|
1354
|
+
fstate.freezeProtect = false;
|
|
1355
|
+
continue; // This is not affected by freeze conditions.
|
|
1356
|
+
}
|
|
1357
|
+
if (freeze && !fstate.isOn) {
|
|
1358
|
+
// This feature should be on because we are freezing.
|
|
1359
|
+
fstate.freezeProtect = true;
|
|
1360
|
+
await sys.board.features.setFeatureStateAsync(feature.id, true);
|
|
1361
|
+
}
|
|
1362
|
+
else if (!freeze && fstate.freezeProtect) {
|
|
1363
|
+
// This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
|
|
1364
|
+
fstate.freezeProtect = false;
|
|
1365
|
+
await sys.board.features.setFeatureStateAsync(feature.id, false);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
let bodyRotationChecked = false;
|
|
1370
|
+
for (let i = 0; i < sys.circuits.length; i++) {
|
|
1371
|
+
let circ = sys.circuits.getItemByIndex(i);
|
|
1372
|
+
let cstate = state.circuits.getItemById(circ.id);
|
|
1373
|
+
if (!circ.freeze || !circ.isActive === true || circ.master !== 1) {
|
|
1374
|
+
cstate.freezeProtect = false;
|
|
1375
|
+
continue; // This is not affected by freeze conditions.
|
|
1376
|
+
}
|
|
1377
|
+
if (sys.equipment.shared && freeze && (circ.id === 1 || circ.id === 6)) {
|
|
1378
|
+
// Exit out of here because we already checked the body rotation. We only want to do this once since it can be expensive turning
|
|
1379
|
+
// on a particular body.
|
|
1380
|
+
if (bodyRotationChecked) continue;
|
|
1381
|
+
// These are our body circuits so we need to check to see if they need to be rotated between pool and spa.
|
|
1382
|
+
let pool = circ.id === 6 ? circ : sys.circuits.getItemById(6);
|
|
1383
|
+
let spa = circ.id === 1 ? circ : sys.circuits.getItemById(1);
|
|
1384
|
+
if (pool.freeze && spa.freeze) {
|
|
1385
|
+
// We only need to rotate between pool and spa when they are both checked.
|
|
1386
|
+
let pstate = circ.id === 6 ? cstate : state.circuits.getItemById(6);
|
|
1387
|
+
let sstate = circ.id === 1 ? cstate : state.circuits.getItemById(1);
|
|
1388
|
+
if (!pstate.isOn && !sstate.isOn) {
|
|
1389
|
+
// Neither the pool or spa are on so we will turn on the pool first.
|
|
1390
|
+
pstate.freezeProtect = true;
|
|
1391
|
+
this.freezeProtectBodyOn = new Date();
|
|
1392
|
+
await sys.board.circuits.setCircuitStateAsync(6, true);
|
|
1393
|
+
}
|
|
1394
|
+
else {
|
|
1395
|
+
// If neither of the bodies were turned on for freeze protection then we need to ignore this.
|
|
1396
|
+
if (!pstate.freezeProtect && !sstate.freezeProtect) {
|
|
1397
|
+
this.freezeProtectBodyOn = undefined;
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// One of the two bodies is on so we need to check for the rotation. If it is time to rotate do the rotation.
|
|
1402
|
+
if (typeof this.freezeProtectBodyOn === 'undefined') this.freezeProtectBodyOn = new Date();
|
|
1403
|
+
let dt = new Date().getTime();
|
|
1404
|
+
if (dt - 1000 * 60 * 15 > this.freezeProtectBodyOn.getTime()) {
|
|
1405
|
+
logger.info(`Swapping bodies for freeze protection pool:${pstate.isOn} spa:${sstate.isOn} interval: ${utils.formatDuration(dt - this.freezeProtectBodyOn.getTime() / 1000)}`);
|
|
1406
|
+
// 10 minutes has elapsed so we will be rotating to the other body.
|
|
1407
|
+
if (pstate.isOn) {
|
|
1408
|
+
// The setCircuitState method will handle turning off the pool body.
|
|
1409
|
+
sstate.freezeProtect = true;
|
|
1410
|
+
pstate.freezeProtect = false;
|
|
1411
|
+
await sys.board.circuits.setCircuitStateAsync(1, true);
|
|
1412
|
+
}
|
|
1413
|
+
else {
|
|
1414
|
+
sstate.freezeProtect = false;
|
|
1415
|
+
pstate.freezeProtect = true;
|
|
1416
|
+
await sys.board.circuits.setCircuitStateAsync(6, true);
|
|
1417
|
+
}
|
|
1418
|
+
// Set a new date as this will be our rotation check now.
|
|
1419
|
+
this.freezeProtectBodyOn = new Date();
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
// Only this circuit is selected for freeze protection so we don't need any special treatment.
|
|
1425
|
+
cstate.freezeProtect = true;
|
|
1426
|
+
if (!cstate.isOn) await sys.board.circuits.setCircuitStateAsync(circ.id, true);
|
|
1427
|
+
}
|
|
1428
|
+
bodyRotationChecked = true;
|
|
1429
|
+
}
|
|
1430
|
+
else if (freeze && !cstate.isOn) {
|
|
1431
|
+
// This circuit should be on because we are freezing.
|
|
1432
|
+
cstate.freezeProtect = true;
|
|
1433
|
+
await sys.board.circuits.setCircuitStateAsync(circ.id, true);
|
|
1434
|
+
}
|
|
1435
|
+
else if (!freeze && cstate.freezeProtect) {
|
|
1436
|
+
// This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
|
|
1437
|
+
await sys.board.circuits.setCircuitStateAsync(circ.id, false);
|
|
1438
|
+
cstate.freezeProtect = false;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
catch (err) { logger.error(`syncFreezeProtection: Error synchronizing freeze protection states: ${err.message}`); }
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1144
1445
|
public async initFilters() {
|
|
1145
1446
|
try {
|
|
1146
1447
|
let filter: Filter;
|
|
1147
1448
|
let sFilter: FilterState;
|
|
1148
1449
|
if (sys.equipment.maxBodies > 0) {
|
|
1149
1450
|
filter = sys.filters.getItemById(1, true, { filterType: 3, name: sys.equipment.shared ? 'Filter' : 'Filter 1' });
|
|
1150
|
-
sFilter = state.filters.getItemById(1, true, { name: filter.name });
|
|
1451
|
+
sFilter = state.filters.getItemById(1, true, { id: 1, name: filter.name });
|
|
1151
1452
|
filter.isActive = true;
|
|
1152
1453
|
filter.master = sys.board.equipmentMaster;
|
|
1153
1454
|
filter.body = sys.equipment.shared ? sys.board.valueMaps.bodies.transformByName('poolspa') : 0;
|
|
1154
|
-
sFilter = state.filters.getItemById(1, true);
|
|
1455
|
+
//sFilter = state.filters.getItemById(1, true);
|
|
1155
1456
|
sFilter.body = filter.body;
|
|
1156
1457
|
sFilter.filterType = filter.filterType;
|
|
1157
1458
|
sFilter.name = filter.name;
|
|
@@ -1277,53 +1578,54 @@ export class BodyCommands extends BoardCommands {
|
|
|
1277
1578
|
sys.board.heaters.syncHeaterStates();
|
|
1278
1579
|
return Promise.resolve(bstate);
|
|
1279
1580
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1581
|
+
public getHeatSources(bodyId: number) {
|
|
1582
|
+
let heatSources = [];
|
|
1583
|
+
let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
|
|
1584
|
+
heatSources.push(this.board.valueMaps.heatSources.transformByName('nochange'));
|
|
1585
|
+
if (heatTypes.total > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('off'));
|
|
1586
|
+
if (heatTypes.gas > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('heater'));
|
|
1587
|
+
if (heatTypes.mastertemp > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('mastertemp'));
|
|
1588
|
+
if (heatTypes.solar > 0) {
|
|
1589
|
+
let hm = this.board.valueMaps.heatSources.transformByName('solar');
|
|
1590
|
+
heatSources.push(hm);
|
|
1591
|
+
if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('solarpref'));
|
|
1592
|
+
}
|
|
1593
|
+
if (heatTypes.heatpump > 0) {
|
|
1594
|
+
let hm = this.board.valueMaps.heatSources.transformByName('heatpump');
|
|
1595
|
+
heatSources.push(hm);
|
|
1596
|
+
if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('heatpumppref'));
|
|
1597
|
+
}
|
|
1598
|
+
if (heatTypes.ultratemp > 0) {
|
|
1599
|
+
let hm = this.board.valueMaps.heatSources.transformByName('ultratemp');
|
|
1600
|
+
heatSources.push(hm);
|
|
1601
|
+
if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('ultratemppref'));
|
|
1602
|
+
}
|
|
1603
|
+
return heatSources;
|
|
1604
|
+
}
|
|
1605
|
+
public getHeatModes(bodyId: number) {
|
|
1606
|
+
let heatModes = [];
|
|
1607
|
+
// RKS: 09-26-20 This will need to be overloaded in IntelliCenterBoard when the other heater types are identified. (e.g. ultratemp, hybrid, maxetherm, and mastertemp)
|
|
1608
|
+
heatModes.push(this.board.valueMaps.heatModes.transformByName('off')); // In IC fw 1.047 off is no longer 0.
|
|
1609
|
+
let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
|
|
1610
|
+
if (heatTypes.gas > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('heater'));
|
|
1611
|
+
if (heatTypes.mastertemp > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('mtheater'));
|
|
1612
|
+
if (heatTypes.solar > 0) {
|
|
1613
|
+
let hm = this.board.valueMaps.heatModes.transformByName('solar');
|
|
1614
|
+
heatModes.push(hm);
|
|
1615
|
+
if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('solarpref'));
|
|
1616
|
+
}
|
|
1617
|
+
if (heatTypes.heatpump > 0) {
|
|
1618
|
+
let hm = this.board.valueMaps.heatModes.transformByName('heatpump');
|
|
1619
|
+
heatModes.push(hm);
|
|
1620
|
+
if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('heatpumppref'));
|
|
1621
|
+
}
|
|
1622
|
+
if (heatTypes.ultratemp > 0) {
|
|
1623
|
+
let hm = this.board.valueMaps.heatModes.transformByName('ultratemp');
|
|
1624
|
+
heatModes.push(hm);
|
|
1625
|
+
if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('ultratemppref'));
|
|
1626
|
+
}
|
|
1627
|
+
return heatModes;
|
|
1628
|
+
}
|
|
1327
1629
|
public getPoolStates(): BodyTempState[] {
|
|
1328
1630
|
let arrPools = [];
|
|
1329
1631
|
for (let i = 0; i < state.temps.bodies.length; i++) {
|
|
@@ -1343,54 +1645,105 @@ export class BodyCommands extends BoardCommands {
|
|
|
1343
1645
|
}
|
|
1344
1646
|
return arrSpas;
|
|
1345
1647
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1648
|
+
public getBodyState(bodyCode: number): BodyTempState {
|
|
1649
|
+
let assoc = sys.board.valueMaps.bodies.transform(bodyCode);
|
|
1650
|
+
switch (assoc.name) {
|
|
1651
|
+
case 'body1':
|
|
1652
|
+
case 'pool':
|
|
1653
|
+
return state.temps.bodies.getItemById(1);
|
|
1654
|
+
case 'body2':
|
|
1655
|
+
case 'spa':
|
|
1656
|
+
return state.temps.bodies.getItemById(2);
|
|
1657
|
+
case 'body3':
|
|
1658
|
+
return state.temps.bodies.getItemById(3);
|
|
1659
|
+
case 'body4':
|
|
1660
|
+
return state.temps.bodies.getItemById(4);
|
|
1661
|
+
case 'poolspa':
|
|
1662
|
+
if (sys.equipment.shared && sys.equipment.maxBodies >= 2) {
|
|
1663
|
+
let body = state.temps.bodies.getItemById(1);
|
|
1664
|
+
if (body.isOn) return body;
|
|
1665
|
+
body = state.temps.bodies.getItemById(2);
|
|
1666
|
+
if (body.isOn) return body;
|
|
1667
|
+
return state.temps.bodies.getItemById(1);
|
|
1668
|
+
}
|
|
1669
|
+
else
|
|
1670
|
+
return state.temps.bodies.getItemById(1);
|
|
1366
1671
|
}
|
|
1367
|
-
else
|
|
1368
|
-
return state.temps.bodies.getItemById(1);
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
public isBodyOn(bodyCode: number): boolean {
|
|
1372
|
-
let assoc = sys.board.valueMaps.bodies.transform(bodyCode);
|
|
1373
|
-
switch (assoc.name) {
|
|
1374
|
-
case 'body1':
|
|
1375
|
-
case 'pool':
|
|
1376
|
-
return state.temps.bodies.getItemById(1).isOn;
|
|
1377
|
-
case 'body2':
|
|
1378
|
-
case 'spa':
|
|
1379
|
-
return state.temps.bodies.getItemById(2).isOn;
|
|
1380
|
-
case 'body3':
|
|
1381
|
-
return state.temps.bodies.getItemById(3).isOn;
|
|
1382
|
-
case 'body4':
|
|
1383
|
-
return state.temps.bodies.getItemById(4).isOn;
|
|
1384
|
-
case 'poolspa':
|
|
1385
|
-
if (sys.equipment.shared && sys.equipment.maxBodies >= 2)
|
|
1386
|
-
return state.temps.bodies.getItemById(1).isOn || state.temps.bodies.getItemById(2).isOn;
|
|
1387
|
-
else
|
|
1388
|
-
return state.temps.bodies.getItemById(1).isOn;
|
|
1389
1672
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1673
|
+
public isBodyOn(bodyCode: number): boolean {
|
|
1674
|
+
let assoc = sys.board.valueMaps.bodies.transform(bodyCode);
|
|
1675
|
+
switch (assoc.name) {
|
|
1676
|
+
case 'body1':
|
|
1677
|
+
case 'pool':
|
|
1678
|
+
return state.temps.bodies.getItemById(1).isOn;
|
|
1679
|
+
case 'body2':
|
|
1680
|
+
case 'spa':
|
|
1681
|
+
return state.temps.bodies.getItemById(2).isOn;
|
|
1682
|
+
case 'body3':
|
|
1683
|
+
return state.temps.bodies.getItemById(3).isOn;
|
|
1684
|
+
case 'body4':
|
|
1685
|
+
return state.temps.bodies.getItemById(4).isOn;
|
|
1686
|
+
case 'poolspa':
|
|
1687
|
+
if (sys.equipment.shared && sys.equipment.maxBodies >= 2) {
|
|
1688
|
+
return state.temps.bodies.getItemById(1).isOn === true || state.temps.bodies.getItemById(2).isOn === true;
|
|
1689
|
+
}
|
|
1690
|
+
else
|
|
1691
|
+
return state.temps.bodies.getItemById(1).isOn;
|
|
1692
|
+
}
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1392
1695
|
}
|
|
1393
1696
|
export class PumpCommands extends BoardCommands {
|
|
1697
|
+
public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
|
|
1698
|
+
try {
|
|
1699
|
+
// First delete the pumps that should be removed.
|
|
1700
|
+
for (let i = 0; i < ctx.pumps.remove.length; i++) {
|
|
1701
|
+
let p = ctx.pumps.remove[i];
|
|
1702
|
+
try {
|
|
1703
|
+
await sys.board.pumps.deletePumpAsync(p);
|
|
1704
|
+
res.addModuleSuccess('pump', `Remove: ${p.id}-${p.name}`);
|
|
1705
|
+
} catch (err) { res.addModuleError('pump', `Remove: ${p.id}-${p.name}: ${err.message}`); }
|
|
1706
|
+
}
|
|
1707
|
+
for (let i = 0; i < ctx.pumps.update.length; i++) {
|
|
1708
|
+
let p = ctx.pumps.update[i];
|
|
1709
|
+
try {
|
|
1710
|
+
await sys.board.pumps.setPumpAsync(p);
|
|
1711
|
+
res.addModuleSuccess('pump', `Update: ${p.id}-${p.name}`);
|
|
1712
|
+
} catch (err) { res.addModuleError('pump', `Update: ${p.id}-${p.name}: ${err.message}`); }
|
|
1713
|
+
}
|
|
1714
|
+
for (let i = 0; i < ctx.pumps.add.length; i++) {
|
|
1715
|
+
let p = ctx.pumps.add[i];
|
|
1716
|
+
try {
|
|
1717
|
+
// pull a little trick to first add the data then perform the update. This way we won't get a new id or
|
|
1718
|
+
// it won't error out.
|
|
1719
|
+
sys.pumps.getItemById(p, true);
|
|
1720
|
+
await sys.board.pumps.setPumpAsync(p);
|
|
1721
|
+
res.addModuleSuccess('pump', `Add: ${p.id}-${p.name}`);
|
|
1722
|
+
} catch (err) { res.addModuleError('pump', `Add: ${p.id}-${p.name}: ${err.message}`); }
|
|
1723
|
+
}
|
|
1724
|
+
return true;
|
|
1725
|
+
} catch (err) { logger.error(`Error restoring pumps: ${err.message}`); res.addModuleError('system', `Error restoring pumps: ${err.message}`); return false; }
|
|
1726
|
+
}
|
|
1727
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
|
|
1728
|
+
try {
|
|
1729
|
+
let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
1730
|
+
// Look at pumps.
|
|
1731
|
+
let cfg = rest.poolConfig;
|
|
1732
|
+
for (let i = 0; i < cfg.pumps.length; i++) {
|
|
1733
|
+
let r = cfg.pumps[i];
|
|
1734
|
+
let c = sys.pumps.find(elem => r.id === elem.id);
|
|
1735
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
1736
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
1737
|
+
}
|
|
1738
|
+
for (let i = 0; i < sys.pumps.length; i++) {
|
|
1739
|
+
let c = sys.pumps.getItemByIndex(i);
|
|
1740
|
+
let r = cfg.pumps.find(elem => elem.id == c.id);
|
|
1741
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
1742
|
+
}
|
|
1743
|
+
return ctx;
|
|
1744
|
+
} catch (err) { logger.error(`Error validating pumps for restore: ${err.message}`); }
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1394
1747
|
public getPumpTypes() { return this.board.valueMaps.pumpTypes.toArray(); }
|
|
1395
1748
|
public getCircuitUnits(pump?: Pump) {
|
|
1396
1749
|
if (typeof pump === 'undefined')
|
|
@@ -1455,14 +1808,15 @@ export class PumpCommands extends BoardCommands {
|
|
|
1455
1808
|
// and props that aren't for this pump type
|
|
1456
1809
|
let _id = pump.id;
|
|
1457
1810
|
if (pump.type !== pumpType || pumpType === 0) {
|
|
1458
|
-
|
|
1811
|
+
let _p = pump.get(true);
|
|
1812
|
+
// const _isVirtual = typeof _p.isVirtual !== 'undefined' ? _p.isVirtual : false;
|
|
1459
1813
|
sys.pumps.removeItemById(_id);
|
|
1460
|
-
|
|
1461
|
-
if (_isVirtual) {
|
|
1814
|
+
pump = sys.pumps.getItemById(_id, true);
|
|
1815
|
+
/* if (_isVirtual) {
|
|
1462
1816
|
// pump.isActive = true;
|
|
1463
1817
|
// pump.isVirtual = true;
|
|
1464
1818
|
pump.master = 1;
|
|
1465
|
-
}
|
|
1819
|
+
} */
|
|
1466
1820
|
state.pumps.removeItemById(pump.id);
|
|
1467
1821
|
pump.type = pumpType;
|
|
1468
1822
|
let type = sys.board.valueMaps.pumpTypes.transform(pumpType);
|
|
@@ -1512,179 +1866,452 @@ export class PumpCommands extends BoardCommands {
|
|
|
1512
1866
|
_availCircuits.push({ type: 'none', id: 255, name: 'Remove' });
|
|
1513
1867
|
return _availCircuits;
|
|
1514
1868
|
}
|
|
1869
|
+
public setPumpValveDelays(circuitIds: number[], delay?: number) {}
|
|
1515
1870
|
}
|
|
1516
1871
|
export class CircuitCommands extends BoardCommands {
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
}
|
|
1527
|
-
} catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); }
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
public syncVirtualCircuitStates() {
|
|
1531
|
-
try {
|
|
1532
|
-
let arrCircuits = sys.board.valueMaps.virtualCircuits.toArray();
|
|
1533
|
-
let poolStates = sys.board.bodies.getPoolStates();
|
|
1534
|
-
let spaStates = sys.board.bodies.getSpaStates();
|
|
1535
|
-
// The following should work for all board types if the virtualCiruit valuemaps use common names. The circuit ids can be
|
|
1536
|
-
// different as well as the descriptions but these should have common names since they are all derived from existing states.
|
|
1537
|
-
|
|
1538
|
-
// This also removes virtual circuits depending on whether heaters exsits on the bodies. Not sure why we are doing this
|
|
1539
|
-
// as the body data contains whether a body is heated or not. Perhapse some attached interface is using
|
|
1540
|
-
// the virtual circuit list as a means to determine whether solar is available. That is totally flawed if that is the case.
|
|
1541
|
-
for (let i = 0; i < arrCircuits.length; i++) {
|
|
1542
|
-
let vc = arrCircuits[i];
|
|
1543
|
-
let remove = false;
|
|
1544
|
-
let bState = false;
|
|
1545
|
-
let cstate: VirtualCircuitState = null;
|
|
1546
|
-
switch (vc.name) {
|
|
1547
|
-
case 'poolHeater':
|
|
1548
|
-
// If any pool is heating up.
|
|
1549
|
-
remove = true;
|
|
1550
|
-
for (let j = 0; j < poolStates.length; j++) {
|
|
1551
|
-
if (poolStates[j].heaterOptions.total > 0) remove = false;
|
|
1872
|
+
public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
|
|
1873
|
+
try {
|
|
1874
|
+
// First delete the circuit/lightGroups that should be removed.
|
|
1875
|
+
for (let i = 0; i < ctx.circuitGroups.remove.length; i++) {
|
|
1876
|
+
let c = ctx.circuitGroups.remove[i];
|
|
1877
|
+
try {
|
|
1878
|
+
await sys.board.circuits.deleteCircuitGroupAsync(c);
|
|
1879
|
+
res.addModuleSuccess('circuitGroup', `Remove: ${c.id}-${c.name}`);
|
|
1880
|
+
} catch (err) { res.addModuleError('circuitGroup', `Remove: ${c.id}-${c.name}: ${err.message}`); }
|
|
1552
1881
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1882
|
+
for (let i = 0; i < ctx.lightGroups.remove.length; i++) {
|
|
1883
|
+
let c = ctx.lightGroups.remove[i];
|
|
1884
|
+
try {
|
|
1885
|
+
await sys.board.circuits.deleteLightGroupAsync(c);
|
|
1886
|
+
res.addModuleSuccess('lightGroup', `Remove: ${c.id}-${c.name}`);
|
|
1887
|
+
} catch (err) { res.addModuleError('lightGroup', `Remove: ${c.id}-${c.name}: ${err.message}`); }
|
|
1557
1888
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1889
|
+
for (let i = 0; i < ctx.circuits.remove.length; i++) {
|
|
1890
|
+
let c = ctx.circuits.remove[i];
|
|
1891
|
+
try {
|
|
1892
|
+
await sys.board.circuits.deleteCircuitAsync(c);
|
|
1893
|
+
res.addModuleSuccess('circuit', `Remove: ${c.id}-${c.name}`);
|
|
1894
|
+
} catch (err) { res.addModuleError('circuit', `Remove: ${c.id}-${c.name}: ${err.message}`); }
|
|
1563
1895
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1896
|
+
for (let i = 0; i < ctx.circuits.add.length; i++) {
|
|
1897
|
+
let c = ctx.circuits.add[i];
|
|
1898
|
+
try {
|
|
1899
|
+
await sys.board.circuits.setCircuitAsync(c);
|
|
1900
|
+
res.addModuleSuccess('circuit', `Add: ${c.id}-${c.name}`);
|
|
1901
|
+
} catch (err) { res.addModuleError('circuit', `Add: ${c.id}-${c.name}: ${err.message}`); }
|
|
1569
1902
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
// If any pool or spa is on
|
|
1577
|
-
for (let j = 0; j < poolStates.length && !bState; j++) {
|
|
1578
|
-
if (poolStates[j].isOn) bState = true;
|
|
1903
|
+
for (let i = 0; i < ctx.circuitGroups.add.length; i++) {
|
|
1904
|
+
let c = ctx.circuitGroups.add[i];
|
|
1905
|
+
try {
|
|
1906
|
+
await sys.board.circuits.setCircuitGroupAsync(c);
|
|
1907
|
+
res.addModuleSuccess('circuitGroup', `Add: ${c.id}-${c.name}`);
|
|
1908
|
+
} catch (err) { res.addModuleError('circuitGroup', `Add: ${c.id}-${c.name}: ${err.message}`); }
|
|
1579
1909
|
}
|
|
1580
|
-
for (let
|
|
1581
|
-
|
|
1910
|
+
for (let i = 0; i < ctx.lightGroups.add.length; i++) {
|
|
1911
|
+
let c = ctx.lightGroups.add[i];
|
|
1912
|
+
try {
|
|
1913
|
+
await sys.board.circuits.setLightGroupAsync(c);
|
|
1914
|
+
res.addModuleSuccess('lightGroup', `Add: ${c.id}-${c.name}`);
|
|
1915
|
+
} catch (err) { res.addModuleError('lightGroup', `Add: ${c.id}-${c.name}: ${err.message}`); }
|
|
1582
1916
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
if (poolStates[j].heaterOptions.solar + poolStates[j].heaterOptions.heatpump > 0) remove = false;
|
|
1917
|
+
for (let i = 0; i < ctx.circuits.update.length; i++) {
|
|
1918
|
+
let c = ctx.circuits.update[i];
|
|
1919
|
+
try {
|
|
1920
|
+
await sys.board.circuits.setCircuitAsync(c);
|
|
1921
|
+
res.addModuleSuccess('circuit', `Update: ${c.id}-${c.name}`);
|
|
1922
|
+
} catch (err) { res.addModuleError('circuit', `Update: ${c.id}-${c.name}: ${err.message}`); }
|
|
1590
1923
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1924
|
+
for (let i = 0; i < ctx.circuitGroups.update.length; i++) {
|
|
1925
|
+
let c = ctx.circuitGroups.update[i];
|
|
1926
|
+
try {
|
|
1927
|
+
await sys.board.circuits.setCircuitGroupAsync(c);
|
|
1928
|
+
res.addModuleSuccess('circuitGroup', `Update: ${c.id}-${c.name}`);
|
|
1929
|
+
} catch (err) { res.addModuleError('circuitGroup', `Update: ${c.id}-${c.name}: ${err.message}`); }
|
|
1595
1930
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
}
|
|
1931
|
+
for (let i = 0; i < ctx.lightGroups.update.length; i++) {
|
|
1932
|
+
let c = ctx.lightGroups.update[i];
|
|
1933
|
+
try {
|
|
1934
|
+
await sys.board.circuits.setLightGroupAsync(c);
|
|
1935
|
+
res.addModuleSuccess('lightGroup', `Update: ${c.id}-${c.name}`);
|
|
1936
|
+
} catch (err) { res.addModuleError('lightGroup', `Update: ${c.id}-${c.name}: ${err.message}`); }
|
|
1603
1937
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1938
|
+
return true;
|
|
1939
|
+
} catch (err) { logger.error(`Error restoring circuits: ${err.message}`); res.addModuleError('system', `Error restoring circuits/features: ${err.message}`); return false; }
|
|
1940
|
+
}
|
|
1941
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }, ctxRoot): Promise<boolean> {
|
|
1942
|
+
try {
|
|
1943
|
+
let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
1944
|
+
// Look at circuits.
|
|
1945
|
+
let cfg = rest.poolConfig;
|
|
1946
|
+
for (let i = 0; i < cfg.circuits.length; i++) {
|
|
1947
|
+
let r = cfg.circuits[i];
|
|
1948
|
+
let c = sys.circuits.find(elem => r.id === elem.id);
|
|
1949
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
1950
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
1951
|
+
}
|
|
1952
|
+
for (let i = 0; i < sys.circuits.length; i++) {
|
|
1953
|
+
let c = sys.circuits.getItemByIndex(i);
|
|
1954
|
+
let r = cfg.circuits.find(elem => elem.id == c.id);
|
|
1955
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
1956
|
+
}
|
|
1957
|
+
ctxRoot.circuits = ctx;
|
|
1958
|
+
ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
1959
|
+
for (let i = 0; i < cfg.circuitGroups.length; i++) {
|
|
1960
|
+
let r = cfg.circuitGroups[i];
|
|
1961
|
+
let c = sys.circuitGroups.find(elem => r.id === elem.id);
|
|
1962
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
1963
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
1964
|
+
}
|
|
1965
|
+
for (let i = 0; i < sys.circuitGroups.length; i++) {
|
|
1966
|
+
let c = sys.circuitGroups.getItemByIndex(i);
|
|
1967
|
+
let r = cfg.circuitGroups.find(elem => elem.id == c.id);
|
|
1968
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
1969
|
+
}
|
|
1970
|
+
ctxRoot.circuitGroups = ctx;
|
|
1971
|
+
ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
1972
|
+
for (let i = 0; i < cfg.lightGroups.length; i++) {
|
|
1973
|
+
let r = cfg.lightGroups[i];
|
|
1974
|
+
let c = sys.lightGroups.find(elem => r.id === elem.id);
|
|
1975
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
1976
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
1977
|
+
}
|
|
1978
|
+
for (let i = 0; i < sys.lightGroups.length; i++) {
|
|
1979
|
+
let c = sys.lightGroups.getItemByIndex(i);
|
|
1980
|
+
let r = cfg.lightGroups.find(elem => elem.id == c.id);
|
|
1981
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
1982
|
+
}
|
|
1983
|
+
ctxRoot.lightGroups = ctx;
|
|
1984
|
+
return true;
|
|
1985
|
+
} catch (err) { logger.error(`Error validating circuits for restore: ${err.message}`); }
|
|
1986
|
+
}
|
|
1987
|
+
public async checkEggTimerExpirationAsync() {
|
|
1988
|
+
// turn off any circuits that have reached their egg timer;
|
|
1989
|
+
// Nixie circuits we have 100% control over;
|
|
1990
|
+
// but features/cg/lg may override OCP control
|
|
1991
|
+
try {
|
|
1992
|
+
for (let i = 0; i < sys.circuits.length; i++) {
|
|
1993
|
+
let c = sys.circuits.getItemByIndex(i);
|
|
1994
|
+
let cstate = state.circuits.getItemByIndex(i);
|
|
1995
|
+
if (!cstate.isActive || !cstate.isOn || typeof cstate.endTime === 'undefined') continue;
|
|
1996
|
+
if (c.master === 1) {
|
|
1997
|
+
await ncp.circuits.checkCircuitEggTimerExpirationAsync(cstate);
|
|
1998
|
+
}
|
|
1609
1999
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
if (
|
|
1613
|
-
|
|
2000
|
+
for (let i = 0; i < sys.features.length; i++) {
|
|
2001
|
+
let fstate = state.features.getItemByIndex(i);
|
|
2002
|
+
if (!fstate.isActive || !fstate.isOn || typeof fstate.endTime === 'undefined') continue;
|
|
2003
|
+
if (fstate.endTime.toDate() < new Timestamp().toDate()) {
|
|
2004
|
+
await sys.board.circuits.setCircuitStateAsync(fstate.id, false);
|
|
2005
|
+
fstate.emitEquipmentChange();
|
|
2006
|
+
}
|
|
1614
2007
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
if (
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
if (heat !== 'off') bState = true;
|
|
1623
|
-
}
|
|
2008
|
+
for (let i = 0; i < sys.circuitGroups.length; i++) {
|
|
2009
|
+
let cgstate = state.circuitGroups.getItemByIndex(i);
|
|
2010
|
+
if (!cgstate.isActive || !cgstate.isOn || typeof cgstate.endTime === 'undefined') continue;
|
|
2011
|
+
if (cgstate.endTime.toDate() < new Timestamp().toDate()) {
|
|
2012
|
+
await sys.board.circuits.setCircuitGroupStateAsync(cgstate.id, false);
|
|
2013
|
+
cgstate.emitEquipmentChange();
|
|
2014
|
+
}
|
|
1624
2015
|
}
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
cstate.type = vc.val;
|
|
1637
|
-
cstate.name = vc.desc;
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
} catch (err) { logger.error(`Error syncronizing virtual circuits`); }
|
|
1642
|
-
}
|
|
1643
|
-
public async setCircuitStateAsync(id: number, val: boolean): Promise<ICircuitState> {
|
|
1644
|
-
sys.board.suspendStatus(true);
|
|
2016
|
+
for (let i = 0; i < sys.lightGroups.length; i++) {
|
|
2017
|
+
let lgstate = state.lightGroups.getItemByIndex(i);
|
|
2018
|
+
if (!lgstate.isActive || !lgstate.isOn || typeof lgstate.endTime === 'undefined') continue;
|
|
2019
|
+
if (lgstate.endTime.toDate() < new Timestamp().toDate()) {
|
|
2020
|
+
await sys.board.circuits.setLightGroupStateAsync(lgstate.id, false);
|
|
2021
|
+
lgstate.emitEquipmentChange();
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
} catch (err) { logger.error(`checkEggTimerExpiration: Error synchronizing circuit relays ${err.message}`); }
|
|
2025
|
+
}
|
|
2026
|
+
public async syncCircuitRelayStates() {
|
|
1645
2027
|
try {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
let circuit: ICircuit = sys.circuits.getInterfaceById(id, false, { isActive: false });
|
|
1653
|
-
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit or Feature id ${id} not valid`, id, 'Circuit'));
|
|
1654
|
-
let circ = state.circuits.getInterfaceById(id, circuit.isActive !== false);
|
|
1655
|
-
let newState = utils.makeBool(val);
|
|
1656
|
-
// First, if we are turning the circuit on, lets determine whether the circuit is a pool or spa circuit and if this is a shared system then we need
|
|
1657
|
-
// to turn off the other body first.
|
|
1658
|
-
//[12, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
|
|
1659
|
-
//[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }]
|
|
1660
|
-
let func = sys.board.valueMaps.circuitFunctions.get(circuit.type);
|
|
1661
|
-
if (newState && (func.name === 'pool' || func.name === 'spa') && sys.equipment.shared === true) {
|
|
1662
|
-
console.log(`Turning off shared body circuit`);
|
|
1663
|
-
// If we are shared we need to turn off the other circuit.
|
|
1664
|
-
let offType = func.name === 'pool' ? sys.board.valueMaps.circuitFunctions.getValue('spa') : sys.board.valueMaps.circuitFunctions.getValue('pool');
|
|
1665
|
-
let off = sys.circuits.get().filter(elem => elem.type === offType);
|
|
1666
|
-
// Turn the circuits off that are part of the shared system. We are going back to the board
|
|
1667
|
-
// just in case we got here for a circuit that isn't on the current defined panel.
|
|
1668
|
-
for (let i = 0; i < off.length; i++) {
|
|
1669
|
-
let coff = off[i];
|
|
1670
|
-
await sys.board.circuits.setCircuitStateAsync(coff.id, false);
|
|
2028
|
+
for (let i = 0; i < sys.circuits.length; i++) {
|
|
2029
|
+
// Run through all the controlled circuits to see whether they should be triggered or not.
|
|
2030
|
+
let circ = sys.circuits.getItemByIndex(i);
|
|
2031
|
+
if (circ.master === 1 && circ.isActive) {
|
|
2032
|
+
let cstate = state.circuits.getItemById(circ.id);
|
|
2033
|
+
if (cstate.isOn) await ncp.circuits.setCircuitStateAsync(cstate, cstate.isOn);
|
|
1671
2034
|
}
|
|
1672
2035
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2036
|
+
} catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); }
|
|
2037
|
+
}
|
|
2038
|
+
public syncVirtualCircuitStates() {
|
|
2039
|
+
try {
|
|
2040
|
+
let arrCircuits = sys.board.valueMaps.virtualCircuits.toArray();
|
|
2041
|
+
let poolStates = sys.board.bodies.getPoolStates();
|
|
2042
|
+
let spaStates = sys.board.bodies.getSpaStates();
|
|
2043
|
+
// The following should work for all board types if the virtualCiruit valuemaps use common names. The circuit ids can be
|
|
2044
|
+
// different as well as the descriptions but these should have common names since they are all derived from existing states.
|
|
2045
|
+
|
|
2046
|
+
// This also removes virtual circuits depending on whether heaters exsits on the bodies. Not sure why we are doing this
|
|
2047
|
+
// as the body data contains whether a body is heated or not. Perhapse some attached interface is using
|
|
2048
|
+
// the virtual circuit list as a means to determine whether solar is available. That is totally flawed if that is the case.
|
|
2049
|
+
for (let i = 0; i < arrCircuits.length; i++) {
|
|
2050
|
+
let vc = arrCircuits[i];
|
|
2051
|
+
let remove = false;
|
|
2052
|
+
let bState = false;
|
|
2053
|
+
let cstate: VirtualCircuitState = null;
|
|
2054
|
+
switch (vc.name) {
|
|
2055
|
+
case 'poolHeater':
|
|
2056
|
+
// If any pool is heating up.
|
|
2057
|
+
remove = true;
|
|
2058
|
+
for (let j = 0; j < poolStates.length; j++) {
|
|
2059
|
+
if (poolStates[j].heaterOptions.total > 0) remove = false;
|
|
2060
|
+
}
|
|
2061
|
+
if (!remove) {
|
|
2062
|
+
// Determine whether the pool heater is on.
|
|
2063
|
+
for (let j = 0; j < poolStates.length; j++) {
|
|
2064
|
+
if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'heater') {
|
|
2065
|
+
// In this instance we may have a delay underway.
|
|
2066
|
+
let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name !== 'solar');
|
|
2067
|
+
bState = typeof hstate === 'undefined';
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
break;
|
|
2072
|
+
case 'spaHeater':
|
|
2073
|
+
remove = true;
|
|
2074
|
+
for (let j = 0; j < spaStates.length; j++) {
|
|
2075
|
+
if (spaStates[j].heaterOptions.total > 0) remove = false;
|
|
2076
|
+
}
|
|
2077
|
+
if (!remove) {
|
|
2078
|
+
// Determine whether the spa heater is on.
|
|
2079
|
+
for (let j = 0; j < spaStates.length; j++) {
|
|
2080
|
+
if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') {
|
|
2081
|
+
// In this instance we may have a delay underway.
|
|
2082
|
+
let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name !== 'solar');
|
|
2083
|
+
bState = typeof hstate === 'undefined';
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
//for (let j = 0; j < spaStates.length; j++) {
|
|
2087
|
+
// if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') bState = true;
|
|
2088
|
+
//}
|
|
2089
|
+
}
|
|
2090
|
+
break;
|
|
2091
|
+
case 'freeze':
|
|
2092
|
+
// If freeze protection has been turned on.
|
|
2093
|
+
bState = state.freeze;
|
|
2094
|
+
break;
|
|
2095
|
+
case 'poolSpa':
|
|
2096
|
+
// If any pool or spa is on
|
|
2097
|
+
for (let j = 0; j < poolStates.length && !bState; j++) {
|
|
2098
|
+
if (poolStates[j].isOn) bState = true;
|
|
2099
|
+
}
|
|
2100
|
+
for (let j = 0; j < spaStates.length && !bState; j++) {
|
|
2101
|
+
if (spaStates[j].isOn) bState = true;
|
|
2102
|
+
}
|
|
2103
|
+
break;
|
|
2104
|
+
case 'solarHeat':
|
|
2105
|
+
case 'solar':
|
|
2106
|
+
// If solar is on for any body
|
|
2107
|
+
remove = true;
|
|
2108
|
+
for (let j = 0; j < poolStates.length; j++) {
|
|
2109
|
+
if (poolStates[j].heaterOptions.solar + poolStates[j].heaterOptions.heatpump > 0) remove = false;
|
|
2110
|
+
}
|
|
2111
|
+
if (remove) {
|
|
2112
|
+
for (let j = 0; j < spaStates.length; j++) {
|
|
2113
|
+
if (spaStates[j].heaterOptions.solar + spaStates[j].heaterOptions.heatpump > 0) remove = false;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
if (!remove) {
|
|
2117
|
+
for (let j = 0; j < poolStates.length && !bState; j++) {
|
|
2118
|
+
if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') bState = true;
|
|
2119
|
+
}
|
|
2120
|
+
for (let j = 0; j < spaStates.length && !bState; j++) {
|
|
2121
|
+
if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') bState = true;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
break;
|
|
2125
|
+
case 'solar1':
|
|
2126
|
+
remove = true;
|
|
2127
|
+
for (let j = 0; j < poolStates.length; j++) {
|
|
2128
|
+
if (poolStates[j].id === 1 && poolStates[j].heaterOptions.solar) {
|
|
2129
|
+
remove = false;
|
|
2130
|
+
vc.desc = `${poolStates[j].name} Solar`;
|
|
2131
|
+
if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') {
|
|
2132
|
+
// In this instance we may have a delay underway.
|
|
2133
|
+
let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name === 'solar');
|
|
2134
|
+
bState = typeof hstate === 'undefined';
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
for (let j = 0; j < spaStates.length; j++) {
|
|
2139
|
+
if (spaStates[j].id === 1 && spaStates[j].heaterOptions.solar) {
|
|
2140
|
+
remove = false;
|
|
2141
|
+
vc.desc = `${spaStates[j].name} Solar`;
|
|
2142
|
+
if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
|
|
2143
|
+
// In this instance we may have a delay underway.
|
|
2144
|
+
let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name === 'solar');
|
|
2145
|
+
bState = typeof hstate === 'undefined';
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
break;
|
|
2151
|
+
case 'solar2':
|
|
2152
|
+
remove = true;
|
|
2153
|
+
for (let j = 0; j < poolStates.length; j++) {
|
|
2154
|
+
if (poolStates[j].id === 2 && poolStates[j].heaterOptions.solar) {
|
|
2155
|
+
remove = false;
|
|
2156
|
+
vc.desc = `${poolStates[j].name} Solar`;
|
|
2157
|
+
if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
|
|
2158
|
+
// In this instance we may have a delay underway.
|
|
2159
|
+
let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name === 'solar');
|
|
2160
|
+
bState = typeof hstate === 'undefined';
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
for (let j = 0; j < spaStates.length; j++) {
|
|
2165
|
+
if (spaStates[j].id === 2 && spaStates[j].heaterOptions.solar) {
|
|
2166
|
+
remove = false;
|
|
2167
|
+
vc.desc = `${spaStates[j].name} Solar`;
|
|
2168
|
+
if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
|
|
2169
|
+
// In this instance we may have a delay underway.
|
|
2170
|
+
let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name === 'solar');
|
|
2171
|
+
bState = typeof hstate === 'undefined';
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
break;
|
|
2176
|
+
case 'solar3':
|
|
2177
|
+
remove = true;
|
|
2178
|
+
for (let j = 0; j < poolStates.length; j++) {
|
|
2179
|
+
if (poolStates[j].id === 3 && poolStates[j].heaterOptions.solar) {
|
|
2180
|
+
remove = false;
|
|
2181
|
+
vc.desc = `${poolStates[j].name} Solar`;
|
|
2182
|
+
if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
|
|
2183
|
+
// In this instance we may have a delay underway.
|
|
2184
|
+
let hstate = state.heaters.find(x => x.bodyId === 3 && x.startupDelay === true && x.type.name === 'solar');
|
|
2185
|
+
bState = typeof hstate === 'undefined';
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
for (let j = 0; j < spaStates.length; j++) {
|
|
2190
|
+
if (spaStates[j].id === 3 && spaStates[j].heaterOptions.solar) {
|
|
2191
|
+
remove = false;
|
|
2192
|
+
vc.desc = `${spaStates[j].name} Solar`;
|
|
2193
|
+
if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
|
|
2194
|
+
// In this instance we may have a delay underway.
|
|
2195
|
+
let hstate = state.heaters.find(x => x.bodyId === 3 && x.startupDelay === true && x.type.name === 'solar');
|
|
2196
|
+
bState = typeof hstate === 'undefined';
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
break;
|
|
2202
|
+
case 'solar4':
|
|
2203
|
+
remove = true;
|
|
2204
|
+
for (let j = 0; j < poolStates.length; j++) {
|
|
2205
|
+
if (poolStates[j].id === 4 && poolStates[j].heaterOptions.solar) {
|
|
2206
|
+
remove = false;
|
|
2207
|
+
vc.desc = `${poolStates[j].name} Solar`;
|
|
2208
|
+
if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
|
|
2209
|
+
// In this instance we may have a delay underway.
|
|
2210
|
+
let hstate = state.heaters.find(x => x.bodyId === 4 && x.startupDelay === true && x.type.name === 'solar');
|
|
2211
|
+
bState = typeof hstate === 'undefined';
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
for (let j = 0; j < spaStates.length; j++) {
|
|
2216
|
+
if (spaStates[j].id === 4 && spaStates[j].heaterOptions.solar) {
|
|
2217
|
+
remove = false;
|
|
2218
|
+
vc.desc = `${spaStates[j].name} Solar`;
|
|
2219
|
+
if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
|
|
2220
|
+
// In this instance we may have a delay underway.
|
|
2221
|
+
let hstate = state.heaters.find(x => x.bodyId === 4 && x.startupDelay === true && x.type.name === 'solar');
|
|
2222
|
+
bState = typeof hstate === 'undefined';
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
break;
|
|
2227
|
+
case 'heater':
|
|
2228
|
+
remove = true;
|
|
2229
|
+
for (let j = 0; j < poolStates.length; j++) {
|
|
2230
|
+
if (poolStates[j].heaterOptions.total > 0) remove = false;
|
|
2231
|
+
}
|
|
2232
|
+
if (remove) {
|
|
2233
|
+
for (let j = 0; j < spaStates.length; j++) {
|
|
2234
|
+
if (spaStates[j].heaterOptions.total > 0) remove = false;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
if (!remove) {
|
|
2238
|
+
for (let j = 0; j < poolStates.length && !bState; j++) {
|
|
2239
|
+
let heat = sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus);
|
|
2240
|
+
if (heat !== 'off') bState = true;
|
|
2241
|
+
}
|
|
2242
|
+
for (let j = 0; j < spaStates.length && !bState; j++) {
|
|
2243
|
+
let heat = sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus);
|
|
2244
|
+
if (heat !== 'off') bState = true;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
break;
|
|
2248
|
+
default:
|
|
2249
|
+
remove = true;
|
|
2250
|
+
break;
|
|
2251
|
+
}
|
|
2252
|
+
if (remove) {
|
|
2253
|
+
if (state.virtualCircuits.exists(x => vc.val === x.id)) {
|
|
2254
|
+
cstate = state.virtualCircuits.getItemById(vc.val, true);
|
|
2255
|
+
cstate.isActive = false;
|
|
2256
|
+
cstate.emitEquipmentChange();
|
|
2257
|
+
}
|
|
2258
|
+
state.virtualCircuits.removeItemById(vc.val);
|
|
2259
|
+
}
|
|
2260
|
+
else {
|
|
2261
|
+
cstate = state.virtualCircuits.getItemById(vc.val, true);
|
|
2262
|
+
cstate.isActive = true;
|
|
2263
|
+
if (cstate !== null) {
|
|
2264
|
+
cstate.isOn = bState;
|
|
2265
|
+
cstate.type = vc.val;
|
|
2266
|
+
cstate.name = vc.desc;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
} catch (err) { logger.error(`Error syncronizing virtual circuits`); }
|
|
1678
2271
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2272
|
+
public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
|
|
2273
|
+
sys.board.suspendStatus(true);
|
|
2274
|
+
try {
|
|
2275
|
+
// We need to do some routing here as it is now critical that circuits, groups, and features
|
|
2276
|
+
// have their own processing. The virtual controller used to only deal with one circuit.
|
|
2277
|
+
if (sys.board.equipmentIds.circuitGroups.isInRange(id))
|
|
2278
|
+
return await sys.board.circuits.setCircuitGroupStateAsync(id, val);
|
|
2279
|
+
else if (sys.board.equipmentIds.features.isInRange(id))
|
|
2280
|
+
return await sys.board.features.setFeatureStateAsync(id, val);
|
|
2281
|
+
let circuit: ICircuit = sys.circuits.getInterfaceById(id, false, { isActive: false });
|
|
2282
|
+
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit or Feature id ${id} not valid`, id, 'Circuit'));
|
|
2283
|
+
let circ = state.circuits.getInterfaceById(id, circuit.isActive !== false);
|
|
2284
|
+
let newState = utils.makeBool(val);
|
|
2285
|
+
// First, if we are turning the circuit on, lets determine whether the circuit is a pool or spa circuit and if this is a shared system then we need
|
|
2286
|
+
// to turn off the other body first.
|
|
2287
|
+
//[12, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
|
|
2288
|
+
//[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }]
|
|
2289
|
+
let func = sys.board.valueMaps.circuitFunctions.get(circuit.type);
|
|
2290
|
+
if (newState && (func.name === 'pool' || func.name === 'spa') && sys.equipment.shared === true) {
|
|
2291
|
+
// If we are shared we need to turn off the other circuit.
|
|
2292
|
+
let offType = func.name === 'pool' ? sys.board.valueMaps.circuitFunctions.getValue('spa') : sys.board.valueMaps.circuitFunctions.getValue('pool');
|
|
2293
|
+
let off = sys.circuits.get().filter(elem => elem.type === offType);
|
|
2294
|
+
// Turn the circuits off that are part of the shared system. We are going back to the board
|
|
2295
|
+
// just in case we got here for a circuit that isn't on the current defined panel.
|
|
2296
|
+
for (let i = 0; i < off.length; i++) {
|
|
2297
|
+
let coff = off[i];
|
|
2298
|
+
await sys.board.circuits.setCircuitStateAsync(coff.id, false);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
if (id === 6) state.temps.bodies.getItemById(1, true).isOn = val;
|
|
2302
|
+
else if (id === 1) state.temps.bodies.getItemById(2, true).isOn = val;
|
|
2303
|
+
// Let the main nixie controller set the circuit state and affect the relays if it needs to.
|
|
2304
|
+
await ncp.circuits.setCircuitStateAsync(circ, newState);
|
|
2305
|
+
await sys.board.syncEquipmentItems();
|
|
2306
|
+
return state.circuits.getInterfaceById(circ.id);
|
|
2307
|
+
}
|
|
2308
|
+
catch (err) { return Promise.reject(`Nixie: Error setCircuitStateAsync ${err.message}`); }
|
|
2309
|
+
finally {
|
|
2310
|
+
ncp.pumps.syncPumpStates();
|
|
2311
|
+
sys.board.suspendStatus(false);
|
|
2312
|
+
state.emitEquipmentChanges();
|
|
2313
|
+
}
|
|
1686
2314
|
}
|
|
1687
|
-
}
|
|
1688
2315
|
public async toggleCircuitStateAsync(id: number): Promise<ICircuitState> {
|
|
1689
2316
|
let circ = state.circuits.getInterfaceById(id);
|
|
1690
2317
|
return await this.setCircuitStateAsync(id, !(circ.isOn || false));
|
|
@@ -1754,7 +2381,11 @@ export class CircuitCommands extends BoardCommands {
|
|
|
1754
2381
|
return arrRefs;
|
|
1755
2382
|
}
|
|
1756
2383
|
public getLightThemes(type?: number) { return sys.board.valueMaps.lightThemes.toArray(); }
|
|
1757
|
-
|
|
2384
|
+
public getCircuitFunctions() {
|
|
2385
|
+
let cf = sys.board.valueMaps.circuitFunctions.toArray();
|
|
2386
|
+
if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' });
|
|
2387
|
+
return cf;
|
|
2388
|
+
}
|
|
1758
2389
|
public getCircuitNames() { return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()]; }
|
|
1759
2390
|
public async setCircuitAsync(data: any): Promise<ICircuit> {
|
|
1760
2391
|
try {
|
|
@@ -1779,7 +2410,7 @@ export class CircuitCommands extends BoardCommands {
|
|
|
1779
2410
|
if (typeof data.id !== 'undefined') {
|
|
1780
2411
|
let circuit = sys.circuits.getItemById(id, true);
|
|
1781
2412
|
let scircuit = state.circuits.getItemById(id, true);
|
|
1782
|
-
circuit.isActive = true;
|
|
2413
|
+
scircuit.isActive = circuit.isActive = true;
|
|
1783
2414
|
circuit.master = 1;
|
|
1784
2415
|
scircuit.isOn = false;
|
|
1785
2416
|
if (data.name) circuit.name = scircuit.name = data.name;
|
|
@@ -1797,6 +2428,7 @@ export class CircuitCommands extends BoardCommands {
|
|
|
1797
2428
|
if (typeof data.deviceBinding !== 'undefined') circuit.deviceBinding = data.deviceBinding;
|
|
1798
2429
|
if (typeof data.showInFeatures !== 'undefined') scircuit.showInFeatures = circuit.showInFeatures = utils.makeBool(data.showInFeatures);
|
|
1799
2430
|
circuit.dontStop = circuit.eggTimer === 1440;
|
|
2431
|
+
|
|
1800
2432
|
sys.emitEquipmentChange();
|
|
1801
2433
|
state.emitEquipmentChanges();
|
|
1802
2434
|
if (circuit.master === 1) await ncp.circuits.setCircuitAsync(circuit, data);
|
|
@@ -1879,8 +2511,9 @@ export class CircuitCommands extends BoardCommands {
|
|
|
1879
2511
|
if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit light group id exceeded`, id, 'LightGroup'));
|
|
1880
2512
|
if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'LightGroup'));
|
|
1881
2513
|
group = sys.lightGroups.getItemById(id, true);
|
|
2514
|
+
let sgroup = state.lightGroups.getItemById(id, true);
|
|
1882
2515
|
return new Promise<LightGroup>((resolve, reject) => {
|
|
1883
|
-
if (typeof obj.name !== 'undefined') group.name = obj.name;
|
|
2516
|
+
if (typeof obj.name !== 'undefined') sgroup.name = group.name = obj.name;
|
|
1884
2517
|
if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440;
|
|
1885
2518
|
if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440);
|
|
1886
2519
|
group.dontStop = group.eggTimer === 1440;
|
|
@@ -1987,7 +2620,9 @@ export class CircuitCommands extends BoardCommands {
|
|
|
1987
2620
|
await sys.board.circuits.setCircuitStateAsync(c.circuit, false);
|
|
1988
2621
|
else if (!cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) !== 'off') await sys.board.circuits.setCircuitStateAsync(c.circuit, true);
|
|
1989
2622
|
}
|
|
1990
|
-
|
|
2623
|
+
let isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false : true;
|
|
2624
|
+
sys.board.circuits.setEndTime(grp, sgrp, isOn);
|
|
2625
|
+
sgrp.isOn = isOn;
|
|
1991
2626
|
// If we truly want to support themes in lightGroups we probably need to program
|
|
1992
2627
|
// the specific on/off toggles to enable that. For now this will go through the motions but it's just a pretender.
|
|
1993
2628
|
switch (theme) {
|
|
@@ -2131,8 +2766,135 @@ export class CircuitCommands extends BoardCommands {
|
|
|
2131
2766
|
logger.error(`Error setting end time for ${thing.id}: ${err}`)
|
|
2132
2767
|
}
|
|
2133
2768
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2769
|
+
public async turnOffDrainCircuits(ignoreDelays: boolean) {
|
|
2770
|
+
try {
|
|
2771
|
+
{
|
|
2772
|
+
let drt = sys.board.valueMaps.circuitFunctions.getValue('spadrain');
|
|
2773
|
+
let drains = sys.circuits.filter(x => { return x.type === drt });
|
|
2774
|
+
for (let i = 0; i < drains.length; i++) {
|
|
2775
|
+
let drain = drains.getItemByIndex(i);
|
|
2776
|
+
let sdrain = state.circuits.getItemById(drain.id);
|
|
2777
|
+
if (sdrain.isOn) await sys.board.circuits.setCircuitStateAsync(drain.id, false, ignoreDelays);
|
|
2778
|
+
sdrain.startDelay = false;
|
|
2779
|
+
sdrain.stopDelay = false;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
{
|
|
2783
|
+
let drt = sys.board.valueMaps.featureFunctions.getValue('spadrain');
|
|
2784
|
+
let drains = sys.features.filter(x => { return x.type === drt });
|
|
2785
|
+
for (let i = 0; i < drains.length; i++) {
|
|
2786
|
+
let drain = drains.getItemByIndex(i);
|
|
2787
|
+
let sdrain = state.features.getItemById(drain.id);
|
|
2788
|
+
if (sdrain.isOn) await sys.board.features.setFeatureStateAsync(drain.id, false, ignoreDelays);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
} catch (err) { return Promise.reject(new BoardProcessError(`turnOffDrainCircuits: ${err.message}`)); }
|
|
2793
|
+
}
|
|
2794
|
+
public async turnOffCleanerCircuits(bstate: BodyTempState, ignoreDelays?: boolean) {
|
|
2795
|
+
try {
|
|
2796
|
+
// First we have to get all the cleaner circuits that are associated with the
|
|
2797
|
+
// body. To do this we get the circuit functions for all cleaner types associated with the body.
|
|
2798
|
+
//
|
|
2799
|
+
// Cleaner ciruits can always be turned off. However, they cannot always be turned on.
|
|
2800
|
+
let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('cleaner') !== -1 && x.body === bstate.id; });
|
|
2801
|
+
let cleaners = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
|
|
2802
|
+
// So now we should have all the cleaner circuits so lets make sure they are off.
|
|
2803
|
+
for (let i = 0; i < cleaners.length; i++) {
|
|
2804
|
+
let cleaner = cleaners.getItemByIndex(i);
|
|
2805
|
+
if (cleaner.isActive) {
|
|
2806
|
+
let cstate = state.circuits.getItemById(cleaner.id, true);
|
|
2807
|
+
if (cstate.isOn || cstate.startDelay) await sys.board.circuits.setCircuitStateAsync(cleaner.id, false, ignoreDelays);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
} catch (err) { return Promise.reject(new BoardProcessError(`turnOffCleanerCircuits: ${err.message}`)); }
|
|
2811
|
+
}
|
|
2812
|
+
public async turnOffSpillwayCircuits(ignoreDelays?: boolean) {
|
|
2813
|
+
try {
|
|
2814
|
+
{
|
|
2815
|
+
let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('spillway') !== -1 });
|
|
2816
|
+
let spillways = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
|
|
2817
|
+
// So now we should have all the cleaner circuits so lets make sure they are off.
|
|
2818
|
+
for (let i = 0; i < spillways.length; i++) {
|
|
2819
|
+
let spillway = spillways.getItemByIndex(i);
|
|
2820
|
+
if (spillway.isActive) {
|
|
2821
|
+
let cstate = state.circuits.getItemById(spillway.id, true);
|
|
2822
|
+
if (cstate.isOn || cstate.startDelay) await sys.board.circuits.setCircuitStateAsync(spillway.id, false, ignoreDelays);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
{
|
|
2827
|
+
let arrTypes = sys.board.valueMaps.featureFunctions.toArray().filter(x => { return x.name.indexOf('spillway') !== -1 });
|
|
2828
|
+
let spillways = sys.features.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
|
|
2829
|
+
// So now we should have all the cleaner features so lets make sure they are off.
|
|
2830
|
+
for (let i = 0; i < spillways.length; i++) {
|
|
2831
|
+
let spillway = spillways.getItemByIndex(i);
|
|
2832
|
+
if (spillway.isActive) {
|
|
2833
|
+
let cstate = state.features.getItemById(spillway.id, true);
|
|
2834
|
+
if (cstate.isOn) await sys.board.features.setFeatureStateAsync(spillway.id, false, ignoreDelays);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
} catch (err) { return Promise.reject(new BoardProcessError(`turnOffSpillwayCircuits: ${err.message}`)); }
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
export class FeatureCommands extends BoardCommands {
|
|
2842
|
+
public getFeatureFunctions() {
|
|
2843
|
+
let cf = sys.board.valueMaps.featureFunctions.toArray();
|
|
2844
|
+
if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' });
|
|
2845
|
+
return cf;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
|
|
2849
|
+
try {
|
|
2850
|
+
// First delete the features that should be removed.
|
|
2851
|
+
for (let i = 0; i < ctx.features.remove.length; i++) {
|
|
2852
|
+
let f = ctx.features.remove[i];
|
|
2853
|
+
try {
|
|
2854
|
+
await sys.board.features.deleteFeatureAsync(f);
|
|
2855
|
+
res.addModuleSuccess('feature', `Remove: ${f.id}-${f.name}`);
|
|
2856
|
+
} catch (err) { res.addModuleError('feature', `Remove: ${f.id}-${f.name}: ${err.message}`) }
|
|
2857
|
+
}
|
|
2858
|
+
for (let i = 0; i < ctx.features.update.length; i++) {
|
|
2859
|
+
let f = ctx.features.update[i];
|
|
2860
|
+
try {
|
|
2861
|
+
await sys.board.features.setFeatureAsync(f);
|
|
2862
|
+
res.addModuleSuccess('feature', `Update: ${f.id}-${f.name}`);
|
|
2863
|
+
} catch (err) { res.addModuleError('feature', `Update: ${f.id}-${f.name}: ${err.message}`); }
|
|
2864
|
+
}
|
|
2865
|
+
for (let i = 0; i < ctx.features.add.length; i++) {
|
|
2866
|
+
// pull a little trick to first add the data then perform the update. This way we won't get a new id or
|
|
2867
|
+
// it won't error out.
|
|
2868
|
+
let f = ctx.features.add[i];
|
|
2869
|
+
try {
|
|
2870
|
+
sys.features.getItemById(f, true);
|
|
2871
|
+
await sys.board.features.setFeatureAsync(f);
|
|
2872
|
+
res.addModuleSuccess('feature', `Add: ${f.id}-${f.name}`);
|
|
2873
|
+
} catch (err) { res.addModuleError('feature', `Add: ${f.id}-${f.name}: ${err.message}`) }
|
|
2874
|
+
}
|
|
2875
|
+
return true;
|
|
2876
|
+
} catch (err) { logger.error(`Error restoring features: ${err.message}`); res.addModuleError('system', `Error restoring features: ${err.message}`); return false; }
|
|
2877
|
+
}
|
|
2878
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
|
|
2879
|
+
try {
|
|
2880
|
+
let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
2881
|
+
// Look at features.
|
|
2882
|
+
let cfg = rest.poolConfig;
|
|
2883
|
+
for (let i = 0; i < cfg.features.length; i++) {
|
|
2884
|
+
let r = cfg.features[i];
|
|
2885
|
+
let c = sys.features.find(elem => r.id === elem.id);
|
|
2886
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
2887
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
2888
|
+
}
|
|
2889
|
+
for (let i = 0; i < sys.features.length; i++) {
|
|
2890
|
+
let c = sys.features.getItemByIndex(i);
|
|
2891
|
+
let r = cfg.features.find(elem => elem.id == c.id);
|
|
2892
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
2893
|
+
}
|
|
2894
|
+
return ctx;
|
|
2895
|
+
} catch (err) { logger.error(`Error validating features for restore: ${err.message}`); }
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2136
2898
|
public async setFeatureAsync(obj: any): Promise<Feature> {
|
|
2137
2899
|
let id = parseInt(obj.id, 10);
|
|
2138
2900
|
if (id <= 0 || isNaN(id)) {
|
|
@@ -2177,7 +2939,7 @@ export class FeatureCommands extends BoardCommands {
|
|
|
2177
2939
|
else
|
|
2178
2940
|
Promise.reject(new InvalidEquipmentIdError('Feature id has not been defined', undefined, 'Feature'));
|
|
2179
2941
|
}
|
|
2180
|
-
public async setFeatureStateAsync(id: number, val: boolean): Promise<ICircuitState> {
|
|
2942
|
+
public async setFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
|
|
2181
2943
|
try {
|
|
2182
2944
|
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
|
|
2183
2945
|
if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
|
|
@@ -2186,7 +2948,6 @@ export class FeatureCommands extends BoardCommands {
|
|
|
2186
2948
|
sys.board.circuits.setEndTime(feature, fstate, val);
|
|
2187
2949
|
fstate.isOn = val;
|
|
2188
2950
|
sys.board.valves.syncValveStates();
|
|
2189
|
-
// sys.board.virtualPumpControllers.start();
|
|
2190
2951
|
ncp.pumps.syncPumpStates();
|
|
2191
2952
|
state.emitEquipmentChanges();
|
|
2192
2953
|
return fstate;
|
|
@@ -2249,6 +3010,57 @@ export class FeatureCommands extends BoardCommands {
|
|
|
2249
3010
|
}
|
|
2250
3011
|
}
|
|
2251
3012
|
export class ChlorinatorCommands extends BoardCommands {
|
|
3013
|
+
public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
|
|
3014
|
+
try {
|
|
3015
|
+
// First delete the chlorinators that should be removed.
|
|
3016
|
+
for (let i = 0; i < ctx.chlorinators.remove.length; i++) {
|
|
3017
|
+
let c = ctx.chlorinators.remove[i];
|
|
3018
|
+
try {
|
|
3019
|
+
await sys.board.chlorinator.deleteChlorAsync(c);
|
|
3020
|
+
res.addModuleSuccess('chlorinator', `Remove: ${c.id}-${c.name}`);
|
|
3021
|
+
} catch (err) { res.addModuleError('chlorinator', `Remove: ${c.id}-${c.name}: ${err.message}`); }
|
|
3022
|
+
}
|
|
3023
|
+
for (let i = 0; i < ctx.chlorinators.update.length; i++) {
|
|
3024
|
+
let c = ctx.chlorinators.update[i];
|
|
3025
|
+
try {
|
|
3026
|
+
await sys.board.chlorinator.setChlorAsync(c);
|
|
3027
|
+
res.addModuleSuccess('chlorinator', `Update: ${c.id}-${c.name}`);
|
|
3028
|
+
} catch (err) { res.addModuleError('chlorinator', `Update: ${c.id}-${c.name}: ${err.message}`); }
|
|
3029
|
+
}
|
|
3030
|
+
for (let i = 0; i < ctx.chlorinators.add.length; i++) {
|
|
3031
|
+
let c = ctx.chlorinators.add[i];
|
|
3032
|
+
try {
|
|
3033
|
+
// pull a little trick to first add the data then perform the update. This way we won't get a new id or
|
|
3034
|
+
// it won't error out.
|
|
3035
|
+
sys.chlorinators.getItemById(c.id, true);
|
|
3036
|
+
await sys.board.chlorinator.setChlorAsync(c);
|
|
3037
|
+
res.addModuleSuccess('chlorinator', `Add: ${c.id}-${c.name}`);
|
|
3038
|
+
} catch (err) { res.addModuleError('chlorinator', `Add: ${c.id}-${c.name}: ${err.message}`); }
|
|
3039
|
+
}
|
|
3040
|
+
return true;
|
|
3041
|
+
} catch (err) { logger.error(`Error restoring chlorinators: ${err.message}`); res.addModuleError('system', `Error restoring chlorinators: ${err.message}`); return false; }
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
|
|
3045
|
+
try {
|
|
3046
|
+
let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
3047
|
+
// Look at chlorinators.
|
|
3048
|
+
let cfg = rest.poolConfig;
|
|
3049
|
+
for (let i = 0; i < cfg.chlorinators.length; i++) {
|
|
3050
|
+
let r = cfg.chlorinators[i];
|
|
3051
|
+
let c = sys.chlorinators.find(elem => r.id === elem.id);
|
|
3052
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
3053
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
3054
|
+
}
|
|
3055
|
+
for (let i = 0; i < sys.chlorinators.length; i++) {
|
|
3056
|
+
let c = sys.chlorinators.getItemByIndex(i);
|
|
3057
|
+
let r = cfg.chlorinators.find(elem => elem.id == c.id);
|
|
3058
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
3059
|
+
}
|
|
3060
|
+
return ctx;
|
|
3061
|
+
} catch (err) { logger.error(`Error validating chlorinators for restore: ${err.message}`); }
|
|
3062
|
+
}
|
|
3063
|
+
|
|
2252
3064
|
public async setChlorAsync(obj: any): Promise<ChlorinatorState> {
|
|
2253
3065
|
try {
|
|
2254
3066
|
let id = parseInt(obj.id, 10);
|
|
@@ -2267,7 +3079,7 @@ export class ChlorinatorCommands extends BoardCommands {
|
|
|
2267
3079
|
public async deleteChlorAsync(obj: any): Promise<ChlorinatorState> {
|
|
2268
3080
|
try {
|
|
2269
3081
|
let id = parseInt(obj.id, 10);
|
|
2270
|
-
if (isNaN(id)) obj.id
|
|
3082
|
+
if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator id is not valid: ${obj.id}`, 'chlorinator', obj.id));
|
|
2271
3083
|
let chlor = state.chlorinators.getItemById(id);
|
|
2272
3084
|
chlor.isActive = false;
|
|
2273
3085
|
await ncp.chlorinators.deleteChlorinatorAsync(id);
|
|
@@ -2291,6 +3103,56 @@ export class ChlorinatorCommands extends BoardCommands {
|
|
|
2291
3103
|
}
|
|
2292
3104
|
}
|
|
2293
3105
|
export class ScheduleCommands extends BoardCommands {
|
|
3106
|
+
public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
|
|
3107
|
+
try {
|
|
3108
|
+
// First delete the schedules that should be removed.
|
|
3109
|
+
for (let i = 0; i < ctx.schedules.remove.length; i++) {
|
|
3110
|
+
let s = ctx.schedules.remove[i];
|
|
3111
|
+
try {
|
|
3112
|
+
await sys.board.schedules.deleteScheduleAsync(ctx.schedules.remove[i]);
|
|
3113
|
+
res.addModuleSuccess('schedule', `Remove: ${s.id}-${s.circuitId}`);
|
|
3114
|
+
} catch (err) { res.addModuleError('schedule', `Remove: ${s.id}-${s.circuitId} ${err.message}`); }
|
|
3115
|
+
}
|
|
3116
|
+
for (let i = 0; i < ctx.schedules.update.length; i++) {
|
|
3117
|
+
let s = ctx.schedules.update[i];
|
|
3118
|
+
try {
|
|
3119
|
+
await sys.board.schedules.setScheduleAsync(s);
|
|
3120
|
+
res.addModuleSuccess('schedule', `Update: ${s.id}-${s.circuitId}`);
|
|
3121
|
+
} catch (err) { res.addModuleError('schedule', `Update: ${s.id}-${s.circuitId} ${err.message}`); }
|
|
3122
|
+
}
|
|
3123
|
+
for (let i = 0; i < ctx.schedules.add.length; i++) {
|
|
3124
|
+
let s = ctx.schedules.add[i];
|
|
3125
|
+
try {
|
|
3126
|
+
// pull a little trick to first add the data then perform the update. This way we won't get a new id or
|
|
3127
|
+
// it won't error out.
|
|
3128
|
+
sys.schedules.getItemById(s.id, true);
|
|
3129
|
+
await sys.board.schedules.setScheduleAsync(s);
|
|
3130
|
+
res.addModuleSuccess('schedule', `Add: ${s.id}-${s.circuitId}`);
|
|
3131
|
+
} catch (err) { res.addModuleError('schedule', `Add: ${s.id}-${s.circuitId} ${err.message}`); }
|
|
3132
|
+
}
|
|
3133
|
+
return true;
|
|
3134
|
+
} catch (err) { logger.error(`Error restoring schedules: ${err.message}`); res.addModuleError('system', `Error restoring schedules: ${err.message}`); return false; }
|
|
3135
|
+
}
|
|
3136
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
|
|
3137
|
+
try {
|
|
3138
|
+
let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
3139
|
+
// Look at schedules.
|
|
3140
|
+
let cfg = rest.poolConfig;
|
|
3141
|
+
for (let i = 0; i < cfg.schedules.length; i++) {
|
|
3142
|
+
let r = cfg.schedules[i];
|
|
3143
|
+
let c = sys.schedules.find(elem => r.id === elem.id);
|
|
3144
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
3145
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
3146
|
+
}
|
|
3147
|
+
for (let i = 0; i < sys.schedules.length; i++) {
|
|
3148
|
+
let c = sys.schedules.getItemByIndex(i);
|
|
3149
|
+
let r = cfg.schedules.find(elem => elem.id == c.id);
|
|
3150
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
3151
|
+
}
|
|
3152
|
+
return ctx;
|
|
3153
|
+
} catch (err) { logger.error(`Error validating schedules for restore: ${err.message}`); }
|
|
3154
|
+
}
|
|
3155
|
+
|
|
2294
3156
|
public transformDays(val: any): number {
|
|
2295
3157
|
if (typeof val === 'number') return val;
|
|
2296
3158
|
let edays = sys.board.valueMaps.scheduleDays.toArray();
|
|
@@ -2411,6 +3273,9 @@ export class ScheduleCommands extends BoardCommands {
|
|
|
2411
3273
|
return Promise.reject(new InvalidEquipmentDataError(`Invalid circuit reference: ${circuit}`, 'Schedule', circuit));
|
|
2412
3274
|
if (schedType === 128 && schedDays === 0) return Promise.reject(new InvalidEquipmentDataError(`Invalid schedule days: ${schedDays}. You must supply days that the schedule is to run.`, 'Schedule', schedDays));
|
|
2413
3275
|
|
|
3276
|
+
// If we made it to here we are valid and the schedula and it state should exist.
|
|
3277
|
+
sched = sys.schedules.getItemById(id, true);
|
|
3278
|
+
ssched = state.schedules.getItemById(id, true);
|
|
2414
3279
|
sched.circuit = ssched.circuit = circuit;
|
|
2415
3280
|
sched.scheduleDays = ssched.scheduleDays = schedDays;
|
|
2416
3281
|
sched.scheduleType = ssched.scheduleType = schedType;
|
|
@@ -2426,6 +3291,7 @@ export class ScheduleCommands extends BoardCommands {
|
|
|
2426
3291
|
sched.startYear = startDate.getFullYear();
|
|
2427
3292
|
sched.startMonth = startDate.getMonth() + 1;
|
|
2428
3293
|
sched.startDay = startDate.getDate();
|
|
3294
|
+
sched.isActive = sched.startTime !== 0;
|
|
2429
3295
|
|
|
2430
3296
|
ssched.display = sched.display = display;
|
|
2431
3297
|
if (typeof sched.startDate === 'undefined')
|
|
@@ -2521,399 +3387,564 @@ export class ScheduleCommands extends BoardCommands {
|
|
|
2521
3387
|
}
|
|
2522
3388
|
}
|
|
2523
3389
|
export class HeaterCommands extends BoardCommands {
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
if (typeof type !== 'undefined') {
|
|
2552
|
-
switch (type.name) {
|
|
2553
|
-
case 'solar':
|
|
3390
|
+
public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
|
|
3391
|
+
try {
|
|
3392
|
+
// First delete the heaters that should be removed.
|
|
3393
|
+
for (let i = 0; i < ctx.heaters.remove.length; i++) {
|
|
3394
|
+
let h = ctx.heaters.remove[i];
|
|
3395
|
+
try {
|
|
3396
|
+
await sys.board.heaters.deleteHeaterAsync(h);
|
|
3397
|
+
res.addModuleSuccess('heater', `Remove: ${h.id}-${h.name}`);
|
|
3398
|
+
} catch (err) { res.addModuleError('heater', `Remove: ${h.id}-${h.name}: ${err.message}`); }
|
|
3399
|
+
}
|
|
3400
|
+
for (let i = 0; i < ctx.heaters.update.length; i++) {
|
|
3401
|
+
let h = ctx.heaters.update[i];
|
|
3402
|
+
try {
|
|
3403
|
+
await sys.board.heaters.setHeaterAsync(h);
|
|
3404
|
+
res.addModuleSuccess('heater', `Update: ${h.id}-${h.name}`);
|
|
3405
|
+
} catch (err) { res.addModuleError('heater', `Update: ${h.id}-${h.name}: ${err.message}`); }
|
|
3406
|
+
}
|
|
3407
|
+
for (let i = 0; i < ctx.heaters.add.length; i++) {
|
|
3408
|
+
let h = ctx.heaters.add[i];
|
|
3409
|
+
try {
|
|
3410
|
+
// pull a little trick to first add the data then perform the update. This way we won't get a new id or
|
|
3411
|
+
// it won't error out.
|
|
3412
|
+
sys.heaters.getItemById(h.id, true);
|
|
3413
|
+
await sys.board.heaters.setHeaterAsync(h);
|
|
3414
|
+
res.addModuleSuccess('heater', `Add: ${h.id}-${h.name}`);
|
|
3415
|
+
} catch (err) { res.addModuleError('heater', `Add: ${h.id}-${h.name}: ${err.message}`); }
|
|
3416
|
+
}
|
|
2554
3417
|
return true;
|
|
3418
|
+
} catch (err) { logger.error(`Error restoring heaters: ${err.message}`); res.addModuleError('system', `Error restoring heaters: ${err.message}`); return false; }
|
|
3419
|
+
}
|
|
3420
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
|
|
3421
|
+
try {
|
|
3422
|
+
let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
3423
|
+
// Look at heaters.
|
|
3424
|
+
let cfg = rest.poolConfig;
|
|
3425
|
+
for (let i = 0; i < cfg.heaters.length; i++) {
|
|
3426
|
+
let r = cfg.heaters[i];
|
|
3427
|
+
let c = sys.heaters.find(elem => r.id === elem.id);
|
|
3428
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
3429
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
3430
|
+
}
|
|
3431
|
+
for (let i = 0; i < sys.heaters.length; i++) {
|
|
3432
|
+
let c = sys.heaters.getItemByIndex(i);
|
|
3433
|
+
let r = cfg.heaters.find(elem => elem.id == c.id);
|
|
3434
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
3435
|
+
}
|
|
3436
|
+
return ctx;
|
|
3437
|
+
} catch (err) { logger.error(`Error validating heaters for restore: ${err.message}`); }
|
|
3438
|
+
}
|
|
3439
|
+
public getHeatersByCircuitId(circuitId: number): Heater[] {
|
|
3440
|
+
let heaters: Heater[] = [];
|
|
3441
|
+
let bodyId = circuitId === 6 ? 1 : circuitId === 1 ? 2 : 0;
|
|
3442
|
+
if (bodyId > 0) {
|
|
3443
|
+
for (let i = 0; i < sys.heaters.length; i++) {
|
|
3444
|
+
let heater = sys.heaters.getItemByIndex(i);
|
|
3445
|
+
if (!heater.isActive) continue;
|
|
3446
|
+
if (bodyId === heater.body || sys.equipment.shared && heater.body === 32) heaters.push(heater);
|
|
3447
|
+
}
|
|
2555
3448
|
}
|
|
2556
|
-
|
|
3449
|
+
return heaters;
|
|
2557
3450
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
3451
|
+
public getInstalledHeaterTypes(body?: number): any {
|
|
3452
|
+
let heaters = sys.heaters.get();
|
|
3453
|
+
let types = sys.board.valueMaps.heaterTypes.toArray();
|
|
3454
|
+
let inst = { total: 0 };
|
|
3455
|
+
for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
|
|
3456
|
+
for (let i = 0; i < heaters.length; i++) {
|
|
3457
|
+
let heater = heaters[i];
|
|
3458
|
+
if (typeof body !== 'undefined' && heater.body !== 'undefined') {
|
|
3459
|
+
if ((heater.body !== 32 && body !== heater.body + 1) || (heater.body === 32 && body > 2)) continue;
|
|
3460
|
+
}
|
|
3461
|
+
let type = types.find(elem => elem.val === heater.type);
|
|
3462
|
+
if (typeof type !== 'undefined') {
|
|
3463
|
+
if (inst[type.name] === 'undefined') inst[type.name] = 0;
|
|
3464
|
+
inst[type.name] = inst[type.name] + 1;
|
|
3465
|
+
if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
|
|
3466
|
+
inst.total++;
|
|
3467
|
+
}
|
|
2570
3468
|
}
|
|
2571
|
-
|
|
3469
|
+
return inst;
|
|
2572
3470
|
}
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
3471
|
+
public isSolarInstalled(body?: number): boolean {
|
|
3472
|
+
let heaters = sys.heaters.get();
|
|
3473
|
+
let types = sys.board.valueMaps.heaterTypes.toArray();
|
|
3474
|
+
for (let i = 0; i < heaters.length; i++) {
|
|
3475
|
+
let heater = heaters[i];
|
|
3476
|
+
if (typeof body !== 'undefined' && body !== heater.body) continue;
|
|
3477
|
+
let type = types.find(elem => elem.val === heater.type);
|
|
3478
|
+
if (typeof type !== 'undefined') {
|
|
3479
|
+
switch (type.name) {
|
|
3480
|
+
case 'solar':
|
|
3481
|
+
return true;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
2578
3485
|
}
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
if (typeof obj !== undefined) {
|
|
2593
|
-
for (var s in obj) {
|
|
2594
|
-
if (s === 'id') continue;
|
|
2595
|
-
heater[s] = obj[s];
|
|
3486
|
+
public isHeatPumpInstalled(body?: number): boolean {
|
|
3487
|
+
let heaters = sys.heaters.get();
|
|
3488
|
+
let types = sys.board.valueMaps.heaterTypes.toArray();
|
|
3489
|
+
for (let i = 0; i < heaters.length; i++) {
|
|
3490
|
+
let heater = heaters[i];
|
|
3491
|
+
if (typeof body !== 'undefined' && body !== heater.body) continue;
|
|
3492
|
+
let type = types.find(elem => elem.val === heater.type);
|
|
3493
|
+
if (typeof type !== 'undefined') {
|
|
3494
|
+
switch (type.name) {
|
|
3495
|
+
case 'heatpump':
|
|
3496
|
+
return true;
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
2596
3499
|
}
|
|
2597
|
-
}
|
|
2598
|
-
let hstate = state.heaters.getItemById(id, true);
|
|
2599
|
-
//hstate.isVirtual = heater.isVirtual = true;
|
|
2600
|
-
hstate.name = heater.name;
|
|
2601
|
-
hstate.type = heater.type;
|
|
2602
|
-
heater.master = 1;
|
|
2603
|
-
if (heater.master === 1) await ncp.heaters.setHeaterAsync(heater, obj);
|
|
2604
|
-
await sys.board.heaters.updateHeaterServices();
|
|
2605
|
-
await sys.board.heaters.syncHeaterStates();
|
|
2606
|
-
return heater;
|
|
2607
|
-
} catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
|
|
2608
|
-
}
|
|
2609
|
-
public async deleteHeaterAsync(obj: any): Promise<Heater> {
|
|
2610
|
-
try {
|
|
2611
|
-
let id = parseInt(obj.id, 10);
|
|
2612
|
-
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
|
|
2613
|
-
let heater = sys.heaters.getItemById(id);
|
|
2614
|
-
heater.isActive = false;
|
|
2615
|
-
if (heater.master === 1) await ncp.heaters.deleteHeaterAsync(heater.id);
|
|
2616
|
-
sys.heaters.removeItemById(id);
|
|
2617
|
-
state.heaters.removeItemById(id);
|
|
2618
|
-
sys.board.heaters.updateHeaterServices();
|
|
2619
|
-
sys.board.heaters.syncHeaterStates();
|
|
2620
|
-
return heater;
|
|
2621
|
-
} catch (err) { return Promise.reject(`Error deleting heater: ${err.message}`) }
|
|
2622
|
-
}
|
|
2623
|
-
public updateHeaterServices() {
|
|
2624
|
-
let htypes = sys.board.heaters.getInstalledHeaterTypes();
|
|
2625
|
-
let solarInstalled = htypes.solar > 0;
|
|
2626
|
-
let heatPumpInstalled = htypes.heatpump > 0;
|
|
2627
|
-
let gasHeaterInstalled = htypes.gas > 0;
|
|
2628
|
-
|
|
2629
|
-
if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
|
|
2630
|
-
if (gasHeaterInstalled) sys.board.valueMaps.heatSources.set(3, { name: 'heater', desc: 'Heater' });
|
|
2631
|
-
if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
|
|
2632
|
-
else if (solarInstalled) sys.board.valueMaps.heatSources.set(5, { name: 'solar', desc: 'Solar' });
|
|
2633
|
-
if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
|
|
2634
|
-
else if (heatPumpInstalled) sys.board.valueMaps.heatSources.set(9, { name: 'heatpump', desc: 'Heat Pump' });
|
|
2635
|
-
sys.board.valueMaps.heatSources.set(32, { name: 'nochange', desc: 'No Change' });
|
|
2636
|
-
|
|
2637
|
-
sys.board.valueMaps.heatModes = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
|
|
2638
|
-
if (gasHeaterInstalled) sys.board.valueMaps.heatModes.set(3, { name: 'heater', desc: 'Heater' });
|
|
2639
|
-
if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
|
|
2640
|
-
else if (solarInstalled) sys.board.valueMaps.heatModes.set(5, { name: 'solar', desc: 'Solar' });
|
|
2641
|
-
if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
|
|
2642
|
-
else if (heatPumpInstalled) sys.board.valueMaps.heatModes.set(9, { name: 'heatpump', desc: 'Heat Pump' });
|
|
2643
|
-
// Now set the body data.
|
|
2644
|
-
for (let i = 0; i < sys.bodies.length; i++) {
|
|
2645
|
-
let body = sys.bodies.getItemByIndex(i);
|
|
2646
|
-
let btemp = state.temps.bodies.getItemById(body.id, body.isActive !== false);
|
|
2647
|
-
let opts = sys.board.heaters.getInstalledHeaterTypes(body.id);
|
|
2648
|
-
btemp.heaterOptions = opts;
|
|
2649
|
-
}
|
|
2650
|
-
this.setActiveTempSensors();
|
|
2651
|
-
}
|
|
2652
|
-
public initTempSensors() {
|
|
2653
|
-
// Add in the potential sensors and delete the ones that shouldn't exist.
|
|
2654
|
-
let maxPairs = sys.equipment.maxBodies + (sys.equipment.shared ? -1 : 0);
|
|
2655
|
-
sys.equipment.tempSensors.getItemById('air', true, { id: 'air', isActive: true, calibration: 0 }).name = 'Air';
|
|
2656
|
-
sys.equipment.tempSensors.getItemById('water1', true, { id: 'water1', isActive: true, calibration: 0 }).name = maxPairs == 1 ? 'Water' : 'Body 1';
|
|
2657
|
-
sys.equipment.tempSensors.getItemById('solar1', true, { id: 'solar1', isActive: false, calibration: 0 }).name = maxPairs == 1 ? 'Solar' : 'Solar 1';
|
|
2658
|
-
if (maxPairs > 1) {
|
|
2659
|
-
sys.equipment.tempSensors.getItemById('water2', true, { id: 'water2', isActive: false, calibration: 0 }).name = 'Body 2';
|
|
2660
|
-
sys.equipment.tempSensors.getItemById('solar2', true, { id: 'solar2', isActive: false, calibration: 0 }).name = 'Solar 2';
|
|
2661
3500
|
}
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
3501
|
+
public setHeater(heater: Heater, obj?: any) {
|
|
3502
|
+
if (typeof obj !== undefined) {
|
|
3503
|
+
for (var s in obj)
|
|
3504
|
+
heater[s] = obj[s];
|
|
3505
|
+
}
|
|
2665
3506
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
3507
|
+
public async setHeaterAsync(obj: any): Promise<Heater> {
|
|
3508
|
+
try {
|
|
3509
|
+
let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
|
|
3510
|
+
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
|
|
3511
|
+
else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Virtual Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
|
|
3512
|
+
let heater: Heater;
|
|
3513
|
+
if (id <= 0) {
|
|
3514
|
+
// We are adding a heater. In this case all heaters are virtual.
|
|
3515
|
+
let vheaters = sys.heaters.filter(h => h.master === 1);
|
|
3516
|
+
id = vheaters.length + 256;
|
|
3517
|
+
}
|
|
3518
|
+
heater = sys.heaters.getItemById(id, true);
|
|
3519
|
+
if (typeof obj !== undefined) {
|
|
3520
|
+
for (var s in obj) {
|
|
3521
|
+
if (s === 'id') continue;
|
|
3522
|
+
heater[s] = obj[s];
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
let hstate = state.heaters.getItemById(id, true);
|
|
3526
|
+
//hstate.isVirtual = heater.isVirtual = true;
|
|
3527
|
+
hstate.name = heater.name;
|
|
3528
|
+
hstate.type = heater.type;
|
|
3529
|
+
heater.master = 1;
|
|
3530
|
+
if (heater.master === 1) await ncp.heaters.setHeaterAsync(heater, obj);
|
|
3531
|
+
await sys.board.heaters.updateHeaterServices();
|
|
3532
|
+
await sys.board.heaters.syncHeaterStates();
|
|
3533
|
+
return heater;
|
|
3534
|
+
} catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
|
|
2669
3535
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
3536
|
+
public async deleteHeaterAsync(obj: any): Promise<Heater> {
|
|
3537
|
+
try {
|
|
3538
|
+
let id = parseInt(obj.id, 10);
|
|
3539
|
+
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
|
|
3540
|
+
let heater = sys.heaters.getItemById(id);
|
|
3541
|
+
heater.isActive = false;
|
|
3542
|
+
if (heater.master === 1) await ncp.heaters.deleteHeaterAsync(heater.id);
|
|
3543
|
+
sys.heaters.removeItemById(id);
|
|
3544
|
+
state.heaters.removeItemById(id);
|
|
3545
|
+
sys.board.heaters.updateHeaterServices();
|
|
3546
|
+
sys.board.heaters.syncHeaterStates();
|
|
3547
|
+
return heater;
|
|
3548
|
+
} catch (err) { return Promise.reject(`Error deleting heater: ${err.message}`) }
|
|
2673
3549
|
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
3550
|
+
public updateHeaterServices() {
|
|
3551
|
+
let htypes = sys.board.heaters.getInstalledHeaterTypes();
|
|
3552
|
+
let solarInstalled = htypes.solar > 0;
|
|
3553
|
+
let heatPumpInstalled = htypes.heatpump > 0;
|
|
3554
|
+
let gasHeaterInstalled = htypes.gas > 0;
|
|
3555
|
+
|
|
3556
|
+
if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
|
|
3557
|
+
if (gasHeaterInstalled) sys.board.valueMaps.heatSources.set(3, { name: 'heater', desc: 'Heater' });
|
|
3558
|
+
if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
|
|
3559
|
+
else if (solarInstalled) sys.board.valueMaps.heatSources.set(5, { name: 'solar', desc: 'Solar' });
|
|
3560
|
+
if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
|
|
3561
|
+
else if (heatPumpInstalled) sys.board.valueMaps.heatSources.set(9, { name: 'heatpump', desc: 'Heat Pump' });
|
|
3562
|
+
sys.board.valueMaps.heatSources.set(32, { name: 'nochange', desc: 'No Change' });
|
|
3563
|
+
|
|
3564
|
+
sys.board.valueMaps.heatModes = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
|
|
3565
|
+
if (gasHeaterInstalled) sys.board.valueMaps.heatModes.set(3, { name: 'heater', desc: 'Heater' });
|
|
3566
|
+
if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
|
|
3567
|
+
else if (solarInstalled) sys.board.valueMaps.heatModes.set(5, { name: 'solar', desc: 'Solar' });
|
|
3568
|
+
if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
|
|
3569
|
+
else if (heatPumpInstalled) sys.board.valueMaps.heatModes.set(9, { name: 'heatpump', desc: 'Heat Pump' });
|
|
3570
|
+
// Now set the body data.
|
|
3571
|
+
for (let i = 0; i < sys.bodies.length; i++) {
|
|
3572
|
+
let body = sys.bodies.getItemByIndex(i);
|
|
3573
|
+
let btemp = state.temps.bodies.getItemById(body.id, body.isActive !== false);
|
|
3574
|
+
let opts = sys.board.heaters.getInstalledHeaterTypes(body.id);
|
|
3575
|
+
btemp.heaterOptions = opts;
|
|
3576
|
+
}
|
|
3577
|
+
this.setActiveTempSensors();
|
|
2677
3578
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
// board. If this situation ever comes up we will see if it works. Whether it reports is another story
|
|
2710
|
-
// since the 2 message is short a byte for this.
|
|
2711
|
-
sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 4 : sys.equipment.maxBodies > 3;
|
|
2712
|
-
break;
|
|
2713
|
-
// Solar sensors are funny ducks. This is because they are for both heatpumps and solar and the equipment
|
|
2714
|
-
// can be installed on specific bodies. This will be true for heaters installed in expansion panels for *Touch, dual body systems,
|
|
2715
|
-
// and any IntelliCenter with more than one body. At some point simply implementing the multi-body functions for touch will make
|
|
2716
|
-
// this all work. This will only be with i10D or expansion panels.
|
|
2717
|
-
case 'solar1':
|
|
2718
|
-
// The first solar sensor is a funny duck in that it should be active for shared systems
|
|
2719
|
-
// if either body has an active solar heater or heatpump.
|
|
2720
|
-
htypes = sys.board.heaters.getInstalledHeaterTypes(1);
|
|
2721
|
-
if ('solar' in htypes || 'heatpump' in htypes) sensor.isActive = true;
|
|
2722
|
-
else if (sys.equipment.shared) {
|
|
2723
|
-
htypes = sys.board.heaters.getInstalledHeaterTypes(2);
|
|
2724
|
-
sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
|
|
2725
|
-
}
|
|
2726
|
-
else sensor.isActive = false;
|
|
2727
|
-
break;
|
|
2728
|
-
case 'solar2':
|
|
2729
|
-
if (sys.equipment.maxBodies > 1 + (sys.equipment.shared ? 1 : 0)) {
|
|
2730
|
-
htypes = sys.board.heaters.getInstalledHeaterTypes(2 + (sys.equipment.shared ? 1 : 0));
|
|
2731
|
-
sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
|
|
2732
|
-
}
|
|
2733
|
-
else sensor.isActive = false;
|
|
2734
|
-
break;
|
|
2735
|
-
case 'solar3':
|
|
2736
|
-
if (sys.equipment.maxBodies > 2 + (sys.equipment.shared ? 1 : 0)) {
|
|
2737
|
-
htypes = sys.board.heaters.getInstalledHeaterTypes(3 + (sys.equipment.shared ? 1 : 0));
|
|
2738
|
-
sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
|
|
2739
|
-
}
|
|
2740
|
-
else sensor.isActive = false;
|
|
2741
|
-
break;
|
|
2742
|
-
case 'solar4':
|
|
2743
|
-
if (sys.equipment.maxBodies > 3 + (sys.equipment.shared ? 1 : 0)) {
|
|
2744
|
-
htypes = sys.board.heaters.getInstalledHeaterTypes(4 + (sys.equipment.shared ? 1 : 0));
|
|
2745
|
-
sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
|
|
2746
|
-
}
|
|
2747
|
-
else sensor.isActive = false;
|
|
2748
|
-
break;
|
|
2749
|
-
default:
|
|
2750
|
-
if (typeof sensor.id === 'undefined') sys.equipment.tempSensors.removeItemByIndex(i);
|
|
2751
|
-
break;
|
|
2752
|
-
}
|
|
3579
|
+
public initTempSensors() {
|
|
3580
|
+
// Add in the potential sensors and delete the ones that shouldn't exist.
|
|
3581
|
+
let maxPairs = sys.equipment.maxBodies + (sys.equipment.shared ? -1 : 0);
|
|
3582
|
+
sys.equipment.tempSensors.getItemById('air', true, { id: 'air', isActive: true, calibration: 0 }).name = 'Air';
|
|
3583
|
+
sys.equipment.tempSensors.getItemById('water1', true, { id: 'water1', isActive: true, calibration: 0 }).name = maxPairs == 1 ? 'Water' : 'Body 1';
|
|
3584
|
+
sys.equipment.tempSensors.getItemById('solar1', true, { id: 'solar1', isActive: false, calibration: 0 }).name = maxPairs == 1 ? 'Solar' : 'Solar 1';
|
|
3585
|
+
if (maxPairs > 1) {
|
|
3586
|
+
sys.equipment.tempSensors.getItemById('water2', true, { id: 'water2', isActive: false, calibration: 0 }).name = 'Body 2';
|
|
3587
|
+
sys.equipment.tempSensors.getItemById('solar2', true, { id: 'solar2', isActive: false, calibration: 0 }).name = 'Solar 2';
|
|
3588
|
+
}
|
|
3589
|
+
else {
|
|
3590
|
+
sys.equipment.tempSensors.removeItemById('water2');
|
|
3591
|
+
sys.equipment.tempSensors.removeItemById('solar2');
|
|
3592
|
+
}
|
|
3593
|
+
if (maxPairs > 2) {
|
|
3594
|
+
sys.equipment.tempSensors.getItemById('water3', true, { id: 'water3', isActive: false, calibration: 0 }).name = 'Body 3';
|
|
3595
|
+
sys.equipment.tempSensors.getItemById('solar3', true, { id: 'solar3', isActive: false, calibration: 0 }).name = 'Solar 3';
|
|
3596
|
+
}
|
|
3597
|
+
else {
|
|
3598
|
+
sys.equipment.tempSensors.removeItemById('water3');
|
|
3599
|
+
sys.equipment.tempSensors.removeItemById('solar3');
|
|
3600
|
+
}
|
|
3601
|
+
if (maxPairs > 3) {
|
|
3602
|
+
sys.equipment.tempSensors.getItemById('water4', true, { id: 'water4', isActive: false, calibration: 0 }).name = 'Body 4';
|
|
3603
|
+
sys.equipment.tempSensors.getItemById('solar4', true, { id: 'solar4', isActive: false, calibration: 0 }).name = 'Solar 4';
|
|
3604
|
+
}
|
|
3605
|
+
else {
|
|
3606
|
+
sys.equipment.tempSensors.removeItemById('water4');
|
|
3607
|
+
sys.equipment.tempSensors.removeItemById('solar4');
|
|
3608
|
+
}
|
|
3609
|
+
|
|
2753
3610
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
for (let j = 0; j < heaters.length; j++) {
|
|
2771
|
-
let heater: Heater = heaters[j];
|
|
2772
|
-
if (heater.isActive === false) continue;
|
|
2773
|
-
let isOn = false;
|
|
2774
|
-
let isCooling = false;
|
|
2775
|
-
let sensorTemp = state.temps.waterSensor1;
|
|
2776
|
-
if (body.id === 4) sensorTemp = state.temps.waterSensor4;
|
|
2777
|
-
if (body.id === 3) sensorTemp = state.temps.waterSensor3;
|
|
2778
|
-
if (body.id === 2 && !sys.equipment.shared) sensorTemp = state.temps.waterSensor2;
|
|
2779
|
-
|
|
2780
|
-
// Determine whether the heater can be used on this body.
|
|
2781
|
-
let isAssociated = false;
|
|
2782
|
-
let b = sys.board.valueMaps.bodies.transform(heater.body);
|
|
2783
|
-
switch (b.name) {
|
|
2784
|
-
case 'body1':
|
|
2785
|
-
case 'pool':
|
|
2786
|
-
if (body.id === 1) isAssociated = true;
|
|
2787
|
-
break;
|
|
2788
|
-
case 'body2':
|
|
2789
|
-
case 'spa':
|
|
2790
|
-
if (body.id === 2) isAssociated = true;
|
|
2791
|
-
break;
|
|
2792
|
-
case 'poolspa':
|
|
2793
|
-
if (body.id === 1 || body.id === 2) isAssociated = true;
|
|
2794
|
-
break;
|
|
2795
|
-
case 'body3':
|
|
2796
|
-
if (body.id === 3) isAssociated = true;
|
|
2797
|
-
break;
|
|
2798
|
-
case 'body4':
|
|
2799
|
-
if (body.id === 4) isAssociated = true;
|
|
2800
|
-
break;
|
|
2801
|
-
}
|
|
2802
|
-
// logger.silly(`Heater ${heater.name} is ${isAssociated === true ? '' : 'not '}associated with ${body.name}`);
|
|
2803
|
-
if (isAssociated) {
|
|
2804
|
-
let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
|
|
2805
|
-
let status = sys.board.valueMaps.heatStatus.transform(body.heatStatus);
|
|
2806
|
-
let hstate = state.heaters.getItemById(heater.id, true);
|
|
2807
|
-
if (heater.isVirtual === true || heater.master === 1) {
|
|
2808
|
-
// We need to do our own calculation as to whether it is on. This is for Nixie heaters.
|
|
2809
|
-
let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
|
|
2810
|
-
switch (htype.name) {
|
|
2811
|
-
case 'solar':
|
|
2812
|
-
if (mode === 'solar' || mode === 'solarpref') {
|
|
2813
|
-
// Measure up against start and stop temp deltas for effective solar heating.
|
|
2814
|
-
if (body.temp < cfgBody.heatSetpoint &&
|
|
2815
|
-
state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
|
|
2816
|
-
isOn = true;
|
|
2817
|
-
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
|
|
2818
|
-
isHeating = true;
|
|
2819
|
-
}
|
|
2820
|
-
else if (heater.coolingEnabled && body.temp > cfgBody.coolSetpoint && state.heliotrope.isNight &&
|
|
2821
|
-
state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
|
|
2822
|
-
isOn = true;
|
|
2823
|
-
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
|
|
2824
|
-
isHeating = true;
|
|
2825
|
-
isCooling = true;
|
|
2826
|
-
}
|
|
2827
|
-
//else if (heater.coolingEnabled && state.time.isNight)
|
|
2828
|
-
}
|
|
3611
|
+
// Sets the active temp sensors based upon the installed equipment. At this point all
|
|
3612
|
+
// detectable temp sensors should exist.
|
|
3613
|
+
public setActiveTempSensors() {
|
|
3614
|
+
let htypes;
|
|
3615
|
+
// We are iterating backwards through the sensors array on purpose. We do this just in case we need
|
|
3616
|
+
// to remove a sensor during the iteration. This way the index values will not be impacted and we can
|
|
3617
|
+
// safely remove from the array we are iterating.
|
|
3618
|
+
for (let i = sys.equipment.tempSensors.length - 1; i >= 0; i--) {
|
|
3619
|
+
let sensor = sys.equipment.tempSensors.getItemByIndex(i);
|
|
3620
|
+
// The names are normalized in this array.
|
|
3621
|
+
switch (sensor.id) {
|
|
3622
|
+
case 'air':
|
|
3623
|
+
sensor.isActive = true;
|
|
3624
|
+
break;
|
|
3625
|
+
case 'water1':
|
|
3626
|
+
sensor.isActive = sys.equipment.maxBodies > 0;
|
|
2829
3627
|
break;
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
3628
|
+
case 'water2':
|
|
3629
|
+
sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 2 : sys.equipment.maxBodies > 1;
|
|
3630
|
+
break;
|
|
3631
|
+
case 'water3':
|
|
3632
|
+
sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 3 : sys.equipment.maxBodies > 2;
|
|
3633
|
+
break;
|
|
3634
|
+
case 'water4':
|
|
3635
|
+
// It's a little weird but technically you should be able to install 3 expansions and a i10D personality
|
|
3636
|
+
// board. If this situation ever comes up we will see if it works. Whether it reports is another story
|
|
3637
|
+
// since the 2 message is short a byte for this.
|
|
3638
|
+
sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 4 : sys.equipment.maxBodies > 3;
|
|
3639
|
+
break;
|
|
3640
|
+
// Solar sensors are funny ducks. This is because they are for both heatpumps and solar and the equipment
|
|
3641
|
+
// can be installed on specific bodies. This will be true for heaters installed in expansion panels for *Touch, dual body systems,
|
|
3642
|
+
// and any IntelliCenter with more than one body. At some point simply implementing the multi-body functions for touch will make
|
|
3643
|
+
// this all work. This will only be with i10D or expansion panels.
|
|
3644
|
+
case 'solar1':
|
|
3645
|
+
// The first solar sensor is a funny duck in that it should be active for shared systems
|
|
3646
|
+
// if either body has an active solar heater or heatpump.
|
|
3647
|
+
htypes = sys.board.heaters.getInstalledHeaterTypes(1);
|
|
3648
|
+
if ('solar' in htypes || 'heatpump' in htypes) sensor.isActive = true;
|
|
3649
|
+
else if (sys.equipment.shared) {
|
|
3650
|
+
htypes = sys.board.heaters.getInstalledHeaterTypes(2);
|
|
3651
|
+
sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
|
|
2848
3652
|
}
|
|
3653
|
+
else sensor.isActive = false;
|
|
2849
3654
|
break;
|
|
2850
|
-
|
|
2851
|
-
if (
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
2855
|
-
isHeating = true;
|
|
2856
|
-
}
|
|
3655
|
+
case 'solar2':
|
|
3656
|
+
if (sys.equipment.maxBodies > 1 + (sys.equipment.shared ? 1 : 0)) {
|
|
3657
|
+
htypes = sys.board.heaters.getInstalledHeaterTypes(2 + (sys.equipment.shared ? 1 : 0));
|
|
3658
|
+
sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
|
|
2857
3659
|
}
|
|
2858
|
-
else
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
2865
|
-
isHeating = true;
|
|
2866
|
-
}
|
|
3660
|
+
else sensor.isActive = false;
|
|
3661
|
+
break;
|
|
3662
|
+
case 'solar3':
|
|
3663
|
+
if (sys.equipment.maxBodies > 2 + (sys.equipment.shared ? 1 : 0)) {
|
|
3664
|
+
htypes = sys.board.heaters.getInstalledHeaterTypes(3 + (sys.equipment.shared ? 1 : 0));
|
|
3665
|
+
sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
|
|
2867
3666
|
}
|
|
3667
|
+
else sensor.isActive = false;
|
|
2868
3668
|
break;
|
|
2869
|
-
|
|
2870
|
-
if (
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
isOn = true;
|
|
2874
|
-
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
2875
|
-
isHeating = true;
|
|
2876
|
-
}
|
|
3669
|
+
case 'solar4':
|
|
3670
|
+
if (sys.equipment.maxBodies > 3 + (sys.equipment.shared ? 1 : 0)) {
|
|
3671
|
+
htypes = sys.board.heaters.getInstalledHeaterTypes(4 + (sys.equipment.shared ? 1 : 0));
|
|
3672
|
+
sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
|
|
2877
3673
|
}
|
|
3674
|
+
else sensor.isActive = false;
|
|
2878
3675
|
break;
|
|
2879
|
-
|
|
2880
|
-
|
|
3676
|
+
default:
|
|
3677
|
+
if (typeof sensor.id === 'undefined') sys.equipment.tempSensors.removeItemByIndex(i);
|
|
2881
3678
|
break;
|
|
2882
|
-
}
|
|
2883
|
-
logger.debug(`Heater Type: ${htype.name} Mode:${mode} Temp: ${body.temp} Setpoint: ${cfgBody.setPoint} Status: ${body.heatStatus}`);
|
|
2884
|
-
}
|
|
2885
|
-
if (isOn === true && typeof hon.find(elem => elem === heater.id) === 'undefined') {
|
|
2886
|
-
hon.push(heater.id);
|
|
2887
|
-
if (heater.master === 1 && isOn) (async () => {
|
|
2888
|
-
try {
|
|
2889
|
-
await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
|
|
2890
|
-
} catch (err) { logger.error(err.message); }
|
|
2891
|
-
})();
|
|
2892
|
-
else hstate.isOn = isOn;
|
|
2893
|
-
}
|
|
2894
3679
|
}
|
|
2895
|
-
}
|
|
2896
3680
|
}
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
3681
|
+
}
|
|
3682
|
+
// This updates the heater states based upon the installed heaters. This is true for heaters that are tied to the OCP
|
|
3683
|
+
// and those that are not.
|
|
3684
|
+
public syncHeaterStates() {
|
|
3685
|
+
try {
|
|
3686
|
+
// Go through the installed heaters and bodies to determine whether they should be on. If there is a
|
|
3687
|
+
// heater that is not controlled by the OCP then we need to determine whether it should be on.
|
|
3688
|
+
let heaters = sys.heaters.toArray();
|
|
3689
|
+
let bodies = state.temps.bodies.toArray();
|
|
3690
|
+
let hon = [];
|
|
3691
|
+
for (let i = 0; i < bodies.length; i++) {
|
|
3692
|
+
let body: BodyTempState = bodies[i];
|
|
3693
|
+
let cfgBody: Body = sys.bodies.getItemById(body.id);
|
|
3694
|
+
let isHeating = false;
|
|
3695
|
+
let isCooling = false;
|
|
3696
|
+
let hstatus = sys.board.valueMaps.heatStatus.getName(body.heatStatus);
|
|
3697
|
+
let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
|
|
3698
|
+
if (body.isOn) {
|
|
3699
|
+
if (typeof body.temp === 'undefined' && heaters.length > 0) logger.warn(`The body temperature for ${body.name} cannot be determined. Heater status for this body cannot be calculated.`);
|
|
3700
|
+
for (let j = 0; j < heaters.length; j++) {
|
|
3701
|
+
let heater: Heater = heaters[j];
|
|
3702
|
+
if (heater.isActive === false) continue;
|
|
3703
|
+
let isOn = false;
|
|
3704
|
+
//let sensorTemp = state.temps.waterSensor1;
|
|
3705
|
+
//if (body.id === 4) sensorTemp = state.temps.waterSensor4;
|
|
3706
|
+
//if (body.id === 3) sensorTemp = state.temps.waterSensor3;
|
|
3707
|
+
//if (body.id === 2 && !sys.equipment.shared) sensorTemp = state.temps.waterSensor2;
|
|
3708
|
+
|
|
3709
|
+
// Determine whether the heater can be used on this body.
|
|
3710
|
+
let isAssociated = false;
|
|
3711
|
+
let b = sys.board.valueMaps.bodies.transform(heater.body);
|
|
3712
|
+
switch (b.name) {
|
|
3713
|
+
case 'body1':
|
|
3714
|
+
case 'pool':
|
|
3715
|
+
if (body.id === 1) isAssociated = true;
|
|
3716
|
+
break;
|
|
3717
|
+
case 'body2':
|
|
3718
|
+
case 'spa':
|
|
3719
|
+
if (body.id === 2) isAssociated = true;
|
|
3720
|
+
break;
|
|
3721
|
+
case 'poolspa':
|
|
3722
|
+
if (body.id === 1 || body.id === 2) isAssociated = true;
|
|
3723
|
+
break;
|
|
3724
|
+
case 'body3':
|
|
3725
|
+
if (body.id === 3) isAssociated = true;
|
|
3726
|
+
break;
|
|
3727
|
+
case 'body4':
|
|
3728
|
+
if (body.id === 4) isAssociated = true;
|
|
3729
|
+
break;
|
|
3730
|
+
}
|
|
3731
|
+
// logger.silly(`Heater ${heater.name} is ${isAssociated === true ? '' : 'not '}associated with ${body.name}`);
|
|
3732
|
+
if (isAssociated) {
|
|
3733
|
+
let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
|
|
3734
|
+
let hstate = state.heaters.getItemById(heater.id, true);
|
|
3735
|
+
if (heater.master === 1) {
|
|
3736
|
+
if (hstatus !== 'cooldown') {
|
|
3737
|
+
// We need to do our own calculation as to whether it is on. This is for Nixie heaters.
|
|
3738
|
+
switch (htype.name) {
|
|
3739
|
+
case 'solar':
|
|
3740
|
+
if (mode === 'solar' || mode === 'solarpref') {
|
|
3741
|
+
// Measure up against start and stop temp deltas for effective solar heating.
|
|
3742
|
+
if (body.temp < cfgBody.heatSetpoint &&
|
|
3743
|
+
state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
|
|
3744
|
+
isOn = true;
|
|
3745
|
+
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
|
|
3746
|
+
isHeating = true;
|
|
3747
|
+
}
|
|
3748
|
+
else if (heater.coolingEnabled && body.temp > cfgBody.coolSetpoint && state.heliotrope.isNight &&
|
|
3749
|
+
state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
|
|
3750
|
+
isOn = true;
|
|
3751
|
+
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
|
|
3752
|
+
isHeating = true;
|
|
3753
|
+
isCooling = true;
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
break;
|
|
3757
|
+
case 'ultratemp':
|
|
3758
|
+
// We need to determine whether we are going to use the air temp or the solar temp
|
|
3759
|
+
// for the sensor.
|
|
3760
|
+
let deltaTemp = Math.max(state.temps.air, state.temps.solar || 0);
|
|
3761
|
+
if (mode === 'ultratemp' || mode === 'ultratemppref') {
|
|
3762
|
+
if (body.temp < cfgBody.heatSetpoint &&
|
|
3763
|
+
deltaTemp > body.temp + heater.differentialTemp || 0) {
|
|
3764
|
+
isOn = true;
|
|
3765
|
+
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
|
|
3766
|
+
isHeating = true;
|
|
3767
|
+
isCooling = false;
|
|
3768
|
+
}
|
|
3769
|
+
else if (body.temp > cfgBody.coolSetpoint && heater.coolingEnabled) {
|
|
3770
|
+
isOn = true;
|
|
3771
|
+
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
|
|
3772
|
+
isHeating = true;
|
|
3773
|
+
isCooling = true;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
break;
|
|
3777
|
+
case 'mastertemp':
|
|
3778
|
+
if (mode === 'mtheater') {
|
|
3779
|
+
if (body.temp < cfgBody.setPoint) {
|
|
3780
|
+
isOn = true;
|
|
3781
|
+
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('mtheat');
|
|
3782
|
+
isHeating = true;
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
break;
|
|
3786
|
+
case 'maxetherm':
|
|
3787
|
+
case 'gas':
|
|
3788
|
+
if (mode === 'heater') {
|
|
3789
|
+
if (body.temp < cfgBody.setPoint) {
|
|
3790
|
+
isOn = true;
|
|
3791
|
+
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
3792
|
+
isHeating = true;
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
else if (mode === 'solarpref' || mode === 'heatpumppref') {
|
|
3796
|
+
// If solar should be running gas heater should be off.
|
|
3797
|
+
if (body.temp < cfgBody.setPoint &&
|
|
3798
|
+
state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) isOn = false;
|
|
3799
|
+
else if (body.temp < cfgBody.setPoint) {
|
|
3800
|
+
isOn = true;
|
|
3801
|
+
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
3802
|
+
isHeating = true;
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
break;
|
|
3806
|
+
case 'heatpump':
|
|
3807
|
+
if (mode === 'heatpump' || mode === 'heatpumppref') {
|
|
3808
|
+
if (body.temp < cfgBody.setPoint &&
|
|
3809
|
+
state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
|
|
3810
|
+
isOn = true;
|
|
3811
|
+
body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
3812
|
+
isHeating = true;
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
break;
|
|
3816
|
+
default:
|
|
3817
|
+
isOn = utils.makeBool(hstate.isOn);
|
|
3818
|
+
break;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
logger.debug(`Heater Type: ${htype.name} Mode:${mode} Temp: ${body.temp} Setpoint: ${cfgBody.setPoint} Status: ${body.heatStatus}`);
|
|
3822
|
+
}
|
|
3823
|
+
else {
|
|
3824
|
+
let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
|
|
3825
|
+
switch (htype.name) {
|
|
3826
|
+
case 'mastertemp':
|
|
3827
|
+
if (hstatus === 'mtheat') isHeating = isOn = true;
|
|
3828
|
+
break;
|
|
3829
|
+
case 'maxetherm':
|
|
3830
|
+
case 'gas':
|
|
3831
|
+
if (hstatus === 'heater') isHeating = isOn = true;
|
|
3832
|
+
break;
|
|
3833
|
+
case 'hybrid':
|
|
3834
|
+
case 'ultratemp':
|
|
3835
|
+
case 'heatpump':
|
|
3836
|
+
if (mode === 'ultratemp' || mode === 'ultratemppref' || mode === 'heatpump' || mode === 'heatpumppref') {
|
|
3837
|
+
if (hstatus === 'heater') isHeating = isOn = true;
|
|
3838
|
+
else if (hstatus === 'cooling') isCooling = isOn = true;
|
|
3839
|
+
}
|
|
3840
|
+
break;
|
|
3841
|
+
case 'solar':
|
|
3842
|
+
if (mode === 'solar' || mode === 'solarpref') {
|
|
3843
|
+
if (hstatus === 'solar') isHeating = isOn = true;
|
|
3844
|
+
else if (hstatus === 'cooling') isCooling = isOn = true;
|
|
3845
|
+
}
|
|
3846
|
+
break;
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
if (isOn === true && typeof hon.find(elem => elem === heater.id) === 'undefined') {
|
|
3850
|
+
hon.push(heater.id);
|
|
3851
|
+
if (heater.master === 1 && isOn) (async () => {
|
|
3852
|
+
try {
|
|
3853
|
+
if (sys.board.valueMaps.heatStatus.getName(body.heatStatus) === 'cooldown')
|
|
3854
|
+
await ncp.heaters.setHeaterStateAsync(hstate, false, false);
|
|
3855
|
+
else if (isOn) {
|
|
3856
|
+
hstate.bodyId = body.id;
|
|
3857
|
+
await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
|
|
3858
|
+
}
|
|
3859
|
+
else if (hstate.isOn !== isOn || hstate.isCooling !== isCooling) {
|
|
3860
|
+
await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
|
|
3861
|
+
}
|
|
3862
|
+
} catch (err) { logger.error(err.message); }
|
|
3863
|
+
})();
|
|
3864
|
+
else {
|
|
3865
|
+
hstate.isOn = isOn;
|
|
3866
|
+
hstate.bodyId = body.id;
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
if (sys.controllerType === ControllerType.Nixie && !isHeating && !isCooling && hstatus !== 'cooldown') body.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
|
|
3872
|
+
|
|
3873
|
+
}
|
|
3874
|
+
else if (sys.controllerType === ControllerType.Nixie) body.heatStatus = 0;
|
|
3875
|
+
}
|
|
3876
|
+
// Turn off any heaters that should be off. The code above only turns heaters on.
|
|
3877
|
+
for (let i = 0; i < heaters.length; i++) {
|
|
3878
|
+
let heater: Heater = heaters[i];
|
|
3879
|
+
if (typeof hon.find(elem => elem === heater.id) === 'undefined') {
|
|
3880
|
+
let hstate = state.heaters.getItemById(heater.id, true);
|
|
3881
|
+
if (heater.master === 1) (async () => {
|
|
3882
|
+
try {
|
|
3883
|
+
await ncp.heaters.setHeaterStateAsync(hstate, false, false);
|
|
3884
|
+
hstate.bodyId = 0;
|
|
3885
|
+
} catch (err) { logger.error(err.message); }
|
|
3886
|
+
})();
|
|
3887
|
+
else {
|
|
3888
|
+
hstate.isOn = false;
|
|
3889
|
+
hstate.bodyId = 0;
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
} catch (err) { logger.error(`Error synchronizing heater states: ${err.message}`); }
|
|
3894
|
+
}
|
|
2915
3895
|
}
|
|
2916
3896
|
export class ValveCommands extends BoardCommands {
|
|
3897
|
+
public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
|
|
3898
|
+
try {
|
|
3899
|
+
// First delete the valves that should be removed.
|
|
3900
|
+
for (let i = 0; i < ctx.valves.remove.length; i++) {
|
|
3901
|
+
let v = ctx.valves.remove[i];
|
|
3902
|
+
try {
|
|
3903
|
+
await sys.board.valves.deleteValveAsync(v);
|
|
3904
|
+
res.addModuleSuccess('valve', `Remove: ${v.id}-${v.name}`);
|
|
3905
|
+
} catch (err) { res.addModuleError('valve', `Remove: ${v.id}-${v.name}: ${err.message}`); }
|
|
3906
|
+
}
|
|
3907
|
+
for (let i = 0; i < ctx.valves.update.length; i++) {
|
|
3908
|
+
let v = ctx.valves.update[i];
|
|
3909
|
+
try {
|
|
3910
|
+
await sys.board.valves.setValveAsync(v);
|
|
3911
|
+
res.addModuleSuccess('valve', `Update: ${v.id}-${v.name}`);
|
|
3912
|
+
} catch (err) { res.addModuleError('valve', `Update: ${v.id}-${v.name}: ${err.message}`); }
|
|
3913
|
+
}
|
|
3914
|
+
for (let i = 0; i < ctx.valves.add.length; i++) {
|
|
3915
|
+
let v = ctx.valves.add[i];
|
|
3916
|
+
try {
|
|
3917
|
+
// pull a little trick to first add the data then perform the update. This way we won't get a new id or
|
|
3918
|
+
// it won't error out.
|
|
3919
|
+
sys.valves.getItemById(ctx.valves.add[i].id, true);
|
|
3920
|
+
await sys.board.valves.setValveAsync(v);
|
|
3921
|
+
res.addModuleSuccess('valve', `Add: ${v.id}-${v.name}`);
|
|
3922
|
+
} catch (err) { res.addModuleError('valve', `Add: ${v.id}-${v.name}: ${err.message}`); }
|
|
3923
|
+
}
|
|
3924
|
+
return true;
|
|
3925
|
+
} catch (err) { logger.error(`Error restoring valves: ${err.message}`); res.addModuleError('system', `Error restoring valves: ${err.message}`); return false; }
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
|
|
3929
|
+
try {
|
|
3930
|
+
let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
3931
|
+
// Look at valves.
|
|
3932
|
+
let cfg = rest.poolConfig;
|
|
3933
|
+
for (let i = 0; i < cfg.valves.length; i++) {
|
|
3934
|
+
let r = cfg.valves[i];
|
|
3935
|
+
let c = sys.valves.find(elem => r.id === elem.id);
|
|
3936
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
3937
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
3938
|
+
}
|
|
3939
|
+
for (let i = 0; i < sys.valves.length; i++) {
|
|
3940
|
+
let c = sys.valves.getItemByIndex(i);
|
|
3941
|
+
let r = cfg.valves.find(elem => elem.id == c.id);
|
|
3942
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
3943
|
+
}
|
|
3944
|
+
return ctx;
|
|
3945
|
+
} catch (err) { logger.error(`Error validating valves for restore: ${err.message}`); }
|
|
3946
|
+
}
|
|
3947
|
+
|
|
2917
3948
|
public async setValveStateAsync(valve: Valve, vstate: ValveState, isDiverted: boolean) {
|
|
2918
3949
|
if (valve.master === 1) await ncp.valves.setValveStateAsync(vstate, isDiverted);
|
|
2919
3950
|
else
|
|
@@ -2958,39 +3989,146 @@ export class ValveCommands extends BoardCommands {
|
|
|
2958
3989
|
} catch (err) { return Promise.reject(new Error(`Error deleting valve: ${err.message}`)); }
|
|
2959
3990
|
// The following code will make sure we do not encroach on any valves defined by the OCP.
|
|
2960
3991
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3992
|
+
public async syncValveStates() {
|
|
3993
|
+
try {
|
|
3994
|
+
// Check to see if there is a drain circuit or feature on. If it is on then the intake will be diverted no mater what.
|
|
3995
|
+
let drain = sys.equipment.shared ? typeof state.circuits.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spadrain' && elem.isOn === true) !== 'undefined' ||
|
|
3996
|
+
typeof state.features.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spadrain' && elem.isOn === true) !== 'undefined' : false;
|
|
3997
|
+
// Check to see if there is a spillway circuit or feature on. If it is on then the return will be diverted no mater what.
|
|
3998
|
+
let spillway = sys.equipment.shared ? typeof state.circuits.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined' ||
|
|
3999
|
+
typeof state.features.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined' : false;
|
|
4000
|
+
let spa = sys.equipment.shared ? state.circuits.getItemById(1).isOn : false;
|
|
4001
|
+
let pool = sys.equipment.shared ? state.circuits.getItemById(6).isOn : false;
|
|
4002
|
+
// Set the valve mode.
|
|
4003
|
+
if (!sys.equipment.shared) state.valveMode = sys.board.valueMaps.valveModes.getValue('off');
|
|
4004
|
+
else if (drain) state.valveMode = sys.board.valueMaps.valveModes.getValue('spadrain');
|
|
4005
|
+
else if (spillway) state.valveMode = sys.board.valueMaps.valveModes.getValue('spillway');
|
|
4006
|
+
else if (spa) state.valveMode = sys.board.valueMaps.valveModes.getValue('spa');
|
|
4007
|
+
else if (pool) state.valveMode = sys.board.valueMaps.valveModes.getValue('pool');
|
|
4008
|
+
else state.valveMode = sys.board.valueMaps.valveModes.getValue('off');
|
|
4009
|
+
|
|
4010
|
+
for (let i = 0; i < sys.valves.length; i++) {
|
|
4011
|
+
// Run through all the valves to see whether they should be triggered or not.
|
|
4012
|
+
let valve = sys.valves.getItemByIndex(i);
|
|
4013
|
+
if (valve.isActive) {
|
|
4014
|
+
let vstate = state.valves.getItemById(valve.id, true);
|
|
4015
|
+
let isDiverted = vstate.isDiverted;
|
|
4016
|
+
if (typeof valve.circuit !== 'undefined' && valve.circuit > 0) {
|
|
4017
|
+
if (sys.equipment.shared && valve.isIntake === true) {
|
|
4018
|
+
// Valve Diverted Positions
|
|
4019
|
+
// Spa: Y
|
|
4020
|
+
// Drain: Y
|
|
4021
|
+
// Spillway: N
|
|
4022
|
+
// Pool: N
|
|
4023
|
+
isDiverted = utils.makeBool(spa || drain); // If the spa is on then the intake is diverted.
|
|
4024
|
+
}
|
|
4025
|
+
else if (sys.equipment.shared && valve.isReturn === true) {
|
|
4026
|
+
// Valve Diverted Positions
|
|
4027
|
+
// Spa: Y
|
|
4028
|
+
// Drain: N
|
|
4029
|
+
// Spillway: Y
|
|
4030
|
+
// Pool: N
|
|
4031
|
+
isDiverted = utils.makeBool((spa || spillway) && !drain);
|
|
4032
|
+
}
|
|
4033
|
+
else {
|
|
4034
|
+
let circ = state.circuits.getInterfaceById(valve.circuit);
|
|
4035
|
+
isDiverted = utils.makeBool(circ.isOn);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
else
|
|
4039
|
+
isDiverted = false;
|
|
4040
|
+
vstate.type = valve.type;
|
|
4041
|
+
vstate.name = valve.name;
|
|
4042
|
+
await sys.board.valves.setValveStateAsync(valve, vstate, isDiverted);
|
|
4043
|
+
}
|
|
2977
4044
|
}
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
4045
|
+
} catch (err) { logger.error(`syncValveStates: Error synchronizing valves ${err.message}`); }
|
|
4046
|
+
}
|
|
4047
|
+
public getBodyValveCircuitIds(isOn?: boolean): number[] {
|
|
4048
|
+
let arrIds: number[] = [];
|
|
4049
|
+
if (sys.equipment.shared !== true) return arrIds;
|
|
4050
|
+
|
|
4051
|
+
{
|
|
4052
|
+
let dtype = sys.board.valueMaps.circuitFunctions.getValue('spadrain');
|
|
4053
|
+
let stype = sys.board.valueMaps.circuitFunctions.getValue('spillway');
|
|
4054
|
+
let ptype = sys.board.valueMaps.circuitFunctions.getValue('pool');
|
|
4055
|
+
let sptype = sys.board.valueMaps.circuitFunctions.getValue('spa');
|
|
4056
|
+
for (let i = 0; i < state.circuits.length; i++) {
|
|
4057
|
+
let cstate = state.circuits.getItemByIndex(i);
|
|
4058
|
+
if (typeof isOn === 'undefined' || cstate.isOn === isOn) {
|
|
4059
|
+
if (cstate.id === 1 || cstate.id === 6) arrIds.push(cstate.id);
|
|
4060
|
+
if (cstate.type === dtype || cstate.type === stype || cstate.type === ptype || cstate.type === sptype) arrIds.push(cstate.id);
|
|
4061
|
+
}
|
|
2981
4062
|
}
|
|
2982
|
-
}
|
|
2983
|
-
else
|
|
2984
|
-
isDiverted = false;
|
|
2985
|
-
vstate.type = valve.type;
|
|
2986
|
-
vstate.name = valve.name;
|
|
2987
|
-
await sys.board.valves.setValveStateAsync(valve, vstate, isDiverted);
|
|
2988
4063
|
}
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
4064
|
+
{
|
|
4065
|
+
let dtype = sys.board.valueMaps.featureFunctions.getValue('spadrain');
|
|
4066
|
+
let stype = sys.board.valueMaps.featureFunctions.getValue('spillway');
|
|
4067
|
+
for (let i = 0; i < state.features.length; i++) {
|
|
4068
|
+
let fstate = state.features.getItemByIndex(i);
|
|
4069
|
+
if (typeof isOn === 'undefined' || fstate.isOn === isOn) {
|
|
4070
|
+
if (fstate.type === dtype || fstate.type === stype) arrIds.push(fstate.id);
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
return arrIds;
|
|
4075
|
+
}
|
|
2992
4076
|
}
|
|
2993
4077
|
export class ChemControllerCommands extends BoardCommands {
|
|
4078
|
+
public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
|
|
4079
|
+
try {
|
|
4080
|
+
// First delete the chemControllers that should be removed.
|
|
4081
|
+
for (let i = 0; i < ctx.chemControllers.remove.length; i++) {
|
|
4082
|
+
let c = ctx.chemControllers.remove[i];
|
|
4083
|
+
try {
|
|
4084
|
+
await sys.board.chemControllers.deleteChemControllerAsync(c);
|
|
4085
|
+
res.addModuleSuccess('chemController', `Remove: ${c.id}-${c.name}`);
|
|
4086
|
+
} catch (err) { res.addModuleError('chemController', `Remove: ${c.id}-${c.name}: ${err.message}`); }
|
|
4087
|
+
}
|
|
4088
|
+
for (let i = 0; i < ctx.chemControllers.update.length; i++) {
|
|
4089
|
+
let c = ctx.chemControllers.update[i];
|
|
4090
|
+
try {
|
|
4091
|
+
await sys.board.chemControllers.setChemControllerAsync(c);
|
|
4092
|
+
res.addModuleSuccess('chemController', `Update: ${c.id}-${c.name}`);
|
|
4093
|
+
} catch (err) { res.addModuleError('chemController', `Update: ${c.id}-${c.name}: ${err.message}`); }
|
|
4094
|
+
}
|
|
4095
|
+
for (let i = 0; i < ctx.chemControllers.add.length; i++) {
|
|
4096
|
+
let c = ctx.chemControllers.add[i];
|
|
4097
|
+
try {
|
|
4098
|
+
// pull a little trick to first add the data then perform the update. This way we won't get a new id or
|
|
4099
|
+
// it won't error out.
|
|
4100
|
+
let chem = sys.chemControllers.getItemById(c.id, true);
|
|
4101
|
+
// RSG 11.24.21. setChemControllerAsync will only set the type/address if it thinks it's new.
|
|
4102
|
+
// For a restore, if we set the type/address here it will pass the validation steps.
|
|
4103
|
+
chem.type = c.type;
|
|
4104
|
+
// chem.address = c.address;
|
|
4105
|
+
await sys.board.chemControllers.setChemControllerAsync(c);
|
|
4106
|
+
res.addModuleSuccess('chemController', `Add: ${c.id}-${c.name}`);
|
|
4107
|
+
} catch (err) { res.addModuleError('chemController', `Add: ${c.id}-${c.name}: ${err.message}`); }
|
|
4108
|
+
}
|
|
4109
|
+
return true;
|
|
4110
|
+
} catch (err) { logger.error(`Error restoring chemControllers: ${err.message}`); res.addModuleError('system', `Error restoring chemControllers: ${err.message}`); return false; }
|
|
4111
|
+
}
|
|
4112
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
|
|
4113
|
+
try {
|
|
4114
|
+
let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
4115
|
+
// Look at chemControllers.
|
|
4116
|
+
let cfg = rest.poolConfig;
|
|
4117
|
+
for (let i = 0; i < cfg.chemControllers.length; i++) {
|
|
4118
|
+
let r = cfg.chemControllers[i];
|
|
4119
|
+
let c = sys.chemControllers.find(elem => r.id === elem.id);
|
|
4120
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
4121
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
4122
|
+
}
|
|
4123
|
+
for (let i = 0; i < sys.chemControllers.length; i++) {
|
|
4124
|
+
let c = sys.chemControllers.getItemByIndex(i);
|
|
4125
|
+
let r = cfg.chemControllers.find(elem => elem.id == c.id);
|
|
4126
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
4127
|
+
}
|
|
4128
|
+
return ctx;
|
|
4129
|
+
} catch (err) { logger.error(`Error validating chemControllers for restore: ${err.message}`); }
|
|
4130
|
+
}
|
|
4131
|
+
|
|
2994
4132
|
public async deleteChemControllerAsync(data: any): Promise<ChemController> {
|
|
2995
4133
|
try {
|
|
2996
4134
|
let id = typeof data.id !== 'undefined' ? parseInt(data.id, 10) : -1;
|
|
@@ -3075,47 +4213,47 @@ export class ChemControllerCommands extends BoardCommands {
|
|
|
3075
4213
|
if (!isNaN(id)) return sys.chemControllers.find(x => x.id === id);
|
|
3076
4214
|
else if (!isNaN(address)) return sys.chemControllers.find(x => x.address === address);
|
|
3077
4215
|
}
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
4216
|
+
public async setChemControllerAsync(data: any): Promise<ChemController> {
|
|
4217
|
+
// The following are the rules related to when an OCP is present.
|
|
4218
|
+
// ==============================================================
|
|
4219
|
+
// 1. IntelliChem cannot be controlled/polled via Nixie, since there is no enable/disable from the OCP at this point we don't know who is in control of polling.
|
|
4220
|
+
// 2. With *Touch Commands will be sent directly to the IntelliChem controller in the hopes that the OCP will pick it up. Turns out this is not correct. The TouchBoard now has the proper interface.
|
|
4221
|
+
// 3. njspc will communicate to the OCP for IntelliChem control via the configuration interface.
|
|
3084
4222
|
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
4223
|
+
// The following are the rules related to when no OCP is present.
|
|
4224
|
+
// =============================================================
|
|
4225
|
+
// 1. All chemControllers will be controlled via Nixie (IntelliChem, REM Chem).
|
|
4226
|
+
try {
|
|
4227
|
+
let chem = sys.board.chemControllers.findChemController(data);
|
|
4228
|
+
let isAdd = typeof chem === 'undefined';
|
|
4229
|
+
let type = sys.board.valueMaps.chemControllerTypes.encode(isAdd ? data.type : chem.type);
|
|
4230
|
+
if (typeof type === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`The chem controller type could not be determined ${data.type || type}`, 'chemController', type));
|
|
4231
|
+
if (isAdd && sys.equipment.maxChemControllers <= sys.chemControllers.length) return Promise.reject(new InvalidEquipmentDataError(`The maximum number of chem controllers have been added to your controller`, 'chemController', sys.equipment.maxChemControllers));
|
|
4232
|
+
let address = typeof data.address !== 'undefined' ? parseInt(data.address, 10) : isAdd ? undefined : chem.address;
|
|
4233
|
+
let t = sys.board.valueMaps.chemControllerTypes.transform(type);
|
|
4234
|
+
if (t.hasAddress) {
|
|
4235
|
+
// First lets make sure the user supplied an address.
|
|
4236
|
+
if (isNaN(address)) return Promise.reject(new InvalidEquipmentDataError(`${t.desc} chem controllers require a valid address`, 'chemController', data.address));
|
|
4237
|
+
if (typeof sys.chemControllers.find(x => x.address === address && x.id !== (isAdd ? -1 : chem.id)) !== 'undefined') return Promise.reject(new InvalidEquipmentDataError(`${type.desc} chem controller addresses must be unique`, 'chemController', data.address));
|
|
4238
|
+
}
|
|
4239
|
+
if (isAdd) {
|
|
4240
|
+
// At this point we are going to add the chem controller no matter what.
|
|
4241
|
+
data.id = sys.chemControllers.getNextControllerId(type);
|
|
4242
|
+
chem = sys.chemControllers.getItemById(data.id, true);
|
|
4243
|
+
chem.type = type;
|
|
4244
|
+
if (t.hasAddress) chem.address = address;
|
|
4245
|
+
}
|
|
4246
|
+
chem.isActive = true;
|
|
4247
|
+
// So here is the thing. If you have an OCP then the IntelliChem must be controlled by that.
|
|
4248
|
+
// the messages on the bus will talk back to the OCP so if you do not do this mayhem will ensue.
|
|
4249
|
+
if (type.name === 'intellichem')
|
|
4250
|
+
await this.setIntelliChemAsync(data);
|
|
4251
|
+
else
|
|
4252
|
+
await ncp.chemControllers.setControllerAsync(chem, data);
|
|
4253
|
+
return Promise.resolve(chem);
|
|
4254
|
+
}
|
|
4255
|
+
catch (err) { return Promise.reject(err); }
|
|
3116
4256
|
}
|
|
3117
|
-
catch (err) { return Promise.reject(err); }
|
|
3118
|
-
}
|
|
3119
4257
|
public async setChemControllerStateAsync(data: any): Promise<ChemControllerState> {
|
|
3120
4258
|
// For the most part all of the settable settings for IntelliChem are config settings. REM is a bit of a different story so that
|
|
3121
4259
|
// should map to the ncp
|
|
@@ -3129,21 +4267,147 @@ export class ChemControllerCommands extends BoardCommands {
|
|
|
3129
4267
|
}
|
|
3130
4268
|
}
|
|
3131
4269
|
export class FilterCommands extends BoardCommands {
|
|
3132
|
-
public async
|
|
4270
|
+
public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
|
|
4271
|
+
try {
|
|
4272
|
+
// First delete the filters that should be removed.
|
|
4273
|
+
for (let i = 0; i < ctx.filters.remove.length; i++) {
|
|
4274
|
+
let filter = ctx.filters.remove[i];
|
|
4275
|
+
try {
|
|
4276
|
+
sys.filters.removeItemById(filter.id);
|
|
4277
|
+
state.filters.removeItemById(filter.id);
|
|
4278
|
+
res.addModuleSuccess('filter', `Remove: ${filter.id}-${filter.name}`);
|
|
4279
|
+
} catch (err) { res.addModuleError('filter', `Remove: ${filter.id}-${filter.name}: ${err.message}`); }
|
|
4280
|
+
}
|
|
4281
|
+
for (let i = 0; i < ctx.filters.update.length; i++) {
|
|
4282
|
+
let filter = ctx.filters.update[i];
|
|
4283
|
+
try {
|
|
4284
|
+
await sys.board.filters.setFilterAsync(filter);
|
|
4285
|
+
res.addModuleSuccess('filter', `Update: ${filter.id}-${filter.name}`);
|
|
4286
|
+
} catch (err) { res.addModuleError('filter', `Update: ${filter.id}-${filter.name}: ${err.message}`); }
|
|
4287
|
+
}
|
|
4288
|
+
for (let i = 0; i < ctx.filters.add.length; i++) {
|
|
4289
|
+
let filter = ctx.filters.add[i];
|
|
4290
|
+
try {
|
|
4291
|
+
// pull a little trick to first add the data then perform the update.
|
|
4292
|
+
sys.filters.getItemById(filter.id, true);
|
|
4293
|
+
await sys.board.filters.setFilterAsync(filter);
|
|
4294
|
+
res.addModuleSuccess('filter', `Add: ${filter.id}-${filter.name}`);
|
|
4295
|
+
} catch (err) { res.addModuleError('filter', `Add: ${filter.id}-${filter.name}: ${err.message}`); }
|
|
4296
|
+
}
|
|
4297
|
+
return true;
|
|
4298
|
+
} catch (err) { logger.error(`Error restoring filters: ${err.message}`); res.addModuleError('system', `Error restoring filters: ${err.message}`); return false; }
|
|
4299
|
+
}
|
|
4300
|
+
public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
|
|
3133
4301
|
try {
|
|
4302
|
+
let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
|
|
4303
|
+
// Look at filters.
|
|
4304
|
+
let cfg = rest.poolConfig;
|
|
4305
|
+
for (let i = 0; i < cfg.filters.length; i++) {
|
|
4306
|
+
let r = cfg.filters[i];
|
|
4307
|
+
let c = sys.filters.find(elem => r.id === elem.id);
|
|
4308
|
+
if (typeof c === 'undefined') ctx.add.push(r);
|
|
4309
|
+
else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
|
|
4310
|
+
}
|
|
3134
4311
|
for (let i = 0; i < sys.filters.length; i++) {
|
|
3135
|
-
|
|
3136
|
-
let
|
|
3137
|
-
if (
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
4312
|
+
let c = sys.filters.getItemByIndex(i);
|
|
4313
|
+
let r = cfg.filters.find(elem => elem.id == c.id);
|
|
4314
|
+
if (typeof r === 'undefined') ctx.remove.push(c.get(true));
|
|
4315
|
+
}
|
|
4316
|
+
return ctx;
|
|
4317
|
+
} catch (err) { logger.error(`Error validating filters for restore: ${err.message}`); }
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
public async syncFilterStates() {
|
|
4321
|
+
try {
|
|
4322
|
+
for (let i = 0; i < sys.filters.length; i++) {
|
|
4323
|
+
// Run through all the valves to see whether they should be triggered or not.
|
|
4324
|
+
let filter = sys.filters.getItemByIndex(i);
|
|
4325
|
+
if (filter.isActive && !isNaN(filter.id)) {
|
|
4326
|
+
let fstate = state.filters.getItemById(filter.id, true);
|
|
4327
|
+
// Check to see if the associated body is on.
|
|
4328
|
+
await sys.board.filters.setFilterStateAsync(filter, fstate, sys.board.bodies.isBodyOn(filter.body));
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
} catch (err) { logger.error(`syncFilterStates: Error synchronizing filters ${err.message}`); }
|
|
4332
|
+
}
|
|
4333
|
+
public async setFilterPressure(id: number, pressure: number, units?: string) {
|
|
4334
|
+
try {
|
|
4335
|
+
let filter = sys.filters.find(elem => elem.id === id);
|
|
4336
|
+
if (typeof filter === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`setFilterPressure: Invalid equipmentId ${id}`, id, 'Filter'));
|
|
4337
|
+
if (isNaN(pressure)) return Promise.reject(new InvalidEquipmentDataError(`setFilterPressure: Invalid filter pressure ${pressure} for ${filter.name}`, 'Filter', pressure));
|
|
4338
|
+
let sfilter = state.filters.getItemById(filter.id, true);
|
|
4339
|
+
// Convert the pressure to the units that we have set on the filter for the pressure units.
|
|
4340
|
+
let pu = sys.board.valueMaps.pressureUnits.transform(filter.pressureUnits || 0);
|
|
4341
|
+
if (typeof units === 'undefined' || units === '') units = pu.name;
|
|
4342
|
+
sfilter.pressureUnits = filter.pressureUnits;
|
|
4343
|
+
sfilter.pressure = Math.round(pressure * 1000) / 1000; // Round this to 3 decimal places just in case we are getting stupid scales.
|
|
4344
|
+
// Check to see if our circuit is the only thing on. If it is then we will be setting our current clean pressure to the incoming pressure and calculating a percentage.
|
|
4345
|
+
// Rules for the circuit.
|
|
4346
|
+
// 1. The assigned circuit must be on.
|
|
4347
|
+
// 2. There must not be a current freeze condition
|
|
4348
|
+
// 3. No heaters can be on.
|
|
4349
|
+
// 4. The assigned circuit must be on exclusively but we will be ignoring any of the light circuit types for the exclusivity.
|
|
4350
|
+
let cstate = state.circuits.getInterfaceById(filter.pressureCircuitId);
|
|
4351
|
+
if (cstate.isOn && state.freeze !== true) {
|
|
4352
|
+
// Ok so our circuit is on. We need to check to see if any other circuits are on. This includes heaters. The reason for this is that even with
|
|
4353
|
+
// a gas heater there may be a heater bypass that will screw up our numbers. Certainly reflow on a solar heater will skew the numbers.
|
|
4354
|
+
let hon = state.temps.bodies.toArray().find(elem => elem.isOn && (elem.heatStatus || 0) !== 0);
|
|
4355
|
+
if (typeof hon === 'undefined') {
|
|
4356
|
+
// Put together the circuit types that could be lights. We don't want these.
|
|
4357
|
+
let ctypes = [];
|
|
4358
|
+
let funcs = sys.board.valueMaps.circuitFunctions.toArray();
|
|
4359
|
+
for (let i = 0; i < funcs.length; i++) {
|
|
4360
|
+
let f = funcs[i];
|
|
4361
|
+
if (f.isLight) ctypes.push(f.val);
|
|
4362
|
+
}
|
|
4363
|
+
let con = state.circuits.find(elem => elem.isOn === true && elem.id !== filter.pressureCircuitId && elem.id !== 1 && elem.id !== 6 && !ctypes.includes(elem.type));
|
|
4364
|
+
if (typeof con === 'undefined') {
|
|
4365
|
+
// This check is the one that will be the most problematic. For this reason we are only going to check features that are not generic. If they are spillway
|
|
4366
|
+
// it definitely has to be off.
|
|
4367
|
+
let feats = state.features.toArray();
|
|
4368
|
+
let fon = false;
|
|
4369
|
+
for (let i = 0; i < feats.length && fon === false; i++) {
|
|
4370
|
+
let f = feats[i];
|
|
4371
|
+
if (!f.isOn) continue;
|
|
4372
|
+
if (f.id === filter.pressureCircuitId) continue;
|
|
4373
|
+
if (f.type !== 0) fon = true;
|
|
4374
|
+
// Check to see if this feature is used on a valve. This will make it
|
|
4375
|
+
// not include this pressure either. We do not care whether the valve is diverted or not.
|
|
4376
|
+
if (typeof sys.valves.find(elem => elem.circuitId === f.id) !== 'undefined')
|
|
4377
|
+
fon = true;
|
|
4378
|
+
else {
|
|
4379
|
+
// Finally if the feature happens to be used on a pump then we don't want it either.
|
|
4380
|
+
let pumps = sys.pumps.get();
|
|
4381
|
+
for (let j = 0; j < pumps.length; j++) {
|
|
4382
|
+
let pmp = pumps[j];
|
|
4383
|
+
if (typeof pmp.circuits !== 'undefined') {
|
|
4384
|
+
if (typeof pmp.circuits.find(elem => elem.circuit === f.id) !== 'undefined') {
|
|
4385
|
+
fon = true;
|
|
4386
|
+
break;
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
if (!fon) {
|
|
4393
|
+
// Finally we have a value we can believe in.
|
|
4394
|
+
sfilter.refPressure = pressure;
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
else {
|
|
4398
|
+
logger.verbose(`Circuit ${con.id}-${con.name} is currently on filter pressure for cleaning ignored.`);
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
else {
|
|
4402
|
+
logger.verbose(`Heater for body ${hon.name} is currently on ${hon.heatStatus} filter pressure for cleaning skipped.`);
|
|
3141
4403
|
}
|
|
3142
4404
|
}
|
|
3143
|
-
|
|
4405
|
+
sfilter.emitEquipmentChange();
|
|
4406
|
+
}
|
|
4407
|
+
catch (err) { logger.error(`setFilterPressure: Error setting filter #${id} pressure to ${pressure}${units || ''}`); }
|
|
3144
4408
|
}
|
|
3145
4409
|
public async setFilterStateAsync(filter: Filter, fstate: FilterState, isOn: boolean) { fstate.isOn = isOn; }
|
|
3146
|
-
public
|
|
4410
|
+
public async setFilterAsync(data: any): Promise<Filter> {
|
|
3147
4411
|
let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
|
|
3148
4412
|
if (id <= 0) id = sys.filters.length + 1; // set max filters?
|
|
3149
4413
|
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid filter id: ${data.id}`, data.id, 'Filter'));
|
|
@@ -3152,50 +4416,48 @@ export class FilterCommands extends BoardCommands {
|
|
|
3152
4416
|
let filterType = typeof data.filterType !== 'undefined' ? parseInt(data.filterType, 10) : filter.filterType;
|
|
3153
4417
|
if (typeof filterType === 'undefined') filterType = sys.board.valueMaps.filterTypes.getValue('unknown');
|
|
3154
4418
|
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
}
|
|
4419
|
+
// The only way to delete a filter is to call deleteFilterAsync.
|
|
4420
|
+
//if (typeof data.isActive !== 'undefined') {
|
|
4421
|
+
// if (utils.makeBool(data.isActive) === false) {
|
|
4422
|
+
// sys.filters.removeItemById(id);
|
|
4423
|
+
// state.filters.removeItemById(id);
|
|
4424
|
+
// return;
|
|
4425
|
+
// }
|
|
4426
|
+
//}
|
|
3162
4427
|
|
|
3163
4428
|
let body = typeof data.body !== 'undefined' ? data.body : filter.body;
|
|
3164
4429
|
let name = typeof data.name !== 'undefined' ? data.name : filter.name;
|
|
3165
|
-
|
|
3166
|
-
let psi = typeof data.psi !== 'undefined' ? parseFloat(data.psi) : sfilter.psi;
|
|
3167
|
-
let lastCleanDate = typeof data.lastCleanDate !== 'undefined' ? data.lastCleanDate : sfilter.lastCleanDate;
|
|
3168
|
-
let filterPsi = typeof data.filterPsi !== 'undefined' ? parseInt(data.filterPsi, 10) : sfilter.filterPsi;
|
|
3169
|
-
let needsCleaning = typeof data.needsCleaning !== 'undefined' ? data.needsCleaning : sfilter.needsCleaning;
|
|
3170
|
-
|
|
3171
|
-
// Ensure all the defaults.
|
|
3172
|
-
if (isNaN(psi)) psi = 0;
|
|
3173
4430
|
if (typeof body === 'undefined') body = 32;
|
|
3174
|
-
|
|
3175
4431
|
// At this point we should have all the data. Validate it.
|
|
3176
4432
|
if (!sys.board.valueMaps.filterTypes.valExists(filterType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid filter type; ${filterType}`, 'Filter', filterType));
|
|
3177
4433
|
|
|
4434
|
+
filter.pressureUnits = typeof data.pressureUnits !== 'undefined' ? data.pressureUnits || 0 : filter.pressureUnits || 0;
|
|
4435
|
+
filter.pressureCircuitId = parseInt(data.pressureCircuitId || filter.pressureCircuitId || 6, 10);
|
|
4436
|
+
filter.cleanPressure = parseFloat(data.cleanPressure || filter.cleanPressure || 0);
|
|
4437
|
+
filter.dirtyPressure = parseFloat(data.dirtyPressure || filter.dirtyPressure || 0);
|
|
4438
|
+
|
|
3178
4439
|
filter.filterType = sfilter.filterType = filterType;
|
|
3179
4440
|
filter.body = sfilter.body = body;
|
|
3180
|
-
filter.filterType = sfilter.filterType = filterType;
|
|
3181
4441
|
filter.name = sfilter.name = name;
|
|
3182
4442
|
filter.capacity = typeof data.capacity === 'number' ? data.capacity : filter.capacity;
|
|
3183
4443
|
filter.capacityUnits = typeof data.capacityUnits !== 'undefined' ? data.capacityUnits : filter.capacity;
|
|
3184
|
-
sfilter.psi = psi;
|
|
3185
|
-
sfilter.filterPsi = filterPsi;
|
|
3186
|
-
filter.needsCleaning = sfilter.needsCleaning = needsCleaning;
|
|
3187
|
-
filter.lastCleanDate = sfilter.lastCleanDate = lastCleanDate;
|
|
3188
4444
|
filter.connectionId = typeof data.connectionId !== 'undefined' ? data.connectionId : filter.connectionId;
|
|
3189
4445
|
filter.deviceBinding = typeof data.deviceBinding !== 'undefined' ? data.deviceBinding : filter.deviceBinding;
|
|
4446
|
+
sfilter.pressureUnits = filter.pressureUnits;
|
|
4447
|
+
sfilter.calcCleanPercentage();
|
|
3190
4448
|
sfilter.emitEquipmentChange();
|
|
3191
4449
|
return filter; // Always return the config when we are dealing with the config not state.
|
|
3192
4450
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
4451
|
+
public async deleteFilterAsync(data: any): Promise<Filter> {
|
|
4452
|
+
try {
|
|
4453
|
+
let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
|
|
4454
|
+
let filter = sys.filters.getItemById(id);
|
|
4455
|
+
let sfilter = state.filters.getItemById(filter.id);
|
|
4456
|
+
filter.isActive = false;
|
|
4457
|
+
sys.filters.removeItemById(id);
|
|
4458
|
+
state.filters.removeItemById(id);
|
|
4459
|
+
sfilter.emitEquipmentChange();
|
|
4460
|
+
return filter;
|
|
4461
|
+
} catch (err) { logger.error(`deleteFilterAsync: Error deleting filter ${err.message}`); }
|
|
3200
4462
|
}
|
|
3201
|
-
}
|
|
4463
|
+
}
|