nodejs-poolcontroller 7.7.0 → 8.0.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 (82) hide show
  1. package/.eslintrc.json +26 -35
  2. package/Changelog +22 -0
  3. package/README.md +7 -3
  4. package/anslq25/MessagesMock.ts +218 -0
  5. package/anslq25/boards/MockBoardFactory.ts +50 -0
  6. package/anslq25/boards/MockEasyTouchBoard.ts +696 -0
  7. package/anslq25/boards/MockSystemBoard.ts +217 -0
  8. package/anslq25/chemistry/MockChlorinator.ts +75 -0
  9. package/anslq25/pumps/MockPump.ts +84 -0
  10. package/app.ts +10 -14
  11. package/config/Config.ts +13 -9
  12. package/config/VersionCheck.ts +6 -2
  13. package/controller/Constants.ts +58 -25
  14. package/controller/Equipment.ts +225 -41
  15. package/controller/Errors.ts +2 -1
  16. package/controller/Lockouts.ts +34 -2
  17. package/controller/State.ts +491 -48
  18. package/controller/boards/AquaLinkBoard.ts +6 -3
  19. package/controller/boards/BoardFactory.ts +5 -1
  20. package/controller/boards/EasyTouchBoard.ts +1971 -1751
  21. package/controller/boards/IntelliCenterBoard.ts +1311 -1688
  22. package/controller/boards/IntelliComBoard.ts +7 -1
  23. package/controller/boards/IntelliTouchBoard.ts +153 -42
  24. package/controller/boards/NixieBoard.ts +209 -66
  25. package/controller/boards/SunTouchBoard.ts +393 -0
  26. package/controller/boards/SystemBoard.ts +1862 -1543
  27. package/controller/comms/Comms.ts +539 -138
  28. package/controller/comms/ScreenLogic.ts +1663 -0
  29. package/controller/comms/messages/Messages.ts +242 -60
  30. package/controller/comms/messages/config/ChlorinatorMessage.ts +4 -3
  31. package/controller/comms/messages/config/CircuitGroupMessage.ts +5 -2
  32. package/controller/comms/messages/config/CircuitMessage.ts +81 -13
  33. package/controller/comms/messages/config/ConfigMessage.ts +3 -1
  34. package/controller/comms/messages/config/CoverMessage.ts +2 -1
  35. package/controller/comms/messages/config/CustomNameMessage.ts +2 -1
  36. package/controller/comms/messages/config/EquipmentMessage.ts +5 -1
  37. package/controller/comms/messages/config/ExternalMessage.ts +33 -3
  38. package/controller/comms/messages/config/FeatureMessage.ts +2 -1
  39. package/controller/comms/messages/config/GeneralMessage.ts +2 -1
  40. package/controller/comms/messages/config/HeaterMessage.ts +3 -1
  41. package/controller/comms/messages/config/IntellichemMessage.ts +2 -1
  42. package/controller/comms/messages/config/OptionsMessage.ts +12 -6
  43. package/controller/comms/messages/config/PumpMessage.ts +9 -12
  44. package/controller/comms/messages/config/RemoteMessage.ts +80 -13
  45. package/controller/comms/messages/config/ScheduleMessage.ts +43 -3
  46. package/controller/comms/messages/config/SecurityMessage.ts +2 -1
  47. package/controller/comms/messages/config/ValveMessage.ts +43 -26
  48. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +8 -7
  49. package/controller/comms/messages/status/EquipmentStateMessage.ts +93 -20
  50. package/controller/comms/messages/status/HeaterStateMessage.ts +24 -5
  51. package/controller/comms/messages/status/IntelliChemStateMessage.ts +7 -4
  52. package/controller/comms/messages/status/IntelliValveStateMessage.ts +2 -1
  53. package/controller/comms/messages/status/PumpStateMessage.ts +72 -4
  54. package/controller/comms/messages/status/VersionMessage.ts +2 -1
  55. package/controller/nixie/Nixie.ts +15 -4
  56. package/controller/nixie/NixieEquipment.ts +1 -0
  57. package/controller/nixie/chemistry/ChemController.ts +300 -129
  58. package/controller/nixie/chemistry/ChemDoser.ts +806 -0
  59. package/controller/nixie/chemistry/Chlorinator.ts +133 -129
  60. package/controller/nixie/circuits/Circuit.ts +171 -30
  61. package/controller/nixie/heaters/Heater.ts +337 -173
  62. package/controller/nixie/pumps/Pump.ts +264 -236
  63. package/controller/nixie/schedules/Schedule.ts +9 -3
  64. package/defaultConfig.json +46 -5
  65. package/logger/Logger.ts +38 -9
  66. package/package.json +13 -9
  67. package/web/Server.ts +235 -122
  68. package/web/bindings/aqualinkD.json +114 -59
  69. package/web/bindings/homeassistant.json +437 -0
  70. package/web/bindings/influxDB.json +15 -0
  71. package/web/bindings/mqtt.json +28 -9
  72. package/web/bindings/mqttAlt.json +15 -0
  73. package/web/interfaces/baseInterface.ts +58 -7
  74. package/web/interfaces/httpInterface.ts +5 -2
  75. package/web/interfaces/influxInterface.ts +9 -2
  76. package/web/interfaces/mqttInterface.ts +234 -74
  77. package/web/interfaces/ruleInterface.ts +87 -0
  78. package/web/services/config/Config.ts +140 -33
  79. package/web/services/config/ConfigSocket.ts +2 -1
  80. package/web/services/state/State.ts +144 -3
  81. package/web/services/state/StateSocket.ts +65 -14
  82. package/web/services/utilities/Utilities.ts +189 -1
@@ -1,5 +1,6 @@
1
1
  /* nodejs-poolController. An application to control pool equipment.
2
- Copyright (C) 2016, 2017, 2018, 2019, 2020. Russell Goldin, tagyoureit. russ.goldin@gmail.com
2
+ Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
+ Russell Goldin, tagyoureit. russ.goldin@gmail.com
3
4
 
4
5
  This program is free software: you can redistribute it and/or modify
5
6
  it under the terms of the GNU Affero General Public License as
@@ -18,12 +19,13 @@ import * as extend from 'extend';
18
19
  import { logger } from '../../logger/Logger';
19
20
  import { Message, Outbound } from '../comms/messages/Messages';
20
21
  import { Timestamp, utils } from '../Constants';
21
- import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, ControllerType, CustomName, CustomNameCollection, EggTimer, Equipment, Feature, Filter, General, Heater, ICircuit, ICircuitGroup, ICircuitGroupCircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, TempSensorCollection, Valve } from '../Equipment';
22
+ import { Body, ChemController, ChemDoser, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, ControllerType, CustomName, CustomNameCollection, EggTimer, Equipment, Feature, Filter, General, Heater, ICircuit, ICircuitGroup, ICircuitGroupCircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, TempSensorCollection, Valve } from '../Equipment';
22
23
  import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, BoardProcessError, InvalidOperationError } from '../Errors';
23
24
  import { ncp } from "../nixie/Nixie";
24
- import { BodyTempState, ChemControllerState, ChlorinatorState, CircuitGroupState, FilterState, ICircuitGroupState, ICircuitState, LightGroupState, ScheduleState, state, TemperatureState, ValveState, VirtualCircuitState } from '../State';
25
+ import { BodyTempState, ChemControllerState, ChemDoserState, ChlorinatorState, CircuitGroupState, FilterState, ICircuitGroupState, ICircuitState, LightGroupState, ScheduleState, state, TemperatureState, ValveState, VirtualCircuitState } from '../State';
25
26
  import { RestoreResults } from '../../web/Server';
26
- import { group } from 'console';
27
+ import { setTimeout } from 'timers/promises';
28
+ import { setTimeout as setTimeoutSync } from 'timers';
27
29
 
28
30
 
29
31
  export class byteValueMap extends Map<number, any> {
@@ -265,7 +267,8 @@ export class byteValueMaps {
265
267
  [134, { name: 'heatEnable', desc: 'Heat Enable', assignableToPumpCircuit: false }],
266
268
  [135, { name: 'pumpSpeedUp', desc: 'Pump Speed +', assignableToPumpCircuit: false }],
267
269
  [136, { name: 'pumpSpeedDown', desc: 'Pump Speed -', assignableToPumpCircuit: false }],
268
- [255, { name: 'notused', desc: 'NOT USED', assignableToPumpCircuit: true }]
270
+ [255, { name: 'notused', desc: 'NOT USED', assignableToPumpCircuit: true }],
271
+ [258, { name: 'anyHeater', desc: 'Any Heater' }],
269
272
  ]);
270
273
  public lightThemes: byteValueMap = new byteValueMap([
271
274
  [0, { name: 'off', desc: 'Off' }],
@@ -330,7 +333,16 @@ export class byteValueMaps {
330
333
  { isOn: true, timeout: 100 },
331
334
  { isOn: false, timeout: 100 }
332
335
  ]
333
- }]
336
+ }],
337
+ [7, {
338
+ name: 'colorsync', desc: 'Sync', types: ['colorlogic'], command: 'colorSync', message: 'Synchronizing Lights', endingTheme: 'voodoolounge',
339
+ sequence: [
340
+ { isOn: true, timeout: 1000 },
341
+ { isOn: false, timeout: 12000 },
342
+ { isOn: true }
343
+ ]
344
+ }],
345
+ [100, { name: 'settheme', types: ['all'], desc: 'Set Theme', message: 'Sequencing Theme' }]
334
346
  ]);
335
347
  public lightGroupCommands = new byteValueMap([
336
348
  [1, { name: 'colorsync', desc: 'Sync', types: ['intellibrite'], command: 'colorSync', message:'Synchronizing' }],
@@ -364,7 +376,8 @@ export class byteValueMaps {
364
376
  [4, { name: 'lighttheme', desc: 'Sequencing Theme/Color Operation' }],
365
377
  [5, { name: 'colorhold', desc: 'Saving Current Color' }],
366
378
  [6, { name: 'colorrecall', desc: 'Recalling Saved Color' }],
367
- [7, { name: 'lightthumper', desc: 'Setting Light Thumper' }]
379
+ [7, { name: 'lightthumper', desc: 'Setting Light Thumper' }],
380
+ [100, { name: 'settheme', desc: 'Setting Light Theme' }]
368
381
  ]);
369
382
  public lightColors: byteValueMap = new byteValueMap([
370
383
  [0, { name: 'white', desc: 'White' }],
@@ -458,13 +471,15 @@ export class byteValueMaps {
458
471
  [21, { name: 'solarpref', desc: 'Solar Preferred' }],
459
472
  [32, { name: 'nochange', desc: 'No Change' }]
460
473
  ]);
461
- public heatStatus: byteValueMap = new byteValueMap([
462
- [0, { name: 'off', desc: 'Off' }],
463
- [1, { name: 'heater', desc: 'Heater' }],
464
- [2, { name: 'solar', desc: 'Solar' }],
465
- [3, { name: 'cooling', desc: 'Cooling' }],
466
- [128, { name: 'cooldown', desc: 'Cooldown' }]
467
- ]);
474
+ public heatStatus: byteValueMap = new byteValueMap([
475
+ [0, { name: 'off', desc: 'Off' }],
476
+ [1, { name: 'heater', desc: 'Heater' }],
477
+ [2, { name: 'solar', desc: 'Solar' }],
478
+ [3, { name: 'cooling', desc: 'Cooling' }],
479
+ [4, { name: 'hpheat', desc: 'Heatpump' }],
480
+ [5, { name: 'dual', desc: 'Dual' }],
481
+ [128, { name: 'cooldown', desc: 'Cooldown' }]
482
+ ]);
468
483
  public pumpStatus: byteValueMap = new byteValueMap([
469
484
  [0, { name: 'off', desc: 'Off' }], // When the pump is disconnected or has no power then we simply report off as the status. This is not the recommended wiring
470
485
  // for a VS/VF pump as is should be powered at all times. When it is, the status will always report a value > 0.
@@ -573,6 +588,10 @@ export class byteValueMaps {
573
588
  [2, { name: 'status', desc: 'Equipment Status' }],
574
589
  [82, { name: 'ivstatus', desc: 'IntelliValve Status' }]
575
590
  ]);
591
+ public chemDoserTypes: byteValueMap = new byteValueMap([
592
+ [0, { name: 'acid', desc: 'Acid' }],
593
+ [1, { name: 'chlor', desc: 'Chlorine' }]
594
+ ]);
576
595
  public chemControllerTypes: byteValueMap = new byteValueMap([
577
596
  [0, { name: 'none', desc: 'None', ph: { min: 6.8, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: false }],
578
597
  [1, { name: 'unknown', desc: 'Unknown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }],
@@ -587,7 +606,7 @@ export class byteValueMaps {
587
606
  public chemPumpTypes: byteValueMap = new byteValueMap([
588
607
  [0, { name: 'none', desc: 'No Pump', ratedFlow: false, tank: false, remAddress: false }],
589
608
  [1, { name: 'relay', desc: 'Relay Pump', ratedFlow: true, tank: true, remAddress: true }],
590
- [2, { name: 'ezo-pmp', desc: 'Altas EZO-PMP', ratedFlow: false, tank: false, remAddress: true }]
609
+ [2, { name: 'ezo-pmp', desc: 'Altas EZO-PMP', ratedFlow: true, tank: true, remAddress: true }]
591
610
  ]);
592
611
  public chemPhProbeTypes: byteValueMap = new byteValueMap([
593
612
  [0, { name: 'none', desc: 'No Probe' }],
@@ -658,6 +677,43 @@ export class byteValueMaps {
658
677
  [1, { name: 'nocomms', desc: 'No Communication' }],
659
678
  [2, { name: 'config', desc: 'Invalid Configuration' }]
660
679
  ]);
680
+ public chemDoserStatus: byteValueMap = new byteValueMap([
681
+ [0, { name: 'ok', desc: 'Ok' }],
682
+ [1, { name: 'nocomms', desc: 'No Communication' }],
683
+ [2, { name: 'config', desc: 'Invalid Configuration' }]
684
+ ]);
685
+ public chemDoserHardwareFaults: byteValueMap = new byteValueMap([
686
+ [0, { name: 'ok', desc: 'Ok - No Faults' }],
687
+ [2, { name: 'pump', desc: 'Pump Fault' }],
688
+ [5, { name: 'chlormismatch', desc: 'Chlorinator body mismatch' }],
689
+ [6, { name: 'invalidbody', desc: 'Body capacity not valid' }],
690
+ [7, { name: 'flowsensor', desc: 'Flow Sensor Fault' }]
691
+ ]);
692
+ public chemDoserAlarms: byteValueMap = new byteValueMap([
693
+ [0, { name: 'ok', desc: 'Ok - No alarm' }],
694
+ [1, { name: 'noflow', desc: 'No Flow Detected' }],
695
+ [32, { name: 'tankempty', desc: 'Tank Empty' }],
696
+ [129, { name: 'tanklow', desc: 'Tank Low' }],
697
+ [131, { name: 'freezeprotect', desc: 'Freeze Protection Lockout' }]
698
+ ]);
699
+ public chemDoserWarnings: byteValueMap = new byteValueMap([
700
+ [0, { name: 'ok', desc: 'Ok - No Warning' }],
701
+ [8, { name: 'invalidsetup', desc: 'Invalid Setup' }],
702
+ [16, { name: 'chlorinatorComms', desc: 'Chlorinator Comms Error' }]
703
+ ]);
704
+ public chemDoserLimits: byteValueMap = new byteValueMap([
705
+ [0, { name: 'ok', desc: 'Ok - No limits reached' }],
706
+ [1, { name: 'lockout', desc: 'Lockout - Chemical will not dose' }],
707
+ [2, { name: 'dailylimit', desc: 'Daily Limit Reached' }],
708
+ [128, { name: 'commslost', desc: 'Communications with Chem Doser Lost' }]
709
+ ]);
710
+ public chemDoserDosingStatus: byteValueMap = new byteValueMap([
711
+ [0, { name: 'dosing', desc: 'Dosing' }],
712
+ [1, { name: 'mixing', desc: 'Mixing' }],
713
+ [2, { name: 'monitoring', desc: 'Monitoring' }]
714
+ ]);
715
+
716
+
661
717
  public chemControllerAlarms: byteValueMap = new byteValueMap([
662
718
  [0, { name: 'ok', desc: 'Ok - No alarm' }],
663
719
  [1, { name: 'noflow', desc: 'No Flow Detected' }],
@@ -681,7 +737,6 @@ export class byteValueMaps {
681
737
  [5, { name: 'chlormismatch', desc: 'Chlorinator body mismatch' }],
682
738
  [6, { name: 'invalidbody', desc: 'Body capacity not valid' }],
683
739
  [7, { name: 'flowsensor', desc: 'Flow Sensor Fault' }]
684
-
685
740
  ]);
686
741
  public chemControllerWarnings: byteValueMap = new byteValueMap([
687
742
  [0, { name: 'ok', desc: 'Ok - No Warning' }],
@@ -769,12 +824,12 @@ export class byteValueMaps {
769
824
  ]);
770
825
  public eqMessageSeverities: byteValueMap = new byteValueMap([
771
826
  [-1, { name: 'unspecified', desc: 'Unspecified' }],
772
- [0, { name: 'info', desc: 'Information' }],
773
- [1, { name: 'reminder', desc: 'Reminder' }],
774
- [2, { name: 'alert', desc: 'Alert' }],
775
- [3, { name: 'warning', desc: 'Warning' }],
776
- [4, { name: 'error', desc: 'Error' }],
777
- [5, { name: 'fatal', desc: 'Fatal' }]
827
+ [0, { name: 'info', desc: 'Information', icon: 'fas fa-circle-info' }],
828
+ [1, { name: 'reminder', desc: 'Reminder', icon: 'fas fa-bell' }],
829
+ [2, { name: 'alert', desc: 'Alert', icon: 'fas fa-circle-exclamation' }],
830
+ [3, { name: 'warning', desc: 'Warning', icon: 'fas fa-circle-exclamation' }],
831
+ [4, { name: 'error', desc: 'Error', icon: 'fas fa-triangle-exclamation' }],
832
+ [5, { name: 'fatal', desc: 'Fatal', icon: 'fas fa-skull-crossbones' }]
778
833
  ]);
779
834
  // need to validate these...
780
835
  public delay: byteValueMap = new byteValueMap([
@@ -826,20 +881,19 @@ export class SystemBoard {
826
881
  public async turnOffAllCircuits() {
827
882
  // turn off all circuits/features
828
883
  for (let i = 0; i < state.circuits.length; i++) {
829
- let s = state.circuits.getItemByIndex(i);
830
- await sys.board.circuits.setCircuitStateAsync(s.id, false);
884
+ let s = state.circuits.getItemByIndex(i)
885
+ s.isOn = s.manualPriorityActive = false;
831
886
  }
832
887
  for (let i = 0; i < state.features.length; i++) {
833
- let s = state.features.getItemByIndex(i);
834
- await sys.board.features.setFeatureStateAsync(s.id, false);
888
+ let s = state.features.getItemByIndex(i)
889
+ s.isOn = s.manualPriorityActive = false;
835
890
  }
836
891
  for (let i = 0; i < state.lightGroups.length; i++) {
837
- let s = state.lightGroups.getItemByIndex(i);
838
- await sys.board.circuits.setCircuitStateAsync(s.id, false);
892
+ let s = state.lightGroups.getItemByIndex(i)
893
+ s.isOn = s.manualPriorityActive = false;
839
894
  }
840
895
  for (let i = 0; i < state.temps.bodies.length; i++) {
841
- let s = state.temps.bodies.getItemByIndex(i);
842
- await sys.board.circuits.setCircuitStateAsync(s.id, false);
896
+ state.temps.bodies.getItemByIndex(i).isOn = false;
843
897
  }
844
898
  // sys.board.virtualPumpControllers.setTargetSpeed();
845
899
  state.emitEquipmentChanges();
@@ -853,8 +907,8 @@ export class SystemBoard {
853
907
  public chlorinator: ChlorinatorCommands = new ChlorinatorCommands(this);
854
908
  public heaters: HeaterCommands = new HeaterCommands(this);
855
909
  public filters: FilterCommands = new FilterCommands(this);
856
- public chemControllers: ChemControllerCommands = new ChemControllerCommands(this);
857
-
910
+ public chemControllers: ChemControllerCommands = new ChemControllerCommands(this);
911
+ public chemDosers: ChemDoserCommands = new ChemDoserCommands(this);
858
912
  public schedules: ScheduleCommands = new ScheduleCommands(this);
859
913
  public equipmentIds: EquipmentIds = new EquipmentIds();
860
914
  //public virtualChlorinatorController = new VirtualChlorinatorController(this);
@@ -879,14 +933,14 @@ export class SystemBoard {
879
933
  this._statusTimer = undefined;
880
934
  this._statusCheckRef = 0;
881
935
  }
882
- public suspendStatus(bSuspend: boolean) {
883
- // The way status suspension works is by using a reference value that is incremented and decremented
884
- // the status check is only performed when the reference value is 0. So suspending the status check 3 times and un-suspending
885
- // it 2 times will still result in the status check being suspended. This method also ensures the reference never falls below 0.
886
- if (bSuspend) this._statusCheckRef++;
887
- else this._statusCheckRef = Math.max(0, this._statusCheckRef - 1);
888
- logger.verbose(`Suspending status check: ${bSuspend} -- ${this._statusCheckRef}`);
889
- }
936
+ public suspendStatus(bSuspend: boolean) {
937
+ // The way status suspension works is by using a reference value that is incremented and decremented
938
+ // the status check is only performed when the reference value is 0. So suspending the status check 3 times and un-suspending
939
+ // it 2 times will still result in the status check being suspended. This method also ensures the reference never falls below 0.
940
+ if (bSuspend) this._statusCheckRef++;
941
+ else this._statusCheckRef = Math.max(0, this._statusCheckRef - 1);
942
+ if (this._statusCheckRef > 1) logger.verbose(`Suspending status check: ${bSuspend} -- ${this._statusCheckRef}`);
943
+ }
890
944
  /// This method processes the status message periodically. The role of this method is to verify the circuit, valve, and heater
891
945
  /// relays. This method does not control RS485 operations such as pumps and chlorinators. These are done through the respective
892
946
  /// equipment polling functions.
@@ -907,20 +961,20 @@ export class SystemBoard {
907
961
  } catch (err) { state.status = 255; logger.error(`Error performing processStatusAsync ${err.message}`); }
908
962
  finally {
909
963
  this.suspendStatus(false);
910
- if (this.statusInterval > 0) this._statusTimer = setTimeout(async () => await self.processStatusAsync(), this.statusInterval);
964
+ if (this.statusInterval > 0) this._statusTimer = setTimeoutSync(async () => await self.processStatusAsync(), this.statusInterval);
911
965
  }
912
966
  }
913
- public async syncEquipmentItems() {
914
- try {
915
- await sys.board.circuits.syncCircuitRelayStates();
916
- await sys.board.features.syncGroupStates();
917
- await sys.board.circuits.syncVirtualCircuitStates();
918
- await sys.board.valves.syncValveStates();
919
- await sys.board.filters.syncFilterStates();
920
- await sys.board.heaters.syncHeaterStates();
967
+ public async syncEquipmentItems() {
968
+ try {
969
+ await sys.board.circuits.syncCircuitRelayStates();
970
+ await sys.board.features.syncGroupStates();
971
+ await sys.board.circuits.syncVirtualCircuitStates();
972
+ await sys.board.valves.syncValveStates();
973
+ await sys.board.filters.syncFilterStates();
974
+ await sys.board.heaters.syncHeaterStates();
975
+ }
976
+ catch (err) { logger.error(`Error synchronizing equipment items: ${err.message}`); }
921
977
  }
922
- catch (err) { logger.error(`Error synchronizing equipment items: ${err.message}`); }
923
- }
924
978
  public async setControllerType(obj): Promise<Equipment> {
925
979
  try {
926
980
  if (obj.controllerType !== sys.controllerType)
@@ -998,347 +1052,413 @@ export class BoardCommands {
998
1052
  constructor(parent: SystemBoard) { this.board = parent; }
999
1053
  }
1000
1054
  export class SystemCommands extends BoardCommands {
1001
- public async restore(rest: { poolConfig: any, poolState: any }): Promise<RestoreResults> {
1002
- let res = new RestoreResults();
1003
- try {
1004
- let ctx = await sys.board.system.validateRestore(rest);
1005
- // Restore the general stuff.
1006
- if (ctx.general.update.length > 0) await sys.board.system.setGeneralAsync(ctx.general.update[0]);
1007
- for (let i = 0; i < ctx.customNames.update.length; i++) {
1008
- let cn = ctx.customNames.update[i];
1055
+ public async restore(rest: { poolConfig: any, poolState: any }): Promise<RestoreResults> {
1056
+ let res = new RestoreResults();
1009
1057
  try {
1010
- await sys.board.system.setCustomNameAsync(cn);
1011
- res.addModuleSuccess('customName', `Update: ${cn.id}-${cn.name}`);
1012
- } catch (err) { res.addModuleError('customName', `Update: ${cn.id}-${cn.name}: ${err.message}`); }
1013
- }
1014
- for (let i = 0; i < ctx.customNames.add.length; i++) {
1015
- let cn = ctx.customNames.add[i];
1058
+ let ctx = await sys.board.system.validateRestore(rest);
1059
+ // Restore the general stuff.
1060
+ if (ctx.general.update.length > 0) await sys.board.system.setGeneralAsync(ctx.general.update[0]);
1061
+ for (let i = 0; i < ctx.customNames.update.length; i++) {
1062
+ let cn = ctx.customNames.update[i];
1063
+ try {
1064
+ await sys.board.system.setCustomNameAsync(cn);
1065
+ res.addModuleSuccess('customName', `Update: ${cn.id}-${cn.name}`);
1066
+ } catch (err) { res.addModuleError('customName', `Update: ${cn.id}-${cn.name}: ${err.message}`); }
1067
+ }
1068
+ for (let i = 0; i < ctx.customNames.add.length; i++) {
1069
+ let cn = ctx.customNames.add[i];
1070
+ try {
1071
+ await sys.board.system.setCustomNameAsync(cn);
1072
+ res.addModuleSuccess('customName', `Add: ${cn.id}-${cn.name}`);
1073
+ } catch (err) { res.addModuleError('customName', `Add: ${cn.id}-${cn.name}: ${err.message}`); }
1074
+ }
1075
+ await sys.board.bodies.restore(rest, ctx, res);
1076
+ await sys.board.filters.restore(rest, ctx, res);
1077
+ await sys.board.circuits.restore(rest, ctx, res);
1078
+ await sys.board.heaters.restore(rest, ctx, res);
1079
+ await sys.board.features.restore(rest, ctx, res);
1080
+ await sys.board.pumps.restore(rest, ctx, res);
1081
+ await sys.board.valves.restore(rest, ctx, res);
1082
+ await sys.board.chlorinator.restore(rest, ctx, res);
1083
+ await sys.board.chemControllers.restore(rest, ctx, res);
1084
+ await sys.board.schedules.restore(rest, ctx, res);
1085
+ return res;
1086
+ //await sys.board.covers.restore(rest, ctx);
1087
+ } catch (err) { logger.error(`Error restoring njsPC server: ${err.message}`); res.addModuleError('system', err.message); return Promise.reject(err); }
1088
+ }
1089
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<any> {
1016
1090
  try {
1017
- await sys.board.system.setCustomNameAsync(cn);
1018
- res.addModuleSuccess('customName', `Add: ${cn.id}-${cn.name}`);
1019
- } catch (err) { res.addModuleError('customName', `Add: ${cn.id}-${cn.name}: ${err.message}`); }
1020
- }
1021
- await sys.board.bodies.restore(rest, ctx, res);
1022
- await sys.board.filters.restore(rest, ctx, res);
1023
- await sys.board.circuits.restore(rest, ctx, res);
1024
- await sys.board.heaters.restore(rest, ctx, res);
1025
- await sys.board.features.restore(rest, ctx, res);
1026
- await sys.board.pumps.restore(rest, ctx, res);
1027
- await sys.board.valves.restore(rest, ctx, res);
1028
- await sys.board.chlorinator.restore(rest, ctx, res);
1029
- await sys.board.chemControllers.restore(rest, ctx, res);
1030
- await sys.board.schedules.restore(rest, ctx, res);
1031
- return res;
1032
- //await sys.board.covers.restore(rest, ctx);
1033
- } catch (err) { logger.error(`Error restoring njsPC server: ${err.message}`); res.addModuleError('system', err.message); return Promise.reject(err);}
1034
- }
1035
- public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<any> {
1036
- try {
1037
- let ctx: any = { board: { errors: [], warnings: [] } };
1091
+ let ctx: any = { board: { errors: [], warnings: [] } };
1038
1092
 
1039
- // Step 1 - Verify that the boards are the same. For instance you do not want to restore an IntelliTouch to an IntelliCenter.
1040
- let cfg = rest.poolConfig;
1041
- if (sys.controllerType === cfg.controllerType) {
1042
- ctx.customNames = { errors: [], warnings: [], add: [], update: [], remove: [] };
1043
- let customNames = sys.customNames.get();
1044
- for (let i = 0; i < rest.poolConfig.customNames.length; i++) {
1045
- let cn = customNames.find(elem => elem.id === rest.poolConfig.customNames[i].id);
1046
- if (typeof cn === 'undefined') ctx.customNames.add.push(rest.poolConfig.customNames[i]);
1047
- else if (JSON.stringify(rest.poolConfig.customNames[i]) !== JSON.stringify(cn)) ctx.customNames.update.push(cn);
1048
- }
1049
- ctx.general = { errors: [], warnings: [], add: [], update: [], remove: [] };
1050
- if (JSON.stringify(sys.general.get()) !== JSON.stringify(cfg.pool)) ctx.general.update.push(cfg.pool);
1051
- ctx.bodies = await sys.board.bodies.validateRestore(rest);
1052
- ctx.pumps = await sys.board.pumps.validateRestore(rest);
1053
- await sys.board.circuits.validateRestore(rest, ctx);
1054
- ctx.features = await sys.board.features.validateRestore(rest);
1055
- ctx.chlorinators = await sys.board.chlorinator.validateRestore(rest);
1056
- ctx.heaters = await sys.board.heaters.validateRestore(rest);
1057
- ctx.valves = await sys.board.valves.validateRestore(rest);
1093
+ // Step 1 - Verify that the boards are the same. For instance you do not want to restore an IntelliTouch to an IntelliCenter.
1094
+ let cfg = rest.poolConfig;
1095
+ if (sys.controllerType === cfg.controllerType) {
1096
+ ctx.customNames = { errors: [], warnings: [], add: [], update: [], remove: [] };
1097
+ let customNames = sys.customNames.get();
1098
+ for (let i = 0; i < rest.poolConfig.customNames.length; i++) {
1099
+ let cn = customNames.find(elem => elem.id === rest.poolConfig.customNames[i].id);
1100
+ if (typeof cn === 'undefined') ctx.customNames.add.push(rest.poolConfig.customNames[i]);
1101
+ else if (JSON.stringify(rest.poolConfig.customNames[i]) !== JSON.stringify(cn)) ctx.customNames.update.push(cn);
1102
+ }
1103
+ ctx.general = { errors: [], warnings: [], add: [], update: [], remove: [] };
1104
+ if (JSON.stringify(sys.general.get()) !== JSON.stringify(cfg.pool)) ctx.general.update.push(cfg.pool);
1105
+ ctx.bodies = await sys.board.bodies.validateRestore(rest);
1106
+ ctx.pumps = await sys.board.pumps.validateRestore(rest);
1107
+ await sys.board.circuits.validateRestore(rest, ctx);
1108
+ ctx.features = await sys.board.features.validateRestore(rest);
1109
+ ctx.chlorinators = await sys.board.chlorinator.validateRestore(rest);
1110
+ ctx.heaters = await sys.board.heaters.validateRestore(rest);
1111
+ ctx.valves = await sys.board.valves.validateRestore(rest);
1058
1112
 
1059
- //ctx.covers = await sys.board.covers.validateRestore(rest);
1060
- ctx.chemControllers = await sys.board.chemControllers.validateRestore(rest);
1061
- ctx.filters = await sys.board.filters.validateRestore(rest);
1062
- ctx.schedules = await sys.board.schedules.validateRestore(rest);
1063
- }
1064
- else ctx.board.errors.push(`Panel Types do not match cannot restore backup from ${sys.controllerType} to ${rest.poolConfig.controllerType}`);
1113
+ //ctx.covers = await sys.board.covers.validateRestore(rest);
1114
+ ctx.chemControllers = await sys.board.chemControllers.validateRestore(rest);
1115
+ ctx.filters = await sys.board.filters.validateRestore(rest);
1116
+ ctx.schedules = await sys.board.schedules.validateRestore(rest);
1117
+ }
1118
+ else ctx.board.errors.push(`Panel Types do not match cannot restore backup from ${sys.controllerType} to ${rest.poolConfig.controllerType}`);
1065
1119
 
1066
- return ctx;
1120
+ return ctx;
1067
1121
 
1068
- } catch (err) { logger.error(`Error validating restore file: ${err.message}`); return Promise.reject(err); }
1122
+ } catch (err) { logger.error(`Error validating restore file: ${err.message}`); return Promise.reject(err); }
1069
1123
 
1070
- }
1071
- public cancelDelay(): Promise<any> { state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); }
1072
- public setManualOperationPriority(id: number): Promise<any> { return Promise.resolve(); }
1073
- public setDateTimeAsync(obj: any): Promise<any> { return Promise.resolve(); }
1074
- public keepManualTime() {
1075
- try {
1076
- // every minute, updated the time from the system clock in server mode
1077
- // but only for Virtual. Likely 'manual' on *Center means OCP time
1078
- if (sys.general.options.clockSource !== 'server') return;
1079
- state.time.setTimeFromSystemClock();
1080
- sys.board.system.setTZ();
1081
- } catch (err) { logger.error(`Error setting manual time: ${err.message}`); }
1082
- }
1083
- public setTZ() {
1084
- let tzOffsetObj = state.time.calcTZOffset();
1085
- if (sys.general.options.clockSource === 'server' || typeof sys.general.location.timeZone === 'undefined') {
1086
- let tzs = sys.board.valueMaps.timeZones.toArray();
1087
- sys.general.location.timeZone = tzs.find(tz => tz.utcOffset === tzOffsetObj.tzOffset).val;
1088
- }
1089
- if (sys.general.options.clockSource === 'server' || typeof sys.general.options.adjustDST === 'undefined') {
1090
- sys.general.options.adjustDST = tzOffsetObj.adjustDST;
1091
- }
1092
- }
1093
- public getDOW() { return this.board.valueMaps.scheduleDays.toArray(); }
1094
- public async setGeneralAsync(obj: any): Promise<General> {
1095
- if (typeof obj.alias === 'string') sys.general.alias = obj.alias;
1096
- if (typeof obj.options !== 'undefined') await sys.board.system.setOptionsAsync(obj.options);
1097
- if (typeof obj.location !== 'undefined') await sys.board.system.setLocationAsync(obj.location);
1098
- if (typeof obj.owner !== 'undefined') await sys.board.system.setOwnerAsync(obj.owner);
1099
- return sys.general;
1100
- }
1101
- public async setTempSensorsAsync(obj: any): Promise<TempSensorCollection> {
1102
- if (typeof obj.waterTempAdj1 != 'undefined' && obj.waterTempAdj1 !== sys.equipment.tempSensors.getCalibration('water1')) {
1103
- sys.equipment.tempSensors.setCalibration('water1', parseFloat(obj.waterTempAdj1));
1104
- }
1105
- if (typeof obj.waterTempAdj2 != 'undefined' && obj.waterTempAdj2 !== sys.equipment.tempSensors.getCalibration('water2')) {
1106
- sys.equipment.tempSensors.setCalibration('water2', parseFloat(obj.waterTempAdj2));
1107
1124
  }
1108
- if (typeof obj.waterTempAdj3 != 'undefined' && obj.waterTempAdj3 !== sys.equipment.tempSensors.getCalibration('water3')) {
1109
- sys.equipment.tempSensors.setCalibration('water3', parseFloat(obj.waterTempAdj3));
1125
+ public cancelDelay(): Promise<any> { state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); }
1126
+ public setManualOperationPriority(id: number): Promise<any> { return Promise.resolve(); }
1127
+ public setDateTimeAsync(obj: any): Promise<any> { return Promise.resolve(); }
1128
+ public keepManualTime() {
1129
+ try {
1130
+ // every minute, updated the time from the system clock in server mode
1131
+ // but only for Virtual. Likely 'manual' on *Center means OCP time
1132
+ if (sys.general.options.clockSource !== 'server') return;
1133
+ state.time.setTimeFromSystemClock();
1134
+ sys.board.system.setTZ();
1135
+ } catch (err) { logger.error(`Error setting manual time: ${err.message}`); }
1110
1136
  }
1111
- if (typeof obj.waterTempAdj4 != 'undefined' && obj.waterTempAdj4 !== sys.equipment.tempSensors.getCalibration('water4')) {
1112
- sys.equipment.tempSensors.setCalibration('water4', parseFloat(obj.waterTempAdj4));
1137
+ public setTZ() {
1138
+ let tzOffsetObj = state.time.calcTZOffset();
1139
+ if (sys.general.options.clockSource === 'server' || typeof sys.general.location.timeZone === 'undefined') {
1140
+ let tzs = sys.board.valueMaps.timeZones.toArray();
1141
+ sys.general.location.timeZone = tzs.find(tz => tz.utcOffset === tzOffsetObj.tzOffset).val;
1142
+ }
1143
+ if (sys.general.options.clockSource === 'server' || typeof sys.general.options.adjustDST === 'undefined') {
1144
+ sys.general.options.adjustDST = tzOffsetObj.adjustDST;
1145
+ }
1113
1146
  }
1114
- if (typeof obj.solarTempAdj1 != 'undefined' && obj.solarTempAdj1 !== sys.equipment.tempSensors.getCalibration('solar1')) {
1115
- sys.equipment.tempSensors.setCalibration('solar1', parseFloat(obj.solarTempAdj1));
1147
+ public getDOW() { return this.board.valueMaps.scheduleDays.toArray(); }
1148
+ public async setGeneralAsync(obj: any): Promise<General> {
1149
+ if (typeof obj.alias === 'string') sys.general.alias = obj.alias;
1150
+ if (typeof obj.options !== 'undefined') await sys.board.system.setOptionsAsync(obj.options);
1151
+ if (typeof obj.location !== 'undefined') await sys.board.system.setLocationAsync(obj.location);
1152
+ if (typeof obj.owner !== 'undefined') await sys.board.system.setOwnerAsync(obj.owner);
1153
+ return sys.general;
1116
1154
  }
1117
- if (typeof obj.solarTempAdj2 != 'undefined' && obj.solarTempAdj2 !== sys.equipment.tempSensors.getCalibration('solar2')) {
1118
- sys.equipment.tempSensors.setCalibration('solar2', parseFloat(obj.solarTempAdj2));
1155
+ public async setTempSensorsAsync(obj: any): Promise<TempSensorCollection> {
1156
+ if (typeof obj.waterTempAdj1 != 'undefined' && obj.waterTempAdj1 !== sys.equipment.tempSensors.getCalibration('water1')) {
1157
+ sys.equipment.tempSensors.setCalibration('water1', parseFloat(obj.waterTempAdj1));
1158
+ }
1159
+ if (typeof obj.waterTempAdj2 != 'undefined' && obj.waterTempAdj2 !== sys.equipment.tempSensors.getCalibration('water2')) {
1160
+ sys.equipment.tempSensors.setCalibration('water2', parseFloat(obj.waterTempAdj2));
1161
+ }
1162
+ if (typeof obj.waterTempAdj3 != 'undefined' && obj.waterTempAdj3 !== sys.equipment.tempSensors.getCalibration('water3')) {
1163
+ sys.equipment.tempSensors.setCalibration('water3', parseFloat(obj.waterTempAdj3));
1164
+ }
1165
+ if (typeof obj.waterTempAdj4 != 'undefined' && obj.waterTempAdj4 !== sys.equipment.tempSensors.getCalibration('water4')) {
1166
+ sys.equipment.tempSensors.setCalibration('water4', parseFloat(obj.waterTempAdj4));
1167
+ }
1168
+ if (typeof obj.solarTempAdj1 != 'undefined' && obj.solarTempAdj1 !== sys.equipment.tempSensors.getCalibration('solar1')) {
1169
+ sys.equipment.tempSensors.setCalibration('solar1', parseFloat(obj.solarTempAdj1));
1170
+ }
1171
+ if (typeof obj.solarTempAdj2 != 'undefined' && obj.solarTempAdj2 !== sys.equipment.tempSensors.getCalibration('solar2')) {
1172
+ sys.equipment.tempSensors.setCalibration('solar2', parseFloat(obj.solarTempAdj2));
1173
+ }
1174
+ if (typeof obj.solarTempAdj3 != 'undefined' && obj.solarTempAdj3 !== sys.equipment.tempSensors.getCalibration('solar3')) {
1175
+ sys.equipment.tempSensors.setCalibration('solar3', parseFloat(obj.solarTempAdj3));
1176
+ }
1177
+ if (typeof obj.solarTempAdj4 != 'undefined' && obj.solarTempAdj4 !== sys.equipment.tempSensors.getCalibration('solar4')) {
1178
+ sys.equipment.tempSensors.setCalibration('solar4', parseFloat(obj.solarTempAdj4));
1179
+ }
1180
+ if (typeof obj.airTempAdj != 'undefined' && obj.airTempAdj !== sys.equipment.tempSensors.getCalibration('air')) {
1181
+ sys.equipment.tempSensors.setCalibration('air', parseFloat(obj.airTempAdj));
1182
+ }
1183
+ return sys.equipment.tempSensors;
1119
1184
  }
1120
- if (typeof obj.solarTempAdj3 != 'undefined' && obj.solarTempAdj3 !== sys.equipment.tempSensors.getCalibration('solar3')) {
1121
- sys.equipment.tempSensors.setCalibration('solar3', parseFloat(obj.solarTempAdj3));
1185
+ public async setOptionsAsync(obj: any): Promise<Options> {
1186
+ if (obj.clockSource === 'server') sys.board.system.setTZ();
1187
+ sys.board.system.setTempSensorsAsync(obj);
1188
+ sys.general.options.set(obj);
1189
+ let bodyUnits = sys.general.options.units === 0 ? 1 : 2;
1190
+ for (let i = 0; i < sys.bodies.length; i++) sys.bodies.getItemByIndex(i).capacityUnits = bodyUnits;
1191
+ state.temps.units = sys.general.options.units === 0 ? 1 : 4;
1192
+ return sys.general.options;
1122
1193
  }
1123
- if (typeof obj.solarTempAdj4 != 'undefined' && obj.solarTempAdj4 !== sys.equipment.tempSensors.getCalibration('solar4')) {
1124
- sys.equipment.tempSensors.setCalibration('solar4', parseFloat(obj.solarTempAdj4));
1194
+ public async setLocationAsync(obj: any): Promise<Location> {
1195
+ sys.general.location.set(obj);
1196
+ return sys.general.location;
1125
1197
  }
1126
- if (typeof obj.airTempAdj != 'undefined' && obj.airTempAdj !== sys.equipment.tempSensors.getCalibration('air')) {
1127
- sys.equipment.tempSensors.setCalibration('air', parseFloat(obj.airTempAdj));
1198
+ public async setOwnerAsync(obj: any): Promise<Owner> {
1199
+ sys.general.owner.set(obj);
1200
+ return sys.general.owner;
1128
1201
  }
1129
- return sys.equipment.tempSensors;
1130
- }
1131
- public async setOptionsAsync(obj: any): Promise<Options> {
1132
- if (obj.clockSource === 'server') sys.board.system.setTZ();
1133
- sys.board.system.setTempSensorsAsync(obj);
1134
- sys.general.options.set(obj);
1135
- let bodyUnits = sys.general.options.units === 0 ? 1 : 2;
1136
- for (let i = 0; i < sys.bodies.length; i++) sys.bodies.getItemByIndex(i).capacityUnits = bodyUnits;
1137
- state.temps.units = sys.general.options.units === 0 ? 1 : 4;
1138
- return sys.general.options;
1139
- }
1140
- public async setLocationAsync(obj: any): Promise<Location> {
1141
- sys.general.location.set(obj);
1142
- return sys.general.location;
1143
- }
1144
- public async setOwnerAsync(obj: any): Promise<Owner> {
1145
- sys.general.owner.set(obj);
1146
- return sys.general.owner;
1147
- }
1148
- public async setTempsAsync(obj: any): Promise<TemperatureState> {
1149
- return new Promise<TemperatureState>((resolve, reject) => {
1150
- for (let prop in obj) {
1151
- switch (prop) {
1152
- case 'air':
1153
- case 'airSensor':
1154
- case 'airSensor1':
1155
- {
1156
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1157
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1158
- state.temps.air = sys.equipment.tempSensors.getCalibration('air') + temp;
1159
- }
1160
- break;
1161
- case 'waterSensor1':
1162
- {
1163
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1164
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1165
- state.temps.waterSensor1 = sys.equipment.tempSensors.getCalibration('water1') + temp;
1166
- let body = state.temps.bodies.getItemById(1);
1167
- if (body.isOn) body.temp = state.temps.waterSensor1;
1168
- else if (sys.equipment.shared) {
1169
- body = state.temps.bodies.getItemById(2);
1170
- if (body.isOn) body.temp = state.temps.waterSensor1;
1171
- }
1172
- }
1173
- break;
1174
- case 'waterSensor2':
1175
- {
1176
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1177
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1178
- state.temps.waterSensor2 = sys.equipment.tempSensors.getCalibration('water2') + temp;
1179
- if (!state.equipment.shared) {
1180
- let body = state.temps.bodies.getItemById(2);
1181
- if (body.isOn) body.temp = state.temps.waterSensor2;
1182
- }
1183
- }
1184
- break;
1185
- case 'waterSensor3':
1186
- {
1187
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1188
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1189
- state.temps.waterSensor3 = sys.equipment.tempSensors.getCalibration('water3') + temp;
1190
- let body = state.temps.bodies.getItemById(3);
1191
- if (body.isOn) body.temp = state.temps.waterSensor3;
1192
- }
1193
- break;
1194
- case 'waterSensor4':
1195
- {
1202
+ public async setTempsAsync(obj: any): Promise<TemperatureState> {
1203
+ return new Promise<TemperatureState>((resolve, reject) => {
1204
+ let units = sys.board.valueMaps.tempUnits.getName(state.temps.units) || 'F';
1205
+ for (let prop in obj) {
1206
+ switch (prop) {
1207
+ case 'air':
1208
+ case 'airSensor':
1209
+ case 'airSensor1':
1210
+ {
1211
+ let temp = 0;
1212
+ if (obj[prop] !== null) {
1213
+ if (typeof obj[prop].temperature !== 'undefined') {
1214
+ temp = parseFloat(obj[prop].temperature);
1215
+ if (typeof obj[prop].units === 'string') temp = utils.convert.temperature.convertUnits(temp, obj[prop].units || units, units);
1216
+ }
1217
+ else temp = parseFloat(obj[prop]);
1218
+ }
1219
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1220
+ state.temps.air = sys.equipment.tempSensors.getCalibration('air') + temp;
1221
+ }
1222
+ break;
1223
+ case 'waterSensor1':
1224
+ {
1225
+ let temp = 0;
1226
+ if (obj[prop] !== null) {
1227
+ if (typeof obj[prop].temperature !== 'undefined') {
1228
+ temp = parseFloat(obj[prop].temperature);
1229
+ if (typeof obj[prop].units === 'string') temp = utils.convert.temperature.convertUnits(temp, obj[prop].units || units, units);
1230
+ }
1231
+ else temp = parseFloat(obj[prop]);
1232
+ }
1233
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1234
+ state.temps.waterSensor1 = sys.equipment.tempSensors.getCalibration('water1') + temp;
1235
+ let body = state.temps.bodies.getItemById(1);
1236
+ if (body.isOn) body.temp = state.temps.waterSensor1;
1237
+ else if (!sys.equipment.dual) {
1238
+ body = state.temps.bodies.find(elem => elem.id === 2);
1239
+ if (typeof body !== 'undefined') {
1240
+ body = state.temps.bodies.getItemById(2);
1241
+ if (body.isOn) body.temp = state.temps.waterSensor1;
1242
+ }
1243
+ }
1244
+ }
1245
+ break;
1246
+ case 'waterSensor2':
1247
+ {
1248
+ let temp = 0;
1249
+ if (obj[prop] !== null) {
1250
+ if (typeof obj[prop].temperature !== 'undefined') {
1251
+ temp = parseFloat(obj[prop].temperature);
1252
+ if (typeof obj[prop].units === 'string') temp = utils.convert.temperature.convertUnits(temp, obj[prop].units || units, units);
1253
+ }
1254
+ else temp = parseFloat(obj[prop]);
1255
+ }
1256
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1257
+ state.temps.waterSensor2 = sys.equipment.tempSensors.getCalibration('water2') + temp;
1258
+ if (state.equipment.dual) {
1259
+ let body = state.temps.bodies.getItemById(2);
1260
+ if (body.isOn) body.temp = state.temps.waterSensor2;
1261
+ }
1262
+ }
1263
+ break;
1264
+ case 'waterSensor3':
1265
+ {
1266
+ let temp = 0;
1267
+ if (obj[prop] !== null) {
1268
+ if (typeof obj[prop].temperature !== 'undefined') {
1269
+ temp = parseFloat(obj[prop].temperature);
1270
+ if (typeof obj[prop].units === 'string') temp = utils.convert.temperature.convertUnits(temp, obj[prop].units || units, units);
1271
+ }
1272
+ else temp = parseFloat(obj[prop]);
1273
+ }
1274
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1275
+ state.temps.waterSensor3 = sys.equipment.tempSensors.getCalibration('water3') + temp;
1276
+ let body = state.temps.bodies.getItemById(3);
1277
+ if (body.isOn) body.temp = state.temps.waterSensor3;
1278
+ }
1279
+ break;
1280
+ case 'waterSensor4':
1281
+ {
1282
+ let temp = 0;
1283
+ if (obj[prop] !== null) {
1284
+ if (typeof obj[prop].temperature !== 'undefined') {
1285
+ temp = parseFloat(obj[prop].temperature);
1286
+ if (typeof obj[prop].units === 'string') temp = utils.convert.temperature.convertUnits(temp, obj[prop].units || units, units);
1287
+ }
1288
+ else temp = parseFloat(obj[prop]);
1289
+ }
1290
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1291
+ let body = state.temps.bodies.getItemById(4);
1292
+ if (body.isOn) body.temp = state.temps.waterSensor4;
1293
+ }
1294
+ break;
1196
1295
 
1197
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1198
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1199
- state.temps.waterSensor4 = sys.equipment.tempSensors.getCalibration('water4') + temp;
1200
- let body = state.temps.bodies.getItemById(4);
1201
- if (body.isOn) body.temp = state.temps.waterSensor4;
1296
+ case 'solarSensor1':
1297
+ case 'solar1':
1298
+ case 'solar':
1299
+ {
1300
+ let temp = 0;
1301
+ if (obj[prop] !== null) {
1302
+ if (typeof obj[prop].temperature !== 'undefined') {
1303
+ temp = parseFloat(obj[prop].temperature);
1304
+ if (typeof obj[prop].units === 'string') temp = utils.convert.temperature.convertUnits(temp, obj[prop].units || units, units);
1305
+ }
1306
+ else temp = parseFloat(obj[prop]);
1307
+ }
1308
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1309
+ state.temps.solar = sys.equipment.tempSensors.getCalibration('solar1') + temp;
1310
+ }
1311
+ break;
1312
+ case 'solar2':
1313
+ case 'solarSensor2':
1314
+ {
1315
+ let temp = 0;
1316
+ if (obj[prop] !== null) {
1317
+ if (typeof obj[prop].temperature !== 'undefined') {
1318
+ temp = parseFloat(obj[prop].temperature);
1319
+ if (typeof obj[prop].units === 'string') temp = utils.convert.temperature.convertUnits(temp, obj[prop].units || units, units);
1320
+ }
1321
+ else temp = parseFloat(obj[prop]);
1322
+ }
1323
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1324
+ state.temps.solarSensor2 = sys.equipment.tempSensors.getCalibration('solar2') + temp;
1325
+ }
1326
+ break;
1327
+ case 'solar3':
1328
+ case 'solarSensor3':
1329
+ {
1330
+ let temp = 0;
1331
+ if (obj[prop] !== null) {
1332
+ if (typeof obj[prop].temperature !== 'undefined') {
1333
+ temp = parseFloat(obj[prop].temperature);
1334
+ if (typeof obj[prop].units === 'string') temp = utils.convert.temperature.convertUnits(temp, obj[prop].units || units, units);
1335
+ }
1336
+ else temp = parseFloat(obj[prop]);
1337
+ }
1338
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1339
+ state.temps.solarSensor3 = sys.equipment.tempSensors.getCalibration('solar3') + temp;
1340
+ }
1341
+ break;
1342
+ case 'solar4':
1343
+ case 'solarSensor4':
1344
+ {
1345
+ let temp = 0;
1346
+ if (obj[prop] !== null) {
1347
+ if (typeof obj[prop].temperature !== 'undefined') {
1348
+ temp = parseFloat(obj[prop].temperature);
1349
+ if (typeof obj[prop].units === 'string') temp = utils.convert.temperature.convertUnits(temp, obj[prop].units || units, units);
1350
+ }
1351
+ else temp = parseFloat(obj[prop]);
1352
+ }
1353
+ if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1354
+ state.temps.solarSensor4 = sys.equipment.tempSensors.getCalibration('solar4') + temp;
1355
+ }
1356
+ break;
1357
+ }
1202
1358
  }
1203
- break;
1359
+ sys.board.heaters.syncHeaterStates();
1360
+ resolve(state.temps);
1361
+ });
1362
+ }
1363
+ public getSensors() {
1364
+ let sensors = [{ name: 'Air Sensor', temp: state.temps.air, tempAdj: sys.equipment.tempSensors.getCalibration('air'), binding: 'airTempAdj' }];
1365
+ if (sys.equipment.shared) {
1366
+ if (sys.equipment.maxBodies > 2)
1367
+ sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1368
+ { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' },
1369
+ { name: 'Water Sensor 3', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1370
+ else
1371
+ sensors.push({ name: 'Water Sensor', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' });
1372
+ if (sys.equipment.maxBodies > 3)
1373
+ sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1204
1374
 
1205
- case 'solarSensor1':
1206
- case 'solar1':
1207
- case 'solar':
1208
- {
1209
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1210
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1211
- state.temps.solar = sys.equipment.tempSensors.getCalibration('solar1') + temp;
1375
+ if (sys.board.heaters.isSolarInstalled()) {
1376
+ if (sys.equipment.maxBodies > 2) {
1377
+ sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1378
+ { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1379
+ }
1380
+ else
1381
+ sensors.push({ name: 'Solar Sensor', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' });
1382
+ if (sys.equipment.maxBodies > 3)
1383
+ sensors.push({ name: 'Solar Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1212
1384
  }
1213
- break;
1214
- case 'solar2':
1215
- case 'solarSensor2':
1216
- {
1217
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1218
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1219
- state.temps.solarSensor2 = sys.equipment.tempSensors.getCalibration('solar2') + temp;
1385
+ }
1386
+ else if (sys.equipment.dual) {
1387
+ sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1388
+ { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' });
1389
+ if (sys.equipment.maxBodies > 2)
1390
+ sensors.push({ name: 'Water Sensor 3', temp: state.temps.waterSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1391
+ if (sys.equipment.maxBodies > 3)
1392
+ sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1393
+ if (sys.board.heaters.isSolarInstalled()) {
1394
+ sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1395
+ { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1396
+ if (sys.equipment.maxBodies > 2)
1397
+ sensors.push({ name: 'Solar Sensor 3', temp: state.temps.solarSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('solar3'), binding: 'solarTempAdj3' });
1398
+ if (sys.equipment.maxBodies > 3)
1399
+ sensors.push({ name: 'Solar Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1220
1400
  }
1221
- break;
1222
- case 'solar3':
1223
- case 'solarSensor3':
1224
- {
1225
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1226
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1227
- state.temps.solarSensor3 = sys.equipment.tempSensors.getCalibration('solar3') + temp;
1401
+ }
1402
+ else {
1403
+ if (sys.equipment.maxBodies > 1) {
1404
+ sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1405
+ { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' });
1406
+ if (sys.equipment.maxBodies > 2)
1407
+ sensors.push({ name: 'Water Sensor 3', temp: state.temps.waterSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1408
+ if (sys.equipment.maxBodies > 3)
1409
+ sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1410
+
1411
+ if (sys.board.heaters.isSolarInstalled()) {
1412
+ sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solarSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1413
+ { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1414
+ if (sys.equipment.maxBodies > 2)
1415
+ sensors.push({ name: 'Solar Sensor 3', temp: state.temps.solarSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('solar3'), binding: 'solarTempAdj3' });
1416
+ if (sys.equipment.maxBodies > 3)
1417
+ sensors.push({ name: 'Water Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1418
+ }
1228
1419
  }
1229
- break;
1230
- case 'solar4':
1231
- case 'solarSensor4':
1232
- {
1233
- let temp = obj[prop] !== null ? parseFloat(obj[prop]) : 0;
1234
- if (isNaN(temp)) return reject(new InvalidEquipmentDataError(`Invalid value for ${prop} ${obj[prop]}`, `Temps:${prop}`, obj[prop]));
1235
- state.temps.solarSensor4 = sys.equipment.tempSensors.getCalibration('solar4') + temp;
1420
+ else {
1421
+ sensors.push({ name: 'Water Sensor', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' });
1422
+ if (sys.board.heaters.isSolarInstalled())
1423
+ sensors.push({ name: 'Solar Sensor', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' });
1236
1424
  }
1237
- break;
1238
1425
  }
1239
- }
1240
- sys.board.heaters.syncHeaterStates();
1241
- resolve(state.temps);
1242
- });
1243
- }
1244
- public getSensors() {
1245
- let sensors = [{ name: 'Air Sensor', temp: state.temps.air, tempAdj: sys.equipment.tempSensors.getCalibration('air'), binding: 'airTempAdj' }];
1246
- if (sys.equipment.shared) {
1247
- if (sys.equipment.maxBodies > 2)
1248
- sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1249
- { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' },
1250
- { name: 'Water Sensor 3', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1251
- else
1252
- sensors.push({ name: 'Water Sensor', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' });
1253
- if (sys.equipment.maxBodies > 3)
1254
- sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1426
+ return sensors;
1427
+ }
1428
+ public async setCustomNamesAsync(names: any[]): Promise<CustomNameCollection> {
1429
+ if (!Array.isArray(names)) return Promise.reject(new InvalidEquipmentDataError(`Data is not an array`, 'customNames', names))
1430
+ let arr = [];
1431
+ for (let i = 0; i < names.length; i++) { arr.push(sys.board.system.setCustomNameAsync(names[i])); }
1432
+ return new Promise<CustomNameCollection>(async (resolve, reject) => {
1433
+ try {
1434
+ await Promise.all(arr).catch(err => reject(err));
1435
+ // sys.board.system.syncCustomNamesValueMap(); Each custom name promise is already syncing the bytevalue array
1255
1436
 
1256
- if (sys.board.heaters.isSolarInstalled()) {
1257
- if (sys.equipment.maxBodies > 2) {
1258
- sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1259
- { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1260
- }
1261
- else
1262
- sensors.push({ name: 'Solar Sensor', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' });
1263
- if (sys.equipment.maxBodies > 3)
1264
- sensors.push({ name: 'Solar Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1265
- }
1437
+ resolve(sys.customNames);
1438
+ }
1439
+ catch (err) { reject(err); }
1440
+ });
1266
1441
  }
1267
- else if (sys.equipment.dual) {
1268
- sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1269
- { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' });
1270
- if (sys.equipment.maxBodies > 2)
1271
- sensors.push({ name: 'Water Sensor 3', temp: state.temps.waterSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1272
- if (sys.equipment.maxBodies > 3)
1273
- sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1274
- if (sys.board.heaters.isSolarInstalled()) {
1275
- sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1276
- { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1277
- if (sys.equipment.maxBodies > 2)
1278
- sensors.push({ name: 'Solar Sensor 3', temp: state.temps.solarSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('solar3'), binding: 'solarTempAdj3' });
1279
- if (sys.equipment.maxBodies > 3)
1280
- sensors.push({ name: 'Solar Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1281
- }
1442
+ public async setCustomNameAsync(data: any, send: boolean = false): Promise<CustomName> {
1443
+ return new Promise<CustomName>((resolve, reject) => {
1444
+ let id = parseInt(data.id, 10);
1445
+ if (isNaN(id)) return reject(new InvalidEquipmentIdError('Invalid Custom Name Id', data.id, 'customName'));
1446
+ if (id > sys.equipment.maxCustomNames) return reject(new InvalidEquipmentIdError('Custom Name Id out of range', data.id, 'customName'));
1447
+ let cname = sys.customNames.getItemById(id, true);
1448
+ cname.name = data.name;
1449
+ sys.board.system.syncCustomNamesValueMap();
1450
+ return resolve(cname);
1451
+ });
1282
1452
  }
1283
- else {
1284
- if (sys.equipment.maxBodies > 1) {
1285
- sensors.push({ name: 'Water Sensor 1', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' },
1286
- { name: 'Water Sensor 2', temp: state.temps.waterSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('water2'), binding: 'waterTempAdj2' });
1287
- if (sys.equipment.maxBodies > 2)
1288
- sensors.push({ name: 'Water Sensor 3', temp: state.temps.waterSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('water3'), binding: 'waterTempAdj3' });
1289
- if (sys.equipment.maxBodies > 3)
1290
- sensors.push({ name: 'Water Sensor 4', temp: state.temps.waterSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('water4'), binding: 'waterTempAdj4' });
1291
-
1292
- if (sys.board.heaters.isSolarInstalled()) {
1293
- sensors.push({ name: 'Solar Sensor 1', temp: state.temps.solarSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' },
1294
- { name: 'Solar Sensor 2', temp: state.temps.solarSensor2, tempAdj: sys.equipment.tempSensors.getCalibration('solar2'), binding: 'solarTempAdj2' });
1295
- if (sys.equipment.maxBodies > 2)
1296
- sensors.push({ name: 'Solar Sensor 3', temp: state.temps.solarSensor3, tempAdj: sys.equipment.tempSensors.getCalibration('solar3'), binding: 'solarTempAdj3' });
1297
- if (sys.equipment.maxBodies > 3)
1298
- sensors.push({ name: 'Water Sensor 4', temp: state.temps.solarSensor4, tempAdj: sys.equipment.tempSensors.getCalibration('solar4'), binding: 'solarTempAdj4' });
1299
- }
1300
- }
1301
- else {
1302
- sensors.push({ name: 'Water Sensor', temp: state.temps.waterSensor1, tempAdj: sys.equipment.tempSensors.getCalibration('water1'), binding: 'waterTempAdj1' });
1303
- if (sys.board.heaters.isSolarInstalled())
1304
- sensors.push({ name: 'Solar Sensor', temp: state.temps.solar, tempAdj: sys.equipment.tempSensors.getCalibration('solar1'), binding: 'solarTempAdj1' });
1305
- }
1453
+ public syncCustomNamesValueMap() {
1454
+ sys.customNames.sortById();
1455
+ sys.board.valueMaps.customNames = new byteValueMap(
1456
+ sys.customNames.get().map((el, idx) => {
1457
+ return [idx + 200, { name: el.name, desc: el.name }];
1458
+ })
1459
+ );
1306
1460
  }
1307
- return sensors;
1308
- }
1309
- public async setCustomNamesAsync(names: any[]): Promise<CustomNameCollection> {
1310
- if (!Array.isArray(names)) return Promise.reject(new InvalidEquipmentDataError(`Data is not an array`, 'customNames', names))
1311
- let arr = [];
1312
- for (let i = 0; i < names.length; i++) { arr.push(sys.board.system.setCustomNameAsync(names[i])); }
1313
- return new Promise<CustomNameCollection>(async (resolve, reject) => {
1314
- try {
1315
- await Promise.all(arr).catch(err => reject(err));
1316
- // sys.board.system.syncCustomNamesValueMap(); Each custom name promise is already syncing the bytevalue array
1317
-
1318
- resolve(sys.customNames);
1319
- }
1320
- catch (err) { reject(err); }
1321
- });
1322
- }
1323
- public async setCustomNameAsync(data: any): Promise<CustomName> {
1324
- return new Promise<CustomName>((resolve, reject) => {
1325
- let id = parseInt(data.id, 10);
1326
- if (isNaN(id)) return reject(new InvalidEquipmentIdError('Invalid Custom Name Id', data.id, 'customName'));
1327
- if (id > sys.equipment.maxCustomNames) return reject(new InvalidEquipmentIdError('Custom Name Id out of range', data.id, 'customName'));
1328
- let cname = sys.customNames.getItemById(id, true);
1329
- cname.name = data.name;
1330
- sys.board.system.syncCustomNamesValueMap();
1331
- return resolve(cname);
1332
- });
1333
- }
1334
- public syncCustomNamesValueMap() {
1335
- sys.customNames.sortById();
1336
- sys.board.valueMaps.customNames = new byteValueMap(
1337
- sys.customNames.get().map((el, idx) => {
1338
- return [idx + 200, { name: el.name, desc: el.name }];
1339
- })
1340
- );
1341
- }
1461
+ public async setPanelModeAsync(data: any): Promise<any> { return { mode: state.mode }; }
1342
1462
  }
1343
1463
  export class BodyCommands extends BoardCommands {
1344
1464
  public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
@@ -1391,125 +1511,125 @@ export class BodyCommands extends BoardCommands {
1391
1511
  }
1392
1512
  public freezeProtectBodyOn: Date;
1393
1513
  public freezeProtectStart: Date;
1394
- public async syncFreezeProtection() {
1395
- try {
1396
- // Go through all the features and circuits to make sure we have the freeze protect set appropriately. The freeze
1397
- // flag will have already been set whether this is a Nixie setup or there is an OCP involved.
1514
+ public async syncFreezeProtection() {
1515
+ try {
1516
+ // Go through all the features and circuits to make sure we have the freeze protect set appropriately. The freeze
1517
+ // flag will have already been set whether this is a Nixie setup or there is an OCP involved.
1398
1518
 
1399
- // First turn on/off any features that are in our control that should be under our control. If this is an OCP we
1400
- // do not create features beyond those controlled by the OCP so we don't need to check these in that condition. That is
1401
- // why it first checks the controller type.
1402
- let freeze = utils.makeBool(state.freeze);
1403
- if (sys.controllerType === ControllerType.Nixie) {
1404
- // If we are a Nixie controller we need to evaluate the current freeze settings against the air temperature.
1405
- if (typeof state.temps.air !== 'undefined') {
1406
- // Start freeze protection when the temperature is <= the threshold but don't stop it until we are 2 degrees above the threshold. This
1407
- // makes for a 3 degree offset.
1408
- if (state.temps.air <= sys.general.options.freezeThreshold) freeze = true;
1409
- else if (state.freeze && state.temps.air - 2 > sys.general.options.freezeThreshold) freeze = false;
1410
- }
1411
- else freeze = false;
1519
+ // First turn on/off any features that are in our control that should be under our control. If this is an OCP we
1520
+ // do not create features beyond those controlled by the OCP so we don't need to check these in that condition. That is
1521
+ // why it first checks the controller type.
1522
+ let freeze = utils.makeBool(state.freeze);
1523
+ if (sys.controllerType === ControllerType.Nixie) {
1524
+ // If we are a Nixie controller we need to evaluate the current freeze settings against the air temperature.
1525
+ if (typeof state.temps.air !== 'undefined') {
1526
+ // Start freeze protection when the temperature is <= the threshold but don't stop it until we are 2 degrees above the threshold. This
1527
+ // makes for a 3 degree offset.
1528
+ if (state.temps.air <= sys.general.options.freezeThreshold) freeze = true;
1529
+ else if (state.freeze && state.temps.air - 2 > sys.general.options.freezeThreshold) freeze = false;
1530
+ }
1531
+ else freeze = false;
1412
1532
 
1413
- // We need to know when we first turned the freeze protection on. This is because we will be rotating between pool and spa
1414
- // on shared body systems when both pool and spa have freeze protection checked.
1415
- if (state.freeze !== freeze) {
1416
- this.freezeProtectStart = freeze ? new Date() : undefined;
1417
- state.freeze = freeze;
1418
- }
1419
- for (let i = 0; i < sys.features.length; i++) {
1420
- let feature = sys.features.getItemByIndex(i);
1421
- let fstate = state.features.getItemById(feature.id, true);
1422
- if (!feature.freeze || !feature.isActive === true || feature.master !== 1) {
1423
- fstate.freezeProtect = false;
1424
- continue; // This is not affected by freeze conditions.
1425
- }
1426
- if (freeze && !fstate.isOn) {
1427
- // This feature should be on because we are freezing.
1428
- fstate.freezeProtect = true;
1429
- await sys.board.features.setFeatureStateAsync(feature.id, true);
1430
- }
1431
- else if (!freeze && fstate.freezeProtect) {
1432
- // This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
1433
- fstate.freezeProtect = false;
1434
- await sys.board.features.setFeatureStateAsync(feature.id, false);
1435
- }
1436
- }
1437
- }
1438
- let bodyRotationChecked = false;
1439
- for (let i = 0; i < sys.circuits.length; i++) {
1440
- let circ = sys.circuits.getItemByIndex(i);
1441
- let cstate = state.circuits.getItemById(circ.id);
1442
- if (!circ.freeze || !circ.isActive === true || circ.master !== 1) {
1443
- cstate.freezeProtect = false;
1444
- continue; // This is not affected by freeze conditions.
1445
- }
1446
- if (sys.equipment.shared && freeze && (circ.id === 1 || circ.id === 6)) {
1447
- // Exit out of here because we already checked the body rotation. We only want to do this once since it can be expensive turning
1448
- // on a particular body.
1449
- if (bodyRotationChecked) continue;
1450
- // These are our body circuits so we need to check to see if they need to be rotated between pool and spa.
1451
- let pool = circ.id === 6 ? circ : sys.circuits.getItemById(6);
1452
- let spa = circ.id === 1 ? circ : sys.circuits.getItemById(1);
1453
- if (pool.freeze && spa.freeze) {
1454
- // We only need to rotate between pool and spa when they are both checked.
1455
- let pstate = circ.id === 6 ? cstate : state.circuits.getItemById(6);
1456
- let sstate = circ.id === 1 ? cstate : state.circuits.getItemById(1);
1457
- if (!pstate.isOn && !sstate.isOn) {
1458
- // Neither the pool or spa are on so we will turn on the pool first.
1459
- pstate.freezeProtect = true;
1460
- this.freezeProtectBodyOn = new Date();
1461
- await sys.board.circuits.setCircuitStateAsync(6, true);
1533
+ // We need to know when we first turned the freeze protection on. This is because we will be rotating between pool and spa
1534
+ // on shared body systems when both pool and spa have freeze protection checked.
1535
+ if (state.freeze !== freeze) {
1536
+ this.freezeProtectStart = freeze ? new Date() : undefined;
1537
+ state.freeze = freeze;
1538
+ }
1539
+ for (let i = 0; i < sys.features.length; i++) {
1540
+ let feature = sys.features.getItemByIndex(i);
1541
+ let fstate = state.features.getItemById(feature.id, true);
1542
+ if (!feature.freeze || !feature.isActive === true || feature.master !== 1) {
1543
+ fstate.freezeProtect = false;
1544
+ continue; // This is not affected by freeze conditions.
1545
+ }
1546
+ if (freeze && !fstate.isOn) {
1547
+ // This feature should be on because we are freezing.
1548
+ fstate.freezeProtect = true;
1549
+ await sys.board.features.setFeatureStateAsync(feature.id, true);
1550
+ }
1551
+ else if (!freeze && fstate.freezeProtect) {
1552
+ // This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
1553
+ fstate.freezeProtect = false;
1554
+ await sys.board.features.setFeatureStateAsync(feature.id, false);
1555
+ }
1556
+ }
1462
1557
  }
1463
- else {
1464
- // If neither of the bodies were turned on for freeze protection then we need to ignore this.
1465
- if (!pstate.freezeProtect && !sstate.freezeProtect) {
1466
- this.freezeProtectBodyOn = undefined;
1467
- continue;
1468
- }
1558
+ let bodyRotationChecked = false;
1559
+ for (let i = 0; i < sys.circuits.length; i++) {
1560
+ let circ = sys.circuits.getItemByIndex(i);
1561
+ let cstate = state.circuits.getItemById(circ.id);
1562
+ if (!circ.freeze || !circ.isActive === true || circ.master !== 1) {
1563
+ cstate.freezeProtect = false;
1564
+ continue; // This is not affected by freeze conditions.
1565
+ }
1566
+ if (sys.equipment.shared && freeze && (circ.id === 1 || circ.id === 6)) {
1567
+ // Exit out of here because we already checked the body rotation. We only want to do this once since it can be expensive turning
1568
+ // on a particular body.
1569
+ if (bodyRotationChecked) continue;
1570
+ // These are our body circuits so we need to check to see if they need to be rotated between pool and spa.
1571
+ let pool = circ.id === 6 ? circ : sys.circuits.getItemById(6);
1572
+ let spa = circ.id === 1 ? circ : sys.circuits.getItemById(1);
1573
+ if (pool.freeze && spa.freeze) {
1574
+ // We only need to rotate between pool and spa when they are both checked.
1575
+ let pstate = circ.id === 6 ? cstate : state.circuits.getItemById(6);
1576
+ let sstate = circ.id === 1 ? cstate : state.circuits.getItemById(1);
1577
+ if (!pstate.isOn && !sstate.isOn) {
1578
+ // Neither the pool or spa are on so we will turn on the pool first.
1579
+ pstate.freezeProtect = true;
1580
+ this.freezeProtectBodyOn = new Date();
1581
+ await sys.board.circuits.setCircuitStateAsync(6, true);
1582
+ }
1583
+ else {
1584
+ // If neither of the bodies were turned on for freeze protection then we need to ignore this.
1585
+ if (!pstate.freezeProtect && !sstate.freezeProtect) {
1586
+ this.freezeProtectBodyOn = undefined;
1587
+ continue;
1588
+ }
1469
1589
 
1470
- // One of the two bodies is on so we need to check for the rotation. If it is time to rotate do the rotation.
1471
- if (typeof this.freezeProtectBodyOn === 'undefined') this.freezeProtectBodyOn = new Date();
1472
- let dt = new Date().getTime();
1473
- if (dt - 1000 * 60 * 15 > this.freezeProtectBodyOn.getTime()) {
1474
- logger.info(`Swapping bodies for freeze protection pool:${pstate.isOn} spa:${sstate.isOn} interval: ${utils.formatDuration(dt - this.freezeProtectBodyOn.getTime() / 1000)}`);
1475
- // 10 minutes has elapsed so we will be rotating to the other body.
1476
- if (pstate.isOn) {
1477
- // The setCircuitState method will handle turning off the pool body.
1478
- sstate.freezeProtect = true;
1479
- pstate.freezeProtect = false;
1480
- await sys.board.circuits.setCircuitStateAsync(1, true);
1590
+ // One of the two bodies is on so we need to check for the rotation. If it is time to rotate do the rotation.
1591
+ if (typeof this.freezeProtectBodyOn === 'undefined') this.freezeProtectBodyOn = new Date();
1592
+ let dt = new Date().getTime();
1593
+ if (dt - 1000 * 60 * 15 > this.freezeProtectBodyOn.getTime()) {
1594
+ logger.info(`Swapping bodies for freeze protection pool:${pstate.isOn} spa:${sstate.isOn} interval: ${utils.formatDuration(dt - this.freezeProtectBodyOn.getTime() / 1000)}`);
1595
+ // 10 minutes has elapsed so we will be rotating to the other body.
1596
+ if (pstate.isOn) {
1597
+ // The setCircuitState method will handle turning off the pool body.
1598
+ sstate.freezeProtect = true;
1599
+ pstate.freezeProtect = false;
1600
+ await sys.board.circuits.setCircuitStateAsync(1, true);
1601
+ }
1602
+ else {
1603
+ sstate.freezeProtect = false;
1604
+ pstate.freezeProtect = true;
1605
+ await sys.board.circuits.setCircuitStateAsync(6, true);
1606
+ }
1607
+ // Set a new date as this will be our rotation check now.
1608
+ this.freezeProtectBodyOn = new Date();
1609
+ }
1610
+ }
1611
+ }
1612
+ else {
1613
+ // Only this circuit is selected for freeze protection so we don't need any special treatment.
1614
+ cstate.freezeProtect = true;
1615
+ if (!cstate.isOn) await sys.board.circuits.setCircuitStateAsync(circ.id, true);
1616
+ }
1617
+ bodyRotationChecked = true;
1481
1618
  }
1482
- else {
1483
- sstate.freezeProtect = false;
1484
- pstate.freezeProtect = true;
1485
- await sys.board.circuits.setCircuitStateAsync(6, true);
1619
+ else if (freeze && !cstate.isOn) {
1620
+ // This circuit should be on because we are freezing.
1621
+ cstate.freezeProtect = true;
1622
+ await sys.board.circuits.setCircuitStateAsync(circ.id, true);
1623
+ }
1624
+ else if (!freeze && cstate.freezeProtect) {
1625
+ // This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
1626
+ await sys.board.circuits.setCircuitStateAsync(circ.id, false);
1627
+ cstate.freezeProtect = false;
1486
1628
  }
1487
- // Set a new date as this will be our rotation check now.
1488
- this.freezeProtectBodyOn = new Date();
1489
- }
1490
1629
  }
1491
- }
1492
- else {
1493
- // Only this circuit is selected for freeze protection so we don't need any special treatment.
1494
- cstate.freezeProtect = true;
1495
- if (!cstate.isOn) await sys.board.circuits.setCircuitStateAsync(circ.id, true);
1496
- }
1497
- bodyRotationChecked = true;
1498
- }
1499
- else if (freeze && !cstate.isOn) {
1500
- // This circuit should be on because we are freezing.
1501
- cstate.freezeProtect = true;
1502
- await sys.board.circuits.setCircuitStateAsync(circ.id, true);
1503
1630
  }
1504
- else if (!freeze && cstate.freezeProtect) {
1505
- // This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
1506
- await sys.board.circuits.setCircuitStateAsync(circ.id, false);
1507
- cstate.freezeProtect = false;
1508
- }
1509
- }
1631
+ catch (err) { logger.error(`syncFreezeProtection: Error synchronizing freeze protection states: ${err.message}`); }
1510
1632
  }
1511
- catch (err) { logger.error(`syncFreezeProtection: Error synchronizing freeze protection states: ${err.message}`); }
1512
- }
1513
1633
 
1514
1634
  public async initFilters() {
1515
1635
  try {
@@ -1650,57 +1770,73 @@ export class BodyCommands extends BoardCommands {
1650
1770
  sys.board.heaters.syncHeaterStates();
1651
1771
  return Promise.resolve(bstate);
1652
1772
  }
1653
- public getHeatSources(bodyId: number) {
1654
- let heatSources = [];
1655
- let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1656
- heatSources.push(this.board.valueMaps.heatSources.transformByName('nochange'));
1657
- if (heatTypes.total > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('off'));
1658
- if (heatTypes.gas > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('heater'));
1659
- if (heatTypes.mastertemp > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('mastertemp'));
1660
- if (heatTypes.solar > 0) {
1661
- let hm = this.board.valueMaps.heatSources.transformByName('solar');
1662
- heatSources.push(hm);
1663
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('solarpref'));
1664
- }
1665
- if (heatTypes.heatpump > 0) {
1666
- let hm = this.board.valueMaps.heatSources.transformByName('heatpump');
1667
- heatSources.push(hm);
1668
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('heatpumppref'));
1669
- }
1670
- if (heatTypes.ultratemp > 0) {
1671
- let hm = this.board.valueMaps.heatSources.transformByName('ultratemp');
1672
- heatSources.push(hm);
1673
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('ultratemppref'));
1773
+ public getHeatSources(bodyId: number) {
1774
+ let heatSources = [];
1775
+ let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1776
+ heatSources.push(this.board.valueMaps.heatSources.transformByName('nochange'));
1777
+ if (heatTypes.total > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('off'));
1778
+ if (heatTypes.gas > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('heater'));
1779
+ if (heatTypes.mastertemp > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('mastertemp'));
1780
+ if (heatTypes.solar > 0) {
1781
+ let hm = this.board.valueMaps.heatSources.transformByName('solar');
1782
+ heatSources.push(hm);
1783
+ if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('solarpref'));
1784
+ }
1785
+ if (heatTypes.heatpump > 0) {
1786
+ let hm = this.board.valueMaps.heatSources.transformByName('heatpump');
1787
+ heatSources.push(hm);
1788
+ if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('heatpumppref'));
1789
+ }
1790
+ if (heatTypes.ultratemp > 0) {
1791
+ let hm = this.board.valueMaps.heatSources.transformByName('ultratemp');
1792
+ heatSources.push(hm);
1793
+ if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('ultratemppref'));
1794
+ }
1795
+ if (heatTypes.hybrid > 0) {
1796
+ heatSources.push(this.board.valueMaps.heatSources.transformByName('hybheat'));
1797
+ heatSources.push(this.board.valueMaps.heatSources.transformByName('hybheatpump'));
1798
+ heatSources.push(this.board.valueMaps.heatSources.transformByName('hybhybrid'));
1799
+ heatSources.push(this.board.valueMaps.heatSources.transformByName('hybdual'));
1800
+ }
1801
+ return heatSources;
1674
1802
  }
1675
- return heatSources;
1676
- }
1677
- public getHeatModes(bodyId: number) {
1678
- let heatModes = [];
1679
- sys.board.heaters.updateHeaterServices();
1803
+ public getHeatModes(bodyId: number) {
1804
+ let heatModes = [];
1805
+ sys.board.heaters.updateHeaterServices();
1680
1806
 
1681
- // 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)
1682
- heatModes.push(this.board.valueMaps.heatModes.transformByName('off')); // In IC fw 1.047 off is no longer 0.
1683
- let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1684
- if (heatTypes.hybrid > 0) heatModes = this.board.valueMaps.heatModes.toArray();
1685
- if (heatTypes.gas > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('heater'));
1686
- if (heatTypes.mastertemp > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('mtheater'));
1687
- if (heatTypes.solar > 0) {
1688
- let hm = this.board.valueMaps.heatModes.transformByName('solar');
1689
- heatModes.push(hm);
1690
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('solarpref'));
1691
- }
1692
- if (heatTypes.heatpump > 0) {
1693
- let hm = this.board.valueMaps.heatModes.transformByName('heatpump');
1694
- heatModes.push(hm);
1695
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('heatpumppref'));
1696
- }
1697
- if (heatTypes.ultratemp > 0) {
1698
- let hm = this.board.valueMaps.heatModes.transformByName('ultratemp');
1699
- heatModes.push(hm);
1700
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('ultratemppref'));
1807
+ // 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)
1808
+ heatModes.push(this.board.valueMaps.heatModes.transformByName('off')); // In IC fw 1.047 off is no longer 0.
1809
+ let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1810
+ if (heatTypes.hybrid > 0) {
1811
+ // RKS: 08-24-22 Unfortunately we mistakenly thought that these needed to be matched to the other heater types. The heat modes
1812
+ // are unique for the hybrid heater.
1813
+ heatModes.push(sys.board.valueMaps.heatModes.transformByName('hybheat'));
1814
+ heatModes.push(sys.board.valueMaps.heatModes.transformByName('hybheatpump'));
1815
+ heatModes.push(sys.board.valueMaps.heatModes.transformByName('hybhybrid'));
1816
+ heatModes.push(sys.board.valueMaps.heatModes.transformByName('hybdual'));
1817
+ //heatModes = this.board.valueMaps.heatModes.toArray();
1818
+ }
1819
+ if (heatTypes.gas > 0) {
1820
+ heatModes.push(this.board.valueMaps.heatModes.transformByName('heater'));
1821
+ }
1822
+ if (heatTypes.mastertemp > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('mtheater'));
1823
+ if (heatTypes.solar > 0) {
1824
+ let hm = this.board.valueMaps.heatModes.transformByName('solar');
1825
+ heatModes.push(hm);
1826
+ if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('solarpref'));
1827
+ }
1828
+ if (heatTypes.heatpump > 0) {
1829
+ let hm = this.board.valueMaps.heatModes.transformByName('heatpump');
1830
+ heatModes.push(hm);
1831
+ if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('heatpumppref'));
1832
+ }
1833
+ if (heatTypes.ultratemp > 0) {
1834
+ let hm = this.board.valueMaps.heatModes.transformByName('ultratemp');
1835
+ heatModes.push(hm);
1836
+ if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('ultratemppref'));
1837
+ }
1838
+ return heatModes;
1701
1839
  }
1702
- return heatModes;
1703
- }
1704
1840
  public getPoolStates(): BodyTempState[] {
1705
1841
  let arrPools = [];
1706
1842
  for (let i = 0; i < state.temps.bodies.length; i++) {
@@ -1833,7 +1969,7 @@ export class PumpCommands extends BoardCommands {
1833
1969
  return this.board.valueMaps.pumpUnits.transform(val);
1834
1970
  }
1835
1971
  }
1836
- public async setPumpAsync(data: any): Promise<Pump> {
1972
+ public async setPumpAsync(data: any, send: boolean = true): Promise<Pump> {
1837
1973
  try {
1838
1974
  let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
1839
1975
  if (id <= 0) id = sys.pumps.filter(elem => elem.master === 1).getMaxId(false, 49) + 1;
@@ -1877,42 +2013,6 @@ export class PumpCommands extends BoardCommands {
1877
2013
  let spump = state.pumps.getItemById(pump.id);
1878
2014
  spump.emitData('pumpExt', spump.getExtended());
1879
2015
  }
1880
-
1881
- public setType(pump: Pump, pumpType: number) {
1882
- // if we are changing pump types, need to clear out circuits
1883
- // and props that aren't for this pump type
1884
- let _id = pump.id;
1885
- if (pump.type !== pumpType || pumpType === 0) {
1886
- let _p = pump.get(true);
1887
- // const _isVirtual = typeof _p.isVirtual !== 'undefined' ? _p.isVirtual : false;
1888
- sys.pumps.removeItemById(_id);
1889
- pump = sys.pumps.getItemById(_id, true);
1890
- /* if (_isVirtual) {
1891
- // pump.isActive = true;
1892
- // pump.isVirtual = true;
1893
- pump.master = 1;
1894
- } */
1895
- state.pumps.removeItemById(pump.id);
1896
- pump.type = pumpType;
1897
- let type = sys.board.valueMaps.pumpTypes.transform(pumpType);
1898
-
1899
- if (type.name === 'vs' || type.name === 'vsf') {
1900
- pump.speedStepSize = 10;
1901
- pump.minSpeed = type.minSpeed;
1902
- pump.maxSpeed = type.maxSpeed;
1903
- }
1904
- if (type.name === 'vf' || type.name === 'vsf') {
1905
- pump.flowStepSize = 1;
1906
- pump.minFlow = type.minFlow;
1907
- pump.maxFlow = type.maxFlow;
1908
- }
1909
- let spump = state.pumps.getItemById(pump.id, true);
1910
- spump.type = pump.type;
1911
- spump.isActive = pump.isActive;
1912
- spump.status = 0;
1913
- spump.emitData('pumpExt', spump.getExtended());
1914
- }
1915
- }
1916
2016
  public availableCircuits() {
1917
2017
  let _availCircuits = [];
1918
2018
  for (let i = 0; i < sys.circuits.length; i++) {
@@ -2110,262 +2210,270 @@ export class CircuitCommands extends BoardCommands {
2110
2210
  }
2111
2211
  } catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); }
2112
2212
  }
2113
- public syncVirtualCircuitStates() {
2114
- try {
2115
- let arrCircuits = sys.board.valueMaps.virtualCircuits.toArray();
2116
- let poolStates = sys.board.bodies.getPoolStates();
2117
- let spaStates = sys.board.bodies.getSpaStates();
2118
- // The following should work for all board types if the virtualCiruit valuemaps use common names. The circuit ids can be
2119
- // different as well as the descriptions but these should have common names since they are all derived from existing states.
2213
+ public syncVirtualCircuitStates() {
2214
+ try {
2215
+ let arrCircuits = sys.board.valueMaps.virtualCircuits.toArray();
2216
+ let poolStates = sys.board.bodies.getPoolStates();
2217
+ let spaStates = sys.board.bodies.getSpaStates();
2218
+ // The following should work for all board types if the virtualCiruit valuemaps use common names. The circuit ids can be
2219
+ // different as well as the descriptions but these should have common names since they are all derived from existing states.
2120
2220
 
2121
- // This also removes virtual circuits depending on whether heaters exsits on the bodies. Not sure why we are doing this
2122
- // as the body data contains whether a body is heated or not. Perhapse some attached interface is using
2123
- // the virtual circuit list as a means to determine whether solar is available. That is totally flawed if that is the case.
2124
- for (let i = 0; i < arrCircuits.length; i++) {
2125
- let vc = arrCircuits[i];
2126
- let remove = false;
2127
- let bState = false;
2128
- let cstate: VirtualCircuitState = null;
2129
- switch (vc.name) {
2130
- case 'poolHeater':
2131
- // If any pool is heating up.
2132
- remove = true;
2133
- for (let j = 0; j < poolStates.length; j++) {
2134
- if (poolStates[j].heaterOptions.total > 0) remove = false;
2135
- }
2136
- if (!remove) {
2137
- // Determine whether the pool heater is on.
2138
- for (let j = 0; j < poolStates.length; j++) {
2139
- if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'heater') {
2140
- // In this instance we may have a delay underway.
2141
- let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name !== 'solar');
2142
- bState = typeof hstate === 'undefined';
2143
- }
2144
- }
2145
- }
2146
- break;
2147
- case 'spaHeater':
2148
- remove = true;
2149
- for (let j = 0; j < spaStates.length; j++) {
2150
- if (spaStates[j].heaterOptions.total > 0) remove = false;
2151
- }
2152
- if (!remove) {
2153
- // Determine whether the spa heater is on.
2154
- for (let j = 0; j < spaStates.length; j++) {
2155
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') {
2156
- // In this instance we may have a delay underway.
2157
- let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name !== 'solar');
2158
- bState = typeof hstate === 'undefined';
2159
- }
2160
- }
2161
- //for (let j = 0; j < spaStates.length; j++) {
2162
- // if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') bState = true;
2163
- //}
2164
- }
2165
- break;
2166
- case 'heater':
2167
- // If heater is on for any body
2168
- // RSG 5-3-22: Heater will now refer to any poolHeater or spaHeater but not solar or other types. anyHeater now takes that role.
2169
- remove = true;
2170
- for (let j = 0; j < poolStates.length; j++) {
2171
- if (poolStates[j].heaterOptions.solar + poolStates[j].heaterOptions.heatpump > 0) remove = false;
2172
- }
2173
- if (remove) {
2174
- for (let j = 0; j < spaStates.length; j++) {
2175
- if (spaStates[j].heaterOptions.solar + spaStates[j].heaterOptions.heatpump > 0) remove = false;
2176
- }
2177
- }
2178
- if (!remove) {
2179
- for (let j = 0; j < poolStates.length && !bState; j++) {
2180
- if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'heater') bState = true;
2181
- }
2182
- for (let j = 0; j < spaStates.length && !bState; j++) {
2183
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') bState = true;
2184
- }
2185
- }
2186
- break;
2187
- case 'freeze':
2188
- // If freeze protection has been turned on.
2189
- bState = state.freeze;
2190
- break;
2191
- case 'poolSpa':
2192
- // If any pool or spa is on
2193
- for (let j = 0; j < poolStates.length && !bState; j++) {
2194
- if (poolStates[j].isOn) bState = true;
2195
- }
2196
- for (let j = 0; j < spaStates.length && !bState; j++) {
2197
- if (spaStates[j].isOn) bState = true;
2198
- }
2199
- break;
2200
- case 'solarHeat':
2201
- case 'solar':
2202
- // If solar is on for any body
2203
- remove = true;
2204
- for (let j = 0; j < poolStates.length; j++) {
2205
- if (poolStates[j].heaterOptions.solar + poolStates[j].heaterOptions.heatpump > 0) remove = false;
2206
- }
2207
- if (remove) {
2208
- for (let j = 0; j < spaStates.length; j++) {
2209
- if (spaStates[j].heaterOptions.solar + spaStates[j].heaterOptions.heatpump > 0) remove = false;
2210
- }
2211
- }
2212
- if (!remove) {
2213
- for (let j = 0; j < poolStates.length && !bState; j++) {
2214
- if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') bState = true;
2215
- }
2216
- for (let j = 0; j < spaStates.length && !bState; j++) {
2217
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') bState = true;
2218
- }
2219
- }
2220
- break;
2221
- case 'solar1':
2222
- remove = true;
2223
- for (let j = 0; j < poolStates.length; j++) {
2224
- if (poolStates[j].id === 1 && poolStates[j].heaterOptions.solar) {
2225
- remove = false;
2226
- vc.desc = `${poolStates[j].name} Solar`;
2227
- if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') {
2228
- // In this instance we may have a delay underway.
2229
- let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name === 'solar');
2230
- bState = typeof hstate === 'undefined';
2231
- }
2232
- }
2233
- }
2234
- for (let j = 0; j < spaStates.length; j++) {
2235
- if (spaStates[j].id === 1 && spaStates[j].heaterOptions.solar) {
2236
- remove = false;
2237
- vc.desc = `${spaStates[j].name} Solar`;
2238
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2239
- // In this instance we may have a delay underway.
2240
- let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name === 'solar');
2241
- bState = typeof hstate === 'undefined';
2242
- }
2243
- }
2244
- }
2221
+ // This also removes virtual circuits depending on whether heaters exsits on the bodies. Not sure why we are doing this
2222
+ // as the body data contains whether a body is heated or not. Perhapse some attached interface is using
2223
+ // the virtual circuit list as a means to determine whether solar is available. That is totally flawed if that is the case.
2224
+ for (let i = 0; i < arrCircuits.length; i++) {
2225
+ let vc = arrCircuits[i];
2226
+ let remove = false;
2227
+ let bState = false;
2228
+ let cstate: VirtualCircuitState = null;
2229
+ switch (vc.name) {
2230
+ case 'poolHeater':
2231
+ // If any pool is heating up.
2232
+ remove = true;
2233
+ for (let j = 0; j < poolStates.length; j++) {
2234
+ if (poolStates[j].heaterOptions.total > 0) remove = false;
2235
+ }
2236
+ if (!remove) {
2237
+ // Determine whether the pool heater is on.
2238
+ for (let j = 0; j < poolStates.length; j++) {
2239
+ let hstatus = sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus);
2240
+ if (hstatus !== 'off' && hstatus !== 'solar') {
2241
+ // In this instance we may have a delay underway.
2242
+ let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name !== 'solar');
2243
+ bState = typeof hstate === 'undefined';
2244
+ }
2245
+ }
2246
+ }
2247
+ break;
2248
+ case 'spaHeater':
2249
+ remove = true;
2250
+ for (let j = 0; j < spaStates.length; j++) {
2251
+ if (spaStates[j].heaterOptions.total > 0) remove = false;
2252
+ }
2253
+ if (!remove) {
2254
+ // Determine whether the spa heater is on.
2255
+ for (let j = 0; j < spaStates.length; j++) {
2256
+ let hstatus = sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus);
2257
+ if (hstatus !== 'off' && hstatus !== 'solar') {
2258
+ // In this instance we may have a delay underway.
2259
+ let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name !== 'solar');
2260
+ bState = typeof hstate === 'undefined';
2261
+ }
2262
+ }
2263
+ //for (let j = 0; j < spaStates.length; j++) {
2264
+ // if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') bState = true;
2265
+ //}
2266
+ }
2267
+ break;
2268
+ case 'heater':
2269
+ // If heater is on for any body
2270
+ // RSG 5-3-22: Heater will now refer to any poolHeater or spaHeater but not solar or other types. anyHeater now takes that role.
2271
+ remove = true;
2272
+ for (let j = 0; j < poolStates.length; j++) {
2273
+ if (poolStates[j].heaterOptions.solar + poolStates[j].heaterOptions.heatpump > 0) remove = false;
2274
+ }
2275
+ if (remove) {
2276
+ for (let j = 0; j < spaStates.length; j++) {
2277
+ if (spaStates[j].heaterOptions.solar + spaStates[j].heaterOptions.heatpump > 0) remove = false;
2278
+ }
2279
+ }
2280
+ if (!remove) {
2281
+ for (let j = 0; j < poolStates.length && !bState; j++) {
2282
+ let hstatus = sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus);
2283
+ if (hstatus === 'heater' || hstatus === 'hpheat' || hstatus === 'mtheat' || hstatus === 'hybheat') bState = true;
2284
+ }
2285
+ for (let j = 0; j < spaStates.length && !bState; j++) {
2286
+ let hstatus = sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus);
2287
+ if (hstatus === 'heater' || hstatus === 'hpheat' || hstatus === 'mtheat' || hstatus === 'hybheat') bState = true;
2288
+ }
2289
+ }
2290
+ break;
2291
+ case 'freeze':
2292
+ // If freeze protection has been turned on.
2293
+ bState = state.freeze;
2294
+ break;
2295
+ case 'poolSpa':
2296
+ // If any pool or spa is on
2297
+ for (let j = 0; j < poolStates.length && !bState; j++) {
2298
+ if (poolStates[j].isOn) bState = true;
2299
+ }
2300
+ for (let j = 0; j < spaStates.length && !bState; j++) {
2301
+ if (spaStates[j].isOn) bState = true;
2302
+ }
2303
+ break;
2304
+ case 'solarHeat':
2305
+ case 'solar':
2306
+ // If solar is on for any body
2307
+ remove = true;
2308
+ for (let j = 0; j < poolStates.length; j++) {
2309
+ // RKS: 05-30-22 - I have no idea why this would include the heatpump options
2310
+ //if (poolStates[j].heaterOptions.solar + poolStates[j].heaterOptions.heatpump > 0) remove = false;
2311
+ if (poolStates[j].heaterOptions.solar) remove = false;
2312
+ }
2313
+ if (remove) {
2314
+ for (let j = 0; j < spaStates.length; j++) {
2315
+ // RKS: 05-30-22 - I have no idea why this would include the heatpump options
2316
+ //if (spaStates[j].heaterOptions.solar + spaStates[j].heaterOptions.heatpump > 0) remove = false;
2317
+ if (spaStates[j].heaterOptions.solar) remove = false;
2318
+ }
2319
+ }
2320
+ if (!remove) {
2321
+ for (let j = 0; j < poolStates.length && !bState; j++) {
2322
+ if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') bState = true;
2323
+ }
2324
+ for (let j = 0; j < spaStates.length && !bState; j++) {
2325
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') bState = true;
2326
+ }
2327
+ }
2328
+ break;
2329
+ case 'solar1':
2330
+ remove = true;
2331
+ for (let j = 0; j < poolStates.length; j++) {
2332
+ if (poolStates[j].id === 1 && poolStates[j].heaterOptions.solar) {
2333
+ remove = false;
2334
+ vc.desc = `${poolStates[j].name} Solar`;
2335
+ if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') {
2336
+ // In this instance we may have a delay underway.
2337
+ let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name === 'solar');
2338
+ bState = typeof hstate === 'undefined';
2339
+ }
2340
+ }
2341
+ }
2342
+ for (let j = 0; j < spaStates.length; j++) {
2343
+ if (spaStates[j].id === 1 && spaStates[j].heaterOptions.solar) {
2344
+ remove = false;
2345
+ vc.desc = `${spaStates[j].name} Solar`;
2346
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2347
+ // In this instance we may have a delay underway.
2348
+ let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name === 'solar');
2349
+ bState = typeof hstate === 'undefined';
2350
+ }
2351
+ }
2352
+ }
2245
2353
 
2246
- break;
2247
- case 'solar2':
2248
- remove = true;
2249
- for (let j = 0; j < poolStates.length; j++) {
2250
- if (poolStates[j].id === 2 && poolStates[j].heaterOptions.solar) {
2251
- remove = false;
2252
- vc.desc = `${poolStates[j].name} Solar`;
2253
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2254
- // In this instance we may have a delay underway.
2255
- let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name === 'solar');
2256
- bState = typeof hstate === 'undefined';
2257
- }
2258
- }
2259
- }
2260
- for (let j = 0; j < spaStates.length; j++) {
2261
- if (spaStates[j].id === 2 && spaStates[j].heaterOptions.solar) {
2262
- remove = false;
2263
- vc.desc = `${spaStates[j].name} Solar`;
2264
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2265
- // In this instance we may have a delay underway.
2266
- let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name === 'solar');
2267
- bState = typeof hstate === 'undefined';
2268
- }
2269
- }
2270
- }
2271
- break;
2272
- case 'solar3':
2273
- remove = true;
2274
- for (let j = 0; j < poolStates.length; j++) {
2275
- if (poolStates[j].id === 3 && poolStates[j].heaterOptions.solar) {
2276
- remove = false;
2277
- vc.desc = `${poolStates[j].name} Solar`;
2278
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2279
- // In this instance we may have a delay underway.
2280
- let hstate = state.heaters.find(x => x.bodyId === 3 && x.startupDelay === true && x.type.name === 'solar');
2281
- bState = typeof hstate === 'undefined';
2282
- }
2283
- }
2284
- }
2285
- for (let j = 0; j < spaStates.length; j++) {
2286
- if (spaStates[j].id === 3 && spaStates[j].heaterOptions.solar) {
2287
- remove = false;
2288
- vc.desc = `${spaStates[j].name} Solar`;
2289
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2290
- // In this instance we may have a delay underway.
2291
- let hstate = state.heaters.find(x => x.bodyId === 3 && x.startupDelay === true && x.type.name === 'solar');
2292
- bState = typeof hstate === 'undefined';
2293
- }
2294
- }
2295
- }
2354
+ break;
2355
+ case 'solar2':
2356
+ remove = true;
2357
+ for (let j = 0; j < poolStates.length; j++) {
2358
+ if (poolStates[j].id === 2 && poolStates[j].heaterOptions.solar) {
2359
+ remove = false;
2360
+ vc.desc = `${poolStates[j].name} Solar`;
2361
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2362
+ // In this instance we may have a delay underway.
2363
+ let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name === 'solar');
2364
+ bState = typeof hstate === 'undefined';
2365
+ }
2366
+ }
2367
+ }
2368
+ for (let j = 0; j < spaStates.length; j++) {
2369
+ if (spaStates[j].id === 2 && spaStates[j].heaterOptions.solar) {
2370
+ remove = false;
2371
+ vc.desc = `${spaStates[j].name} Solar`;
2372
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2373
+ // In this instance we may have a delay underway.
2374
+ let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name === 'solar');
2375
+ bState = typeof hstate === 'undefined';
2376
+ }
2377
+ }
2378
+ }
2379
+ break;
2380
+ case 'solar3':
2381
+ remove = true;
2382
+ for (let j = 0; j < poolStates.length; j++) {
2383
+ if (poolStates[j].id === 3 && poolStates[j].heaterOptions.solar) {
2384
+ remove = false;
2385
+ vc.desc = `${poolStates[j].name} Solar`;
2386
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2387
+ // In this instance we may have a delay underway.
2388
+ let hstate = state.heaters.find(x => x.bodyId === 3 && x.startupDelay === true && x.type.name === 'solar');
2389
+ bState = typeof hstate === 'undefined';
2390
+ }
2391
+ }
2392
+ }
2393
+ for (let j = 0; j < spaStates.length; j++) {
2394
+ if (spaStates[j].id === 3 && spaStates[j].heaterOptions.solar) {
2395
+ remove = false;
2396
+ vc.desc = `${spaStates[j].name} Solar`;
2397
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2398
+ // In this instance we may have a delay underway.
2399
+ let hstate = state.heaters.find(x => x.bodyId === 3 && x.startupDelay === true && x.type.name === 'solar');
2400
+ bState = typeof hstate === 'undefined';
2401
+ }
2402
+ }
2403
+ }
2296
2404
 
2297
- break;
2298
- case 'solar4':
2299
- remove = true;
2300
- for (let j = 0; j < poolStates.length; j++) {
2301
- if (poolStates[j].id === 4 && poolStates[j].heaterOptions.solar) {
2302
- remove = false;
2303
- vc.desc = `${poolStates[j].name} Solar`;
2304
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2305
- // In this instance we may have a delay underway.
2306
- let hstate = state.heaters.find(x => x.bodyId === 4 && x.startupDelay === true && x.type.name === 'solar');
2307
- bState = typeof hstate === 'undefined';
2405
+ break;
2406
+ case 'solar4':
2407
+ remove = true;
2408
+ for (let j = 0; j < poolStates.length; j++) {
2409
+ if (poolStates[j].id === 4 && poolStates[j].heaterOptions.solar) {
2410
+ remove = false;
2411
+ vc.desc = `${poolStates[j].name} Solar`;
2412
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2413
+ // In this instance we may have a delay underway.
2414
+ let hstate = state.heaters.find(x => x.bodyId === 4 && x.startupDelay === true && x.type.name === 'solar');
2415
+ bState = typeof hstate === 'undefined';
2416
+ }
2417
+ }
2418
+ }
2419
+ for (let j = 0; j < spaStates.length; j++) {
2420
+ if (spaStates[j].id === 4 && spaStates[j].heaterOptions.solar) {
2421
+ remove = false;
2422
+ vc.desc = `${spaStates[j].name} Solar`;
2423
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2424
+ // In this instance we may have a delay underway.
2425
+ let hstate = state.heaters.find(x => x.bodyId === 4 && x.startupDelay === true && x.type.name === 'solar');
2426
+ bState = typeof hstate === 'undefined';
2427
+ }
2428
+ }
2429
+ }
2430
+ break;
2431
+ case 'anyHeater':
2432
+ // RSG 5-3-22 anyHeater now represents any solar, gas, etc heater. This replaces 'heater' which now refers to only gas heaters.
2433
+ remove = true;
2434
+ for (let j = 0; j < poolStates.length; j++) {
2435
+ if (poolStates[j].heaterOptions.total > 0) remove = false;
2436
+ }
2437
+ if (remove) {
2438
+ for (let j = 0; j < spaStates.length; j++) {
2439
+ if (spaStates[j].heaterOptions.total > 0) remove = false;
2440
+ }
2441
+ }
2442
+ if (!remove) {
2443
+ for (let j = 0; j < poolStates.length && !bState; j++) {
2444
+ let heat = sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus);
2445
+ if (heat !== 'off') bState = true;
2446
+ }
2447
+ for (let j = 0; j < spaStates.length && !bState; j++) {
2448
+ let heat = sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus);
2449
+ if (heat !== 'off') bState = true;
2450
+ }
2451
+ }
2452
+ break;
2453
+ default:
2454
+ remove = true;
2455
+ break;
2308
2456
  }
2309
- }
2310
- }
2311
- for (let j = 0; j < spaStates.length; j++) {
2312
- if (spaStates[j].id === 4 && spaStates[j].heaterOptions.solar) {
2313
- remove = false;
2314
- vc.desc = `${spaStates[j].name} Solar`;
2315
- if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2316
- // In this instance we may have a delay underway.
2317
- let hstate = state.heaters.find(x => x.bodyId === 4 && x.startupDelay === true && x.type.name === 'solar');
2318
- bState = typeof hstate === 'undefined';
2457
+ if (remove) {
2458
+ if (state.virtualCircuits.exists(x => vc.val === x.id)) {
2459
+ cstate = state.virtualCircuits.getItemById(vc.val, true);
2460
+ cstate.isActive = false;
2461
+ cstate.emitEquipmentChange();
2462
+ }
2463
+ state.virtualCircuits.removeItemById(vc.val);
2464
+ }
2465
+ else {
2466
+ cstate = state.virtualCircuits.getItemById(vc.val, true);
2467
+ cstate.isActive = true;
2468
+ if (cstate !== null) {
2469
+ cstate.isOn = bState;
2470
+ cstate.type = vc.val;
2471
+ cstate.name = vc.desc;
2472
+ }
2319
2473
  }
2320
- }
2321
- }
2322
- break;
2323
- case 'anyHeater':
2324
- // RSG 5-3-22 anyHeater now represents any solar, gas, etc heater. This replaces 'heater' which now refers to only gas heaters.
2325
- remove = true;
2326
- for (let j = 0; j < poolStates.length; j++) {
2327
- if (poolStates[j].heaterOptions.total > 0) remove = false;
2328
- }
2329
- if (remove) {
2330
- for (let j = 0; j < spaStates.length; j++) {
2331
- if (spaStates[j].heaterOptions.total > 0) remove = false;
2332
- }
2333
- }
2334
- if (!remove) {
2335
- for (let j = 0; j < poolStates.length && !bState; j++) {
2336
- let heat = sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus);
2337
- if (heat !== 'off') bState = true;
2338
- }
2339
- for (let j = 0; j < spaStates.length && !bState; j++) {
2340
- let heat = sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus);
2341
- if (heat !== 'off') bState = true;
2342
- }
2343
2474
  }
2344
- break;
2345
- default:
2346
- remove = true;
2347
- break;
2348
- }
2349
- if (remove) {
2350
- if (state.virtualCircuits.exists(x => vc.val === x.id)) {
2351
- cstate = state.virtualCircuits.getItemById(vc.val, true);
2352
- cstate.isActive = false;
2353
- cstate.emitEquipmentChange();
2354
- }
2355
- state.virtualCircuits.removeItemById(vc.val);
2356
- }
2357
- else {
2358
- cstate = state.virtualCircuits.getItemById(vc.val, true);
2359
- cstate.isActive = true;
2360
- if (cstate !== null) {
2361
- cstate.isOn = bState;
2362
- cstate.type = vc.val;
2363
- cstate.name = vc.desc;
2364
- }
2365
- }
2366
- }
2367
- } catch (err) { logger.error(`Error syncronizing virtual circuits`); }
2368
- }
2475
+ } catch (err) { logger.error(`Error synchronizing virtual circuits`); }
2476
+ }
2369
2477
  public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
2370
2478
  sys.board.suspendStatus(true);
2371
2479
  try {
@@ -2443,60 +2551,50 @@ export class CircuitCommands extends BoardCommands {
2443
2551
  await sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, false);
2444
2552
  //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence));
2445
2553
  }
2446
- await utils.sleep(10000);
2554
+ await setTimeout(10000);
2447
2555
  for (let i = 0; i < arrCircs.length; i++) {
2448
2556
  await sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, true);
2449
2557
  //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence));
2450
2558
  }
2451
-
2452
- //if (proms.length > 0) {
2453
- // //await Promise.all(proms);
2454
- // // Let it simmer for 6 seconds then turn it off and back on.
2455
- // proms.length = 0;
2456
- // for (let i = 0; i < arrCircs.length; i++) {
2457
- // proms.push(sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, false));
2458
- // }
2459
- // await Promise.all(proms);
2460
- // // Let it be off for 3 seconds then turn it back on.
2461
- // await utils.sleep(10000);
2462
- // proms.length = 0;
2463
- // for (let i = 0; i < arrCircs.length; i++) {
2464
- // proms.push(sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, true));
2465
- // }
2466
- // await Promise.all(proms);
2467
- //}
2468
2559
  sgrp.action = 0;
2469
2560
  sgrp.emitEquipmentChange();
2470
2561
  return state.lightGroups.getItemById(id);
2471
2562
  }
2472
2563
  catch (err) { return Promise.reject(`Error runLightGroupCommandAsync ${err.message}`); }
2473
2564
  }
2474
- public async runLightCommandAsync(obj: any): Promise<ICircuitState> {
2475
- // Do all our validation.
2476
- try {
2477
- let id = parseInt(obj.id, 10);
2478
- let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightCommands.findItem(obj.command) : { val: 0, name: 'undefined' };
2479
- if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light command ${cmd.name} does not exist`, 'runLightCommandAsync'));
2480
- if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light ${id} does not exist`, 'runLightCommandAsync'));
2481
- let circ = sys.circuits.getItemById(id);
2482
- if (!circ.isActive) return Promise.reject(new InvalidOperationError(`Light circuit #${id} is not active`, 'runLightCommandAsync'));
2483
- let type = sys.board.valueMaps.circuitFunctions.transform(circ.type);
2484
- if (!type.isLight) return Promise.reject(new InvalidOperationError(`Circuit #${id} is not a light`, 'runLightCommandAsync'));
2485
- let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name);
2486
- let slight = state.circuits.getItemById(circ.id);
2487
- slight.action = nop;
2488
- console.log(nop);
2489
- slight.emitEquipmentChange();
2490
- await ncp.circuits.sendOnOffSequenceAsync(circ.id, cmd.sequence);
2491
- await utils.sleep(7000);
2492
- await sys.board.circuits.setCircuitStateAsync(circ.id, false);
2493
- await sys.board.circuits.setCircuitStateAsync(circ.id, true);
2494
- slight.action = 0;
2495
- slight.emitEquipmentChange();
2496
- return slight;
2565
+ public async runLightCommandAsync(obj: any): Promise<ICircuitState> {
2566
+ // Do all our validation.
2567
+ try {
2568
+ let id = parseInt(obj.id, 10);
2569
+ let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightCommands.findItem(obj.command) : { val: 0, name: 'undefined' };
2570
+ if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light command ${cmd.name} does not exist`, 'runLightCommandAsync'));
2571
+ if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light ${id} does not exist`, 'runLightCommandAsync'));
2572
+ let circ = sys.circuits.getItemById(id);
2573
+ if (!circ.isActive) return Promise.reject(new InvalidOperationError(`Light circuit #${id} is not active`, 'runLightCommandAsync'));
2574
+ let type = sys.board.valueMaps.circuitFunctions.transform(circ.type);
2575
+ if (!type.isLight) return Promise.reject(new InvalidOperationError(`Circuit #${id} is not a light`, 'runLightCommandAsync'));
2576
+ let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name);
2577
+ let slight = state.circuits.getItemById(circ.id);
2578
+ slight.action = nop;
2579
+ console.log(nop);
2580
+ slight.emitEquipmentChange();
2581
+ await ncp.circuits.sendOnOffSequenceAsync(circ.id, cmd.sequence);
2582
+ if (cmd.sequence.length > 0) {
2583
+ await sys.board.circuits.setCircuitStateAsync(circ.id, cmd.sequence[cmd.sequence.length - 1].isOn);
2584
+ }
2585
+ if (typeof cmd.endingTheme !== 'undefined') {
2586
+ let thm = sys.board.valueMaps.lightThemes.findItem(cmd.endingTheme);
2587
+ if (typeof thm !== 'undefined') slight.lightingTheme = circ.lightingTheme = thm.val;
2588
+ }
2589
+ //await setTimeout(7000);
2590
+ //await sys.board.circuits.setCircuitStateAsync(circ.id, false);
2591
+ //await sys.board.circuits.setCircuitStateAsync(circ.id, true);
2592
+ slight.action = 0;
2593
+ slight.emitEquipmentChange();
2594
+ return slight;
2595
+ }
2596
+ catch (err) { return Promise.reject(`Error runLightCommandAsync ${err.message}`); }
2497
2597
  }
2498
- catch (err) { return Promise.reject(`Error runLightCommandAsync ${err.message}`); }
2499
- }
2500
2598
  public async setLightThemeAsync(id: number, theme: number): Promise<ICircuitState> {
2501
2599
  let cstate = state.circuits.getItemById(id);
2502
2600
  let circ = sys.circuits.getItemById(id);
@@ -2507,7 +2605,7 @@ export class CircuitCommands extends BoardCommands {
2507
2605
  try {
2508
2606
  if (typeof thm !== 'undefined' && typeof thm.sequence !== 'undefined' && circ.master === 1) {
2509
2607
  await sys.board.circuits.setCircuitStateAsync(id, true);
2510
- await ncp.circuits.sendOnOffSequenceAsync(id, thm.sequence);
2608
+ await ncp.circuits.setLightThemeAsync(id, thm);
2511
2609
  }
2512
2610
  cstate.lightingTheme = theme;
2513
2611
  return cstate;
@@ -2595,63 +2693,64 @@ export class CircuitCommands extends BoardCommands {
2595
2693
  }
2596
2694
  public getLightThemes(type?: number) { return sys.board.valueMaps.lightThemes.toArray(); }
2597
2695
  public getCircuitFunctions() {
2598
- let cf = sys.board.valueMaps.circuitFunctions.toArray();
2599
- if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' });
2600
- return cf;
2601
- }
2602
- public getCircuitNames() { return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()]; }
2603
- public async setCircuitAsync(data: any): Promise<ICircuit> {
2604
- try {
2605
- let id = parseInt(data.id, 10);
2606
- if (id <= 0 || typeof data.id === 'undefined') {
2607
- // We are adding a new circuit. If we are operating as a nixie controller then we need to start this
2608
- // circuit outside the range of circuits that can be defined on the panel. For any of the non-OCP controllers
2609
- // these are added within the range of the circuits starting with 1. For all others these are added with an id > 255.
2610
- switch (state.equipment.controllerType) {
2611
- case 'intellicenter':
2612
- case 'intellitouch':
2613
- case 'easytouch':
2614
- id = sys.circuits.getNextEquipmentId(new EquipmentIdRange(255, 300));
2615
- break;
2616
- default:
2617
- id = sys.circuits.getNextEquipmentId(sys.board.equipmentIds.circuits, [1, 6]);
2618
- break;
2619
- }
2620
- }
2621
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.id}`, data.id, 'Circuit'));
2622
- //if (!sys.board.equipmentIds.circuits.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit id is out of range: ${id}`, data.id, 'Circuit'));;
2623
- if (typeof data.id !== 'undefined') {
2624
- let circuit = sys.circuits.getItemById(id, true);
2625
- let scircuit = state.circuits.getItemById(id, true);
2626
- scircuit.isActive = circuit.isActive = true;
2627
- circuit.master = 1;
2628
- scircuit.isOn = false;
2629
- if (data.name) circuit.name = scircuit.name = data.name;
2630
- else if (!circuit.name && !data.name) circuit.name = scircuit.name = `circuit${data.id}`;
2631
- if (typeof data.type !== 'undefined' || typeof circuit.type === 'undefined') {
2632
- circuit.type = scircuit.type = parseInt(data.type, 10) || 0;
2633
- }
2634
- if (id === 6) circuit.type = sys.board.valueMaps.circuitFunctions.getValue('pool');
2635
- if (id === 1 && sys.equipment.shared) circuit.type = sys.board.valueMaps.circuitFunctions.getValue('spa');
2636
- if (typeof data.freeze !== 'undefined' || typeof circuit.freeze === 'undefined') circuit.freeze = utils.makeBool(data.freeze) || false;
2637
- if (typeof data.showInFeatures !== 'undefined' || typeof data.showInFeatures === 'undefined') circuit.showInFeatures = scircuit.showInFeatures = utils.makeBool(data.showInFeatures) || true;
2638
- if (typeof data.dontStop !== 'undefined' && utils.makeBool(data.dontStop) === true) data.eggTimer = 1440;
2639
- if (typeof data.eggTimer !== 'undefined' || typeof circuit.eggTimer === 'undefined') circuit.eggTimer = parseInt(data.eggTimer, 10) || 0;
2640
- if (typeof data.connectionId !== 'undefined') circuit.connectionId = data.connectionId;
2641
- if (typeof data.deviceBinding !== 'undefined') circuit.deviceBinding = data.deviceBinding;
2642
- if (typeof data.showInFeatures !== 'undefined') scircuit.showInFeatures = circuit.showInFeatures = utils.makeBool(data.showInFeatures);
2643
- circuit.dontStop = circuit.eggTimer === 1440;
2696
+ let cf = sys.board.valueMaps.circuitFunctions.toArray();
2697
+ if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' });
2698
+ return cf;
2699
+ }
2700
+ public getCircuitNames() { return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()]; }
2701
+ public async setCircuitAsync(data: any, send: boolean = true): Promise<ICircuit> {
2702
+ try {
2703
+ let id = parseInt(data.id, 10);
2704
+ if (id <= 0 || typeof data.id === 'undefined') {
2705
+ // We are adding a new circuit. If we are operating as a nixie controller then we need to start this
2706
+ // circuit outside the range of circuits that can be defined on the panel. For any of the non-OCP controllers
2707
+ // these are added within the range of the circuits starting with 1. For all others these are added with an id > 255.
2708
+ switch (state.equipment.controllerType) {
2709
+ case 'intellicenter':
2710
+ case 'intellitouch':
2711
+ case 'easytouch':
2712
+ case 'suntouch':
2713
+ id = sys.circuits.getNextEquipmentId(new EquipmentIdRange(255, 300));
2714
+ break;
2715
+ default:
2716
+ id = sys.circuits.getNextEquipmentId(sys.board.equipmentIds.circuits, [1, 6]);
2717
+ break;
2718
+ }
2719
+ }
2720
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.id}`, data.id, 'Circuit'));
2721
+ //if (!sys.board.equipmentIds.circuits.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit id is out of range: ${id}`, data.id, 'Circuit'));;
2722
+ if (typeof data.id !== 'undefined') {
2723
+ let circuit = sys.circuits.getItemById(id, true);
2724
+ let scircuit = state.circuits.getItemById(id, true);
2725
+ scircuit.isActive = circuit.isActive = true;
2726
+ circuit.master = 1;
2727
+ scircuit.isOn = false;
2728
+ if (data.name) circuit.name = scircuit.name = data.name;
2729
+ else if (!circuit.name && !data.name) circuit.name = scircuit.name = `circuit${data.id}`;
2730
+ if (typeof data.type !== 'undefined' || typeof circuit.type === 'undefined') {
2731
+ circuit.type = scircuit.type = parseInt(data.type, 10) || 0;
2732
+ }
2733
+ if (id === 6) circuit.type = sys.board.valueMaps.circuitFunctions.getValue('pool');
2734
+ if (id === 1 && sys.equipment.shared) circuit.type = sys.board.valueMaps.circuitFunctions.getValue('spa');
2735
+ if (typeof data.freeze !== 'undefined' || typeof circuit.freeze === 'undefined') circuit.freeze = utils.makeBool(data.freeze) || false;
2736
+ if (typeof data.showInFeatures !== 'undefined' || typeof data.showInFeatures === 'undefined') circuit.showInFeatures = scircuit.showInFeatures = utils.makeBool(data.showInFeatures);
2737
+ if (typeof data.dontStop !== 'undefined' && utils.makeBool(data.dontStop) === true) data.eggTimer = 1440;
2738
+ if (typeof data.eggTimer !== 'undefined' || typeof circuit.eggTimer === 'undefined') circuit.eggTimer = parseInt(data.eggTimer, 10) || 0;
2739
+ if (typeof data.connectionId !== 'undefined') circuit.connectionId = data.connectionId;
2740
+ if (typeof data.deviceBinding !== 'undefined') circuit.deviceBinding = data.deviceBinding;
2741
+ if (typeof data.showInFeatures !== 'undefined') scircuit.showInFeatures = circuit.showInFeatures = utils.makeBool(data.showInFeatures);
2742
+ circuit.dontStop = circuit.eggTimer === 1440;
2644
2743
 
2645
- sys.emitEquipmentChange();
2646
- state.emitEquipmentChanges();
2647
- if (circuit.master === 1) await ncp.circuits.setCircuitAsync(circuit, data);
2648
- return Promise.resolve(circuit);
2649
- }
2650
- else
2651
- return Promise.reject(new Error('Circuit id has not been defined'));
2744
+ sys.emitEquipmentChange();
2745
+ state.emitEquipmentChanges();
2746
+ if (circuit.master === 1) await ncp.circuits.setCircuitAsync(circuit, data);
2747
+ return Promise.resolve(circuit);
2748
+ }
2749
+ else
2750
+ return Promise.reject(new Error('Circuit id has not been defined'));
2751
+ }
2752
+ catch (err) { logger.error(`setCircuitAsync error with ${data}. ${err}`); return Promise.reject(err); }
2652
2753
  }
2653
- catch (err) { logger.error(`setCircuitAsync error with ${data}. ${err}`); return Promise.reject(err); }
2654
- }
2655
2754
  public async setCircuitGroupAsync(obj: any): Promise<CircuitGroup> {
2656
2755
  let group: CircuitGroup = null;
2657
2756
  let sgroup: CircuitGroupState = null;
@@ -2714,7 +2813,7 @@ export class CircuitCommands extends BoardCommands {
2714
2813
  });
2715
2814
 
2716
2815
  }
2717
- public async setLightGroupAsync(obj: any): Promise<LightGroup> {
2816
+ public async setLightGroupAsync(obj: any, send: boolean = true): Promise<LightGroup> {
2718
2817
  let group: LightGroup = null;
2719
2818
  let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1;
2720
2819
  if (id <= 0) {
@@ -2885,7 +2984,7 @@ export class CircuitCommands extends BoardCommands {
2885
2984
  if (nop > 0) {
2886
2985
  sgroup.action = nop;
2887
2986
  sgroup.emitEquipmentChange();
2888
- await utils.sleep(10000);
2987
+ await setTimeout(10000);
2889
2988
  sgroup.action = 0;
2890
2989
  state.emitAllEquipmentChanges();
2891
2990
  }
@@ -3112,32 +3211,33 @@ export class FeatureCommands extends BoardCommands {
3112
3211
  } catch (err) { logger.error(`Error validating features for restore: ${err.message}`); }
3113
3212
  }
3114
3213
 
3115
- public async setFeatureAsync(obj: any): Promise<Feature> {
3116
- let id = parseInt(obj.id, 10);
3117
- if (id <= 0 || isNaN(id)) {
3118
- id = sys.features.getNextEquipmentId(sys.board.equipmentIds.features);
3119
- }
3120
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${obj.id}`, obj.id, 'Feature'));
3121
- if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Feature id out of range: ${id}: ${sys.board.equipmentIds.features.start} to ${sys.board.equipmentIds.features.end}`, obj.id, 'Feature'));
3122
- let feature = sys.features.getItemById(id, true);
3123
- let sfeature = state.features.getItemById(id, true);
3124
- feature.isActive = true;
3125
- sfeature.isOn = false;
3126
- if (obj.nameId) {
3127
- feature.nameId = sfeature.nameId = obj.nameId;
3128
- feature.name = sfeature.name = sys.board.valueMaps.circuitNames.get(obj.nameId);
3214
+ public async setFeatureAsync(obj: any): Promise<Feature> {
3215
+ let id = parseInt(obj.id, 10);
3216
+ if (id <= 0 || isNaN(id)) {
3217
+ id = sys.features.getNextEquipmentId(sys.board.equipmentIds.features);
3218
+ }
3219
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${obj.id}`, obj.id, 'Feature'));
3220
+ if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Feature id out of range: ${id}: ${sys.board.equipmentIds.features.start} to ${sys.board.equipmentIds.features.end}`, obj.id, 'Feature'));
3221
+ let feature = sys.features.getItemById(id, true);
3222
+ let sfeature = state.features.getItemById(id, true);
3223
+ feature.isActive = true;
3224
+ sfeature.isOn = false;
3225
+ if (obj.nameId) {
3226
+ feature.nameId = sfeature.nameId = obj.nameId;
3227
+ feature.name = sfeature.name = sys.board.valueMaps.circuitNames.get(obj.nameId);
3228
+ }
3229
+ else if (obj.name) feature.name = sfeature.name = obj.name;
3230
+ else if (!feature.name && !obj.name) feature.name = sfeature.name = `feature${obj.id}`;
3231
+ if (typeof obj.type !== 'undefined') feature.type = sfeature.type = parseInt(obj.type, 10);
3232
+ else if (!feature.type && typeof obj.type !== 'undefined') feature.type = sfeature.type = 0;
3233
+ if (typeof obj.freeze !== 'undefined') feature.freeze = utils.makeBool(obj.freeze);
3234
+ if (typeof obj.showInFeatures !== 'undefined') feature.showInFeatures = sfeature.showInFeatures = utils.makeBool(obj.showInFeatures);
3235
+ sfeature.showInFeatures = feature.showInFeatures;
3236
+ if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440;
3237
+ if (typeof obj.eggTimer !== 'undefined') feature.eggTimer = parseInt(obj.eggTimer, 10);
3238
+ feature.dontStop = feature.eggTimer === 1440;
3239
+ return new Promise<Feature>((resolve, reject) => { resolve(feature); });
3129
3240
  }
3130
- else if (obj.name) feature.name = sfeature.name = obj.name;
3131
- else if (!feature.name && !obj.name) feature.name = sfeature.name = `feature${obj.id}`;
3132
- if (typeof obj.type !== 'undefined') feature.type = sfeature.type = parseInt(obj.type, 10);
3133
- else if (!feature.type && typeof obj.type !== 'undefined') feature.type = sfeature.type = 0;
3134
- if (typeof obj.freeze !== 'undefined') feature.freeze = utils.makeBool(obj.freeze);
3135
- if (typeof obj.showInFeatures !== 'undefined') feature.showInFeatures = sfeature.showInFeatures = utils.makeBool(obj.showInFeatures);
3136
- if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440;
3137
- if (typeof obj.eggTimer !== 'undefined') feature.eggTimer = parseInt(obj.eggTimer, 10);
3138
- feature.dontStop = feature.eggTimer === 1440;
3139
- return new Promise<Feature>((resolve, reject) => { resolve(feature); });
3140
- }
3141
3241
  public async deleteFeatureAsync(obj: any): Promise<Feature> {
3142
3242
  let id = parseInt(obj.id, 10);
3143
3243
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${obj.id}`, obj.id, 'Feature'));
@@ -3278,7 +3378,7 @@ export class ChlorinatorCommands extends BoardCommands {
3278
3378
  } catch (err) { logger.error(`Error validating chlorinators for restore: ${err.message}`); }
3279
3379
  }
3280
3380
 
3281
- public async setChlorAsync(obj: any): Promise<ChlorinatorState> {
3381
+ public async setChlorAsync(obj: any, send: boolean = true): Promise<ChlorinatorState> {
3282
3382
  try {
3283
3383
  let id = parseInt(obj.id, 10);
3284
3384
  let chlor: Chlorinator;
@@ -3338,6 +3438,7 @@ export class ChlorinatorCommands extends BoardCommands {
3338
3438
  schlor.model = chlor.model = typeof obj.model !== 'undefined' ? sys.board.valueMaps.chlorinatorModel.encode(obj.model) : chlor.model;
3339
3439
  chlor.type = schlor.type = typeof obj.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(obj.type) : chlor.type || 0;
3340
3440
  chlor.body = schlor.body = body.val;
3441
+ chlor.address = typeof obj.address !== 'undefined' ? parseInt(obj.address,10) : 80;
3341
3442
  schlor.poolSetpoint = chlor.poolSetpoint = poolSetpoint;
3342
3443
  schlor.spaSetpoint = chlor.spaSetpoint = spaSetpoint;
3343
3444
  chlor.ignoreSaltReading = typeof obj.ignoreSaltReading !== 'undefined' ? utils.makeBool(obj.ignoreSaltReading) : utils.makeBool(chlor.ignoreSaltReading);
@@ -3506,16 +3607,16 @@ export class ScheduleCommands extends BoardCommands {
3506
3607
  // if scheduleDays includes today
3507
3608
  if (days.includes(state.time.toDate().getDay())) {
3508
3609
  if (sched.changeHeatSetpoint && (sched.heatSource as any).val !== sys.board.valueMaps.heatSources.getValue('off') && sched.heatSetpoint > 0 && sched.heatSetpoint !== tbody.setPoint) {
3509
- setTimeout(() => sys.board.bodies.setHeatSetpointAsync(cbody, sched.heatSetpoint), 100);
3610
+ setTimeoutSync(() => sys.board.bodies.setHeatSetpointAsync(cbody, sched.heatSetpoint), 100);
3510
3611
  }
3511
3612
  if ((sched.heatSource as any).val !== sys.board.valueMaps.heatSources.getValue('nochange') && sched.heatSource !== tbody.heatMode) {
3512
- setTimeout(() => sys.board.bodies.setHeatModeAsync(cbody, sys.board.valueMaps.heatModes.getValue((sched.heatSource as any).name)), 100);
3613
+ setTimeoutSync(() => sys.board.bodies.setHeatModeAsync(cbody, sys.board.valueMaps.heatModes.getValue((sched.heatSource as any).name)), 100);
3513
3614
  }
3514
3615
  }
3515
3616
  }
3516
3617
  };
3517
3618
  }
3518
- public async setScheduleAsync(data: any): Promise<Schedule> {
3619
+ public async setScheduleAsync(data: any, send: boolean = true): Promise<Schedule> {
3519
3620
  let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
3520
3621
  if (id <= 0) id = sys.schedules.getNextEquipmentId(new EquipmentIdRange(1, sys.equipment.maxSchedules));
3521
3622
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid schedule id: ${data.id}`, data.id, 'Schedule'));
@@ -3626,7 +3727,7 @@ export class ScheduleCommands extends BoardCommands {
3626
3727
  }
3627
3728
  } catch (err) { logger.error(`Error synchronizing schedule states`); }
3628
3729
  }
3629
- public async setEggTimerAsync(data?: any): Promise<EggTimer> { return Promise.resolve(sys.eggTimers.getItemByIndex(1)); }
3730
+ public async setEggTimerAsync(data?: any, send: boolean = true): Promise<EggTimer> { return Promise.resolve(sys.eggTimers.getItemByIndex(1)); }
3630
3731
  public async deleteEggTimerAsync(data?: any): Promise<EggTimer> { return Promise.resolve(sys.eggTimers.getItemByIndex(1)); }
3631
3732
  public includesCircuit(sched: Schedule, circuit: number) {
3632
3733
  let bIncludes = false;
@@ -3682,7 +3783,7 @@ export class ScheduleCommands extends BoardCommands {
3682
3783
  if (state.circuitGroups.getInterfaceById(circuit).manualPriorityActive) return true;
3683
3784
  if (grp && grp.isActive) cgc = grp.circuits.toArray();
3684
3785
  for (let i = 0; i < cgc.length; i++) {
3685
- let c = state.circuits.getInterfaceById(cgc[i].id);
3786
+ let c = state.circuits.getInterfaceById(cgc[i].circuit);
3686
3787
  if (c.manualPriorityActive) return true;
3687
3788
  }
3688
3789
  return false;
@@ -3702,7 +3803,7 @@ export class ScheduleCommands extends BoardCommands {
3702
3803
 
3703
3804
  if (schedule.isActive === false) return false;
3704
3805
  if (schedule.disabled) return false;
3705
- if (!sys.general.options.manualPriority) return false;
3806
+ //if (!sys.general.options.manualPriority) return false; //if we override a circuit to be mOP, this will not be true
3706
3807
 
3707
3808
  let currGrp: ICircuitGroup;
3708
3809
  let currSchedGrpCircs = [];
@@ -3743,7 +3844,7 @@ export class ScheduleCommands extends BoardCommands {
3743
3844
  let cgc = grp.circuits.getItemByIndex(j);
3744
3845
  let scgc = state.circuits.getInterfaceById(cgc.circuit);
3745
3846
  // if the circuit id's match and mOP is active, we delay
3746
- if (scgc.id === schedule.circuit && scgc.manualPriorityActive) return true;
3847
+ if (scgc.id === schedule.circuit && manualPriorityByProxy) return true;
3747
3848
  // check all the other cgc against this cgc
3748
3849
  // note: circuit/light groups cannot be part of a group themselves
3749
3850
  for (let k = 0; k < currSchedGrpCircs.length; k++) {
@@ -3788,569 +3889,607 @@ export class ScheduleCommands extends BoardCommands {
3788
3889
  // if we make it this far, nothing is impacting us
3789
3890
  return false;
3790
3891
  }
3892
+ public async updateSunriseSunsetAsync():Promise<boolean> {return Promise.resolve(false);};
3791
3893
  }
3792
3894
  export class HeaterCommands extends BoardCommands {
3793
- public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
3794
- try {
3795
- // First delete the heaters that should be removed.
3796
- for (let i = 0; i < ctx.heaters.remove.length; i++) {
3797
- let h = ctx.heaters.remove[i];
3798
- try {
3799
- await sys.board.heaters.deleteHeaterAsync(h);
3800
- res.addModuleSuccess('heater', `Remove: ${h.id}-${h.name}`);
3801
- } catch (err) { res.addModuleError('heater', `Remove: ${h.id}-${h.name}: ${err.message}`); }
3802
- }
3803
- for (let i = 0; i < ctx.heaters.update.length; i++) {
3804
- let h = ctx.heaters.update[i];
3805
- try {
3806
- await sys.board.heaters.setHeaterAsync(h);
3807
- res.addModuleSuccess('heater', `Update: ${h.id}-${h.name}`);
3808
- } catch (err) { res.addModuleError('heater', `Update: ${h.id}-${h.name}: ${err.message}`); }
3809
- }
3810
- for (let i = 0; i < ctx.heaters.add.length; i++) {
3811
- let h = ctx.heaters.add[i];
3895
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
3812
3896
  try {
3813
- // pull a little trick to first add the data then perform the update. This way we won't get a new id or
3814
- // it won't error out.
3815
- sys.heaters.getItemById(h.id, true);
3816
- await sys.board.heaters.setHeaterAsync(h);
3817
- res.addModuleSuccess('heater', `Add: ${h.id}-${h.name}`);
3818
- } catch (err) { res.addModuleError('heater', `Add: ${h.id}-${h.name}: ${err.message}`); }
3819
- }
3820
- return true;
3821
- } catch (err) { logger.error(`Error restoring heaters: ${err.message}`); res.addModuleError('system', `Error restoring heaters: ${err.message}`); return false; }
3822
- }
3823
- public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
3824
- try {
3825
- let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
3826
- // Look at heaters.
3827
- let cfg = rest.poolConfig;
3828
- for (let i = 0; i < cfg.heaters.length; i++) {
3829
- let r = cfg.heaters[i];
3830
- let c = sys.heaters.find(elem => r.id === elem.id);
3831
- if (typeof c === 'undefined') ctx.add.push(r);
3832
- else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
3833
- }
3834
- for (let i = 0; i < sys.heaters.length; i++) {
3835
- let c = sys.heaters.getItemByIndex(i);
3836
- let r = cfg.heaters.find(elem => elem.id == c.id);
3837
- if (typeof r === 'undefined') ctx.remove.push(c.get(true));
3838
- }
3839
- return ctx;
3840
- } catch (err) { logger.error(`Error validating heaters for restore: ${err.message}`); }
3841
- }
3842
- public getHeatersByCircuitId(circuitId: number): Heater[] {
3843
- let heaters: Heater[] = [];
3844
- let bodyId = circuitId === 6 ? 1 : circuitId === 1 ? 2 : 0;
3845
- if (bodyId > 0) {
3846
- for (let i = 0; i < sys.heaters.length; i++) {
3847
- let heater = sys.heaters.getItemByIndex(i);
3848
- if (!heater.isActive) continue;
3849
- if (bodyId === heater.body || sys.equipment.shared && heater.body === 32) heaters.push(heater);
3850
- }
3897
+ // First delete the heaters that should be removed.
3898
+ for (let i = 0; i < ctx.heaters.remove.length; i++) {
3899
+ let h = ctx.heaters.remove[i];
3900
+ try {
3901
+ await sys.board.heaters.deleteHeaterAsync(h);
3902
+ res.addModuleSuccess('heater', `Remove: ${h.id}-${h.name}`);
3903
+ } catch (err) { res.addModuleError('heater', `Remove: ${h.id}-${h.name}: ${err.message}`); }
3904
+ }
3905
+ for (let i = 0; i < ctx.heaters.update.length; i++) {
3906
+ let h = ctx.heaters.update[i];
3907
+ try {
3908
+ await sys.board.heaters.setHeaterAsync(h);
3909
+ res.addModuleSuccess('heater', `Update: ${h.id}-${h.name}`);
3910
+ } catch (err) { res.addModuleError('heater', `Update: ${h.id}-${h.name}: ${err.message}`); }
3911
+ }
3912
+ for (let i = 0; i < ctx.heaters.add.length; i++) {
3913
+ let h = ctx.heaters.add[i];
3914
+ try {
3915
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
3916
+ // it won't error out.
3917
+ sys.heaters.getItemById(h.id, true);
3918
+ await sys.board.heaters.setHeaterAsync(h);
3919
+ res.addModuleSuccess('heater', `Add: ${h.id}-${h.name}`);
3920
+ } catch (err) { res.addModuleError('heater', `Add: ${h.id}-${h.name}: ${err.message}`); }
3921
+ }
3922
+ return true;
3923
+ } catch (err) { logger.error(`Error restoring heaters: ${err.message}`); res.addModuleError('system', `Error restoring heaters: ${err.message}`); return false; }
3851
3924
  }
3852
- return heaters;
3853
- }
3854
- public getInstalledHeaterTypes(body?: number): any {
3855
- let heaters = sys.heaters.get();
3856
- let types = sys.board.valueMaps.heaterTypes.toArray();
3857
- let inst = { total: 0 };
3858
- for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
3859
- for (let i = 0; i < heaters.length; i++) {
3860
- let heater = heaters[i];
3861
- if (typeof body !== 'undefined' && heater.body !== 'undefined') {
3862
- if ((heater.body !== 32 && body !== heater.body + 1) || (heater.body === 32 && body > 2)) continue;
3863
- }
3864
- let type = types.find(elem => elem.val === heater.type);
3865
- if (typeof type !== 'undefined') {
3866
- if (inst[type.name] === 'undefined') inst[type.name] = 0;
3867
- inst[type.name] = inst[type.name] + 1;
3868
- if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
3869
- inst.total++;
3870
- }
3925
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
3926
+ try {
3927
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
3928
+ // Look at heaters.
3929
+ let cfg = rest.poolConfig;
3930
+ for (let i = 0; i < cfg.heaters.length; i++) {
3931
+ let r = cfg.heaters[i];
3932
+ let c = sys.heaters.find(elem => r.id === elem.id);
3933
+ if (typeof c === 'undefined') ctx.add.push(r);
3934
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
3935
+ }
3936
+ for (let i = 0; i < sys.heaters.length; i++) {
3937
+ let c = sys.heaters.getItemByIndex(i);
3938
+ let r = cfg.heaters.find(elem => elem.id == c.id);
3939
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
3940
+ }
3941
+ return ctx;
3942
+ } catch (err) { logger.error(`Error validating heaters for restore: ${err.message}`); }
3871
3943
  }
3872
- return inst;
3873
- }
3874
- public isSolarInstalled(body?: number): boolean {
3875
- let heaters = sys.heaters.get();
3876
- let types = sys.board.valueMaps.heaterTypes.toArray();
3877
- for (let i = 0; i < heaters.length; i++) {
3878
- let heater = heaters[i];
3879
- if (typeof body !== 'undefined' && body !== heater.body) continue;
3880
- let type = types.find(elem => elem.val === heater.type);
3881
- if (typeof type !== 'undefined') {
3882
- switch (type.name) {
3883
- case 'solar':
3884
- return true;
3944
+ public getHeatersByCircuitId(circuitId: number): Heater[] {
3945
+ let heaters: Heater[] = [];
3946
+ let bodyId = circuitId === 6 ? 1 : circuitId === 1 ? 2 : 0;
3947
+ if (bodyId > 0) {
3948
+ for (let i = 0; i < sys.heaters.length; i++) {
3949
+ let heater = sys.heaters.getItemByIndex(i);
3950
+ if (!heater.isActive) continue;
3951
+ if (bodyId === heater.body || sys.equipment.shared && heater.body === 32) heaters.push(heater);
3952
+ }
3885
3953
  }
3886
- }
3954
+ return heaters;
3887
3955
  }
3888
- }
3889
- public isHeatPumpInstalled(body?: number): boolean {
3890
- let heaters = sys.heaters.get();
3891
- let types = sys.board.valueMaps.heaterTypes.toArray();
3892
- for (let i = 0; i < heaters.length; i++) {
3893
- let heater = heaters[i];
3894
- if (typeof body !== 'undefined' && body !== heater.body) continue;
3895
- let type = types.find(elem => elem.val === heater.type);
3896
- if (typeof type !== 'undefined') {
3897
- switch (type.name) {
3898
- case 'heatpump':
3899
- return true;
3956
+ public getInstalledHeaterTypes(body?: number): any {
3957
+ let heaters = sys.heaters.get();
3958
+ let types = sys.board.valueMaps.heaterTypes.toArray();
3959
+ let inst = { total: 0 };
3960
+ for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
3961
+ for (let i = 0; i < heaters.length; i++) {
3962
+ let heater = heaters[i];
3963
+ if (typeof body !== 'undefined' && heater.body !== 'undefined') {
3964
+ if ((heater.body !== 32 && body !== heater.body + 1) || (heater.body === 32 && body > 2)) continue;
3965
+ }
3966
+ let type = types.find(elem => elem.val === heater.type);
3967
+ if (typeof type !== 'undefined') {
3968
+ if (inst[type.name] === 'undefined') inst[type.name] = 0;
3969
+ inst[type.name] = inst[type.name] + 1;
3970
+ if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
3971
+ inst.total++;
3972
+ }
3900
3973
  }
3901
- }
3902
- }
3903
- }
3904
- public setHeater(heater: Heater, obj?: any) {
3905
- if (typeof obj !== undefined) {
3906
- for (var s in obj)
3907
- heater[s] = obj[s];
3974
+ return inst;
3908
3975
  }
3909
- }
3910
- public async setHeaterAsync(obj: any): Promise<Heater> {
3911
- try {
3912
- let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
3913
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
3914
- else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Virtual Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
3915
- let heater: Heater;
3916
- if (id <= 0) {
3917
- // We are adding a heater. In this case all heaters are virtual.
3918
- let vheaters = sys.heaters.filter(h => h.master === 1);
3919
- id = vheaters.length + 256;
3920
- }
3921
- heater = sys.heaters.getItemById(id, true);
3922
- if (typeof obj !== undefined) {
3923
- for (var s in obj) {
3924
- if (s === 'id') continue;
3925
- heater[s] = obj[s];
3976
+ public isSolarInstalled(body?: number): boolean {
3977
+ let heaters = sys.heaters.get();
3978
+ let types = sys.board.valueMaps.heaterTypes.toArray();
3979
+ for (let i = 0; i < heaters.length; i++) {
3980
+ let heater = heaters[i];
3981
+ if (typeof body !== 'undefined' && body !== heater.body) continue;
3982
+ let type = types.find(elem => elem.val === heater.type);
3983
+ if (typeof type !== 'undefined') {
3984
+ switch (type.name) {
3985
+ case 'solar':
3986
+ return true;
3987
+ }
3988
+ }
3926
3989
  }
3927
- }
3928
- let hstate = state.heaters.getItemById(id, true);
3929
- //hstate.isVirtual = heater.isVirtual = true;
3930
- hstate.name = heater.name;
3931
- hstate.type = heater.type;
3932
- heater.master = 1;
3933
- if (heater.master === 1) await ncp.heaters.setHeaterAsync(heater, obj);
3934
- await sys.board.heaters.updateHeaterServices();
3935
- await sys.board.heaters.syncHeaterStates();
3936
- return heater;
3937
- } catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
3938
- }
3939
- public async deleteHeaterAsync(obj: any): Promise<Heater> {
3940
- try {
3941
- let id = parseInt(obj.id, 10);
3942
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
3943
- let heater = sys.heaters.getItemById(id);
3944
- heater.isActive = false;
3945
- if (heater.master === 1) await ncp.heaters.deleteHeaterAsync(heater.id);
3946
- sys.heaters.removeItemById(id);
3947
- state.heaters.removeItemById(id);
3948
- sys.board.heaters.updateHeaterServices();
3949
- sys.board.heaters.syncHeaterStates();
3950
- return heater;
3951
- } catch (err) { return Promise.reject(`Error deleting heater: ${err.message}`) }
3952
- }
3953
- public updateHeaterServices() {
3954
- let htypes = sys.board.heaters.getInstalledHeaterTypes();
3955
- let solarInstalled = htypes.solar > 0;
3956
- let heatPumpInstalled = htypes.heatpump > 0;
3957
- let gasHeaterInstalled = htypes.gas > 0;
3958
-
3959
- if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
3960
- if (gasHeaterInstalled) sys.board.valueMaps.heatSources.set(3, { name: 'heater', desc: 'Heater' });
3961
- if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
3962
- else if (solarInstalled) sys.board.valueMaps.heatSources.set(5, { name: 'solar', desc: 'Solar' });
3963
- if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
3964
- else if (heatPumpInstalled) sys.board.valueMaps.heatSources.set(9, { name: 'heatpump', desc: 'Heat Pump' });
3965
- sys.board.valueMaps.heatSources.set(32, { name: 'nochange', desc: 'No Change' });
3966
-
3967
- sys.board.valueMaps.heatModes = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
3968
- if (gasHeaterInstalled) sys.board.valueMaps.heatModes.set(3, { name: 'heater', desc: 'Heater' });
3969
- if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
3970
- else if (solarInstalled) sys.board.valueMaps.heatModes.set(5, { name: 'solar', desc: 'Solar' });
3971
- if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
3972
- else if (heatPumpInstalled) sys.board.valueMaps.heatModes.set(9, { name: 'heatpump', desc: 'Heat Pump' });
3973
- // Now set the body data.
3974
- for (let i = 0; i < sys.bodies.length; i++) {
3975
- let body = sys.bodies.getItemByIndex(i);
3976
- let btemp = state.temps.bodies.getItemById(body.id, body.isActive !== false);
3977
- let opts = sys.board.heaters.getInstalledHeaterTypes(body.id);
3978
- btemp.heaterOptions = opts;
3979
- }
3980
- this.setActiveTempSensors();
3981
- }
3982
- public initTempSensors() {
3983
- // Add in the potential sensors and delete the ones that shouldn't exist.
3984
- let maxPairs = sys.equipment.maxBodies + (sys.equipment.shared ? -1 : 0);
3985
- sys.equipment.tempSensors.getItemById('air', true, { id: 'air', isActive: true, calibration: 0 }).name = 'Air';
3986
- sys.equipment.tempSensors.getItemById('water1', true, { id: 'water1', isActive: true, calibration: 0 }).name = maxPairs == 1 ? 'Water' : 'Body 1';
3987
- sys.equipment.tempSensors.getItemById('solar1', true, { id: 'solar1', isActive: false, calibration: 0 }).name = maxPairs == 1 ? 'Solar' : 'Solar 1';
3988
- if (maxPairs > 1) {
3989
- sys.equipment.tempSensors.getItemById('water2', true, { id: 'water2', isActive: false, calibration: 0 }).name = 'Body 2';
3990
- sys.equipment.tempSensors.getItemById('solar2', true, { id: 'solar2', isActive: false, calibration: 0 }).name = 'Solar 2';
3991
3990
  }
3992
- else {
3993
- sys.equipment.tempSensors.removeItemById('water2');
3994
- sys.equipment.tempSensors.removeItemById('solar2');
3995
- }
3996
- if (maxPairs > 2) {
3997
- sys.equipment.tempSensors.getItemById('water3', true, { id: 'water3', isActive: false, calibration: 0 }).name = 'Body 3';
3998
- sys.equipment.tempSensors.getItemById('solar3', true, { id: 'solar3', isActive: false, calibration: 0 }).name = 'Solar 3';
3991
+ public isHeatPumpInstalled(body?: number): boolean {
3992
+ let heaters = sys.heaters.get();
3993
+ let types = sys.board.valueMaps.heaterTypes.toArray();
3994
+ for (let i = 0; i < heaters.length; i++) {
3995
+ let heater = heaters[i];
3996
+ if (typeof body !== 'undefined' && body !== heater.body) continue;
3997
+ let type = types.find(elem => elem.val === heater.type);
3998
+ if (typeof type !== 'undefined') {
3999
+ switch (type.name) {
4000
+ case 'heatpump':
4001
+ return true;
4002
+ }
4003
+ }
4004
+ }
3999
4005
  }
4000
- else {
4001
- sys.equipment.tempSensors.removeItemById('water3');
4002
- sys.equipment.tempSensors.removeItemById('solar3');
4006
+ public setHeater(heater: Heater, obj?: any) {
4007
+ if (typeof obj !== undefined) {
4008
+ for (var s in obj)
4009
+ heater[s] = obj[s];
4010
+ }
4003
4011
  }
4004
- if (maxPairs > 3) {
4005
- sys.equipment.tempSensors.getItemById('water4', true, { id: 'water4', isActive: false, calibration: 0 }).name = 'Body 4';
4006
- sys.equipment.tempSensors.getItemById('solar4', true, { id: 'solar4', isActive: false, calibration: 0 }).name = 'Solar 4';
4012
+ public async setHeaterAsync(obj: any, send: boolean = true): Promise<Heater> {
4013
+ try {
4014
+ let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
4015
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
4016
+ else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Virtual Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
4017
+ let heater: Heater;
4018
+ if (id <= 0) {
4019
+ // We are adding a heater. In this case all heaters are virtual.
4020
+ let vheaters = sys.heaters.filter(h => h.master === 1);
4021
+ id = vheaters.length + 256;
4022
+ }
4023
+ heater = sys.heaters.getItemById(id, true);
4024
+ if (typeof obj !== undefined) {
4025
+ for (var s in obj) {
4026
+ if (s === 'id') continue;
4027
+ heater[s] = obj[s];
4028
+ }
4029
+ }
4030
+ let hstate = state.heaters.getItemById(id, true);
4031
+ //hstate.isVirtual = heater.isVirtual = true;
4032
+ hstate.name = heater.name;
4033
+ hstate.type = heater.type;
4034
+ heater.master = 1;
4035
+ if (heater.master === 1) await ncp.heaters.setHeaterAsync(heater, obj);
4036
+ await sys.board.heaters.updateHeaterServices();
4037
+ await sys.board.heaters.syncHeaterStates();
4038
+ return heater;
4039
+ } catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
4007
4040
  }
4008
- else {
4009
- sys.equipment.tempSensors.removeItemById('water4');
4010
- sys.equipment.tempSensors.removeItemById('solar4');
4041
+ public async deleteHeaterAsync(obj: any): Promise<Heater> {
4042
+ try {
4043
+ let id = parseInt(obj.id, 10);
4044
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
4045
+ let heater = sys.heaters.getItemById(id);
4046
+ heater.isActive = false;
4047
+ if (heater.master === 1) await ncp.heaters.deleteHeaterAsync(heater.id);
4048
+ sys.heaters.removeItemById(id);
4049
+ state.heaters.removeItemById(id);
4050
+ sys.board.heaters.updateHeaterServices();
4051
+ sys.board.heaters.syncHeaterStates();
4052
+ return heater;
4053
+ } catch (err) { return Promise.reject(`Error deleting heater: ${err.message}`) }
4011
4054
  }
4055
+ public updateHeaterServices() {
4056
+ let htypes = sys.board.heaters.getInstalledHeaterTypes();
4057
+ let solarInstalled = htypes.solar > 0;
4058
+ let heatPumpInstalled = htypes.heatpump > 0;
4059
+ let gasHeaterInstalled = htypes.gas > 0;
4012
4060
 
4013
- }
4014
- // Sets the active temp sensors based upon the installed equipment. At this point all
4015
- // detectable temp sensors should exist.
4016
- public setActiveTempSensors() {
4017
- let htypes;
4018
- // We are iterating backwards through the sensors array on purpose. We do this just in case we need
4019
- // to remove a sensor during the iteration. This way the index values will not be impacted and we can
4020
- // safely remove from the array we are iterating.
4021
- for (let i = sys.equipment.tempSensors.length - 1; i >= 0; i--) {
4022
- let sensor = sys.equipment.tempSensors.getItemByIndex(i);
4023
- // The names are normalized in this array.
4024
- switch (sensor.id) {
4025
- case 'air':
4026
- sensor.isActive = true;
4027
- break;
4028
- case 'water1':
4029
- sensor.isActive = sys.equipment.maxBodies > 0;
4030
- break;
4031
- case 'water2':
4032
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 2 : sys.equipment.maxBodies > 1;
4033
- break;
4034
- case 'water3':
4035
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 3 : sys.equipment.maxBodies > 2;
4036
- break;
4037
- case 'water4':
4038
- // It's a little weird but technically you should be able to install 3 expansions and a i10D personality
4039
- // board. If this situation ever comes up we will see if it works. Whether it reports is another story
4040
- // since the 2 message is short a byte for this.
4041
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 4 : sys.equipment.maxBodies > 3;
4042
- break;
4043
- // Solar sensors are funny ducks. This is because they are for both heatpumps and solar and the equipment
4044
- // can be installed on specific bodies. This will be true for heaters installed in expansion panels for *Touch, dual body systems,
4045
- // and any IntelliCenter with more than one body. At some point simply implementing the multi-body functions for touch will make
4046
- // this all work. This will only be with i10D or expansion panels.
4047
- case 'solar1':
4048
- // The first solar sensor is a funny duck in that it should be active for shared systems
4049
- // if either body has an active solar heater or heatpump.
4050
- htypes = sys.board.heaters.getInstalledHeaterTypes(1);
4051
- if ('solar' in htypes || 'heatpump' in htypes) sensor.isActive = true;
4052
- else if (sys.equipment.shared) {
4053
- htypes = sys.board.heaters.getInstalledHeaterTypes(2);
4054
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4055
- }
4056
- else sensor.isActive = false;
4057
- break;
4058
- case 'solar2':
4059
- if (sys.equipment.maxBodies > 1 + (sys.equipment.shared ? 1 : 0)) {
4060
- htypes = sys.board.heaters.getInstalledHeaterTypes(2 + (sys.equipment.shared ? 1 : 0));
4061
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4062
- }
4063
- else sensor.isActive = false;
4064
- break;
4065
- case 'solar3':
4066
- if (sys.equipment.maxBodies > 2 + (sys.equipment.shared ? 1 : 0)) {
4067
- htypes = sys.board.heaters.getInstalledHeaterTypes(3 + (sys.equipment.shared ? 1 : 0));
4068
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4069
- }
4070
- else sensor.isActive = false;
4071
- break;
4072
- case 'solar4':
4073
- if (sys.equipment.maxBodies > 3 + (sys.equipment.shared ? 1 : 0)) {
4074
- htypes = sys.board.heaters.getInstalledHeaterTypes(4 + (sys.equipment.shared ? 1 : 0));
4075
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4076
- }
4077
- else sensor.isActive = false;
4078
- break;
4079
- default:
4080
- if (typeof sensor.id === 'undefined') sys.equipment.tempSensors.removeItemByIndex(i);
4081
- break;
4082
- }
4061
+ if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
4062
+ if (gasHeaterInstalled) sys.board.valueMaps.heatSources.set(3, { name: 'heater', desc: 'Heater' });
4063
+ if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
4064
+ else if (solarInstalled) sys.board.valueMaps.heatSources.set(5, { name: 'solar', desc: 'Solar' });
4065
+ if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
4066
+ else if (heatPumpInstalled) sys.board.valueMaps.heatSources.set(9, { name: 'heatpump', desc: 'Heat Pump' });
4067
+ sys.board.valueMaps.heatSources.set(32, { name: 'nochange', desc: 'No Change' });
4068
+
4069
+ sys.board.valueMaps.heatModes = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
4070
+ if (gasHeaterInstalled) sys.board.valueMaps.heatModes.set(3, { name: 'heater', desc: 'Heater' });
4071
+ if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
4072
+ else if (solarInstalled) sys.board.valueMaps.heatModes.set(5, { name: 'solar', desc: 'Solar' });
4073
+ if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
4074
+ else if (heatPumpInstalled) sys.board.valueMaps.heatModes.set(9, { name: 'heatpump', desc: 'Heat Pump' });
4075
+ // Now set the body data.
4076
+ for (let i = 0; i < sys.bodies.length; i++) {
4077
+ let body = sys.bodies.getItemByIndex(i);
4078
+ let btemp = state.temps.bodies.getItemById(body.id, body.isActive !== false);
4079
+ let opts = sys.board.heaters.getInstalledHeaterTypes(body.id);
4080
+ btemp.heaterOptions = opts;
4081
+ }
4082
+ this.setActiveTempSensors();
4083
4083
  }
4084
- }
4085
- // This updates the heater states based upon the installed heaters. This is true for heaters that are tied to the OCP
4086
- // and those that are not.
4087
- public syncHeaterStates() {
4088
- try {
4089
- // Go through the installed heaters and bodies to determine whether they should be on. If there is a
4090
- // heater that is not controlled by the OCP then we need to determine whether it should be on.
4091
- let heaters = sys.heaters.toArray();
4092
- let bodies = state.temps.bodies.toArray();
4093
- let hon = [];
4094
- for (let i = 0; i < bodies.length; i++) {
4095
- let body: BodyTempState = bodies[i];
4096
- let cfgBody: Body = sys.bodies.getItemById(body.id);
4097
- let isHeating = false;
4098
- let isCooling = false;
4099
- let hstatus = sys.board.valueMaps.heatStatus.getName(body.heatStatus);
4100
- let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
4101
- if (body.isOn) {
4102
- 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.`);
4103
- // Now get all the heaters associated with the body in an array.
4104
- let bodyHeaters: Heater[] = [];
4105
- for (let j = 0; j < heaters.length; j++) {
4106
- let heater: Heater = heaters[j];
4107
- if (heater.isActive === false) continue;
4108
- if (heater.body === body.id) bodyHeaters.push(heater);
4109
- else {
4110
- let b = sys.board.valueMaps.bodies.transform(heater.body);
4111
- switch (b.name) {
4112
- case 'body1':
4113
- case 'pool':
4114
- if (body.id === 1) bodyHeaters.push(heater);
4115
- break;
4116
- case 'body2':
4117
- case 'spa':
4118
- if (body.id === 2) bodyHeaters.push(heater);
4119
- break;
4120
- case 'poolspa':
4121
- if (body.id === 1 || body.id === 2) bodyHeaters.push(heater);
4122
- break;
4123
- case 'body3':
4124
- if (body.id === 3) bodyHeaters.push(heater);
4125
- break;
4126
- case 'body4':
4127
- if (body.id === 4) bodyHeaters.push(heater);
4128
- break;
4129
- }
4130
- }
4131
- }
4132
- // Alright we have all the body heaters so sort them in a way that will make our heater preferences work. Solar, heatpumps, and ultratemp should be in the list first
4133
- // so that if we have a heater preference set up then we do not have to evaluate the other heater.
4134
- let heaterTypes = sys.board.valueMaps.heaterTypes;
4135
- bodyHeaters.sort((a, b) => {
4136
- if (heaterTypes.transform(a.type).hasPreference) return -1;
4137
- else if (heaterTypes.transform(b.type).hasPreference) return 1;
4138
- return 0;
4139
- });
4084
+ public initTempSensors() {
4085
+ // Add in the potential sensors and delete the ones that shouldn't exist.
4086
+ let maxPairs = sys.equipment.maxBodies + (sys.equipment.shared ? -1 : 0);
4087
+ sys.equipment.tempSensors.getItemById('air', true, { id: 'air', isActive: true, calibration: 0 }).name = 'Air';
4088
+ sys.equipment.tempSensors.getItemById('water1', true, { id: 'water1', isActive: true, calibration: 0 }).name = maxPairs == 1 ? 'Water' : 'Body 1';
4089
+ sys.equipment.tempSensors.getItemById('solar1', true, { id: 'solar1', isActive: false, calibration: 0 }).name = maxPairs == 1 ? 'Solar' : 'Solar 1';
4090
+ if (maxPairs > 1) {
4091
+ sys.equipment.tempSensors.getItemById('water2', true, { id: 'water2', isActive: false, calibration: 0 }).name = 'Body 2';
4092
+ sys.equipment.tempSensors.getItemById('solar2', true, { id: 'solar2', isActive: false, calibration: 0 }).name = 'Solar 2';
4093
+ }
4094
+ else {
4095
+ sys.equipment.tempSensors.removeItemById('water2');
4096
+ sys.equipment.tempSensors.removeItemById('solar2');
4097
+ }
4098
+ if (maxPairs > 2) {
4099
+ sys.equipment.tempSensors.getItemById('water3', true, { id: 'water3', isActive: false, calibration: 0 }).name = 'Body 3';
4100
+ sys.equipment.tempSensors.getItemById('solar3', true, { id: 'solar3', isActive: false, calibration: 0 }).name = 'Solar 3';
4101
+ }
4102
+ else {
4103
+ sys.equipment.tempSensors.removeItemById('water3');
4104
+ sys.equipment.tempSensors.removeItemById('solar3');
4105
+ }
4106
+ if (maxPairs > 3) {
4107
+ sys.equipment.tempSensors.getItemById('water4', true, { id: 'water4', isActive: false, calibration: 0 }).name = 'Body 4';
4108
+ sys.equipment.tempSensors.getItemById('solar4', true, { id: 'solar4', isActive: false, calibration: 0 }).name = 'Solar 4';
4109
+ }
4110
+ else {
4111
+ sys.equipment.tempSensors.removeItemById('water4');
4112
+ sys.equipment.tempSensors.removeItemById('solar4');
4113
+ }
4140
4114
 
4141
- // Alright so now we should have a sorted array that has preference type heaters first.
4142
- for (let j = 0; j < bodyHeaters.length; j++) {
4143
- let heater: Heater = bodyHeaters[j];
4144
- let isOn = false;
4145
- let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
4146
- let hstate = state.heaters.getItemById(heater.id, true);
4147
- if (heater.master === 1) {
4148
- if (hstatus !== 'cooldown') {
4149
- // We need to do our own calculation as to whether it is on. This is for Nixie heaters.
4150
- switch (htype.name) {
4151
- case 'solar':
4152
- if (mode === 'solar' || mode === 'solarpref') {
4153
- // Measure up against start and stop temp deltas for effective solar heating.
4154
- if (body.temp < cfgBody.heatSetpoint &&
4155
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
4156
- isOn = true;
4157
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
4158
- isHeating = true;
4159
- }
4160
- else if (heater.coolingEnabled && body.temp > cfgBody.coolSetpoint && state.heliotrope.isNight &&
4161
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
4162
- isOn = true;
4163
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
4164
- isHeating = true;
4165
- isCooling = true;
4166
- }
4115
+ }
4116
+ // Sets the active temp sensors based upon the installed equipment. At this point all
4117
+ // detectable temp sensors should exist.
4118
+ public setActiveTempSensors() {
4119
+ let htypes;
4120
+ // We are iterating backwards through the sensors array on purpose. We do this just in case we need
4121
+ // to remove a sensor during the iteration. This way the index values will not be impacted and we can
4122
+ // safely remove from the array we are iterating.
4123
+ for (let i = sys.equipment.tempSensors.length - 1; i >= 0; i--) {
4124
+ let sensor = sys.equipment.tempSensors.getItemByIndex(i);
4125
+ // The names are normalized in this array.
4126
+ switch (sensor.id) {
4127
+ case 'air':
4128
+ sensor.isActive = true;
4129
+ break;
4130
+ case 'water1':
4131
+ sensor.isActive = sys.equipment.maxBodies > 0;
4132
+ break;
4133
+ case 'water2':
4134
+ sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 2 : sys.equipment.maxBodies > 1;
4135
+ break;
4136
+ case 'water3':
4137
+ sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 3 : sys.equipment.maxBodies > 2;
4138
+ break;
4139
+ case 'water4':
4140
+ // It's a little weird but technically you should be able to install 3 expansions and a i10D personality
4141
+ // board. If this situation ever comes up we will see if it works. Whether it reports is another story
4142
+ // since the 2 message is short a byte for this.
4143
+ sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 4 : sys.equipment.maxBodies > 3;
4144
+ break;
4145
+ // Solar sensors are funny ducks. This is because they are for both heatpumps and solar and the equipment
4146
+ // can be installed on specific bodies. This will be true for heaters installed in expansion panels for *Touch, dual body systems,
4147
+ // and any IntelliCenter with more than one body. At some point simply implementing the multi-body functions for touch will make
4148
+ // this all work. This will only be with i10D or expansion panels.
4149
+ case 'solar1':
4150
+ // The first solar sensor is a funny duck in that it should be active for shared systems
4151
+ // if either body has an active solar heater or heatpump.
4152
+ htypes = sys.board.heaters.getInstalledHeaterTypes(1);
4153
+ if ('solar' in htypes || 'heatpump' in htypes) sensor.isActive = true;
4154
+ else if (sys.equipment.shared) {
4155
+ htypes = sys.board.heaters.getInstalledHeaterTypes(2);
4156
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4167
4157
  }
4158
+ else sensor.isActive = false;
4168
4159
  break;
4169
- case 'ultratemp':
4170
- // There is a temperature differential setting on UltraTemp. This is how
4171
- // much the water temperature needs to drop below the set temperature, for the heater
4172
- // to start up again. For instance, if the set temperature and the water temperature is 82 and then the
4173
- // heater will shut off and not turn on again until the water temperature = setpoint - differentialTemperature.
4174
- // This is the default operation on IntelliCenter and it appears to simply not start on the setpoint. We can do better
4175
- // than this by heating 1 degree past the setpoint then applying this rule for 30 minutes. This allows for a more
4176
- // responsive heater.
4177
- //
4178
- // For Ultratemp we need to determine whether the differential temp
4179
- // is within range. The other thing that needs to be calculated here is
4180
- // whether Ultratemp can effeciently heat the pool.
4181
- if (mode === 'ultratemp' || mode === 'ultratemppref') {
4182
- if (hstate.isOn) {
4183
- // For the preference mode we will try to reach the setpoint for a period of time then
4184
- // switch over to the gas heater. Our algorithm for this is to check the rate of
4185
- // change when the heater first kicks on. If we go for longer than an hour and still
4186
- // haven't reached the setpoint then we will switch to gas.
4187
- if (mode === 'ultratemppref' &&
4188
- typeof hstate.startTime !== 'undefined' &&
4189
- hstate.startTime.getTime() < new Date().getTime() - (60 * 60 * 1000))
4190
- break;
4191
- // If the heater is already on we will heat to 1 degree past the setpoint.
4192
- if (body.temp - 1 < cfgBody.heatSetpoint) {
4193
- isOn = true;
4194
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
4195
- isHeating = true;
4196
- isCooling = false;
4197
- }
4198
- else if (body.temp + 1 > cfgBody.coolSetpoint && heater.coolingEnabled) {
4199
- isOn = true;
4200
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
4201
- isHeating = false;
4202
- isCooling = true;
4203
- }
4204
- }
4205
- else {
4206
- let delayStart = typeof hstate.endTime !== 'undefined' ? (hstate.endTime.getTime() + (30 * 60 * 1000)) > new Date().getTime() : false;
4207
- // The heater is not currently on lets turn it on if we pass all the criteria.
4208
- if ((body.temp < cfgBody.heatSetpoint && !delayStart)
4209
- || body.temp + heater.differentialTemp < cfgBody.heatSetpoint) {
4210
- isOn = true;
4211
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
4212
- isHeating = true;
4213
- isCooling = false;
4214
- }
4215
- else if (body.temp > cfgBody.coolSetpoint && heater.coolingEnabled) {
4216
- if (!delayStart || body.temp - heater.differentialTemp > cfgBody.coolSetpoint) {
4217
- isOn = true;
4218
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
4219
- isHeating = false;
4220
- isCooling = true;
4221
- }
4222
- }
4223
- }
4160
+ case 'solar2':
4161
+ if (sys.equipment.maxBodies > 1 + (sys.equipment.shared ? 1 : 0)) {
4162
+ htypes = sys.board.heaters.getInstalledHeaterTypes(2 + (sys.equipment.shared ? 1 : 0));
4163
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4224
4164
  }
4165
+ else sensor.isActive = false;
4225
4166
  break;
4226
- case 'mastertemp':
4227
- // If we make it here, the other heater is not heating the body.
4228
- if (mode === 'mtheater' || mode === 'heatpumppref' || mode === 'ultratemppref' || mode === 'solarpref') {
4229
- if (body.temp < cfgBody.setPoint) {
4230
- isOn = true;
4231
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('mtheat');
4232
- isHeating = true;
4233
- }
4167
+ case 'solar3':
4168
+ if (sys.equipment.maxBodies > 2 + (sys.equipment.shared ? 1 : 0)) {
4169
+ htypes = sys.board.heaters.getInstalledHeaterTypes(3 + (sys.equipment.shared ? 1 : 0));
4170
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4234
4171
  }
4172
+ else sensor.isActive = false;
4235
4173
  break;
4236
- case 'maxetherm':
4237
- case 'gas':
4238
- // If we make it here, the other heater is not heating the body.
4239
- if (mode === 'heater' || mode === 'solarpref' || mode === 'heatpumppref' || mode === 'ultratemppref') {
4240
- // Heat past the setpoint for the heater but only if the heater is currently on.
4241
- if ((body.temp - (hstate.isOn ? heater.stopTempDelta : 0)) < cfgBody.setPoint) {
4242
- isOn = true;
4243
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
4244
- isHeating = true;
4245
- }
4174
+ case 'solar4':
4175
+ if (sys.equipment.maxBodies > 3 + (sys.equipment.shared ? 1 : 0)) {
4176
+ htypes = sys.board.heaters.getInstalledHeaterTypes(4 + (sys.equipment.shared ? 1 : 0));
4177
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4246
4178
  }
4179
+ else sensor.isActive = false;
4247
4180
  break;
4248
- case 'heatpump':
4249
- if (mode === 'heatpump' || mode === 'heatpumppref') {
4250
- if (hstate.isOn) {
4251
- // If the heater is already on we will heat to 1 degree past the setpoint.
4252
- if (body.temp - 1 < cfgBody.heatSetpoint) {
4253
- isOn = true;
4254
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
4255
- isHeating = true;
4256
- isCooling = false;
4181
+ default:
4182
+ if (typeof sensor.id === 'undefined') sys.equipment.tempSensors.removeItemByIndex(i);
4183
+ break;
4184
+ }
4185
+ }
4186
+ }
4187
+ // This updates the heater states based upon the installed heaters. This is true for heaters that are tied to the OCP
4188
+ // and those that are not.
4189
+ public syncHeaterStates() {
4190
+ try {
4191
+
4192
+ // Go through the installed heaters and bodies to determine whether they should be on. If there is a
4193
+ // heater that is not controlled by the OCP then we need to determine whether it should be on.
4194
+ let heaters = sys.heaters.toArray();
4195
+ let bodies = state.temps.bodies.toArray();
4196
+ let hon = [];
4197
+ for (let i = 0; i < bodies.length; i++) {
4198
+ let body: BodyTempState = bodies[i];
4199
+ let cfgBody: Body = sys.bodies.getItemById(body.id);
4200
+ let isHeating = false;
4201
+ let isCooling = false;
4202
+ let hstatus = sys.board.valueMaps.heatStatus.getName(body.heatStatus);
4203
+ let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
4204
+ if (body.isOn) {
4205
+ 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.`);
4206
+ // Now get all the heaters associated with the body in an array.
4207
+ let bodyHeaters: Heater[] = [];
4208
+ for (let j = 0; j < heaters.length; j++) {
4209
+ let heater: Heater = heaters[j];
4210
+ if (heater.isActive === false) continue;
4211
+ if (heater.body === body.id) bodyHeaters.push(heater);
4212
+ else {
4213
+ let b = sys.board.valueMaps.bodies.transform(heater.body);
4214
+ switch (b.name) {
4215
+ case 'body1':
4216
+ case 'pool':
4217
+ if (body.id === 1) bodyHeaters.push(heater);
4218
+ break;
4219
+ case 'body2':
4220
+ case 'spa':
4221
+ if (body.id === 2) bodyHeaters.push(heater);
4222
+ break;
4223
+ case 'poolspa':
4224
+ if (body.id === 1 || body.id === 2) bodyHeaters.push(heater);
4225
+ break;
4226
+ case 'body3':
4227
+ if (body.id === 3) bodyHeaters.push(heater);
4228
+ break;
4229
+ case 'body4':
4230
+ if (body.id === 4) bodyHeaters.push(heater);
4231
+ break;
4232
+ }
4233
+ }
4234
+ }
4235
+ // Alright we have all the body heaters so sort them in a way that will make our heater preferences work. Solar, heatpumps, and ultratemp should be in the list first
4236
+ // so that if we have a heater preference set up then we do not have to evaluate the other heater.
4237
+ let heaterTypes = sys.board.valueMaps.heaterTypes;
4238
+ bodyHeaters.sort((a, b) => {
4239
+ if (heaterTypes.transform(a.type).hasPreference) return -1;
4240
+ else if (heaterTypes.transform(b.type).hasPreference) return 1;
4241
+ return 0;
4242
+ });
4243
+
4244
+ // Alright so now we should have a sorted array that has preference type heaters first.
4245
+ for (let j = 0; j < bodyHeaters.length; j++) {
4246
+ let heater: Heater = bodyHeaters[j];
4247
+ let isOn = false;
4248
+ let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
4249
+ let hstate = state.heaters.getItemById(heater.id, true);
4250
+ if (heater.master === 1) {
4251
+ if (hstatus !== 'cooldown') {
4252
+ // We need to do our own calculation as to whether it is on. This is for Nixie heaters.
4253
+ switch (htype.name) {
4254
+ case 'solar':
4255
+ if (mode === 'solar' || mode === 'solarpref') {
4256
+ // Measure up against start and stop temp deltas for effective solar heating.
4257
+ if (body.temp < cfgBody.heatSetpoint &&
4258
+ state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
4259
+ isOn = true;
4260
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
4261
+ isHeating = true;
4262
+ }
4263
+ else if (heater.coolingEnabled && body.temp > cfgBody.coolSetpoint && state.heliotrope.isNight &&
4264
+ state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
4265
+ isOn = true;
4266
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
4267
+ isHeating = true;
4268
+ isCooling = true;
4269
+ }
4270
+ }
4271
+ break;
4272
+ case 'ultratemp':
4273
+ // There is a temperature differential setting on UltraTemp. This is how
4274
+ // much the water temperature needs to drop below the set temperature, for the heater
4275
+ // to start up again. For instance, if the set temperature and the water temperature is 82 and then the
4276
+ // heater will shut off and not turn on again until the water temperature = setpoint - differentialTemperature.
4277
+ // This is the default operation on IntelliCenter and it appears to simply not start on the setpoint. We can do better
4278
+ // than this by heating 1 degree past the setpoint then applying this rule for 30 minutes. This allows for a more
4279
+ // responsive heater.
4280
+ //
4281
+ // For Ultratemp we need to determine whether the differential temp
4282
+ // is within range. The other thing that needs to be calculated here is
4283
+ // whether Ultratemp can effeciently heat the pool.
4284
+ if (mode === 'ultratemp' || mode === 'ultratemppref') {
4285
+ if (hstate.isOn) {
4286
+ // For the preference mode we will try to reach the setpoint for a period of time then
4287
+ // switch over to the gas heater. Our algorithm for this is to check the rate of
4288
+ // change when the heater first kicks on. If we go for longer than an hour and still
4289
+ // haven't reached the setpoint then we will switch to gas.
4290
+ if (mode === 'ultratemppref' &&
4291
+ typeof hstate.startTime !== 'undefined' &&
4292
+ hstate.startTime.getTime() < new Date().getTime() - (60 * 60 * 1000))
4293
+ break;
4294
+ // If the heater is already on we will heat to 1 degree past the setpoint.
4295
+ if (body.temp - 1 < cfgBody.heatSetpoint) {
4296
+ isOn = true;
4297
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
4298
+ isHeating = true;
4299
+ isCooling = false;
4300
+ }
4301
+ else if (body.temp + 1 > cfgBody.coolSetpoint && heater.coolingEnabled) {
4302
+ isOn = true;
4303
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
4304
+ isHeating = false;
4305
+ isCooling = true;
4306
+ }
4307
+ }
4308
+ else {
4309
+ let delayStart = typeof hstate.endTime !== 'undefined' ? (hstate.endTime.getTime() + (30 * 60 * 1000)) > new Date().getTime() : false;
4310
+ // The heater is not currently on lets turn it on if we pass all the criteria.
4311
+ if ((body.temp < cfgBody.heatSetpoint && !delayStart)
4312
+ || body.temp + heater.differentialTemp < cfgBody.heatSetpoint) {
4313
+ isOn = true;
4314
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
4315
+ isHeating = true;
4316
+ isCooling = false;
4317
+ }
4318
+ else if (body.temp > cfgBody.coolSetpoint && heater.coolingEnabled) {
4319
+ if (!delayStart || body.temp - heater.differentialTemp > cfgBody.coolSetpoint) {
4320
+ isOn = true;
4321
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
4322
+ isHeating = false;
4323
+ isCooling = true;
4324
+ }
4325
+ }
4326
+ }
4327
+ }
4328
+ break;
4329
+ case 'hybrid':
4330
+ if (mode !== 'off') {
4331
+ //console.log(`Mode: ${mode} Setpoint: ${cfgBody.setPoint}`);
4332
+ isHeating = isOn = true;
4333
+ isCooling = false;
4334
+ if (hstate.isOn) {
4335
+ // If the heater is already on we will heat to 1 degree past the setpoint.
4336
+ if (body.temp - 1 < cfgBody.heatSetpoint) {
4337
+ isOn = true;
4338
+ // Heat Status will be set by the returns from the heater.
4339
+ //body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
4340
+ isHeating = true;
4341
+ isCooling = false;
4342
+ }
4343
+ }
4344
+ else {
4345
+ let delayStart = typeof hstate.endTime !== 'undefined' ? (hstate.endTime.getTime() + (30 * 60 * 1000)) > new Date().getTime() : false;
4346
+ // The heater is not currently on lets turn it on if we pass all the criteria.
4347
+ if ((body.temp < cfgBody.heatSetpoint && !delayStart)
4348
+ || body.temp + heater.differentialTemp < cfgBody.heatSetpoint) {
4349
+ isOn = true;
4350
+ // Heat Status will be set by the returns from the heater.
4351
+ //body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
4352
+ isHeating = true;
4353
+ isCooling = false;
4354
+ }
4355
+ }
4356
+ }
4357
+ break;
4358
+
4359
+ case 'mastertemp':
4360
+ // If we make it here, the other heater is not heating the body.
4361
+ if (mode === 'mtheater' || mode === 'heatpumppref' || mode === 'ultratemppref' || mode === 'solarpref') {
4362
+ if (body.temp < cfgBody.setPoint) {
4363
+ isOn = true;
4364
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('mtheat');
4365
+ isHeating = true;
4366
+ }
4367
+ }
4368
+ break;
4369
+ case 'maxetherm':
4370
+ case 'gas':
4371
+ // If we make it here, the other heater is not heating the body.
4372
+ if (mode === 'heater' || mode === 'solarpref' || mode === 'heatpumppref' || mode === 'ultratemppref') {
4373
+ // Heat past the setpoint for the heater but only if the heater is currently on.
4374
+ if ((body.temp - (hstate.isOn ? heater.stopTempDelta : 0)) < cfgBody.setPoint) {
4375
+ isOn = true;
4376
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
4377
+ isHeating = true;
4378
+ }
4379
+ }
4380
+ break;
4381
+ case 'heatpump':
4382
+ if (mode === 'heatpump' || mode === 'heatpumppref') {
4383
+ // Heat past the setpoint for the heater but only if the heater is currently on.
4384
+ if ((body.temp - (hstate.isOn ? heater.stopTempDelta : 0)) < cfgBody.setPoint) {
4385
+ isOn = true;
4386
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
4387
+ isHeating = true;
4388
+ }
4389
+ //if (hstate.isOn) {
4390
+ // // If the heater is already on we will heat to 1 degree past the setpoint.
4391
+ // if (body.temp - 1 < cfgBody.heatSetpoint) {
4392
+ // isOn = true;
4393
+ // body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
4394
+ // isHeating = true;
4395
+ // isCooling = false;
4396
+ // }
4397
+ //}
4398
+ //else {
4399
+ // // The heater is not currently on lets turn it on if we pass all the criteria.
4400
+ // if ((body.temp < cfgBody.heatSetpoint && hstate.endTime.getTime() < new Date().getTime() + (30 * 60 * 1000))
4401
+ // || body.temp + heater.differentialTemp < cfgBody.heatSetpoint) {
4402
+ // isOn = true;
4403
+ // body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
4404
+ // isHeating = true;
4405
+ // isCooling = false;
4406
+ // }
4407
+ //}
4408
+ }
4409
+ break;
4410
+ default:
4411
+ isOn = utils.makeBool(hstate.isOn);
4412
+ break;
4413
+ }
4414
+ logger.debug(`Heater Type: ${htype.name} Mode:${mode} Temp: ${body.temp} Setpoint: ${cfgBody.setPoint} Status: ${body.heatStatus}`);
4415
+ }
4416
+ }
4417
+ else {
4418
+ let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
4419
+ switch (htype.name) {
4420
+ case 'mastertemp':
4421
+ if (hstatus === 'mtheat') isHeating = isOn = true;
4422
+ break;
4423
+ case 'maxetherm':
4424
+ case 'gas':
4425
+ if (hstatus === 'heater') isHeating = isOn = true;
4426
+ break;
4427
+ case 'hybrid':
4428
+ if (hstatus === 'mtheat' || hstatus === 'heater' || hstatus === 'dual' || hstatus === 'hybheat') isHeating = isOn = true;
4429
+ break;
4430
+ case 'ultratemp':
4431
+ case 'heatpump':
4432
+ if (mode === 'ultratemp' || mode === 'ultratemppref' || mode === 'heatpump' || mode === 'heatpumppref') {
4433
+ if (hstatus === 'heater') isHeating = isOn = true;
4434
+ else if (hstatus === 'cooling') isCooling = isOn = true;
4435
+ }
4436
+ break;
4437
+ case 'solar':
4438
+ if (mode === 'solar' || mode === 'solarpref') {
4439
+ if (hstatus === 'solar') isHeating = isOn = true;
4440
+ else if (hstatus === 'cooling') isCooling = isOn = true;
4441
+ }
4442
+ break;
4443
+ }
4257
4444
  }
4258
- }
4259
- else {
4260
- // The heater is not currently on lets turn it on if we pass all the criteria.
4261
- if ((body.temp < cfgBody.heatSetpoint && hstate.endTime.getTime() < new Date().getTime() + (30 * 60 * 1000))
4262
- || body.temp + heater.differentialTemp < cfgBody.heatSetpoint) {
4263
- isOn = true;
4264
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
4265
- isHeating = true;
4266
- isCooling = false;
4445
+ if (isOn === true && typeof hon.find(elem => elem === heater.id) === 'undefined') {
4446
+ hon.push(heater.id);
4447
+ if (heater.master === 1 && isOn) (async () => {
4448
+ try {
4449
+ hstate.bodyId = body.id;
4450
+ if (sys.board.valueMaps.heatStatus.getName(body.heatStatus) === 'cooldown')
4451
+ await ncp.heaters.setHeaterStateAsync(hstate, false, false);
4452
+ else if (isOn) {
4453
+ hstate.bodyId = body.id;
4454
+ await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
4455
+ }
4456
+ else if (hstate.isOn !== isOn || hstate.isCooling !== isCooling) {
4457
+ await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
4458
+ }
4459
+ } catch (err) { logger.error(`Error setting heater state ${hstate.name}: ${err.message}`); }
4460
+ })();
4461
+ else {
4462
+ hstate.isOn = isOn;
4463
+ hstate.bodyId = body.id;
4464
+ }
4267
4465
  }
4268
- }
4466
+ // If there is a heater on for the body we need break out of the loop. This will make sure for instance a gas heater
4467
+ // isn't started when one of the more economical methods are.
4468
+ if (isOn === true) break;
4269
4469
  }
4270
- break;
4271
- default:
4272
- isOn = utils.makeBool(hstate.isOn);
4273
- break;
4274
4470
  }
4275
- logger.debug(`Heater Type: ${htype.name} Mode:${mode} Temp: ${body.temp} Setpoint: ${cfgBody.setPoint} Status: ${body.heatStatus}`);
4276
- }
4277
- }
4278
- else {
4279
- let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
4280
- switch (htype.name) {
4281
- case 'mastertemp':
4282
- if (hstatus === 'mtheat') isHeating = isOn = true;
4283
- break;
4284
- case 'maxetherm':
4285
- case 'gas':
4286
- if (hstatus === 'heater') isHeating = isOn = true;
4287
- break;
4288
- case 'hybrid':
4289
- if (hstatus === 'mtheat' || hstatus === 'heater' || hstatus === 'dual') isHeating = isOn = true;
4290
- break;
4291
- case 'ultratemp':
4292
- case 'heatpump':
4293
- if (mode === 'ultratemp' || mode === 'ultratemppref' || mode === 'heatpump' || mode === 'heatpumppref') {
4294
- if (hstatus === 'heater') isHeating = isOn = true;
4295
- else if (hstatus === 'cooling') isCooling = isOn = true;
4296
- }
4297
- break;
4298
- case 'solar':
4299
- if (mode === 'solar' || mode === 'solarpref') {
4300
- if (hstatus === 'solar') isHeating = isOn = true;
4301
- else if (hstatus === 'cooling') isCooling = isOn = true;
4302
- }
4303
- break;
4304
- }
4471
+ if (sys.controllerType === ControllerType.Nixie && !isHeating && !isCooling && hstatus !== 'cooldown') body.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
4472
+ //else if (sys.controllerType === ControllerType.Nixie) body.heatStatus = 0;
4305
4473
  }
4306
- if (isOn === true && typeof hon.find(elem => elem === heater.id) === 'undefined') {
4307
- hon.push(heater.id);
4308
- if (heater.master === 1 && isOn) (async () => {
4309
- try {
4310
- hstate.bodyId = body.id;
4311
- if (sys.board.valueMaps.heatStatus.getName(body.heatStatus) === 'cooldown')
4312
- await ncp.heaters.setHeaterStateAsync(hstate, false, false);
4313
- else if (isOn) {
4314
- hstate.bodyId = body.id;
4315
- await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
4316
- }
4317
- else if (hstate.isOn !== isOn || hstate.isCooling !== isCooling) {
4318
- await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
4319
- }
4320
- } catch (err) { logger.error(err.message); }
4321
- })();
4322
- else {
4323
- hstate.isOn = isOn;
4324
- hstate.bodyId = body.id;
4325
- }
4474
+ // Turn off any heaters that should be off. The code above only turns heaters on.
4475
+ for (let i = 0; i < heaters.length; i++) {
4476
+ let heater: Heater = heaters[i];
4477
+ if (typeof hon.find(elem => elem === heater.id) === 'undefined') {
4478
+ let hstate = state.heaters.getItemById(heater.id, true);
4479
+ if (heater.master === 1) (async () => {
4480
+ try {
4481
+ await ncp.heaters.setHeaterStateAsync(hstate, false, false);
4482
+ hstate.bodyId = 0;
4483
+ } catch (err) { logger.error(`Error turning off heater ${heater.name}: ${err.message}`); }
4484
+ })();
4485
+ else {
4486
+ hstate.isOn = false;
4487
+ hstate.bodyId = 0;
4488
+ }
4489
+ }
4326
4490
  }
4327
- // If there is a heater on for the body we need break out of the loop. This will make sure for instance a gas heater
4328
- // isn't started when one of the more economical methods are.
4329
- if (isOn === true) break;
4330
- }
4331
- }
4332
- if (sys.controllerType === ControllerType.Nixie && !isHeating && !isCooling && hstatus !== 'cooldown') body.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
4333
- //else if (sys.controllerType === ControllerType.Nixie) body.heatStatus = 0;
4334
- }
4335
- // Turn off any heaters that should be off. The code above only turns heaters on.
4336
- for (let i = 0; i < heaters.length; i++) {
4337
- let heater: Heater = heaters[i];
4338
- if (typeof hon.find(elem => elem === heater.id) === 'undefined') {
4339
- let hstate = state.heaters.getItemById(heater.id, true);
4340
- if (heater.master === 1) (async () => {
4341
- try {
4342
- await ncp.heaters.setHeaterStateAsync(hstate, false, false);
4343
- hstate.bodyId = 0;
4344
- } catch (err) { logger.error(err.message); }
4345
- })();
4346
- else {
4347
- hstate.isOn = false;
4348
- hstate.bodyId = 0;
4349
- }
4350
- }
4351
- }
4352
- } catch (err) { logger.error(`Error synchronizing heater states: ${err.message}`); }
4353
- }
4491
+ } catch (err) { logger.error(`Error synchronizing heater states: ${err.message}`); }
4492
+ }
4354
4493
  }
4355
4494
  export class ValveCommands extends BoardCommands {
4356
4495
  public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
@@ -4409,7 +4548,7 @@ export class ValveCommands extends BoardCommands {
4409
4548
  else
4410
4549
  vstate.isDiverted = isDiverted;
4411
4550
  }
4412
- public async setValveAsync(obj: any): Promise<Valve> {
4551
+ public async setValveAsync(obj: any, send: boolean = true): Promise<Valve> {
4413
4552
  try {
4414
4553
  let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1;
4415
4554
  obj.master = 1;
@@ -4533,60 +4672,224 @@ export class ValveCommands extends BoardCommands {
4533
4672
  return arrIds;
4534
4673
  }
4535
4674
  }
4536
- export class ChemControllerCommands extends BoardCommands {
4537
- public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
4538
- try {
4539
- // First delete the chemControllers that should be removed.
4540
- for (let i = 0; i < ctx.chemControllers.remove.length; i++) {
4541
- let c = ctx.chemControllers.remove[i];
4675
+ export class ChemDoserCommands extends BoardCommands {
4676
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
4542
4677
  try {
4543
- await sys.board.chemControllers.deleteChemControllerAsync(c);
4544
- res.addModuleSuccess('chemController', `Remove: ${c.id}-${c.name}`);
4545
- } catch (err) { res.addModuleError('chemController', `Remove: ${c.id}-${c.name}: ${err.message}`); }
4546
- }
4547
- for (let i = 0; i < ctx.chemControllers.update.length; i++) {
4548
- let c = ctx.chemControllers.update[i];
4678
+ // First delete the chemDosers that should be removed.
4679
+ for (let i = 0; i < ctx.chemDosers.remove.length; i++) {
4680
+ let c = ctx.chemDosers.remove[i];
4681
+ try {
4682
+ await sys.board.chemDosers.deleteChemDoserAsync(c);
4683
+ res.addModuleSuccess('chemDoser', `Remove: ${c.id}-${c.name}`);
4684
+ } catch (err) { res.addModuleError('chemDoser', `Remove: ${c.id}-${c.name}: ${err.message}`); }
4685
+ }
4686
+ for (let i = 0; i < ctx.chemDosers.update.length; i++) {
4687
+ let c = ctx.chemDosers.update[i];
4688
+ try {
4689
+ await sys.board.chemDosers.setChemDoserAsync(c);
4690
+ res.addModuleSuccess('chemDoser', `Update: ${c.id}-${c.name}`);
4691
+ } catch (err) { res.addModuleError('chemDoser', `Update: ${c.id}-${c.name}: ${err.message}`); }
4692
+ }
4693
+ for (let i = 0; i < ctx.chemDosers.add.length; i++) {
4694
+ let c = ctx.chemDosers.add[i];
4695
+ try {
4696
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
4697
+ // it won't error out.
4698
+ let chem = sys.chemDosers.getItemById(c.id, true);
4699
+ await sys.board.chemDosers.setChemDoserAsync(c);
4700
+ res.addModuleSuccess('chemDoser', `Add: ${c.id}-${c.name}`);
4701
+ } catch (err) { res.addModuleError('chemDoser', `Add: ${c.id}-${c.name}: ${err.message}`); }
4702
+ }
4703
+ return true;
4704
+ } catch (err) { logger.error(`Error restoring chemDosers: ${err.message}`); res.addModuleError('system', `Error restoring chemDosers: ${err.message}`); return false; }
4705
+ }
4706
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
4549
4707
  try {
4550
- await sys.board.chemControllers.setChemControllerAsync(c);
4551
- res.addModuleSuccess('chemController', `Update: ${c.id}-${c.name}`);
4552
- } catch (err) { res.addModuleError('chemController', `Update: ${c.id}-${c.name}: ${err.message}`); }
4553
- }
4554
- for (let i = 0; i < ctx.chemControllers.add.length; i++) {
4555
- let c = ctx.chemControllers.add[i];
4708
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
4709
+ // Look at chemDosers.
4710
+ let cfg = rest.poolConfig;
4711
+ for (let i = 0; i < cfg.chemDosers.length; i++) {
4712
+ let r = cfg.chemDosers[i];
4713
+ let c = sys.chemDosers.find(elem => r.id === elem.id);
4714
+ if (typeof c === 'undefined') ctx.add.push(r);
4715
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
4716
+ }
4717
+ for (let i = 0; i < sys.chemDosers.length; i++) {
4718
+ let c = sys.chemDosers.getItemByIndex(i);
4719
+ let r = cfg.chemDosers.find(elem => elem.id == c.id);
4720
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
4721
+ }
4722
+ return ctx;
4723
+ } catch (err) { logger.error(`Error validating chemDosers for restore: ${err.message}`); }
4724
+ }
4725
+
4726
+ public async deleteChemDoserAsync(data: any): Promise<ChemDoser> {
4556
4727
  try {
4557
- // pull a little trick to first add the data then perform the update. This way we won't get a new id or
4558
- // it won't error out.
4559
- let chem = sys.chemControllers.getItemById(c.id, true);
4560
- // RSG 11.24.21. setChemControllerAsync will only set the type/address if it thinks it's new.
4561
- // For a restore, if we set the type/address here it will pass the validation steps.
4562
- chem.type = c.type;
4563
- // chem.address = c.address;
4564
- await sys.board.chemControllers.setChemControllerAsync(c);
4565
- res.addModuleSuccess('chemController', `Add: ${c.id}-${c.name}`);
4566
- } catch (err) { res.addModuleError('chemController', `Add: ${c.id}-${c.name}: ${err.message}`); }
4567
- }
4568
- return true;
4569
- } catch (err) { logger.error(`Error restoring chemControllers: ${err.message}`); res.addModuleError('system', `Error restoring chemControllers: ${err.message}`); return false; }
4570
- }
4571
- public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
4572
- try {
4573
- let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
4574
- // Look at chemControllers.
4575
- let cfg = rest.poolConfig;
4576
- for (let i = 0; i < cfg.chemControllers.length; i++) {
4577
- let r = cfg.chemControllers[i];
4578
- let c = sys.chemControllers.find(elem => r.id === elem.id);
4579
- if (typeof c === 'undefined') ctx.add.push(r);
4580
- else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
4581
- }
4582
- for (let i = 0; i < sys.chemControllers.length; i++) {
4583
- let c = sys.chemControllers.getItemByIndex(i);
4584
- let r = cfg.chemControllers.find(elem => elem.id == c.id);
4585
- if (typeof r === 'undefined') ctx.remove.push(c.get(true));
4586
- }
4587
- return ctx;
4588
- } catch (err) { logger.error(`Error validating chemControllers for restore: ${err.message}`); }
4589
- }
4728
+ let id = typeof data.id !== 'undefined' ? parseInt(data.id, 10) : -1;
4729
+ if (typeof id === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid Chem Controller Id`, id, 'chemDoser'));
4730
+ let chem = sys.chemDosers.getItemById(id);
4731
+ let schem = state.chemDosers.getItemById(id);
4732
+ schem.isActive = chem.isActive = false;
4733
+ await ncp.chemDosers.removeById(id);
4734
+ sys.chemDosers.removeItemById(id);
4735
+ state.chemDosers.removeItemById(id);
4736
+ sys.emitEquipmentChange();
4737
+ return Promise.resolve(chem);
4738
+ } catch (err) { logger.error(`Error deleting chem controller ${err.message}`); }
4739
+ }
4740
+ public async manualDoseAsync(data: any): Promise<ChemDoserState> {
4741
+ try {
4742
+ let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined;
4743
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot begin dosing: Invalid chem doser id was provided ${data.id}`, 'chemDoser', data.id));
4744
+ let chem = sys.chemDosers.find(elem => elem.id === id);
4745
+ if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot begin dosing: Chem doser was not found ${data.id}`, 'chemDoser', data.id));
4746
+ // Let's check the type. AFAIK you cannot manual dose an IntelliChem.
4747
+ // We are down to the nitty gritty. Let REM Chem do its thing.
4748
+ await ncp.chemDosers.manualDoseAsync(chem.id, data);
4749
+ return Promise.resolve(state.chemDosers.getItemById(id));
4750
+ } catch (err) { return Promise.reject(err); }
4751
+ }
4752
+ public async calibrateDoseAsync(data: any): Promise<ChemDoserState> {
4753
+ try {
4754
+ let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined;
4755
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot begin calibration: Invalid chem doser id was provided ${data.id}`, 'chemDoser', data.id));
4756
+ let chem = sys.chemDosers.find(elem => elem.id === id);
4757
+ if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot begin calibration: Chem doser was not found ${data.id}`, 'chemDoser', data.id));
4758
+ // We are down to the nitty gritty. Let REM Chem do its thing.
4759
+ await ncp.chemDosers.calibrateDoseAsync(chem.id, data);
4760
+ return Promise.resolve(state.chemDosers.getItemById(id));
4761
+ } catch (err) { return Promise.reject(err); }
4762
+ }
4763
+
4764
+ public async cancelDosingAsync(data: any): Promise<ChemDoserState> {
4765
+ try {
4766
+ let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined;
4767
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel dosing: Invalid chem doser id was provided ${data.id}`, 'chemDoser', data.id));
4768
+ let chem = sys.chemDosers.find(elem => elem.id === id);
4769
+ if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel dosing: Chem doser was not found ${data.id}`, 'chemDoser', data.id));
4770
+ // We are down to the nitty gritty. Let REM Chem do its thing.
4771
+ await ncp.chemDosers.cancelDoseAsync(chem.id, data);
4772
+ return Promise.resolve(state.chemDosers.getItemById(id));
4773
+ } catch (err) { return Promise.reject(err); }
4774
+ }
4775
+ public async manualMixAsync(data: any): Promise<ChemDoserState> {
4776
+ try {
4777
+ let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined;
4778
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot begin mixing: Invalid chem doser id was provided ${data.id}`, 'chemDoser', data.id));
4779
+ let chem = sys.chemDosers.find(elem => elem.id === id);
4780
+ if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot begin mixing: Chem doser was not found ${data.id}`, 'chemDoser', data.id));
4781
+ // We are down to the nitty gritty. Let REM Chem do its thing.
4782
+ await ncp.chemDosers.manualMixAsync(chem.id, data);
4783
+ return Promise.resolve(state.chemDosers.getItemById(id));
4784
+ } catch (err) { return Promise.reject(err); }
4785
+ }
4786
+ public async cancelMixingAsync(data: any): Promise<ChemDoserState> {
4787
+ try {
4788
+ let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined;
4789
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel mixing: Invalid chem doser id was provided ${data.id}`, 'chemDoser', data.id));
4790
+ let chem = sys.chemDosers.find(elem => elem.id === id);
4791
+ if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel mixing: Chem doser was not found ${data.id}`, 'chemDoser', data.id));
4792
+ // We are down to the nitty gritty. Let REM Chem do its thing.
4793
+ await ncp.chemDosers.cancelMixingAsync(chem.id, data);
4794
+ return Promise.resolve(state.chemDosers.getItemById(id));
4795
+ } catch (err) { return Promise.reject(err); }
4796
+ }
4797
+ public findChemDoser(data: any) {
4798
+ let id = parseInt(data.id, 10);
4799
+ if (!isNaN(id)) return sys.chemDosers.find(x => x.id === id);
4800
+ }
4801
+ public async setChemDoserAsync(data: any): Promise<ChemDoser> {
4802
+ // The following are the rules related to when an OCP is present.
4803
+ // ==============================================================
4804
+ // 1. IntelliChem cannot be controlled/polled via Nixie, since there is no enable/disable from the OCP at this point we don't know who is in control of polling.
4805
+ // 2. With *Touch Commands will be sent directly to the IntelliChem controller in the hopes that the OCP will pick it up. Turns out this is not correct. The TouchBoard now has the proper interface.
4806
+ // 3. njspc will communicate to the OCP for IntelliChem control via the configuration interface.
4807
+
4808
+ // The following are the rules related to when no OCP is present.
4809
+ // =============================================================
4810
+ // 1. All chemDosers will be controlled via Nixie (IntelliChem, REM Chem).
4811
+ try {
4812
+ let chem = sys.board.chemDosers.findChemDoser(data);
4813
+ let isAdd = typeof chem === 'undefined';
4814
+ if (isAdd && sys.equipment.maxChemDosers <= sys.chemDosers.length) return Promise.reject(new InvalidEquipmentDataError(`The maximum number of chem controllers have been added to your controller`, 'chemDoser', sys.equipment.maxChemDosers));
4815
+ if (isAdd) {
4816
+ // At this point we are going to add the chem controller no matter what.
4817
+ data.id = sys.chemDosers.getNextEquipmentId(new EquipmentIdRange(1, 10));
4818
+ chem = sys.chemDosers.getItemById(data.id, true);
4819
+ }
4820
+ chem.isActive = true;
4821
+ // So here is the thing. If you have an OCP then the IntelliChem must be controlled by that.
4822
+ // the messages on the bus will talk back to the OCP so if you do not do this mayhem will ensue.
4823
+ await ncp.chemDosers.setDoserAsync(chem, data);
4824
+ return Promise.resolve(chem);
4825
+ }
4826
+ catch (err) { return Promise.reject(err); }
4827
+ }
4828
+ public async setChemDoserStateAsync(data: any): Promise<ChemDoserState> {
4829
+ let chem = sys.board.chemDosers.findChemDoser(data);
4830
+ if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`A valid chem doser could not be found for id:${data.id}`, data.id, 'chemDoser'));
4831
+ data.id = chem.id;
4832
+ logger.info(`Setting ${chem.name} data ${chem.master}`);
4833
+ if (chem.master === 1) await ncp.chemDosers.setDoserAsync(chem, data);
4834
+ else await sys.board.chemDosers.setChemDoserAsync(data);
4835
+ let schem = state.chemDosers.getItemById(chem.id, true);
4836
+ return Promise.resolve(schem);
4837
+ }
4838
+ }
4839
+ export class ChemControllerCommands extends BoardCommands {
4840
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
4841
+ try {
4842
+ // First delete the chemControllers that should be removed.
4843
+ for (let i = 0; i < ctx.chemControllers.remove.length; i++) {
4844
+ let c = ctx.chemControllers.remove[i];
4845
+ try {
4846
+ await sys.board.chemControllers.deleteChemControllerAsync(c);
4847
+ res.addModuleSuccess('chemController', `Remove: ${c.id}-${c.name}`);
4848
+ } catch (err) { res.addModuleError('chemController', `Remove: ${c.id}-${c.name}: ${err.message}`); }
4849
+ }
4850
+ for (let i = 0; i < ctx.chemControllers.update.length; i++) {
4851
+ let c = ctx.chemControllers.update[i];
4852
+ try {
4853
+ await sys.board.chemControllers.setChemControllerAsync(c);
4854
+ res.addModuleSuccess('chemController', `Update: ${c.id}-${c.name}`);
4855
+ } catch (err) { res.addModuleError('chemController', `Update: ${c.id}-${c.name}: ${err.message}`); }
4856
+ }
4857
+ for (let i = 0; i < ctx.chemControllers.add.length; i++) {
4858
+ let c = ctx.chemControllers.add[i];
4859
+ try {
4860
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
4861
+ // it won't error out.
4862
+ let chem = sys.chemControllers.getItemById(c.id, true);
4863
+ // RSG 11.24.21. setChemControllerAsync will only set the type/address if it thinks it's new.
4864
+ // For a restore, if we set the type/address here it will pass the validation steps.
4865
+ chem.type = c.type;
4866
+ // chem.address = c.address;
4867
+ await sys.board.chemControllers.setChemControllerAsync(c);
4868
+ res.addModuleSuccess('chemController', `Add: ${c.id}-${c.name}`);
4869
+ } catch (err) { res.addModuleError('chemController', `Add: ${c.id}-${c.name}: ${err.message}`); }
4870
+ }
4871
+ return true;
4872
+ } catch (err) { logger.error(`Error restoring chemControllers: ${err.message}`); res.addModuleError('system', `Error restoring chemControllers: ${err.message}`); return false; }
4873
+ }
4874
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
4875
+ try {
4876
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
4877
+ // Look at chemControllers.
4878
+ let cfg = rest.poolConfig;
4879
+ for (let i = 0; i < cfg.chemControllers.length; i++) {
4880
+ let r = cfg.chemControllers[i];
4881
+ let c = sys.chemControllers.find(elem => r.id === elem.id);
4882
+ if (typeof c === 'undefined') ctx.add.push(r);
4883
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
4884
+ }
4885
+ for (let i = 0; i < sys.chemControllers.length; i++) {
4886
+ let c = sys.chemControllers.getItemByIndex(i);
4887
+ let r = cfg.chemControllers.find(elem => elem.id == c.id);
4888
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
4889
+ }
4890
+ return ctx;
4891
+ } catch (err) { logger.error(`Error validating chemControllers for restore: ${err.message}`); }
4892
+ }
4590
4893
 
4591
4894
  public async deleteChemControllerAsync(data: any): Promise<ChemController> {
4592
4895
  try {
@@ -4602,20 +4905,35 @@ export class ChemControllerCommands extends BoardCommands {
4602
4905
  return Promise.resolve(chem);
4603
4906
  } catch (err) { logger.error(`Error deleting chem controller ${err.message}`); }
4604
4907
  }
4605
- public async manualDoseAsync(data: any): Promise<ChemControllerState> {
4606
- try {
4607
- let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined;
4608
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot begin dosing: Invalid chem controller id was provided ${data.id}`, 'chemController', data.id));
4609
- let chem = sys.chemControllers.find(elem => elem.id === id);
4610
- if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot begin dosing: Chem controller was not found ${data.id}`, 'chemController', data.id));
4611
- // Let's check the type. AFAIK you cannot manual dose an IntelliChem.
4612
- let type = sys.board.valueMaps.chemControllerTypes.transform(chem.type);
4613
- if (type.name !== 'rem') return Promise.reject(new InvalidEquipmentDataError(`You can only perform manual dosing on REM Chem controllers. Cannot manually dose ${type.desc}`, 'chemController', data.id));
4614
- // We are down to the nitty gritty. Let REM Chem do its thing.
4615
- await ncp.chemControllers.manualDoseAsync(chem.id, data);
4616
- return Promise.resolve(state.chemControllers.getItemById(id));
4617
- } catch (err) { return Promise.reject(err); }
4618
- }
4908
+ public async manualDoseAsync(data: any): Promise<ChemControllerState> {
4909
+ try {
4910
+ let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined;
4911
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot begin dosing: Invalid chem controller id was provided ${data.id}`, 'chemController', data.id));
4912
+ let chem = sys.chemControllers.find(elem => elem.id === id);
4913
+ if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot begin dosing: Chem controller was not found ${data.id}`, 'chemController', data.id));
4914
+ // Let's check the type. AFAIK you cannot manual dose an IntelliChem.
4915
+ let type = sys.board.valueMaps.chemControllerTypes.transform(chem.type);
4916
+ if (type.name !== 'rem') return Promise.reject(new InvalidEquipmentDataError(`You can only perform manual dosing on REM Chem controllers. Cannot manually dose ${type.desc}`, 'chemController', data.id));
4917
+ // We are down to the nitty gritty. Let REM Chem do its thing.
4918
+ await ncp.chemControllers.manualDoseAsync(chem.id, data);
4919
+ return Promise.resolve(state.chemControllers.getItemById(id));
4920
+ } catch (err) { return Promise.reject(err); }
4921
+ }
4922
+ public async calibrateDoseAsync(data: any): Promise<ChemControllerState> {
4923
+ try {
4924
+ let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined;
4925
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot begin calibration: Invalid chem controller id was provided ${data.id}`, 'chemController', data.id));
4926
+ let chem = sys.chemControllers.find(elem => elem.id === id);
4927
+ if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot begin calibration: Chem controller was not found ${data.id}`, 'chemController', data.id));
4928
+ // Let's check the type. AFAIK you cannot manual dose an IntelliChem.
4929
+ let type = sys.board.valueMaps.chemControllerTypes.transform(chem.type);
4930
+ if (type.name !== 'rem') return Promise.reject(new InvalidEquipmentDataError(`You can only perform calibration dosing on REM Chem controllers. Cannot calibrate ${type.desc}`, 'chemController', data.id));
4931
+ // We are down to the nitty gritty. Let REM Chem do its thing.
4932
+ await ncp.chemControllers.calibrateDoseAsync(chem.id, data);
4933
+ return Promise.resolve(state.chemControllers.getItemById(id));
4934
+ } catch (err) { return Promise.reject(err); }
4935
+ }
4936
+
4619
4937
  public async cancelDosingAsync(data: any): Promise<ChemControllerState> {
4620
4938
  try {
4621
4939
  let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined;
@@ -4672,7 +4990,7 @@ export class ChemControllerCommands extends BoardCommands {
4672
4990
  if (!isNaN(id)) return sys.chemControllers.find(x => x.id === id);
4673
4991
  else if (!isNaN(address)) return sys.chemControllers.find(x => x.address === address);
4674
4992
  }
4675
- public async setChemControllerAsync(data: any): Promise<ChemController> {
4993
+ public async setChemControllerAsync(data: any, send: boolean = true): Promise<ChemController> {
4676
4994
  // The following are the rules related to when an OCP is present.
4677
4995
  // ==============================================================
4678
4996
  // 1. IntelliChem cannot be controlled/polled via Nixie, since there is no enable/disable from the OCP at this point we don't know who is in control of polling.
@@ -4683,8 +5001,9 @@ export class ChemControllerCommands extends BoardCommands {
4683
5001
  // =============================================================
4684
5002
  // 1. All chemControllers will be controlled via Nixie (IntelliChem, REM Chem).
4685
5003
  try {
5004
+ // let c1 = sys.chemControllers.getItemById(1);
4686
5005
  let chem = sys.board.chemControllers.findChemController(data);
4687
- let isAdd = typeof chem === 'undefined';
5006
+ let isAdd = typeof chem === 'undefined' || typeof chem.isActive === 'undefined';
4688
5007
  let type = sys.board.valueMaps.chemControllerTypes.encode(isAdd ? data.type : chem.type);
4689
5008
  if (typeof type === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`The chem controller type could not be determined ${data.type || type}`, 'chemController', type));
4690
5009
  if (isAdd && sys.equipment.maxChemControllers <= sys.chemControllers.length) return Promise.reject(new InvalidEquipmentDataError(`The maximum number of chem controllers have been added to your controller`, 'chemController', sys.equipment.maxChemControllers));