nodejs-poolcontroller 7.5.1 → 7.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -0
  2. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -0
  3. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  5. package/Changelog +19 -0
  6. package/Dockerfile +3 -3
  7. package/README.md +13 -8
  8. package/app.ts +1 -1
  9. package/config/Config.ts +38 -2
  10. package/config/VersionCheck.ts +27 -12
  11. package/controller/Constants.ts +2 -1
  12. package/controller/Equipment.ts +193 -9
  13. package/controller/Errors.ts +10 -0
  14. package/controller/Lockouts.ts +503 -0
  15. package/controller/State.ts +269 -64
  16. package/controller/boards/AquaLinkBoard.ts +1000 -0
  17. package/controller/boards/BoardFactory.ts +4 -0
  18. package/controller/boards/EasyTouchBoard.ts +468 -144
  19. package/controller/boards/IntelliCenterBoard.ts +466 -307
  20. package/controller/boards/IntelliTouchBoard.ts +37 -5
  21. package/controller/boards/NixieBoard.ts +671 -141
  22. package/controller/boards/SystemBoard.ts +1397 -641
  23. package/controller/comms/Comms.ts +462 -362
  24. package/controller/comms/messages/Messages.ts +174 -30
  25. package/controller/comms/messages/config/ChlorinatorMessage.ts +6 -3
  26. package/controller/comms/messages/config/CircuitMessage.ts +1 -0
  27. package/controller/comms/messages/config/ExternalMessage.ts +10 -8
  28. package/controller/comms/messages/config/HeaterMessage.ts +141 -29
  29. package/controller/comms/messages/config/OptionsMessage.ts +9 -2
  30. package/controller/comms/messages/config/PumpMessage.ts +53 -35
  31. package/controller/comms/messages/config/ScheduleMessage.ts +33 -25
  32. package/controller/comms/messages/config/ValveMessage.ts +2 -2
  33. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +38 -86
  34. package/controller/comms/messages/status/EquipmentStateMessage.ts +59 -23
  35. package/controller/comms/messages/status/HeaterStateMessage.ts +57 -3
  36. package/controller/comms/messages/status/IntelliChemStateMessage.ts +56 -8
  37. package/controller/comms/messages/status/PumpStateMessage.ts +23 -1
  38. package/controller/nixie/Nixie.ts +1 -1
  39. package/controller/nixie/bodies/Body.ts +3 -0
  40. package/controller/nixie/chemistry/ChemController.ts +164 -51
  41. package/controller/nixie/chemistry/Chlorinator.ts +137 -88
  42. package/controller/nixie/circuits/Circuit.ts +51 -19
  43. package/controller/nixie/heaters/Heater.ts +241 -31
  44. package/controller/nixie/pumps/Pump.ts +488 -206
  45. package/controller/nixie/schedules/Schedule.ts +91 -35
  46. package/controller/nixie/valves/Valve.ts +1 -1
  47. package/defaultConfig.json +20 -0
  48. package/package.json +21 -21
  49. package/web/Server.ts +94 -49
  50. package/web/bindings/aqualinkD.json +505 -0
  51. package/web/bindings/influxDB.json +71 -1
  52. package/web/bindings/mqtt.json +98 -39
  53. package/web/bindings/mqttAlt.json +59 -1
  54. package/web/interfaces/baseInterface.ts +1 -0
  55. package/web/interfaces/httpInterface.ts +23 -2
  56. package/web/interfaces/influxInterface.ts +45 -10
  57. package/web/interfaces/mqttInterface.ts +114 -54
  58. package/web/services/config/Config.ts +55 -132
  59. package/web/services/state/State.ts +81 -4
  60. package/web/services/state/StateSocket.ts +4 -4
  61. package/web/services/utilities/Utilities.ts +8 -6
  62. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
  63. package/config copy.json +0 -300
  64. package/issue_template.md +0 -52
@@ -17,13 +17,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
  import * as extend from 'extend';
18
18
  import { EventEmitter } from 'events';
19
19
  import { SystemBoard, byteValueMap, byteValueMaps, ConfigQueue, ConfigRequest, CircuitCommands, FeatureCommands, ChlorinatorCommands, PumpCommands, BodyCommands, ScheduleCommands, HeaterCommands, EquipmentIdRange, ValveCommands, SystemCommands, ChemControllerCommands } from './SystemBoard';
20
- import { PoolSystem, Body, Schedule, Pump, ConfigVersion, sys, Heater, ICircuitGroup, LightGroupCircuit, LightGroup, ExpansionPanel, ExpansionModule, ExpansionModuleCollection, Valve, General, Options, Location, Owner, ICircuit, Feature, CircuitGroup, ChemController, TempSensorCollection } from '../Equipment';
20
+ import { PoolSystem, Body, Schedule, Pump, ConfigVersion, sys, Heater, ICircuitGroup, LightGroupCircuit, LightGroup, ExpansionPanel, ExpansionModule, ExpansionModuleCollection, Valve, General, Options, Location, Owner, ICircuit, Feature, CircuitGroup, ChemController, TempSensorCollection, Chlorinator } from '../Equipment';
21
21
  import { Protocol, Outbound, Inbound, Message, Response } from '../comms/messages/Messages';
22
22
  import { conn } from '../comms/Comms';
23
23
  import { logger } from '../../logger/Logger';
24
24
  import { state, ChlorinatorState, LightGroupState, VirtualCircuitState, ICircuitState, BodyTempState, CircuitGroupState, ICircuitGroupState, ChemControllerState } from '../State';
25
25
  import { utils } from '../../controller/Constants';
26
- import { InvalidEquipmentIdError, InvalidEquipmentDataError, EquipmentNotFoundError, MessageError } from '../Errors';
26
+ import { InvalidEquipmentIdError, InvalidEquipmentDataError, EquipmentNotFoundError, MessageError, InvalidOperationError } from '../Errors';
27
27
  import { ncp } from '../nixie/Nixie';
28
28
  export class IntelliCenterBoard extends SystemBoard {
29
29
  public needsConfigChanges: boolean = false;
@@ -47,26 +47,28 @@ export class IntelliCenterBoard extends SystemBoard {
47
47
  this.valueMaps.circuitFunctions = new byteValueMap([
48
48
  [0, { name: 'generic', desc: 'Generic' }],
49
49
  [1, { name: 'spillway', desc: 'Spillway' }],
50
- [2, { name: 'mastercleaner', desc: 'Master Cleaner' }],
50
+ [2, { name: 'mastercleaner', desc: 'Master Cleaner', body: 1 }],
51
51
  [3, { name: 'chemrelay', desc: 'Chem Relay' }],
52
52
  [4, { name: 'light', desc: 'Light', isLight: true }],
53
- [5, { name: 'intellibrite', desc: 'Intellibrite', isLight: true }],
54
- [6, { name: 'globrite', desc: 'GloBrite', isLight: true }],
53
+ [5, { name: 'intellibrite', desc: 'Intellibrite', isLight: true, theme: 'intellibrite' }],
54
+ [6, { name: 'globrite', desc: 'GloBrite', isLight: true, theme: 'intellibrite' }],
55
55
  [7, { name: 'globritewhite', desc: 'GloBrite White', isLight: true }],
56
- [8, { name: 'magicstream', desc: 'Magicstream', isLight: true }],
56
+ [8, { name: 'magicstream', desc: 'Magicstream', isLight: true, theme: 'intellibrite' }],
57
57
  [9, { name: 'dimmer', desc: 'Dimmer', isLight: true }],
58
- [10, { name: 'colorcascade', desc: 'ColorCascade', isLight: true }],
59
- [11, { name: 'mastercleaner2', desc: 'Master Cleaner 2' }],
60
- [12, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
61
- [13, { name: 'spa', desc: 'Spa', hasHeatSource: true }]
58
+ [10, { name: 'colorcascade', desc: 'ColorCascade', isLight: true, theme: 'intellibrite' }],
59
+ [11, { name: 'mastercleaner2', desc: 'Master Cleaner 2', body: 2 }],
60
+ [12, { name: 'pool', desc: 'Pool', hasHeatSource: true, body: 1 }],
61
+ [13, { name: 'spa', desc: 'Spa', hasHeatSource: true, body: 2 }]
62
62
  ]);
63
63
  this.valueMaps.pumpTypes = new byteValueMap([
64
- [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody:true }],
65
- [2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody:true }],
64
+ [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody: true }],
65
+ [2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody: true }],
66
66
  [3, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true }],
67
67
  [4, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
68
68
  [5, { name: 'vf', desc: 'Intelliflo VF', maxPrimingTime: 6, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
69
- [100, {name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1}]
69
+ [100, { name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1, maxSpeeds: 4, relays: [{ id: 1, name: 'Program #1' }, { id: 2, name: 'Program #2' }, { id: 3, name: 'Program #3' }, { id: 4, name: 'Program #4' }] }],
70
+ [101, { name: 'hwrly', desc: 'Hayward Relay VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1, maxSpeeds: 8, relays: [{ id: 1, name: 'Step #1' }, { id: 2, name: 'Step #2' }, { id: 3, name: 'Step #3' }, { id: 4, name: 'Pump On' }] }],
71
+ [102, { name: 'hwvs', desc: 'Hayward Eco/TriStar VS', minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true, equipmentMaster: 1 }]
70
72
  ]);
71
73
  // RSG - same as systemBoard definition; can delete.
72
74
  this.valueMaps.heatModes = new byteValueMap([
@@ -91,12 +93,12 @@ export class IntelliCenterBoard extends SystemBoard {
91
93
  ]);
92
94
  this.valueMaps.heaterTypes = new byteValueMap([
93
95
  [1, { name: 'gas', desc: 'Gas Heater', hasAddress: false }],
94
- [2, { name: 'solar', desc: 'Solar Heater', hasAddress: false, hasCoolSetpoint: true }],
95
- [3, { name: 'heatpump', desc: 'Heat Pump', hasAddress: true }],
96
- [4, { name: 'ultratemp', desc: 'UltraTemp', hasAddress: true, hasCoolSetpoint: true }],
96
+ [2, { name: 'solar', desc: 'Solar Heater', hasAddress: false, hasCoolSetpoint: true, hasPreference: true }],
97
+ [3, { name: 'heatpump', desc: 'Heat Pump', hasAddress: true, hasPreference: true }],
98
+ [4, { name: 'ultratemp', desc: 'UltraTemp', hasAddress: true, hasCoolSetpoint: true, hasPreference: true }],
97
99
  [5, { name: 'hybrid', desc: 'Hybrid', hasAddress: true }],
98
- [6, { name: 'maxetherm', desc: 'Max-E-Therm', hasAddress: true }],
99
- [7, { name: 'mastertemp', desc: 'MasterTemp', hasAddress: true }]
100
+ [6, { name: 'mastertemp', desc: 'MasterTemp', hasAddress: true }],
101
+ [7, { name: 'maxetherm', desc: 'Max-E-Therm', hasAddress: true }],
100
102
  ]);
101
103
 
102
104
 
@@ -133,7 +135,7 @@ export class IntelliCenterBoard extends SystemBoard {
133
135
  [4, { name: 'i10P', part: '521993Z', desc: 'i10P Personality Card', bodies: 1, valves: 2, circuits: 10, shared: false, dual: false, chlorinators: 1, chemControllers: 1 }], // This is a guess
134
136
  [5, { name: 'i10PS', part: '521873Z', desc: 'i10PS Personality Card', bodies: 2, valves: 4, circuits: 11, shared: true, dual: false, chlorinators: 1, chemControllers: 1 }],
135
137
  [6, { name: 'i10x', part: '522997Z', desc: 'i10x Expansion Module', circuits: 10 }],
136
- [7, { name: 'i10D', part: '523029Z', desc: 'i10D Personality Card', bodies: 2, valves: 2, circuits: 11, shared: false, dual: true, chlorinators: 1, chemControllers: 1 }], // We have witnessed this in the wild
138
+ [7, { name: 'i10D', part: '523029Z', desc: 'i10D Personality Card', bodies: 2, valves: 2, circuits: 11, shared: false, dual: true, chlorinators: 1, chemControllers: 2 }], // We have witnessed this in the wild
137
139
  [8, { name: 'Valve Exp', part: '522440', desc: 'Valve Expansion Module', valves: 6 }],
138
140
  [9, { name: 'A/D Module', part: '522039', desc: 'A/D Cover Module', covers: 2 }], // Finally have a user with one of these
139
141
  [10, { name: 'iChlor Mux', part: '522719', desc: 'iChlor MUX Card', chlorinators: 3 }], // This is a guess
@@ -173,20 +175,49 @@ export class IntelliCenterBoard extends SystemBoard {
173
175
  [2, { name: 'sunset', desc: 'Sunset' }]
174
176
  ]);
175
177
  this.valueMaps.lightThemes = new byteValueMap([
176
- [0, { name: 'white', desc: 'White', sequence: 11 }],
177
- [1, { name: 'green', desc: 'Green', sequence: 9 }],
178
- [2, { name: 'blue', desc: 'Blue', sequence: 8 }],
179
- [3, { name: 'magenta', desc: 'Magenta', sequence: 12 }],
180
- [4, { name: 'red', desc: 'Red', sequence: 10 }],
181
- [5, { name: 'sam', desc: 'SAm Mode', sequence: 1 }],
182
- [6, { name: 'party', desc: 'Party', sequence: 2 }],
183
- [7, { name: 'romance', desc: 'Romance', sequence: 3 }],
184
- [8, { name: 'caribbean', desc: 'Caribbean', sequence: 4 }],
185
- [9, { name: 'american', desc: 'American', sequence: 5 }],
186
- [10, { name: 'sunset', desc: 'Sunset', sequence: 6 }],
187
- [11, { name: 'royal', desc: 'Royal', sequence: 7 }],
178
+ [0, { name: 'white', desc: 'White', sequence: 11, types:['intellibrite', 'magicstream'] }],
179
+ [1, { name: 'green', desc: 'Green', sequence: 9, types: ['intellibrite', 'magicstream'] }],
180
+ [2, { name: 'blue', desc: 'Blue', sequence: 8, types: ['intellibrite', 'magicstream'] }],
181
+ [3, { name: 'magenta', desc: 'Magenta', sequence: 12, types: ['intellibrite', 'magicstream'] }],
182
+ [4, { name: 'red', desc: 'Red', sequence: 10, types: ['intellibrite', 'magicstream'] }],
183
+ [5, { name: 'sam', desc: 'SAm Mode', sequence: 1, types: ['intellibrite', 'magicstream'] }],
184
+ [6, { name: 'party', desc: 'Party', sequence: 2, types: ['intellibrite', 'magicstream'] }],
185
+ [7, { name: 'romance', desc: 'Romance', sequence: 3, types: ['intellibrite', 'magicstream'] }],
186
+ [8, { name: 'caribbean', desc: 'Caribbean', sequence: 4, types: ['intellibrite', 'magicstream'] }],
187
+ [9, { name: 'american', desc: 'American', sequence: 5, types: ['intellibrite', 'magicstream'] }],
188
+ [10, { name: 'sunset', desc: 'Sunset', sequence: 6, types: ['intellibrite', 'magicstream'] }],
189
+ [11, { name: 'royal', desc: 'Royal', sequence: 7, types: ['intellibrite', 'magicstream'] }],
188
190
  [255, { name: 'none', desc: 'None' }]
189
191
  ]);
192
+ this.valueMaps.lightGroupCommands = new byteValueMap([
193
+ [1, { name: 'colorsync', desc: 'Sync', types: ['intellibrite'], command: 'colorSync', message: 'Synchronizing' }],
194
+ [2, { name: 'colorset', desc: 'Set', types: ['intellibrite'], command: 'colorSet', message: 'Sequencing Set Operation' }],
195
+ [3, { name: 'colorswim', desc: 'Swim', types: ['intellibrite'], command: 'colorSwim', message: 'Sequencing Swim Operation' }],
196
+ [12, { name: 'colorhold', desc: 'Hold', types: ['intellibrite', 'magicstream'], command: 'colorHold', message: 'Saving Current Colors', sequence: 13 }],
197
+ [13, { name: 'colorrecall', desc: 'Recall', types: ['intellibrite', 'magicstream'], command: 'colorRecall', message: 'Recalling Saved Colors', sequence: 14 }]
198
+ ]);
199
+
200
+ this.valueMaps.lightCommands = new byteValueMap([
201
+ [12, { name: 'colorhold', desc: 'Hold', types: ['intellibrite'], sequence: 13 }],
202
+ [13, { name: 'colorrecall', desc: 'Recall', types: ['intellibrite'], sequence: 14 }],
203
+ [15, {
204
+ name: 'lightthumper', desc: 'Thumper', types: ['magicstream'], command: 'lightThumper', message: 'Toggling Thumper',
205
+ sequence: [ // Cycle party mode 3 times.
206
+ { isOn: false, timeout: 100 },
207
+ { isOn: true, timeout: 100 },
208
+ { isOn: false, timeout: 100 },
209
+ { isOn: true, timeout: 5000 },
210
+ { isOn: false, timeout: 100 },
211
+ { isOn: true, timeout: 100 },
212
+ { isOn: false, timeout: 100 },
213
+ { isOn: true, timeout: 5000 },
214
+ { isOn: false, timeout: 100 },
215
+ { isOn: true, timeout: 100 },
216
+ { isOn: false, timeout: 100 },
217
+ { isOn: true, timeout: 1000 },
218
+ ]
219
+ }]
220
+ ]);
190
221
  this.valueMaps.lightColors = new byteValueMap([
191
222
  [0, { name: 'white', desc: 'White' }],
192
223
  [16, { name: 'lightgreen', desc: 'Light Green' }],
@@ -241,7 +272,6 @@ export class IntelliCenterBoard extends SystemBoard {
241
272
  public heaters: IntelliCenterHeaterCommands = new IntelliCenterHeaterCommands(this);
242
273
  public valves: IntelliCenterValveCommands = new IntelliCenterValveCommands(this);
243
274
  public chemControllers: IntelliCenterChemControllerCommands = new IntelliCenterChemControllerCommands(this);
244
-
245
275
  public reloadConfig() {
246
276
  //sys.resetSystem();
247
277
  sys.configVersion.clear();
@@ -569,7 +599,7 @@ export class IntelliCenterBoard extends SystemBoard {
569
599
  }
570
600
  public get commandSourceAddress(): number { return Message.pluginAddress; }
571
601
  public get commandDestAddress(): number { return 15; }
572
- public static getAckResponse(action: number): Response { return Response.create({ dest: sys.board.commandSourceAddress, action: 1, payload: [action] }); }
602
+ public static getAckResponse(action: number, source?: number, dest?: number): Response { return Response.create({ source: source, dest: dest || sys.board.commandSourceAddress, action: 1, payload: [action] }); }
573
603
  }
574
604
  class IntelliCenterConfigRequest extends ConfigRequest {
575
605
  constructor(cat: number, ver: number, items?: number[], oncomplete?: Function) {
@@ -640,14 +670,14 @@ class IntelliCenterConfigQueue extends ConfigQueue {
640
670
  response: Response.create({ dest:-1, action: 30, payload: [this.curr.category, itm], callback: () => { self.processNext(out); } })
641
671
  });
642
672
  logger.verbose(`Requesting config for: ${ConfigCategories[this.curr.category]} - Item: ${itm}`);
643
- setTimeout(conn.queueSendMessage, 50, out);
673
+ setTimeout(() => { conn.queueSendMessage(out) }, 50);
644
674
  } else {
645
675
  // Now that we are done check the configuration a final time. If we have anything outstanding
646
676
  // it will get picked up.
647
677
  state.status = 1;
648
678
  this.curr = null;
649
679
  this._processing = false;
650
- if (this._failed) setTimeout(function () { sys.checkConfiguration(); }, 100);
680
+ if (this._failed) setTimeout(() => { sys.checkConfiguration(); }, 100);
651
681
  logger.info(`Configuration Complete`);
652
682
  sys.board.heaters.updateHeaterServices();
653
683
  state.cleanupState();
@@ -752,24 +782,41 @@ class IntelliCenterConfigQueue extends ConfigQueue {
752
782
  this.maybeQueueItems(curr.general, ver.general, ConfigCategories.general, [0, 1, 2, 3, 4, 5, 6, 7]);
753
783
  this.maybeQueueItems(curr.covers, ver.covers, ConfigCategories.covers, [0, 1]);
754
784
  if (this.compareVersions(curr.schedules, ver.schedules)) {
755
- let req = new IntelliCenterConfigRequest(ConfigCategories.schedules, ver.schedules, [0, 1, 2, 3, 4], function (req: IntelliCenterConfigRequest) {
785
+ // Alright we used to think we could rely on the schedule start time as the trigger that identifies an active schedule. However, active
786
+ // schedules are actually determined by looking at the schedule type messages[8-10].
787
+ let req = new IntelliCenterConfigRequest(ConfigCategories.schedules, ver.schedules, [8, 9, 10], function (req: IntelliCenterConfigRequest) {
756
788
  let maxSchedId = sys.schedules.getMaxId();
757
789
  req.fillRange(5, 5 + Math.min(Math.ceil(maxSchedId / 40), 7)); // Circuits
758
- req.fillRange(8, 8 + Math.min(Math.ceil(maxSchedId / 40), 10)); // Flags
759
790
  req.fillRange(11, 11 + Math.min(Math.ceil(maxSchedId / 40), 13)); // Schedule days bitmask
760
- req.fillRange(14, 14 + Math.min(Math.ceil(maxSchedId / 40), 16)); // Unknown (one byte per schedule)
761
- req.fillRange(17, 17 + Math.min(Math.ceil(maxSchedId / 40), 19)); // Unknown (one byte per schedule)
762
- req.fillRange(20, 20 + Math.min(Math.ceil(maxSchedId / 40), 22)); // Unknown (one byte per schedule)
791
+ req.fillRange(0, Math.min(Math.ceil(maxSchedId / 40), 4)); // Start Time
763
792
  req.fillRange(23, 23 + Math.min(Math.ceil(maxSchedId / 20), 26)); // End Time
793
+ req.fillRange(14, 14 + Math.min(Math.ceil(maxSchedId / 40), 16)); // Start Month
794
+ req.fillRange(17, 17 + Math.min(Math.ceil(maxSchedId / 40), 19)); // Start Day
795
+ req.fillRange(20, 20 + Math.min(Math.ceil(maxSchedId / 40), 22)); // Start Year
764
796
  req.fillRange(28, 28 + Math.min(Math.ceil(maxSchedId / 40), 30)); // Heat Mode
765
797
  req.fillRange(31, 31 + Math.min(Math.ceil(maxSchedId / 40), 33)); // Heat Mode
766
798
  req.fillRange(34, 34 + Math.min(Math.ceil(maxSchedId / 40), 36)); // Heat Mode
767
799
  });
800
+ // DEPRECATED: 12-26-21 This was the old order of fetching the schedule. This did not work properly with start times of midnight since the start time of 0
801
+ // was previously being used to determine whether the schedule was active. The schedule/time type messages are now being used.
802
+ //let req = new IntelliCenterConfigRequest(ConfigCategories.schedules, ver.schedules, [0, 1, 2, 3, 4], function (req: IntelliCenterConfigRequest) {
803
+ // let maxSchedId = sys.schedules.getMaxId();
804
+ // req.fillRange(5, 5 + Math.min(Math.ceil(maxSchedId / 40), 7)); // Circuits
805
+ // req.fillRange(8, 8 + Math.min(Math.ceil(maxSchedId / 40), 10)); // Flags
806
+ // req.fillRange(11, 11 + Math.min(Math.ceil(maxSchedId / 40), 13)); // Schedule days bitmask
807
+ // req.fillRange(14, 14 + Math.min(Math.ceil(maxSchedId / 40), 16)); // Unknown (one byte per schedule)
808
+ // req.fillRange(17, 17 + Math.min(Math.ceil(maxSchedId / 40), 19)); // Unknown (one byte per schedule)
809
+ // req.fillRange(20, 20 + Math.min(Math.ceil(maxSchedId / 40), 22)); // Unknown (one byte per schedule)
810
+ // req.fillRange(23, 23 + Math.min(Math.ceil(maxSchedId / 20), 26)); // End Time
811
+ // req.fillRange(28, 28 + Math.min(Math.ceil(maxSchedId / 40), 30)); // Heat Mode
812
+ // req.fillRange(31, 31 + Math.min(Math.ceil(maxSchedId / 40), 33)); // Heat Mode
813
+ // req.fillRange(34, 34 + Math.min(Math.ceil(maxSchedId / 40), 36)); // Heat Mode
814
+ //});
768
815
  this.push(req);
769
816
  }
770
817
  this.maybeQueueItems(curr.systemState, ver.systemState, ConfigCategories.systemState, [0]);
771
818
  logger.info(`Queued ${this.remainingItems} configuration items`);
772
- if (this.remainingItems > 0) setTimeout(function () { self.processNext(); }, 50);
819
+ if (this.remainingItems > 0) setTimeout(() => { self.processNext(); }, 50);
773
820
  else {
774
821
  this._processing = false;
775
822
  if (this._newRequest) {
@@ -1292,7 +1339,7 @@ class IntelliCenterSystemCommands extends SystemCommands {
1292
1339
  let out = Outbound.create({
1293
1340
  action: 168,
1294
1341
  retries: 5,
1295
- payload: [12, 0, 10, parseInt(obj.timeZone, 10)],
1342
+ payload: [12, 0, 13, parseInt(obj.timeZone, 10)],
1296
1343
  response: IntelliCenterBoard.getAckResponse(168),
1297
1344
  onComplete: (err, msg) => {
1298
1345
  if (err) reject(err);
@@ -1956,15 +2003,83 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
1956
2003
  }
1957
2004
  catch (err) { return Promise.reject(err); }
1958
2005
  }
1959
- public sequenceLightGroupAsync(id: number, operation: string): Promise<LightGroupState> {
2006
+ public async runLightGroupCommandAsync(obj: any): Promise<ICircuitState> {
2007
+ // Do all our validation.
2008
+ try {
2009
+ let id = parseInt(obj.id, 10);
2010
+ let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightGroupCommands.findItem(obj.command) : { val: 0, name: 'undefined' };
2011
+ if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light group command ${cmd.name} does not exist`, 'runLightGroupCommandAsync'));
2012
+ if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light group ${id} does not exist`, 'runLightGroupCommandAsync'));
2013
+ let grp = sys.lightGroups.getItemById(id);
2014
+ let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name);
2015
+ let sgrp = state.lightGroups.getItemById(grp.id);
2016
+ sgrp.action = nop;
2017
+ sgrp.emitEquipmentChange();
2018
+ switch (cmd.name) {
2019
+ case 'colorset':
2020
+ await this.sequenceLightGroupAsync(id, 'colorset');
2021
+ break;
2022
+ case 'colorswim':
2023
+ await this.sequenceLightGroupAsync(id, 'colorswim');
2024
+ break;
2025
+ case 'colorhold':
2026
+ await this.setLightGroupThemeAsync(id, 12);
2027
+ break;
2028
+ case 'colorrecall':
2029
+ await this.setLightGroupThemeAsync(id, 13);
2030
+ break;
2031
+ case 'lightthumper':
2032
+ break;
2033
+ }
2034
+ sgrp.action = 0;
2035
+ sgrp.emitEquipmentChange();
2036
+ return sgrp;
2037
+ }
2038
+ catch (err) { return Promise.reject(`Error runLightGroupCommandAsync ${err.message}`); }
2039
+ }
2040
+ public async runLightCommandAsync(obj: any): Promise<ICircuitState> {
2041
+ // Do all our validation.
2042
+ try {
2043
+ let id = parseInt(obj.id, 10);
2044
+ let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightCommands.findItem(obj.command) : { val: 0, name: 'undefined' };
2045
+ if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light command ${cmd.name} does not exist`, 'runLightCommandAsync'));
2046
+ if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light ${id} does not exist`, 'runLightCommandAsync'));
2047
+ let circ = sys.circuits.getItemById(id);
2048
+ if (!circ.isActive) return Promise.reject(new InvalidOperationError(`Light circuit #${id} is not active`, 'runLightCommandAsync'));
2049
+ let type = sys.board.valueMaps.circuitFunctions.transform(circ.type);
2050
+ if (!type.isLight) return Promise.reject(new InvalidOperationError(`Circuit #${id} is not a light`, 'runLightCommandAsync'));
2051
+ let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name);
2052
+ let slight = state.circuits.getItemById(circ.id);
2053
+ slight.action = nop;
2054
+ slight.emitEquipmentChange();
2055
+ switch (cmd.name) {
2056
+ case 'colorhold':
2057
+ await this.setLightThemeAsync(id, 12);
2058
+ break;
2059
+ case 'colorrecall':
2060
+ await this.setLightThemeAsync(id, 13);
2061
+ break;
2062
+ case 'lightthumper':
2063
+ // I do not know how to trigger the thumper.
2064
+ break;
2065
+ }
2066
+ slight.action = 0;
2067
+ slight.emitEquipmentChange();
2068
+ return slight;
2069
+ }
2070
+ catch (err) { return Promise.reject(`Error runLightCommandAsync ${err.message}`); }
2071
+ }
2072
+ public async sequenceLightGroupAsync(id: number, operation: string): Promise<LightGroupState> {
1960
2073
  let sgroup = state.lightGroups.getItemById(id);
1961
- let nop = sys.board.valueMaps.intellibriteActions.getValue(operation);
1962
- if (nop > 0) {
1963
- let out = this.createCircuitStateMessage(id, true);
2074
+ try {
2075
+ if (!sgroup.isActive) return Promise.reject(new InvalidEquipmentIdError(`An active light group could not be found with id ${id}`, id, 'lightGroup'));
2076
+ let cmd = sys.board.valueMaps.lightGroupCommands.findItem(operation.toLowerCase());
1964
2077
  let ndx = id - sys.board.equipmentIds.circuitGroups.start;
1965
2078
  let byteNdx = Math.floor(ndx / 4);
1966
2079
  let bitNdx = (ndx * 2) - (byteNdx * 8);
2080
+ let out = this.createCircuitStateMessage(id, true);
1967
2081
  let byte = out.payload[28 + byteNdx];
2082
+
1968
2083
  // Each light group is represented by two bits on the status byte. There are 3 status bytes that give us only 12 of the 16 on the config stream but the 168 message
1969
2084
  // does acutally send 4 so all are represented there.
1970
2085
  // [10] = Set
@@ -1973,46 +2088,96 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
1973
2088
  // [11] = No sequencing underway.
1974
2089
  // In the end we are only trying to impact the specific bits in the middle of the byte that represent
1975
2090
  // the light group we are dealing with.
1976
- switch (nop) {
1977
- case 1: // Sync
2091
+ switch (cmd.name) {
2092
+ case 'colorsync':
1978
2093
  byte &= ((0xFC << bitNdx) | (0xFF >> (8 - bitNdx)));
1979
2094
  break;
1980
- case 2: // Color Set
2095
+ case 'colorset':
1981
2096
  byte &= ((0xFE << bitNdx) | (0xFF >> (8 - bitNdx)));
1982
2097
  break;
1983
- case 3: // Color Swim
2098
+ case 'colorswim':
1984
2099
  byte &= ((0xFD << bitNdx) | (0xFF >> (8 - bitNdx)));
1985
2100
  break;
2101
+ default:
2102
+ return Promise.reject(new InvalidOperationError(`Invalid Light Group Sequence ${operation}`, 'sequenceLightGroupAsync'));
1986
2103
  }
1987
- console.log({ groupNdx: ndx, action: nop, byteNdx: byteNdx, bitNdx: bitNdx, byte: byte })
2104
+ sgroup.emitEquipmentChange();
1988
2105
  out.payload[28 + byteNdx] = byte;
1989
- return new Promise<LightGroupState>((resolve, reject) => {
2106
+ // So now we have all the info we need to sequence the group.
2107
+ await new Promise((resolve, reject) => {
1990
2108
  out.retries = 5;
1991
2109
  out.response = IntelliCenterBoard.getAckResponse(168);
1992
2110
  out.onComplete = (err, msg) => {
1993
2111
  if (!err) {
1994
- sgroup.action = nop;
2112
+ sgroup.action = sys.board.valueMaps.circuitActions.getValue(cmd.name);
1995
2113
  state.emitEquipmentChanges();
1996
2114
  resolve(sgroup);
1997
2115
  }
1998
- else reject(err);
2116
+ else {
2117
+ sgroup.action = 0;
2118
+ reject(err);
2119
+ }
1999
2120
  };
2000
2121
  conn.queueSendMessage(out);
2001
2122
  });
2002
- }
2003
- return Promise.resolve(sgroup);
2004
- }
2005
- public getLightThemes(type: number): any[] {
2006
- switch (type) {
2007
- case 5: // Intellibrite
2008
- case 6: // Globrite
2009
- case 8: // Magicstream
2010
- case 10: // ColorCascade
2011
- return sys.board.valueMaps.lightThemes.toArray();
2012
- default:
2013
- return [];
2014
- }
2123
+ return sgroup;
2124
+ } catch (err) { return Promise.reject(new InvalidOperationError(`Error Sequencing Light Group: ${err.message}`, 'sequenceLightGroupAsync')); }
2125
+ //let nop = sys.board.valueMaps.circuitActions.getValue(operation);
2126
+ //if (nop > 0) {
2127
+ // let out = this.createCircuitStateMessage(id, true);
2128
+ // let ndx = id - sys.board.equipmentIds.circuitGroups.start;
2129
+ // let byteNdx = Math.floor(ndx / 4);
2130
+ // let bitNdx = (ndx * 2) - (byteNdx * 8);
2131
+ // let byte = out.payload[28 + byteNdx];
2132
+ // // Each light group is represented by two bits on the status byte. There are 3 status bytes that give us only 12 of the 16 on the config stream but the 168 message
2133
+ // // does acutally send 4 so all are represented there.
2134
+ // // [10] = Set
2135
+ // // [01] = Swim
2136
+ // // [00] = Sync
2137
+ // // [11] = No sequencing underway.
2138
+ // // In the end we are only trying to impact the specific bits in the middle of the byte that represent
2139
+ // // the light group we are dealing with.
2140
+ // switch (nop) {
2141
+ // case 1: // Sync
2142
+ // byte &= ((0xFC << bitNdx) | (0xFF >> (8 - bitNdx)));
2143
+ // break;
2144
+ // case 2: // Color Set
2145
+ // byte &= ((0xFE << bitNdx) | (0xFF >> (8 - bitNdx)));
2146
+ // break;
2147
+ // case 3: // Color Swim
2148
+ // byte &= ((0xFD << bitNdx) | (0xFF >> (8 - bitNdx)));
2149
+ // break;
2150
+ // }
2151
+ // console.log({ groupNdx: ndx, action: nop, byteNdx: byteNdx, bitNdx: bitNdx, byte: byte })
2152
+ // out.payload[28 + byteNdx] = byte;
2153
+ // return new Promise<LightGroupState>((resolve, reject) => {
2154
+ // out.retries = 5;
2155
+ // out.response = IntelliCenterBoard.getAckResponse(168);
2156
+ // out.onComplete = (err, msg) => {
2157
+ // if (!err) {
2158
+ // sgroup.action = nop;
2159
+ // state.emitEquipmentChanges();
2160
+ // resolve(sgroup);
2161
+ // }
2162
+ // else reject(err);
2163
+ // };
2164
+ // conn.queueSendMessage(out);
2165
+ // });
2166
+ //}
2167
+ //return Promise.resolve(sgroup);
2015
2168
  }
2169
+ // 12-01-21 RKS: This has been deprecated. This allows for multiple vendor light themes driven by the metadata on the valuemaps.
2170
+ //public getLightThemes(type: number): any[] {
2171
+ // switch (type) {
2172
+ // case 5: // Intellibrite
2173
+ // case 6: // Globrite
2174
+ // case 8: // Magicstream
2175
+ // case 10: // ColorCascade
2176
+ // return sys.board.valueMaps.lightThemes.toArray();
2177
+ // default:
2178
+ // return [];
2179
+ // }
2180
+ //}
2016
2181
  private async verifyVersionAsync(): Promise<boolean> {
2017
2182
  return new Promise<boolean>((resolve, reject) => {
2018
2183
  let out = Outbound.create({
@@ -2072,7 +2237,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
2072
2237
  conn.queueSendMessage(out);
2073
2238
  });
2074
2239
  }
2075
- public async setCircuitStateAsync(id: number, val: boolean): Promise<ICircuitState> {
2240
+ public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
2076
2241
  let c = sys.circuits.getInterfaceById(id);
2077
2242
  if (c.master !== 0) return await super.setCircuitStateAsync(id, val);
2078
2243
  // As of 1.047 there is a sequence to this.
@@ -2164,6 +2329,30 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
2164
2329
  }
2165
2330
  catch (err) { return Promise.reject(err); }
2166
2331
  }
2332
+ public async setColorHoldAsync(id: number): Promise<ICircuitState> {
2333
+ let circuit = sys.circuits.getInterfaceById(id);
2334
+ if (circuit.master === 1) return await super.setColorHoldAsync(id);
2335
+ try {
2336
+ if (sys.board.equipmentIds.circuitGroups.isInRange(id)) {
2337
+ await this.setLightGroupThemeAsync(id, 12);
2338
+ return Promise.resolve(state.lightGroups.getItemById(id));
2339
+ }
2340
+ return await this.setLightThemeAsync(id, 12);
2341
+ }
2342
+ catch (err) { return Promise.reject(err); }
2343
+ }
2344
+ public async setColorRecallAsync(id: number): Promise<ICircuitState> {
2345
+ let circuit = sys.circuits.getInterfaceById(id);
2346
+ if (circuit.master === 1) return await super.setColorHoldAsync(id);
2347
+ try {
2348
+ if (sys.board.equipmentIds.circuitGroups.isInRange(id)) {
2349
+ await this.setLightGroupThemeAsync(id, 13);
2350
+ return Promise.resolve(state.lightGroups.getItemById(id));
2351
+ }
2352
+ return await this.setLightThemeAsync(id, 13);
2353
+ }
2354
+ catch (err) { return Promise.reject(err); }
2355
+ }
2167
2356
  public async setLightThemeAsync(id: number, theme: number): Promise<ICircuitState> {
2168
2357
  let circuit = sys.circuits.getInterfaceById(id);
2169
2358
  if (circuit.master === 1) return await super.setLightThemeAsync(id, theme);
@@ -2179,6 +2368,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
2179
2368
  action: 168, payload: [1, 0, id - 1, circuit.type, circuit.freeze ? 1 : 0, circuit.showInFeatures ? 1 : 0,
2180
2369
  theme, Math.floor(circuit.eggTimer / 60), circuit.eggTimer - ((Math.floor(circuit.eggTimer) / 60) * 60), circuit.dontStop ? 1 : 0]
2181
2370
  });
2371
+ cstate.action = sys.board.valueMaps.circuitActions.getValue('lighttheme');
2182
2372
  out.response = IntelliCenterBoard.getAckResponse(168);
2183
2373
  out.retries = 5;
2184
2374
  await new Promise<void>((resolve, reject) => {
@@ -2191,6 +2381,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
2191
2381
  else {
2192
2382
  reject(err);
2193
2383
  }
2384
+ cstate.action = 0;
2194
2385
  };
2195
2386
  out.appendPayloadString(circuit.name, 16);
2196
2387
  conn.queueSendMessage(out);
@@ -2479,20 +2670,18 @@ class IntelliCenterFeatureCommands extends FeatureCommands {
2479
2670
  class IntelliCenterChlorinatorCommands extends ChlorinatorCommands {
2480
2671
  public async setChlorAsync(obj: any): Promise<ChlorinatorState> {
2481
2672
  let id = parseInt(obj.id, 10);
2673
+ // Bail out right away if this is not controlled by the OCP.
2674
+ if (typeof obj.master !== 'undefined' && parseInt(obj.master, 10) !== 0) return super.setChlorAsync(obj);
2482
2675
  let isAdd = false;
2483
- let chlor = sys.chlorinators.getItemById(id);
2484
- if (id <= 0 || isNaN(id)) {
2485
- isAdd = true;
2486
- chlor.master = utils.makeBool(obj.master) ? 1 : 0;
2487
- // Calculate an id for the chlorinator. The messed up part is that if a chlorinator is not attached to the OCP, its address
2488
- // cannot be set by the MUX. This will have to wait.
2676
+ if (isNaN(id) || id <= 0) {
2677
+ // We are adding so we need to see if there is another chlorinator that is not external.
2678
+ if (sys.chlorinators.count(elem => elem.master !== 2) > sys.equipment.maxChlorinators) return Promise.reject(new InvalidEquipmentDataError(`The max number of chlorinators has been exceeded you may only add ${sys.equipment.maxChlorinators}`, 'chlorinator', sys.equipment.maxChlorinators));
2489
2679
  id = 1;
2680
+ isAdd = true;
2490
2681
  }
2682
+ let chlor = sys.chlorinators.getItemById(id);
2683
+ if (chlor.master !== 0 && !isAdd) return super.setChlorAsync(obj);
2491
2684
 
2492
- //let chlor = extend(true, {}, sys.chlorinators.getItemById(id).get(), obj);
2493
- // If this is a virtual chlorinator then go to the base class and handle it from there.
2494
- if (chlor.master === 1) return super.setChlorAsync(obj);
2495
- if (typeof chlor.master === 'undefined') chlor.master = 0;
2496
2685
  let name = obj.name || chlor.name || 'IntelliChlor' + id;
2497
2686
  let superChlorHours = parseInt(obj.superChlorHours, 10);
2498
2687
  if (typeof obj.superChlorinate !== 'undefined') obj.superChlor = utils.makeBool(obj.superChlorinate);
@@ -2505,8 +2694,7 @@ class IntelliCenterChlorinatorCommands extends ChlorinatorCommands {
2505
2694
  let poolSetpoint = typeof obj.poolSetpoint !== 'undefined' ? parseInt(obj.poolSetpoint, 10) : chlor.poolSetpoint;
2506
2695
  let spaSetpoint = typeof obj.spaSetpoint !== 'undefined' ? parseInt(obj.spaSetpoint, 10) : chlor.spaSetpoint;
2507
2696
  if (poolSetpoint === 0) console.log(obj);
2508
-
2509
- let model = typeof obj.model !== 'undefined' ? obj.model : chlor.model;
2697
+ let model = typeof obj.model !== 'undefined' ? sys.board.valueMaps.chlorinatorModel.encode(obj.model) : chlor.model || 0;
2510
2698
  let chlorType = typeof obj.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(obj.type) : chlor.type || 0;
2511
2699
  if (isAdd) {
2512
2700
  if (isNaN(poolSetpoint)) poolSetpoint = 50;
@@ -2523,10 +2711,12 @@ class IntelliCenterChlorinatorCommands extends ChlorinatorCommands {
2523
2711
  if (typeof obj.disabled !== 'undefined') chlor.disabled = utils.makeBool(obj.disabled);
2524
2712
  if (typeof chlor.body === 'undefined') chlor.body = obj.body || 32;
2525
2713
  // Verify the data.
2526
- let body = sys.board.bodies.mapBodyAssociation(chlor.body);
2714
+ let body = sys.board.bodies.mapBodyAssociation(typeof obj.body === 'undefined' ? chlor.body || 0 : obj.body);
2527
2715
  if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body association is not valid: ${chlor.body}`, 'chlorinator', chlor.body));
2528
2716
  if (poolSetpoint > 100 || poolSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator poolSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.poolSetpoint));
2529
2717
  if (spaSetpoint > 100 || spaSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator spaSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.spaSetpoint));
2718
+ let portId = typeof obj.portId !== 'undefined' ? parseInt(obj.portId, 10) : chlor.portId;
2719
+ if (portId !== chlor.portId && sys.chlorinators.count(elem => elem.id !== chlor.id && elem.portId === portId && elem.master !== 2) > 0) return Promise.reject(new InvalidEquipmentDataError(`Another chlorinator is installed on port #${portId}. Only one chlorinator can be installed per port.`, 'Chlorinator', portId));
2530
2720
  if (typeof obj.ignoreSaltReading !== 'undefined') chlor.ignoreSaltReading = utils.makeBool(obj.ignoreSaltReading);
2531
2721
  return new Promise<ChlorinatorState>((resolve, reject) => {
2532
2722
  let out = Outbound.create({
@@ -2542,8 +2732,10 @@ class IntelliCenterChlorinatorCommands extends ChlorinatorCommands {
2542
2732
  else {
2543
2733
  let schlor = state.chlorinators.getItemById(id, true);
2544
2734
  let cchlor = sys.chlorinators.getItemById(id, true);
2735
+ chlor.master = 0;
2736
+ schlor.body = chlor.body = body.val;
2545
2737
  chlor.disabled = disabled;
2546
- chlor.model = model;
2738
+ schlor.model = chlor.model = model;
2547
2739
  schlor.type = chlor.type = chlorType;
2548
2740
  chlor.name = schlor.name = name;
2549
2741
  chlor.isDosing = isDosing;
@@ -2759,10 +2951,11 @@ class IntelliCenterPumpCommands extends PumpCommands {
2759
2951
  let speed = parseInt(c.speed, 10);
2760
2952
  let flow = parseInt(c.flow, 10);
2761
2953
  let circuit = i < type.maxCircuits ? parseInt(c.circuit, 10) : 256;
2762
- let units = parseInt(c.units, 10);
2763
- if (isNaN(units)) units = 0;
2764
- if (type.name === 'vs') units = 0;
2765
- else if (type.name === 'vf') units = 1;
2954
+ let units;
2955
+ if (type.name === 'vf') units = sys.board.valueMaps.pumpUnits.getValue('gpm');
2956
+ else if (type.name === 'vs') units = sys.board.valueMaps.pumpUnits.getValue('rpm');
2957
+ else units = sys.board.valueMaps.pumpUnits.encode(c.units);
2958
+ if (isNaN(units)) units = sys.board.valueMaps.pumpUnits.getValue('rpm');
2766
2959
  outc.setPayloadByte(i + 18, circuit - 1, circ.circuit - 1);
2767
2960
  if (typeof type.minSpeed !== 'undefined' && (parseInt(c.units, 10) === 0 || isNaN(parseInt(c.units, 10)))) {
2768
2961
  outc.setPayloadByte(i + 26, 0); // Set to rpm
@@ -2898,6 +3091,9 @@ class IntelliCenterPumpCommands extends PumpCommands {
2898
3091
  // We now need to get the type for the pump. If the incoming data doesn't include it then we need to
2899
3092
  // get it from the current pump configuration.
2900
3093
  let pump = sys.pumps.getItemById(id, false);
3094
+ // Check to see if this happens to be a Nixie Pump.
3095
+ if (pump.master === 1) return super.deletePumpAsync(data);
3096
+
2901
3097
  if (typeof pump.type === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Pump #${data.id} does not exist in configuration`, data.id, 'Schedule'));
2902
3098
  let outc = Outbound.create({ action: 168, payload: [4, 0, id - 1, 0, 0, id + 95] });
2903
3099
  outc.appendPayloadInt(450); // 6
@@ -2943,6 +3139,94 @@ class IntelliCenterPumpCommands extends PumpCommands {
2943
3139
  }
2944
3140
  }
2945
3141
  class IntelliCenterBodyCommands extends BodyCommands {
3142
+ private bodyHeatSettings: {
3143
+ processing: boolean,
3144
+ bytes: number[],
3145
+ body1: { heatMode: number, heatSetpoint: number, coolSetpoint: number },
3146
+ body2: { heatMode: number, heatSetpoint: number, coolSetpoint: number }
3147
+ };
3148
+ private async queueBodyHeatSettings(bodyId?: number, byte?: number, data?: any): Promise<Boolean> {
3149
+ if (typeof this.bodyHeatSettings === 'undefined') {
3150
+ let body1 = sys.bodies.getItemById(1);
3151
+ let body2 = sys.bodies.getItemById(2);
3152
+ this.bodyHeatSettings = {
3153
+ processing: false,
3154
+ bytes: [],
3155
+ body1: { heatMode: body1.heatMode || 1, heatSetpoint: body1.heatSetpoint || 78, coolSetpoint: body1.coolSetpoint || 100 },
3156
+ body2: { heatMode: body2.heatMode || 1, heatSetpoint: body2.heatSetpoint || 78, coolSetpoint: body2.coolSetpoint || 100 }
3157
+ }
3158
+ }
3159
+ let bhs = this.bodyHeatSettings;
3160
+ if (typeof data !== 'undefined' && typeof bodyId !== 'undefined' && bodyId > 0) {
3161
+ let body = bodyId === 2 ? bhs.body2 : bhs.body1;
3162
+ if (!bhs.bytes.includes(byte) && byte) bhs.bytes.push(byte);
3163
+ if (typeof data.heatSetpoint !== 'undefined') body.heatSetpoint = data.heatSetpoint;
3164
+ if (typeof data.coolSetpoint !== 'undefined') body.coolSetpoint = data.coolSetpoint;
3165
+ if (typeof data.heatMode !== 'undefined') body.heatMode = data.heatMode;
3166
+ }
3167
+ if (!bhs.processing && bhs.bytes.length > 0) {
3168
+ bhs.processing = true;
3169
+ let byte2 = bhs.bytes.shift();
3170
+ let fnToByte = function (num) { return num < 0 ? Math.abs(num) | 0x80 : Math.abs(num) || 0; };
3171
+ let payload = [0, 0, byte2, 1,
3172
+ fnToByte(sys.equipment.tempSensors.getCalibration('water1')),
3173
+ fnToByte(sys.equipment.tempSensors.getCalibration('solar1')),
3174
+ fnToByte(sys.equipment.tempSensors.getCalibration('air')),
3175
+ fnToByte(sys.equipment.tempSensors.getCalibration('water2')),
3176
+ fnToByte(sys.equipment.tempSensors.getCalibration('solar2')),
3177
+ fnToByte(sys.equipment.tempSensors.getCalibration('water3')),
3178
+ fnToByte(sys.equipment.tempSensors.getCalibration('solar3')),
3179
+ fnToByte(sys.equipment.tempSensors.getCalibration('water4')),
3180
+ fnToByte(sys.equipment.tempSensors.getCalibration('solar4')),
3181
+ 0,
3182
+ 0x10 | (sys.general.options.clockMode === 24 ? 0x40 : 0x00) | (sys.general.options.adjustDST ? 0x80 : 0x00) | (sys.general.options.clockSource === 'internet' ? 0x20 : 0x00),
3183
+ 89, 27, 110, 3, 0, 0,
3184
+ bhs.body1.heatSetpoint, bhs.body1.coolSetpoint, bhs.body2.heatSetpoint, bhs.body2.coolSetpoint, bhs.body1.heatMode, bhs.body2.heatMode, 0, 0, 15,
3185
+ sys.general.options.pumpDelay ? 1 : 0, sys.general.options.cooldownDelay ? 1 : 0, 0, 100, 0, 0, 0, 0, sys.general.options.manualPriority ? 1 : 0, sys.general.options.manualHeat ? 1 : 0, 0
3186
+ ];
3187
+ return new Promise<boolean>((resolve, reject) => {
3188
+ let out = Outbound.create({
3189
+ action: 168,
3190
+ payload: payload,
3191
+ retries: 2,
3192
+ response: IntelliCenterBoard.getAckResponse(168),
3193
+ onComplete: (err, msg) => {
3194
+ bhs.processing = false;
3195
+ if (err) reject(err);
3196
+ else {
3197
+ let body1 = sys.bodies.getItemById(1);
3198
+ let sbody1 = state.temps.bodies.getItemById(1);
3199
+ body1.heatMode = sbody1.heatMode = bhs.body1.heatMode;
3200
+ body1.heatSetpoint = sbody1.heatSetpoint = bhs.body1.heatSetpoint;
3201
+ body1.coolSetpoint = sbody1.coolSetpoint = bhs.body1.coolSetpoint;
3202
+ if (sys.equipment.dual || sys.equipment.shared) {
3203
+ let body2 = sys.bodies.getItemById(2);
3204
+ let sbody2 = state.temps.bodies.getItemById(2);
3205
+ body2.heatMode = sbody2.heatMode = bhs.body2.heatMode;
3206
+ body2.heatSetpoint = sbody2.heatSetpoint = bhs.body2.heatSetpoint;
3207
+ body2.coolSetpoint = sbody2.coolSetpoint = bhs.body2.coolSetpoint;
3208
+ }
3209
+ state.emitEquipmentChanges();
3210
+ resolve(true);
3211
+ }
3212
+ }
3213
+ });
3214
+ conn.queueSendMessage(out);
3215
+ });
3216
+ }
3217
+ else {
3218
+ // Try every second to re-try if we have a bunch at once.
3219
+ if (bhs.bytes.length > 0) {
3220
+ setTimeout(async () => {
3221
+ try {
3222
+ await this.queueBodyHeatSettings();
3223
+ } catch (err) { logger.error(`Error sending queued body setpoint message: ${err.message}`); }
3224
+ }, 3000);
3225
+ }
3226
+ else bhs.processing = false;
3227
+ return true;
3228
+ }
3229
+ }
2946
3230
  public async setBodyAsync(obj: any): Promise<Body> {
2947
3231
  let byte = 0;
2948
3232
  let id = parseInt(obj.id, 10);
@@ -3013,40 +3297,53 @@ class IntelliCenterBodyCommands extends BodyCommands {
3013
3297
  });
3014
3298
  }
3015
3299
  }
3300
+ if (typeof obj.showInDashBoard !== 'undefined') {
3301
+ let sbody = state.temps.bodies.getItemById(id, false);
3302
+ body.showInDashboard = sbody.showInDashboard = utils.makeBool(obj.showInDashboard);
3303
+ }
3016
3304
  return Promise.resolve(body);
3017
3305
  }
3018
3306
  catch (err) { return Promise.reject(err); }
3019
3307
  }
3020
3308
  public async setHeatModeAsync(body: Body, mode: number): Promise<BodyTempState> {
3309
+ let modes = sys.board.bodies.getHeatModes(body.id);
3310
+ if (typeof modes.find(elem => elem.val === mode) === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot set heat mode to ${mode} since this is not a valid mode for the ${body.name}`, 'Body', mode));
3311
+ await this.queueBodyHeatSettings(body.id, body.id === 2 ? 23 : 22, { heatMode: mode });
3312
+ return state.temps.bodies.getItemById(body.id);
3313
+ /*
3314
+
3315
+ let byte2 = 22;
3316
+ let body1 = sys.bodies.getItemById(1);
3317
+ let body2 = sys.bodies.getItemById(2);
3318
+
3319
+ let heat1 = body1.heatSetpoint || 78;
3320
+ let cool1 = body1.coolSetpoint || 100;
3321
+ let heat2 = body2.heatSetpoint || 78;
3322
+ let cool2 = body2.coolSetpoint || 103;
3323
+
3324
+ let mode1 = body1.heatMode || 1;
3325
+ let mode2 = body2.heatMode || 1;
3326
+ let bitopts = 0;
3327
+ if (sys.general.options.clockSource) bitopts += 32;
3328
+ if (sys.general.options.clockMode === 24) bitopts += 64;
3329
+ if (sys.general.options.adjustDST) bitopts += 128;
3330
+
3331
+ switch (body.id) {
3332
+ case 1:
3333
+ byte2 = 22;
3334
+ mode1 = mode;
3335
+ break;
3336
+ case 2:
3337
+ byte2 = 23;
3338
+ mode2 = mode;
3339
+ break;
3340
+ }
3021
3341
  return new Promise<BodyTempState>((resolve, reject) => {
3022
- const self = this;
3023
- let byte2 = 18;
3024
- let mode1 = sys.bodies.getItemById(1).setPoint || 100;
3025
- let mode2 = sys.bodies.getItemById(2).setPoint || 100;
3026
- let mode3 = sys.bodies.getItemById(3).setPoint || 100;
3027
- let mode4 = sys.bodies.getItemById(4).setPoint || 100;
3028
- switch (body.id) {
3029
- case 1:
3030
- byte2 = 22;
3031
- mode1 = mode;
3032
- break;
3033
- case 2:
3034
- byte2 = 23;
3035
- mode2 = mode;
3036
- break;
3037
- case 3:
3038
- byte2 = 24;
3039
- mode3 = mode;
3040
- break;
3041
- case 4:
3042
- byte2 = 25;
3043
- mode4 = mode;
3044
- break;
3045
- }
3046
3342
  let out = Outbound.create({
3047
3343
  action: 168,
3048
- payload: [0, 0, byte2, 1, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 176, 89, 27, 110, 3, 0, 0, 100, 100, 100, 100, mode1, mode2, mode3, mode4, 15, 0
3049
- , 0, 0, 0, 100, 0, 0, 0, 0, 0, 0],
3344
+ payload: [0, 0, byte2, 1, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, bitopts, 89, 27, 110, 3, 0, 0,
3345
+ heat1, cool1, heat2, cool2, mode1, mode2, 0, 0, 15,
3346
+ sys.general.options.pumpDelay ? 1 : 0, sys.general.options.cooldownDelay ? 1 : 0, 0, 100, 0, 0, 0, 0, sys.general.options.manualPriority ? 1 : 0, sys.general.options.manualHeat ? 1 : 0, 0],
3050
3347
  retries: 5,
3051
3348
  response: IntelliCenterBoard.getAckResponse(168),
3052
3349
  onComplete: (err, msg) => {
@@ -3062,36 +3359,36 @@ class IntelliCenterBodyCommands extends BodyCommands {
3062
3359
  })
3063
3360
  conn.queueSendMessage(out);
3064
3361
  });
3362
+ */
3065
3363
  }
3066
3364
  public async setHeatSetpointAsync(body: Body, setPoint: number): Promise<BodyTempState> {
3365
+ if (typeof setPoint === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot set heat setpoint to undefined for the ${body.name}`, 'Body', setPoint));
3366
+ else if (setPoint < 0 || setPoint > 110) return Promise.reject(new InvalidEquipmentDataError(`Cannot set heat setpoint to ${setPoint} for the ${body.name}`, 'Body', setPoint));
3367
+ await this.queueBodyHeatSettings(body.id, body.id === 2 ? 20 : 18, { heatSetpoint: setPoint });
3368
+ return state.temps.bodies.getItemById(body.id);
3369
+ /*
3067
3370
  let byte2 = 18;
3068
3371
  let body1 = sys.bodies.getItemById(1);
3069
3372
  let body2 = sys.bodies.getItemById(2);
3070
- let body3 = sys.bodies.getItemById(3);
3071
- let body4 = sys.bodies.getItemById(4);
3072
3373
 
3073
- let temp1 = sys.bodies.getItemById(1).setPoint || 100;
3074
- let temp2 = sys.bodies.getItemById(2).setPoint || 100;
3075
- let temp3 = sys.bodies.getItemById(3).setPoint || 100;
3076
- let temp4 = sys.bodies.getItemById(4).setPoint || 100;
3374
+ let heat1 = body1.heatSetpoint || 78;
3375
+ let cool1 = body1.coolSetpoint || 100;
3376
+ let heat2 = body2.heatSetpoint || 78;
3377
+ let cool2 = body2.coolSetpoint || 103;
3077
3378
  switch (body.id) {
3078
3379
  case 1:
3079
3380
  byte2 = 18;
3080
- temp1 = setPoint;
3381
+ heat1 = setPoint;
3081
3382
  break;
3082
3383
  case 2:
3083
3384
  byte2 = 20;
3084
- temp2 = setPoint;
3085
- break;
3086
- case 3:
3087
- byte2 = 19;
3088
- temp3 = setPoint;
3089
- break;
3090
- case 4:
3091
- byte2 = 21;
3092
- temp4 = setPoint;
3385
+ heat2 = setPoint;
3093
3386
  break;
3094
3387
  }
3388
+ let bitopts = 0;
3389
+ if (sys.general.options.clockSource) bitopts += 32;
3390
+ if (sys.general.options.clockMode === 24) bitopts += 64;
3391
+ if (sys.general.options.adjustDST) bitopts += 128;
3095
3392
  // 6 15 17 18 21 22 24 25
3096
3393
  //[255, 0, 255][165, 63, 15, 16, 168, 41][0, 0, 18, 1, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 176, 89, 27, 110, 3, 0, 0, 89, 100, 98, 100, 0, 0, 0, 0, 15, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0][5, 243]
3097
3394
  //[255, 0, 255][165, 63, 15, 16, 168, 41][0, 0, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 235, 27, 167, 1, 0, 0, 89, 81, 98, 103, 5, 0, 0, 0, 15, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0][6, 48]
@@ -3099,8 +3396,8 @@ class IntelliCenterBodyCommands extends BodyCommands {
3099
3396
  action: 168,
3100
3397
  response: IntelliCenterBoard.getAckResponse(168),
3101
3398
  retries: 5,
3102
- payload: [0, 0, byte2, 1, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 176, 89, 27, 110, 3, 0, 0,
3103
- temp1, temp3, temp2, temp4, body1.heatMode || 0, body2.heatMode || 0, body3.heatMode || 0, body4.heatMode || 0, 15,
3399
+ payload: [0, 0, byte2, 1, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, bitopts, 89, 27, 110, 3, 0, 0,
3400
+ heat1, cool1, heat2, cool2, body1.heatMode || 1, body2.heatMode || 1, 0, 0, 15,
3104
3401
  sys.general.options.pumpDelay ? 1 : 0, sys.general.options.cooldownDelay ? 1 : 0, 0, 100, 0, 0, 0, 0, sys.general.options.manualPriority ? 1 : 0, sys.general.options.manualHeat ? 1 : 0, 0]
3105
3402
  });
3106
3403
  return new Promise<BodyTempState>((resolve, reject) => {
@@ -3114,25 +3411,22 @@ class IntelliCenterBodyCommands extends BodyCommands {
3114
3411
  };
3115
3412
  conn.queueSendMessage(out);
3116
3413
  });
3414
+ */
3117
3415
  }
3118
3416
  public async setCoolSetpointAsync(body: Body, setPoint: number): Promise<BodyTempState> {
3119
- //[165, 1, 15, 16, 168, 41][0, 0, 19, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 110, 30, 188, 3, 0, 0, 76, 99, 78, 100, 5, 5, 0, 0, 15, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0][5, 33]
3417
+ if (typeof setPoint === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot set cooling setpoint to undefined for the ${body.name}`, 'Body', setPoint));
3418
+ else if (setPoint < 0 || setPoint > 110) return Promise.reject(new InvalidEquipmentDataError(`Cannot set cooling setpoint to ${setPoint} for the ${body.name}`, 'Body', setPoint));
3419
+ await this.queueBodyHeatSettings(body.id, body.id === 2 ? 21 : 19, { coolSetpoint: setPoint });
3420
+ return state.temps.bodies.getItemById(body.id);
3421
+ /*
3120
3422
  let byte2 = 19;
3121
3423
  let body1 = sys.bodies.getItemById(1);
3122
3424
  let body2 = sys.bodies.getItemById(2);
3123
- let body3 = sys.bodies.getItemById(3);
3124
- let body4 = sys.bodies.getItemById(3);
3125
3425
 
3126
- let temp1 = sys.bodies.getItemById(1).setPoint || 100;
3127
- let cool1 = sys.bodies.getItemById(1).coolSetpoint || 100;
3128
- let temp2 = sys.bodies.getItemById(2).setPoint || 100;
3129
- let cool2 = sys.bodies.getItemById(2).coolSetpoint || 100;
3130
-
3131
- //Them
3132
- //[165, 63, 15, 16, 168, 41][0, 0, 19, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 59, 30, 5, 5, 0, 0, 90, 102, 98, 81, 3, 1, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0][4, 129]
3133
- //Us
3134
- //[165, 63, 15, 33, 168, 40][0, 0, 19, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 89, 27, 5, 5, 0, 0, 90, 103, 98, 81, 3, 1, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0][5, 249]
3135
- //[165, 63, 15, 33, 168, 40][0, 0, 19, 1, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 176, 89, 27, 110, 3, 0, 0, 90, 103, 98, 81, 3, 1, 0, 0, 15, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0][5, 249]
3426
+ let heat1 = body1.heatSetpoint || 78;
3427
+ let cool1 = body1.coolSetpoint || 100;
3428
+ let heat2 = body2.heatSetpoint || 78;
3429
+ let cool2 = body2.coolSetpoint || 103;
3136
3430
  switch (body.id) {
3137
3431
  case 1:
3138
3432
  byte2 = 19;
@@ -3143,6 +3437,10 @@ class IntelliCenterBodyCommands extends BodyCommands {
3143
3437
  cool2 = setPoint;
3144
3438
  break;
3145
3439
  }
3440
+ let bitopts = 0;
3441
+ if (sys.general.options.clockSource) bitopts += 32;
3442
+ if (sys.general.options.clockMode === 24) bitopts += 64;
3443
+ if (sys.general.options.adjustDST) bitopts += 128;
3146
3444
  // 6 15 17 18 21 22 24 25
3147
3445
  //[255, 0, 255][165, 63, 15, 16, 168, 41][0, 0, 18, 1, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 176, 89, 27, 110, 3, 0, 0, 89, 100, 98, 100, 0, 0, 0, 0, 15, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0][5, 243]
3148
3446
  //[255, 0, 255][165, 63, 15, 16, 168, 41][0, 0, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 235, 27, 167, 1, 0, 0, 89, 81, 98, 103, 5, 0, 0, 0, 15, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0][6, 48]
@@ -3150,8 +3448,8 @@ class IntelliCenterBodyCommands extends BodyCommands {
3150
3448
  action: 168,
3151
3449
  response: IntelliCenterBoard.getAckResponse(168),
3152
3450
  retries: 5,
3153
- payload: [0, 0, byte2, 1, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 176, 89, 27, 110, 3, 0, 0,
3154
- temp1, cool1, temp2, cool2, body1.heatMode || 0, body2.heatMode || 0, body3.heatMode || 0, body4.heatMode || 0, 15,
3451
+ payload: [0, 0, byte2, 1, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, bitopts, 89, 27, 110, 3, 0, 0,
3452
+ heat1, cool1, heat2, cool2, body1.heatMode || 1, body2.heatMode || 1, 0, 0, 15,
3155
3453
  sys.general.options.pumpDelay ? 1 : 0, sys.general.options.cooldownDelay ? 1 : 0, 0, 100, 0, 0, 0, 0, sys.general.options.manualPriority ? 1 : 0, sys.general.options.manualHeat ? 1 : 0, 0]
3156
3454
  });
3157
3455
  return new Promise<BodyTempState>((resolve, reject) => {
@@ -3165,6 +3463,7 @@ class IntelliCenterBodyCommands extends BodyCommands {
3165
3463
  };
3166
3464
  conn.queueSendMessage(out);
3167
3465
  });
3466
+ */
3168
3467
  }
3169
3468
  }
3170
3469
  class IntelliCenterScheduleCommands extends ScheduleCommands {
@@ -3299,7 +3598,7 @@ class IntelliCenterScheduleCommands extends ScheduleCommands {
3299
3598
  , startDate.getMonth() + 1
3300
3599
  , startDate.getDay() || 0
3301
3600
  , startDate.getFullYear() - 2000
3302
- , 32
3601
+ , 0 // This changed to 0 to mean no change in 1.047
3303
3602
  , 78
3304
3603
  , 100
3305
3604
  ],
@@ -3349,9 +3648,8 @@ class IntelliCenterHeaterCommands extends HeaterCommands {
3349
3648
  if (isNaN(id)) return reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
3350
3649
  let heater: Heater;
3351
3650
  if (id <= 0) {
3352
- // We are adding a heater. In this case all heaters are virtual.
3353
- let vheaters = sys.heaters.filter(h => h.master === 1);
3354
- id = vheaters.length + 1;
3651
+ // We are adding a heater. In this case we need to find the first id slot that is empty.
3652
+ id = sys.heaters.getNextEquipmentId(new EquipmentIdRange(1, 16));
3355
3653
  }
3356
3654
  heater = sys.heaters.getItemById(id, false);
3357
3655
  let type = 0;
@@ -3551,7 +3849,7 @@ class IntelliCenterHeaterCommands extends HeaterCommands {
3551
3849
 
3552
3850
  sys.board.valueMaps.heatModes = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]);
3553
3851
  if (gasHeaterInstalled) sys.board.valueMaps.heatModes.merge([[2, { name: 'heater', desc: 'Heater' }]]);
3554
- if (mastertempInstalled) sys.board.valueMaps.heatModes.merge([11, { name: 'mtheater', desc: 'MasterTemp' }]);
3852
+ if (mastertempInstalled) sys.board.valueMaps.heatModes.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]);
3555
3853
  if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar Only' }], [4, { name: 'solarpref', desc: 'Solar Preferred' }]]);
3556
3854
  else if (solarInstalled) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar' }]]);
3557
3855
  if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only' }], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref' }]]);
@@ -3661,24 +3959,24 @@ export class IntelliCenterChemControllerCommands extends ChemControllerCommands
3661
3959
  }
3662
3960
  if (isNaN(pHSetpoint) || pHSetpoint > type.ph.max || pHSetpoint < type.ph.min) Promise.reject(new InvalidEquipmentDataError(`Invalid pH setpoint`, 'ph.setpoint', pHSetpoint));
3663
3961
  if (isNaN(orpSetpoint) || orpSetpoint > type.orp.max || orpSetpoint < type.orp.min) Promise.reject(new InvalidEquipmentDataError(`Invalid orp setpoint`, 'orp.setpoint', orpSetpoint));
3664
- let phTolerance = typeof data.ph.tolerance !== 'undefined' ? data.ph.tolerance : chem.ph.tolerance;
3665
- let orpTolerance = typeof data.orp.tolerance !== 'undefined' ? data.orp.tolerance : chem.orp.tolerance;
3666
- if (typeof data.ph.tolerance !== 'undefined') {
3962
+ let phTolerance = typeof data.ph !== 'undefined' && typeof data.ph.tolerance !== 'undefined' ? data.ph.tolerance : chem.ph.tolerance;
3963
+ let orpTolerance = typeof data.orp !== 'undefined' && typeof data.orp.tolerance !== 'undefined' ? data.orp.tolerance : chem.orp.tolerance;
3964
+ if (typeof data.ph !== 'undefined' && typeof data.ph.tolerance !== 'undefined') {
3667
3965
  if (typeof data.ph.tolerance.enabled !== 'undefined') phTolerance.enabled = utils.makeBool(data.ph.tolerance.enabled);
3668
3966
  if (typeof data.ph.tolerance.low !== 'undefined') phTolerance.low = parseFloat(data.ph.tolerance.low);
3669
3967
  if (typeof data.ph.tolerance.high !== 'undefined') phTolerance.high = parseFloat(data.ph.tolerance.high);
3670
3968
  if (isNaN(phTolerance.low)) phTolerance.low = type.ph.min;
3671
3969
  if (isNaN(phTolerance.high)) phTolerance.high = type.ph.max;
3672
3970
  }
3673
- if (typeof data.orp.tolerance !== 'undefined') {
3971
+ if (typeof data.orp !== 'undefined' && typeof data.orp.tolerance !== 'undefined') {
3674
3972
  if (typeof data.orp.tolerance.enabled !== 'undefined') orpTolerance.enabled = utils.makeBool(data.orp.tolerance.enabled);
3675
3973
  if (typeof data.orp.tolerance.low !== 'undefined') orpTolerance.low = parseFloat(data.orp.tolerance.low);
3676
3974
  if (typeof data.orp.tolerance.high !== 'undefined') orpTolerance.high = parseFloat(data.orp.tolerance.high);
3677
3975
  if (isNaN(orpTolerance.low)) orpTolerance.low = type.orp.min;
3678
3976
  if (isNaN(orpTolerance.high)) orpTolerance.high = type.orp.max;
3679
3977
  }
3680
- let phEnabled = typeof data.ph.enabled !== 'undefined' ? utils.makeBool(data.ph.enabled) : chem.ph.enabled;
3681
- let orpEnabled = typeof data.orp.enabled !== 'undefined' ? utils.makeBool(data.orp.enabled) : chem.orp.enabled;
3978
+ let phEnabled = typeof data.ph !== 'undefined' && typeof data.ph.enabled !== 'undefined' ? utils.makeBool(data.ph.enabled) : chem.ph.enabled;
3979
+ let orpEnabled = typeof data.orp !== 'undefined' && typeof data.orp.enabled !== 'undefined' ? utils.makeBool(data.orp.enabled) : chem.orp.enabled;
3682
3980
  let siCalcType = typeof data.siCalcType !== 'undefined' ? sys.board.valueMaps.siCalcTypes.encode(data.siCalcType, 0) : chem.siCalcType;
3683
3981
 
3684
3982
  let saltLevel = (state.chlorinators.length > 0) ? state.chlorinators.getItemById(1).saltLevel || 1000 : 1000
@@ -3686,14 +3984,18 @@ export class IntelliCenterChemControllerCommands extends ChemControllerCommands
3686
3984
  chem.orp.tank.capacity = 6;
3687
3985
  let acidTankLevel = typeof data.ph !== 'undefined' && typeof data.ph.tank !== 'undefined' && typeof data.ph.tank.level !== 'undefined' ? parseInt(data.ph.tank.level, 10) : schem.ph.tank.level;
3688
3986
  let orpTankLevel = typeof data.orp !== 'undefined' && typeof data.orp.tank !== 'undefined' && typeof data.orp.tank.level !== 'undefined' ? parseInt(data.orp.tank.level, 10) : schem.orp.tank.level;
3987
+ //Them
3988
+ //[255, 0, 255][165, 63, 15, 16, 168, 20][8, 0, 0, 32, 1, 144, 1, 248, 2, 144, 1, 1, 1, 29, 0, 0, 0, 100, 0, 0][4, 135]
3989
+ //Us
3990
+ //[255, 0, 255][165, 0, 15, 33, 168, 20][8, 0, 0, 32, 1, 144, 1, 248, 2, 144, 1, 1, 1, 33, 0, 0, 0, 100, 0, 0][4, 93]
3689
3991
  return new Promise<ChemController>((resolve, reject) => {
3690
3992
  let out = Outbound.create({
3691
- protocol: Protocol.IntelliChem,
3993
+ protocol: Protocol.Broadcast,
3692
3994
  action: 168,
3693
3995
  payload: [],
3694
3996
  retries: 3, // We are going to try 4 times.
3695
3997
  response: IntelliCenterBoard.getAckResponse(168),
3696
- onAbort: () => { },
3998
+ //onAbort: () => { },
3697
3999
  onComplete: (err) => {
3698
4000
  if (err) reject(err);
3699
4001
  else {
@@ -3728,172 +4030,29 @@ export class IntelliCenterChemControllerCommands extends ChemControllerCommands
3728
4030
  }
3729
4031
  }
3730
4032
  });
4033
+
3731
4034
  //[8, 0, chem.id - 1, body.val, 1, chem.address, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0]
3732
- out.insertPayloadBytes(0, 0, 18);
4035
+ out.insertPayloadBytes(0, 0, 20);
3733
4036
  out.setPayloadByte(0, 8);
3734
4037
  out.setPayloadByte(1, 0);
3735
4038
  out.setPayloadByte(2, chem.id - 1);
3736
4039
  out.setPayloadByte(3, body.val);
3737
- out.setPayloadByte(4, 1);
4040
+ out.setPayloadByte(4, acidTankLevel + 1);
3738
4041
  out.setPayloadByte(5, address);
3739
4042
  out.setPayloadByte(6, 1);
3740
4043
  out.setPayloadInt(7, Math.round(pHSetpoint * 100), 700);
3741
4044
  out.setPayloadInt(9, orpSetpoint, 400);
3742
4045
  out.setPayloadByte(11, 1);
3743
4046
  out.setPayloadByte(12, 1);
4047
+ //out.setPayloadByte(11, acidTankLevel + 1, 1);
4048
+ //out.setPayloadByte(12, orpTankLevel + 1, 1);
4049
+
3744
4050
  out.setPayloadInt(13, calciumHardness, 25);
3745
4051
  out.setPayloadInt(15, cyanuricAcid, 0);
3746
4052
  out.setPayloadInt(17, alkalinity, 25);
3747
4053
  conn.queueSendMessage(out);
3748
4054
  });
3749
4055
  }
3750
-
3751
- //protected async setIntelliChemStateAsync(data: any): Promise<ChemControllerState> {
3752
- // try {
3753
- // // This is a protected method so the id will always be valid if we made it here. Do
3754
- // // one more check since we cannot lock a thread.
3755
- // let chem = sys.chemControllers.find(elem => elem.id === data.id);
3756
- // if (typeof chem === 'undefined') return Promise.reject(`A valid IntelliChem controller could not be found at id ${data.id}`);
3757
- // // If we are virtual send it back to the SystemBoard for processing.
3758
- // if (chem.master !== 0) return super.setIntelliChemStateAsync(data);
3759
- // let address = typeof data.address !== 'undefined' ? parseInt(data.address, 10) : chem.address;
3760
- // if (typeof address === 'undefined' || isNaN(address) || (address < 144 || address > 158)) return Promise.reject(new InvalidEquipmentDataError(`Invalid IntelliChem address`, 'chemController', address));
3761
- // let pHSetpoint = typeof data.ph !== 'undefined' && typeof data.ph.setpoint !== 'undefined' ? parseFloat(data.ph.setpoint) : chem.ph.setpoint;
3762
- // let orpSetpoint = typeof data.orp !== 'undefined' && typeof data.orp.setpoint !== 'undefined' ? parseInt(data.orp.setpoint, 10) : chem.orp.setpoint;
3763
- // let calciumHardness = typeof data.calciumHardness !== 'undefined' ? parseInt(data.calciumHardness, 10) : chem.calciumHardness;
3764
- // let cyanuricAcid = typeof data.cyanuricAcid !== 'undefined' ? parseInt(data.cyanuricAcid, 10) : chem.cyanuricAcid;
3765
- // let alkalinity = typeof data.alkalinity !== 'undefined' ? parseInt(data.alkalinity, 10) : chem.alkalinity;
3766
- // let body = sys.board.bodies.mapBodyAssociation(typeof data.body === 'undefined' ? chem.body : data.body);
3767
- // if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid body assignment`, 'chemController', data.body || chem.body));
3768
- // // Do a final validation pass so we dont send this off in a mess.
3769
- // if (isNaN(pHSetpoint)) return Promise.reject(new InvalidEquipmentDataError(`Invalid pH Setpoint`, 'chemController', pHSetpoint));
3770
- // if (isNaN(orpSetpoint)) return Promise.reject(new InvalidEquipmentDataError(`Invalid orp Setpoint`, 'chemController', orpSetpoint));
3771
- // if (isNaN(calciumHardness)) return Promise.reject(new InvalidEquipmentDataError(`Invalid calcium hardness`, 'chemController', calciumHardness));
3772
- // if (isNaN(cyanuricAcid)) return Promise.reject(new InvalidEquipmentDataError(`Invalid cyanuric acid`, 'chemController', cyanuricAcid));
3773
- // if (isNaN(alkalinity)) return Promise.reject(new InvalidEquipmentDataError(`Invalid alkalinity`, 'chemController', alkalinity));
3774
- // return new Promise<ChemControllerState>(async (resolve, reject) => {
3775
- // let out = Outbound.create({
3776
- // action: 168,
3777
- // response: IntelliCenterBoard.getAckResponse(168),
3778
- // retries: 3,
3779
- // payload: [8, 0, chem.id - 1, body.val, 1, chem.address, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0],
3780
- // onComplete: (err) => {
3781
- // if (err) { reject(err); }
3782
- // else {
3783
- // let cstate = state.chemControllers.getItemById(chem.id, true);
3784
- // chem.isActive = true;
3785
- // chem.isVirtual = false;
3786
- // //chem.address = address;
3787
- // chem.body = body;
3788
- // chem.calciumHardness = calciumHardness;
3789
- // chem.orp.setpoint = cstate.orp.setpoint = orpSetpoint;
3790
- // chem.ph.setpoint = cstate.ph.setpoint = pHSetpoint;
3791
- // chem.cyanuricAcid = cyanuricAcid;
3792
- // chem.alkalinity = alkalinity;
3793
- // chem.type = 2;
3794
- // chem.name = typeof chem.name === 'undefined' ? `IntelliChem ${chem.id}` : chem.name;
3795
- // chem.ph.tank.capacity = chem.orp.tank.capacity = 6;
3796
- // chem.ph.tank.units = chem.orp.tank.units = '';
3797
- // cstate.body = chem.body;
3798
- // cstate.address = chem.address;
3799
- // cstate.name = chem.name;
3800
- // cstate.type = chem.type;
3801
- // cstate.isActive = chem.isActive;
3802
- // resolve(cstate);
3803
- // }
3804
- // }
3805
- // });
3806
- // out.setPayloadInt(7, Math.round(pHSetpoint * 100), 700);
3807
- // out.setPayloadInt(9, orpSetpoint, 400);
3808
- // out.setPayloadInt(13, calciumHardness, 25);
3809
- // out.setPayloadInt(15, cyanuricAcid, 0);
3810
- // out.setPayloadInt(17, alkalinity, 25);
3811
- // conn.queueSendMessage(out);
3812
- // });
3813
- // }
3814
- // catch (err) { return Promise.reject(err); }
3815
- //}
3816
- //protected async setIntelliChemAsync(data: any): Promise<ChemController> {
3817
- // try {
3818
- // // This is a protected method so the id will always be valid if we made it here. Do
3819
- // // one more check since we cannot lock a thread.
3820
- // let chem = sys.chemControllers.find(elem => elem.id === data.id);
3821
- // let ichemType = sys.board.valueMaps.chemControllerTypes.encode('intellichem');
3822
- // if (typeof chem === 'undefined') {
3823
- // // We are adding an IntelliChem. Check to see how many intellichems we have.
3824
- // let arr = sys.chemControllers.toArray();
3825
- // let count = 0;
3826
- // for (let i = 0; i < arr.length; i++) {
3827
- // let cc: ChemController = arr[i];
3828
- // if (cc.type === ichemType) count++;
3829
- // }
3830
- // if (count >= sys.equipment.maxChemControllers) return Promise.reject(new InvalidEquipmentDataError(`The max number of IntelliChem controllers has been reached: ${sys.equipment.maxChemControllers}`, 'chemController', sys.equipment.maxChemControllers));
3831
- // let id = (sys.chemControllers.getMaxId() || 0) + 1;
3832
- // chem = sys.chemControllers.getItemById(id);
3833
- // }
3834
- // let address = typeof data.address !== 'undefined' ? parseInt(data.address, 10) : chem.address;
3835
- // if (typeof address === 'undefined' || isNaN(address) || (address < 144 || address > 158)) return Promise.reject(new InvalidEquipmentDataError(`Invalid IntelliChem address`, 'chemController', address));
3836
- // if (typeof sys.chemControllers.find(elem => elem.id !== data.id && elem.type === ichemType && elem.address === address) !== 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid IntelliChem address: Address is used on another IntelliChem`, 'chemController', address));
3837
- // let pHSetpoint = typeof data.ph.setpoint !== 'undefined' ? parseFloat(data.ph.setpoint) : chem.ph.setpoint;
3838
- // let orpSetpoint = typeof data.orp.setpoint !== 'undefined' ? parseInt(data.orp.setpoint, 10) : chem.orp.setpoint;
3839
- // let calciumHardness = typeof data.calciumHardness !== 'undefined' ? parseInt(data.calciumHardness, 10) : chem.calciumHardness;
3840
- // let cyanuricAcid = typeof data.cyanuricAcid !== 'undefined' ? parseInt(data.cyanuricAcid, 10) : chem.cyanuricAcid;
3841
- // let alkalinity = typeof data.alkalinity !== 'undefined' ? parseInt(data.alkalinity, 10) : chem.alkalinity;
3842
- // let body = sys.board.bodies.mapBodyAssociation(typeof data.body === 'undefined' ? chem.body : data.body);
3843
- // let name = typeof data.name === 'undefined' ? chem.name || `IntelliChem ${chem.id}` : data.name;
3844
- // if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid body assignment`, 'chemController', data.body || chem.body));
3845
- // // Do a final validation pass so we dont send this off in a mess.
3846
- // if (isNaN(address)) return Promise.reject(new InvalidEquipmentDataError(`Invalid address ${data.address}`, 'chemController', data.address));
3847
- // if (isNaN(pHSetpoint)) return Promise.reject(new InvalidEquipmentDataError(`Invalid pH Setpoint`, 'chemController', pHSetpoint));
3848
- // if (isNaN(orpSetpoint)) return Promise.reject(new InvalidEquipmentDataError(`Invalid orp Setpoint`, 'chemController', orpSetpoint));
3849
- // if (isNaN(calciumHardness)) return Promise.reject(new InvalidEquipmentDataError(`Invalid calcium hardness`, 'chemController', calciumHardness));
3850
- // if (isNaN(cyanuricAcid)) return Promise.reject(new InvalidEquipmentDataError(`Invalid cyanuric acid`, 'chemController', cyanuricAcid));
3851
- // if (isNaN(alkalinity)) return Promise.reject(new InvalidEquipmentDataError(`Invalid alkalinity`, 'chemController', alkalinity));
3852
-
3853
- // return new Promise<ChemController>(async (resolve, reject) => {
3854
- // let out = Outbound.create({
3855
- // action: 168,
3856
- // response: IntelliCenterBoard.getAckResponse(168),
3857
- // retries: 3,
3858
- // payload: [8, 0, chem.id - 1, body.val, 1, address, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0],
3859
- // onComplete: (err) => {
3860
- // if (err) { reject(err); }
3861
- // else {
3862
- // chem = sys.chemControllers.getItemById(chem.id, true);
3863
- // let cstate = state.chemControllers.getItemById(chem.id, true);
3864
- // chem.master = sys.board.equipmentMaster;
3865
- // chem.isActive = true;
3866
- // chem.isVirtual = false;
3867
- // chem.address = address;
3868
- // chem.body = body;
3869
- // chem.calciumHardness = calciumHardness;
3870
- // chem.orp.setpoint = cstate.orp.setpoint = orpSetpoint;
3871
- // chem.ph.setpoint = cstate.ph.setpoint = pHSetpoint;
3872
- // chem.cyanuricAcid = cyanuricAcid;
3873
- // chem.alkalinity = alkalinity;
3874
- // chem.type = 2;
3875
- // chem.name = name;
3876
- // chem.ph.tank.capacity = chem.orp.tank.capacity = 6;
3877
- // chem.ph.tank.units = chem.orp.tank.units = '';
3878
- // cstate.body = chem.body;
3879
- // cstate.address = chem.address;
3880
- // cstate.name = chem.name;
3881
- // cstate.type = chem.type;
3882
- // cstate.isActive = chem.isActive;
3883
- // resolve(chem);
3884
- // }
3885
- // }
3886
- // });
3887
- // out.setPayloadInt(7, Math.round(pHSetpoint * 100), 700);
3888
- // out.setPayloadInt(9, Math.floor(orpSetpoint), 400);
3889
- // out.setPayloadInt(13, Math.floor(calciumHardness), 25);
3890
- // out.setPayloadInt(15, Math.floor(cyanuricAcid), 0);
3891
- // out.setPayloadInt(17, Math.floor(alkalinity), 25);
3892
- // conn.queueSendMessage(out);
3893
- // });
3894
- // }
3895
- // catch (err) { return Promise.reject(err); }
3896
- //}
3897
4056
  public async deleteChemControllerAsync(data: any): Promise<ChemController> {
3898
4057
  let id = typeof data.id !== 'undefined' ? parseInt(data.id, 10) : -1;
3899
4058
  if (typeof id === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid Chem Controller Id`, id, 'chemController'));