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.
Files changed (60) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/Changelog +23 -0
  3. package/README.md +5 -5
  4. package/app.ts +2 -0
  5. package/config/Config.ts +3 -0
  6. package/config/VersionCheck.ts +8 -4
  7. package/controller/Constants.ts +88 -0
  8. package/controller/Equipment.ts +246 -66
  9. package/controller/Errors.ts +24 -1
  10. package/controller/Lockouts.ts +423 -0
  11. package/controller/State.ts +314 -54
  12. package/controller/boards/EasyTouchBoard.ts +107 -59
  13. package/controller/boards/IntelliCenterBoard.ts +186 -125
  14. package/controller/boards/IntelliTouchBoard.ts +104 -30
  15. package/controller/boards/NixieBoard.ts +721 -159
  16. package/controller/boards/SystemBoard.ts +2370 -1108
  17. package/controller/comms/Comms.ts +85 -10
  18. package/controller/comms/messages/Messages.ts +10 -4
  19. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -4
  20. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  21. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  22. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  23. package/controller/comms/messages/config/ExternalMessage.ts +44 -26
  24. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  25. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  26. package/controller/comms/messages/config/HeaterMessage.ts +15 -9
  27. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  28. package/controller/comms/messages/config/OptionsMessage.ts +13 -1
  29. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  30. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  31. package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
  32. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  33. package/controller/comms/messages/config/ValveMessage.ts +13 -3
  34. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
  35. package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
  36. package/controller/comms/messages/status/HeaterStateMessage.ts +42 -9
  37. package/controller/comms/messages/status/IntelliChemStateMessage.ts +37 -26
  38. package/controller/nixie/Nixie.ts +18 -16
  39. package/controller/nixie/bodies/Body.ts +4 -1
  40. package/controller/nixie/chemistry/ChemController.ts +80 -77
  41. package/controller/nixie/chemistry/Chlorinator.ts +9 -8
  42. package/controller/nixie/circuits/Circuit.ts +55 -6
  43. package/controller/nixie/heaters/Heater.ts +192 -32
  44. package/controller/nixie/pumps/Pump.ts +146 -84
  45. package/controller/nixie/schedules/Schedule.ts +3 -2
  46. package/controller/nixie/valves/Valve.ts +1 -1
  47. package/defaultConfig.json +32 -1
  48. package/issue_template.md +1 -1
  49. package/logger/DataLogger.ts +37 -22
  50. package/package.json +20 -18
  51. package/web/Server.ts +520 -29
  52. package/web/bindings/influxDB.json +96 -8
  53. package/web/bindings/mqtt.json +151 -40
  54. package/web/bindings/mqttAlt.json +114 -4
  55. package/web/interfaces/httpInterface.ts +2 -0
  56. package/web/interfaces/influxInterface.ts +36 -19
  57. package/web/interfaces/mqttInterface.ts +14 -3
  58. package/web/services/config/Config.ts +171 -44
  59. package/web/services/state/State.ts +49 -5
  60. 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
- constructor() {
119
- this.pumpStatus.transform = function (byte) {
120
- // if (byte === 0) return this.get(0);
121
- if (byte === 0) return extend(true, {}, this.get(0), { val: byte });
122
- for (let b = 16; b > 0; b--) {
123
- let bit = (1 << (b - 1));
124
- if ((byte & bit) > 0) {
125
- let v = this.get(b);
126
- if (typeof v !== 'undefined') {
127
- return extend(true, {}, v, { val: byte });
128
- }
129
- }
130
- }
131
- return { val: byte, name: 'error' + byte, desc: 'Unspecified Error ' + byte };
132
- };
133
- this.chlorinatorStatus.transform = function (byte) {
134
- if (byte === 128) return { val: 128, name: 'commlost', desc: 'Communication Lost' };
135
- else if (byte === 0) return { val: 0, name: 'ok', desc: 'Ok' };
136
- for (let b = 8; b > 0; b--) {
137
- let bit = (1 << (b - 1));
138
- if ((byte & bit) > 0) {
139
- let v = this.get(b);
140
- if (typeof v !== "undefined") {
141
- return extend(true, {}, v, { val: byte & 0x00FF });
142
- }
143
- }
144
- }
145
- return { val: byte, name: 'unknown' + byte, desc: 'Unknown status ' + byte };
146
- };
147
- this.scheduleTypes.transform = function (byte) {
148
- return (byte & 128) > 0 ? extend(true, { val: 128 }, this.get(128)) : extend(true, { val: 0 }, this.get(0));
149
- };
150
- this.scheduleDays.transform = function (byte) {
151
- let days = [];
152
- let b = byte & 0x007F;
153
- for (let bit = 7; bit >= 0; bit--) {
154
- if ((byte & (1 << (bit - 1))) > 0) days.push(extend(true, {}, this.get(bit)));
155
- }
156
- return { val: b, days: days };
157
- };
158
- this.scheduleDays.toArray = function () {
159
- let arrKeys = Array.from(this.keys());
160
- let arr = [];
161
- for (let i = 0; i < arrKeys.length; i++) arr.push(extend(true, { val: arrKeys[i] }, this.get(arrKeys[i])));
162
- return arr;
163
- };
164
- this.virtualCircuits.transform = function (byte) {
165
- return extend(true, {}, { val: byte, name: 'Unknown ' + byte }, this.get(byte), { val: byte });
166
- };
167
- this.tempUnits.transform = function (byte) { return extend(true, {}, { val: byte & 0x04 }, this.get(byte & 0x04)); };
168
- this.panelModes.transform = function (byte) { return extend(true, { val: byte & 0x83 }, this.get(byte & 0x83)); };
169
- this.controllerStatus.transform = function (byte: number, percent?: number) {
170
- let v = extend(true, {}, this.get(byte) || this.get(0));
171
- if (typeof percent !== 'undefined') v.percent = percent;
172
- return v;
173
- };
174
- this.lightThemes.transform = function (byte) { return typeof byte === 'undefined' ? this.get(255) : extend(true, { val: byte }, this.get(byte) || this.get(255)); };
175
- this.timeZones.findItem = function (val: string | number | { val: any, name: string }) {
176
- if (typeof val === null || typeof val === 'undefined') return;
177
- else if (typeof val === 'number') {
178
- if (val <= 12) { // We are looking for timezones based upon the utcOffset.
179
- let arr = this.toArray();
180
- let tz = arr.find(elem => elem.utcOffset === val);
181
- return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined;
182
- }
183
- return this.transform(val);
184
- }
185
- else if (typeof val === 'string') {
186
- let v = parseInt(val, 10);
187
- if (!isNaN(v)) {
188
- if (v <= 12) {
189
- let arr = this.toArray();
190
- let tz = arr.find(elem => elem.utcOffset === val);
191
- return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined;
192
- }
193
- return this.transform(v);
194
- }
195
- else {
196
- let arr = this.toArray();
197
- let tz = arr.find(elem => elem.abbrev === val || elem.name === val);
198
- return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined;
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
- public expansionBoards: byteValueMap = new byteValueMap();
208
- // Identifies which controller manages the underlying equipment.
209
- public equipmentMaster: byteValueMap = new byteValueMap([
210
- [0, { val: 0, name: 'ocp', desc: 'Outdoor Control Panel' }],
211
- [1, { val: 1, name: 'ncp', desc: 'Nixie Control Panel' }]
212
- ]);
213
- public equipmentCommStatus: byteValueMap = new byteValueMap([
214
- [0, { val: 0, name: 'ready', desc: 'Ready' }],
215
- [1, { val: 1, name: 'commerr', desc: 'Communication Error' }]
216
- ]);
217
- public panelModes: byteValueMap = new byteValueMap([
218
- [0, { val: 0, name: 'auto', desc: 'Auto' }],
219
- [1, { val: 1, name: 'service', desc: 'Service' }],
220
- [8, { val: 8, name: 'freeze', desc: 'Freeze' }],
221
- [128, { val: 128, name: 'timeout', desc: 'Timeout' }],
222
- [129, { val: 129, name: 'service-timeout', desc: 'Service/Timeout' }],
223
- [255, { name: 'error', desc: 'System Error' }]
224
- ]);
225
- public controllerStatus: byteValueMap = new byteValueMap([
226
- [0, { val: 0, name: 'initializing', desc: 'Initializing', percent: 0 }],
227
- [1, { val: 1, name: 'ready', desc: 'Ready', percent: 100 }],
228
- [2, { val: 2, name: 'loading', desc: 'Loading', percent: 0 }],
229
- [3, { val: 255, name: 'Error', desc: 'Error', percent: 0 }]
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
- public circuitFunctions: byteValueMap = new byteValueMap([
233
- [0, { name: 'generic', desc: 'Generic' }],
234
- [1, { name: 'spa', desc: 'Spa', hasHeatSource: true }],
235
- [2, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
236
- [5, { name: 'mastercleaner', desc: 'Master Cleaner' }],
237
- [7, { name: 'light', desc: 'Light', isLight: true }],
238
- [9, { name: 'samlight', desc: 'SAM Light', isLight: true }],
239
- [10, { name: 'sallight', desc: 'SAL Light', isLight: true }],
240
- [11, { name: 'photongen', desc: 'Photon Gen', isLight: true }],
241
- [12, { name: 'colorwheel', desc: 'Color Wheel', isLight: true }],
242
- [13, { name: 'valve', desc: 'Valve' }],
243
- [14, { name: 'spillway', desc: 'Spillway' }],
244
- [15, { name: 'floorcleaner', desc: 'Floor Cleaner' }],
245
- [16, { name: 'intellibrite', desc: 'Intellibrite', isLight: true }],
246
- [17, { name: 'magicstream', desc: 'Magicstream', isLight: true }],
247
- [19, { name: 'notused', desc: 'Not Used' }]
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
- // Feature functions are used as the available options to define a circuit.
251
- public featureFunctions: byteValueMap = new byteValueMap([[0, { name: 'generic', desc: 'Generic' }], [1, { name: 'spillway', desc: 'Spillway' }]]);
252
- public virtualCircuits: byteValueMap = new byteValueMap([
253
- [128, { name: 'solar', desc: 'Solar', assignableToPumpCircuit: true }],
254
- [129, { name: 'heater', desc: 'Either Heater', assignableToPumpCircuit: true }],
255
- [130, { name: 'poolHeater', desc: 'Pool Heater', assignableToPumpCircuit: true }],
256
- [131, { name: 'spaHeater', desc: 'Spa Heater', assignableToPumpCircuit: true }],
257
- [132, { name: 'freeze', desc: 'Freeze', assignableToPumpCircuit: true }],
258
- [133, { name: 'heatBoost', desc: 'Heat Boost', assignableToPumpCircuit: false }],
259
- [134, { name: 'heatEnable', desc: 'Heat Enable', assignableToPumpCircuit: false }],
260
- [135, { name: 'pumpSpeedUp', desc: 'Pump Speed +', assignableToPumpCircuit: false }],
261
- [136, { name: 'pumpSpeedDown', desc: 'Pump Speed -', assignableToPumpCircuit: false }],
262
- [255, { name: 'notused', desc: 'NOT USED', assignableToPumpCircuit: true }]
263
- ]);
264
- public lightThemes: byteValueMap = new byteValueMap([
265
- [0, { name: 'off', desc: 'Off', type: 'intellibrite' }],
266
- [1, { name: 'on', desc: 'On', type: 'intellibrite' }],
267
- [128, { name: 'colorsync', desc: 'Color Sync', type: 'intellibrite' }],
268
- [144, { name: 'colorswim', desc: 'Color Swim', type: 'intellibrite' }],
269
- [160, { name: 'colorset', desc: 'Color Set', type: 'intellibrite' }],
270
- [177, { name: 'party', desc: 'Party', type: 'intellibrite', sequence: 2 }],
271
- [178, { name: 'romance', desc: 'Romance', type: 'intellibrite', sequence: 3 }],
272
- [179, { name: 'caribbean', desc: 'Caribbean', type: 'intellibrite', sequence: 4 }],
273
- [180, { name: 'american', desc: 'American', type: 'intellibrite', sequence: 5 }],
274
- [181, { name: 'sunset', desc: 'Sunset', type: 'intellibrite', sequence: 6 }],
275
- [182, { name: 'royal', desc: 'Royal', type: 'intellibrite', sequence: 7 }],
276
- [190, { name: 'save', desc: 'Save', type: 'intellibrite', sequence: 13 }],
277
- [191, { name: 'recall', desc: 'Recall', type: 'intellibrite', sequence: 14 }],
278
- [193, { name: 'blue', desc: 'Blue', type: 'intellibrite', sequence: 8 }],
279
- [194, { name: 'green', desc: 'Green', type: 'intellibrite', sequence: 9 }],
280
- [195, { name: 'red', desc: 'Red', type: 'intellibrite', sequence: 10 }],
281
- [196, { name: 'white', desc: 'White', type: 'intellibrite', sequence: 11 }],
282
- [197, { name: 'magenta', desc: 'Magenta', type: 'intellibrite', sequence: 12 }],
283
- [208, { name: 'thumper', desc: 'Thumper', type: 'magicstream' }],
284
- [209, { name: 'hold', desc: 'Hold', type: 'magicstream' }],
285
- [210, { name: 'reset', desc: 'Reset', type: 'magicstream' }],
286
- [211, { name: 'mode', desc: 'Mode', type: 'magicstream' }],
287
- [254, { name: 'unknown', desc: 'unknown' }],
288
- [255, { name: 'none', desc: 'None' }]
289
- ]);
290
- public lightColors: byteValueMap = new byteValueMap([
291
- [0, { name: 'white', desc: 'White' }],
292
- [2, { name: 'lightgreen', desc: 'Light Green' }],
293
- [4, { name: 'green', desc: 'Green' }],
294
- [6, { name: 'cyan', desc: 'Cyan' }],
295
- [8, { name: 'blue', desc: 'Blue' }],
296
- [10, { name: 'lavender', desc: 'Lavender' }],
297
- [12, { name: 'magenta', desc: 'Magenta' }],
298
- [14, { name: 'lightmagenta', desc: 'Light Magenta' }]
299
- ]);
300
- public scheduleDays: byteValueMap = new byteValueMap([
301
- [1, { name: 'sat', desc: 'Saturday', dow: 6 }],
302
- [2, { name: 'fri', desc: 'Friday', dow: 5 }],
303
- [3, { name: 'thu', desc: 'Thursday', dow: 4 }],
304
- [4, { name: 'wed', desc: 'Wednesday', dow: 3 }],
305
- [5, { name: 'tue', desc: 'Tuesday', dow: 2 }],
306
- [6, { name: 'mon', desc: 'Monday', dow: 1 }],
307
- [7, { name: 'sun', desc: 'Sunday', dow: 0 }]
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
- public pumpTypes: byteValueMap = new byteValueMap([
319
- [1, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }],
320
- [64, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }],
321
- [65, { name: 'ds', desc: 'Two-Speed', maxCircuits: 40, hasAddress: false, hasBody: true }],
322
- [128, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, maxCircuits: 8, hasAddress: true }],
323
- [169, { name: 'vssvrs', desc: 'IntelliFlo VS+SVRS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, maxCircuits: 8, hasAddress: true }]
324
- ]);
325
- public pumpSSModels: byteValueMap = new byteValueMap([
326
- [0, { name: 'unspecified', desc: 'Unspecified', amps: 0, pf: 0, volts: 0, watts: 0 }],
327
- [1, { name: 'wf1hpE', desc: '1hp WhisperFlo E+', amps: 7.4, pf: .9, volts: 230, watts: 1532 }],
328
- [2, { name: 'wf1hpMax', desc: '1hp WhisperFlo Max', amps: 9, pf: .87, volts: 230, watts: 1600 }],
329
- [3, { name: 'generic15hp', desc: '1.5hp Pump', amps: 9.3, pf: .9, volts: 230, watts: 1925 }],
330
- [4, { name: 'generic2hp', desc: '2hp Pump', amps: 12, pf: .9, volts: 230, watts: 2484 }],
331
- [5, { name: 'generic25hp', desc: '2.5hp Pump', amps: 12.5, pf: .9, volts: 230, watts: 2587 }],
332
- [6, { name: 'generic3hp', desc: '3hp Pump', amps: 13.5, pf: .9, volts: 230, watts: 2794 }]
333
- ]);
334
- public pumpDSModels: byteValueMap = new byteValueMap([
335
- [0, { name: 'unspecified', desc: 'Unspecified', loAmps: 0, hiAmps: 0, pf: 0, volts: 0, loWatts: 0, hiWatts: 0 }],
336
- [1, { name: 'generic1hp', desc: '1hp Pump', loAmps: 2.4, hiAmps: 6.5, pf: .9, volts: 230, loWatts: 497, hiWatts: 1345 }],
337
- [2, { name: 'generic15hp', desc: '1.5hp Pump', loAmps: 2.7, hiAmps: 9.3, pf: .9, volts: 230, loWatts: 558, hiWatts: 1925 }],
338
- [3, { name: 'generic2hp', desc: '2hp Pump', loAmps: 2.9, hiAmps: 12, pf: .9, volts: 230, loWatts: 600, hiWatts: 2484 }],
339
- [4, { name: 'generic25hp', desc: '2.5hp Pump', loAmps: 3.1, hiAmps: 12.5, pf: .9, volts: 230, loWatts: 642, hiWatts: 2587 }],
340
- [5, { name: 'generic3hp', desc: '3hp Pump', loAmps: 3.3, hiAmps: 13.5, pf: .9, volts: 230, loWatts: 683, hiWatts: 2794 }]
341
- ]);
342
- public pumpVSModels: byteValueMap = new byteValueMap([
343
- [0, { name: 'intelliflovs', desc: 'IntelliFlo VS' }]
344
- ]);
345
- public pumpVFModels: byteValueMap = new byteValueMap([
346
- [0, { name: 'intelliflovf', desc: 'IntelliFlo VF' }]
347
- ]);
348
- public pumpVSFModels: byteValueMap = new byteValueMap([
349
- [0, { name: 'intelliflovsf', desc: 'IntelliFlo VSF' }]
350
- ]);
351
- public pumpVSSVRSModels: byteValueMap = new byteValueMap([
352
- [0, { name: 'intelliflovssvrs', desc: 'IntelliFlo VS+SVRS' }]
353
- ]);
354
- // These are used for single-speed pump definitions. Essentially the way this works is that when
355
- // the body circuit is running the single speed pump is on.
356
- public pumpBodies: byteValueMap = new byteValueMap([
357
- [0, { name: 'pool', desc: 'Pool' }],
358
- [101, { name: 'spa', desc: 'Spa' }],
359
- [255, { name: 'poolspa', desc: 'Pool/Spa' }]
360
- ]);
361
- public heaterTypes: byteValueMap = new byteValueMap([
362
- [1, { name: 'gas', desc: 'Gas Heater', hasAddress: false }],
363
- [2, { name: 'solar', desc: 'Solar Heater', hasAddress: false, hasCoolSetpoint: true }],
364
- [3, { name: 'heatpump', desc: 'Heat Pump', hasAddress: true }],
365
- [4, { name: 'ultratemp', desc: 'UltraTemp', hasAddress: true, hasCoolSetpoint: true }],
366
- [5, { name: 'hybrid', desc: 'Hybrid', hasAddress: true }],
367
- [6, { name: 'maxetherm', desc: 'Max-E-Therm', hasAddress: true }],
368
- [7, { name: 'mastertemp', desc: 'MasterTemp', hasAddress: true }]
369
- ]);
370
- public heatModes: byteValueMap = new byteValueMap([
371
- [0, { name: 'off', desc: 'Off' }],
372
- [3, { name: 'heater', desc: 'Heater' }],
373
- [5, { name: 'solar', desc: 'Solar Only' }],
374
- [12, { name: 'solarpref', desc: 'Solar Preferred' }]
375
- ]);
376
- public heatSources: byteValueMap = new byteValueMap([
377
- [0, { name: 'off', desc: 'No Heater' }],
378
- [3, { name: 'heater', desc: 'Heater' }],
379
- [5, { name: 'solar', desc: 'Solar Only' }],
380
- [21, { name: 'solarpref', desc: 'Solar Preferred' }],
381
- [32, { name: 'nochange', desc: 'No Change' }]
382
- ]);
383
- public heatStatus: byteValueMap = new byteValueMap([
384
- [0, { name: 'off', desc: 'Off' }],
385
- [1, { name: 'heater', desc: 'Heater' }],
386
- [2, { name: 'solar', desc: 'Solar' }],
387
- [3, { name: 'cooling', desc: 'Cooling' }]
388
- ]);
389
- public pumpStatus: byteValueMap = new byteValueMap([
390
- [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
391
- // for a VS/VF pump as is should be powered at all times. When it is, the status will always report a value > 0.
392
- [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
393
- // 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
394
- // 0 if it is not running. Essentially this is no error but it is not a status either.
395
- [2, { name: 'filter', desc: 'Filter warning' }],
396
- [3, { name: 'overcurrent', desc: 'Overcurrent condition' }],
397
- [4, { name: 'priming', desc: 'Priming' }],
398
- [5, { name: 'blocked', desc: 'System blocked' }],
399
- [6, { name: 'general', desc: 'General alarm' }],
400
- [7, { name: 'overtemp', desc: 'Overtemp condition' }],
401
- [8, { name: 'power', dec: 'Power outage' }],
402
- [9, { name: 'overcurrent2', desc: 'Overcurrent condition 2' }],
403
- [10, { name: 'overvoltage', desc: 'Overvoltage condition' }],
404
- [11, { name: 'error11', desc: 'Unspecified Error 11' }],
405
- [12, { name: 'error12', desc: 'Unspecified Error 12' }],
406
- [13, { name: 'error13', desc: 'Unspecified Error 13' }],
407
- [14, { name: 'error14', desc: 'Unspecified Error 14' }],
408
- [15, { name: 'error15', desc: 'Unspecified Error 15' }],
409
- [16, { name: 'commfailure', desc: 'Communication failure' }]
410
- ]);
411
- public pumpUnits: byteValueMap = new byteValueMap([
412
- [0, { name: 'rpm', desc: 'RPM' }],
413
- [1, { name: 'gpm', desc: 'GPM' }]
414
- ]);
415
- public bodyTypes: byteValueMap = new byteValueMap([
416
- [0, { name: 'pool', desc: 'Pool' }],
417
- [1, { name: 'spa', desc: 'Spa' }]
418
- ]);
419
- public bodies: byteValueMap = new byteValueMap([
420
- [0, { name: 'pool', desc: 'Pool' }],
421
- [1, { name: 'spa', desc: 'Spa' }],
422
- [2, { name: 'body3', desc: 'Body 3' }],
423
- [3, { name: 'body4', desc: 'Body 4' }],
424
- [32, { name: 'poolspa', desc: 'Pool/Spa' }]
425
- ]);
426
- public chlorinatorStatus: byteValueMap = new byteValueMap([
427
- [0, { name: 'ok', desc: 'Ok' }],
428
- [1, { name: 'lowflow', desc: 'Low Flow' }],
429
- [2, { name: 'lowsalt', desc: 'Low Salt' }],
430
- [3, { name: 'verylowsalt', desc: 'Very Low Salt' }],
431
- [4, { name: 'highcurrent', desc: 'High Current' }],
432
- [5, { name: 'clean', desc: 'Clean Cell' }],
433
- [6, { name: 'lowvoltage', desc: 'Low Voltage' }],
434
- [7, { name: 'lowtemp', desc: 'Water Temp Low' }],
435
- [8, { name: 'commlost', desc: 'Communication Lost' }]
436
- ]);
437
- public chlorinatorType: byteValueMap = new byteValueMap([
438
- [0, { name: 'pentair', desc: 'Pentair' }],
439
- [1, { name: 'unknown', desc: 'unknown' }],
440
- [2, { name: 'aquarite', desc: 'Aquarite' }],
441
- [3, { name: 'unknown', desc: 'unknown' }]
442
- ]);
443
- public chlorinatorModel: byteValueMap = new byteValueMap([
444
- [0, { name: 'unknown', desc: 'unknown', capacity: 0, chlorinePerDay: 0, chlorinePerSec: 0 }],
445
- [1, { name: 'intellichlor--15', desc: 'IC15', capacity: 15000, chlorinePerDay: 0.60, chlorinePerSec: 0.60/86400 }],
446
- [2, { name: 'intellichlor--20', desc: 'IC20', capacity: 20000, chlorinePerDay: 0.70, chlorinePerSec: 0.70/86400 }],
447
- [3, { name: 'intellichlor--40', desc: 'IC40', capacity: 40000, chlorinePerDay: 1.40, chlorinePerSec: 1.4/86400 }],
448
- [4, { name: 'intellichlor--60', desc: 'IC60', capacity: 60000, chlorinePerDay: 2, chlorinePerSec: 2/86400 }],
449
- ])
450
- public customNames: byteValueMap = new byteValueMap();
451
- public circuitNames: byteValueMap = new byteValueMap();
452
- public scheduleTypes: byteValueMap = new byteValueMap([
453
- [0, { name: 'runonce', desc: 'Run Once', startDate: true, startTime: true, endTime: true, days: false, heatSource: true, heatSetpoint: true }],
454
- [128, { name: 'repeat', desc: 'Repeats', startDate: false, startTime: true, endTime: true, days: 'multi', heatSource: true, heatSetpoint: true }]
455
- ]);
456
- public circuitGroupTypes: byteValueMap = new byteValueMap([
457
- [0, { name: 'none', desc: 'Unspecified' }],
458
- [1, { name: 'light', desc: 'Light' }],
459
- [2, { name: 'circuit', desc: 'Circuit' }],
460
- [3, { name: 'intellibrite', desc: 'IntelliBrite' }]
461
- ]);
462
- public groupCircuitStates: byteValueMap = new byteValueMap([
463
- [0, { name: 'off', desc: 'Off' }],
464
- [1, { name: 'on', desc: 'On' }]
465
- ]);
466
- public tempUnits: byteValueMap = new byteValueMap([
467
- [0, { name: 'F', desc: 'Fahrenheit' }],
468
- [4, { name: 'C', desc: 'Celsius' }]
469
- ]);
470
- public valveTypes: byteValueMap = new byteValueMap([
471
- [0, { name: 'standard', desc: 'Standard' }],
472
- [1, { name: 'intellivalve', desc: 'IntelliValve' }]
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
- public async processStatusAsync() {
778
- let self = this;
779
- try {
780
- if (this._statusCheckRef > 0) return;
781
- this.suspendStatus(true);
782
- if (typeof this._statusTimer !== 'undefined' && this._statusTimer) clearTimeout(this._statusTimer);
783
- // Go through all the assigned equipment and verify the current state.
784
- sys.board.system.keepManualTime();
785
- await sys.board.circuits.syncCircuitRelayStates();
786
- await sys.board.features.syncGroupStates();
787
- await sys.board.circuits.syncVirtualCircuitStates();
788
- await sys.board.valves.syncValveStates();
789
- await sys.board.filters.syncFilterStates();
790
- await sys.board.heaters.syncHeaterStates();
791
- await sys.board.schedules.syncScheduleStates();
792
- state.emitControllerChange();
793
- state.emitEquipmentChanges();
794
- } catch (err) { state.status = 255; logger.error(`Error performing processStatusAsync ${err.message}`); }
795
- finally {
796
- this.suspendStatus(false);
797
- if (this.statusInterval > 0) this._statusTimer = setTimeout(() => self.processStatusAsync(), this.statusInterval);
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
- public getHeatSources(bodyId: number) {
1281
- let heatSources = [];
1282
- let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1283
- heatSources.push(this.board.valueMaps.heatSources.transformByName('nochange'));
1284
- if (heatTypes.total > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('off'));
1285
- if (heatTypes.gas > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('heater'));
1286
- if (heatTypes.solar > 0) {
1287
- let hm = this.board.valueMaps.heatSources.transformByName('solar');
1288
- heatSources.push(hm);
1289
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('solarpref'));
1290
- }
1291
- if (heatTypes.heatpump > 0) {
1292
- let hm = this.board.valueMaps.heatSources.transformByName('heatpump');
1293
- heatSources.push(hm);
1294
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('heatpumppref'));
1295
- }
1296
- if (heatTypes.ultratemp > 0) {
1297
- let hm = this.board.valueMaps.heatSources.transformByName('ultratemp');
1298
- heatSources.push(hm);
1299
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('ultratemppref'));
1300
- }
1301
- return heatSources;
1302
- }
1303
- public getHeatModes(bodyId: number) {
1304
- let heatModes = [];
1305
- // 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)
1306
- heatModes.push(this.board.valueMaps.heatModes.transformByName('off')); // In IC fw 1.047 off is no longer 0.
1307
- let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1308
- if (heatTypes.gas > 0)
1309
- heatModes.push(this.board.valueMaps.heatModes.transformByName('heater'));
1310
- if (heatTypes.solar > 0) {
1311
- let hm = this.board.valueMaps.heatModes.transformByName('solar');
1312
- heatModes.push(hm);
1313
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('solarpref'));
1314
- }
1315
- if (heatTypes.heatpump > 0) {
1316
- let hm = this.board.valueMaps.heatModes.transformByName('heatpump');
1317
- heatModes.push(hm);
1318
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('heatpumppref'));
1319
- }
1320
- if (heatTypes.ultratemp > 0) {
1321
- let hm = this.board.valueMaps.heatModes.transformByName('ultratemp');
1322
- heatModes.push(hm);
1323
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('ultratemppref'));
1324
- }
1325
- return heatModes;
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
- public getBodyState(bodyCode: number): BodyTempState {
1347
- let assoc = sys.board.valueMaps.bodies.transform(bodyCode);
1348
- switch (assoc.name) {
1349
- case 'body1':
1350
- case 'pool':
1351
- return state.temps.bodies.getItemById(1);
1352
- case 'body2':
1353
- case 'spa':
1354
- return state.temps.bodies.getItemById(2);
1355
- case 'body3':
1356
- return state.temps.bodies.getItemById(3);
1357
- case 'body4':
1358
- return state.temps.bodies.getItemById(4);
1359
- case 'poolspa':
1360
- if (sys.equipment.shared && sys.equipment.maxBodies >= 2) {
1361
- let body = state.temps.bodies.getItemById(1);
1362
- if (body.isOn) return body;
1363
- body = state.temps.bodies.getItemById(2);
1364
- if (body.isOn) return body;
1365
- return state.temps.bodies.getItemById(1);
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
- return false;
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
- const _isVirtual = sys.pumps.getItemById(_id).isVirtual;
1811
+ let _p = pump.get(true);
1812
+ // const _isVirtual = typeof _p.isVirtual !== 'undefined' ? _p.isVirtual : false;
1459
1813
  sys.pumps.removeItemById(_id);
1460
- let pump = sys.pumps.getItemById(_id, true);
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
- public async syncCircuitRelayStates() {
1518
- try {
1519
- for (let i = 0; i < sys.circuits.length; i++) {
1520
- // Run through all the valves to see whether they should be triggered or not.
1521
- let circ = sys.circuits.getItemByIndex(i);
1522
- if (circ.master === 1 && circ.isActive) {
1523
- let cstate = state.circuits.getItemById(circ.id);
1524
- if (cstate.isOn) await ncp.circuits.setCircuitStateAsync(cstate, cstate.isOn);
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
- if (!remove) {
1554
- // Determine whether the pool heater is on.
1555
- for (let j = 0; j < poolStates.length; j++)
1556
- if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'heater') bState = true;
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
- break;
1559
- case 'spaHeater':
1560
- remove = true;
1561
- for (let j = 0; j < spaStates.length; j++) {
1562
- if (spaStates[j].heaterOptions.total > 0) remove = false;
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
- if (!remove) {
1565
- // Determine whether the spa heater is on.
1566
- for (let j = 0; j < spaStates.length; j++) {
1567
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') bState = true;
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
- break;
1571
- case 'freeze':
1572
- // If freeze protection has been turned on.
1573
- bState = state.freeze;
1574
- break;
1575
- case 'poolSpa':
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 j = 0; j < spaStates.length && !bState; j++) {
1581
- if (spaStates[j].isOn) bState = true;
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
- break;
1584
- case 'solarHeat':
1585
- case 'solar':
1586
- // If solar is on for any body
1587
- remove = true;
1588
- for (let j = 0; j < poolStates.length; j++) {
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
- if (remove) {
1592
- for (let j = 0; j < spaStates.length; j++) {
1593
- if (spaStates[j].heaterOptions.solar + spaStates[j].heaterOptions.heatpump > 0) remove = false;
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
- if (!remove) {
1597
- for (let j = 0; j < poolStates.length && !bState; j++) {
1598
- if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') bState = true;
1599
- }
1600
- for (let j = 0; j < spaStates.length && !bState; j++) {
1601
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') bState = true;
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
- break;
1605
- case 'heater':
1606
- remove = true;
1607
- for (let j = 0; j < poolStates.length; j++) {
1608
- if (poolStates[j].heaterOptions.total > 0) remove = false;
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
- if (remove) {
1611
- for (let j = 0; j < spaStates.length; j++) {
1612
- if (spaStates[j].heaterOptions.total > 0) remove = false;
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
- if (!remove) {
1616
- for (let j = 0; j < poolStates.length && !bState; j++) {
1617
- let heat = sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus);
1618
- if (heat !== 'off') bState = true;
1619
- }
1620
- for (let j = 0; j < spaStates.length && !bState; j++) {
1621
- let heat = sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus);
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
- break;
1626
- default:
1627
- remove = true;
1628
- break;
1629
- }
1630
- if (remove)
1631
- state.virtualCircuits.removeItemById(vc.val);
1632
- else {
1633
- cstate = state.virtualCircuits.getItemById(vc.val, true);
1634
- if (cstate !== null) {
1635
- cstate.isOn = bState;
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
- // We need to do some routing here as it is now critical that circuits, groups, and features
1647
- // have their own processing. The virtual controller used to only deal with one circuit.
1648
- if (sys.board.equipmentIds.circuitGroups.isInRange(id))
1649
- return await sys.board.circuits.setCircuitGroupStateAsync(id, val);
1650
- else if (sys.board.equipmentIds.features.isInRange(id))
1651
- return await sys.board.features.setFeatureStateAsync(id, val);
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
- if (id === 6) state.temps.bodies.getItemById(1, true).isOn = val;
1674
- else if (id === 1) state.temps.bodies.getItemById(2, true).isOn = val;
1675
- // Let the main nixie controller set the circuit state and affect the relays if it needs to.
1676
- await ncp.circuits.setCircuitStateAsync(circ, newState);
1677
- return state.circuits.getInterfaceById(circ.id);
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
- catch (err) { return Promise.reject(`Nixie: Error setCircuitStateAsync ${err.message}`); }
1680
- finally {
1681
- // sys.board.virtualPumpControllers.start();
1682
- ncp.pumps.syncPumpStates();
1683
- sys.board.suspendStatus(false);
1684
- this.board.processStatusAsync();
1685
- state.emitEquipmentChanges();
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
- public getCircuitFunctions() { return sys.board.valueMaps.circuitFunctions.toArray(); }
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
- sgrp.isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false : true;
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
- export class FeatureCommands extends BoardCommands {
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 = 1;
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
- public getInstalledHeaterTypes(body?: number): any {
2525
- let heaters = sys.heaters.get();
2526
- let types = sys.board.valueMaps.heaterTypes.toArray();
2527
- let inst = { total: 0 };
2528
- for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
2529
- for (let i = 0; i < heaters.length; i++) {
2530
- let heater = heaters[i];
2531
- if (typeof body !== 'undefined' && heater.body !== 'undefined') {
2532
- if ((heater.body !== 32 && body !== heater.body + 1) || (heater.body === 32 && body > 2)) continue;
2533
- }
2534
- let type = types.find(elem => elem.val === heater.type);
2535
- if (typeof type !== 'undefined') {
2536
- if (inst[type.name] === 'undefined') inst[type.name] = 0;
2537
- inst[type.name] = inst[type.name] + 1;
2538
- if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
2539
- inst.total++;
2540
- }
2541
- }
2542
- return inst;
2543
- }
2544
- public isSolarInstalled(body?: number): boolean {
2545
- let heaters = sys.heaters.get();
2546
- let types = sys.board.valueMaps.heaterTypes.toArray();
2547
- for (let i = 0; i < heaters.length; i++) {
2548
- let heater = heaters[i];
2549
- if (typeof body !== 'undefined' && body !== heater.body) continue;
2550
- let type = types.find(elem => elem.val === heater.type);
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
- public isHeatPumpInstalled(body?: number): boolean {
2560
- let heaters = sys.heaters.get();
2561
- let types = sys.board.valueMaps.heaterTypes.toArray();
2562
- for (let i = 0; i < heaters.length; i++) {
2563
- let heater = heaters[i];
2564
- if (typeof body !== 'undefined' && body !== heater.body) continue;
2565
- let type = types.find(elem => elem.val === heater.type);
2566
- if (typeof type !== 'undefined') {
2567
- switch (type.name) {
2568
- case 'heatpump':
2569
- return true;
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
- public setHeater(heater: Heater, obj?: any) {
2575
- if (typeof obj !== undefined) {
2576
- for (var s in obj)
2577
- heater[s] = obj[s];
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
- public async setHeaterAsync(obj: any): Promise<Heater> {
2581
- try {
2582
- let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
2583
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
2584
- else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Virtual Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
2585
- let heater: Heater;
2586
- if (id <= 0) {
2587
- // We are adding a heater. In this case all heaters are virtual.
2588
- let vheaters = sys.heaters.filter(h => h.isVirtual === true);
2589
- id = vheaters.length + 256;
2590
- }
2591
- heater = sys.heaters.getItemById(id, true);
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
- else {
2663
- sys.equipment.tempSensors.removeItemById('water2');
2664
- sys.equipment.tempSensors.removeItemById('solar2');
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
- if (maxPairs > 2) {
2667
- sys.equipment.tempSensors.getItemById('water3', true, { id: 'water3', isActive: false, calibration: 0 }).name = 'Body 3';
2668
- sys.equipment.tempSensors.getItemById('solar3', true, { id: 'solar3', isActive: false, calibration: 0 }).name = 'Solar 3';
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
- else {
2671
- sys.equipment.tempSensors.removeItemById('water3');
2672
- sys.equipment.tempSensors.removeItemById('solar3');
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
- if (maxPairs > 3) {
2675
- sys.equipment.tempSensors.getItemById('water4', true, { id: 'water4', isActive: false, calibration: 0 }).name = 'Body 4';
2676
- sys.equipment.tempSensors.getItemById('solar4', true, { id: 'solar4', isActive: false, calibration: 0 }).name = 'Solar 4';
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
- else {
2679
- sys.equipment.tempSensors.removeItemById('water4');
2680
- sys.equipment.tempSensors.removeItemById('solar4');
2681
- }
2682
-
2683
- }
2684
- // Sets the active temp sensors based upon the installed equipment. At this point all
2685
- // detectable temp sensors should exist.
2686
- public setActiveTempSensors() {
2687
- let htypes;
2688
- // We are iterating backwards through the sensors array on purpose. We do this just in case we need
2689
- // to remove a sensor during the iteration. This way the index values will not be impacted and we can
2690
- // safely remove from the array we are iterating.
2691
- for (let i = sys.equipment.tempSensors.length - 1; i >= 0; i--) {
2692
- let sensor = sys.equipment.tempSensors.getItemByIndex(i);
2693
- // The names are normalized in this array.
2694
- switch (sensor.id) {
2695
- case 'air':
2696
- sensor.isActive = true;
2697
- break;
2698
- case 'water1':
2699
- sensor.isActive = sys.equipment.maxBodies > 0;
2700
- break;
2701
- case 'water2':
2702
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 2 : sys.equipment.maxBodies > 1;
2703
- break;
2704
- case 'water3':
2705
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 3 : sys.equipment.maxBodies > 2;
2706
- break;
2707
- case 'water4':
2708
- // It's a little weird but technically you should be able to install 3 expansions and a i10D personality
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
- // This updates the heater states based upon the installed heaters. This is true for heaters that are tied to the OCP
2756
- // and those that are not.
2757
- public syncHeaterStates() {
2758
- try {
2759
- // Go through the installed heaters and bodies to determine whether they should be on. If there is a
2760
- // heater that is not controlled by the OCP then we need to determine whether it should be on.
2761
- let heaters = sys.heaters.toArray();
2762
- let bodies = state.temps.bodies.toArray();
2763
- let hon = [];
2764
- for (let i = 0; i < bodies.length; i++) {
2765
- let body: BodyTempState = bodies[i];
2766
- let cfgBody: Body = sys.bodies.getItemById(body.id);
2767
- let isHeating = false;
2768
- if (body.isOn) {
2769
- 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.`);
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
- case 'ultratemp':
2831
- // We need to determine whether we are going to use the air temp or the solar temp
2832
- // for the sensor.
2833
- let deltaTemp = Math.max(state.temps.air, state.temps.solar || 0);
2834
- if (mode === 'ultratemp' || mode === 'ultratemppref') {
2835
- if (body.temp < cfgBody.heatSetpoint &&
2836
- deltaTemp > body.temp + heater.differentialTemp || 0) {
2837
- isOn = true;
2838
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
2839
- isHeating = true;
2840
- isCooling = false;
2841
- }
2842
- else if (body.temp > cfgBody.coolSetpoint && heater.coolingEnabled) {
2843
- isOn = true;
2844
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
2845
- isHeating = true;
2846
- isCooling = true;
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
- case 'gas':
2851
- if (mode === 'heater') {
2852
- if (body.temp < cfgBody.setPoint) {
2853
- isOn = true;
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 if (mode === 'solarpref' || mode === 'heatpumppref') {
2859
- // If solar should be running gas heater should be off.
2860
- if (body.temp < cfgBody.setPoint &&
2861
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) isOn = false;
2862
- else if (body.temp < cfgBody.setPoint) {
2863
- isOn = true;
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
- case 'heatpump':
2870
- if (mode === 'heatpump' || mode === 'heatpumppref') {
2871
- if (body.temp < cfgBody.setPoint &&
2872
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
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
- default:
2880
- isOn = utils.makeBool(hstate.isOn);
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
- // When the controller is a virtual one we need to control the heat status ourselves.
2898
- if (!isHeating && (sys.controllerType === ControllerType.Virtual || sys.controllerType === ControllerType.Nixie)) body.heatStatus = 0;
2899
- }
2900
- // Turn off any heaters that should be off. The code above only turns heaters on.
2901
- for (let i = 0; i < heaters.length; i++) {
2902
- let heater: Heater = heaters[i];
2903
- if (typeof hon.find(elem => elem === heater.id) === 'undefined') {
2904
- let hstate = state.heaters.getItemById(heater.id, true);
2905
- if (heater.master === 1) (async () => {
2906
- try {
2907
- await ncp.heaters.setHeaterStateAsync(hstate, false, false);
2908
- } catch (err) { logger.error(err.message); }
2909
- })();
2910
- else hstate.isOn = false;
2911
- }
2912
- }
2913
- } catch (err) { logger.error(`Error synchronizing heater states`); }
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
- public async syncValveStates() {
2962
- try {
2963
- for (let i = 0; i < sys.valves.length; i++) {
2964
- // Run through all the valves to see whether they should be triggered or not.
2965
- let valve = sys.valves.getItemByIndex(i);
2966
- if (valve.isActive) {
2967
- let vstate = state.valves.getItemById(valve.id, true);
2968
- let isDiverted = vstate.isDiverted;
2969
- if (typeof valve.circuit !== 'undefined' && valve.circuit > 0) {
2970
- if (sys.equipment.shared && valve.isIntake === true)
2971
- isDiverted = utils.makeBool(state.circuits.getItemById(1).isOn); // If the spa is on then the intake is diverted.
2972
- else if (sys.equipment.shared && valve.isReturn === true) {
2973
- // 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.
2974
- let spillway = typeof state.circuits.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined' ||
2975
- typeof state.features.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined';
2976
- isDiverted = utils.makeBool(spillway || state.circuits.getItemById(1).isOn);
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
- else {
2979
- let circ = state.circuits.getInterfaceById(valve.circuit);
2980
- isDiverted = utils.makeBool(circ.isOn);
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
- } catch (err) { logger.error(`syncValveStates: Error synchronizing valves ${err.message}`); }
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
- public async setChemControllerAsync(data: any): Promise<ChemController> {
3079
- // The following are the rules related to when an OCP is present.
3080
- // ==============================================================
3081
- // 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.
3082
- // 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.
3083
- // 3. njspc will communicate to the OCP for IntelliChem control via the configuration interface.
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
- // The following are the rules related to when no OCP is present.
3086
- // =============================================================
3087
- // 1. All chemControllers will be controlled via Nixie (IntelliChem, REM Chem).
3088
- try {
3089
- let chem = sys.board.chemControllers.findChemController(data);
3090
- let isAdd = typeof chem === 'undefined';
3091
- let type = sys.board.valueMaps.chemControllerTypes.encode(isAdd ? data.type : chem.type);
3092
- if (typeof type === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`The chem controller type could not be determined ${data.type || type}`, 'chemController', type));
3093
- 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));
3094
- let address = parseInt(data.address, 10);
3095
- let t = sys.board.valueMaps.chemControllerTypes.transform(type);
3096
- if (t.hasAddress) {
3097
- // First lets make sure the user supplied an address.
3098
- if (isNaN(address)) return Promise.reject(new InvalidEquipmentDataError(`${type.desc} chem controllers require a valid address`, 'chemController', data.address));
3099
- 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));
3100
- }
3101
- if (isAdd) {
3102
- // At this point we are going to add the chem controller no matter what.
3103
- data.id = sys.chemControllers.getNextControllerId(type);
3104
- chem = sys.chemControllers.getItemById(data.id, true);
3105
- chem.type = type;
3106
- if (t.hasAddress) chem.address = address;
3107
- }
3108
- chem.isActive = true;
3109
- // So here is the thing. If you have an OCP then the IntelliChem must be controlled by that.
3110
- // the messages on the bus will talk back to the OCP so if you do not do this mayhem will ensue.
3111
- if (type.name === 'intellichem')
3112
- await this.setIntelliChemAsync(data);
3113
- else
3114
- await ncp.chemControllers.setControllerAsync(chem, data);
3115
- return Promise.resolve(chem);
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 syncFilterStates() {
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
- // Run through all the valves to see whether they should be triggered or not.
3136
- let filter = sys.filters.getItemByIndex(i);
3137
- if (filter.isActive) {
3138
- let fstate = state.filters.getItemById(filter.id, true);
3139
- // Check to see if the associated body is on.
3140
- await sys.board.filters.setFilterStateAsync(filter, fstate, sys.board.bodies.isBodyOn(filter.body));
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
- } catch (err) { logger.error(`syncFilterStates: Error synchronizing filters ${err.message}`); }
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 setFilter(data: any): any {
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
- if (typeof data.isActive !== 'undefined') {
3156
- if (utils.makeBool(data.isActive) === false) {
3157
- sys.filters.removeItemById(id);
3158
- state.filters.removeItemById(id);
3159
- return;
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
- public deleteFilter(data: any): any {
3195
- let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
3196
- if (isNaN(id)) return;
3197
- sys.filters.removeItemById(id);
3198
- state.filters.removeItemById(id);
3199
- return state.filters.getItemById(id);
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
+ }