nodejs-poolcontroller 7.4.0 → 7.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/Changelog +3 -0
  3. package/README.md +2 -2
  4. package/app.ts +2 -0
  5. package/config/Config.ts +3 -0
  6. package/config/VersionCheck.ts +8 -4
  7. package/controller/Equipment.ts +89 -29
  8. package/controller/Errors.ts +14 -1
  9. package/controller/State.ts +75 -31
  10. package/controller/boards/EasyTouchBoard.ts +81 -36
  11. package/controller/boards/IntelliCenterBoard.ts +96 -32
  12. package/controller/boards/IntelliTouchBoard.ts +103 -29
  13. package/controller/boards/NixieBoard.ts +79 -27
  14. package/controller/boards/SystemBoard.ts +1552 -822
  15. package/controller/comms/Comms.ts +84 -9
  16. package/controller/comms/messages/Messages.ts +10 -4
  17. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -4
  18. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  19. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  20. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  21. package/controller/comms/messages/config/ExternalMessage.ts +43 -25
  22. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  23. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  24. package/controller/comms/messages/config/HeaterMessage.ts +10 -9
  25. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  26. package/controller/comms/messages/config/OptionsMessage.ts +13 -1
  27. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  28. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  29. package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
  30. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  31. package/controller/comms/messages/config/ValveMessage.ts +12 -2
  32. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
  33. package/controller/comms/messages/status/EquipmentStateMessage.ts +74 -22
  34. package/controller/comms/messages/status/HeaterStateMessage.ts +15 -6
  35. package/controller/comms/messages/status/IntelliChemStateMessage.ts +37 -26
  36. package/controller/nixie/Nixie.ts +18 -16
  37. package/controller/nixie/chemistry/ChemController.ts +57 -37
  38. package/controller/nixie/chemistry/Chlorinator.ts +7 -8
  39. package/controller/nixie/circuits/Circuit.ts +17 -0
  40. package/controller/nixie/pumps/Pump.ts +49 -24
  41. package/controller/nixie/schedules/Schedule.ts +1 -1
  42. package/defaultConfig.json +15 -0
  43. package/issue_template.md +1 -1
  44. package/logger/DataLogger.ts +37 -22
  45. package/package.json +3 -1
  46. package/web/Server.ts +515 -27
  47. package/web/bindings/influxDB.json +35 -0
  48. package/web/bindings/mqtt.json +62 -3
  49. package/web/bindings/mqttAlt.json +57 -4
  50. package/web/interfaces/httpInterface.ts +2 -0
  51. package/web/interfaces/influxInterface.ts +3 -2
  52. package/web/interfaces/mqttInterface.ts +12 -1
  53. package/web/services/config/Config.ts +162 -37
  54. package/web/services/state/State.ts +47 -3
  55. package/web/services/state/StateSocket.ts +1 -1
@@ -22,6 +22,8 @@ import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupC
22
22
  import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError } 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)); }
@@ -216,10 +218,10 @@ export class byteValueMaps {
216
218
  ]);
217
219
  public panelModes: byteValueMap = new byteValueMap([
218
220
  [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' }],
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' }],
223
225
  [255, { name: 'error', desc: 'System Error' }]
224
226
  ]);
225
227
  public controllerStatus: byteValueMap = new byteValueMap([
@@ -244,7 +246,9 @@ export class byteValueMaps {
244
246
  [15, { name: 'floorcleaner', desc: 'Floor Cleaner' }],
245
247
  [16, { name: 'intellibrite', desc: 'Intellibrite', isLight: true }],
246
248
  [17, { name: 'magicstream', desc: 'Magicstream', isLight: true }],
247
- [19, { name: 'notused', desc: 'Not Used' }]
249
+ [19, { name: 'notused', desc: 'Not Used' }],
250
+ [65, { name: 'lotemp', desc: 'Lo-Temp' }],
251
+ [66, { name: 'hightemp', desc: 'Hi-Temp' }]
248
252
  ]);
249
253
 
250
254
  // Feature functions are used as the available options to define a circuit.
@@ -287,25 +291,25 @@ export class byteValueMaps {
287
291
  [254, { name: 'unknown', desc: 'unknown' }],
288
292
  [255, { name: 'none', desc: 'None' }]
289
293
  ]);
290
- public colorLogicThemes = new byteValueMap([
291
- [0, { name: 'cloudwhite', desc: 'Cloud White', type: 'colorlogic', sequence: 7 }],
292
- [1, { name: 'deepsea', desc: 'Deep Sea', type: 'colorlogic', sequence: 2 }],
293
- [2, { name: 'royalblue', desc: 'Royal Blue', type: 'colorlogic', sequence: 3 }],
294
- [3, { name: 'afernoonskies', desc: 'Afternoon Skies', type: 'colorlogic', sequence: 4 }],
295
- [4, { name: 'aquagreen', desc: 'Aqua Green', type: 'colorlogic', sequence: 5 }],
296
- [5, { name: 'emerald', desc: 'Emerald', type: 'colorlogic', sequence: 6 }],
297
- [6, { name: 'warmred', desc: 'Warm Red', type: 'colorlogic', sequence: 8 }],
298
- [7, { name: 'flamingo', desc: 'Flamingo', type: 'colorlogic', sequence: 9 }],
299
- [8, { name: 'vividviolet', desc: 'Vivid Violet', type: 'colorlogic', sequence: 10 }],
300
- [9, { name: 'sangria', desc: 'Sangria', type: 'colorlogic', sequence: 11 }],
301
- [10, { name: 'twilight', desc: 'Twilight', type: 'colorlogic', sequence: 12 }],
302
- [11, { name: 'tranquility', desc: 'Tranquility', type: 'colorlogic', sequence: 13 }],
303
- [12, { name: 'gemstone', desc: 'Gemstone', type: 'colorlogic', sequence: 14 }],
304
- [13, { name: 'usa', desc: 'USA', type: 'colorlogic', sequence: 15 }],
305
- [14, { name: 'mardigras', desc: 'Mardi Gras', type: 'colorlogic', sequence: 16 }],
306
- [15, { name: 'cabaret', desc: 'Cabaret', type: 'colorlogic', sequence: 17 }],
307
- [255, { name: 'none', desc: 'None' }]
308
- ]);
294
+ public colorLogicThemes = new byteValueMap([
295
+ [0, { name: 'cloudwhite', desc: 'Cloud White', type: 'colorlogic', sequence: 7 }],
296
+ [1, { name: 'deepsea', desc: 'Deep Sea', type: 'colorlogic', sequence: 2 }],
297
+ [2, { name: 'royalblue', desc: 'Royal Blue', type: 'colorlogic', sequence: 3 }],
298
+ [3, { name: 'afernoonskies', desc: 'Afternoon Skies', type: 'colorlogic', sequence: 4 }],
299
+ [4, { name: 'aquagreen', desc: 'Aqua Green', type: 'colorlogic', sequence: 5 }],
300
+ [5, { name: 'emerald', desc: 'Emerald', type: 'colorlogic', sequence: 6 }],
301
+ [6, { name: 'warmred', desc: 'Warm Red', type: 'colorlogic', sequence: 8 }],
302
+ [7, { name: 'flamingo', desc: 'Flamingo', type: 'colorlogic', sequence: 9 }],
303
+ [8, { name: 'vividviolet', desc: 'Vivid Violet', type: 'colorlogic', sequence: 10 }],
304
+ [9, { name: 'sangria', desc: 'Sangria', type: 'colorlogic', sequence: 11 }],
305
+ [10, { name: 'twilight', desc: 'Twilight', type: 'colorlogic', sequence: 12 }],
306
+ [11, { name: 'tranquility', desc: 'Tranquility', type: 'colorlogic', sequence: 13 }],
307
+ [12, { name: 'gemstone', desc: 'Gemstone', type: 'colorlogic', sequence: 14 }],
308
+ [13, { name: 'usa', desc: 'USA', type: 'colorlogic', sequence: 15 }],
309
+ [14, { name: 'mardigras', desc: 'Mardi Gras', type: 'colorlogic', sequence: 16 }],
310
+ [15, { name: 'cabaret', desc: 'Cabaret', type: 'colorlogic', sequence: 17 }],
311
+ [255, { name: 'none', desc: 'None' }]
312
+ ]);
309
313
 
310
314
  public lightColors: byteValueMap = new byteValueMap([
311
315
  [0, { name: 'white', desc: 'White' }],
@@ -434,7 +438,9 @@ export class byteValueMaps {
434
438
  ]);
435
439
  public bodyTypes: byteValueMap = new byteValueMap([
436
440
  [0, { name: 'pool', desc: 'Pool' }],
437
- [1, { name: 'spa', desc: 'Spa' }]
441
+ [1, { name: 'spa', desc: 'Spa' }],
442
+ [2, { name: 'spa', desc: 'Spa' }],
443
+ [3, { name: 'spa', desc: 'Spa' }]
438
444
  ]);
439
445
  public bodies: byteValueMap = new byteValueMap([
440
446
  [0, { name: 'pool', desc: 'Pool' }],
@@ -462,10 +468,16 @@ export class byteValueMaps {
462
468
  ]);
463
469
  public chlorinatorModel: byteValueMap = new byteValueMap([
464
470
  [0, { name: 'unknown', desc: 'unknown', capacity: 0, chlorinePerDay: 0, chlorinePerSec: 0 }],
465
- [1, { name: 'intellichlor--15', desc: 'IC15', capacity: 15000, chlorinePerDay: 0.60, chlorinePerSec: 0.60/86400 }],
466
- [2, { name: 'intellichlor--20', desc: 'IC20', capacity: 20000, chlorinePerDay: 0.70, chlorinePerSec: 0.70/86400 }],
467
- [3, { name: 'intellichlor--40', desc: 'IC40', capacity: 40000, chlorinePerDay: 1.40, chlorinePerSec: 1.4/86400 }],
468
- [4, { name: 'intellichlor--60', desc: 'IC60', capacity: 60000, chlorinePerDay: 2, chlorinePerSec: 2/86400 }],
471
+ [1, { name: 'intellichlor--15', desc: 'IntelliChlor IC15', capacity: 15000, chlorinePerDay: 0.60, chlorinePerSec: 0.60 / 86400 }],
472
+ [2, { name: 'intellichlor--20', desc: 'IntelliChlor IC20', capacity: 20000, chlorinePerDay: 0.70, chlorinePerSec: 0.70 / 86400 }],
473
+ [3, { name: 'intellichlor--40', desc: 'IntelliChlor IC40', capacity: 40000, chlorinePerDay: 1.40, chlorinePerSec: 1.4 / 86400 }],
474
+ [4, { name: 'intellichlor--60', desc: 'IntelliChlor IC60', capacity: 60000, chlorinePerDay: 2, chlorinePerSec: 2 / 86400 }],
475
+ [5, { name: 'aquarite-t15', desc: 'AquaRite T15', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }],
476
+ [6, { name: 'aquarite-t9', desc: 'AquaRite T9', capacity: 30000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }],
477
+ [7, { name: 'aquarite-t5', desc: 'AquaRite T5', capacity: 20000, chlorinePerDay: 0.735, chlorinePerSec: 0.735 / 86400 }],
478
+ [8, { name: 'aquarite-t3', desc: 'AquaRite T3', capacity: 15000, chlorinePerDay: 0.53, chlorinePerSec: 0.53 / 86400 }],
479
+ [9, { name: 'aquarite-925', desc: 'AquaRite 925', capacity: 25000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }],
480
+ [10, { name: 'aquarite-940', desc: 'AquaRite 940', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }]
469
481
  ])
470
482
  public customNames: byteValueMap = new byteValueMap();
471
483
  public circuitNames: byteValueMap = new byteValueMap();
@@ -483,6 +495,10 @@ export class byteValueMaps {
483
495
  [0, { name: 'off', desc: 'Off' }],
484
496
  [1, { name: 'on', desc: 'On' }]
485
497
  ]);
498
+ public systemUnits: byteValueMap = new byteValueMap([
499
+ [0, { name: 'english', desc: 'English' }],
500
+ [4, { name: 'metric', desc: 'Metric' }]
501
+ ]);
486
502
  public tempUnits: byteValueMap = new byteValueMap([
487
503
  [0, { name: 'F', desc: 'Fahrenheit' }],
488
504
  [4, { name: 'C', desc: 'Celsius' }]
@@ -507,7 +523,7 @@ export class byteValueMaps {
507
523
  [0, { name: 'none', desc: 'None', ph: { min: 6.8, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: false }],
508
524
  [1, { name: 'unknown', desc: 'Unknown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }],
509
525
  [2, { name: 'intellichem', desc: 'IntelliChem', ph: { min: 7.2, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: true }],
510
- [3, { name: 'homegrown', desc: 'Homegrown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }],
526
+ // [3, { name: 'homegrown', desc: 'Homegrown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }],
511
527
  [4, { name: 'rem', desc: 'REM Chem', ph: { min: 6.8, max: 8.0 }, hasAddress: false }]
512
528
  ]);
513
529
  public siCalcTypes: byteValueMap = new byteValueMap([
@@ -558,13 +574,13 @@ export class byteValueMaps {
558
574
  [6, { name: 'qt', desc: 'Quarts' }],
559
575
  [7, { name: 'pt', desc: 'Pints' }]
560
576
  ]);
561
- public pressureUnits: byteValueMap = new byteValueMap([
562
- [0, { name: 'psi', desc: 'Pounds per Sqare Inch' }],
563
- [1, { name: 'Pa', desc: 'Pascal' }],
564
- [2, { name: 'kPa', desc: 'Kilo-pascals' }],
565
- [3, { name: 'atm', desc: 'Atmospheres' }],
566
- [4, { name: 'bar', desc: 'Barometric' }]
567
- ]);
577
+ public pressureUnits: byteValueMap = new byteValueMap([
578
+ [0, { name: 'psi', desc: 'Pounds per Sqare Inch' }],
579
+ [1, { name: 'Pa', desc: 'Pascal' }],
580
+ [2, { name: 'kPa', desc: 'Kilo-pascals' }],
581
+ [3, { name: 'atm', desc: 'Atmospheres' }],
582
+ [4, { name: 'bar', desc: 'Barometric' }]
583
+ ]);
568
584
 
569
585
  public areaUnits: byteValueMap = new byteValueMap([
570
586
  [0, { name: '', desc: 'No Units' }],
@@ -802,36 +818,37 @@ export class SystemBoard {
802
818
  /// This method processes the status message periodically. The role of this method is to verify the circuit, valve, and heater
803
819
  /// relays. This method does not control RS485 operations such as pumps and chlorinators. These are done through the respective
804
820
  /// equipment polling functions.
805
- public async processStatusAsync() {
806
- let self = this;
807
- try {
808
- if (this._statusCheckRef > 0) return;
809
- this.suspendStatus(true);
810
- if (typeof this._statusTimer !== 'undefined' && this._statusTimer) clearTimeout(this._statusTimer);
811
- // Go through all the assigned equipment and verify the current state.
812
- sys.board.system.keepManualTime();
813
- await sys.board.bodies.syncFreezeProtection();
814
- await sys.board.syncEquipmentItems();
815
- await sys.board.schedules.syncScheduleStates();
816
- state.emitControllerChange();
817
- state.emitEquipmentChanges();
818
- } catch (err) { state.status = 255; logger.error(`Error performing processStatusAsync ${err.message}`); }
819
- finally {
820
- this.suspendStatus(false);
821
- if (this.statusInterval > 0) this._statusTimer = setTimeout(async () => await self.processStatusAsync(), this.statusInterval);
822
- }
823
- }
824
- public async syncEquipmentItems() {
825
- try {
826
- await sys.board.circuits.syncCircuitRelayStates();
827
- await sys.board.features.syncGroupStates();
828
- await sys.board.circuits.syncVirtualCircuitStates();
829
- await sys.board.valves.syncValveStates();
830
- await sys.board.filters.syncFilterStates();
831
- await sys.board.heaters.syncHeaterStates();
832
- }
833
- catch (err) { logger.error(`Error synchronizing equipment items: ${err.message}`); }
821
+ public async processStatusAsync() {
822
+ let self = this;
823
+ try {
824
+ if (this._statusCheckRef > 0) return;
825
+ this.suspendStatus(true);
826
+ if (typeof this._statusTimer !== 'undefined' && this._statusTimer) clearTimeout(this._statusTimer);
827
+ // Go through all the assigned equipment and verify the current state.
828
+ sys.board.system.keepManualTime();
829
+ await sys.board.bodies.syncFreezeProtection();
830
+ await sys.board.syncEquipmentItems();
831
+ await sys.board.schedules.syncScheduleStates();
832
+ await sys.board.circuits.checkEggTimerExpirationAsync();
833
+ state.emitControllerChange();
834
+ state.emitEquipmentChanges();
835
+ } catch (err) { state.status = 255; logger.error(`Error performing processStatusAsync ${err.message}`); }
836
+ finally {
837
+ this.suspendStatus(false);
838
+ if (this.statusInterval > 0) this._statusTimer = setTimeout(async () => await self.processStatusAsync(), this.statusInterval);
839
+ }
840
+ }
841
+ public async syncEquipmentItems() {
842
+ try {
843
+ await sys.board.circuits.syncCircuitRelayStates();
844
+ await sys.board.features.syncGroupStates();
845
+ await sys.board.circuits.syncVirtualCircuitStates();
846
+ await sys.board.valves.syncValveStates();
847
+ await sys.board.filters.syncFilterStates();
848
+ await sys.board.heaters.syncHeaterStates();
834
849
  }
850
+ catch (err) { logger.error(`Error synchronizing equipment items: ${err.message}`); }
851
+ }
835
852
  public async setControllerType(obj): Promise<Equipment> {
836
853
  try {
837
854
  if (obj.controllerType !== sys.controllerType)
@@ -909,392 +926,513 @@ export class BoardCommands {
909
926
  constructor(parent: SystemBoard) { this.board = parent; }
910
927
  }
911
928
  export class SystemCommands extends BoardCommands {
912
- public cancelDelay(): Promise<any> { state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); }
913
- public setDateTimeAsync(obj: any): Promise<any> { return Promise.resolve(); }
914
- public keepManualTime() {
929
+ public async restore(rest: { poolConfig: any, poolState: any }): Promise<RestoreResults> {
930
+ let res = new RestoreResults();
931
+ try {
932
+ let ctx = await sys.board.system.validateRestore(rest);
933
+ // Restore the general stuff.
934
+ if (ctx.general.update.length > 0) await sys.board.system.setGeneralAsync(ctx.general.update[0]);
935
+ for (let i = 0; i < ctx.customNames.update.length; i++) {
936
+ let cn = ctx.customNames.update[i];
915
937
  try {
916
- // every minute, updated the time from the system clock in server mode
917
- // but only for Virtual. Likely 'manual' on *Center means OCP time
918
- if (sys.general.options.clockSource !== 'server') return;
919
- state.time.setTimeFromSystemClock();
920
- sys.board.system.setTZ();
921
- } catch (err) { logger.error(`Error setting manual time: ${err.message}`); }
922
- }
923
- public setTZ() {
924
- let tzOffsetObj = state.time.calcTZOffset();
925
- if (sys.general.options.clockSource === 'server' || typeof sys.general.location.timeZone === 'undefined') {
926
- let tzs = sys.board.valueMaps.timeZones.toArray();
927
- sys.general.location.timeZone = tzs.find(tz => tz.utcOffset === tzOffsetObj.tzOffset).val;
928
- }
929
- if (sys.general.options.clockSource === 'server' || typeof sys.general.options.adjustDST === 'undefined') {
930
- sys.general.options.adjustDST = tzOffsetObj.adjustDST;
931
- }
932
- }
933
- public getDOW() { return this.board.valueMaps.scheduleDays.toArray(); }
934
- public async setGeneralAsync(obj: any): Promise<General> {
935
- let general = sys.general.get();
936
- if (typeof obj.alias === 'string') sys.general.alias = obj.alias;
937
- if (typeof obj.options !== 'undefined') await sys.board.system.setOptionsAsync(obj.options);
938
- if (typeof obj.location !== 'undefined') await sys.board.system.setLocationAsync(obj.location);
939
- if (typeof obj.owner !== 'undefined') await sys.board.system.setOwnerAsync(obj.owner);
940
- return new Promise<General>(function (resolve, reject) { resolve(sys.general); });
941
- }
942
- public async setTempSensorsAsync(obj: any): Promise<TempSensorCollection> {
943
- if (typeof obj.waterTempAdj1 != 'undefined' && obj.waterTempAdj1 !== sys.equipment.tempSensors.getCalibration('water1')) {
944
- sys.equipment.tempSensors.setCalibration('water1', parseFloat(obj.waterTempAdj1));
945
- }
946
- if (typeof obj.waterTempAdj2 != 'undefined' && obj.waterTempAdj2 !== sys.equipment.tempSensors.getCalibration('water2')) {
947
- sys.equipment.tempSensors.setCalibration('water2', parseFloat(obj.waterTempAdj2));
948
- }
949
- if (typeof obj.waterTempAdj3 != 'undefined' && obj.waterTempAdj3 !== sys.equipment.tempSensors.getCalibration('water3')) {
950
- sys.equipment.tempSensors.setCalibration('water3', parseFloat(obj.waterTempAdj3));
951
- }
952
- if (typeof obj.waterTempAdj4 != 'undefined' && obj.waterTempAdj4 !== sys.equipment.tempSensors.getCalibration('water4')) {
953
- sys.equipment.tempSensors.setCalibration('water4', parseFloat(obj.waterTempAdj4));
954
- }
955
- if (typeof obj.solarTempAdj1 != 'undefined' && obj.solarTempAdj1 !== sys.equipment.tempSensors.getCalibration('solar1')) {
956
- sys.equipment.tempSensors.setCalibration('solar1', parseFloat(obj.solarTempAdj1));
957
- }
958
- if (typeof obj.solarTempAdj2 != 'undefined' && obj.solarTempAdj2 !== sys.equipment.tempSensors.getCalibration('solar2')) {
959
- sys.equipment.tempSensors.setCalibration('solar2', parseFloat(obj.solarTempAdj2));
960
- }
961
- if (typeof obj.solarTempAdj3 != 'undefined' && obj.solarTempAdj3 !== sys.equipment.tempSensors.getCalibration('solar3')) {
962
- sys.equipment.tempSensors.setCalibration('solar3', parseFloat(obj.solarTempAdj3));
963
- }
964
- if (typeof obj.solarTempAdj4 != 'undefined' && obj.solarTempAdj4 !== sys.equipment.tempSensors.getCalibration('solar4')) {
965
- sys.equipment.tempSensors.setCalibration('solar4', parseFloat(obj.solarTempAdj4));
966
- }
967
- if (typeof obj.airTempAdj != 'undefined' && obj.airTempAdj !== sys.equipment.tempSensors.getCalibration('air')) {
968
- sys.equipment.tempSensors.setCalibration('air', parseFloat(obj.airTempAdj));
969
- }
970
- return new Promise<TempSensorCollection>((resolve, reject) => { resolve(sys.equipment.tempSensors); });
971
- }
972
- public async setOptionsAsync(obj: any): Promise<Options> {
973
- if (obj.clockSource === 'server') sys.board.system.setTZ();
974
- sys.board.system.setTempSensorsAsync(obj);
975
- sys.general.options.set(obj);
976
- return new Promise<Options>(function (resolve, reject) { resolve(sys.general.options); });
977
- }
978
- public async setLocationAsync(obj: any): Promise<Location> {
979
- sys.general.location.set(obj);
980
- return new Promise<Location>(function (resolve, reject) { resolve(sys.general.location); });
981
- }
982
- public async setOwnerAsync(obj: any): Promise<Owner> {
983
- sys.general.owner.set(obj);
984
- return new Promise<Owner>(function (resolve, reject) { resolve(sys.general.owner); });
985
- }
986
- public async setTempsAsync(obj: any): Promise<TemperatureState> {
987
- return new Promise<TemperatureState>((resolve, reject) => {
988
- for (let prop in obj) {
989
- switch (prop) {
990
- case 'air':
991
- case 'airSensor':
992
- case 'airSensor1':
993
- {
994
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
995
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
996
- state.temps.air = sys.equipment.tempSensors.getCalibration('air') + temp;
997
- }
998
- break;
999
- case 'waterSensor1':
1000
- {
1001
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1002
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1003
- state.temps.waterSensor1 = sys.equipment.tempSensors.getCalibration('water1') + temp;
1004
- let body = state.temps.bodies.getItemById(1);
1005
- if (body.isOn) body.temp = state.temps.waterSensor1;
1006
- else if (sys.equipment.shared) {
1007
- body = state.temps.bodies.getItemById(2);
1008
- if (body.isOn) body.temp = state.temps.waterSensor1;
1009
- }
1010
- }
1011
- break;
1012
- case 'waterSensor2':
1013
- {
1014
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1015
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1016
- state.temps.waterSensor2 = sys.equipment.tempSensors.getCalibration('water2') + temp;
1017
- if (!state.equipment.shared) {
1018
- let body = state.temps.bodies.getItemById(2);
1019
- if (body.isOn) body.temp = state.temps.waterSensor2;
1020
- }
1021
- }
1022
- break;
1023
- case 'waterSensor3':
1024
- {
1025
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1026
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1027
- state.temps.waterSensor3 = sys.equipment.tempSensors.getCalibration('water3') + temp;
1028
- let body = state.temps.bodies.getItemById(3);
1029
- if (body.isOn) body.temp = state.temps.waterSensor3;
1030
- }
1031
- break;
1032
- case 'waterSensor4':
1033
- {
1034
-
1035
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1036
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1037
- state.temps.waterSensor4 = sys.equipment.tempSensors.getCalibration('water4') + temp;
1038
- let body = state.temps.bodies.getItemById(4);
1039
- if (body.isOn) body.temp = state.temps.waterSensor4;
1040
- }
1041
- break;
938
+ await sys.board.system.setCustomNameAsync(cn);
939
+ res.addModuleSuccess('customName', `Update: ${cn.id}-${cn.name}`);
940
+ } catch (err) { res.addModuleError('customName', `Update: ${cn.id}-${cn.name}: ${err.message}`); }
941
+ }
942
+ for (let i = 0; i < ctx.customNames.add.length; i++) {
943
+ let cn = ctx.customNames.add[i];
944
+ try {
945
+ await sys.board.system.setCustomNameAsync(cn);
946
+ res.addModuleSuccess('customName', `Add: ${cn.id}-${cn.name}`);
947
+ } catch (err) { res.addModuleError('customName', `Add: ${cn.id}-${cn.name}: ${err.message}`); }
948
+ }
949
+ await sys.board.bodies.restore(rest, ctx, res);
950
+ await sys.board.filters.restore(rest, ctx, res);
951
+ await sys.board.circuits.restore(rest, ctx, res);
952
+ await sys.board.heaters.restore(rest, ctx, res);
953
+ await sys.board.features.restore(rest, ctx, res);
954
+ await sys.board.pumps.restore(rest, ctx, res);
955
+ await sys.board.valves.restore(rest, ctx, res);
956
+ await sys.board.chlorinator.restore(rest, ctx, res);
957
+ await sys.board.chemControllers.restore(rest, ctx, res);
958
+ await sys.board.schedules.restore(rest, ctx, res);
959
+ return res;
960
+ //await sys.board.covers.restore(rest, ctx);
961
+ } catch (err) { logger.error(`Error restoring njsPC server: ${err.message}`); res.addModuleError('system', err.message); return Promise.reject(err);}
962
+ }
963
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<any> {
964
+ try {
965
+ let ctx: any = { board: { errors: [], warnings: [] } };
1042
966
 
1043
- case 'solarSensor1':
1044
- case 'solar1':
1045
- case 'solar':
1046
- {
1047
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1048
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1049
- state.temps.solar = sys.equipment.tempSensors.getCalibration('solar1') + temp;
1050
- }
1051
- break;
1052
- case 'solar2':
1053
- case 'solarSensor2':
1054
- {
1055
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1056
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1057
- state.temps.solarSensor2 = sys.equipment.tempSensors.getCalibration('solar2') + temp;
1058
- }
1059
- break;
1060
- case 'solar3':
1061
- case 'solarSensor3':
1062
- {
1063
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1064
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1065
- state.temps.solarSensor3 = sys.equipment.tempSensors.getCalibration('solar3') + temp;
1066
- }
1067
- break;
1068
- case 'solar4':
1069
- case 'solarSensor4':
1070
- {
1071
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1072
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1073
- state.temps.solarSensor4 = sys.equipment.tempSensors.getCalibration('solar4') + temp;
1074
- }
1075
- break;
1076
- }
967
+ // Step 1 - Verify that the boards are the same. For instance you do not want to restore an IntelliTouch to an IntelliCenter.
968
+ let cfg = rest.poolConfig;
969
+ if (sys.controllerType === cfg.controllerType) {
970
+ ctx.customNames = { errors: [], warnings: [], add: [], update: [], remove: [] };
971
+ let customNames = sys.customNames.get();
972
+ for (let i = 0; i < rest.poolConfig.customNames.length; i++) {
973
+ let cn = customNames.find(elem => elem.id === rest.poolConfig.customNames[i].id);
974
+ if (typeof cn === 'undefined') ctx.customNames.add.push(rest.poolConfig.customNames[i]);
975
+ else if (JSON.stringify(rest.poolConfig.customNames[i]) !== JSON.stringify(cn)) ctx.customNames.update.push(cn);
976
+ }
977
+ ctx.general = { errors: [], warnings: [], add: [], update: [], remove: [] };
978
+ if (JSON.stringify(sys.general.get()) !== JSON.stringify(cfg.pool)) ctx.general.update.push(cfg.pool);
979
+ ctx.bodies = await sys.board.bodies.validateRestore(rest);
980
+ ctx.pumps = await sys.board.pumps.validateRestore(rest);
981
+ await sys.board.circuits.validateRestore(rest, ctx);
982
+ ctx.features = await sys.board.features.validateRestore(rest);
983
+ ctx.chlorinators = await sys.board.chlorinator.validateRestore(rest);
984
+ ctx.heaters = await sys.board.heaters.validateRestore(rest);
985
+ ctx.valves = await sys.board.valves.validateRestore(rest);
986
+
987
+ //ctx.covers = await sys.board.covers.validateRestore(rest);
988
+ ctx.chemControllers = await sys.board.chemControllers.validateRestore(rest);
989
+ ctx.filters = await sys.board.filters.validateRestore(rest);
990
+ ctx.schedules = await sys.board.schedules.validateRestore(rest);
991
+ }
992
+ else ctx.board.errors.push(`Panel Types do not match cannot restore bakup from ${sys.controllerType} to ${rest.poolConfig.controllerType}`);
993
+
994
+ return ctx;
995
+
996
+ } catch (err) { logger.error(`Error validating restore file: ${err.message}`); return Promise.reject(err);}
997
+
998
+ }
999
+ public cancelDelay(): Promise<any> { state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); }
1000
+ public setDateTimeAsync(obj: any): Promise<any> { return Promise.resolve(); }
1001
+ public keepManualTime() {
1002
+ try {
1003
+ // every minute, updated the time from the system clock in server mode
1004
+ // but only for Virtual. Likely 'manual' on *Center means OCP time
1005
+ if (sys.general.options.clockSource !== 'server') return;
1006
+ state.time.setTimeFromSystemClock();
1007
+ sys.board.system.setTZ();
1008
+ } catch (err) { logger.error(`Error setting manual time: ${err.message}`); }
1009
+ }
1010
+ public setTZ() {
1011
+ let tzOffsetObj = state.time.calcTZOffset();
1012
+ if (sys.general.options.clockSource === 'server' || typeof sys.general.location.timeZone === 'undefined') {
1013
+ let tzs = sys.board.valueMaps.timeZones.toArray();
1014
+ sys.general.location.timeZone = tzs.find(tz => tz.utcOffset === tzOffsetObj.tzOffset).val;
1015
+ }
1016
+ if (sys.general.options.clockSource === 'server' || typeof sys.general.options.adjustDST === 'undefined') {
1017
+ sys.general.options.adjustDST = tzOffsetObj.adjustDST;
1018
+ }
1019
+ }
1020
+ public getDOW() { return this.board.valueMaps.scheduleDays.toArray(); }
1021
+ public async setGeneralAsync(obj: any): Promise<General> {
1022
+ let general = sys.general.get();
1023
+ if (typeof obj.alias === 'string') sys.general.alias = obj.alias;
1024
+ if (typeof obj.options !== 'undefined') await sys.board.system.setOptionsAsync(obj.options);
1025
+ if (typeof obj.location !== 'undefined') await sys.board.system.setLocationAsync(obj.location);
1026
+ if (typeof obj.owner !== 'undefined') await sys.board.system.setOwnerAsync(obj.owner);
1027
+ return new Promise<General>(function (resolve, reject) { resolve(sys.general); });
1028
+ }
1029
+ public async setTempSensorsAsync(obj: any): Promise<TempSensorCollection> {
1030
+ if (typeof obj.waterTempAdj1 != 'undefined' && obj.waterTempAdj1 !== sys.equipment.tempSensors.getCalibration('water1')) {
1031
+ sys.equipment.tempSensors.setCalibration('water1', parseFloat(obj.waterTempAdj1));
1032
+ }
1033
+ if (typeof obj.waterTempAdj2 != 'undefined' && obj.waterTempAdj2 !== sys.equipment.tempSensors.getCalibration('water2')) {
1034
+ sys.equipment.tempSensors.setCalibration('water2', parseFloat(obj.waterTempAdj2));
1035
+ }
1036
+ if (typeof obj.waterTempAdj3 != 'undefined' && obj.waterTempAdj3 !== sys.equipment.tempSensors.getCalibration('water3')) {
1037
+ sys.equipment.tempSensors.setCalibration('water3', parseFloat(obj.waterTempAdj3));
1038
+ }
1039
+ if (typeof obj.waterTempAdj4 != 'undefined' && obj.waterTempAdj4 !== sys.equipment.tempSensors.getCalibration('water4')) {
1040
+ sys.equipment.tempSensors.setCalibration('water4', parseFloat(obj.waterTempAdj4));
1041
+ }
1042
+ if (typeof obj.solarTempAdj1 != 'undefined' && obj.solarTempAdj1 !== sys.equipment.tempSensors.getCalibration('solar1')) {
1043
+ sys.equipment.tempSensors.setCalibration('solar1', parseFloat(obj.solarTempAdj1));
1044
+ }
1045
+ if (typeof obj.solarTempAdj2 != 'undefined' && obj.solarTempAdj2 !== sys.equipment.tempSensors.getCalibration('solar2')) {
1046
+ sys.equipment.tempSensors.setCalibration('solar2', parseFloat(obj.solarTempAdj2));
1047
+ }
1048
+ if (typeof obj.solarTempAdj3 != 'undefined' && obj.solarTempAdj3 !== sys.equipment.tempSensors.getCalibration('solar3')) {
1049
+ sys.equipment.tempSensors.setCalibration('solar3', parseFloat(obj.solarTempAdj3));
1050
+ }
1051
+ if (typeof obj.solarTempAdj4 != 'undefined' && obj.solarTempAdj4 !== sys.equipment.tempSensors.getCalibration('solar4')) {
1052
+ sys.equipment.tempSensors.setCalibration('solar4', parseFloat(obj.solarTempAdj4));
1053
+ }
1054
+ if (typeof obj.airTempAdj != 'undefined' && obj.airTempAdj !== sys.equipment.tempSensors.getCalibration('air')) {
1055
+ sys.equipment.tempSensors.setCalibration('air', parseFloat(obj.airTempAdj));
1056
+ }
1057
+ return new Promise<TempSensorCollection>((resolve, reject) => { resolve(sys.equipment.tempSensors); });
1058
+ }
1059
+ public async setOptionsAsync(obj: any): Promise<Options> {
1060
+ if (obj.clockSource === 'server') sys.board.system.setTZ();
1061
+ sys.board.system.setTempSensorsAsync(obj);
1062
+ sys.general.options.set(obj);
1063
+ let bodyUnits = sys.general.options.units === 0 ? 1 : 2;
1064
+ for (let i = 0; i < sys.bodies.length; i++) sys.bodies.getItemByIndex(i).capacityUnits = bodyUnits;
1065
+ state.temps.units = sys.general.options.units === 0 ? 1 : 4;
1066
+ return new Promise<Options>(function (resolve, reject) { resolve(sys.general.options); });
1067
+ }
1068
+ public async setLocationAsync(obj: any): Promise<Location> {
1069
+ sys.general.location.set(obj);
1070
+ return new Promise<Location>(function (resolve, reject) { resolve(sys.general.location); });
1071
+ }
1072
+ public async setOwnerAsync(obj: any): Promise<Owner> {
1073
+ sys.general.owner.set(obj);
1074
+ return new Promise<Owner>(function (resolve, reject) { resolve(sys.general.owner); });
1075
+ }
1076
+ public async setTempsAsync(obj: any): Promise<TemperatureState> {
1077
+ return new Promise<TemperatureState>((resolve, reject) => {
1078
+ for (let prop in obj) {
1079
+ switch (prop) {
1080
+ case 'air':
1081
+ case 'airSensor':
1082
+ case 'airSensor1':
1083
+ {
1084
+ let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1085
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1086
+ state.temps.air = sys.equipment.tempSensors.getCalibration('air') + temp;
1077
1087
  }
1078
- sys.board.heaters.syncHeaterStates();
1079
- resolve(state.temps);
1080
- });
1081
- }
1082
- public getSensors() {
1083
- let sensors = [{ name: 'Air Sensor', temp: state.temps.air, tempAdj: sys.equipment.tempSensors.getCalibration('air'), binding: 'airTempAdj' }];
1084
- if (sys.equipment.shared) {
1085
- if (sys.equipment.maxBodies > 2)
1086
- sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1087
- { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' },
1088
- { name: 'Water Sensor 3', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1089
- else
1090
- sensors.push({ name: 'Water Sensor', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' });
1091
- if (sys.equipment.maxBodies > 3)
1092
- sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1093
-
1094
- if (sys.board.heaters.isSolarInstalled()) {
1095
- if (sys.equipment.maxBodies > 2) {
1096
- sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1097
- { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1098
- }
1099
- else
1100
- sensors.push({ name: 'Solar Sensor', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' });
1101
- if (sys.equipment.maxBodies > 3)
1102
- sensors.push({ name: 'Solar Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1088
+ break;
1089
+ case 'waterSensor1':
1090
+ {
1091
+ let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1092
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1093
+ state.temps.waterSensor1 = sys.equipment.tempSensors.getCalibration('water1') + temp;
1094
+ let body = state.temps.bodies.getItemById(1);
1095
+ if (body.isOn) body.temp = state.temps.waterSensor1;
1096
+ else if (sys.equipment.shared) {
1097
+ body = state.temps.bodies.getItemById(2);
1098
+ if (body.isOn) body.temp = state.temps.waterSensor1;
1099
+ }
1103
1100
  }
1104
- }
1105
- else if (sys.equipment.dual) {
1106
- sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1107
- { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' });
1108
- if (sys.equipment.maxBodies > 2)
1109
- sensors.push({ name: 'Water Sensor 3', temp: state.temps.waterSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1110
- if (sys.equipment.maxBodies > 3)
1111
- sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1112
- if (sys.board.heaters.isSolarInstalled()) {
1113
- sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1114
- { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1115
- if (sys.equipment.maxBodies > 2)
1116
- sensors.push({ name: 'Solar Sensor 3', temp: state.temps.solarSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('solar3'), binding: 'solarTempAdj3' });
1117
- if (sys.equipment.maxBodies > 3)
1118
- sensors.push({ name: 'Solar Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1101
+ break;
1102
+ case 'waterSensor2':
1103
+ {
1104
+ let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1105
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1106
+ state.temps.waterSensor2 = sys.equipment.tempSensors.getCalibration('water2') + temp;
1107
+ if (!state.equipment.shared) {
1108
+ let body = state.temps.bodies.getItemById(2);
1109
+ if (body.isOn) body.temp = state.temps.waterSensor2;
1110
+ }
1119
1111
  }
1120
- }
1121
- else {
1122
- if (sys.equipment.maxBodies > 1) {
1123
- sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1124
- { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' });
1125
- if (sys.equipment.maxBodies > 2)
1126
- sensors.push({ name: 'Water Sensor 3', temp: state.temps.waterSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1127
- if (sys.equipment.maxBodies > 3)
1128
- sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1129
-
1130
- if (sys.board.heaters.isSolarInstalled()) {
1131
- sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solarSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1132
- { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1133
- if (sys.equipment.maxBodies > 2)
1134
- sensors.push({ name: 'Solar Sensor 3', temp: state.temps.solarSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('solar3'), binding: 'solarTempAdj3' });
1135
- if (sys.equipment.maxBodies > 3)
1136
- sensors.push({ name: 'Water Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1137
- }
1112
+ break;
1113
+ case 'waterSensor3':
1114
+ {
1115
+ let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1116
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1117
+ state.temps.waterSensor3 = sys.equipment.tempSensors.getCalibration('water3') + temp;
1118
+ let body = state.temps.bodies.getItemById(3);
1119
+ if (body.isOn) body.temp = state.temps.waterSensor3;
1138
1120
  }
1139
- else {
1140
- sensors.push({ name: 'Water Sensor', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' });
1141
- if (sys.board.heaters.isSolarInstalled())
1142
- sensors.push({ name: 'Solar Sensor', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' });
1121
+ break;
1122
+ case 'waterSensor4':
1123
+ {
1124
+
1125
+ let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1126
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1127
+ state.temps.waterSensor4 = sys.equipment.tempSensors.getCalibration('water4') + temp;
1128
+ let body = state.temps.bodies.getItemById(4);
1129
+ if (body.isOn) body.temp = state.temps.waterSensor4;
1130
+ }
1131
+ break;
1132
+
1133
+ case 'solarSensor1':
1134
+ case 'solar1':
1135
+ case 'solar':
1136
+ {
1137
+ let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1138
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1139
+ state.temps.solar = sys.equipment.tempSensors.getCalibration('solar1') + temp;
1143
1140
  }
1141
+ break;
1142
+ case 'solar2':
1143
+ case 'solarSensor2':
1144
+ {
1145
+ let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1146
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1147
+ state.temps.solarSensor2 = sys.equipment.tempSensors.getCalibration('solar2') + temp;
1148
+ }
1149
+ break;
1150
+ case 'solar3':
1151
+ case 'solarSensor3':
1152
+ {
1153
+ let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1154
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1155
+ state.temps.solarSensor3 = sys.equipment.tempSensors.getCalibration('solar3') + temp;
1156
+ }
1157
+ break;
1158
+ case 'solar4':
1159
+ case 'solarSensor4':
1160
+ {
1161
+ let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1162
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1163
+ state.temps.solarSensor4 = sys.equipment.tempSensors.getCalibration('solar4') + temp;
1164
+ }
1165
+ break;
1144
1166
  }
1145
- return sensors;
1167
+ }
1168
+ sys.board.heaters.syncHeaterStates();
1169
+ resolve(state.temps);
1170
+ });
1171
+ }
1172
+ public getSensors() {
1173
+ let sensors = [{ name: 'Air Sensor', temp: state.temps.air, tempAdj: sys.equipment.tempSensors.getCalibration('air'), binding: 'airTempAdj' }];
1174
+ if (sys.equipment.shared) {
1175
+ if (sys.equipment.maxBodies > 2)
1176
+ sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1177
+ { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' },
1178
+ { name: 'Water Sensor 3', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1179
+ else
1180
+ sensors.push({ name: 'Water Sensor', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' });
1181
+ if (sys.equipment.maxBodies > 3)
1182
+ sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1183
+
1184
+ if (sys.board.heaters.isSolarInstalled()) {
1185
+ if (sys.equipment.maxBodies > 2) {
1186
+ sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1187
+ { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1188
+ }
1189
+ else
1190
+ sensors.push({ name: 'Solar Sensor', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' });
1191
+ if (sys.equipment.maxBodies > 3)
1192
+ sensors.push({ name: 'Solar Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1193
+ }
1194
+ }
1195
+ else if (sys.equipment.dual) {
1196
+ sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1197
+ { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' });
1198
+ if (sys.equipment.maxBodies > 2)
1199
+ sensors.push({ name: 'Water Sensor 3', temp: state.temps.waterSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1200
+ if (sys.equipment.maxBodies > 3)
1201
+ sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1202
+ if (sys.board.heaters.isSolarInstalled()) {
1203
+ sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1204
+ { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1205
+ if (sys.equipment.maxBodies > 2)
1206
+ sensors.push({ name: 'Solar Sensor 3', temp: state.temps.solarSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('solar3'), binding: 'solarTempAdj3' });
1207
+ if (sys.equipment.maxBodies > 3)
1208
+ sensors.push({ name: 'Solar Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1209
+ }
1146
1210
  }
1147
- public async setCustomNamesAsync(names: any[]): Promise<CustomNameCollection> {
1148
- if (!Array.isArray(names)) return Promise.reject(new InvalidEquipmentDataError(`Data is not an array`, 'customNames', names))
1149
- let arr = [];
1150
- for (let i = 0; i < names.length; i++) { arr.push(sys.board.system.setCustomNameAsync(names[i])); }
1151
- return new Promise<CustomNameCollection>(async (resolve, reject) => {
1152
- try {
1153
- await Promise.all(arr).catch(err => reject(err));
1154
- // sys.board.system.syncCustomNamesValueMap(); Each custom name promise is already syncing the bytevalue array
1211
+ else {
1212
+ if (sys.equipment.maxBodies > 1) {
1213
+ sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1214
+ { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' });
1215
+ if (sys.equipment.maxBodies > 2)
1216
+ sensors.push({ name: 'Water Sensor 3', temp: state.temps.waterSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1217
+ if (sys.equipment.maxBodies > 3)
1218
+ sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1155
1219
 
1156
- resolve(sys.customNames);
1157
- }
1158
- catch (err) { reject(err); }
1159
- });
1160
- }
1161
- public async setCustomNameAsync(data: any): Promise<CustomName> {
1162
- return new Promise<CustomName>((resolve, reject) => {
1163
- let id = parseInt(data.id, 10);
1164
- if (isNaN(id)) return reject(new InvalidEquipmentIdError('Invalid Custom Name Id', data.id, 'customName'));
1165
- if (id > sys.equipment.maxCustomNames) return reject(new InvalidEquipmentIdError('Custom Name Id out of range', data.id, 'customName'));
1166
- let cname = sys.customNames.getItemById(id, true);
1167
- cname.name = data.name;
1168
- sys.board.system.syncCustomNamesValueMap();
1169
- return resolve(cname);
1170
- });
1171
- }
1172
- public syncCustomNamesValueMap() {
1173
- sys.customNames.sortById();
1174
- sys.board.valueMaps.customNames = new byteValueMap(
1175
- sys.customNames.get().map((el, idx) => {
1176
- return [idx + 200, { name: el.name, desc: el.name }];
1177
- })
1178
- );
1220
+ if (sys.board.heaters.isSolarInstalled()) {
1221
+ sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solarSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1222
+ { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1223
+ if (sys.equipment.maxBodies > 2)
1224
+ sensors.push({ name: 'Solar Sensor 3', temp: state.temps.solarSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('solar3'), binding: 'solarTempAdj3' });
1225
+ if (sys.equipment.maxBodies > 3)
1226
+ sensors.push({ name: 'Water Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1227
+ }
1228
+ }
1229
+ else {
1230
+ sensors.push({ name: 'Water Sensor', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' });
1231
+ if (sys.board.heaters.isSolarInstalled())
1232
+ sensors.push({ name: 'Solar Sensor', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' });
1233
+ }
1179
1234
  }
1235
+ return sensors;
1236
+ }
1237
+ public async setCustomNamesAsync(names: any[]): Promise<CustomNameCollection> {
1238
+ if (!Array.isArray(names)) return Promise.reject(new InvalidEquipmentDataError(`Data is not an array`, 'customNames', names))
1239
+ let arr = [];
1240
+ for (let i = 0; i < names.length; i++) { arr.push(sys.board.system.setCustomNameAsync(names[i])); }
1241
+ return new Promise<CustomNameCollection>(async (resolve, reject) => {
1242
+ try {
1243
+ await Promise.all(arr).catch(err => reject(err));
1244
+ // sys.board.system.syncCustomNamesValueMap(); Each custom name promise is already syncing the bytevalue array
1245
+
1246
+ resolve(sys.customNames);
1247
+ }
1248
+ catch (err) { reject(err); }
1249
+ });
1250
+ }
1251
+ public async setCustomNameAsync(data: any): Promise<CustomName> {
1252
+ return new Promise<CustomName>((resolve, reject) => {
1253
+ let id = parseInt(data.id, 10);
1254
+ if (isNaN(id)) return reject(new InvalidEquipmentIdError('Invalid Custom Name Id', data.id, 'customName'));
1255
+ if (id > sys.equipment.maxCustomNames) return reject(new InvalidEquipmentIdError('Custom Name Id out of range', data.id, 'customName'));
1256
+ let cname = sys.customNames.getItemById(id, true);
1257
+ cname.name = data.name;
1258
+ sys.board.system.syncCustomNamesValueMap();
1259
+ return resolve(cname);
1260
+ });
1261
+ }
1262
+ public syncCustomNamesValueMap() {
1263
+ sys.customNames.sortById();
1264
+ sys.board.valueMaps.customNames = new byteValueMap(
1265
+ sys.customNames.get().map((el, idx) => {
1266
+ return [idx + 200, { name: el.name, desc: el.name }];
1267
+ })
1268
+ );
1269
+ }
1180
1270
  }
1181
1271
  export class BodyCommands extends BoardCommands {
1182
- public freezeProtectBodyOn: Date;
1183
- public freezeProtectStart: Date;
1184
- public async syncFreezeProtection() {
1272
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
1273
+ try {
1274
+ // First delete the bodies that should be removed.
1275
+ for (let i = 0; i < ctx.bodies.remove.length; i++) {
1276
+ let body = ctx.bodies.remove[i];
1185
1277
  try {
1186
- // Go through all the features and circuits to make sure we have the freeze protect set appropriately. The freeze
1187
- // flag will have already been set whether this is a Nixie setup or there is an OCP involved.
1188
-
1189
- // First turn on/off any features that are in our control that should be under our control. If this is an OCP we
1190
- // do not create features beyond those controlled by the OCP so we don't need to check these in that condition. That is
1191
- // why it first checks the controller type.
1192
- let freeze = utils.makeBool(state.freeze);
1193
- if (sys.controllerType === ControllerType.Nixie) {
1194
- // If we are a Nixie controller we need to evaluate the current freeze settings against the air temperature.
1195
- if (typeof state.temps.air !== 'undefined') freeze = state.temps.air <= sys.general.options.freezeThreshold;
1196
- else freeze = false;
1197
-
1198
- // We need to know when we first turned the freeze protection on. This is because we will be rotating between pool and spa
1199
- // on shared body systems when both pool and spa have freeze protection checked.
1200
- if (state.freeze !== freeze) {
1201
- this.freezeProtectStart = freeze ? new Date() : undefined;
1202
- state.freeze = freeze;
1203
- }
1204
- for (let i = 0; i < sys.features.length; i++) {
1205
- let feature = sys.features.getItemByIndex(i);
1206
- let fstate = state.features.getItemById(feature.id, true);
1207
- if (!feature.freeze || !feature.isActive === true || feature.master !== 1) {
1208
- fstate.freezeProtect = false;
1209
- continue; // This is not affected by freeze conditions.
1210
- }
1211
- if (freeze && !fstate.isOn) {
1212
- // This feature should be on because we are freezing.
1213
- fstate.freezeProtect = true;
1214
- await sys.board.features.setFeatureStateAsync(feature.id, true);
1215
- }
1216
- else if (!freeze && fstate.freezeProtect) {
1217
- // This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
1218
- fstate.freezeProtect = false;
1219
- await sys.board.features.setFeatureStateAsync(feature.id, false);
1220
- }
1221
- }
1278
+ sys.bodies.removeItemById(body.id);
1279
+ state.temps.bodies.removeItemById(body.id);
1280
+ res.addModuleSuccess('body', `Remove: ${body.id}-${body.name}`);
1281
+ } catch (err) { res.addModuleError('body', `Remove: ${body.id}-${body.name}: ${err.message}`); }
1282
+ }
1283
+ for (let i = 0; i < ctx.bodies.update.length; i++) {
1284
+ let body = ctx.bodies.update[i];
1285
+ try {
1286
+ await sys.board.bodies.setBodyAsync(body);
1287
+ res.addModuleSuccess('body', `Update: ${body.id}-${body.name}`);
1288
+ } catch (err) { res.addModuleError('body', `Update: ${body.id}-${body.name}: ${err.message}`); }
1289
+ }
1290
+ for (let i = 0; i < ctx.bodies.add.length; i++) {
1291
+ let body = ctx.bodies.add[i];
1292
+ try {
1293
+ // pull a little trick to first add the data then perform the update.
1294
+ sys.bodies.getItemById(body.id, true);
1295
+ await sys.board.bodies.setBodyAsync(body);
1296
+ } catch (err) { res.addModuleError('body', `Add: ${body.id}-${body.name}: ${err.message}`); }
1297
+ }
1298
+ return true;
1299
+ } catch (err) { logger.error(`Error restoring bodies: ${err.message}`); res.addModuleError('system', `Error restoring bodies: ${err.message}`); return false; }
1300
+ }
1301
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any}> {
1302
+ try {
1303
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
1304
+ // Look at bodies.
1305
+ let cfg = rest.poolConfig;
1306
+ for (let i = 0; i < cfg.bodies.length; i++) {
1307
+ let r = cfg.bodies[i];
1308
+ let c = sys.bodies.find(elem => r.id === elem.id);
1309
+ if (typeof c === 'undefined') ctx.add.push(r);
1310
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
1311
+ }
1312
+ for (let i = 0; i < sys.bodies.length; i++) {
1313
+ let c = sys.bodies.getItemByIndex(i);
1314
+ let r = cfg.bodies.find(elem => elem.id == c.id);
1315
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
1316
+ }
1317
+ return ctx;
1318
+ } catch (err) { logger.error(`Error validating bodies for restore: ${err.message}`); }
1319
+ }
1320
+ public freezeProtectBodyOn: Date;
1321
+ public freezeProtectStart: Date;
1322
+ public async syncFreezeProtection() {
1323
+ try {
1324
+ // Go through all the features and circuits to make sure we have the freeze protect set appropriately. The freeze
1325
+ // flag will have already been set whether this is a Nixie setup or there is an OCP involved.
1326
+
1327
+ // First turn on/off any features that are in our control that should be under our control. If this is an OCP we
1328
+ // do not create features beyond those controlled by the OCP so we don't need to check these in that condition. That is
1329
+ // why it first checks the controller type.
1330
+ let freeze = utils.makeBool(state.freeze);
1331
+ if (sys.controllerType === ControllerType.Nixie) {
1332
+ // If we are a Nixie controller we need to evaluate the current freeze settings against the air temperature.
1333
+ if (typeof state.temps.air !== 'undefined') freeze = state.temps.air <= sys.general.options.freezeThreshold;
1334
+ else freeze = false;
1335
+
1336
+ // We need to know when we first turned the freeze protection on. This is because we will be rotating between pool and spa
1337
+ // on shared body systems when both pool and spa have freeze protection checked.
1338
+ if (state.freeze !== freeze) {
1339
+ this.freezeProtectStart = freeze ? new Date() : undefined;
1340
+ state.freeze = freeze;
1341
+ }
1342
+ for (let i = 0; i < sys.features.length; i++) {
1343
+ let feature = sys.features.getItemByIndex(i);
1344
+ let fstate = state.features.getItemById(feature.id, true);
1345
+ if (!feature.freeze || !feature.isActive === true || feature.master !== 1) {
1346
+ fstate.freezeProtect = false;
1347
+ continue; // This is not affected by freeze conditions.
1348
+ }
1349
+ if (freeze && !fstate.isOn) {
1350
+ // This feature should be on because we are freezing.
1351
+ fstate.freezeProtect = true;
1352
+ await sys.board.features.setFeatureStateAsync(feature.id, true);
1353
+ }
1354
+ else if (!freeze && fstate.freezeProtect) {
1355
+ // This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
1356
+ fstate.freezeProtect = false;
1357
+ await sys.board.features.setFeatureStateAsync(feature.id, false);
1358
+ }
1359
+ }
1360
+ }
1361
+ let bodyRotationChecked = false;
1362
+ for (let i = 0; i < sys.circuits.length; i++) {
1363
+ let circ = sys.circuits.getItemByIndex(i);
1364
+ let cstate = state.circuits.getItemById(circ.id);
1365
+ if (!circ.freeze || !circ.isActive === true || circ.master !== 1) {
1366
+ cstate.freezeProtect = false;
1367
+ continue; // This is not affected by freeze conditions.
1368
+ }
1369
+ if (sys.equipment.shared && freeze && (circ.id === 1 || circ.id === 6)) {
1370
+ // Exit out of here because we already checked the body rotation. We only want to do this once since it can be expensive turning
1371
+ // on a particular body.
1372
+ if (bodyRotationChecked) continue;
1373
+ // These are our body circuits so we need to check to see if they need to be rotated between pool and spa.
1374
+ let pool = circ.id === 6 ? circ : sys.circuits.getItemById(6);
1375
+ let spa = circ.id === 1 ? circ : sys.circuits.getItemById(1);
1376
+ if (pool.freeze && spa.freeze) {
1377
+ // We only need to rotate between pool and spa when they are both checked.
1378
+ let pstate = circ.id === 6 ? cstate : state.circuits.getItemById(6);
1379
+ let sstate = circ.id === 1 ? cstate : state.circuits.getItemById(1);
1380
+ if (!pstate.isOn && !sstate.isOn) {
1381
+ // Neither the pool or spa are on so we will turn on the pool first.
1382
+ pstate.freezeProtect = true;
1383
+ this.freezeProtectBodyOn = new Date();
1384
+ await sys.board.circuits.setCircuitStateAsync(6, true);
1222
1385
  }
1223
- let bodyRotationChecked = false;
1224
- for (let i = 0; i < sys.circuits.length; i++) {
1225
- let circ = sys.circuits.getItemByIndex(i);
1226
- let cstate = state.circuits.getItemById(circ.id);
1227
- if (!circ.freeze || !circ.isActive === true || circ.master !== 1) {
1228
- cstate.freezeProtect = false;
1229
- continue; // This is not affected by freeze conditions.
1230
- }
1231
- if (sys.equipment.shared && freeze && (circ.id === 1 || circ.id === 6)) {
1232
- // Exit out of here because we already checked the body rotation. We only want to do this once since it can be expensive turning
1233
- // on a particular body.
1234
- if (bodyRotationChecked) continue;
1235
- // These are our body circuits so we need to check to see if they need to be rotated between pool and spa.
1236
- let pool = circ.id === 6 ? circ : sys.circuits.getItemById(6);
1237
- let spa = circ.id === 1 ? circ : sys.circuits.getItemById(1);
1238
- if (pool.freeze && spa.freeze) {
1239
- // We only need to rotate between pool and spa when they are both checked.
1240
- let pstate = circ.id === 6 ? cstate : state.circuits.getItemById(6);
1241
- let sstate = circ.id === 1 ? cstate : state.circuits.getItemById(1);
1242
- if (!pstate.isOn && !sstate.isOn) {
1243
- // Neither the pool or spa are on so we will turn on the pool first.
1244
- pstate.freezeProtect = true;
1245
- this.freezeProtectBodyOn = new Date();
1246
- await sys.board.circuits.setCircuitStateAsync(6, true);
1247
- }
1248
- else {
1249
- // If neither of the bodies were turned on for freeze protection then we need to ignore this.
1250
- if (!pstate.freezeProtect && !sstate.freezeProtect) {
1251
- this.freezeProtectBodyOn = undefined;
1252
- continue;
1253
- }
1254
-
1255
- // One of the two bodies is on so we need to check for the rotation. If it is time to rotate do the rotation.
1256
- if (typeof this.freezeProtectBodyOn === 'undefined') this.freezeProtectBodyOn = new Date();
1257
- let dt = new Date().getTime();
1258
- if (dt - 1000 * 60 * 15 > this.freezeProtectBodyOn.getTime()) {
1259
- logger.info(`Swapping bodies for freeze protection pool:${pstate.isOn} spa:${sstate.isOn} interval: ${utils.formatDuration(dt - this.freezeProtectBodyOn.getTime() / 1000)}`);
1260
- // 10 minutes has elapsed so we will be rotating to the other body.
1261
- if (pstate.isOn) {
1262
- // The setCircuitState method will handle turning off the pool body.
1263
- sstate.freezeProtect = true;
1264
- pstate.freezeProtect = false;
1265
- await sys.board.circuits.setCircuitStateAsync(1, true);
1266
- }
1267
- else {
1268
- sstate.freezeProtect = false;
1269
- pstate.freezeProtect = true;
1270
- await sys.board.circuits.setCircuitStateAsync(6, true);
1271
- }
1272
- // Set a new date as this will be our rotation check now.
1273
- this.freezeProtectBodyOn = new Date();
1274
- }
1275
- }
1276
- }
1277
- else {
1278
- // Only this circuit is selected for freeze protection so we don't need any special treatment.
1279
- cstate.freezeProtect = true;
1280
- if (!cstate.isOn) await sys.board.circuits.setCircuitStateAsync(circ.id, true);
1281
- }
1282
- bodyRotationChecked = true;
1283
- }
1284
- else if (freeze && !cstate.isOn) {
1285
- // This circuit should be on because we are freezing.
1286
- cstate.freezeProtect = true;
1287
- await sys.board.features.setFeatureStateAsync(circ.id, true);
1386
+ else {
1387
+ // If neither of the bodies were turned on for freeze protection then we need to ignore this.
1388
+ if (!pstate.freezeProtect && !sstate.freezeProtect) {
1389
+ this.freezeProtectBodyOn = undefined;
1390
+ continue;
1391
+ }
1392
+
1393
+ // One of the two bodies is on so we need to check for the rotation. If it is time to rotate do the rotation.
1394
+ if (typeof this.freezeProtectBodyOn === 'undefined') this.freezeProtectBodyOn = new Date();
1395
+ let dt = new Date().getTime();
1396
+ if (dt - 1000 * 60 * 15 > this.freezeProtectBodyOn.getTime()) {
1397
+ logger.info(`Swapping bodies for freeze protection pool:${pstate.isOn} spa:${sstate.isOn} interval: ${utils.formatDuration(dt - this.freezeProtectBodyOn.getTime() / 1000)}`);
1398
+ // 10 minutes has elapsed so we will be rotating to the other body.
1399
+ if (pstate.isOn) {
1400
+ // The setCircuitState method will handle turning off the pool body.
1401
+ sstate.freezeProtect = true;
1402
+ pstate.freezeProtect = false;
1403
+ await sys.board.circuits.setCircuitStateAsync(1, true);
1288
1404
  }
1289
- else if (!freeze && cstate.freezeProtect) {
1290
- // This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
1291
- await sys.board.circuits.setCircuitStateAsync(circ.id, false);
1292
- cstate.freezeProtect = false;
1405
+ else {
1406
+ sstate.freezeProtect = false;
1407
+ pstate.freezeProtect = true;
1408
+ await sys.board.circuits.setCircuitStateAsync(6, true);
1293
1409
  }
1410
+ // Set a new date as this will be our rotation check now.
1411
+ this.freezeProtectBodyOn = new Date();
1412
+ }
1294
1413
  }
1414
+ }
1415
+ else {
1416
+ // Only this circuit is selected for freeze protection so we don't need any special treatment.
1417
+ cstate.freezeProtect = true;
1418
+ if (!cstate.isOn) await sys.board.circuits.setCircuitStateAsync(circ.id, true);
1419
+ }
1420
+ bodyRotationChecked = true;
1421
+ }
1422
+ else if (freeze && !cstate.isOn) {
1423
+ // This circuit should be on because we are freezing.
1424
+ cstate.freezeProtect = true;
1425
+ await sys.board.features.setFeatureStateAsync(circ.id, true);
1295
1426
  }
1296
- catch (err) { logger.error(`syncFreezeProtection: Error synchronizing freeze protection states`); }
1427
+ else if (!freeze && cstate.freezeProtect) {
1428
+ // This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
1429
+ await sys.board.circuits.setCircuitStateAsync(circ.id, false);
1430
+ cstate.freezeProtect = false;
1431
+ }
1432
+ }
1297
1433
  }
1434
+ catch (err) { logger.error(`syncFreezeProtection: Error synchronizing freeze protection states`); }
1435
+ }
1298
1436
 
1299
1437
  public async initFilters() {
1300
1438
  try {
@@ -1302,7 +1440,7 @@ export class BodyCommands extends BoardCommands {
1302
1440
  let sFilter: FilterState;
1303
1441
  if (sys.equipment.maxBodies > 0) {
1304
1442
  filter = sys.filters.getItemById(1, true, { filterType: 3, name: sys.equipment.shared ? 'Filter' : 'Filter 1' });
1305
- sFilter = state.filters.getItemById(1, true, { id: 1, name: filter.name });
1443
+ sFilter = state.filters.getItemById(1, true, { id: 1, name: filter.name });
1306
1444
  filter.isActive = true;
1307
1445
  filter.master = sys.board.equipmentMaster;
1308
1446
  filter.body = sys.equipment.shared ? sys.board.valueMaps.bodies.transformByName('poolspa') : 0;
@@ -1432,53 +1570,54 @@ export class BodyCommands extends BoardCommands {
1432
1570
  sys.board.heaters.syncHeaterStates();
1433
1571
  return Promise.resolve(bstate);
1434
1572
  }
1435
- public getHeatSources(bodyId: number) {
1436
- let heatSources = [];
1437
- let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1438
- heatSources.push(this.board.valueMaps.heatSources.transformByName('nochange'));
1439
- if (heatTypes.total > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('off'));
1440
- if (heatTypes.gas > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('heater'));
1441
- if (heatTypes.solar > 0) {
1442
- let hm = this.board.valueMaps.heatSources.transformByName('solar');
1443
- heatSources.push(hm);
1444
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('solarpref'));
1445
- }
1446
- if (heatTypes.heatpump > 0) {
1447
- let hm = this.board.valueMaps.heatSources.transformByName('heatpump');
1448
- heatSources.push(hm);
1449
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('heatpumppref'));
1450
- }
1451
- if (heatTypes.ultratemp > 0) {
1452
- let hm = this.board.valueMaps.heatSources.transformByName('ultratemp');
1453
- heatSources.push(hm);
1454
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('ultratemppref'));
1455
- }
1456
- return heatSources;
1457
- }
1458
- public getHeatModes(bodyId: number) {
1459
- let heatModes = [];
1460
- // 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)
1461
- heatModes.push(this.board.valueMaps.heatModes.transformByName('off')); // In IC fw 1.047 off is no longer 0.
1462
- let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1463
- if (heatTypes.gas > 0)
1464
- heatModes.push(this.board.valueMaps.heatModes.transformByName('heater'));
1465
- if (heatTypes.solar > 0) {
1466
- let hm = this.board.valueMaps.heatModes.transformByName('solar');
1467
- heatModes.push(hm);
1468
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('solarpref'));
1469
- }
1470
- if (heatTypes.heatpump > 0) {
1471
- let hm = this.board.valueMaps.heatModes.transformByName('heatpump');
1472
- heatModes.push(hm);
1473
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('heatpumppref'));
1474
- }
1475
- if (heatTypes.ultratemp > 0) {
1476
- let hm = this.board.valueMaps.heatModes.transformByName('ultratemp');
1477
- heatModes.push(hm);
1478
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('ultratemppref'));
1479
- }
1480
- return heatModes;
1481
- }
1573
+ public getHeatSources(bodyId: number) {
1574
+ let heatSources = [];
1575
+ let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1576
+ heatSources.push(this.board.valueMaps.heatSources.transformByName('nochange'));
1577
+ if (heatTypes.total > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('off'));
1578
+ if (heatTypes.gas > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('heater'));
1579
+ if (heatTypes.mastertemp > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('mastertemp'));
1580
+ if (heatTypes.solar > 0) {
1581
+ let hm = this.board.valueMaps.heatSources.transformByName('solar');
1582
+ heatSources.push(hm);
1583
+ if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('solarpref'));
1584
+ }
1585
+ if (heatTypes.heatpump > 0) {
1586
+ let hm = this.board.valueMaps.heatSources.transformByName('heatpump');
1587
+ heatSources.push(hm);
1588
+ if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('heatpumppref'));
1589
+ }
1590
+ if (heatTypes.ultratemp > 0) {
1591
+ let hm = this.board.valueMaps.heatSources.transformByName('ultratemp');
1592
+ heatSources.push(hm);
1593
+ if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('ultratemppref'));
1594
+ }
1595
+ return heatSources;
1596
+ }
1597
+ public getHeatModes(bodyId: number) {
1598
+ let heatModes = [];
1599
+ // 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)
1600
+ heatModes.push(this.board.valueMaps.heatModes.transformByName('off')); // In IC fw 1.047 off is no longer 0.
1601
+ let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1602
+ if (heatTypes.gas > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('heater'));
1603
+ if (heatTypes.mastertemp > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('mastertemp'));
1604
+ if (heatTypes.solar > 0) {
1605
+ let hm = this.board.valueMaps.heatModes.transformByName('solar');
1606
+ heatModes.push(hm);
1607
+ if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('solarpref'));
1608
+ }
1609
+ if (heatTypes.heatpump > 0) {
1610
+ let hm = this.board.valueMaps.heatModes.transformByName('heatpump');
1611
+ heatModes.push(hm);
1612
+ if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('heatpumppref'));
1613
+ }
1614
+ if (heatTypes.ultratemp > 0) {
1615
+ let hm = this.board.valueMaps.heatModes.transformByName('ultratemp');
1616
+ heatModes.push(hm);
1617
+ if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('ultratemppref'));
1618
+ }
1619
+ return heatModes;
1620
+ }
1482
1621
  public getPoolStates(): BodyTempState[] {
1483
1622
  let arrPools = [];
1484
1623
  for (let i = 0; i < state.temps.bodies.length; i++) {
@@ -1546,6 +1685,56 @@ export class BodyCommands extends BoardCommands {
1546
1685
  }
1547
1686
  }
1548
1687
  export class PumpCommands extends BoardCommands {
1688
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
1689
+ try {
1690
+ // First delete the pumps that should be removed.
1691
+ for (let i = 0; i < ctx.pumps.remove.length; i++) {
1692
+ let p = ctx.pumps.remove[i];
1693
+ try {
1694
+ await sys.board.pumps.deletePumpAsync(p);
1695
+ res.addModuleSuccess('pump', `Remove: ${p.id}-${p.name}`);
1696
+ } catch (err) { res.addModuleError('pump', `Remove: ${p.id}-${p.name}: ${err.message}`); }
1697
+ }
1698
+ for (let i = 0; i < ctx.pumps.update.length; i++) {
1699
+ let p = ctx.pumps.update[i];
1700
+ try {
1701
+ await sys.board.pumps.setPumpAsync(p);
1702
+ res.addModuleSuccess('pump', `Update: ${p.id}-${p.name}`);
1703
+ } catch (err) { res.addModuleError('pump', `Update: ${p.id}-${p.name}: ${err.message}`); }
1704
+ }
1705
+ for (let i = 0; i < ctx.pumps.add.length; i++) {
1706
+ let p = ctx.pumps.add[i];
1707
+ try {
1708
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
1709
+ // it won't error out.
1710
+ sys.pumps.getItemById(p, true);
1711
+ await sys.board.pumps.setPumpAsync(p);
1712
+ res.addModuleSuccess('pump', `Add: ${p.id}-${p.name}`);
1713
+ } catch (err) { res.addModuleError('pump', `Add: ${p.id}-${p.name}: ${err.message}`); }
1714
+ }
1715
+ return true;
1716
+ } catch (err) { logger.error(`Error restoring pumps: ${err.message}`); res.addModuleError('system', `Error restoring pumps: ${err.message}`); return false; }
1717
+ }
1718
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
1719
+ try {
1720
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
1721
+ // Look at pumps.
1722
+ let cfg = rest.poolConfig;
1723
+ for (let i = 0; i < cfg.pumps.length; i++) {
1724
+ let r = cfg.pumps[i];
1725
+ let c = sys.pumps.find(elem => r.id === elem.id);
1726
+ if (typeof c === 'undefined') ctx.add.push(r);
1727
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
1728
+ }
1729
+ for (let i = 0; i < sys.pumps.length; i++) {
1730
+ let c = sys.pumps.getItemByIndex(i);
1731
+ let r = cfg.pumps.find(elem => elem.id == c.id);
1732
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
1733
+ }
1734
+ return ctx;
1735
+ } catch (err) { logger.error(`Error validating pumps for restore: ${err.message}`); }
1736
+ }
1737
+
1549
1738
  public getPumpTypes() { return this.board.valueMaps.pumpTypes.toArray(); }
1550
1739
  public getCircuitUnits(pump?: Pump) {
1551
1740
  if (typeof pump === 'undefined')
@@ -1610,14 +1799,15 @@ export class PumpCommands extends BoardCommands {
1610
1799
  // and props that aren't for this pump type
1611
1800
  let _id = pump.id;
1612
1801
  if (pump.type !== pumpType || pumpType === 0) {
1613
- const _isVirtual = sys.pumps.getItemById(_id).isVirtual;
1802
+ let _p = pump.get(true);
1803
+ // const _isVirtual = typeof _p.isVirtual !== 'undefined' ? _p.isVirtual : false;
1614
1804
  sys.pumps.removeItemById(_id);
1615
- let pump = sys.pumps.getItemById(_id, true);
1616
- if (_isVirtual) {
1805
+ pump = sys.pumps.getItemById(_id, true);
1806
+ /* if (_isVirtual) {
1617
1807
  // pump.isActive = true;
1618
1808
  // pump.isVirtual = true;
1619
1809
  pump.master = 1;
1620
- }
1810
+ } */
1621
1811
  state.pumps.removeItemById(pump.id);
1622
1812
  pump.type = pumpType;
1623
1813
  let type = sys.board.valueMaps.pumpTypes.transform(pumpType);
@@ -1669,174 +1859,328 @@ export class PumpCommands extends BoardCommands {
1669
1859
  }
1670
1860
  }
1671
1861
  export class CircuitCommands extends BoardCommands {
1672
- public async syncCircuitRelayStates() {
1862
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
1863
+ try {
1864
+ // First delete the circuit/lightGroups that should be removed.
1865
+ for (let i = 0; i < ctx.circuitGroups.remove.length; i++) {
1866
+ let c = ctx.circuitGroups.remove[i];
1673
1867
  try {
1674
- for (let i = 0; i < sys.circuits.length; i++) {
1675
- // Run through all the controlled circuits to see whether they should be triggered or not.
1676
- let circ = sys.circuits.getItemByIndex(i);
1677
- if (circ.master === 1 && circ.isActive) {
1678
- let cstate = state.circuits.getItemById(circ.id);
1679
- if (cstate.isOn) await ncp.circuits.setCircuitStateAsync(cstate, cstate.isOn);
1680
- }
1681
- }
1682
- } catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); }
1683
- }
1684
- public syncVirtualCircuitStates() {
1868
+ await sys.board.circuits.deleteCircuitGroupAsync(c);
1869
+ res.addModuleSuccess('circuitGroup', `Remove: ${c.id}-${c.name}`);
1870
+ } catch (err) { res.addModuleError('circuitGroup', `Remove: ${c.id}-${c.name}: ${err.message}`); }
1871
+ }
1872
+ for (let i = 0; i < ctx.lightGroups.remove.length; i++) {
1873
+ let c = ctx.lightGroups.remove[i];
1685
1874
  try {
1686
- let arrCircuits = sys.board.valueMaps.virtualCircuits.toArray();
1687
- let poolStates = sys.board.bodies.getPoolStates();
1688
- let spaStates = sys.board.bodies.getSpaStates();
1689
- // The following should work for all board types if the virtualCiruit valuemaps use common names. The circuit ids can be
1690
- // different as well as the descriptions but these should have common names since they are all derived from existing states.
1691
-
1692
- // This also removes virtual circuits depending on whether heaters exsits on the bodies. Not sure why we are doing this
1693
- // as the body data contains whether a body is heated or not. Perhapse some attached interface is using
1694
- // the virtual circuit list as a means to determine whether solar is available. That is totally flawed if that is the case.
1695
- for (let i = 0; i < arrCircuits.length; i++) {
1696
- let vc = arrCircuits[i];
1697
- let remove = false;
1698
- let bState = false;
1699
- let cstate: VirtualCircuitState = null;
1700
- switch (vc.name) {
1701
- case 'poolHeater':
1702
- // If any pool is heating up.
1703
- remove = true;
1704
- for (let j = 0; j < poolStates.length; j++) {
1705
- if (poolStates[j].heaterOptions.total > 0) remove = false;
1706
- }
1707
- if (!remove) {
1708
- // Determine whether the pool heater is on.
1709
- for (let j = 0; j < poolStates.length; j++)
1710
- if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'heater') bState = true;
1711
- }
1712
- break;
1713
- case 'spaHeater':
1714
- remove = true;
1715
- for (let j = 0; j < spaStates.length; j++) {
1716
- if (spaStates[j].heaterOptions.total > 0) remove = false;
1717
- }
1718
- if (!remove) {
1719
- // Determine whether the spa heater is on.
1720
- for (let j = 0; j < spaStates.length; j++) {
1721
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') bState = true;
1722
- }
1723
- }
1724
- break;
1725
- case 'freeze':
1726
- // If freeze protection has been turned on.
1727
- bState = state.freeze;
1728
- break;
1729
- case 'poolSpa':
1730
- // If any pool or spa is on
1731
- for (let j = 0; j < poolStates.length && !bState; j++) {
1732
- if (poolStates[j].isOn) bState = true;
1733
- }
1734
- for (let j = 0; j < spaStates.length && !bState; j++) {
1735
- if (spaStates[j].isOn) bState = true;
1736
- }
1737
- break;
1738
- case 'solarHeat':
1739
- case 'solar':
1740
- // If solar is on for any body
1741
- remove = true;
1742
- for (let j = 0; j < poolStates.length; j++) {
1743
- if (poolStates[j].heaterOptions.solar + poolStates[j].heaterOptions.heatpump > 0) remove = false;
1744
- }
1745
- if (remove) {
1746
- for (let j = 0; j < spaStates.length; j++) {
1747
- if (spaStates[j].heaterOptions.solar + spaStates[j].heaterOptions.heatpump > 0) remove = false;
1748
- }
1749
- }
1750
- if (!remove) {
1751
- for (let j = 0; j < poolStates.length && !bState; j++) {
1752
- if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') bState = true;
1753
- }
1754
- for (let j = 0; j < spaStates.length && !bState; j++) {
1755
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') bState = true;
1756
- }
1757
- }
1758
- break;
1759
- case 'heater':
1760
- remove = true;
1761
- for (let j = 0; j < poolStates.length; j++) {
1762
- if (poolStates[j].heaterOptions.total > 0) remove = false;
1763
- }
1764
- if (remove) {
1765
- for (let j = 0; j < spaStates.length; j++) {
1766
- if (spaStates[j].heaterOptions.total > 0) remove = false;
1767
- }
1768
- }
1769
- if (!remove) {
1770
- for (let j = 0; j < poolStates.length && !bState; j++) {
1771
- let heat = sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus);
1772
- if (heat !== 'off') bState = true;
1773
- }
1774
- for (let j = 0; j < spaStates.length && !bState; j++) {
1775
- let heat = sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus);
1776
- if (heat !== 'off') bState = true;
1777
- }
1778
- }
1779
- break;
1780
- default:
1781
- remove = true;
1782
- break;
1783
- }
1784
- if (remove)
1785
- state.virtualCircuits.removeItemById(vc.val);
1786
- else {
1787
- cstate = state.virtualCircuits.getItemById(vc.val, true);
1788
- if (cstate !== null) {
1789
- cstate.isOn = bState;
1790
- cstate.type = vc.val;
1791
- cstate.name = vc.desc;
1792
- }
1793
- }
1794
- }
1795
- } catch (err) { logger.error(`Error syncronizing virtual circuits`); }
1796
- }
1797
- public async setCircuitStateAsync(id: number, val: boolean): Promise<ICircuitState> {
1798
- sys.board.suspendStatus(true);
1875
+ await sys.board.circuits.deleteLightGroupAsync(c);
1876
+ res.addModuleSuccess('lightGroup', `Remove: ${c.id}-${c.name}`);
1877
+ } catch (err) { res.addModuleError('lightGroup', `Remove: ${c.id}-${c.name}: ${err.message}`); }
1878
+ }
1879
+ for (let i = 0; i < ctx.circuits.remove.length; i++) {
1880
+ let c = ctx.circuits.remove[i];
1799
1881
  try {
1800
- // We need to do some routing here as it is now critical that circuits, groups, and features
1801
- // have their own processing. The virtual controller used to only deal with one circuit.
1802
- if (sys.board.equipmentIds.circuitGroups.isInRange(id))
1803
- return await sys.board.circuits.setCircuitGroupStateAsync(id, val);
1804
- else if (sys.board.equipmentIds.features.isInRange(id))
1805
- return await sys.board.features.setFeatureStateAsync(id, val);
1806
- let circuit: ICircuit = sys.circuits.getInterfaceById(id, false, { isActive: false });
1807
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit or Feature id ${id} not valid`, id, 'Circuit'));
1808
- let circ = state.circuits.getInterfaceById(id, circuit.isActive !== false);
1809
- let newState = utils.makeBool(val);
1810
- // 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
1811
- // to turn off the other body first.
1812
- //[12, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
1813
- //[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }]
1814
- let func = sys.board.valueMaps.circuitFunctions.get(circuit.type);
1815
- if (newState && (func.name === 'pool' || func.name === 'spa') && sys.equipment.shared === true) {
1816
- // If we are shared we need to turn off the other circuit.
1817
- let offType = func.name === 'pool' ? sys.board.valueMaps.circuitFunctions.getValue('spa') : sys.board.valueMaps.circuitFunctions.getValue('pool');
1818
- let off = sys.circuits.get().filter(elem => elem.type === offType);
1819
- // Turn the circuits off that are part of the shared system. We are going back to the board
1820
- // just in case we got here for a circuit that isn't on the current defined panel.
1821
- for (let i = 0; i < off.length; i++) {
1822
- let coff = off[i];
1823
- await sys.board.circuits.setCircuitStateAsync(coff.id, false);
1824
- }
1882
+ await sys.board.circuits.deleteCircuitAsync(c);
1883
+ res.addModuleSuccess('circuit', `Remove: ${c.id}-${c.name}`);
1884
+ } catch (err) { res.addModuleError('circuit', `Remove: ${c.id}-${c.name}: ${err.message}`); }
1885
+ }
1886
+ for (let i = 0; i < ctx.circuits.add.length; i++) {
1887
+ let c = ctx.circuits.add[i];
1888
+ try {
1889
+ await sys.board.circuits.setCircuitAsync(c);
1890
+ res.addModuleSuccess('circuit', `Add: ${c.id}-${c.name}`);
1891
+ } catch (err) { res.addModuleError('circuit', `Add: ${c.id}-${c.name}: ${err.message}`); }
1892
+ }
1893
+ for (let i = 0; i < ctx.circuitGroups.add.length; i++) {
1894
+ let c = ctx.circuitGroups.add[i];
1895
+ try {
1896
+ await sys.board.circuits.setCircuitGroupAsync(c);
1897
+ res.addModuleSuccess('circuitGroup', `Add: ${c.id}-${c.name}`);
1898
+ } catch (err) { res.addModuleError('circuitGroup', `Add: ${c.id}-${c.name}: ${err.message}`); }
1899
+ }
1900
+ for (let i = 0; i < ctx.lightGroups.add.length; i++) {
1901
+ let c = ctx.lightGroups.add[i];
1902
+ try {
1903
+ await sys.board.circuits.setLightGroupAsync(c);
1904
+ res.addModuleSuccess('lightGroup', `Add: ${c.id}-${c.name}`);
1905
+ } catch (err) { res.addModuleError('lightGroup', `Add: ${c.id}-${c.name}: ${err.message}`); }
1906
+ }
1907
+ for (let i = 0; i < ctx.circuits.update.length; i++) {
1908
+ let c = ctx.circuits.update[i];
1909
+ try {
1910
+ await sys.board.circuits.setCircuitAsync(c);
1911
+ res.addModuleSuccess('circuit', `Update: ${c.id}-${c.name}`);
1912
+ } catch (err) { res.addModuleError('circuit', `Update: ${c.id}-${c.name}: ${err.message}`); }
1913
+ }
1914
+ for (let i = 0; i < ctx.circuitGroups.update.length; i++) {
1915
+ let c = ctx.circuitGroups.update[i];
1916
+ try {
1917
+ await sys.board.circuits.setCircuitGroupAsync(c);
1918
+ res.addModuleSuccess('circuitGroup', `Update: ${c.id}-${c.name}`);
1919
+ } catch (err) { res.addModuleError('circuitGroup', `Update: ${c.id}-${c.name}: ${err.message}`); }
1920
+ }
1921
+ for (let i = 0; i < ctx.lightGroups.add.length; i++) {
1922
+ let c = ctx.lightGroups.update[i];
1923
+ try {
1924
+ await sys.board.circuits.setLightGroupAsync(c);
1925
+ res.addModuleSuccess('lightGroup', `Update: ${c.id}-${c.name}`);
1926
+ } catch (err) { res.addModuleError('lightGroup', `Update: ${c.id}-${c.name}: ${err.message}`); }
1927
+ }
1928
+ return true;
1929
+ } catch (err) { logger.error(`Error restoring circuits: ${err.message}`); res.addModuleError('system', `Error restoring circuits/features: ${err.message}`); return false; }
1930
+ }
1931
+ public async validateRestore(rest: { poolConfig: any, poolState: any }, ctxRoot): Promise<boolean> {
1932
+ try {
1933
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
1934
+ // Look at circuits.
1935
+ let cfg = rest.poolConfig;
1936
+ for (let i = 0; i < cfg.circuits.length; i++) {
1937
+ let r = cfg.circuits[i];
1938
+ let c = sys.circuits.find(elem => r.id === elem.id);
1939
+ if (typeof c === 'undefined') ctx.add.push(r);
1940
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
1941
+ }
1942
+ for (let i = 0; i < sys.circuits.length; i++) {
1943
+ let c = sys.circuits.getItemByIndex(i);
1944
+ let r = cfg.circuits.find(elem => elem.id == c.id);
1945
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
1946
+ }
1947
+ ctxRoot.circuits = ctx;
1948
+ ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
1949
+ for (let i = 0; i < cfg.circuitGroups.length; i++) {
1950
+ let r = cfg.circuitGroups[i];
1951
+ let c = sys.circuitGroups.find(elem => r.id === elem.id);
1952
+ if (typeof c === 'undefined') ctx.add.push(r);
1953
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
1954
+ }
1955
+ for (let i = 0; i < sys.circuitGroups.length; i++) {
1956
+ let c = sys.circuitGroups.getItemByIndex(i);
1957
+ let r = cfg.circuitGroups.find(elem => elem.id == c.id);
1958
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
1959
+ }
1960
+ ctxRoot.circuitGroups = ctx;
1961
+ ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
1962
+ for (let i = 0; i < cfg.lightGroups.length; i++) {
1963
+ let r = cfg.lightGroups[i];
1964
+ let c = sys.lightGroups.find(elem => r.id === elem.id);
1965
+ if (typeof c === 'undefined') ctx.add.push(r);
1966
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
1967
+ }
1968
+ for (let i = 0; i < sys.lightGroups.length; i++) {
1969
+ let c = sys.lightGroups.getItemByIndex(i);
1970
+ let r = cfg.lightGroups.find(elem => elem.id == c.id);
1971
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
1972
+ }
1973
+ ctxRoot.lightGroups = ctx;
1974
+ return true;
1975
+ } catch (err) { logger.error(`Error validating circuits for restore: ${err.message}`); }
1976
+ }
1977
+ public async checkEggTimerExpirationAsync() {
1978
+ // turn off any circuits that have reached their egg timer;
1979
+ // Nixie circuits we have 100% control over;
1980
+ // but features/cg/lg may override OCP control
1981
+ try {
1982
+ for (let i = 0; i < sys.circuits.length; i++) {
1983
+ let c = sys.circuits.getItemByIndex(i);
1984
+ let cstate = state.circuits.getItemByIndex(i);
1985
+ if (!cstate.isActive || !cstate.isOn) continue;
1986
+ if (c.master === 1) {
1987
+ await ncp.circuits.checkCircuitEggTimerExpirationAsync(cstate);
1988
+ }
1989
+ }
1990
+ for (let i = 0; i < sys.features.length; i++) {
1991
+ let fstate = state.features.getItemByIndex(i);
1992
+ if (!fstate.isActive || !fstate.isOn) continue;
1993
+ if (fstate.endTime.toDate() < new Timestamp().toDate()) {
1994
+ await sys.board.circuits.setCircuitStateAsync(fstate.id, false);
1995
+ fstate.emitEquipmentChange();
1996
+ }
1997
+ }
1998
+ for (let i = 0; i < sys.circuitGroups.length; i++) {
1999
+ let cgstate = state.circuitGroups.getItemByIndex(i);
2000
+ if (!cgstate.isActive || !cgstate.isOn) continue;
2001
+ if (cgstate.endTime.toDate() < new Timestamp().toDate()) {
2002
+ await sys.board.circuits.setCircuitGroupStateAsync(cgstate.id, false);
2003
+ cgstate.emitEquipmentChange();
2004
+ }
2005
+ }
2006
+ for (let i = 0; i < sys.lightGroups.length; i++) {
2007
+ let lgstate = state.lightGroups.getItemByIndex(i);
2008
+ if (!lgstate.isActive || !lgstate.isOn) continue;
2009
+ if (lgstate.endTime.toDate() < new Timestamp().toDate()) {
2010
+ await sys.board.circuits.setLightGroupStateAsync(lgstate.id, false);
2011
+ lgstate.emitEquipmentChange();
2012
+ }
2013
+ }
2014
+ } catch (err) { logger.error(`checkEggTimerExpiration: Error synchronizing circuit relays ${err.message}`); }
2015
+ }
2016
+ public async syncCircuitRelayStates() {
2017
+ try {
2018
+ for (let i = 0; i < sys.circuits.length; i++) {
2019
+ // Run through all the controlled circuits to see whether they should be triggered or not.
2020
+ let circ = sys.circuits.getItemByIndex(i);
2021
+ if (circ.master === 1 && circ.isActive) {
2022
+ let cstate = state.circuits.getItemById(circ.id);
2023
+ if (cstate.isOn) await ncp.circuits.setCircuitStateAsync(cstate, cstate.isOn);
2024
+ }
2025
+ }
2026
+ } catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); }
2027
+ }
2028
+ public syncVirtualCircuitStates() {
2029
+ try {
2030
+ let arrCircuits = sys.board.valueMaps.virtualCircuits.toArray();
2031
+ let poolStates = sys.board.bodies.getPoolStates();
2032
+ let spaStates = sys.board.bodies.getSpaStates();
2033
+ // The following should work for all board types if the virtualCiruit valuemaps use common names. The circuit ids can be
2034
+ // different as well as the descriptions but these should have common names since they are all derived from existing states.
2035
+
2036
+ // This also removes virtual circuits depending on whether heaters exsits on the bodies. Not sure why we are doing this
2037
+ // as the body data contains whether a body is heated or not. Perhapse some attached interface is using
2038
+ // the virtual circuit list as a means to determine whether solar is available. That is totally flawed if that is the case.
2039
+ for (let i = 0; i < arrCircuits.length; i++) {
2040
+ let vc = arrCircuits[i];
2041
+ let remove = false;
2042
+ let bState = false;
2043
+ let cstate: VirtualCircuitState = null;
2044
+ switch (vc.name) {
2045
+ case 'poolHeater':
2046
+ // If any pool is heating up.
2047
+ remove = true;
2048
+ for (let j = 0; j < poolStates.length; j++) {
2049
+ if (poolStates[j].heaterOptions.total > 0) remove = false;
2050
+ }
2051
+ if (!remove) {
2052
+ // Determine whether the pool heater is on.
2053
+ for (let j = 0; j < poolStates.length; j++)
2054
+ if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'heater') bState = true;
2055
+ }
2056
+ break;
2057
+ case 'spaHeater':
2058
+ remove = true;
2059
+ for (let j = 0; j < spaStates.length; j++) {
2060
+ if (spaStates[j].heaterOptions.total > 0) remove = false;
2061
+ }
2062
+ if (!remove) {
2063
+ // Determine whether the spa heater is on.
2064
+ for (let j = 0; j < spaStates.length; j++) {
2065
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') bState = true;
2066
+ }
2067
+ }
2068
+ break;
2069
+ case 'freeze':
2070
+ // If freeze protection has been turned on.
2071
+ bState = state.freeze;
2072
+ break;
2073
+ case 'poolSpa':
2074
+ // If any pool or spa is on
2075
+ for (let j = 0; j < poolStates.length && !bState; j++) {
2076
+ if (poolStates[j].isOn) bState = true;
2077
+ }
2078
+ for (let j = 0; j < spaStates.length && !bState; j++) {
2079
+ if (spaStates[j].isOn) bState = true;
2080
+ }
2081
+ break;
2082
+ case 'solarHeat':
2083
+ case 'solar':
2084
+ // If solar is on for any body
2085
+ remove = true;
2086
+ for (let j = 0; j < poolStates.length; j++) {
2087
+ if (poolStates[j].heaterOptions.solar + poolStates[j].heaterOptions.heatpump > 0) remove = false;
2088
+ }
2089
+ if (remove) {
2090
+ for (let j = 0; j < spaStates.length; j++) {
2091
+ if (spaStates[j].heaterOptions.solar + spaStates[j].heaterOptions.heatpump > 0) remove = false;
2092
+ }
2093
+ }
2094
+ if (!remove) {
2095
+ for (let j = 0; j < poolStates.length && !bState; j++) {
2096
+ if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') bState = true;
2097
+ }
2098
+ for (let j = 0; j < spaStates.length && !bState; j++) {
2099
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') bState = true;
2100
+ }
2101
+ }
2102
+ break;
2103
+ case 'heater':
2104
+ remove = true;
2105
+ for (let j = 0; j < poolStates.length; j++) {
2106
+ if (poolStates[j].heaterOptions.total > 0) remove = false;
2107
+ }
2108
+ if (remove) {
2109
+ for (let j = 0; j < spaStates.length; j++) {
2110
+ if (spaStates[j].heaterOptions.total > 0) remove = false;
2111
+ }
1825
2112
  }
1826
- if (id === 6) state.temps.bodies.getItemById(1, true).isOn = val;
1827
- else if (id === 1) state.temps.bodies.getItemById(2, true).isOn = val;
1828
- // Let the main nixie controller set the circuit state and affect the relays if it needs to.
1829
- await ncp.circuits.setCircuitStateAsync(circ, newState);
1830
- await sys.board.syncEquipmentItems();
1831
- return state.circuits.getInterfaceById(circ.id);
2113
+ if (!remove) {
2114
+ for (let j = 0; j < poolStates.length && !bState; j++) {
2115
+ let heat = sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus);
2116
+ if (heat !== 'off') bState = true;
2117
+ }
2118
+ for (let j = 0; j < spaStates.length && !bState; j++) {
2119
+ let heat = sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus);
2120
+ if (heat !== 'off') bState = true;
2121
+ }
2122
+ }
2123
+ break;
2124
+ default:
2125
+ remove = true;
2126
+ break;
2127
+ }
2128
+ if (remove)
2129
+ state.virtualCircuits.removeItemById(vc.val);
2130
+ else {
2131
+ cstate = state.virtualCircuits.getItemById(vc.val, true);
2132
+ if (cstate !== null) {
2133
+ cstate.isOn = bState;
2134
+ cstate.type = vc.val;
2135
+ cstate.name = vc.desc;
2136
+ }
1832
2137
  }
1833
- catch (err) { return Promise.reject(`Nixie: Error setCircuitStateAsync ${err.message}`); }
1834
- finally {
1835
- ncp.pumps.syncPumpStates();
1836
- sys.board.suspendStatus(false);
1837
- state.emitEquipmentChanges();
2138
+ }
2139
+ } catch (err) { logger.error(`Error syncronizing virtual circuits`); }
2140
+ }
2141
+ public async setCircuitStateAsync(id: number, val: boolean): Promise<ICircuitState> {
2142
+ sys.board.suspendStatus(true);
2143
+ try {
2144
+ // We need to do some routing here as it is now critical that circuits, groups, and features
2145
+ // have their own processing. The virtual controller used to only deal with one circuit.
2146
+ if (sys.board.equipmentIds.circuitGroups.isInRange(id))
2147
+ return await sys.board.circuits.setCircuitGroupStateAsync(id, val);
2148
+ else if (sys.board.equipmentIds.features.isInRange(id))
2149
+ return await sys.board.features.setFeatureStateAsync(id, val);
2150
+ let circuit: ICircuit = sys.circuits.getInterfaceById(id, false, { isActive: false });
2151
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit or Feature id ${id} not valid`, id, 'Circuit'));
2152
+ let circ = state.circuits.getInterfaceById(id, circuit.isActive !== false);
2153
+ let newState = utils.makeBool(val);
2154
+ // 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
2155
+ // to turn off the other body first.
2156
+ //[12, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
2157
+ //[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }]
2158
+ let func = sys.board.valueMaps.circuitFunctions.get(circuit.type);
2159
+ if (newState && (func.name === 'pool' || func.name === 'spa') && sys.equipment.shared === true) {
2160
+ // If we are shared we need to turn off the other circuit.
2161
+ let offType = func.name === 'pool' ? sys.board.valueMaps.circuitFunctions.getValue('spa') : sys.board.valueMaps.circuitFunctions.getValue('pool');
2162
+ let off = sys.circuits.get().filter(elem => elem.type === offType);
2163
+ // Turn the circuits off that are part of the shared system. We are going back to the board
2164
+ // just in case we got here for a circuit that isn't on the current defined panel.
2165
+ for (let i = 0; i < off.length; i++) {
2166
+ let coff = off[i];
2167
+ await sys.board.circuits.setCircuitStateAsync(coff.id, false);
1838
2168
  }
2169
+ }
2170
+ if (id === 6) state.temps.bodies.getItemById(1, true).isOn = val;
2171
+ else if (id === 1) state.temps.bodies.getItemById(2, true).isOn = val;
2172
+ // Let the main nixie controller set the circuit state and affect the relays if it needs to.
2173
+ await ncp.circuits.setCircuitStateAsync(circ, newState);
2174
+ await sys.board.syncEquipmentItems();
2175
+ return state.circuits.getInterfaceById(circ.id);
2176
+ }
2177
+ catch (err) { return Promise.reject(`Nixie: Error setCircuitStateAsync ${err.message}`); }
2178
+ finally {
2179
+ ncp.pumps.syncPumpStates();
2180
+ sys.board.suspendStatus(false);
2181
+ state.emitEquipmentChanges();
1839
2182
  }
2183
+ }
1840
2184
  public async toggleCircuitStateAsync(id: number): Promise<ICircuitState> {
1841
2185
  let circ = state.circuits.getInterfaceById(id);
1842
2186
  return await this.setCircuitStateAsync(id, !(circ.isOn || false));
@@ -1908,58 +2252,58 @@ export class CircuitCommands extends BoardCommands {
1908
2252
  public getLightThemes(type?: number) { return sys.board.valueMaps.lightThemes.toArray(); }
1909
2253
  public getCircuitFunctions() { return sys.board.valueMaps.circuitFunctions.toArray(); }
1910
2254
  public getCircuitNames() { return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()]; }
1911
- public async setCircuitAsync(data: any): Promise<ICircuit> {
1912
- try {
1913
- let id = parseInt(data.id, 10);
1914
- if (id <= 0 || typeof data.id === 'undefined') {
1915
- // We are adding a new circuit. If we are operating as a nixie controller then we need to start this
1916
- // circuit outside the range of circuits that can be defined on the panel. For any of the non-OCP controllers
1917
- // these are added within the range of the circuits starting with 1. For all others these are added with an id > 255.
1918
- switch (state.equipment.controllerType) {
1919
- case 'intellicenter':
1920
- case 'intellitouch':
1921
- case 'easytouch':
1922
- id = sys.circuits.getNextEquipmentId(new EquipmentIdRange(255, 300));
1923
- break;
1924
- default:
1925
- id = sys.circuits.getNextEquipmentId(sys.board.equipmentIds.circuits, [1, 6]);
1926
- break;
1927
- }
1928
- }
1929
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.id}`, data.id, 'Circuit'));
1930
- //if (!sys.board.equipmentIds.circuits.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit id is out of range: ${id}`, data.id, 'Circuit'));;
1931
- if (typeof data.id !== 'undefined') {
1932
- let circuit = sys.circuits.getItemById(id, true);
1933
- let scircuit = state.circuits.getItemById(id, true);
1934
- scircuit.isActive = circuit.isActive = true;
1935
- circuit.master = 1;
1936
- scircuit.isOn = false;
1937
- if (data.name) circuit.name = scircuit.name = data.name;
1938
- else if (!circuit.name && !data.name) circuit.name = scircuit.name = `circuit${data.id}`;
1939
- if (typeof data.type !== 'undefined' || typeof circuit.type === 'undefined') {
1940
- circuit.type = scircuit.type = parseInt(data.type, 10) || 0;
1941
- }
1942
- if (id === 6) circuit.type = sys.board.valueMaps.circuitFunctions.getValue('pool');
1943
- if (id === 1 && sys.equipment.shared) circuit.type = sys.board.valueMaps.circuitFunctions.getValue('spa');
1944
- if (typeof data.freeze !== 'undefined' || typeof circuit.freeze === 'undefined') circuit.freeze = utils.makeBool(data.freeze) || false;
1945
- if (typeof data.showInFeatures !== 'undefined' || typeof data.showInFeatures === 'undefined') circuit.showInFeatures = scircuit.showInFeatures = utils.makeBool(data.showInFeatures) || true;
1946
- if (typeof data.dontStop !== 'undefined' && utils.makeBool(data.dontStop) === true) data.eggTimer = 1440;
1947
- if (typeof data.eggTimer !== 'undefined' || typeof circuit.eggTimer === 'undefined') circuit.eggTimer = parseInt(data.eggTimer, 10) || 0;
1948
- if (typeof data.connectionId !== 'undefined') circuit.connectionId = data.connectionId;
1949
- if (typeof data.deviceBinding !== 'undefined') circuit.deviceBinding = data.deviceBinding;
1950
- if (typeof data.showInFeatures !== 'undefined') scircuit.showInFeatures = circuit.showInFeatures = utils.makeBool(data.showInFeatures);
1951
- circuit.dontStop = circuit.eggTimer === 1440;
1952
-
1953
- sys.emitEquipmentChange();
1954
- state.emitEquipmentChanges();
1955
- if (circuit.master === 1) await ncp.circuits.setCircuitAsync(circuit, data);
1956
- return Promise.resolve(circuit);
1957
- }
1958
- else
1959
- return Promise.reject(new Error('Circuit id has not been defined'));
2255
+ public async setCircuitAsync(data: any): Promise<ICircuit> {
2256
+ try {
2257
+ let id = parseInt(data.id, 10);
2258
+ if (id <= 0 || typeof data.id === 'undefined') {
2259
+ // We are adding a new circuit. If we are operating as a nixie controller then we need to start this
2260
+ // circuit outside the range of circuits that can be defined on the panel. For any of the non-OCP controllers
2261
+ // these are added within the range of the circuits starting with 1. For all others these are added with an id > 255.
2262
+ switch (state.equipment.controllerType) {
2263
+ case 'intellicenter':
2264
+ case 'intellitouch':
2265
+ case 'easytouch':
2266
+ id = sys.circuits.getNextEquipmentId(new EquipmentIdRange(255, 300));
2267
+ break;
2268
+ default:
2269
+ id = sys.circuits.getNextEquipmentId(sys.board.equipmentIds.circuits, [1, 6]);
2270
+ break;
2271
+ }
2272
+ }
2273
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.id}`, data.id, 'Circuit'));
2274
+ //if (!sys.board.equipmentIds.circuits.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit id is out of range: ${id}`, data.id, 'Circuit'));;
2275
+ if (typeof data.id !== 'undefined') {
2276
+ let circuit = sys.circuits.getItemById(id, true);
2277
+ let scircuit = state.circuits.getItemById(id, true);
2278
+ scircuit.isActive = circuit.isActive = true;
2279
+ circuit.master = 1;
2280
+ scircuit.isOn = false;
2281
+ if (data.name) circuit.name = scircuit.name = data.name;
2282
+ else if (!circuit.name && !data.name) circuit.name = scircuit.name = `circuit${data.id}`;
2283
+ if (typeof data.type !== 'undefined' || typeof circuit.type === 'undefined') {
2284
+ circuit.type = scircuit.type = parseInt(data.type, 10) || 0;
1960
2285
  }
1961
- catch (err) { logger.error(`setCircuitAsync error with ${data}. ${err}`); return Promise.reject(err); }
2286
+ if (id === 6) circuit.type = sys.board.valueMaps.circuitFunctions.getValue('pool');
2287
+ if (id === 1 && sys.equipment.shared) circuit.type = sys.board.valueMaps.circuitFunctions.getValue('spa');
2288
+ if (typeof data.freeze !== 'undefined' || typeof circuit.freeze === 'undefined') circuit.freeze = utils.makeBool(data.freeze) || false;
2289
+ if (typeof data.showInFeatures !== 'undefined' || typeof data.showInFeatures === 'undefined') circuit.showInFeatures = scircuit.showInFeatures = utils.makeBool(data.showInFeatures) || true;
2290
+ if (typeof data.dontStop !== 'undefined' && utils.makeBool(data.dontStop) === true) data.eggTimer = 1440;
2291
+ if (typeof data.eggTimer !== 'undefined' || typeof circuit.eggTimer === 'undefined') circuit.eggTimer = parseInt(data.eggTimer, 10) || 0;
2292
+ if (typeof data.connectionId !== 'undefined') circuit.connectionId = data.connectionId;
2293
+ if (typeof data.deviceBinding !== 'undefined') circuit.deviceBinding = data.deviceBinding;
2294
+ if (typeof data.showInFeatures !== 'undefined') scircuit.showInFeatures = circuit.showInFeatures = utils.makeBool(data.showInFeatures);
2295
+ circuit.dontStop = circuit.eggTimer === 1440;
2296
+
2297
+ sys.emitEquipmentChange();
2298
+ state.emitEquipmentChanges();
2299
+ if (circuit.master === 1) await ncp.circuits.setCircuitAsync(circuit, data);
2300
+ return Promise.resolve(circuit);
2301
+ }
2302
+ else
2303
+ return Promise.reject(new Error('Circuit id has not been defined'));
1962
2304
  }
2305
+ catch (err) { logger.error(`setCircuitAsync error with ${data}. ${err}`); return Promise.reject(err); }
2306
+ }
1963
2307
  public async setCircuitGroupAsync(obj: any): Promise<CircuitGroup> {
1964
2308
  let group: CircuitGroup = null;
1965
2309
  let sgroup: CircuitGroupState = null;
@@ -2032,8 +2376,9 @@ export class CircuitCommands extends BoardCommands {
2032
2376
  if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit light group id exceeded`, id, 'LightGroup'));
2033
2377
  if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'LightGroup'));
2034
2378
  group = sys.lightGroups.getItemById(id, true);
2379
+ let sgroup = state.lightGroups.getItemById(id, true);
2035
2380
  return new Promise<LightGroup>((resolve, reject) => {
2036
- if (typeof obj.name !== 'undefined') group.name = obj.name;
2381
+ if (typeof obj.name !== 'undefined') sgroup.name = group.name = obj.name;
2037
2382
  if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440;
2038
2383
  if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440);
2039
2384
  group.dontStop = group.eggTimer === 1440;
@@ -2288,6 +2633,56 @@ export class CircuitCommands extends BoardCommands {
2288
2633
  }
2289
2634
  }
2290
2635
  export class FeatureCommands extends BoardCommands {
2636
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
2637
+ try {
2638
+ // First delete the features that should be removed.
2639
+ for (let i = 0; i < ctx.features.remove.length; i++) {
2640
+ let f = ctx.features.remove[i];
2641
+ try {
2642
+ await sys.board.features.deleteFeatureAsync(f);
2643
+ res.addModuleSuccess('feature', `Remove: ${f.id}-${f.name}`);
2644
+ } catch (err) { res.addModuleError('feature', `Remove: ${f.id}-${f.name}: ${err.message}`) }
2645
+ }
2646
+ for (let i = 0; i < ctx.features.update.length; i++) {
2647
+ let f = ctx.features.update[i];
2648
+ try {
2649
+ await sys.board.features.setFeatureAsync(f);
2650
+ res.addModuleSuccess('feature', `Update: ${f.id}-${f.name}`);
2651
+ } catch (err) { res.addModuleError('feature', `Update: ${f.id}-${f.name}: ${err.message}`); }
2652
+ }
2653
+ for (let i = 0; i < ctx.features.add.length; i++) {
2654
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
2655
+ // it won't error out.
2656
+ let f = ctx.features.add[i];
2657
+ try {
2658
+ sys.features.getItemById(f, true);
2659
+ await sys.board.features.setFeatureAsync(f);
2660
+ res.addModuleSuccess('feature', `Add: ${f.id}-${f.name}`);
2661
+ } catch (err) { res.addModuleError('feature', `Add: ${f.id}-${f.name}: ${err.message}`) }
2662
+ }
2663
+ return true;
2664
+ } catch (err) { logger.error(`Error restoring features: ${err.message}`); res.addModuleError('system', `Error restoring features: ${err.message}`); return false; }
2665
+ }
2666
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
2667
+ try {
2668
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
2669
+ // Look at features.
2670
+ let cfg = rest.poolConfig;
2671
+ for (let i = 0; i < cfg.features.length; i++) {
2672
+ let r = cfg.features[i];
2673
+ let c = sys.features.find(elem => r.id === elem.id);
2674
+ if (typeof c === 'undefined') ctx.add.push(r);
2675
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
2676
+ }
2677
+ for (let i = 0; i < sys.features.length; i++) {
2678
+ let c = sys.features.getItemByIndex(i);
2679
+ let r = cfg.features.find(elem => elem.id == c.id);
2680
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
2681
+ }
2682
+ return ctx;
2683
+ } catch (err) { logger.error(`Error validating features for restore: ${err.message}`); }
2684
+ }
2685
+
2291
2686
  public async setFeatureAsync(obj: any): Promise<Feature> {
2292
2687
  let id = parseInt(obj.id, 10);
2293
2688
  if (id <= 0 || isNaN(id)) {
@@ -2403,6 +2798,57 @@ export class FeatureCommands extends BoardCommands {
2403
2798
  }
2404
2799
  }
2405
2800
  export class ChlorinatorCommands extends BoardCommands {
2801
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
2802
+ try {
2803
+ // First delete the chlorinators that should be removed.
2804
+ for (let i = 0; i < ctx.chlorinators.remove.length; i++) {
2805
+ let c = ctx.chlorinators.remove[i];
2806
+ try {
2807
+ await sys.board.chlorinator.deleteChlorAsync(c);
2808
+ res.addModuleSuccess('chlorinator', `Remove: ${c.id}-${c.name}`);
2809
+ } catch (err) { res.addModuleError('chlorinator', `Remove: ${c.id}-${c.name}: ${err.message}`); }
2810
+ }
2811
+ for (let i = 0; i < ctx.chlorinators.update.length; i++) {
2812
+ let c = ctx.chlorinators.update[i];
2813
+ try {
2814
+ await sys.board.chlorinator.setChlorAsync(c);
2815
+ res.addModuleSuccess('chlorinator', `Update: ${c.id}-${c.name}`);
2816
+ } catch (err) { res.addModuleError('chlorinator', `Update: ${c.id}-${c.name}: ${err.message}`); }
2817
+ }
2818
+ for (let i = 0; i < ctx.chlorinators.add.length; i++) {
2819
+ let c = ctx.chlorinators.add[i];
2820
+ try {
2821
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
2822
+ // it won't error out.
2823
+ sys.chlorinators.getItemById(c.id, true);
2824
+ await sys.board.chlorinator.setChlorAsync(c);
2825
+ res.addModuleSuccess('chlorinator', `Add: ${c.id}-${c.name}`);
2826
+ } catch (err) { res.addModuleError('chlorinator', `Add: ${c.id}-${c.name}: ${err.message}`); }
2827
+ }
2828
+ return true;
2829
+ } catch (err) { logger.error(`Error restoring chlorinators: ${err.message}`); res.addModuleError('system', `Error restoring chlorinators: ${err.message}`); return false; }
2830
+ }
2831
+
2832
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
2833
+ try {
2834
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
2835
+ // Look at chlorinators.
2836
+ let cfg = rest.poolConfig;
2837
+ for (let i = 0; i < cfg.chlorinators.length; i++) {
2838
+ let r = cfg.chlorinators[i];
2839
+ let c = sys.chlorinators.find(elem => r.id === elem.id);
2840
+ if (typeof c === 'undefined') ctx.add.push(r);
2841
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
2842
+ }
2843
+ for (let i = 0; i < sys.chlorinators.length; i++) {
2844
+ let c = sys.chlorinators.getItemByIndex(i);
2845
+ let r = cfg.chlorinators.find(elem => elem.id == c.id);
2846
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
2847
+ }
2848
+ return ctx;
2849
+ } catch (err) { logger.error(`Error validating chlorinators for restore: ${err.message}`); }
2850
+ }
2851
+
2406
2852
  public async setChlorAsync(obj: any): Promise<ChlorinatorState> {
2407
2853
  try {
2408
2854
  let id = parseInt(obj.id, 10);
@@ -2421,7 +2867,7 @@ export class ChlorinatorCommands extends BoardCommands {
2421
2867
  public async deleteChlorAsync(obj: any): Promise<ChlorinatorState> {
2422
2868
  try {
2423
2869
  let id = parseInt(obj.id, 10);
2424
- if (isNaN(id)) obj.id = 1;
2870
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator id is not valid: ${obj.id}`, 'chlorinator', obj.id));
2425
2871
  let chlor = state.chlorinators.getItemById(id);
2426
2872
  chlor.isActive = false;
2427
2873
  await ncp.chlorinators.deleteChlorinatorAsync(id);
@@ -2445,6 +2891,56 @@ export class ChlorinatorCommands extends BoardCommands {
2445
2891
  }
2446
2892
  }
2447
2893
  export class ScheduleCommands extends BoardCommands {
2894
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
2895
+ try {
2896
+ // First delete the schedules that should be removed.
2897
+ for (let i = 0; i < ctx.schedules.remove.length; i++) {
2898
+ let s = ctx.schedules.remove[i];
2899
+ try {
2900
+ await sys.board.schedules.deleteScheduleAsync(ctx.schedules.remove[i]);
2901
+ res.addModuleSuccess('schedule', `Remove: ${s.id}-${s.circuitId}`);
2902
+ } catch (err) { res.addModuleError('schedule', `Remove: ${s.id}-${s.circuitId} ${err.message}`); }
2903
+ }
2904
+ for (let i = 0; i < ctx.schedules.update.length; i++) {
2905
+ let s = ctx.schedules.update[i];
2906
+ try {
2907
+ await sys.board.schedules.setScheduleAsync(s);
2908
+ res.addModuleSuccess('schedule', `Update: ${s.id}-${s.circuitId}`);
2909
+ } catch (err) { res.addModuleError('schedule', `Update: ${s.id}-${s.circuitId} ${err.message}`); }
2910
+ }
2911
+ for (let i = 0; i < ctx.schedules.add.length; i++) {
2912
+ let s = ctx.schedules.add[i];
2913
+ try {
2914
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
2915
+ // it won't error out.
2916
+ sys.schedules.getItemById(s.id, true);
2917
+ await sys.board.schedules.setScheduleAsync(s);
2918
+ res.addModuleSuccess('schedule', `Add: ${s.id}-${s.circuitId}`);
2919
+ } catch (err) { res.addModuleError('schedule', `Add: ${s.id}-${s.circuitId} ${err.message}`); }
2920
+ }
2921
+ return true;
2922
+ } catch (err) { logger.error(`Error restoring schedules: ${err.message}`); res.addModuleError('system', `Error restoring schedules: ${err.message}`); return false; }
2923
+ }
2924
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
2925
+ try {
2926
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
2927
+ // Look at schedules.
2928
+ let cfg = rest.poolConfig;
2929
+ for (let i = 0; i < cfg.schedules.length; i++) {
2930
+ let r = cfg.schedules[i];
2931
+ let c = sys.schedules.find(elem => r.id === elem.id);
2932
+ if (typeof c === 'undefined') ctx.add.push(r);
2933
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
2934
+ }
2935
+ for (let i = 0; i < sys.schedules.length; i++) {
2936
+ let c = sys.schedules.getItemByIndex(i);
2937
+ let r = cfg.schedules.find(elem => elem.id == c.id);
2938
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
2939
+ }
2940
+ return ctx;
2941
+ } catch (err) { logger.error(`Error validating schedules for restore: ${err.message}`); }
2942
+ }
2943
+
2448
2944
  public transformDays(val: any): number {
2449
2945
  if (typeof val === 'number') return val;
2450
2946
  let edays = sys.board.valueMaps.scheduleDays.toArray();
@@ -2565,6 +3061,9 @@ export class ScheduleCommands extends BoardCommands {
2565
3061
  return Promise.reject(new InvalidEquipmentDataError(`Invalid circuit reference: ${circuit}`, 'Schedule', circuit));
2566
3062
  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));
2567
3063
 
3064
+ // If we made it to here we are valid and the schedula and it state should exist.
3065
+ sched = sys.schedules.getItemById(id, true);
3066
+ ssched = state.schedules.getItemById(id, true);
2568
3067
  sched.circuit = ssched.circuit = circuit;
2569
3068
  sched.scheduleDays = ssched.scheduleDays = schedDays;
2570
3069
  sched.scheduleType = ssched.scheduleType = schedType;
@@ -2676,6 +3175,56 @@ export class ScheduleCommands extends BoardCommands {
2676
3175
  }
2677
3176
  }
2678
3177
  export class HeaterCommands extends BoardCommands {
3178
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
3179
+ try {
3180
+ // First delete the heaters that should be removed.
3181
+ for (let i = 0; i < ctx.heaters.remove.length; i++) {
3182
+ let h = ctx.heaters.remove[i];
3183
+ try {
3184
+ await sys.board.heaters.deleteHeaterAsync(h);
3185
+ res.addModuleSuccess('heater', `Remove: ${h.id}-${h.name}`);
3186
+ } catch (err) { res.addModuleError('heater', `Remove: ${h.id}-${h.name}: ${err.message}`); }
3187
+ }
3188
+ for (let i = 0; i < ctx.heaters.update.length; i++) {
3189
+ let h = ctx.heaters.update[i];
3190
+ try {
3191
+ await sys.board.heaters.setHeaterAsync(h);
3192
+ res.addModuleSuccess('heater', `Update: ${h.id}-${h.name}`);
3193
+ } catch (err) { res.addModuleError('heater', `Update: ${h.id}-${h.name}: ${err.message}`); }
3194
+ }
3195
+ for (let i = 0; i < ctx.heaters.add.length; i++) {
3196
+ let h = ctx.heaters.add[i];
3197
+ try {
3198
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
3199
+ // it won't error out.
3200
+ sys.heaters.getItemById(h, true);
3201
+ await sys.board.heaters.setHeaterAsync(h);
3202
+ res.addModuleSuccess('heater', `Add: ${h.id}-${h.name}`);
3203
+ } catch (err) { res.addModuleError('heater', `Add: ${h.id}-${h.name}: ${err.message}`); }
3204
+ }
3205
+ return true;
3206
+ } catch (err) { logger.error(`Error restoring heaters: ${err.message}`); res.addModuleError('system', `Error restoring heaters: ${err.message}`); return false; }
3207
+ }
3208
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
3209
+ try {
3210
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
3211
+ // Look at heaters.
3212
+ let cfg = rest.poolConfig;
3213
+ for (let i = 0; i < cfg.heaters.length; i++) {
3214
+ let r = cfg.heaters[i];
3215
+ let c = sys.heaters.find(elem => r.id === elem.id);
3216
+ if (typeof c === 'undefined') ctx.add.push(r);
3217
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
3218
+ }
3219
+ for (let i = 0; i < sys.heaters.length; i++) {
3220
+ let c = sys.heaters.getItemByIndex(i);
3221
+ let r = cfg.heaters.find(elem => elem.id == c.id);
3222
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
3223
+ }
3224
+ return ctx;
3225
+ } catch (err) { logger.error(`Error validating heaters for restore: ${err.message}`); }
3226
+ }
3227
+
2679
3228
  public getInstalledHeaterTypes(body?: number): any {
2680
3229
  let heaters = sys.heaters.get();
2681
3230
  let types = sys.board.valueMaps.heaterTypes.toArray();
@@ -2740,7 +3289,7 @@ export class HeaterCommands extends BoardCommands {
2740
3289
  let heater: Heater;
2741
3290
  if (id <= 0) {
2742
3291
  // We are adding a heater. In this case all heaters are virtual.
2743
- let vheaters = sys.heaters.filter(h => h.isVirtual === true);
3292
+ let vheaters = sys.heaters.filter(h => h.master === 1);
2744
3293
  id = vheaters.length + 256;
2745
3294
  }
2746
3295
  heater = sys.heaters.getItemById(id, true);
@@ -2959,7 +3508,7 @@ export class HeaterCommands extends BoardCommands {
2959
3508
  let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
2960
3509
  let status = sys.board.valueMaps.heatStatus.transform(body.heatStatus);
2961
3510
  let hstate = state.heaters.getItemById(heater.id, true);
2962
- if (heater.isVirtual === true || heater.master === 1) {
3511
+ if (heater.master === 1) {
2963
3512
  // We need to do our own calculation as to whether it is on. This is for Nixie heaters.
2964
3513
  let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
2965
3514
  switch (htype.name) {
@@ -3001,6 +3550,16 @@ export class HeaterCommands extends BoardCommands {
3001
3550
  }
3002
3551
  }
3003
3552
  break;
3553
+ case 'mastertemp':
3554
+ if (mode === 'mtheater') {
3555
+ if (body.temp < cfgBody.setPoint) {
3556
+ isOn = true;
3557
+ body.heatStatus = sys.board.valueMaps.heaterTypes.getValue('mtheater');
3558
+ isHeating = true;
3559
+ }
3560
+ }
3561
+ break;
3562
+ case 'maxetherm':
3004
3563
  case 'gas':
3005
3564
  if (mode === 'heater') {
3006
3565
  if (body.temp < cfgBody.setPoint) {
@@ -3040,6 +3599,8 @@ export class HeaterCommands extends BoardCommands {
3040
3599
  let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
3041
3600
  switch (htype.name) {
3042
3601
  case 'mastertemp':
3602
+ if (status === 'mtheater') isHeating = isOn = true;
3603
+ break;
3043
3604
  case 'maxetherm':
3044
3605
  case 'gas':
3045
3606
  if (status === 'heater') isHeating = isOn = true;
@@ -3073,7 +3634,7 @@ export class HeaterCommands extends BoardCommands {
3073
3634
  }
3074
3635
  }
3075
3636
  // When the controller is a virtual one we need to control the heat status ourselves.
3076
- if (!isHeating && (sys.controllerType === ControllerType.Virtual || sys.controllerType === ControllerType.Nixie)) body.heatStatus = 0;
3637
+ if (!isHeating && (sys.controllerType === ControllerType.Nixie)) body.heatStatus = 0;
3077
3638
  }
3078
3639
  // Turn off any heaters that should be off. The code above only turns heaters on.
3079
3640
  for (let i = 0; i < heaters.length; i++) {
@@ -3092,6 +3653,57 @@ export class HeaterCommands extends BoardCommands {
3092
3653
  }
3093
3654
  }
3094
3655
  export class ValveCommands extends BoardCommands {
3656
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
3657
+ try {
3658
+ // First delete the valves that should be removed.
3659
+ for (let i = 0; i < ctx.valves.remove.length; i++) {
3660
+ let v = ctx.valves.remove[i];
3661
+ try {
3662
+ await sys.board.valves.deleteValveAsync(v);
3663
+ res.addModuleSuccess('valve', `Remove: ${v.id}-${v.name}`);
3664
+ } catch (err) { res.addModuleError('valve', `Remove: ${v.id}-${v.name}: ${err.message}`); }
3665
+ }
3666
+ for (let i = 0; i < ctx.valves.update.length; i++) {
3667
+ let v = ctx.valves.update[i];
3668
+ try {
3669
+ await sys.board.valves.setValveAsync(v);
3670
+ res.addModuleSuccess('valve', `Update: ${v.id}-${v.name}`);
3671
+ } catch (err) { res.addModuleError('valve', `Update: ${v.id}-${v.name}: ${err.message}`); }
3672
+ }
3673
+ for (let i = 0; i < ctx.valves.add.length; i++) {
3674
+ let v = ctx.valves.add[i];
3675
+ try {
3676
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
3677
+ // it won't error out.
3678
+ sys.valves.getItemById(ctx.valves.add[i].id, true);
3679
+ await sys.board.valves.setValveAsync(v);
3680
+ res.addModuleSuccess('valve', `Add: ${v.id}-${v.name}`);
3681
+ } catch (err) { res.addModuleError('valve', `Add: ${v.id}-${v.name}: ${err.message}`); }
3682
+ }
3683
+ return true;
3684
+ } catch (err) { logger.error(`Error restoring valves: ${err.message}`); res.addModuleError('system', `Error restoring valves: ${err.message}`); return false; }
3685
+ }
3686
+
3687
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
3688
+ try {
3689
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
3690
+ // Look at valves.
3691
+ let cfg = rest.poolConfig;
3692
+ for (let i = 0; i < cfg.valves.length; i++) {
3693
+ let r = cfg.valves[i];
3694
+ let c = sys.valves.find(elem => r.id === elem.id);
3695
+ if (typeof c === 'undefined') ctx.add.push(r);
3696
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
3697
+ }
3698
+ for (let i = 0; i < sys.valves.length; i++) {
3699
+ let c = sys.valves.getItemByIndex(i);
3700
+ let r = cfg.valves.find(elem => elem.id == c.id);
3701
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
3702
+ }
3703
+ return ctx;
3704
+ } catch (err) { logger.error(`Error validating valves for restore: ${err.message}`); }
3705
+ }
3706
+
3095
3707
  public async setValveStateAsync(valve: Valve, vstate: ValveState, isDiverted: boolean) {
3096
3708
  if (valve.master === 1) await ncp.valves.setValveStateAsync(vstate, isDiverted);
3097
3709
  else
@@ -3169,6 +3781,60 @@ export class ValveCommands extends BoardCommands {
3169
3781
  }
3170
3782
  }
3171
3783
  export class ChemControllerCommands extends BoardCommands {
3784
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
3785
+ try {
3786
+ // First delete the chemControllers that should be removed.
3787
+ for (let i = 0; i < ctx.chemControllers.remove.length; i++) {
3788
+ let c = ctx.chemControllers.remove[i];
3789
+ try {
3790
+ await sys.board.chemControllers.deleteChemControllerAsync(c);
3791
+ res.addModuleSuccess('chemController', `Remove: ${c.id}-${c.name}`);
3792
+ } catch (err) { res.addModuleError('chemController', `Remove: ${c.id}-${c.name}: ${err.message}`); }
3793
+ }
3794
+ for (let i = 0; i < ctx.chemControllers.update.length; i++) {
3795
+ let c = ctx.chemControllers.update[i];
3796
+ try {
3797
+ await sys.board.chemControllers.setChemControllerAsync(c);
3798
+ res.addModuleSuccess('chemController', `Update: ${c.id}-${c.name}`);
3799
+ } catch (err) { res.addModuleError('chemController', `Update: ${c.id}-${c.name}: ${err.message}`); }
3800
+ }
3801
+ for (let i = 0; i < ctx.chemControllers.add.length; i++) {
3802
+ let c = ctx.chemControllers.add[i];
3803
+ try {
3804
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
3805
+ // it won't error out.
3806
+ let chem = sys.chemControllers.getItemById(c.id, true);
3807
+ // RSG 11.24.21. setChemControllerAsync will only set the type/address if it thinks it's new.
3808
+ // For a restore, if we set the type/address here it will pass the validation steps.
3809
+ chem.type = c.type;
3810
+ // chem.address = c.address;
3811
+ await sys.board.chemControllers.setChemControllerAsync(c);
3812
+ res.addModuleSuccess('chemController', `Add: ${c.id}-${c.name}`);
3813
+ } catch (err) { res.addModuleError('chemController', `Add: ${c.id}-${c.name}: ${err.message}`); }
3814
+ }
3815
+ return true;
3816
+ } catch (err) { logger.error(`Error restoring chemControllers: ${err.message}`); res.addModuleError('system', `Error restoring chemControllers: ${err.message}`); return false; }
3817
+ }
3818
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
3819
+ try {
3820
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
3821
+ // Look at chemControllers.
3822
+ let cfg = rest.poolConfig;
3823
+ for (let i = 0; i < cfg.chemControllers.length; i++) {
3824
+ let r = cfg.chemControllers[i];
3825
+ let c = sys.chemControllers.find(elem => r.id === elem.id);
3826
+ if (typeof c === 'undefined') ctx.add.push(r);
3827
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
3828
+ }
3829
+ for (let i = 0; i < sys.chemControllers.length; i++) {
3830
+ let c = sys.chemControllers.getItemByIndex(i);
3831
+ let r = cfg.chemControllers.find(elem => elem.id == c.id);
3832
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
3833
+ }
3834
+ return ctx;
3835
+ } catch (err) { logger.error(`Error validating chemControllers for restore: ${err.message}`); }
3836
+ }
3837
+
3172
3838
  public async deleteChemControllerAsync(data: any): Promise<ChemController> {
3173
3839
  try {
3174
3840
  let id = typeof data.id !== 'undefined' ? parseInt(data.id, 10) : -1;
@@ -3307,133 +3973,197 @@ export class ChemControllerCommands extends BoardCommands {
3307
3973
  }
3308
3974
  }
3309
3975
  export class FilterCommands extends BoardCommands {
3310
- public async syncFilterStates() {
3976
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
3977
+ try {
3978
+ // First delete the filters that should be removed.
3979
+ for (let i = 0; i < ctx.filters.remove.length; i++) {
3980
+ let filter = ctx.filters.remove[i];
3311
3981
  try {
3312
- for (let i = 0; i < sys.filters.length; i++) {
3313
- // Run through all the valves to see whether they should be triggered or not.
3314
- let filter = sys.filters.getItemByIndex(i);
3315
- if (filter.isActive && !isNaN(filter.id)) {
3316
- let fstate = state.filters.getItemById(filter.id, true);
3317
- // Check to see if the associated body is on.
3318
- await sys.board.filters.setFilterStateAsync(filter, fstate, sys.board.bodies.isBodyOn(filter.body));
3319
- }
3320
- }
3321
- } catch (err) { logger.error(`syncFilterStates: Error synchronizing filters ${err.message}`); }
3322
- }
3323
- public async setFilterPressure(id: number, pressure: number, units?: string) {
3982
+ sys.filters.removeItemById(filter.id);
3983
+ state.filters.removeItemById(filter.id);
3984
+ res.addModuleSuccess('filter', `Remove: ${filter.id}-${filter.name}`);
3985
+ } catch (err) { res.addModuleError('filter', `Remove: ${filter.id}-${filter.name}: ${err.message}`); }
3986
+ }
3987
+ for (let i = 0; i < ctx.filters.update.length; i++) {
3988
+ let filter = ctx.filters.update[i];
3324
3989
  try {
3325
- let filter = sys.filters.find(elem => elem.id === id);
3326
- if (typeof filter === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`setFilterPressure: Invalid equipmentId ${id}`, id, 'Filter'));
3327
- if (isNaN(pressure)) return Promise.reject(new InvalidEquipmentDataError(`setFilterPressure: Invalid filter pressure ${pressure} for ${filter.name}`, 'Filter', pressure));
3328
- let sfilter = state.filters.getItemById(filter.id, true);
3329
- // Convert the pressure to the units that we have set on the filter for the pressure units.
3330
- let pu = sys.board.valueMaps.pressureUnits.transform(filter.pressureUnits || 0);
3331
- if (typeof units === 'undefined' || units === '') units = pu.name;
3332
- sfilter.pressureUnits = filter.pressureUnits;
3333
- sfilter.pressure = Math.round(pressure * 1000) / 1000; // Round this to 3 decimal places just in case we are getting stupid scales.
3334
- // 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.
3335
- // Rules for the circuit.
3336
- // 1. The assigned circuit must be on.
3337
- // 2. There must not be a current freeze condition
3338
- // 3. No heaters can be on.
3339
- // 4. The assigned circuit must be on exclusively but we will be ignoring any of the light circuit types for the exclusivity.
3340
- let cstate = state.circuits.getInterfaceById(filter.pressureCircuitId);
3341
- if (cstate.isOn && state.freeze !== true) {
3342
- // 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
3343
- // 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.
3344
- let hon = state.temps.bodies.toArray().find(elem => elem.isOn && (elem.heatStatus || 0) !== 0);
3345
- if (typeof hon === 'undefined') {
3346
- // Put together the circuit types that could be lights. We don't want these.
3347
- let ctypes = [];
3348
- let funcs = sys.board.valueMaps.circuitFunctions.toArray();
3349
- for (let i = 0; i < funcs.length; i++) {
3350
- let f = funcs[i];
3351
- if (f.isLight) ctypes.push(f.val);
3352
- }
3353
- let con = state.circuits.find(elem => elem.isOn === true && elem.id !== filter.pressureCircuitId && elem.id !== 1 && elem.id !== 6 && !ctypes.includes(elem.type));
3354
- if (typeof con === 'undefined') {
3355
- // 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
3356
- // it definitely has to be off.
3357
- let feats = state.features.toArray();
3358
- let fon = false;
3359
- for (let i = 0; i < feats.length && fon === false; i++) {
3360
- let f = feats[i];
3361
- if (!f.isOn) continue;
3362
- if (f.id === filter.pressureCircuitId) continue;
3363
- if (f.type != 0) fon = true;
3364
- // Check to see if this feature is used on a valve. This will make it
3365
- // us not include this either. We do not care whether the valve is diverted or not.
3366
- if (typeof state.valves.find(elem => elem.circuitId === f.id) !== 'undefined') fon = true;
3367
- }
3368
- if (!fon) {
3369
- // Finally we have a value we can believe in.
3370
- sfilter.refPressure = pressure;
3371
- }
3372
- }
3373
- else {
3374
- logger.verbose(`Circuit ${con.id}-${con.name} is currently on filter pressure for cleaning ignored.`);
3990
+ await sys.board.filters.setFilterAsync(filter);
3991
+ res.addModuleSuccess('filter', `Update: ${filter.id}-${filter.name}`);
3992
+ } catch (err) { res.addModuleError('filter', `Update: ${filter.id}-${filter.name}: ${err.message}`); }
3993
+ }
3994
+ for (let i = 0; i < ctx.filters.add.length; i++) {
3995
+ let filter = ctx.filters.add[i];
3996
+ try {
3997
+ // pull a little trick to first add the data then perform the update.
3998
+ sys.filters.getItemById(filter.id, true);
3999
+ await sys.board.filters.setFilterAsync(filter);
4000
+ res.addModuleSuccess('filter', `Add: ${filter.id}-${filter.name}`);
4001
+ } catch (err) { res.addModuleError('filter', `Add: ${filter.id}-${filter.name}: ${err.message}`); }
4002
+ }
4003
+ return true;
4004
+ } catch (err) { logger.error(`Error restoring filters: ${err.message}`); res.addModuleError('system', `Error restoring filters: ${err.message}`); return false; }
4005
+ }
4006
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
4007
+ try {
4008
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
4009
+ // Look at filters.
4010
+ let cfg = rest.poolConfig;
4011
+ for (let i = 0; i < cfg.filters.length; i++) {
4012
+ let r = cfg.filters[i];
4013
+ let c = sys.filters.find(elem => r.id === elem.id);
4014
+ if (typeof c === 'undefined') ctx.add.push(r);
4015
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
4016
+ }
4017
+ for (let i = 0; i < sys.filters.length; i++) {
4018
+ let c = sys.filters.getItemByIndex(i);
4019
+ let r = cfg.filters.find(elem => elem.id == c.id);
4020
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
4021
+ }
4022
+ return ctx;
4023
+ } catch (err) { logger.error(`Error validating filters for restore: ${err.message}`); }
4024
+ }
4025
+
4026
+ public async syncFilterStates() {
4027
+ try {
4028
+ for (let i = 0; i < sys.filters.length; i++) {
4029
+ // Run through all the valves to see whether they should be triggered or not.
4030
+ let filter = sys.filters.getItemByIndex(i);
4031
+ if (filter.isActive && !isNaN(filter.id)) {
4032
+ let fstate = state.filters.getItemById(filter.id, true);
4033
+ // Check to see if the associated body is on.
4034
+ await sys.board.filters.setFilterStateAsync(filter, fstate, sys.board.bodies.isBodyOn(filter.body));
4035
+ }
4036
+ }
4037
+ } catch (err) { logger.error(`syncFilterStates: Error synchronizing filters ${err.message}`); }
4038
+ }
4039
+ public async setFilterPressure(id: number, pressure: number, units?: string) {
4040
+ try {
4041
+ let filter = sys.filters.find(elem => elem.id === id);
4042
+ if (typeof filter === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`setFilterPressure: Invalid equipmentId ${id}`, id, 'Filter'));
4043
+ if (isNaN(pressure)) return Promise.reject(new InvalidEquipmentDataError(`setFilterPressure: Invalid filter pressure ${pressure} for ${filter.name}`, 'Filter', pressure));
4044
+ let sfilter = state.filters.getItemById(filter.id, true);
4045
+ // Convert the pressure to the units that we have set on the filter for the pressure units.
4046
+ let pu = sys.board.valueMaps.pressureUnits.transform(filter.pressureUnits || 0);
4047
+ if (typeof units === 'undefined' || units === '') units = pu.name;
4048
+ sfilter.pressureUnits = filter.pressureUnits;
4049
+ sfilter.pressure = Math.round(pressure * 1000) / 1000; // Round this to 3 decimal places just in case we are getting stupid scales.
4050
+ // 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.
4051
+ // Rules for the circuit.
4052
+ // 1. The assigned circuit must be on.
4053
+ // 2. There must not be a current freeze condition
4054
+ // 3. No heaters can be on.
4055
+ // 4. The assigned circuit must be on exclusively but we will be ignoring any of the light circuit types for the exclusivity.
4056
+ let cstate = state.circuits.getInterfaceById(filter.pressureCircuitId);
4057
+ if (cstate.isOn && state.freeze !== true) {
4058
+ // 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
4059
+ // 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.
4060
+ let hon = state.temps.bodies.toArray().find(elem => elem.isOn && (elem.heatStatus || 0) !== 0);
4061
+ if (typeof hon === 'undefined') {
4062
+ // Put together the circuit types that could be lights. We don't want these.
4063
+ let ctypes = [];
4064
+ let funcs = sys.board.valueMaps.circuitFunctions.toArray();
4065
+ for (let i = 0; i < funcs.length; i++) {
4066
+ let f = funcs[i];
4067
+ if (f.isLight) ctypes.push(f.val);
4068
+ }
4069
+ let con = state.circuits.find(elem => elem.isOn === true && elem.id !== filter.pressureCircuitId && elem.id !== 1 && elem.id !== 6 && !ctypes.includes(elem.type));
4070
+ if (typeof con === 'undefined') {
4071
+ // 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
4072
+ // it definitely has to be off.
4073
+ let feats = state.features.toArray();
4074
+ let fon = false;
4075
+ for (let i = 0; i < feats.length && fon === false; i++) {
4076
+ let f = feats[i];
4077
+ if (!f.isOn) continue;
4078
+ if (f.id === filter.pressureCircuitId) continue;
4079
+ if (f.type !== 0) fon = true;
4080
+ // Check to see if this feature is used on a valve. This will make it
4081
+ // not include this pressure either. We do not care whether the valve is diverted or not.
4082
+ if (typeof sys.valves.find(elem => elem.circuitId === f.id) !== 'undefined')
4083
+ fon = true;
4084
+ else {
4085
+ // Finally if the feature happens to be used on a pump then we don't want it either.
4086
+ let pumps = sys.pumps.get();
4087
+ for (let j = 0; j < pumps.length; j++) {
4088
+ let pmp = pumps[j];
4089
+ if (typeof pmp.circuits !== 'undefined') {
4090
+ if (typeof pmp.circuits.find(elem => elem.circuit === f.id) !== 'undefined') {
4091
+ fon = true;
4092
+ break;
3375
4093
  }
4094
+ }
3376
4095
  }
3377
- else {
3378
- logger.verbose(`Heater for body ${hon.name} is currently on ${hon.heatStatus} filter pressure for cleaning skipped.`);
3379
- }
4096
+ }
4097
+ }
4098
+ if (!fon) {
4099
+ // Finally we have a value we can believe in.
4100
+ sfilter.refPressure = pressure;
3380
4101
  }
3381
- sfilter.emitEquipmentChange();
4102
+ }
4103
+ else {
4104
+ logger.verbose(`Circuit ${con.id}-${con.name} is currently on filter pressure for cleaning ignored.`);
4105
+ }
3382
4106
  }
3383
- catch (err) { logger.error(`setFilterPressure: Error setting filter #${id} pressure to ${pressure}${units || ''}`); }
3384
- }
3385
- public async setFilterStateAsync(filter: Filter, fstate: FilterState, isOn: boolean) { fstate.isOn = isOn; }
3386
- public async setFilterAsync(data: any): Promise<Filter> {
3387
- let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
3388
- if (id <= 0) id = sys.filters.length + 1; // set max filters?
3389
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid filter id: ${data.id}`, data.id, 'Filter'));
3390
- let filter = sys.filters.getItemById(id, id > 0);
3391
- let sfilter = state.filters.getItemById(id, id > 0);
3392
- let filterType = typeof data.filterType !== 'undefined' ? parseInt(data.filterType, 10) : filter.filterType;
3393
- if (typeof filterType === 'undefined') filterType = sys.board.valueMaps.filterTypes.getValue('unknown');
3394
-
3395
- // The only way to delete a filter is to call deleteFilterAsync.
3396
- //if (typeof data.isActive !== 'undefined') {
3397
- // if (utils.makeBool(data.isActive) === false) {
3398
- // sys.filters.removeItemById(id);
3399
- // state.filters.removeItemById(id);
3400
- // return;
3401
- // }
3402
- //}
3403
-
3404
- let body = typeof data.body !== 'undefined' ? data.body : filter.body;
3405
- let name = typeof data.name !== 'undefined' ? data.name : filter.name;
3406
- if (typeof body === 'undefined') body = 32;
3407
- // At this point we should have all the data. Validate it.
3408
- if (!sys.board.valueMaps.filterTypes.valExists(filterType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid filter type; ${filterType}`, 'Filter', filterType));
3409
-
3410
- filter.pressureUnits = typeof data.pressureUnits !== 'undefined' ? data.pressureUnits || 0 : filter.pressureUnits || 0;
3411
- filter.pressureCircuitId = parseInt(data.pressureCicuitId || filter.pressureCircuitId || 6, 10);
3412
- filter.cleanPressure = parseFloat(data.cleanPressure || filter.cleanPressure || 0);
3413
- filter.dirtyPressure = parseFloat(data.dirtyPressure || filter.dirtyPressure || 0);
3414
-
3415
- filter.filterType = sfilter.filterType = filterType;
3416
- filter.body = sfilter.body = body;
3417
- filter.name = sfilter.name = name;
3418
- filter.capacity = typeof data.capacity === 'number' ? data.capacity : filter.capacity;
3419
- filter.capacityUnits = typeof data.capacityUnits !== 'undefined' ? data.capacityUnits : filter.capacity;
3420
- filter.connectionId = typeof data.connectionId !== 'undefined' ? data.connectionId : filter.connectionId;
3421
- filter.deviceBinding = typeof data.deviceBinding !== 'undefined' ? data.deviceBinding : filter.deviceBinding;
3422
- sfilter.pressureUnits = filter.pressureUnits;
3423
- sfilter.calcCleanPercentage();
3424
- sfilter.emitEquipmentChange();
3425
- return filter; // Always return the config when we are dealing with the config not state.
3426
- }
3427
- public async deleteFilterAsync(data: any): Promise<Filter> {
3428
- try {
3429
- let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
3430
- let filter = sys.filters.getItemById(id);
3431
- let sfilter = state.filters.getItemById(filter.id);
3432
- filter.isActive = false;
3433
- sys.filters.removeItemById(id);
3434
- state.filters.removeItemById(id);
3435
- sfilter.emitEquipmentChange();
3436
- return filter;
3437
- } catch (err) { logger.error(`deleteFilterAsync: Error deleting filter ${err.message}`); }
4107
+ else {
4108
+ logger.verbose(`Heater for body ${hon.name} is currently on ${hon.heatStatus} filter pressure for cleaning skipped.`);
4109
+ }
4110
+ }
4111
+ sfilter.emitEquipmentChange();
3438
4112
  }
4113
+ catch (err) { logger.error(`setFilterPressure: Error setting filter #${id} pressure to ${pressure}${units || ''}`); }
4114
+ }
4115
+ public async setFilterStateAsync(filter: Filter, fstate: FilterState, isOn: boolean) { fstate.isOn = isOn; }
4116
+ public async setFilterAsync(data: any): Promise<Filter> {
4117
+ let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
4118
+ if (id <= 0) id = sys.filters.length + 1; // set max filters?
4119
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid filter id: ${data.id}`, data.id, 'Filter'));
4120
+ let filter = sys.filters.getItemById(id, id > 0);
4121
+ let sfilter = state.filters.getItemById(id, id > 0);
4122
+ let filterType = typeof data.filterType !== 'undefined' ? parseInt(data.filterType, 10) : filter.filterType;
4123
+ if (typeof filterType === 'undefined') filterType = sys.board.valueMaps.filterTypes.getValue('unknown');
4124
+
4125
+ // The only way to delete a filter is to call deleteFilterAsync.
4126
+ //if (typeof data.isActive !== 'undefined') {
4127
+ // if (utils.makeBool(data.isActive) === false) {
4128
+ // sys.filters.removeItemById(id);
4129
+ // state.filters.removeItemById(id);
4130
+ // return;
4131
+ // }
4132
+ //}
4133
+
4134
+ let body = typeof data.body !== 'undefined' ? data.body : filter.body;
4135
+ let name = typeof data.name !== 'undefined' ? data.name : filter.name;
4136
+ if (typeof body === 'undefined') body = 32;
4137
+ // At this point we should have all the data. Validate it.
4138
+ if (!sys.board.valueMaps.filterTypes.valExists(filterType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid filter type; ${filterType}`, 'Filter', filterType));
4139
+
4140
+ filter.pressureUnits = typeof data.pressureUnits !== 'undefined' ? data.pressureUnits || 0 : filter.pressureUnits || 0;
4141
+ filter.pressureCircuitId = parseInt(data.pressureCircuitId || filter.pressureCircuitId || 6, 10);
4142
+ filter.cleanPressure = parseFloat(data.cleanPressure || filter.cleanPressure || 0);
4143
+ filter.dirtyPressure = parseFloat(data.dirtyPressure || filter.dirtyPressure || 0);
4144
+
4145
+ filter.filterType = sfilter.filterType = filterType;
4146
+ filter.body = sfilter.body = body;
4147
+ filter.name = sfilter.name = name;
4148
+ filter.capacity = typeof data.capacity === 'number' ? data.capacity : filter.capacity;
4149
+ filter.capacityUnits = typeof data.capacityUnits !== 'undefined' ? data.capacityUnits : filter.capacity;
4150
+ filter.connectionId = typeof data.connectionId !== 'undefined' ? data.connectionId : filter.connectionId;
4151
+ filter.deviceBinding = typeof data.deviceBinding !== 'undefined' ? data.deviceBinding : filter.deviceBinding;
4152
+ sfilter.pressureUnits = filter.pressureUnits;
4153
+ sfilter.calcCleanPercentage();
4154
+ sfilter.emitEquipmentChange();
4155
+ return filter; // Always return the config when we are dealing with the config not state.
4156
+ }
4157
+ public async deleteFilterAsync(data: any): Promise<Filter> {
4158
+ try {
4159
+ let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
4160
+ let filter = sys.filters.getItemById(id);
4161
+ let sfilter = state.filters.getItemById(filter.id);
4162
+ filter.isActive = false;
4163
+ sys.filters.removeItemById(id);
4164
+ state.filters.removeItemById(id);
4165
+ sfilter.emitEquipmentChange();
4166
+ return filter;
4167
+ } catch (err) { logger.error(`deleteFilterAsync: Error deleting filter ${err.message}`); }
4168
+ }
3439
4169
  }