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
@@ -19,8 +19,8 @@ import { logger } from '../../logger/Logger';
19
19
  import { conn } from '../comms/Comms';
20
20
  import { Message, Outbound, Protocol, Response } from '../comms/messages/Messages';
21
21
  import { utils } from '../Constants';
22
- import { Body, ChemController, ConfigVersion, CustomName, EggTimer, Feature, Heater, ICircuit, LightGroup, LightGroupCircuit, PoolSystem, Pump, Schedule, sys } from '../Equipment';
23
- import { EquipmentTimeoutError, InvalidEquipmentDataError, InvalidEquipmentIdError } from '../Errors';
22
+ import { Body, ChemController, ConfigVersion, CustomName, EggTimer, Feature, Heater, ICircuit, LightGroup, LightGroupCircuit, Options, PoolSystem, Pump, Schedule, sys } from '../Equipment';
23
+ import { EquipmentTimeoutError, InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError } from '../Errors';
24
24
  import { ncp } from "../nixie/Nixie";
25
25
  import { BodyTempState, ChlorinatorState, ICircuitGroupState, ICircuitState, LightGroupState, state } from '../State';
26
26
  import { BodyCommands, byteValueMap, ChemControllerCommands, ChlorinatorCommands, CircuitCommands, ConfigQueue, ConfigRequest, EquipmentIdRange, FeatureCommands, HeaterCommands, PumpCommands, ScheduleCommands, SystemBoard, SystemCommands } from './SystemBoard';
@@ -44,7 +44,9 @@ export class EasyTouchBoard extends SystemBoard {
44
44
  [0, { name: 'off', desc: 'Off' }],
45
45
  [1, { name: 'heater', desc: 'Heater' }],
46
46
  [2, { name: 'cooling', desc: 'Cooling' }],
47
- [3, { name: 'solar', desc: 'Solar' }]
47
+ [3, { name: 'solar', desc: 'Solar' }],
48
+ [4, { name: 'hpheat', desc: 'Heatpump' }],
49
+ [5, { name: 'dual', desc: 'Dual'}]
48
50
  ]);
49
51
  this.valueMaps.customNames = new byteValueMap(
50
52
  sys.customNames.get().map((el, idx) => {
@@ -160,22 +162,25 @@ export class EasyTouchBoard extends SystemBoard {
160
162
  [101, { name: 'feature8', desc: 'Feature 8' }]
161
163
  ]);
162
164
  // We need this because there is a no-pump thing in *Touch.
163
- // RKS: 05-04-21 The no-pump item was removed as this was only required for -webClient. deletePumpAsync should remove the pump from operation.
165
+ // RKS: 05-04-21 The no-pump item was removed as this was only required for -webClient. deletePumpAsync should remove the pump from operation. Do not use 255 as EasyTouch reports
166
+ // 255 or 0 for pumps that are not installed.
164
167
  this.valueMaps.pumpTypes = new byteValueMap([
165
168
  [1, { name: 'vf', desc: 'Intelliflo VF', maxPrimingTime: 6, minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }],
166
169
  [64, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }],
167
170
  [65, { name: 'ds', desc: 'Two-Speed', maxCircuits: 40, hasAddress: false, hasBody: true }],
168
171
  [128, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 10, minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, maxCircuits: 8, hasAddress: true }],
169
172
  [169, { name: 'vssvrs', desc: 'IntelliFlo VS+SVRS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, maxCircuits: 8, hasAddress: true }],
170
- [257, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody: true, equipmentMaster: 1 }],
171
- [256, { name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1 }]
173
+ [257, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody: true, equipmentMaster: 1, maxRelays: 1, relays: [{ id: 1, name: 'Pump On/Off' }] }],
174
+ [256, { 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' }] }],
175
+ [258, { 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' }] }],
176
+ [259, { name: 'hwvs', desc: 'Hayward Eco/TriStar VS', minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true, equipmentMaster: 1 }]
172
177
  ]);
173
178
  this.valueMaps.heaterTypes = new byteValueMap([
174
179
  [0, { name: 'none', desc: 'No Heater', hasAddress: false }],
175
180
  [1, { name: 'gas', desc: 'Gas Heater', hasAddress: false }],
176
- [2, { name: 'solar', desc: 'Solar Heater', hasAddress: false }],
177
- [3, { name: 'heatpump', desc: 'Heat Pump', hasAddress: true }],
178
- [4, { name: 'ultratemp', desc: 'UltraTemp', hasAddress: true, hasCoolSetpoint: true }],
181
+ [2, { name: 'solar', desc: 'Solar Heater', hasAddress: false, hasPreference: true }],
182
+ [3, { name: 'heatpump', desc: 'Heat Pump', hasAddress: true, hasPreference: true }],
183
+ [4, { name: 'ultratemp', desc: 'UltraTemp', hasAddress: true, hasCoolSetpoint: true, hasPreference: true }],
179
184
  [5, { name: 'hybrid', desc: 'Hybrid', hasAddress: true }],
180
185
  [6, { name: 'maxetherm', desc: 'Max-E-Therm', hasAddress: true }],
181
186
  [7, { name: 'mastertemp', desc: 'MasterTemp', hasAddress: true }]
@@ -296,6 +301,14 @@ export class EasyTouchBoard extends SystemBoard {
296
301
  }
297
302
  return { val: b, days: days };
298
303
  };
304
+ this.valueMaps.lightCommands = new byteValueMap([
305
+ [128, { name: 'colorsync', desc: 'Sync', types: ['intellibrite'] }],
306
+ [144, { name: 'colorset', desc: 'Set', types: ['intellibrite'] }],
307
+ [160, { name: 'colorswim', desc: 'Swim', types: ['intellibrite'] }],
308
+ [190, { name: 'colorhold', desc: 'Hold', types: ['intellibrite'], sequence: 13 }],
309
+ [191, { name: 'colorrecall', desc: 'Recall', types: ['intellibrite'], sequence: 14 }],
310
+ [208, { name: 'thumper', desc: 'Thumper', types: ['magicstream'] }]
311
+ ]);
299
312
  this.valueMaps.lightThemes.transform = function (byte) { return extend(true, { val: byte }, this.get(byte) || this.get(255)); };
300
313
  this.valueMaps.circuitNames.transform = function (byte) {
301
314
  if (byte < 200) {
@@ -313,7 +326,7 @@ export class EasyTouchBoard extends SystemBoard {
313
326
  [128, { val: 128, name: 'timeout', desc: 'Timeout' }],
314
327
  [129, { val: 129, name: 'service-timeout', desc: 'Service/Timeout' }],
315
328
  [255, { name: 'error', desc: 'System Error' }]
316
- ]);
329
+ ]);
317
330
  this.valueMaps.expansionBoards = new byteValueMap([
318
331
  [0, { name: 'ET28', part: 'ET2-8', desc: 'EasyTouch2 8', circuits: 8, shared: true }],
319
332
  [1, { name: 'ET28P', part: 'ET2-8P', desc: 'EasyTouch2 8P', circuits: 8, shared: false }],
@@ -329,15 +342,18 @@ export class EasyTouchBoard extends SystemBoard {
329
342
  ]);
330
343
  }
331
344
  public initHeaterDefaults() {
332
- let heater = sys.heaters.getItemById(1, true);
333
- heater.isActive = true;
334
- heater.type = 1;
335
- heater.name = "Gas Heater";
336
- let sheater = state.heaters.getItemById(1, true);
337
- sheater.type = heater.type;
338
- sheater.name = heater.name;
339
- //sheater.isVirtual = heater.isVirtual = false;
340
- sys.equipment.shared ? heater.body = 32 : heater.body = 0;
345
+ sys.board.heaters.updateHeaterServices();
346
+ // RKS: 03-03-22 This is not correct. As it turns out there is a case where the only heater installed is not
347
+ // a gas heater. This also does not work for dual body systems.
348
+ //let heater = sys.heaters.getItemById(1, true);
349
+ //heater.isActive = true;
350
+ //heater.type = 1;
351
+ //heater.name = "Gas Heater";
352
+ //let sheater = state.heaters.getItemById(1, true);
353
+ //sheater.type = heater.type;
354
+ //sheater.name = heater.name;
355
+ ////sheater.isVirtual = heater.isVirtual = false;
356
+ //sys.equipment.shared ? heater.body = 32 : heater.body = 0;
341
357
  }
342
358
  public initBodyDefaults() {
343
359
  // Initialize the bodies. We will need these very soon.
@@ -348,10 +364,15 @@ export class EasyTouchBoard extends SystemBoard {
348
364
  cbody.isActive = true;
349
365
  // If the body doesn't represent a spa then we set the type.
350
366
  // RSG - 10-5-21: If a single body IT (i5+3s/i9+3s) the bodies are the same; set to pool
351
- tbody.type = cbody.type = i > 1 && !sys.equipment.shared && sys.equipment.intakeReturnValves ? 1 : 0;
367
+ // RKS: 04-13-22 - This is not really correct. IntelliTouch (S) models are actually shared body systems that do
368
+ // not have intake/return valves but there are two bodies that are named Hi-Temp (spa) and Lo-Temp (pool). This
369
+ // is very confusing in the control panels but I see why it is done this way. If they didn't they would need
370
+ // different controllers for the Indoor and Wireless controllers since the top 2 horizontal buttons are body controls.
371
+ //tbody.type = cbody.type = i > 1 && !sys.equipment.shared && sys.equipment.intakeReturnValves ? 1 : 0;
372
+ tbody.type = cbody.type = i - 1; // This will set the first body to pool/Lo-Temp and the second body to spa/Hi-Temp.
352
373
  if (typeof cbody.name === 'undefined') {
353
374
  let bt = sys.board.valueMaps.bodyTypes.transform(cbody.type);
354
- tbody.name = cbody.name = bt.name;
375
+ tbody.name = cbody.name = bt.desc;
355
376
  }
356
377
  }
357
378
  if (!sys.equipment.shared && !sys.equipment.dual && state.equipment.controllerType !== 'intellitouch') {
@@ -420,6 +441,17 @@ export class EasyTouchBoard extends SystemBoard {
420
441
  let b = sys.bodies.getItemByIndex(i);
421
442
  b.master = 0;
422
443
  }
444
+ state.equipment.maxBodies = sys.equipment.maxBodies;
445
+ state.equipment.maxCircuitGroups = sys.equipment.maxCircuitGroups;
446
+ state.equipment.maxCircuits = sys.equipment.maxCircuits;
447
+ state.equipment.maxFeatures = sys.equipment.maxFeatures;
448
+ state.equipment.maxHeaters = sys.equipment.maxHeaters;
449
+ state.equipment.maxLightGroups = sys.equipment.maxLightGroups;
450
+ state.equipment.maxPumps = sys.equipment.maxPumps;
451
+ state.equipment.maxSchedules = sys.equipment.maxSchedules;
452
+ state.equipment.maxValves = sys.equipment.maxValves;
453
+ state.equipment.shared = sys.equipment.shared;
454
+ state.equipment.dual = sys.equipment.dual;
423
455
  state.emitControllerChange();
424
456
  }
425
457
  public bodies: TouchBodyCommands = new TouchBodyCommands(this);
@@ -1054,8 +1086,69 @@ class TouchSystemCommands extends SystemCommands {
1054
1086
  conn.queueSendMessage(out);
1055
1087
  });
1056
1088
  }
1089
+ public async setOptionsAsync(obj: any): Promise<Options> {
1090
+ // Proxy for setBodyAsync. See below for explanation.
1091
+ await sys.board.bodies.setBodyAsync(obj);
1092
+ if (typeof obj.clockSource !== 'undefined') {
1093
+ sys.general.options.clockSource = obj.clockSource;
1094
+ if (sys.general.options.clockSource === 'server') sys.board.system.setTZ();
1095
+ }
1096
+
1097
+ return sys.general.options;
1098
+ }
1057
1099
  }
1058
1100
  class TouchBodyCommands extends BodyCommands {
1101
+ public async setBodyAsync(obj: any): Promise<Body> {
1102
+ // The 168 is a funky packet in *Touch because it can set:
1103
+ // * Intellichem Installed (byte 3, bit 1)
1104
+ // * Manual spa heat (byte 4, bit 1) which only applies to the spa but is a
1105
+ // general option
1106
+ // * Manual Priority (byte 5, bit 1 - Intellitouch only)
1107
+ // and this function can be called by either setIntelliChem (protected)
1108
+ // or directly from setBodyAsync (/config/body endpoint) or from setGeneralAsync (/config/options)
1109
+ // for Manual Priority.
1110
+ // We also need to return the proper body setting manual heat, but it is irrelevant
1111
+ // for when we are returning to chemController
1112
+ try {
1113
+ return new Promise<Body>((resolve, reject) => {
1114
+ let manualHeat = sys.general.options.manualHeat;
1115
+ let manualPriority = sys.general.options.manualPriority;
1116
+ if (typeof obj.manualHeat !== 'undefined') manualHeat = utils.makeBool(obj.manualHeat);
1117
+ if (typeof obj.manualPriority !== 'undefined') manualPriority = utils.makeBool(obj.manualPriority);
1118
+ let body = sys.bodies.getItemById(obj.id, false);
1119
+ let intellichemInstalled = sys.chemControllers.getItemByAddress(144, false).isActive;
1120
+ let out = Outbound.create({
1121
+ dest: 16,
1122
+ action: 168,
1123
+ retries: 3,
1124
+ response: true,
1125
+ onComplete: (err, msg) => {
1126
+ if (err) reject(err);
1127
+ else {
1128
+ sys.general.options.manualHeat = manualHeat;
1129
+ sys.general.options.manualPriority = manualPriority;
1130
+ let sbody = state.temps.bodies.getItemById(body.id, true);
1131
+ if (body.type === 1){ // spa
1132
+ body.manualHeat = manualHeat;
1133
+ };
1134
+ if (typeof obj.name !== 'undefined') body.name = sbody.name = obj.name;
1135
+ if (typeof obj.capacity !== 'undefined') body.capacity = parseInt(obj.capacity, 10);
1136
+ if (typeof obj.showInDashboard !== 'undefined') body.showInDashboard = sbody.showInDashboard = utils.makeBool(obj.showInDashboard);
1137
+ state.emitEquipmentChanges();
1138
+ resolve(body);
1139
+ }
1140
+ }
1141
+ });
1142
+ out.insertPayloadBytes(0, 0, 9);
1143
+ out.setPayloadByte(3, intellichemInstalled ? 255 : 254);
1144
+ out.setPayloadByte(4, manualHeat ? 1 : 0);
1145
+ out.setPayloadByte(5, manualPriority ? 1 : 0);
1146
+ conn.queueSendMessage(out);
1147
+ });
1148
+
1149
+ }
1150
+ catch (err) { return Promise.reject(err); }
1151
+ }
1059
1152
  public async setHeatModeAsync(body: Body, mode: number): Promise<BodyTempState> {
1060
1153
  return new Promise<BodyTempState>((resolve, reject) => {
1061
1154
  // [16,34,136,4],[POOL HEAT Temp,SPA HEAT Temp,Heat Mode,0,2,56]
@@ -1065,6 +1158,15 @@ class TouchBodyCommands extends BodyCommands {
1065
1158
  // 1 | 97 | Spa setpoint
1066
1159
  // 2 | 7 | Pool/spa heat modes (01 = Heater spa 11 = Solar Only pool)
1067
1160
  // 3 | 0 | Cool set point for ultratemp
1161
+
1162
+
1163
+ // Heat modes
1164
+ // 0 = Off
1165
+ // 1 = Heater
1166
+ // 2 = Solar/Heatpump Pref
1167
+ // 3 = Solar
1168
+ //
1169
+
1068
1170
  const body1 = sys.bodies.getItemById(1);
1069
1171
  const body2 = sys.bodies.getItemById(2);
1070
1172
  const temp1 = body1.setPoint || 100;
@@ -1253,18 +1355,20 @@ class TouchBodyCommands extends BodyCommands {
1253
1355
  }
1254
1356
  }
1255
1357
  export class TouchCircuitCommands extends CircuitCommands {
1256
- public getLightThemes(type?: number): any[] {
1257
- let themes = sys.board.valueMaps.lightThemes.toArray();
1258
- if (typeof type === 'undefined') return themes;
1259
- switch (type) {
1260
- case 8: // Magicstream
1261
- return themes.filter(theme => theme.type === 'magicstream');
1262
- case 16: // Intellibrite
1263
- return themes.filter(theme => theme.type === 'intellibrite');
1264
- default:
1265
- return [];
1266
- }
1267
- }
1358
+ // RKS: 12-01-2021 This has been deprecated we are now driving this through metadata on the valuemaps. This allows
1359
+ // for multiple types of standardized on/off sequences with nixie controllers.
1360
+ //public getLightThemes(type?: number): any[] {
1361
+ // let themes = sys.board.valueMaps.lightThemes.toArray();
1362
+ // if (typeof type === 'undefined') return themes;
1363
+ // switch (type) {
1364
+ // case 8: // Magicstream
1365
+ // return themes.filter(theme => theme.types.includes('magicstream'));
1366
+ // case 16: // Intellibrite
1367
+ // return themes.filter(theme => theme.types.includes('intellibrite'));
1368
+ // default:
1369
+ // return [];
1370
+ // }
1371
+ //}
1268
1372
  public async setCircuitAsync(data: any): Promise<ICircuit> {
1269
1373
  try {
1270
1374
  // example [255,0,255][165,33,16,34,139,5][17,14,209,0,0][2,120]
@@ -1336,7 +1440,7 @@ export class TouchCircuitCommands extends CircuitCommands {
1336
1440
  data.functionId = sys.board.valueMaps.circuitFunctions.getValue('notused');
1337
1441
  return this.setCircuitAsync(data);
1338
1442
  }
1339
- public async setCircuitStateAsync(id: number, val: boolean): Promise<ICircuitState> {
1443
+ public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
1340
1444
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Circuit or Feature id not valid', id, 'Circuit'));
1341
1445
  let c = sys.circuits.getInterfaceById(id);
1342
1446
  if (c.master !== 0) return await super.setCircuitStateAsync(id, val);
@@ -1523,15 +1627,74 @@ export class TouchCircuitCommands extends CircuitCommands {
1523
1627
  });
1524
1628
 
1525
1629
  }
1526
- public async setLightThemeAsync(id: number, theme: number) {
1630
+ public async setLightThemeAsync(id: number, theme: number): Promise<ICircuitState> {
1527
1631
  // Re-route this as we cannot set individual circuit themes in *Touch.
1528
1632
  return this.setLightGroupThemeAsync(id, theme);
1529
1633
  }
1634
+ public async runLightGroupCommandAsync(obj: any): Promise<ICircuitState> {
1635
+ // Do all our validation.
1636
+ try {
1637
+ let id = parseInt(obj.id, 10);
1638
+ let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightGroupCommands.findItem(obj.command) : { val: 0, name: 'undefined' };
1639
+ if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light group command ${cmd.name} does not exist`, 'runLightGroupCommandAsync'));
1640
+ if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light group ${id} does not exist`, 'runLightGroupCommandAsync'));
1641
+ let grp = sys.lightGroups.getItemById(id);
1642
+ let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name);
1643
+ let sgrp = state.lightGroups.getItemById(grp.id);
1644
+ sgrp.action = nop;
1645
+ sgrp.emitEquipmentChange();
1646
+ switch (cmd.name) {
1647
+ case 'colorset':
1648
+ await this.sequenceLightGroupAsync(id, 'colorset');
1649
+ break;
1650
+ case 'colorswim':
1651
+ await this.sequenceLightGroupAsync(id, 'colorswim');
1652
+ break;
1653
+ case 'colorhold':
1654
+ await this.setLightGroupThemeAsync(id, 190);
1655
+ break;
1656
+ case 'colorrecall':
1657
+ await this.setLightGroupThemeAsync(id, 191);
1658
+ break;
1659
+ case 'lightthumper':
1660
+ await this.setLightGroupThemeAsync(id, 208);
1661
+ break;
1662
+ }
1663
+ sgrp.action = 0;
1664
+ sgrp.emitEquipmentChange();
1665
+ return sgrp;
1666
+ }
1667
+ catch (err) { return Promise.reject(`Error runLightGroupCommandAsync ${err.message}`); }
1668
+ }
1669
+ public async runLightCommandAsync(obj: any): Promise<ICircuitState> {
1670
+ // Do all our validation.
1671
+ try {
1672
+ let id = parseInt(obj.id, 10);
1673
+ let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightCommands.findItem(obj.command) : { val: 0, name: 'undefined' };
1674
+ if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light command ${cmd.name} does not exist`, 'runLightCommandAsync'));
1675
+ if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light ${id} does not exist`, 'runLightCommandAsync'));
1676
+ let circ = sys.circuits.getItemById(id);
1677
+ if (!circ.isActive) return Promise.reject(new InvalidOperationError(`Light circuit #${id} is not active`, 'runLightCommandAsync'));
1678
+ let type = sys.board.valueMaps.circuitFunctions.transform(circ.type);
1679
+ if (!type.isLight) return Promise.reject(new InvalidOperationError(`Circuit #${id} is not a light`, 'runLightCommandAsync'));
1680
+ let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name);
1681
+ let slight = state.circuits.getItemById(circ.id);
1682
+ slight.action = nop;
1683
+ slight.emitEquipmentChange();
1684
+ // Touch boards cannot change the theme or color of a single light.
1685
+ slight.action = 0;
1686
+ slight.emitEquipmentChange();
1687
+ return slight;
1688
+ }
1689
+ catch (err) { return Promise.reject(`Error runLightCommandAsync ${err.message}`); }
1690
+ }
1530
1691
  public async setLightGroupThemeAsync(id = sys.board.equipmentIds.circuitGroups.start, theme: number): Promise<ICircuitState> {
1531
1692
  return new Promise<ICircuitState>((resolve, reject) => {
1532
1693
  const grp = sys.lightGroups.getItemById(id);
1533
1694
  const sgrp = state.lightGroups.getItemById(id);
1534
1695
  grp.lightingTheme = sgrp.lightingTheme = theme;
1696
+ sgrp.action = sys.board.valueMaps.circuitActions.getValue('lighttheme');
1697
+ sgrp.emitEquipmentChange();
1535
1698
  let out = Outbound.create({
1536
1699
  action: 96,
1537
1700
  payload: [theme, 0],
@@ -1575,6 +1738,7 @@ export class TouchCircuitCommands extends CircuitCommands {
1575
1738
  setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'color'); });
1576
1739
  // other themes for magicstream?
1577
1740
  }
1741
+ sgrp.action = 0;
1578
1742
  sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
1579
1743
  state.emitEquipmentChanges();
1580
1744
  resolve(sgrp);
@@ -1668,17 +1832,18 @@ class TouchFeatureCommands extends FeatureCommands {
1668
1832
  class TouchChlorinatorCommands extends ChlorinatorCommands {
1669
1833
  public async setChlorAsync(obj: any): Promise<ChlorinatorState> {
1670
1834
  let id = parseInt(obj.id, 10);
1835
+ // Bail out right away if this is not controlled by the OCP.
1836
+ if (typeof obj.master !== 'undefined' && parseInt(obj.master, 10) !== 0) return super.setChlorAsync(obj);
1671
1837
  let isAdd = false;
1672
- let chlor = sys.chlorinators.getItemById(id);
1673
- if (id <= 0 || isNaN(id)) {
1674
- isAdd = true;
1675
- chlor.master = utils.makeBool(obj.master) ? 1 : 0;
1676
- // Calculate an id for the chlorinator. The messed up part is that if a chlorinator is not attached to the OCP, its address
1677
- // cannot be set by the MUX. This will have to wait.
1838
+ if (isNaN(id) || id <= 0) {
1839
+ // We are adding so we need to see if there is another chlorinator that is not external.
1840
+ 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));
1678
1841
  id = 1;
1679
- }
1680
- // If this is a Nixie chlorinator then go to the base class and handle it from there.
1681
- if (chlor.master === 1) return super.setChlorAsync(obj);
1842
+ isAdd = true;
1843
+ }
1844
+ let chlor = sys.chlorinators.getItemById(id);
1845
+ if (chlor.master !== 0 && !isAdd) return super.setChlorAsync(obj);
1846
+
1682
1847
  // RKS: I am not even sure this can be done with Touch as the master on the RS485 bus.
1683
1848
  if (typeof chlor.master === 'undefined') chlor.master = 0;
1684
1849
  let name = obj.name || chlor.name || 'IntelliChlor' + id;
@@ -1689,8 +1854,10 @@ class TouchChlorinatorCommands extends ChlorinatorCommands {
1689
1854
  let disabled = typeof obj.disabled !== 'undefined' ? utils.makeBool(obj.disabled) : chlor.disabled;
1690
1855
  let poolSetpoint = typeof obj.poolSetpoint !== 'undefined' ? parseInt(obj.poolSetpoint, 10) : chlor.poolSetpoint;
1691
1856
  let spaSetpoint = typeof obj.spaSetpoint !== 'undefined' ? parseInt(obj.spaSetpoint, 10) : chlor.spaSetpoint;
1692
- let model = typeof obj.model !== 'undefined' ? obj.model : chlor.model;
1857
+ let model = typeof obj.model !== 'undefined' ? sys.board.valueMaps.chlorinatorModel.encode(obj.model) : chlor.model || 0;
1693
1858
  let chlorType = typeof obj.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(obj.type) : chlor.type || 0;
1859
+ let portId = typeof obj.portId !== 'undefined' ? parseInt(obj.portId, 10) : chlor.portId;
1860
+ 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));
1694
1861
  if (isAdd) {
1695
1862
  if (isNaN(poolSetpoint)) poolSetpoint = 50;
1696
1863
  if (isNaN(spaSetpoint)) spaSetpoint = 10;
@@ -1709,7 +1876,7 @@ class TouchChlorinatorCommands extends ChlorinatorCommands {
1709
1876
  let body = sys.board.bodies.mapBodyAssociation(chlor.body);
1710
1877
  if (typeof body === 'undefined') {
1711
1878
  if (sys.equipment.shared) body = 32;
1712
- else if (!sys.equipment.dual) body = 1;
1879
+ else if (!sys.equipment.dual) body = 0;
1713
1880
  else return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body association is not valid: ${body}`, 'chlorinator', body));
1714
1881
  }
1715
1882
  if (poolSetpoint > 100 || poolSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator poolSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.poolSetpoint));
@@ -1727,7 +1894,7 @@ class TouchChlorinatorCommands extends ChlorinatorCommands {
1727
1894
  utils.makeBool(superChlorinate) && superChlorHours > 0 ? superChlorHours + 128 : 0, // We only want to set the superChlor when the user sends superChlor = true
1728
1895
  0, 0, 0, 0, 0, 0, 0],
1729
1896
  retries: 3,
1730
- response: true,
1897
+ response: true,
1731
1898
  // scope: Math.random(),
1732
1899
  onComplete: (err)=>{
1733
1900
  if (err) {
@@ -1747,7 +1914,7 @@ class TouchChlorinatorCommands extends ChlorinatorCommands {
1747
1914
  if (typeof reject === 'undefined' || typeof resolve === 'undefined') return;
1748
1915
  reject(new EquipmentTimeoutError(`no chlor response in 7 seconds`, `chlorTimeOut`));
1749
1916
  reject = undefined;
1750
-
1917
+
1751
1918
  }, 3000);
1752
1919
  });
1753
1920
  await request153packet;
@@ -1761,9 +1928,10 @@ class TouchChlorinatorCommands extends ChlorinatorCommands {
1761
1928
  schlor.body = chlor.body = body;
1762
1929
  chlor.address = 79 + id;
1763
1930
  chlor.name = schlor.name = name;
1764
- chlor.model = model;
1931
+ schlor.model = chlor.model = model;
1765
1932
  schlor.type = chlor.type = chlorType;
1766
1933
  chlor.isDosing = isDosing;
1934
+ chlor.portId = portId;
1767
1935
 
1768
1936
  let request217Packet = new Promise<void>((resolve, reject) => {
1769
1937
  let out = Outbound.create({
@@ -1792,7 +1960,7 @@ class TouchChlorinatorCommands extends ChlorinatorCommands {
1792
1960
  if (typeof _timeout !== 'undefined'){
1793
1961
  clearTimeout(_timeout);
1794
1962
  _timeout = undefined;
1795
- }
1963
+ }
1796
1964
  state.emitEquipmentChanges();
1797
1965
  return state.chlorinators.getItemById(id);
1798
1966
  } catch (err) {
@@ -2058,13 +2226,22 @@ class TouchPumpCommands extends PumpCommands {
2058
2226
  }
2059
2227
  if (typeof type.maxCircuits !== 'undefined' && type.maxCircuits > 0 && typeof data.circuits !== 'undefined') { // This pump type supports circuits
2060
2228
  for (let i = 1; i <= data.circuits.length && i <= type.maxCircuits; i++) {
2061
- let c = data.circuits[i - 1];
2229
+ // RKS: This notion of always returning the max number of circuits was misguided. It leaves gaps in the circuit definitions and makes the pump
2230
+ // layouts difficult when there are a variety of supported circuits. For instance with SF pumps you only get 4.
2231
+ let c = i >= data.circuits.length - 1 ? { speed: type.minSpeed || 0, flow: type.minFlow || 0, circuit: 0 } : data.circuits[i - 1];
2232
+ //let c = data.circuits[i - 1];
2062
2233
  let speed = parseInt(c.speed, 10);
2063
2234
  let flow = parseInt(c.flow, 10);
2064
2235
  if (isNaN(speed)) speed = type.minSpeed;
2065
2236
  if (isNaN(flow)) flow = type.minFlow;
2066
2237
  outc.setPayloadByte(i * 2 + 3, parseInt(c.circuit, 10), 0);
2067
- c.units = parseInt(c.units, 10) || type.name === 'vf' ? sys.board.valueMaps.pumpUnits.getValue('gpm') : sys.board.valueMaps.pumpUnits.getValue('rpm');
2238
+ let units;
2239
+ if (type.name === 'vf') units = sys.board.valueMaps.pumpUnits.getValue('gpm');
2240
+ else if (type.name === 'vs') units = sys.board.valueMaps.pumpUnits.getValue('rpm');
2241
+ else units = sys.board.valueMaps.pumpUnits.encode(c.units);
2242
+ if (isNaN(units)) units = sys.board.valueMaps.pumpUnits.getValue('rpm');
2243
+ c.units = units;
2244
+ //c.units = parseInt(c.units, 10) || type.name === 'vf' ? sys.board.valueMaps.pumpUnits.getValue('gpm') : sys.board.valueMaps.pumpUnits.getValue('rpm');
2068
2245
  if (typeof type.minSpeed !== 'undefined' && c.units === sys.board.valueMaps.pumpUnits.getValue('rpm')) {
2069
2246
  outc.setPayloadByte(i * 2 + 4, Math.floor(speed / 256)); // Set to rpm
2070
2247
  outc.setPayloadByte(i + 21, speed % 256);
@@ -2180,9 +2357,11 @@ class TouchPumpCommands extends PumpCommands {
2180
2357
  spump.type = pump.type;
2181
2358
  spump.status = 0;
2182
2359
  }
2183
- public async deletePumpAsync(pump: Pump):Promise<Pump>{
2184
- let id = pump.id;
2185
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`DeletePumpAsync: Pump ${id} is not valid.`, 0, `pump`))
2360
+ public async deletePumpAsync(data: any):Promise<Pump>{
2361
+ let id = parseInt(data.id, 10);
2362
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`deletePumpAsync: Pump ${id} is not valid.`, 0, `pump`));
2363
+ let pump = sys.pumps.getItemById(id, false);
2364
+ if (pump.master === 1) return super.deletePumpAsync(data);
2186
2365
  const outc = Outbound.create({
2187
2366
  action: 155,
2188
2367
  payload: [id, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
@@ -2273,26 +2452,146 @@ class TouchHeaterCommands extends HeaterCommands {
2273
2452
  let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
2274
2453
  if (isNaN(id)) return reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
2275
2454
  let heater: Heater;
2455
+ let address: number;
2456
+ let out = Outbound.create({
2457
+ action: 162,
2458
+ payload: [5, 0, 0],
2459
+ retries: 2,
2460
+ // I am assuming that there should be an action 34 when the 162 is sent but I do not have this
2461
+ // data.
2462
+ response: Response.create({ dest: -1, action: 34 })
2463
+ });
2464
+ let htype;
2276
2465
  if (id <= 0) {
2277
- // We are adding a heater. In this case all heaters are virtual.
2278
- let heaters = sys.heaters.filter(h => h.master === 1);
2279
- id = heaters.getMaxId() + 1;
2466
+ // Touch only supports two installed heaters. So the type determines the id.
2467
+ if (sys.heaters.length > sys.equipment.maxHeaters) return reject(new InvalidEquipmentDataError('The maximum number of heaters are already installed.', 'Heater', sys.heaters.length));
2468
+ htype = sys.board.valueMaps.heaterTypes.findItem(obj.type);
2469
+ if (typeof htype === 'undefined') return reject(new InvalidEquipmentDataError('Heater type is not valid.', 'Heater', obj.heaterType));
2470
+ // Check to see if we can find any heaters of this type already installed.
2471
+ if (sys.heaters.count(h => h.type === htype.val) > 0) return reject(new InvalidEquipmentDataError(`Only one ${htype.desc} heater can be installed`, 'Heater', htype));
2472
+ // Next we need to see if this heater is compatible with all the other heaters. For Touch you may only have the following combos.
2473
+ // 1 Gas + 1 Solar
2474
+ // 1 Gas + 1 Heatpump
2475
+ // 1 Hybrid
2476
+
2477
+ // Heater ids are as follows.
2478
+ // 1 = Gas Heater
2479
+ // 2 = Solar
2480
+ // 3 = UltraTemp (HEATPUMPCOM)
2481
+ // 4 = UltraTemp ETi (Hybrid)
2482
+ switch (htype.name) {
2483
+ case 'gas':
2484
+ id = 1;
2485
+ break;
2486
+ case 'solar':
2487
+ out.setPayloadByte(0, out.payload[0] | 0x02);
2488
+ // Set the start and stop temp delta.
2489
+ out.setPayloadByte(1, (obj.freeze ? 0x80 : 0x00) | (obj.coolingEnabled ? 0x20 : 0x00));
2490
+ out.setPayloadByte(2, ((obj.startTempDelta || 6) - 3 << 6) | ((obj.stopTempDelta || 3) - 2 << 1));
2491
+ id = 2;
2492
+ break;
2493
+ case 'ultratemp':
2494
+ case 'heatpump':
2495
+ address = 112;
2496
+ out.setPayloadByte(0, out.payload[0] | 0x02);
2497
+ out.setPayloadByte(1, out.payload[1] | 0x10 | (obj.coolingEnabled ? 0x20 : 0x00));
2498
+ id = 3;
2499
+ break;
2500
+ case 'hybrid':
2501
+ // If we are adding a hybrid heater this means that the gas heater is to be replaced. This means that only
2502
+ // a gas heater can be installed.
2503
+ if (sys.heaters.length > 1) return reject(new InvalidEquipmentDataError(`Hybrid heaters can only be installed by themselves`, 'Heater', htype));
2504
+ if (sys.heaters.getItemByIndex(0).type > 1) return reject(new InvalidEquipmentDataError(`Hybrid heaters can only replace the gas heater`, 'Heater', htype));
2505
+ out.setPayloadByte(0, 5);
2506
+ out.setPayloadByte(1, 16);
2507
+ // NOTE: byte 2 makes absolutely no sense. Perhaps this is because we have no idea what message action 16 is. This probably contains the rest of the info
2508
+ // for heaters on Touch panels.
2509
+ out.setPayloadByte(2, 118);
2510
+ id = 4;
2511
+ break;
2512
+ }
2280
2513
  }
2281
- heater = sys.heaters.getItemById(id, true);
2282
- if (typeof obj !== undefined) {
2283
- for (var s in obj) {
2284
- if (s === 'id') continue;
2285
- heater[s] = obj[s];
2514
+ else {
2515
+ // This all works because there are 0 items that can be set on a Touch heater with the exception of a few items on solar. This means that the
2516
+ // first two bytes are calculated based upon the existing heaters.
2517
+ heater = sys.heaters.find(x => id === x.id);
2518
+ if (typeof heater === 'undefined') return reject(new InvalidEquipmentIdError(`Heater #${id} is not installed and cannot be updated.`, id, 'Heater'));
2519
+ // So here we go with the settings.
2520
+ htype = sys.board.valueMaps.heaterTypes.findItem(heater.type);
2521
+ switch (htype.name) {
2522
+ case 'gas':
2523
+ break;
2524
+ case 'solar':
2525
+ out.setPayloadByte(0, out.payload[0] | 0x02);
2526
+ // Set the start and stop temp delta.
2527
+ out.setPayloadByte(1, (obj.freeze ? 0x80 : 0x00) | (obj.coolingEnabled ? 0x20 : 0x00));
2528
+ out.setPayloadByte(2, ((obj.startTempDelta || 6) - 3 << 6) | ((obj.stopTempDelta || 3) - 2 << 1));
2529
+ break;
2530
+ case 'ultratemp':
2531
+ case 'heatpump':
2532
+ address = 112;
2533
+ out.setPayloadByte(0, out.payload[0] | 0x02);
2534
+ out.setPayloadByte(1, out.payload[1] | 0x10 | (obj.coolingEnabled ? 0x20 : 0x00));
2535
+ break;
2536
+ case 'hybrid':
2537
+ address = 112;
2538
+ out.setPayloadByte(0, 5);
2539
+ out.setPayloadByte(1, 16);
2540
+ // NOTE: byte 2 makes absolutely no sense. Perhaps this is because we have no idea what message action 144/16 is. This probably contains the rest of the info
2541
+ // for heaters on Touch panels.
2542
+ out.setPayloadByte(2, 118);
2543
+ break;
2544
+ }
2545
+ }
2546
+ // Set the bytes from the existing installed heaters.
2547
+ for (let i = 0; i < sys.heaters.length; i++) {
2548
+ let h = sys.heaters.getItemByIndex(i);
2549
+ if (h.id === id) continue;
2550
+ let ht = sys.board.valueMaps.heaterTypes.transform(h.type);
2551
+ switch (ht.name) {
2552
+ case 'gas':
2553
+ break;
2554
+ case 'solar':
2555
+ out.setPayloadByte(0, out.payload[0] | 0x02);
2556
+ out.setPayloadByte(1, (h.freeze ? 0x80 : 0x00) | (h.coolingEnabled ? 0x20 : 0x00));
2557
+ out.setPayloadByte(2, ((h.startTempDelta || 6) - 3 << 6) | ((h.stopTempDelta || 3) - 2 << 1));
2558
+ break;
2559
+ case 'ultratemp':
2560
+ case 'heatpump':
2561
+ out.setPayloadByte(0, out.payload[0] | 0x02);
2562
+ out.setPayloadByte(1, out.payload[1] | 0x10 | (h.coolingEnabled ? 0x20 : 0x00));
2563
+ break;
2564
+ case 'hybrid':
2565
+ break;
2566
+ }
2567
+ }
2568
+ out.onComplete = (err, msg) => {
2569
+ if (err) reject(err);
2570
+ else {
2571
+ heater = sys.heaters.getItemById(id, true);
2572
+ let sheater = state.heaters.getItemById(id, true);
2573
+ for (var s in obj) {
2574
+ switch (s) {
2575
+ case 'id':
2576
+ case 'name':
2577
+ case 'type':
2578
+ case 'address':
2579
+ break;
2580
+ default:
2581
+ heater[s] = obj[s];
2582
+ break;
2583
+ }
2584
+ }
2585
+ sheater.name = heater.name = typeof obj.name !== 'undefined' ? obj.name : heater.name;
2586
+ sheater.type = heater.type = htype.val;
2587
+ heater.address = address;
2588
+ heater.master = 0;
2589
+ heater.body = sys.equipment.shared ? 32 : 0;
2590
+ sys.board.heaters.updateHeaterServices();
2591
+ sys.board.heaters.syncHeaterStates();
2592
+ resolve(heater);
2286
2593
  }
2287
2594
  }
2288
- let hstate = state.heaters.getItemById(id, true);
2289
-
2290
- hstate.name = heater.name;
2291
- hstate.type = heater.type;
2292
- heater.master = 1;
2293
- sys.board.heaters.updateHeaterServices();
2294
- sys.board.heaters.syncHeaterStates();
2295
- resolve(heater);
2296
2595
  });
2297
2596
  }
2298
2597
  public async deleteHeaterAsync(obj: any): Promise<Heater> {
@@ -2315,45 +2614,64 @@ class TouchHeaterCommands extends HeaterCommands {
2315
2614
  let heatPumpInstalled = htypes.heatpump > 0;
2316
2615
  let ultratempInstalled = htypes.ultratemp > 0;
2317
2616
  let gasHeaterInstalled = htypes.gas > 0;
2617
+ let hybridInstalled = htypes.hybrid > 0;
2318
2618
  sys.board.valueMaps.heatModes.set(0, { name: 'off', desc: 'Off' });
2319
2619
  sys.board.valueMaps.heatSources.set(0, { name: 'off', desc: 'Off' });
2320
- if (gasHeaterInstalled) {
2321
- sys.board.valueMaps.heatModes.set(1, { name: 'heater', desc: 'Heater' });
2322
- sys.board.valueMaps.heatSources.set(2, { name: 'heater', desc: 'Heater' });
2323
- }
2324
- else {
2325
- // no heaters (virtual controller)
2326
- sys.board.valueMaps.heatModes.delete(1);
2327
- sys.board.valueMaps.heatSources.delete(2);
2328
- }
2329
- if (solarInstalled && gasHeaterInstalled) {
2330
- sys.board.valueMaps.heatModes.set(2, { name: 'solarpref', desc: 'Solar Preferred' });
2331
- sys.board.valueMaps.heatModes.set(3, { name: 'solar', desc: 'Solar Only' });
2332
- sys.board.valueMaps.heatSources.set(5, { name: 'solarpref', desc: 'Solar Preferred' });
2333
- sys.board.valueMaps.heatSources.set(21, { name: 'solar', desc: 'Solar Only' });
2334
- }
2335
- else if (heatPumpInstalled && gasHeaterInstalled) {
2336
- sys.board.valueMaps.heatModes.set(2, { name: 'heatpumppref', desc: 'Heat Pump Preferred' });
2337
- sys.board.valueMaps.heatModes.set(3, { name: 'heatpump', desc: 'Heat Pump Only' });
2338
- sys.board.valueMaps.heatSources.set(5, { name: 'heatpumppref', desc: 'Heat Pump Preferred' });
2339
- sys.board.valueMaps.heatSources.set(21, { name: 'heatpump', desc: 'Heat Pump Only' });
2340
- }
2341
- else if (ultratempInstalled && gasHeaterInstalled) {
2342
- sys.board.valueMaps.heatModes.merge([
2343
- [2, { name: 'ultratemppref', desc: 'UltraTemp Pref' }],
2344
- [3, { name: 'ultratemp', desc: 'UltraTemp Only' }]
2345
- ]);
2346
- sys.board.valueMaps.heatSources.merge([
2347
- [5, { name: 'ultratemppref', desc: 'Ultratemp Pref', hasCoolSetpoint: htypes.hasCoolSetpoint }],
2348
- [21, { name: 'ultratemp', desc: 'Ultratemp Only', hasCoolSetpoint: htypes.hasCoolSetpoint }]
2349
- ])
2620
+ if (hybridInstalled) {
2621
+ // Source Issue #390
2622
+ // 1 = Heat Pump
2623
+ // 2 = Gas Heater
2624
+ // 3 = Hybrid
2625
+ // 16 = Dual
2626
+ sys.board.valueMaps.heatModes.set(1, { name: 'heatpump', desc: 'Heat Pump' });
2627
+ sys.board.valueMaps.heatModes.set(2, { name: 'heater', desc: 'Gas Heat' });
2628
+ sys.board.valueMaps.heatModes.set(3, { name: 'heatpumppref', desc: 'Hybrid' });
2629
+ sys.board.valueMaps.heatModes.set(16, { name: 'dual', desc: 'Dual Heat' });
2630
+
2631
+ sys.board.valueMaps.heatSources.set(2, { name: 'heater', desc: 'Gas Heat' });
2632
+ sys.board.valueMaps.heatSources.set(5, { name: 'heatpumppref', desc: 'Hybrid' });
2633
+ sys.board.valueMaps.heatSources.set(20, { name: 'dual', desc: 'Dual Heat' });
2634
+ sys.board.valueMaps.heatSources.set(21, { name: 'heatpump', desc: 'Heat Pump' });
2350
2635
  }
2351
2636
  else {
2352
- // only gas
2353
- sys.board.valueMaps.heatModes.delete(2);
2354
- sys.board.valueMaps.heatModes.delete(3);
2355
- sys.board.valueMaps.heatSources.delete(5);
2356
- sys.board.valueMaps.heatSources.delete(21);
2637
+ if (gasHeaterInstalled) {
2638
+ sys.board.valueMaps.heatModes.set(1, { name: 'heater', desc: 'Heater' });
2639
+ sys.board.valueMaps.heatSources.set(2, { name: 'heater', desc: 'Heater' });
2640
+ }
2641
+ else {
2642
+ // no heaters (virtual controller)
2643
+ sys.board.valueMaps.heatModes.delete(1);
2644
+ sys.board.valueMaps.heatSources.delete(2);
2645
+ }
2646
+ if (solarInstalled && gasHeaterInstalled) {
2647
+ sys.board.valueMaps.heatModes.set(2, { name: 'solarpref', desc: 'Solar Preferred' });
2648
+ sys.board.valueMaps.heatModes.set(3, { name: 'solar', desc: 'Solar Only' });
2649
+ sys.board.valueMaps.heatSources.set(5, { name: 'solarpref', desc: 'Solar Preferred' });
2650
+ sys.board.valueMaps.heatSources.set(21, { name: 'solar', desc: 'Solar Only' });
2651
+ }
2652
+ else if (heatPumpInstalled && gasHeaterInstalled) {
2653
+ sys.board.valueMaps.heatModes.set(2, { name: 'heatpumppref', desc: 'Heat Pump Preferred' });
2654
+ sys.board.valueMaps.heatModes.set(3, { name: 'heatpump', desc: 'Heat Pump Only' });
2655
+ sys.board.valueMaps.heatSources.set(5, { name: 'heatpumppref', desc: 'Heat Pump Preferred' });
2656
+ sys.board.valueMaps.heatSources.set(21, { name: 'heatpump', desc: 'Heat Pump Only' });
2657
+ }
2658
+ else if (ultratempInstalled && gasHeaterInstalled) {
2659
+ sys.board.valueMaps.heatModes.merge([
2660
+ [2, { name: 'ultratemppref', desc: 'UltraTemp Pref' }],
2661
+ [3, { name: 'ultratemp', desc: 'UltraTemp Only' }]
2662
+ ]);
2663
+ sys.board.valueMaps.heatSources.merge([
2664
+ [5, { name: 'ultratemppref', desc: 'Ultratemp Pref', hasCoolSetpoint: htypes.hasCoolSetpoint }],
2665
+ [21, { name: 'ultratemp', desc: 'Ultratemp Only', hasCoolSetpoint: htypes.hasCoolSetpoint }]
2666
+ ])
2667
+ }
2668
+ else {
2669
+ // only gas
2670
+ sys.board.valueMaps.heatModes.delete(2);
2671
+ sys.board.valueMaps.heatModes.delete(3);
2672
+ sys.board.valueMaps.heatSources.delete(5);
2673
+ sys.board.valueMaps.heatSources.delete(21);
2674
+ }
2357
2675
  }
2358
2676
  sys.board.valueMaps.heatSources.set(32, { name: 'nochange', desc: 'No Change' });
2359
2677
  this.setActiveTempSensors();
@@ -2430,6 +2748,8 @@ class TouchChemControllerCommands extends ChemControllerCommands {
2430
2748
  chem.orp.tank.capacity = 6;
2431
2749
  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;
2432
2750
  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;
2751
+ // OCP needs to set the IntelliChem as active so it knows that it exists
2752
+
2433
2753
  return new Promise<ChemController>((resolve, reject) => {
2434
2754
  let out = Outbound.create({
2435
2755
  action: 211,
@@ -2450,7 +2770,7 @@ class TouchChemControllerCommands extends ChemControllerCommands {
2450
2770
  chem.cyanuricAcid = cyanuricAcid;
2451
2771
  chem.alkalinity = alkalinity;
2452
2772
  chem.borates = borates;
2453
- chem.body = schem.body = body;
2773
+ chem.body = schem.body = body.val;
2454
2774
  schem.isActive = chem.isActive = true;
2455
2775
  chem.lsiRange.enabled = lsiRange.enabled;
2456
2776
  chem.lsiRange.low = lsiRange.low;
@@ -2467,7 +2787,8 @@ class TouchChemControllerCommands extends ChemControllerCommands {
2467
2787
  chem.address = schem.address = address;
2468
2788
  chem.name = schem.name = name;
2469
2789
  chem.flowSensor.enabled = false;
2470
- resolve(chem);
2790
+ sys.board.bodies.setBodyAsync(sys.bodies.getItemById(1, false))
2791
+ .then(()=>{resolve(chem)});
2471
2792
  }
2472
2793
  }
2473
2794
  });
@@ -2491,45 +2812,48 @@ class TouchChemControllerCommands extends ChemControllerCommands {
2491
2812
  public async deleteChemControllerAsync(data: any): Promise<ChemController> {
2492
2813
  let id = typeof data.id !== 'undefined' ? parseInt(data.id, 10) : -1;
2493
2814
  if (typeof id === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid Chem Controller Id`, id, 'chemController'));
2494
- let chem = sys.chemControllers.getItemById(id);
2815
+ let chem = sys.board.chemControllers.findChemController(data);
2495
2816
  if (chem.master === 1) return super.deleteChemControllerAsync(data);
2496
2817
  return new Promise<ChemController>((resolve, reject) => {
2497
- let out = Outbound.create({
2498
- action: 211,
2499
- response: Response.create({ protocol: Protocol.IntelliChem, action: 1, payload: [211] }),
2500
- retries: 3,
2501
- payload: [],
2502
- onComplete: (err) => {
2503
- if (err) { reject(err); }
2504
- else {
2505
- let schem = state.chemControllers.getItemById(id);
2506
- chem.isActive = false;
2507
- chem.ph.tank.capacity = chem.orp.tank.capacity = 6;
2508
- chem.ph.tank.units = chem.orp.tank.units = '';
2509
- schem.isActive = false;
2510
- sys.chemControllers.removeItemById(id);
2511
- state.chemControllers.removeItemById(id);
2512
- resolve(chem);
2513
- }
2818
+ let out = Outbound.create({
2819
+ action: 211,
2820
+ response: Response.create({ protocol: Protocol.IntelliChem, action: 1, payload: [211] }),
2821
+ retries: 3,
2822
+ payload: [],
2823
+ onComplete: (err) => {
2824
+ if (err) { reject(err); }
2825
+ else {
2826
+ let schem = state.chemControllers.getItemById(id);
2827
+ chem.isActive = false;
2828
+ chem.ph.tank.capacity = chem.orp.tank.capacity = 6;
2829
+ chem.ph.tank.units = chem.orp.tank.units = '';
2830
+ schem.isActive = false;
2831
+ sys.board.bodies.setBodyAsync(sys.bodies.getItemById(1, false))
2832
+ .then(()=>{
2833
+ sys.chemControllers.removeItemById(id);
2834
+ state.chemControllers.removeItemById(id);
2835
+ resolve(chem);
2836
+ })
2837
+ .catch(()=>{reject(err);});
2514
2838
  }
2515
- });
2516
- // I think this payload should delete the controller on Touch.
2517
- out.insertPayloadBytes(0, 0, 22);
2518
- out.setPayloadByte(0, chem.address - 144);
2519
- out.setPayloadByte(1, Math.floor((chem.ph.setpoint * 100) / 256) || 0);
2520
- out.setPayloadByte(2, Math.round((chem.ph.setpoint * 100) % 256) || 0);
2521
- out.setPayloadByte(3, Math.floor(chem.orp.setpoint / 256) || 0);
2522
- out.setPayloadByte(4, Math.round(chem.orp.setpoint % 256) || 0);
2523
- out.setPayloadByte(5, 0);
2524
- out.setPayloadByte(6, 0);
2525
- out.setPayloadByte(7, Math.floor(chem.calciumHardness / 256) || 0);
2526
- out.setPayloadByte(8, Math.round(chem.calciumHardness % 256) || 0);
2527
- out.setPayloadByte(9, chem.cyanuricAcid || 0);
2528
- out.setPayloadByte(11, Math.floor(chem.alkalinity / 256) || 0);
2529
- out.setPayloadByte(12, Math.round(chem.alkalinity % 256) || 0);
2530
- out.setPayloadByte(13, 20);
2531
- conn.queueSendMessage(out);
2839
+ }
2532
2840
  });
2841
+ // I think this payload should delete the controller on Touch.
2842
+ out.insertPayloadBytes(0, 0, 22);
2843
+ out.setPayloadByte(0, chem.address - 144 || 0);
2844
+ out.setPayloadByte(1, Math.floor((chem.ph.setpoint * 100) / 256) || 0);
2845
+ out.setPayloadByte(2, Math.round((chem.ph.setpoint * 100) % 256) || 0);
2846
+ out.setPayloadByte(3, Math.floor(chem.orp.setpoint / 256) || 0);
2847
+ out.setPayloadByte(4, Math.round(chem.orp.setpoint % 256) || 0);
2848
+ out.setPayloadByte(5, 0);
2849
+ out.setPayloadByte(6, 0);
2850
+ out.setPayloadByte(7, Math.floor(chem.calciumHardness / 256) || 0);
2851
+ out.setPayloadByte(8, Math.round(chem.calciumHardness % 256) || 0);
2852
+ out.setPayloadByte(9, chem.cyanuricAcid || 0);
2853
+ out.setPayloadByte(11, Math.floor(chem.alkalinity / 256) || 0);
2854
+ out.setPayloadByte(12, Math.round(chem.alkalinity % 256) || 0);
2855
+ out.setPayloadByte(13, 20);
2856
+ conn.queueSendMessage(out);
2857
+ });
2533
2858
  }
2534
-
2535
2859
  }