nodejs-poolcontroller 7.2.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 (64) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/Changelog +13 -0
  3. package/Dockerfile +1 -0
  4. package/README.md +5 -5
  5. package/app.ts +11 -0
  6. package/config/Config.ts +3 -0
  7. package/config/VersionCheck.ts +8 -4
  8. package/controller/Constants.ts +165 -9
  9. package/controller/Equipment.ts +186 -65
  10. package/controller/Errors.ts +22 -1
  11. package/controller/State.ts +273 -57
  12. package/controller/boards/EasyTouchBoard.ts +194 -95
  13. package/controller/boards/IntelliCenterBoard.ts +115 -42
  14. package/controller/boards/IntelliTouchBoard.ts +104 -30
  15. package/controller/boards/NixieBoard.ts +155 -53
  16. package/controller/boards/SystemBoard.ts +1529 -514
  17. package/controller/comms/Comms.ts +219 -42
  18. package/controller/comms/messages/Messages.ts +16 -4
  19. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -3
  20. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  21. package/controller/comms/messages/config/CircuitMessage.ts +1 -1
  22. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  23. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  24. package/controller/comms/messages/config/ExternalMessage.ts +43 -25
  25. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  26. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  27. package/controller/comms/messages/config/HeaterMessage.ts +15 -9
  28. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  29. package/controller/comms/messages/config/OptionsMessage.ts +13 -1
  30. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  31. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  32. package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
  33. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  34. package/controller/comms/messages/config/ValveMessage.ts +12 -2
  35. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +14 -6
  36. package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
  37. package/controller/comms/messages/status/HeaterStateMessage.ts +25 -5
  38. package/controller/comms/messages/status/IntelliChemStateMessage.ts +55 -26
  39. package/controller/nixie/Nixie.ts +18 -16
  40. package/controller/nixie/NixieEquipment.ts +6 -6
  41. package/controller/nixie/bodies/Body.ts +7 -4
  42. package/controller/nixie/bodies/Filter.ts +7 -4
  43. package/controller/nixie/chemistry/ChemController.ts +800 -283
  44. package/controller/nixie/chemistry/Chlorinator.ts +22 -14
  45. package/controller/nixie/circuits/Circuit.ts +42 -7
  46. package/controller/nixie/heaters/Heater.ts +303 -30
  47. package/controller/nixie/pumps/Pump.ts +57 -30
  48. package/controller/nixie/schedules/Schedule.ts +10 -7
  49. package/controller/nixie/valves/Valve.ts +7 -5
  50. package/defaultConfig.json +32 -1
  51. package/issue_template.md +1 -1
  52. package/logger/DataLogger.ts +37 -22
  53. package/package.json +20 -18
  54. package/web/Server.ts +529 -31
  55. package/web/bindings/influxDB.json +157 -5
  56. package/web/bindings/mqtt.json +112 -13
  57. package/web/bindings/mqttAlt.json +109 -11
  58. package/web/interfaces/baseInterface.ts +2 -1
  59. package/web/interfaces/httpInterface.ts +2 -0
  60. package/web/interfaces/influxInterface.ts +103 -54
  61. package/web/interfaces/mqttInterface.ts +16 -5
  62. package/web/services/config/Config.ts +179 -43
  63. package/web/services/state/State.ts +51 -5
  64. package/web/services/state/StateSocket.ts +19 -2
@@ -16,14 +16,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
16
16
  */
17
17
  import * as extend from 'extend';
18
18
  import { logger } from '../../logger/Logger';
19
- import { webApp } from '../../web/Server';
20
- import { conn } from '../comms/Comms';
21
- import { ncp } from "../nixie/Nixie"
22
- import { Message, Outbound, Protocol, Response } from '../comms/messages/Messages';
23
- import { utils, Heliotrope, Timestamp } from '../Constants';
24
- import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, CustomName, CustomNameCollection, EggTimer, Feature, General, Heater, ICircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, Valve, ControllerType, TempSensorCollection, Filter, Equipment } from '../Equipment';
25
- import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ParameterOutOfRangeError } from '../Errors';
26
- import { BodyTempState, ValveState, ChemControllerState, ChlorinatorState, ICircuitGroupState, ICircuitState, LightGroupState, PumpState, state, TemperatureState, VirtualCircuitState, HeaterState, ScheduleState, FilterState, ChemicalState, CircuitGroupState, CircuitState } from '../State';
19
+ import { Message, Outbound } from '../comms/messages/Messages';
20
+ import { Timestamp, utils } from '../Constants';
21
+ import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, ControllerType, CustomName, CustomNameCollection, EggTimer, Equipment, Feature, Filter, General, Heater, ICircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, TempSensorCollection, Valve } from '../Equipment';
22
+ import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError } from '../Errors';
23
+ import { ncp } from "../nixie/Nixie";
24
+ import { BodyTempState, ChemControllerState, ChlorinatorState, CircuitGroupState, FilterState, ICircuitGroupState, ICircuitState, LightGroupState, ScheduleState, state, TemperatureState, ValveState, VirtualCircuitState } from '../State';
25
+ import { RestoreResults } from '../../web/Server';
26
+
27
27
 
28
28
  export class byteValueMap extends Map<number, any> {
29
29
  public transform(byte: number, ext?: number) { return extend(true, { val: byte || 0 }, this.get(byte) || this.get(0)); }
@@ -218,10 +218,10 @@ export class byteValueMaps {
218
218
  ]);
219
219
  public panelModes: byteValueMap = new byteValueMap([
220
220
  [0, { val: 0, name: 'auto', desc: 'Auto' }],
221
- [1, { val: 1, name: 'service', desc: 'Service' }],
222
- [8, { val: 8, name: 'freeze', desc: 'Freeze' }],
223
- [128, { val: 128, name: 'timeout', desc: 'Timeout' }],
224
- [129, { val: 129, name: 'service-timeout', desc: 'Service/Timeout' }],
221
+ // [1, { val: 1, name: 'service', desc: 'Service' }],
222
+ // [8, { val: 8, name: 'freeze', desc: 'Freeze' }],
223
+ // [128, { val: 128, name: 'timeout', desc: 'Timeout' }],
224
+ // [129, { val: 129, name: 'service-timeout', desc: 'Service/Timeout' }],
225
225
  [255, { name: 'error', desc: 'System Error' }]
226
226
  ]);
227
227
  public controllerStatus: byteValueMap = new byteValueMap([
@@ -246,7 +246,9 @@ export class byteValueMaps {
246
246
  [15, { name: 'floorcleaner', desc: 'Floor Cleaner' }],
247
247
  [16, { name: 'intellibrite', desc: 'Intellibrite', isLight: true }],
248
248
  [17, { name: 'magicstream', desc: 'Magicstream', isLight: true }],
249
- [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' }]
250
252
  ]);
251
253
 
252
254
  // Feature functions are used as the available options to define a circuit.
@@ -289,6 +291,26 @@ export class byteValueMaps {
289
291
  [254, { name: 'unknown', desc: 'unknown' }],
290
292
  [255, { name: 'none', desc: 'None' }]
291
293
  ]);
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
+ ]);
313
+
292
314
  public lightColors: byteValueMap = new byteValueMap([
293
315
  [0, { name: 'white', desc: 'White' }],
294
316
  [2, { name: 'lightgreen', desc: 'Light Green' }],
@@ -416,7 +438,9 @@ export class byteValueMaps {
416
438
  ]);
417
439
  public bodyTypes: byteValueMap = new byteValueMap([
418
440
  [0, { name: 'pool', desc: 'Pool' }],
419
- [1, { name: 'spa', desc: 'Spa' }]
441
+ [1, { name: 'spa', desc: 'Spa' }],
442
+ [2, { name: 'spa', desc: 'Spa' }],
443
+ [3, { name: 'spa', desc: 'Spa' }]
420
444
  ]);
421
445
  public bodies: byteValueMap = new byteValueMap([
422
446
  [0, { name: 'pool', desc: 'Pool' }],
@@ -442,6 +466,19 @@ export class byteValueMaps {
442
466
  [2, { name: 'aquarite', desc: 'Aquarite' }],
443
467
  [3, { name: 'unknown', desc: 'unknown' }]
444
468
  ]);
469
+ public chlorinatorModel: byteValueMap = new byteValueMap([
470
+ [0, { name: 'unknown', desc: 'unknown', capacity: 0, chlorinePerDay: 0, chlorinePerSec: 0 }],
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 }]
481
+ ])
445
482
  public customNames: byteValueMap = new byteValueMap();
446
483
  public circuitNames: byteValueMap = new byteValueMap();
447
484
  public scheduleTypes: byteValueMap = new byteValueMap([
@@ -458,6 +495,10 @@ export class byteValueMaps {
458
495
  [0, { name: 'off', desc: 'Off' }],
459
496
  [1, { name: 'on', desc: 'On' }]
460
497
  ]);
498
+ public systemUnits: byteValueMap = new byteValueMap([
499
+ [0, { name: 'english', desc: 'English' }],
500
+ [4, { name: 'metric', desc: 'Metric' }]
501
+ ]);
461
502
  public tempUnits: byteValueMap = new byteValueMap([
462
503
  [0, { name: 'F', desc: 'Fahrenheit' }],
463
504
  [4, { name: 'C', desc: 'Celsius' }]
@@ -482,7 +523,7 @@ export class byteValueMaps {
482
523
  [0, { name: 'none', desc: 'None', ph: { min: 6.8, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: false }],
483
524
  [1, { name: 'unknown', desc: 'Unknown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }],
484
525
  [2, { name: 'intellichem', desc: 'IntelliChem', ph: { min: 7.2, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: true }],
485
- [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 }],
486
527
  [4, { name: 'rem', desc: 'REM Chem', ph: { min: 6.8, max: 8.0 }, hasAddress: false }]
487
528
  ]);
488
529
  public siCalcTypes: byteValueMap = new byteValueMap([
@@ -510,12 +551,15 @@ export class byteValueMaps {
510
551
  [2, { name: 'rate', desc: 'Rate Sensor', remAddress: true }],
511
552
  [4, { name: 'pressure', desc: 'Pressure Sensor', remAddress: true }],
512
553
  ]);
513
-
514
554
  public chemDosingMethods: byteValueMap = new byteValueMap([
515
555
  [0, { name: 'manual', desc: 'Manual' }],
516
556
  [1, { name: 'time', desc: 'Time' }],
517
557
  [2, { name: 'volume', desc: 'Volume' }]
518
558
  ]);
559
+ public chemChlorDosingMethods: byteValueMap = new byteValueMap([
560
+ [0, { name: 'chlor', desc: 'Use Chlorinator Settings' }],
561
+ [1, { name: 'target', desc: 'Dynamic based on ORP Setpoint' }]
562
+ ]);
519
563
  public phSupplyTypes: byteValueMap = new byteValueMap([
520
564
  [0, { name: 'base', desc: 'Base pH+' }],
521
565
  [1, { name: 'acid', desc: 'Acid pH-' }]
@@ -530,6 +574,14 @@ export class byteValueMaps {
530
574
  [6, { name: 'qt', desc: 'Quarts' }],
531
575
  [7, { name: 'pt', desc: 'Pints' }]
532
576
  ]);
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
+ ]);
584
+
533
585
  public areaUnits: byteValueMap = new byteValueMap([
534
586
  [0, { name: '', desc: 'No Units' }],
535
587
  [1, { name: 'sqft', desc: 'Square Feet' }],
@@ -767,27 +819,36 @@ export class SystemBoard {
767
819
  /// relays. This method does not control RS485 operations such as pumps and chlorinators. These are done through the respective
768
820
  /// equipment polling functions.
769
821
  public async processStatusAsync() {
822
+ let self = this;
770
823
  try {
771
824
  if (this._statusCheckRef > 0) return;
772
825
  this.suspendStatus(true);
773
826
  if (typeof this._statusTimer !== 'undefined' && this._statusTimer) clearTimeout(this._statusTimer);
774
827
  // Go through all the assigned equipment and verify the current state.
775
828
  sys.board.system.keepManualTime();
776
- await sys.board.circuits.syncCircuitRelayStates();
777
- await sys.board.features.syncGroupStates();
778
- await sys.board.circuits.syncVirtualCircuitStates();
779
- await sys.board.valves.syncValveStates();
780
- await sys.board.filters.syncFilterStates();
781
- await sys.board.heaters.syncHeaterStates();
829
+ await sys.board.bodies.syncFreezeProtection();
830
+ await sys.board.syncEquipmentItems();
782
831
  await sys.board.schedules.syncScheduleStates();
832
+ await sys.board.circuits.checkEggTimerExpirationAsync();
783
833
  state.emitControllerChange();
784
834
  state.emitEquipmentChanges();
785
835
  } catch (err) { state.status = 255; logger.error(`Error performing processStatusAsync ${err.message}`); }
786
836
  finally {
787
837
  this.suspendStatus(false);
788
- if (this.statusInterval > 0) this._statusTimer = setTimeout(() => this.processStatusAsync(), this.statusInterval);
838
+ if (this.statusInterval > 0) this._statusTimer = setTimeout(async () => await self.processStatusAsync(), this.statusInterval);
789
839
  }
790
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();
849
+ }
850
+ catch (err) { logger.error(`Error synchronizing equipment items: ${err.message}`); }
851
+ }
791
852
  public async setControllerType(obj): Promise<Equipment> {
792
853
  try {
793
854
  if (obj.controllerType !== sys.controllerType)
@@ -865,6 +926,76 @@ export class BoardCommands {
865
926
  constructor(parent: SystemBoard) { this.board = parent; }
866
927
  }
867
928
  export class SystemCommands extends BoardCommands {
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];
937
+ try {
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: [] } };
966
+
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
+ }
868
999
  public cancelDelay(): Promise<any> { state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); }
869
1000
  public setDateTimeAsync(obj: any): Promise<any> { return Promise.resolve(); }
870
1001
  public keepManualTime() {
@@ -929,6 +1060,9 @@ export class SystemCommands extends BoardCommands {
929
1060
  if (obj.clockSource === 'server') sys.board.system.setTZ();
930
1061
  sys.board.system.setTempSensorsAsync(obj);
931
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;
932
1066
  return new Promise<Options>(function (resolve, reject) { resolve(sys.general.options); });
933
1067
  }
934
1068
  public async setLocationAsync(obj: any): Promise<Location> {
@@ -959,7 +1093,10 @@ export class SystemCommands extends BoardCommands {
959
1093
  state.temps.waterSensor1 = sys.equipment.tempSensors.getCalibration('water1') + temp;
960
1094
  let body = state.temps.bodies.getItemById(1);
961
1095
  if (body.isOn) body.temp = state.temps.waterSensor1;
962
-
1096
+ else if (sys.equipment.shared) {
1097
+ body = state.temps.bodies.getItemById(2);
1098
+ if (body.isOn) body.temp = state.temps.waterSensor1;
1099
+ }
963
1100
  }
964
1101
  break;
965
1102
  case 'waterSensor2':
@@ -1132,17 +1269,182 @@ export class SystemCommands extends BoardCommands {
1132
1269
  }
1133
1270
  }
1134
1271
  export class BodyCommands extends BoardCommands {
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];
1277
+ try {
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);
1385
+ }
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);
1404
+ }
1405
+ else {
1406
+ sstate.freezeProtect = false;
1407
+ pstate.freezeProtect = true;
1408
+ await sys.board.circuits.setCircuitStateAsync(6, true);
1409
+ }
1410
+ // Set a new date as this will be our rotation check now.
1411
+ this.freezeProtectBodyOn = new Date();
1412
+ }
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);
1426
+ }
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
+ }
1433
+ }
1434
+ catch (err) { logger.error(`syncFreezeProtection: Error synchronizing freeze protection states`); }
1435
+ }
1436
+
1135
1437
  public async initFilters() {
1136
1438
  try {
1137
1439
  let filter: Filter;
1138
1440
  let sFilter: FilterState;
1139
1441
  if (sys.equipment.maxBodies > 0) {
1140
1442
  filter = sys.filters.getItemById(1, true, { filterType: 3, name: sys.equipment.shared ? 'Filter' : 'Filter 1' });
1141
- sFilter = state.filters.getItemById(1, true, { name: filter.name });
1443
+ sFilter = state.filters.getItemById(1, true, { id: 1, name: filter.name });
1142
1444
  filter.isActive = true;
1143
1445
  filter.master = sys.board.equipmentMaster;
1144
1446
  filter.body = sys.equipment.shared ? sys.board.valueMaps.bodies.transformByName('poolspa') : 0;
1145
- sFilter = state.filters.getItemById(1, true);
1447
+ //sFilter = state.filters.getItemById(1, true);
1146
1448
  sFilter.body = filter.body;
1147
1449
  sFilter.filterType = filter.filterType;
1148
1450
  sFilter.name = filter.name;
@@ -1268,53 +1570,54 @@ export class BodyCommands extends BoardCommands {
1268
1570
  sys.board.heaters.syncHeaterStates();
1269
1571
  return Promise.resolve(bstate);
1270
1572
  }
1271
- public getHeatSources(bodyId: number) {
1272
- let heatSources = [];
1273
- let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1274
- heatSources.push(this.board.valueMaps.heatSources.transformByName('nochange'));
1275
- if (heatTypes.total > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('off'));
1276
- if (heatTypes.gas > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('heater'));
1277
- if (heatTypes.solar > 0) {
1278
- let hm = this.board.valueMaps.heatSources.transformByName('solar');
1279
- heatSources.push(hm);
1280
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('solarpref'));
1281
- }
1282
- if (heatTypes.heatpump > 0) {
1283
- let hm = this.board.valueMaps.heatSources.transformByName('heatpump');
1284
- heatSources.push(hm);
1285
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('heatpumppref'));
1286
- }
1287
- if (heatTypes.ultratemp > 0) {
1288
- let hm = this.board.valueMaps.heatSources.transformByName('ultratemp');
1289
- heatSources.push(hm);
1290
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('ultratemppref'));
1291
- }
1292
- return heatSources;
1293
- }
1294
- public getHeatModes(bodyId: number) {
1295
- let heatModes = [];
1296
- // 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)
1297
- heatModes.push(this.board.valueMaps.heatModes.transformByName('off')); // In IC fw 1.047 off is no longer 0.
1298
- let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1299
- if (heatTypes.gas > 0)
1300
- heatModes.push(this.board.valueMaps.heatModes.transformByName('heater'));
1301
- if (heatTypes.solar > 0) {
1302
- let hm = this.board.valueMaps.heatModes.transformByName('solar');
1303
- heatModes.push(hm);
1304
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('solarpref'));
1305
- }
1306
- if (heatTypes.heatpump > 0) {
1307
- let hm = this.board.valueMaps.heatModes.transformByName('heatpump');
1308
- heatModes.push(hm);
1309
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('heatpumppref'));
1310
- }
1311
- if (heatTypes.ultratemp > 0) {
1312
- let hm = this.board.valueMaps.heatModes.transformByName('ultratemp');
1313
- heatModes.push(hm);
1314
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('ultratemppref'));
1315
- }
1316
- return heatModes;
1317
- }
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
+ }
1318
1621
  public getPoolStates(): BodyTempState[] {
1319
1622
  let arrPools = [];
1320
1623
  for (let i = 0; i < state.temps.bodies.length; i++) {
@@ -1382,6 +1685,56 @@ export class BodyCommands extends BoardCommands {
1382
1685
  }
1383
1686
  }
1384
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
+
1385
1738
  public getPumpTypes() { return this.board.valueMaps.pumpTypes.toArray(); }
1386
1739
  public getCircuitUnits(pump?: Pump) {
1387
1740
  if (typeof pump === 'undefined')
@@ -1446,14 +1799,15 @@ export class PumpCommands extends BoardCommands {
1446
1799
  // and props that aren't for this pump type
1447
1800
  let _id = pump.id;
1448
1801
  if (pump.type !== pumpType || pumpType === 0) {
1449
- const _isVirtual = sys.pumps.getItemById(_id).isVirtual;
1802
+ let _p = pump.get(true);
1803
+ // const _isVirtual = typeof _p.isVirtual !== 'undefined' ? _p.isVirtual : false;
1450
1804
  sys.pumps.removeItemById(_id);
1451
- let pump = sys.pumps.getItemById(_id, true);
1452
- if (_isVirtual) {
1805
+ pump = sys.pumps.getItemById(_id, true);
1806
+ /* if (_isVirtual) {
1453
1807
  // pump.isActive = true;
1454
1808
  // pump.isVirtual = true;
1455
1809
  pump.master = 1;
1456
- }
1810
+ } */
1457
1811
  state.pumps.removeItemById(pump.id);
1458
1812
  pump.type = pumpType;
1459
1813
  let type = sys.board.valueMaps.pumpTypes.transform(pumpType);
@@ -1505,10 +1859,164 @@ export class PumpCommands extends BoardCommands {
1505
1859
  }
1506
1860
  }
1507
1861
  export class CircuitCommands extends BoardCommands {
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];
1867
+ try {
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];
1874
+ try {
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];
1881
+ try {
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
+ }
1508
2016
  public async syncCircuitRelayStates() {
1509
2017
  try {
1510
2018
  for (let i = 0; i < sys.circuits.length; i++) {
1511
- // Run through all the valves to see whether they should be triggered or not.
2019
+ // Run through all the controlled circuits to see whether they should be triggered or not.
1512
2020
  let circ = sys.circuits.getItemByIndex(i);
1513
2021
  if (circ.master === 1 && circ.isActive) {
1514
2022
  let cstate = state.circuits.getItemById(circ.id);
@@ -1517,7 +2025,6 @@ export class CircuitCommands extends BoardCommands {
1517
2025
  }
1518
2026
  } catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); }
1519
2027
  }
1520
-
1521
2028
  public syncVirtualCircuitStates() {
1522
2029
  try {
1523
2030
  let arrCircuits = sys.board.valueMaps.virtualCircuits.toArray();
@@ -1650,7 +2157,6 @@ export class CircuitCommands extends BoardCommands {
1650
2157
  //[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }]
1651
2158
  let func = sys.board.valueMaps.circuitFunctions.get(circuit.type);
1652
2159
  if (newState && (func.name === 'pool' || func.name === 'spa') && sys.equipment.shared === true) {
1653
- console.log(`Turning off shared body circuit`);
1654
2160
  // If we are shared we need to turn off the other circuit.
1655
2161
  let offType = func.name === 'pool' ? sys.board.valueMaps.circuitFunctions.getValue('spa') : sys.board.valueMaps.circuitFunctions.getValue('pool');
1656
2162
  let off = sys.circuits.get().filter(elem => elem.type === offType);
@@ -1665,14 +2171,13 @@ export class CircuitCommands extends BoardCommands {
1665
2171
  else if (id === 1) state.temps.bodies.getItemById(2, true).isOn = val;
1666
2172
  // Let the main nixie controller set the circuit state and affect the relays if it needs to.
1667
2173
  await ncp.circuits.setCircuitStateAsync(circ, newState);
2174
+ await sys.board.syncEquipmentItems();
1668
2175
  return state.circuits.getInterfaceById(circ.id);
1669
2176
  }
1670
2177
  catch (err) { return Promise.reject(`Nixie: Error setCircuitStateAsync ${err.message}`); }
1671
2178
  finally {
1672
- // sys.board.virtualPumpControllers.start();
1673
2179
  ncp.pumps.syncPumpStates();
1674
2180
  sys.board.suspendStatus(false);
1675
- this.board.processStatusAsync();
1676
2181
  state.emitEquipmentChanges();
1677
2182
  }
1678
2183
  }
@@ -1770,7 +2275,7 @@ export class CircuitCommands extends BoardCommands {
1770
2275
  if (typeof data.id !== 'undefined') {
1771
2276
  let circuit = sys.circuits.getItemById(id, true);
1772
2277
  let scircuit = state.circuits.getItemById(id, true);
1773
- circuit.isActive = true;
2278
+ scircuit.isActive = circuit.isActive = true;
1774
2279
  circuit.master = 1;
1775
2280
  scircuit.isOn = false;
1776
2281
  if (data.name) circuit.name = scircuit.name = data.name;
@@ -1788,6 +2293,7 @@ export class CircuitCommands extends BoardCommands {
1788
2293
  if (typeof data.deviceBinding !== 'undefined') circuit.deviceBinding = data.deviceBinding;
1789
2294
  if (typeof data.showInFeatures !== 'undefined') scircuit.showInFeatures = circuit.showInFeatures = utils.makeBool(data.showInFeatures);
1790
2295
  circuit.dontStop = circuit.eggTimer === 1440;
2296
+
1791
2297
  sys.emitEquipmentChange();
1792
2298
  state.emitEquipmentChanges();
1793
2299
  if (circuit.master === 1) await ncp.circuits.setCircuitAsync(circuit, data);
@@ -1870,8 +2376,9 @@ export class CircuitCommands extends BoardCommands {
1870
2376
  if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit light group id exceeded`, id, 'LightGroup'));
1871
2377
  if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'LightGroup'));
1872
2378
  group = sys.lightGroups.getItemById(id, true);
2379
+ let sgroup = state.lightGroups.getItemById(id, true);
1873
2380
  return new Promise<LightGroup>((resolve, reject) => {
1874
- if (typeof obj.name !== 'undefined') group.name = obj.name;
2381
+ if (typeof obj.name !== 'undefined') sgroup.name = group.name = obj.name;
1875
2382
  if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440;
1876
2383
  if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440);
1877
2384
  group.dontStop = group.eggTimer === 1440;
@@ -1978,7 +2485,9 @@ export class CircuitCommands extends BoardCommands {
1978
2485
  await sys.board.circuits.setCircuitStateAsync(c.circuit, false);
1979
2486
  else if (!cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) !== 'off') await sys.board.circuits.setCircuitStateAsync(c.circuit, true);
1980
2487
  }
1981
- sgrp.isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false : true;
2488
+ let isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false : true;
2489
+ sys.board.circuits.setEndTime(grp, sgrp, isOn);
2490
+ sgrp.isOn = isOn;
1982
2491
  // If we truly want to support themes in lightGroups we probably need to program
1983
2492
  // the specific on/off toggles to enable that. For now this will go through the motions but it's just a pretender.
1984
2493
  switch (theme) {
@@ -2058,46 +2567,122 @@ export class CircuitCommands extends BoardCommands {
2058
2567
  public async setLightGroupStateAsync(id: number, val: boolean): Promise<ICircuitGroupState> {
2059
2568
  return sys.board.circuits.setCircuitGroupStateAsync(id, val);
2060
2569
  }
2061
- public setEndTime(thing: ICircuit, thingState: ICircuitState, isOn: boolean) {
2062
- // this is a generic fn for circuits, features, circuitGroups, lightGroups
2063
- // to set the end time based on the egg timer.
2064
- // it will be called from set[]StateAsync calls as well as when then state is
2065
- // eval'ed from status packets/external messages and schedule changes.
2066
- // instead of maintaining timers here which would increase the amount of
2067
- // emits substantially, let the clients keep their own local timers
2068
- // or just display the end time.
2069
- if (thing.dontStop || !isOn) {
2070
- console.log(`setting thing ${thingState.id} end time to undefined.`);
2071
- thingState.endTime = undefined;
2072
- }
2073
- else if (!thingState.isOn && isOn) {
2074
- let endTime: Timestamp;
2075
- let eggTimerEndTime: Timestamp;
2076
- // let remainingDuration: number;
2077
- if (typeof thing.eggTimer !== 'undefined') {
2078
- eggTimerEndTime = state.time.clone().addHours(0, thing.eggTimer);
2079
- }
2080
- // egg timers don't come into play if a schedule will control the circuit
2081
- for (let i = 0; i < sys.schedules.length; i++) {
2082
- let sched = sys.schedules.getItemByIndex(i);
2083
- if (sched.isActive && sys.board.schedules.includesCircuit(sched, thing.id)) {
2084
- let nearestStartTime = sys.board.schedules.getNearestStartTime(sched);
2085
- let nearestEndTime = sys.board.schedules.getNearestEndTime(sched);
2086
- // if the schedule doesn't have an end date (eg no days)...
2087
- if (nearestEndTime.getTime() === 0) continue;
2088
- // else if the egg timer will turn the circuit off before the schedule begins
2089
- else if (eggTimerEndTime.getTime() < nearestStartTime.getTime()) continue;
2090
- // else if the end time isn't yet set
2091
- else if (typeof endTime === 'undefined') endTime = nearestEndTime.clone();
2092
- // and finally, compare the end time of this sched to the existing closest end time
2093
- else if (nearestEndTime.getTime() < endTime.getTime()) endTime = nearestEndTime.clone();
2570
+ public setEndTime(thing: ICircuit, thingState: ICircuitState, isOn: boolean, bForce: boolean= false) {
2571
+ /*
2572
+ this is a generic fn for circuits, features, circuitGroups, lightGroups
2573
+ to set the end time based on the egg timer.
2574
+ it will be called from set[]StateAsync calls as well as when then state is
2575
+ eval'ed from status packets/external messages and schedule changes.
2576
+ instead of maintaining timers here which would increase the amount of
2577
+ emits substantially, let the clients keep their own local timers
2578
+ or just display the end time.
2579
+
2580
+ bForce is an override sent by the syncScheduleStates. It gets set after the circuit gets set but we need to know if the sched is on. This allows the circuit end time to be
2581
+ re-evaluated even though it already has an end time.
2582
+
2583
+ Logic gets fun here...
2584
+ 0. If the circuit is off, or has don't stop enabled, don't set an end time
2585
+ 0.1. If the circuit state hasn't changed, abort (unless bForce is true).
2586
+ 1. If the schedule is on, the egg timer does not come into play
2587
+ 2. If the schedule is off...
2588
+ 2.1. and the egg timer will turn off the circuit off before the schedule starts, use egg timer time
2589
+ 2.2. else if the schedule will start before the egg timer turns it off, use the schedule end time
2590
+ 3. Iterate over each schedule for 1-2 above; nearest end time wins
2591
+ */
2592
+ try {
2593
+ if (thing.dontStop || !isOn) {
2594
+ thingState.endTime = undefined;
2595
+ }
2596
+ else if (!thingState.isOn && isOn || bForce) {
2597
+ let endTime: Timestamp;
2598
+ let eggTimerEndTime: Timestamp;
2599
+ // let remainingDuration: number;
2600
+ if (typeof thing.eggTimer !== 'undefined') {
2601
+ eggTimerEndTime = state.time.clone().addHours(0, thing.eggTimer);
2602
+ }
2603
+ // egg timers don't come into play if a schedule will control the circuit
2604
+ for (let i = 0; i < sys.schedules.length; i++) {
2605
+ let sched = sys.schedules.getItemByIndex(i);
2606
+ let ssched = state.schedules.getItemById(sched.id);
2607
+ if (sched.isActive && sys.board.schedules.includesCircuit(sched, thing.id)) {
2608
+ let nearestStartTime = sys.board.schedules.getNearestStartTime(sched);
2609
+ let nearestEndTime = sys.board.schedules.getNearestEndTime(sched);
2610
+ // if the schedule doesn't have an end date (eg no days)...
2611
+ if (nearestEndTime.getTime() === 0) continue;
2612
+ if (ssched.isOn) {
2613
+ if (typeof endTime === 'undefined' || nearestEndTime.getTime() < endTime.getTime()) {
2614
+ endTime = nearestEndTime.clone();
2615
+ eggTimerEndTime = undefined;
2616
+ }
2617
+ }
2618
+ else {
2619
+ if (typeof eggTimerEndTime !== 'undefined' && eggTimerEndTime.getTime() < nearestStartTime.getTime()) {
2620
+ if (typeof endTime === 'undefined' || eggTimerEndTime.getTime() < endTime.getTime()) endTime = eggTimerEndTime.clone();
2621
+ }
2622
+ else if (typeof endTime === 'undefined' || nearestEndTime.getTime() < endTime.getTime()) endTime = nearestEndTime.clone();
2623
+ }
2624
+ }
2094
2625
  }
2626
+ if (typeof endTime !== 'undefined') thingState.endTime = endTime;
2627
+ else if (typeof eggTimerEndTime !== 'undefined') thingState.endTime = eggTimerEndTime;
2095
2628
  }
2096
- thingState.endTime = typeof endTime !== 'undefined' ? endTime : eggTimerEndTime;
2629
+ }
2630
+ catch (err) {
2631
+ logger.error(`Error setting end time for ${thing.id}: ${err}`)
2097
2632
  }
2098
2633
  }
2099
2634
  }
2100
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
+
2101
2686
  public async setFeatureAsync(obj: any): Promise<Feature> {
2102
2687
  let id = parseInt(obj.id, 10);
2103
2688
  if (id <= 0 || isNaN(id)) {
@@ -2151,7 +2736,6 @@ export class FeatureCommands extends BoardCommands {
2151
2736
  sys.board.circuits.setEndTime(feature, fstate, val);
2152
2737
  fstate.isOn = val;
2153
2738
  sys.board.valves.syncValveStates();
2154
- // sys.board.virtualPumpControllers.start();
2155
2739
  ncp.pumps.syncPumpStates();
2156
2740
  state.emitEquipmentChanges();
2157
2741
  return fstate;
@@ -2214,6 +2798,57 @@ export class FeatureCommands extends BoardCommands {
2214
2798
  }
2215
2799
  }
2216
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
+
2217
2852
  public async setChlorAsync(obj: any): Promise<ChlorinatorState> {
2218
2853
  try {
2219
2854
  let id = parseInt(obj.id, 10);
@@ -2232,7 +2867,7 @@ export class ChlorinatorCommands extends BoardCommands {
2232
2867
  public async deleteChlorAsync(obj: any): Promise<ChlorinatorState> {
2233
2868
  try {
2234
2869
  let id = parseInt(obj.id, 10);
2235
- 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));
2236
2871
  let chlor = state.chlorinators.getItemById(id);
2237
2872
  chlor.isActive = false;
2238
2873
  await ncp.chlorinators.deleteChlorinatorAsync(id);
@@ -2256,6 +2891,56 @@ export class ChlorinatorCommands extends BoardCommands {
2256
2891
  }
2257
2892
  }
2258
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
+
2259
2944
  public transformDays(val: any): number {
2260
2945
  if (typeof val === 'number') return val;
2261
2946
  let edays = sys.board.valueMaps.scheduleDays.toArray();
@@ -2376,6 +3061,9 @@ export class ScheduleCommands extends BoardCommands {
2376
3061
  return Promise.reject(new InvalidEquipmentDataError(`Invalid circuit reference: ${circuit}`, 'Schedule', circuit));
2377
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));
2378
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);
2379
3067
  sched.circuit = ssched.circuit = circuit;
2380
3068
  sched.scheduleDays = ssched.scheduleDays = schedDays;
2381
3069
  sched.scheduleType = ssched.scheduleType = schedType;
@@ -2391,6 +3079,7 @@ export class ScheduleCommands extends BoardCommands {
2391
3079
  sched.startYear = startDate.getFullYear();
2392
3080
  sched.startMonth = startDate.getMonth() + 1;
2393
3081
  sched.startDay = startDate.getDate();
3082
+ sched.isActive = sched.startTime !== 0;
2394
3083
 
2395
3084
  ssched.display = sched.display = display;
2396
3085
  if (typeof sched.startDate === 'undefined')
@@ -2428,10 +3117,10 @@ export class ScheduleCommands extends BoardCommands {
2428
3117
  (ssched.scheduleDays & dayVal) > 0 &&
2429
3118
  ts >= ssched.startTime && ts <= ssched.endTime) schedIsOn = true
2430
3119
  else schedIsOn = false;
2431
- if (schedIsOn !== ssched.isOn){
3120
+ if (schedIsOn !== ssched.isOn) {
2432
3121
  // if the schedule state changes, it may affect the end time
2433
- sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(ssched.circuit), scirc, ssched.isOn);
2434
3122
  ssched.isOn = schedIsOn;
3123
+ sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(ssched.circuit), scirc, scirc.isOn, true);
2435
3124
  }
2436
3125
  ssched.emitEquipmentChange();
2437
3126
  }
@@ -2460,8 +3149,8 @@ export class ScheduleCommands extends BoardCommands {
2460
3149
  let days = sys.board.valueMaps.scheduleDays.transform(sched.scheduleDays).days;
2461
3150
  for (let i = 0; i < days.length; i++) {
2462
3151
  let schedDay = days[i].dow;
2463
- let dateDiff = schedDay + 7 - startDateDay % 7;
2464
- if (schedDay === startDateDay && sched.endTime > todayTime) dateDiff = 0;
3152
+ let dateDiff = (schedDay + 7 - startDateDay) % 7;
3153
+ if (schedDay === startDateDay && sched.endTime < todayTime) dateDiff = 7;
2465
3154
  let endDateTime = startDate.clone().addHours(dateDiff * 24, sched.endTime);
2466
3155
  if (nearestEndTime.getTime() === 0 || endDateTime.getTime() < nearestEndTime.getTime()) nearestEndTime = endDateTime;
2467
3156
  }
@@ -2477,8 +3166,8 @@ export class ScheduleCommands extends BoardCommands {
2477
3166
  let days = sys.board.valueMaps.scheduleDays.transform(sched.scheduleDays).days;
2478
3167
  for (let i = 0; i < days.length; i++) {
2479
3168
  let schedDay = days[i].dow;
2480
- let dateDiff = schedDay + 7 - startDateDay % 7;
2481
- if (schedDay === startDateDay && sched.startTime > todayTime) dateDiff = 0;
3169
+ let dateDiff = (schedDay + 7 - startDateDay) % 7;
3170
+ if (schedDay === startDateDay && sched.startTime < todayTime) dateDiff = 7;
2482
3171
  let startDateTime = startDate.clone().addHours(dateDiff * 24, sched.startTime);
2483
3172
  if (nearestStartTime.getTime() === 0 || startDateTime.getTime() < nearestStartTime.getTime()) nearestStartTime = startDateTime;
2484
3173
  }
@@ -2486,387 +3175,535 @@ export class ScheduleCommands extends BoardCommands {
2486
3175
  }
2487
3176
  }
2488
3177
  export class HeaterCommands extends BoardCommands {
2489
- public getInstalledHeaterTypes(body?: number): any {
2490
- let heaters = sys.heaters.get();
2491
- let types = sys.board.valueMaps.heaterTypes.toArray();
2492
- let inst = { total: 0 };
2493
- for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
2494
- for (let i = 0; i < heaters.length; i++) {
2495
- let heater = heaters[i];
2496
- if (typeof body !== 'undefined' && heater.body !== 'undefined') {
2497
- if ((heater.body !== 32 && body !== heater.body + 1) || (heater.body === 32 && body > 2)) continue;
2498
- }
2499
- let type = types.find(elem => elem.val === heater.type);
2500
- if (typeof type !== 'undefined') {
2501
- if (inst[type.name] === 'undefined') inst[type.name] = 0;
2502
- inst[type.name] = inst[type.name] + 1;
2503
- if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
2504
- inst.total++;
2505
- }
2506
- }
2507
- return inst;
2508
- }
2509
- public isSolarInstalled(body?: number): boolean {
2510
- let heaters = sys.heaters.get();
2511
- let types = sys.board.valueMaps.heaterTypes.toArray();
2512
- for (let i = 0; i < heaters.length; i++) {
2513
- let heater = heaters[i];
2514
- if (typeof body !== 'undefined' && body !== heater.body) continue;
2515
- let type = types.find(elem => elem.val === heater.type);
2516
- if (typeof type !== 'undefined') {
2517
- switch (type.name) {
2518
- case 'solar':
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
+ }
2519
3205
  return true;
2520
- }
2521
- }
3206
+ } catch (err) { logger.error(`Error restoring heaters: ${err.message}`); res.addModuleError('system', `Error restoring heaters: ${err.message}`); return false; }
2522
3207
  }
2523
- }
2524
- public isHeatPumpInstalled(body?: number): boolean {
2525
- let heaters = sys.heaters.get();
2526
- let types = sys.board.valueMaps.heaterTypes.toArray();
2527
- for (let i = 0; i < heaters.length; i++) {
2528
- let heater = heaters[i];
2529
- if (typeof body !== 'undefined' && body !== heater.body) continue;
2530
- let type = types.find(elem => elem.val === heater.type);
2531
- if (typeof type !== 'undefined') {
2532
- switch (type.name) {
2533
- case 'heatpump':
2534
- return true;
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
+
3228
+ public getInstalledHeaterTypes(body?: number): any {
3229
+ let heaters = sys.heaters.get();
3230
+ let types = sys.board.valueMaps.heaterTypes.toArray();
3231
+ let inst = { total: 0 };
3232
+ for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
3233
+ for (let i = 0; i < heaters.length; i++) {
3234
+ let heater = heaters[i];
3235
+ if (typeof body !== 'undefined' && heater.body !== 'undefined') {
3236
+ if ((heater.body !== 32 && body !== heater.body + 1) || (heater.body === 32 && body > 2)) continue;
3237
+ }
3238
+ let type = types.find(elem => elem.val === heater.type);
3239
+ if (typeof type !== 'undefined') {
3240
+ if (inst[type.name] === 'undefined') inst[type.name] = 0;
3241
+ inst[type.name] = inst[type.name] + 1;
3242
+ if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
3243
+ inst.total++;
3244
+ }
2535
3245
  }
2536
- }
3246
+ return inst;
2537
3247
  }
2538
- }
2539
- public setHeater(heater: Heater, obj?: any) {
2540
- if (typeof obj !== undefined) {
2541
- for (var s in obj)
2542
- heater[s] = obj[s];
3248
+ public isSolarInstalled(body?: number): boolean {
3249
+ let heaters = sys.heaters.get();
3250
+ let types = sys.board.valueMaps.heaterTypes.toArray();
3251
+ for (let i = 0; i < heaters.length; i++) {
3252
+ let heater = heaters[i];
3253
+ if (typeof body !== 'undefined' && body !== heater.body) continue;
3254
+ let type = types.find(elem => elem.val === heater.type);
3255
+ if (typeof type !== 'undefined') {
3256
+ switch (type.name) {
3257
+ case 'solar':
3258
+ return true;
3259
+ }
3260
+ }
3261
+ }
2543
3262
  }
2544
- }
2545
- public async setHeaterAsync(obj: any): Promise<Heater> {
2546
- try {
2547
- let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
2548
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
2549
- else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Virtual Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
2550
- let heater: Heater;
2551
- if (id <= 0) {
2552
- // We are adding a heater. In this case all heaters are virtual.
2553
- let vheaters = sys.heaters.filter(h => h.isVirtual === true);
2554
- id = vheaters.length + 256;
2555
- }
2556
- heater = sys.heaters.getItemById(id, true);
2557
- if (typeof obj !== undefined) {
2558
- for (var s in obj) {
2559
- if (s === 'id') continue;
2560
- heater[s] = obj[s];
2561
- }
2562
- }
2563
- let hstate = state.heaters.getItemById(id, true);
2564
- hstate.isVirtual = heater.isVirtual = true;
2565
- hstate.name = heater.name;
2566
- hstate.type = heater.type;
2567
- heater.master = 1;
2568
- if (heater.master === 1) await ncp.heaters.setHeaterAsync(heater, obj);
2569
- await sys.board.heaters.updateHeaterServices();
2570
- await sys.board.heaters.syncHeaterStates();
2571
- return heater;
2572
- } catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
2573
- }
2574
- public async deleteHeaterAsync(obj: any): Promise<Heater> {
2575
- try {
2576
- let id = parseInt(obj.id, 10);
2577
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
2578
- let heater = sys.heaters.getItemById(id);
2579
- heater.isActive = false;
2580
- if (heater.master === 1) await ncp.heaters.deleteHeaterAsync(heater.id);
2581
- sys.heaters.removeItemById(id);
2582
- state.heaters.removeItemById(id);
2583
- sys.board.heaters.updateHeaterServices();
2584
- sys.board.heaters.syncHeaterStates();
2585
- return heater;
2586
- } catch (err) { return Promise.reject(`Error deleting heater: ${err.message}`) }
2587
- }
2588
- public updateHeaterServices() {
2589
- let htypes = sys.board.heaters.getInstalledHeaterTypes();
2590
- let solarInstalled = htypes.solar > 0;
2591
- let heatPumpInstalled = htypes.heatpump > 0;
2592
- let gasHeaterInstalled = htypes.gas > 0;
2593
-
2594
- if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
2595
- if (gasHeaterInstalled) sys.board.valueMaps.heatSources.set(3, { name: 'heater', desc: 'Heater' });
2596
- if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
2597
- else if (solarInstalled) sys.board.valueMaps.heatSources.set(5, { name: 'solar', desc: 'Solar' });
2598
- if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
2599
- else if (heatPumpInstalled) sys.board.valueMaps.heatSources.set(9, { name: 'heatpump', desc: 'Heat Pump' });
2600
- sys.board.valueMaps.heatSources.set(32, { name: 'nochange', desc: 'No Change' });
2601
-
2602
- sys.board.valueMaps.heatModes = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
2603
- if (gasHeaterInstalled) sys.board.valueMaps.heatModes.set(3, { name: 'heater', desc: 'Heater' });
2604
- if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
2605
- else if (solarInstalled) sys.board.valueMaps.heatModes.set(5, { name: 'solar', desc: 'Solar' });
2606
- if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
2607
- else if (heatPumpInstalled) sys.board.valueMaps.heatModes.set(9, { name: 'heatpump', desc: 'Heat Pump' });
2608
- // Now set the body data.
2609
- for (let i = 0; i < sys.bodies.length; i++) {
2610
- let body = sys.bodies.getItemByIndex(i);
2611
- let btemp = state.temps.bodies.getItemById(body.id, body.isActive !== false);
2612
- let opts = sys.board.heaters.getInstalledHeaterTypes(body.id);
2613
- btemp.heaterOptions = opts;
2614
- }
2615
- this.setActiveTempSensors();
2616
- }
2617
- public initTempSensors() {
2618
- // Add in the potential sensors and delete the ones that shouldn't exist.
2619
- let maxPairs = sys.equipment.maxBodies + (sys.equipment.shared ? -1 : 0);
2620
- sys.equipment.tempSensors.getItemById('air', true, { id: 'air', isActive: true, calibration: 0 }).name = 'Air';
2621
- sys.equipment.tempSensors.getItemById('water1', true, { id: 'water1', isActive: true, calibration: 0 }).name = maxPairs == 1 ? 'Water' : 'Body 1';
2622
- sys.equipment.tempSensors.getItemById('solar1', true, { id: 'solar1', isActive: false, calibration: 0 }).name = maxPairs == 1 ? 'Solar' : 'Solar 1';
2623
- if (maxPairs > 1) {
2624
- sys.equipment.tempSensors.getItemById('water2', true, { id: 'water2', isActive: false, calibration: 0 }).name = 'Body 2';
2625
- sys.equipment.tempSensors.getItemById('solar2', true, { id: 'solar2', isActive: false, calibration: 0 }).name = 'Solar 2';
3263
+ public isHeatPumpInstalled(body?: number): boolean {
3264
+ let heaters = sys.heaters.get();
3265
+ let types = sys.board.valueMaps.heaterTypes.toArray();
3266
+ for (let i = 0; i < heaters.length; i++) {
3267
+ let heater = heaters[i];
3268
+ if (typeof body !== 'undefined' && body !== heater.body) continue;
3269
+ let type = types.find(elem => elem.val === heater.type);
3270
+ if (typeof type !== 'undefined') {
3271
+ switch (type.name) {
3272
+ case 'heatpump':
3273
+ return true;
3274
+ }
3275
+ }
3276
+ }
2626
3277
  }
2627
- else {
2628
- sys.equipment.tempSensors.removeItemById('water2');
2629
- sys.equipment.tempSensors.removeItemById('solar2');
3278
+ public setHeater(heater: Heater, obj?: any) {
3279
+ if (typeof obj !== undefined) {
3280
+ for (var s in obj)
3281
+ heater[s] = obj[s];
3282
+ }
2630
3283
  }
2631
- if (maxPairs > 2) {
2632
- sys.equipment.tempSensors.getItemById('water3', true, { id: 'water3', isActive: false, calibration: 0 }).name = 'Body 3';
2633
- sys.equipment.tempSensors.getItemById('solar3', true, { id: 'solar3', isActive: false, calibration: 0 }).name = 'Solar 3';
3284
+ public async setHeaterAsync(obj: any): Promise<Heater> {
3285
+ try {
3286
+ let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
3287
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
3288
+ else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Virtual Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
3289
+ let heater: Heater;
3290
+ if (id <= 0) {
3291
+ // We are adding a heater. In this case all heaters are virtual.
3292
+ let vheaters = sys.heaters.filter(h => h.master === 1);
3293
+ id = vheaters.length + 256;
3294
+ }
3295
+ heater = sys.heaters.getItemById(id, true);
3296
+ if (typeof obj !== undefined) {
3297
+ for (var s in obj) {
3298
+ if (s === 'id') continue;
3299
+ heater[s] = obj[s];
3300
+ }
3301
+ }
3302
+ let hstate = state.heaters.getItemById(id, true);
3303
+ //hstate.isVirtual = heater.isVirtual = true;
3304
+ hstate.name = heater.name;
3305
+ hstate.type = heater.type;
3306
+ heater.master = 1;
3307
+ if (heater.master === 1) await ncp.heaters.setHeaterAsync(heater, obj);
3308
+ await sys.board.heaters.updateHeaterServices();
3309
+ await sys.board.heaters.syncHeaterStates();
3310
+ return heater;
3311
+ } catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
2634
3312
  }
2635
- else {
2636
- sys.equipment.tempSensors.removeItemById('water3');
2637
- sys.equipment.tempSensors.removeItemById('solar3');
3313
+ public async deleteHeaterAsync(obj: any): Promise<Heater> {
3314
+ try {
3315
+ let id = parseInt(obj.id, 10);
3316
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
3317
+ let heater = sys.heaters.getItemById(id);
3318
+ heater.isActive = false;
3319
+ if (heater.master === 1) await ncp.heaters.deleteHeaterAsync(heater.id);
3320
+ sys.heaters.removeItemById(id);
3321
+ state.heaters.removeItemById(id);
3322
+ sys.board.heaters.updateHeaterServices();
3323
+ sys.board.heaters.syncHeaterStates();
3324
+ return heater;
3325
+ } catch (err) { return Promise.reject(`Error deleting heater: ${err.message}`) }
2638
3326
  }
2639
- if (maxPairs > 3) {
2640
- sys.equipment.tempSensors.getItemById('water4', true, { id: 'water4', isActive: false, calibration: 0 }).name = 'Body 4';
2641
- sys.equipment.tempSensors.getItemById('solar4', true, { id: 'solar4', isActive: false, calibration: 0 }).name = 'Solar 4';
3327
+ public updateHeaterServices() {
3328
+ let htypes = sys.board.heaters.getInstalledHeaterTypes();
3329
+ let solarInstalled = htypes.solar > 0;
3330
+ let heatPumpInstalled = htypes.heatpump > 0;
3331
+ let gasHeaterInstalled = htypes.gas > 0;
3332
+
3333
+ if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
3334
+ if (gasHeaterInstalled) sys.board.valueMaps.heatSources.set(3, { name: 'heater', desc: 'Heater' });
3335
+ if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
3336
+ else if (solarInstalled) sys.board.valueMaps.heatSources.set(5, { name: 'solar', desc: 'Solar' });
3337
+ if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
3338
+ else if (heatPumpInstalled) sys.board.valueMaps.heatSources.set(9, { name: 'heatpump', desc: 'Heat Pump' });
3339
+ sys.board.valueMaps.heatSources.set(32, { name: 'nochange', desc: 'No Change' });
3340
+
3341
+ sys.board.valueMaps.heatModes = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
3342
+ if (gasHeaterInstalled) sys.board.valueMaps.heatModes.set(3, { name: 'heater', desc: 'Heater' });
3343
+ if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
3344
+ else if (solarInstalled) sys.board.valueMaps.heatModes.set(5, { name: 'solar', desc: 'Solar' });
3345
+ if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
3346
+ else if (heatPumpInstalled) sys.board.valueMaps.heatModes.set(9, { name: 'heatpump', desc: 'Heat Pump' });
3347
+ // Now set the body data.
3348
+ for (let i = 0; i < sys.bodies.length; i++) {
3349
+ let body = sys.bodies.getItemByIndex(i);
3350
+ let btemp = state.temps.bodies.getItemById(body.id, body.isActive !== false);
3351
+ let opts = sys.board.heaters.getInstalledHeaterTypes(body.id);
3352
+ btemp.heaterOptions = opts;
3353
+ }
3354
+ this.setActiveTempSensors();
2642
3355
  }
2643
- else {
2644
- sys.equipment.tempSensors.removeItemById('water4');
2645
- sys.equipment.tempSensors.removeItemById('solar4');
2646
- }
2647
-
2648
- }
2649
- // Sets the active temp sensors based upon the installed equipment. At this point all
2650
- // detectable temp sensors should exist.
2651
- public setActiveTempSensors() {
2652
- let htypes;
2653
- // We are iterating backwards through the sensors array on purpose. We do this just in case we need
2654
- // to remove a sensor during the iteration. This way the index values will not be impacted and we can
2655
- // safely remove from the array we are iterating.
2656
- for (let i = sys.equipment.tempSensors.length - 1; i >= 0; i--) {
2657
- let sensor = sys.equipment.tempSensors.getItemByIndex(i);
2658
- // The names are normalized in this array.
2659
- switch (sensor.id) {
2660
- case 'air':
2661
- sensor.isActive = true;
2662
- break;
2663
- case 'water1':
2664
- sensor.isActive = sys.equipment.maxBodies > 0;
2665
- break;
2666
- case 'water2':
2667
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 2 : sys.equipment.maxBodies > 1;
2668
- break;
2669
- case 'water3':
2670
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 3 : sys.equipment.maxBodies > 2;
2671
- break;
2672
- case 'water4':
2673
- // It's a little weird but technically you should be able to install 3 expansions and a i10D personality
2674
- // board. If this situation ever comes up we will see if it works. Whether it reports is another story
2675
- // since the 2 message is short a byte for this.
2676
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 4 : sys.equipment.maxBodies > 3;
2677
- break;
2678
- // Solar sensors are funny ducks. This is because they are for both heatpumps and solar and the equipment
2679
- // can be installed on specific bodies. This will be true for heaters installed in expansion panels for *Touch, dual body systems,
2680
- // and any IntelliCenter with more than one body. At some point simply implementing the multi-body functions for touch will make
2681
- // this all work. This will only be with i10D or expansion panels.
2682
- case 'solar1':
2683
- // The first solar sensor is a funny duck in that it should be active for shared systems
2684
- // if either body has an active solar heater or heatpump.
2685
- htypes = sys.board.heaters.getInstalledHeaterTypes(1);
2686
- if ('solar' in htypes || 'heatpump' in htypes) sensor.isActive = true;
2687
- else if (sys.equipment.shared) {
2688
- htypes = sys.board.heaters.getInstalledHeaterTypes(2);
2689
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
2690
- }
2691
- else sensor.isActive = false;
2692
- break;
2693
- case 'solar2':
2694
- if (sys.equipment.maxBodies > 1 + (sys.equipment.shared ? 1 : 0)) {
2695
- htypes = sys.board.heaters.getInstalledHeaterTypes(2 + (sys.equipment.shared ? 1 : 0));
2696
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
2697
- }
2698
- else sensor.isActive = false;
2699
- break;
2700
- case 'solar3':
2701
- if (sys.equipment.maxBodies > 2 + (sys.equipment.shared ? 1 : 0)) {
2702
- htypes = sys.board.heaters.getInstalledHeaterTypes(3 + (sys.equipment.shared ? 1 : 0));
2703
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
2704
- }
2705
- else sensor.isActive = false;
2706
- break;
2707
- case 'solar4':
2708
- if (sys.equipment.maxBodies > 3 + (sys.equipment.shared ? 1 : 0)) {
2709
- htypes = sys.board.heaters.getInstalledHeaterTypes(4 + (sys.equipment.shared ? 1 : 0));
2710
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
2711
- }
2712
- else sensor.isActive = false;
2713
- break;
2714
- default:
2715
- if (typeof sensor.id === 'undefined') sys.equipment.tempSensors.removeItemByIndex(i);
2716
- break;
2717
- }
3356
+ public initTempSensors() {
3357
+ // Add in the potential sensors and delete the ones that shouldn't exist.
3358
+ let maxPairs = sys.equipment.maxBodies + (sys.equipment.shared ? -1 : 0);
3359
+ sys.equipment.tempSensors.getItemById('air', true, { id: 'air', isActive: true, calibration: 0 }).name = 'Air';
3360
+ sys.equipment.tempSensors.getItemById('water1', true, { id: 'water1', isActive: true, calibration: 0 }).name = maxPairs == 1 ? 'Water' : 'Body 1';
3361
+ sys.equipment.tempSensors.getItemById('solar1', true, { id: 'solar1', isActive: false, calibration: 0 }).name = maxPairs == 1 ? 'Solar' : 'Solar 1';
3362
+ if (maxPairs > 1) {
3363
+ sys.equipment.tempSensors.getItemById('water2', true, { id: 'water2', isActive: false, calibration: 0 }).name = 'Body 2';
3364
+ sys.equipment.tempSensors.getItemById('solar2', true, { id: 'solar2', isActive: false, calibration: 0 }).name = 'Solar 2';
3365
+ }
3366
+ else {
3367
+ sys.equipment.tempSensors.removeItemById('water2');
3368
+ sys.equipment.tempSensors.removeItemById('solar2');
3369
+ }
3370
+ if (maxPairs > 2) {
3371
+ sys.equipment.tempSensors.getItemById('water3', true, { id: 'water3', isActive: false, calibration: 0 }).name = 'Body 3';
3372
+ sys.equipment.tempSensors.getItemById('solar3', true, { id: 'solar3', isActive: false, calibration: 0 }).name = 'Solar 3';
3373
+ }
3374
+ else {
3375
+ sys.equipment.tempSensors.removeItemById('water3');
3376
+ sys.equipment.tempSensors.removeItemById('solar3');
3377
+ }
3378
+ if (maxPairs > 3) {
3379
+ sys.equipment.tempSensors.getItemById('water4', true, { id: 'water4', isActive: false, calibration: 0 }).name = 'Body 4';
3380
+ sys.equipment.tempSensors.getItemById('solar4', true, { id: 'solar4', isActive: false, calibration: 0 }).name = 'Solar 4';
3381
+ }
3382
+ else {
3383
+ sys.equipment.tempSensors.removeItemById('water4');
3384
+ sys.equipment.tempSensors.removeItemById('solar4');
3385
+ }
3386
+
2718
3387
  }
2719
- }
2720
- // This updates the heater states based upon the installed heaters. This is true for heaters that are tied to the OCP
2721
- // and those that are not.
2722
- public syncHeaterStates() {
2723
- try {
2724
- // Go through the installed heaters and bodies to determine whether they should be on. If there is a
2725
- // heater that is not controlled by the OCP then we need to determine whether it should be on.
2726
- let heaters = sys.heaters.toArray();
2727
- let bodies = state.temps.bodies.toArray();
2728
- let hon = [];
2729
- for (let i = 0; i < bodies.length; i++) {
2730
- let body: BodyTempState = bodies[i];
2731
- let cfgBody: Body = sys.bodies.getItemById(body.id);
2732
- let isHeating = false;
2733
- if (body.isOn) {
2734
- if (typeof body.temp === 'undefined' && heaters.length > 0) logger.warn(`The body temperature for ${body.name} cannot be determined. Heater status for this body cannot be calculated.`);
2735
- for (let j = 0; j < heaters.length; j++) {
2736
- let heater: Heater = heaters[j];
2737
- if (heater.isActive === false) continue;
2738
- let isOn = false;
2739
- // Determine whether the heater can be used on this body.
2740
- let isAssociated = false;
2741
- let b = sys.board.valueMaps.bodies.transform(heater.body);
2742
- switch (b.name) {
2743
- case 'body1':
2744
- case 'pool':
2745
- if (body.id === 1) isAssociated = true;
2746
- break;
2747
- case 'body2':
2748
- case 'spa':
2749
- if (body.id === 2) isAssociated = true;
2750
- break;
2751
- case 'poolspa':
2752
- if (body.id === 1 || body.id === 2) isAssociated = true;
2753
- break;
2754
- case 'body3':
2755
- if (body.id === 3) isAssociated = true;
2756
- break;
2757
- case 'body4':
2758
- if (body.id === 4) isAssociated = true;
2759
- break;
2760
- }
2761
- logger.silly(`Heater ${heater.name} is ${isAssociated === true ? '' : 'not '}associated with ${body.name}`);
2762
- if (isAssociated) {
2763
- let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
2764
- let status = sys.board.valueMaps.heatStatus.transform(body.heatStatus);
2765
- let hstate = state.heaters.getItemById(heater.id, true);
2766
- if (heater.isVirtual === true || heater.master === 1) {
2767
- // We need to do our own calculation as to whether it is on. This is for Nixie heaters.
2768
- let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
2769
- switch (htype.name) {
2770
- case 'solar':
2771
- if (mode === 'solar' || mode === 'solarpref') {
2772
- // Measure up against start and stop temp deltas for effective solar heating.
2773
- if (body.temp < cfgBody.heatSetpoint &&
2774
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
2775
- isOn = true;
2776
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
2777
- isHeating = true;
2778
- }
2779
- else if (heater.coolingEnabled && body.temp > cfgBody.coolSetpoint && state.heliotrope.isNight &&
2780
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
2781
- isOn = true;
2782
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
2783
- isHeating = true;
2784
- }
2785
- //else if (heater.coolingEnabled && state.time.isNight)
2786
- }
3388
+ // Sets the active temp sensors based upon the installed equipment. At this point all
3389
+ // detectable temp sensors should exist.
3390
+ public setActiveTempSensors() {
3391
+ let htypes;
3392
+ // We are iterating backwards through the sensors array on purpose. We do this just in case we need
3393
+ // to remove a sensor during the iteration. This way the index values will not be impacted and we can
3394
+ // safely remove from the array we are iterating.
3395
+ for (let i = sys.equipment.tempSensors.length - 1; i >= 0; i--) {
3396
+ let sensor = sys.equipment.tempSensors.getItemByIndex(i);
3397
+ // The names are normalized in this array.
3398
+ switch (sensor.id) {
3399
+ case 'air':
3400
+ sensor.isActive = true;
3401
+ break;
3402
+ case 'water1':
3403
+ sensor.isActive = sys.equipment.maxBodies > 0;
3404
+ break;
3405
+ case 'water2':
3406
+ sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 2 : sys.equipment.maxBodies > 1;
3407
+ break;
3408
+ case 'water3':
3409
+ sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 3 : sys.equipment.maxBodies > 2;
2787
3410
  break;
2788
- case 'ultratemp':
2789
- if (mode === 'ultratemp' || mode === 'ultratemppref') {
2790
- if (body.temp < cfgBody.heatSetpoint &&
2791
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
2792
- isOn = true;
2793
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
2794
- isHeating = true;
2795
- }
2796
- else if (body.temp > cfgBody.coolSetpoint && heater.coolingEnabled) {
2797
- isOn = true;
2798
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
2799
- isHeating = true;
2800
- }
3411
+ case 'water4':
3412
+ // It's a little weird but technically you should be able to install 3 expansions and a i10D personality
3413
+ // board. If this situation ever comes up we will see if it works. Whether it reports is another story
3414
+ // since the 2 message is short a byte for this.
3415
+ sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 4 : sys.equipment.maxBodies > 3;
3416
+ break;
3417
+ // Solar sensors are funny ducks. This is because they are for both heatpumps and solar and the equipment
3418
+ // can be installed on specific bodies. This will be true for heaters installed in expansion panels for *Touch, dual body systems,
3419
+ // and any IntelliCenter with more than one body. At some point simply implementing the multi-body functions for touch will make
3420
+ // this all work. This will only be with i10D or expansion panels.
3421
+ case 'solar1':
3422
+ // The first solar sensor is a funny duck in that it should be active for shared systems
3423
+ // if either body has an active solar heater or heatpump.
3424
+ htypes = sys.board.heaters.getInstalledHeaterTypes(1);
3425
+ if ('solar' in htypes || 'heatpump' in htypes) sensor.isActive = true;
3426
+ else if (sys.equipment.shared) {
3427
+ htypes = sys.board.heaters.getInstalledHeaterTypes(2);
3428
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
2801
3429
  }
3430
+ else sensor.isActive = false;
2802
3431
  break;
2803
- case 'gas':
2804
- if (mode === 'heater') {
2805
- if (body.temp < cfgBody.setPoint) {
2806
- isOn = true;
2807
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
2808
- isHeating = true;
2809
- }
3432
+ case 'solar2':
3433
+ if (sys.equipment.maxBodies > 1 + (sys.equipment.shared ? 1 : 0)) {
3434
+ htypes = sys.board.heaters.getInstalledHeaterTypes(2 + (sys.equipment.shared ? 1 : 0));
3435
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
2810
3436
  }
2811
- else if (mode === 'solarpref' || mode === 'heatpumppref') {
2812
- // If solar should be running gas heater should be off.
2813
- if (body.temp < cfgBody.setPoint &&
2814
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) isOn = false;
2815
- else if (body.temp < cfgBody.setPoint) {
2816
- isOn = true;
2817
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
2818
- isHeating = true;
2819
- }
3437
+ else sensor.isActive = false;
3438
+ break;
3439
+ case 'solar3':
3440
+ if (sys.equipment.maxBodies > 2 + (sys.equipment.shared ? 1 : 0)) {
3441
+ htypes = sys.board.heaters.getInstalledHeaterTypes(3 + (sys.equipment.shared ? 1 : 0));
3442
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
2820
3443
  }
3444
+ else sensor.isActive = false;
2821
3445
  break;
2822
- case 'heatpump':
2823
- if (mode === 'heatpump' || mode === 'heatpumppref') {
2824
- if (body.temp < cfgBody.setPoint &&
2825
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
2826
- isOn = true;
2827
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
2828
- isHeating = true;
2829
- }
3446
+ case 'solar4':
3447
+ if (sys.equipment.maxBodies > 3 + (sys.equipment.shared ? 1 : 0)) {
3448
+ htypes = sys.board.heaters.getInstalledHeaterTypes(4 + (sys.equipment.shared ? 1 : 0));
3449
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
2830
3450
  }
3451
+ else sensor.isActive = false;
2831
3452
  break;
2832
- default:
2833
- isOn = utils.makeBool(hstate.isOn);
3453
+ default:
3454
+ if (typeof sensor.id === 'undefined') sys.equipment.tempSensors.removeItemByIndex(i);
2834
3455
  break;
2835
- }
2836
- logger.debug(`Heater Type: ${htype.name} Mode:${mode} Temp: ${body.temp} Setpoint: ${cfgBody.setPoint} Status: ${body.heatStatus}`);
2837
- }
2838
- if (isOn === true && typeof hon.find(elem => elem === heater.id) === 'undefined') {
2839
- hon.push(heater.id);
2840
- if (heater.master === 1 && isOn) (async () => {
2841
- try {
2842
- await ncp.heaters.setHeaterStateAsync(hstate, isOn);
2843
- } catch (err) { logger.error(err.message); }
2844
- })();
2845
- else hstate.isOn = isOn;
2846
- }
2847
3456
  }
2848
- }
2849
3457
  }
2850
- // When the controller is a virtual one we need to control the heat status ourselves.
2851
- if (!isHeating && (sys.controllerType === ControllerType.Virtual || sys.controllerType === ControllerType.Nixie)) body.heatStatus = 0;
3458
+ }
3459
+ // This updates the heater states based upon the installed heaters. This is true for heaters that are tied to the OCP
3460
+ // and those that are not.
3461
+ public syncHeaterStates() {
3462
+ try {
3463
+ // Go through the installed heaters and bodies to determine whether they should be on. If there is a
3464
+ // heater that is not controlled by the OCP then we need to determine whether it should be on.
3465
+ let heaters = sys.heaters.toArray();
3466
+ let bodies = state.temps.bodies.toArray();
3467
+ let hon = [];
3468
+ for (let i = 0; i < bodies.length; i++) {
3469
+ let body: BodyTempState = bodies[i];
3470
+ let cfgBody: Body = sys.bodies.getItemById(body.id);
3471
+ let isHeating = false;
3472
+ if (body.isOn) {
3473
+ if (typeof body.temp === 'undefined' && heaters.length > 0) logger.warn(`The body temperature for ${body.name} cannot be determined. Heater status for this body cannot be calculated.`);
3474
+ for (let j = 0; j < heaters.length; j++) {
3475
+ let heater: Heater = heaters[j];
3476
+ if (heater.isActive === false) continue;
3477
+ let isOn = false;
3478
+ let isCooling = false;
3479
+ let sensorTemp = state.temps.waterSensor1;
3480
+ if (body.id === 4) sensorTemp = state.temps.waterSensor4;
3481
+ if (body.id === 3) sensorTemp = state.temps.waterSensor3;
3482
+ if (body.id === 2 && !sys.equipment.shared) sensorTemp = state.temps.waterSensor2;
3483
+
3484
+ // Determine whether the heater can be used on this body.
3485
+ let isAssociated = false;
3486
+ let b = sys.board.valueMaps.bodies.transform(heater.body);
3487
+ switch (b.name) {
3488
+ case 'body1':
3489
+ case 'pool':
3490
+ if (body.id === 1) isAssociated = true;
3491
+ break;
3492
+ case 'body2':
3493
+ case 'spa':
3494
+ if (body.id === 2) isAssociated = true;
3495
+ break;
3496
+ case 'poolspa':
3497
+ if (body.id === 1 || body.id === 2) isAssociated = true;
3498
+ break;
3499
+ case 'body3':
3500
+ if (body.id === 3) isAssociated = true;
3501
+ break;
3502
+ case 'body4':
3503
+ if (body.id === 4) isAssociated = true;
3504
+ break;
3505
+ }
3506
+ // logger.silly(`Heater ${heater.name} is ${isAssociated === true ? '' : 'not '}associated with ${body.name}`);
3507
+ if (isAssociated) {
3508
+ let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
3509
+ let status = sys.board.valueMaps.heatStatus.transform(body.heatStatus);
3510
+ let hstate = state.heaters.getItemById(heater.id, true);
3511
+ if (heater.master === 1) {
3512
+ // We need to do our own calculation as to whether it is on. This is for Nixie heaters.
3513
+ let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
3514
+ switch (htype.name) {
3515
+ case 'solar':
3516
+ if (mode === 'solar' || mode === 'solarpref') {
3517
+ // Measure up against start and stop temp deltas for effective solar heating.
3518
+ if (body.temp < cfgBody.heatSetpoint &&
3519
+ state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
3520
+ isOn = true;
3521
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
3522
+ isHeating = true;
3523
+ }
3524
+ else if (heater.coolingEnabled && body.temp > cfgBody.coolSetpoint && state.heliotrope.isNight &&
3525
+ state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
3526
+ isOn = true;
3527
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
3528
+ isHeating = true;
3529
+ isCooling = true;
3530
+ }
3531
+ }
3532
+ break;
3533
+ case 'ultratemp':
3534
+ // We need to determine whether we are going to use the air temp or the solar temp
3535
+ // for the sensor.
3536
+ let deltaTemp = Math.max(state.temps.air, state.temps.solar || 0);
3537
+ if (mode === 'ultratemp' || mode === 'ultratemppref') {
3538
+ if (body.temp < cfgBody.heatSetpoint &&
3539
+ deltaTemp > body.temp + heater.differentialTemp || 0) {
3540
+ isOn = true;
3541
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
3542
+ isHeating = true;
3543
+ isCooling = false;
3544
+ }
3545
+ else if (body.temp > cfgBody.coolSetpoint && heater.coolingEnabled) {
3546
+ isOn = true;
3547
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
3548
+ isHeating = true;
3549
+ isCooling = true;
3550
+ }
3551
+ }
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':
3563
+ case 'gas':
3564
+ if (mode === 'heater') {
3565
+ if (body.temp < cfgBody.setPoint) {
3566
+ isOn = true;
3567
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
3568
+ isHeating = true;
3569
+ }
3570
+ }
3571
+ else if (mode === 'solarpref' || mode === 'heatpumppref') {
3572
+ // If solar should be running gas heater should be off.
3573
+ if (body.temp < cfgBody.setPoint &&
3574
+ state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) isOn = false;
3575
+ else if (body.temp < cfgBody.setPoint) {
3576
+ isOn = true;
3577
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
3578
+ isHeating = true;
3579
+ }
3580
+ }
3581
+ break;
3582
+ case 'heatpump':
3583
+ if (mode === 'heatpump' || mode === 'heatpumppref') {
3584
+ if (body.temp < cfgBody.setPoint &&
3585
+ state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
3586
+ isOn = true;
3587
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
3588
+ isHeating = true;
3589
+ }
3590
+ }
3591
+ break;
3592
+ default:
3593
+ isOn = utils.makeBool(hstate.isOn);
3594
+ break;
3595
+ }
3596
+ logger.debug(`Heater Type: ${htype.name} Mode:${mode} Temp: ${body.temp} Setpoint: ${cfgBody.setPoint} Status: ${body.heatStatus}`);
3597
+ }
3598
+ else {
3599
+ let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
3600
+ switch (htype.name) {
3601
+ case 'mastertemp':
3602
+ if (status === 'mtheater') isHeating = isOn = true;
3603
+ break;
3604
+ case 'maxetherm':
3605
+ case 'gas':
3606
+ if (status === 'heater') isHeating = isOn = true;
3607
+ break;
3608
+ case 'hybrid':
3609
+ case 'ultratemp':
3610
+ case 'heatpump':
3611
+ if (mode === 'ultratemp' || mode === 'ultratemppref' || mode === 'heatpump' || mode === 'heatpumppref') {
3612
+ if (status === 'heater') isHeating = isOn = true;
3613
+ else if (status === 'cooling') isCooling = isOn = true;
3614
+ }
3615
+ break;
3616
+ case 'solar':
3617
+ if (mode === 'solar' || mode === 'solarpref') {
3618
+ if (status === 'solar') isHeating = isOn = true;
3619
+ else if (status === 'cooling') isCooling = isOn = true;
3620
+ }
3621
+ break;
3622
+ }
3623
+ }
3624
+ if (isOn === true && typeof hon.find(elem => elem === heater.id) === 'undefined') {
3625
+ hon.push(heater.id);
3626
+ if (heater.master === 1 && isOn) (async () => {
3627
+ try {
3628
+ await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
3629
+ } catch (err) { logger.error(err.message); }
3630
+ })();
3631
+ else hstate.isOn = isOn;
3632
+ }
3633
+ }
3634
+ }
3635
+ }
3636
+ // When the controller is a virtual one we need to control the heat status ourselves.
3637
+ if (!isHeating && (sys.controllerType === ControllerType.Nixie)) body.heatStatus = 0;
3638
+ }
3639
+ // Turn off any heaters that should be off. The code above only turns heaters on.
3640
+ for (let i = 0; i < heaters.length; i++) {
3641
+ let heater: Heater = heaters[i];
3642
+ if (typeof hon.find(elem => elem === heater.id) === 'undefined') {
3643
+ let hstate = state.heaters.getItemById(heater.id, true);
3644
+ if (heater.master === 1) (async () => {
3645
+ try {
3646
+ await ncp.heaters.setHeaterStateAsync(hstate, false, false);
3647
+ } catch (err) { logger.error(err.message); }
3648
+ })();
3649
+ else hstate.isOn = false;
3650
+ }
3651
+ }
3652
+ } catch (err) { logger.error(`Error synchronizing heater states`); }
3653
+ }
3654
+ }
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}`); }
2852
3665
  }
2853
- // Turn off any heaters that should be off. The code above only turns heaters on.
2854
- for (let i = 0; i < heaters.length; i++) {
2855
- let heater: Heater = heaters[i];
2856
- if (typeof hon.find(elem => elem === heater.id) === 'undefined') {
2857
- let hstate = state.heaters.getItemById(heater.id, true);
2858
- if (heater.master === 1) (async () => {
2859
- try {
2860
- await ncp.heaters.setHeaterStateAsync(hstate, false);
2861
- } catch (err) { logger.error(err.message); }
2862
- })();
2863
- else hstate.isOn = false;
2864
- }
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}`); }
2865
3672
  }
2866
- } catch (err) { logger.error(`Error synchronizing heater states`); }
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; }
2867
3685
  }
2868
- }
2869
- export class ValveCommands extends BoardCommands {
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
+
2870
3707
  public async setValveStateAsync(valve: Valve, vstate: ValveState, isDiverted: boolean) {
2871
3708
  if (valve.master === 1) await ncp.valves.setValveStateAsync(vstate, isDiverted);
2872
3709
  else
@@ -2944,6 +3781,60 @@ export class ValveCommands extends BoardCommands {
2944
3781
  }
2945
3782
  }
2946
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
+
2947
3838
  public async deleteChemControllerAsync(data: any): Promise<ChemController> {
2948
3839
  try {
2949
3840
  let id = typeof data.id !== 'undefined' ? parseInt(data.id, 10) : -1;
@@ -3082,12 +3973,62 @@ export class ChemControllerCommands extends BoardCommands {
3082
3973
  }
3083
3974
  }
3084
3975
  export class FilterCommands extends BoardCommands {
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];
3981
+ try {
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];
3989
+ try {
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
+
3085
4026
  public async syncFilterStates() {
3086
4027
  try {
3087
4028
  for (let i = 0; i < sys.filters.length; i++) {
3088
4029
  // Run through all the valves to see whether they should be triggered or not.
3089
4030
  let filter = sys.filters.getItemByIndex(i);
3090
- if (filter.isActive) {
4031
+ if (filter.isActive && !isNaN(filter.id)) {
3091
4032
  let fstate = state.filters.getItemById(filter.id, true);
3092
4033
  // Check to see if the associated body is on.
3093
4034
  await sys.board.filters.setFilterStateAsync(filter, fstate, sys.board.bodies.isBodyOn(filter.body));
@@ -3095,8 +4036,84 @@ export class FilterCommands extends BoardCommands {
3095
4036
  }
3096
4037
  } catch (err) { logger.error(`syncFilterStates: Error synchronizing filters ${err.message}`); }
3097
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;
4093
+ }
4094
+ }
4095
+ }
4096
+ }
4097
+ }
4098
+ if (!fon) {
4099
+ // Finally we have a value we can believe in.
4100
+ sfilter.refPressure = pressure;
4101
+ }
4102
+ }
4103
+ else {
4104
+ logger.verbose(`Circuit ${con.id}-${con.name} is currently on filter pressure for cleaning ignored.`);
4105
+ }
4106
+ }
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();
4112
+ }
4113
+ catch (err) { logger.error(`setFilterPressure: Error setting filter #${id} pressure to ${pressure}${units || ''}`); }
4114
+ }
3098
4115
  public async setFilterStateAsync(filter: Filter, fstate: FilterState, isOn: boolean) { fstate.isOn = isOn; }
3099
- public setFilter(data: any): any {
4116
+ public async setFilterAsync(data: any): Promise<Filter> {
3100
4117
  let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
3101
4118
  if (id <= 0) id = sys.filters.length + 1; // set max filters?
3102
4119
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid filter id: ${data.id}`, data.id, 'Filter'));
@@ -3105,50 +4122,48 @@ export class FilterCommands extends BoardCommands {
3105
4122
  let filterType = typeof data.filterType !== 'undefined' ? parseInt(data.filterType, 10) : filter.filterType;
3106
4123
  if (typeof filterType === 'undefined') filterType = sys.board.valueMaps.filterTypes.getValue('unknown');
3107
4124
 
3108
- if (typeof data.isActive !== 'undefined') {
3109
- if (utils.makeBool(data.isActive) === false) {
3110
- sys.filters.removeItemById(id);
3111
- state.filters.removeItemById(id);
3112
- return;
3113
- }
3114
- }
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
+ //}
3115
4133
 
3116
4134
  let body = typeof data.body !== 'undefined' ? data.body : filter.body;
3117
4135
  let name = typeof data.name !== 'undefined' ? data.name : filter.name;
3118
-
3119
- let psi = typeof data.psi !== 'undefined' ? parseFloat(data.psi) : sfilter.psi;
3120
- let lastCleanDate = typeof data.lastCleanDate !== 'undefined' ? data.lastCleanDate : sfilter.lastCleanDate;
3121
- let filterPsi = typeof data.filterPsi !== 'undefined' ? parseInt(data.filterPsi, 10) : sfilter.filterPsi;
3122
- let needsCleaning = typeof data.needsCleaning !== 'undefined' ? data.needsCleaning : sfilter.needsCleaning;
3123
-
3124
- // Ensure all the defaults.
3125
- if (isNaN(psi)) psi = 0;
3126
4136
  if (typeof body === 'undefined') body = 32;
3127
-
3128
4137
  // At this point we should have all the data. Validate it.
3129
4138
  if (!sys.board.valueMaps.filterTypes.valExists(filterType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid filter type; ${filterType}`, 'Filter', filterType));
3130
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
+
3131
4145
  filter.filterType = sfilter.filterType = filterType;
3132
4146
  filter.body = sfilter.body = body;
3133
- filter.filterType = sfilter.filterType = filterType;
3134
4147
  filter.name = sfilter.name = name;
3135
4148
  filter.capacity = typeof data.capacity === 'number' ? data.capacity : filter.capacity;
3136
4149
  filter.capacityUnits = typeof data.capacityUnits !== 'undefined' ? data.capacityUnits : filter.capacity;
3137
- sfilter.psi = psi;
3138
- sfilter.filterPsi = filterPsi;
3139
- filter.needsCleaning = sfilter.needsCleaning = needsCleaning;
3140
- filter.lastCleanDate = sfilter.lastCleanDate = lastCleanDate;
3141
4150
  filter.connectionId = typeof data.connectionId !== 'undefined' ? data.connectionId : filter.connectionId;
3142
4151
  filter.deviceBinding = typeof data.deviceBinding !== 'undefined' ? data.deviceBinding : filter.deviceBinding;
4152
+ sfilter.pressureUnits = filter.pressureUnits;
4153
+ sfilter.calcCleanPercentage();
3143
4154
  sfilter.emitEquipmentChange();
3144
4155
  return filter; // Always return the config when we are dealing with the config not state.
3145
4156
  }
3146
-
3147
- public deleteFilter(data: any): any {
3148
- let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
3149
- if (isNaN(id)) return;
3150
- sys.filters.removeItemById(id);
3151
- state.filters.removeItemById(id);
3152
- return state.filters.getItemById(id);
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}`); }
3153
4168
  }
3154
- }
4169
+ }