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
@@ -18,11 +18,12 @@ import * as extend from 'extend';
18
18
  import { logger } from '../../logger/Logger';
19
19
  import { Message, Outbound } from '../comms/messages/Messages';
20
20
  import { Timestamp, utils } from '../Constants';
21
- import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, ControllerType, CustomName, CustomNameCollection, EggTimer, Equipment, Feature, Filter, General, Heater, ICircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, TempSensorCollection, Valve } from '../Equipment';
22
- import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError } from '../Errors';
21
+ import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, ControllerType, CustomName, CustomNameCollection, EggTimer, Equipment, Feature, Filter, General, Heater, ICircuit, ICircuitGroup, ICircuitGroupCircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, TempSensorCollection, Valve } from '../Equipment';
22
+ import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, BoardProcessError, InvalidOperationError } from '../Errors';
23
23
  import { ncp } from "../nixie/Nixie";
24
24
  import { BodyTempState, ChemControllerState, ChlorinatorState, CircuitGroupState, FilterState, ICircuitGroupState, ICircuitState, LightGroupState, ScheduleState, state, TemperatureState, ValveState, VirtualCircuitState } from '../State';
25
25
  import { RestoreResults } from '../../web/Server';
26
+ import { group } from 'console';
26
27
 
27
28
 
28
29
  export class byteValueMap extends Map<number, any> {
@@ -210,7 +211,8 @@ export class byteValueMaps {
210
211
  // Identifies which controller manages the underlying equipment.
211
212
  public equipmentMaster: byteValueMap = new byteValueMap([
212
213
  [0, { val: 0, name: 'ocp', desc: 'Outdoor Control Panel' }],
213
- [1, { val: 1, name: 'ncp', desc: 'Nixie Control Panel' }]
214
+ [1, { val: 1, name: 'ncp', desc: 'Nixie Control Panel' }],
215
+ [2, { val: 2, name: 'ext', desc: 'External Control Panel' }]
214
216
  ]);
215
217
  public equipmentCommStatus: byteValueMap = new byteValueMap([
216
218
  [0, { val: 0, name: 'ready', desc: 'Ready' }],
@@ -233,9 +235,9 @@ export class byteValueMaps {
233
235
 
234
236
  public circuitFunctions: byteValueMap = new byteValueMap([
235
237
  [0, { name: 'generic', desc: 'Generic' }],
236
- [1, { name: 'spa', desc: 'Spa', hasHeatSource: true }],
237
- [2, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
238
- [5, { name: 'mastercleaner', desc: 'Master Cleaner' }],
238
+ [1, { name: 'spa', desc: 'Spa', hasHeatSource: true, body: 2 }],
239
+ [2, { name: 'pool', desc: 'Pool', hasHeatSource: true, body: 1 }],
240
+ [5, { name: 'mastercleaner', desc: 'Master Cleaner', body: 1 }],
239
241
  [7, { name: 'light', desc: 'Light', isLight: true }],
240
242
  [9, { name: 'samlight', desc: 'SAM Light', isLight: true }],
241
243
  [10, { name: 'sallight', desc: 'SAL Light', isLight: true }],
@@ -243,9 +245,9 @@ export class byteValueMaps {
243
245
  [12, { name: 'colorwheel', desc: 'Color Wheel', isLight: true }],
244
246
  [13, { name: 'valve', desc: 'Valve' }],
245
247
  [14, { name: 'spillway', desc: 'Spillway' }],
246
- [15, { name: 'floorcleaner', desc: 'Floor Cleaner' }],
247
- [16, { name: 'intellibrite', desc: 'Intellibrite', isLight: true }],
248
- [17, { name: 'magicstream', desc: 'Magicstream', isLight: true }],
248
+ [15, { name: 'floorcleaner', desc: 'Floor Cleaner', body: 1 }], // This circuit function does not seem to exist in IntelliTouch.
249
+ [16, { name: 'intellibrite', desc: 'Intellibrite', isLight: true, theme: 'intellibrite' }],
250
+ [17, { name: 'magicstream', desc: 'Magicstream', isLight: true, theme: 'magicstream' }],
249
251
  [19, { name: 'notused', desc: 'Not Used' }],
250
252
  [65, { name: 'lotemp', desc: 'Lo-Temp' }],
251
253
  [66, { name: 'hightemp', desc: 'Hi-Temp' }]
@@ -266,51 +268,104 @@ export class byteValueMaps {
266
268
  [255, { name: 'notused', desc: 'NOT USED', assignableToPumpCircuit: true }]
267
269
  ]);
268
270
  public lightThemes: byteValueMap = new byteValueMap([
269
- [0, { name: 'off', desc: 'Off', type: 'intellibrite' }],
270
- [1, { name: 'on', desc: 'On', type: 'intellibrite' }],
271
- [128, { name: 'colorsync', desc: 'Color Sync', type: 'intellibrite' }],
272
- [144, { name: 'colorswim', desc: 'Color Swim', type: 'intellibrite' }],
273
- [160, { name: 'colorset', desc: 'Color Set', type: 'intellibrite' }],
274
- [177, { name: 'party', desc: 'Party', type: 'intellibrite', sequence: 2 }],
275
- [178, { name: 'romance', desc: 'Romance', type: 'intellibrite', sequence: 3 }],
276
- [179, { name: 'caribbean', desc: 'Caribbean', type: 'intellibrite', sequence: 4 }],
277
- [180, { name: 'american', desc: 'American', type: 'intellibrite', sequence: 5 }],
278
- [181, { name: 'sunset', desc: 'Sunset', type: 'intellibrite', sequence: 6 }],
279
- [182, { name: 'royal', desc: 'Royal', type: 'intellibrite', sequence: 7 }],
280
- [190, { name: 'save', desc: 'Save', type: 'intellibrite', sequence: 13 }],
281
- [191, { name: 'recall', desc: 'Recall', type: 'intellibrite', sequence: 14 }],
282
- [193, { name: 'blue', desc: 'Blue', type: 'intellibrite', sequence: 8 }],
283
- [194, { name: 'green', desc: 'Green', type: 'intellibrite', sequence: 9 }],
284
- [195, { name: 'red', desc: 'Red', type: 'intellibrite', sequence: 10 }],
285
- [196, { name: 'white', desc: 'White', type: 'intellibrite', sequence: 11 }],
286
- [197, { name: 'magenta', desc: 'Magenta', type: 'intellibrite', sequence: 12 }],
287
- [208, { name: 'thumper', desc: 'Thumper', type: 'magicstream' }],
288
- [209, { name: 'hold', desc: 'Hold', type: 'magicstream' }],
289
- [210, { name: 'reset', desc: 'Reset', type: 'magicstream' }],
290
- [211, { name: 'mode', desc: 'Mode', type: 'magicstream' }],
271
+ [0, { name: 'off', desc: 'Off' }],
272
+ [1, { name: 'on', desc: 'On' }],
273
+ [128, { name: 'colorsync', desc: 'Color Sync' }],
274
+ [144, { name: 'colorswim', desc: 'Color Swim' }],
275
+ [160, { name: 'colorset', desc: 'Color Set' }],
276
+ [177, { name: 'party', desc: 'Party', types: ['intellibrite'], sequence: 2 }],
277
+ [178, { name: 'romance', desc: 'Romance', types: ['intellibrite'], sequence: 3 }],
278
+ [179, { name: 'caribbean', desc: 'Caribbean', types: ['intellibrite'], sequence: 4 }],
279
+ [180, { name: 'american', desc: 'American', types: ['intellibrite'], sequence: 5 }],
280
+ [181, { name: 'sunset', desc: 'Sunset', types: ['intellibrite'], sequence: 6 }],
281
+ [182, { name: 'royal', desc: 'Royal', types: ['intellibrite'], sequence: 7 }],
282
+ [190, { name: 'save', desc: 'Save', types: ['intellibrite'], sequence: 13 }],
283
+ [191, { name: 'recall', desc: 'Recall', types: ['intellibrite'], sequence: 14 }],
284
+ [193, { name: 'blue', desc: 'Blue', types: ['intellibrite'], sequence: 8 }],
285
+ [194, { name: 'green', desc: 'Green', types: ['intellibrite'], sequence: 9 }],
286
+ [195, { name: 'red', desc: 'Red', types: ['intellibrite'], sequence: 10 }],
287
+ [196, { name: 'white', desc: 'White', types: ['intellibrite'], sequence: 11 }],
288
+ [197, { name: 'magenta', desc: 'Magenta', types: ['intellibrite'], sequence: 12 }],
289
+ [208, { name: 'thumper', desc: 'Thumper', types: ['magicstream'] }],
290
+ [209, { name: 'hold', desc: 'Hold', types: ['magicstream'] }],
291
+ [210, { name: 'reset', desc: 'Reset', types: ['magicstream'] }],
292
+ [211, { name: 'mode', desc: 'Mode', types: ['magicstream'] }],
291
293
  [254, { name: 'unknown', desc: 'unknown' }],
292
294
  [255, { name: 'none', desc: 'None' }]
293
295
  ]);
294
296
  public colorLogicThemes = new byteValueMap([
295
- [0, { name: 'cloudwhite', desc: 'Cloud White', type: 'colorlogic', sequence: 7 }],
296
- [1, { name: 'deepsea', desc: 'Deep Sea', type: 'colorlogic', sequence: 2 }],
297
- [2, { name: 'royalblue', desc: 'Royal Blue', type: 'colorlogic', sequence: 3 }],
298
- [3, { name: 'afernoonskies', desc: 'Afternoon Skies', type: 'colorlogic', sequence: 4 }],
299
- [4, { name: 'aquagreen', desc: 'Aqua Green', type: 'colorlogic', sequence: 5 }],
300
- [5, { name: 'emerald', desc: 'Emerald', type: 'colorlogic', sequence: 6 }],
301
- [6, { name: 'warmred', desc: 'Warm Red', type: 'colorlogic', sequence: 8 }],
302
- [7, { name: 'flamingo', desc: 'Flamingo', type: 'colorlogic', sequence: 9 }],
303
- [8, { name: 'vividviolet', desc: 'Vivid Violet', type: 'colorlogic', sequence: 10 }],
304
- [9, { name: 'sangria', desc: 'Sangria', type: 'colorlogic', sequence: 11 }],
305
- [10, { name: 'twilight', desc: 'Twilight', type: 'colorlogic', sequence: 12 }],
306
- [11, { name: 'tranquility', desc: 'Tranquility', type: 'colorlogic', sequence: 13 }],
307
- [12, { name: 'gemstone', desc: 'Gemstone', type: 'colorlogic', sequence: 14 }],
308
- [13, { name: 'usa', desc: 'USA', type: 'colorlogic', sequence: 15 }],
309
- [14, { name: 'mardigras', desc: 'Mardi Gras', type: 'colorlogic', sequence: 16 }],
310
- [15, { name: 'cabaret', desc: 'Cabaret', type: 'colorlogic', sequence: 17 }],
297
+ [0, { name: 'cloudwhite', desc: 'Cloud White', types: ['colorlogic'], sequence: 7 }],
298
+ [1, { name: 'deepsea', desc: 'Deep Sea', types: ['colorlogic'], sequence: 2 }],
299
+ [2, { name: 'royalblue', desc: 'Royal Blue', types: ['colorlogic'], sequence: 3 }],
300
+ [3, { name: 'afernoonskies', desc: 'Afternoon Skies', types: ['colorlogic'], sequence: 4 }],
301
+ [4, { name: 'aquagreen', desc: 'Aqua Green', types: ['colorlogic'], sequence: 5 }],
302
+ [5, { name: 'emerald', desc: 'Emerald', types: ['colorlogic'], sequence: 6 }],
303
+ [6, { name: 'warmred', desc: 'Warm Red', types: ['colorlogic'], sequence: 8 }],
304
+ [7, { name: 'flamingo', desc: 'Flamingo', types: ['colorlogic'], sequence: 9 }],
305
+ [8, { name: 'vividviolet', desc: 'Vivid Violet', types: ['colorlogic'], sequence: 10 }],
306
+ [9, { name: 'sangria', desc: 'Sangria', types: ['colorlogic'], sequence: 11 }],
307
+ [10, { name: 'twilight', desc: 'Twilight', types: ['colorlogic'], sequence: 12 }],
308
+ [11, { name: 'tranquility', desc: 'Tranquility', types: ['colorlogic'], sequence: 13 }],
309
+ [12, { name: 'gemstone', desc: 'Gemstone', types: ['colorlogic'], sequence: 14 }],
310
+ [13, { name: 'usa', desc: 'USA', types: ['colorlogic'], sequence: 15 }],
311
+ [14, { name: 'mardigras', desc: 'Mardi Gras', types: ['colorlogic'], sequence: 16 }],
312
+ [15, { name: 'cabaret', desc: 'Cabaret', types: ['colorlogic'], sequence: 17 }],
311
313
  [255, { name: 'none', desc: 'None' }]
312
314
  ]);
313
-
315
+ public lightCommands = new byteValueMap([
316
+ [4, { name: 'colorhold', desc: 'Hold', types: ['intellibrite', 'magicstream'], command: 'colorHold', sequence: 13 }],
317
+ [5, { name: 'colorrecall', desc: 'Recall', types: ['intellibrite', 'magicstream'], command: 'colorRecall', sequence: 14 }],
318
+ [6, {
319
+ name: 'lightthumper', desc: 'Thumper', types: ['magicstream'], command: 'lightThumper', message: 'Toggling Thumper',
320
+ sequence: [ // Cycle party mode 3 times.
321
+ { isOn: false, timeout: 100 },
322
+ { isOn: true, timeout: 100 },
323
+ { isOn: false, timeout: 100 },
324
+ { isOn: true, timeout: 5000 },
325
+ { isOn: false, timeout: 100 },
326
+ { isOn: true, timeout: 100 },
327
+ { isOn: false, timeout: 100 },
328
+ { isOn: true, timeout: 5000 },
329
+ { isOn: false, timeout: 100 },
330
+ { isOn: true, timeout: 100 },
331
+ { isOn: false, timeout: 100 }
332
+ ]
333
+ }]
334
+ ]);
335
+ public lightGroupCommands = new byteValueMap([
336
+ [1, { name: 'colorsync', desc: 'Sync', types: ['intellibrite'], command: 'colorSync', message:'Synchronizing' }],
337
+ [2, { name: 'colorset', desc: 'Set', types: ['intellibrite'], command: 'colorSet', message: 'Sequencing Set Operation' }],
338
+ [3, { name: 'colorswim', desc: 'Swim', types: ['intellibrite'], command: 'colorSwim', message:'Sequencing Swim Operation' }],
339
+ [4, { name: 'colorhold', desc: 'Hold', types: ['intellibrite', 'magicstream'], command: 'colorHold', message: 'Saving Current Colors', sequence: 13 }],
340
+ [5, { name: 'colorrecall', desc: 'Recall', types: ['intellibrite', 'magicstream'], command: 'colorRecall', message: 'Recalling Saved Colors', sequence: 14 }],
341
+ [6, {
342
+ name: 'lightthumper', desc: 'Thumper', types: ['magicstream'], command: 'lightThumper', message: 'Toggling Thumper',
343
+ sequence: [ // Cycle party mode 3 times.
344
+ { isOn: false, timeout: 100 },
345
+ { isOn: true, timeout: 100 },
346
+ { isOn: false, timeout: 100 },
347
+ { isOn: true, timeout: 5000 },
348
+ { isOn: false, timeout: 100 },
349
+ { isOn: true, timeout: 100 },
350
+ { isOn: false, timeout: 100 },
351
+ { isOn: true, timeout: 5000 },
352
+ { isOn: false, timeout: 100 },
353
+ { isOn: true, timeout: 100 },
354
+ { isOn: false, timeout: 100 },
355
+ { isOn: true, timeout: 1000 },
356
+ ]
357
+ }]
358
+ ]);
359
+ public circuitActions: byteValueMap = new byteValueMap([
360
+ [0, { name: 'ready', desc: 'Ready' }],
361
+ [1, { name: 'colorsync', desc: 'Synchronizing' }],
362
+ [2, { name: 'colorset', desc: 'Sequencing Set Operation' }],
363
+ [3, { name: 'colorswim', desc: 'Sequencing Swim Operation' }],
364
+ [4, { name: 'lighttheme', desc: 'Sequencing Theme/Color Operation' }],
365
+ [5, { name: 'colorhold', desc: 'Saving Current Color' }],
366
+ [6, { name: 'colorrecall', desc: 'Recalling Saved Color' }],
367
+ [7, { name: 'lightthumper', desc: 'Setting Light Thumper' }]
368
+ ]);
314
369
  public lightColors: byteValueMap = new byteValueMap([
315
370
  [0, { name: 'white', desc: 'White' }],
316
371
  [2, { name: 'lightgreen', desc: 'Light Green' }],
@@ -338,7 +393,6 @@ export class byteValueMaps {
338
393
  [1, { name: 'active', desc: 'When Active' }],
339
394
  [2, { name: 'never', desc: 'Never' }]
340
395
  ]);
341
-
342
396
  public pumpTypes: byteValueMap = new byteValueMap([
343
397
  [1, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }],
344
398
  [64, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }],
@@ -384,12 +438,12 @@ export class byteValueMaps {
384
438
  ]);
385
439
  public heaterTypes: byteValueMap = new byteValueMap([
386
440
  [1, { name: 'gas', desc: 'Gas Heater', hasAddress: false }],
387
- [2, { name: 'solar', desc: 'Solar Heater', hasAddress: false, hasCoolSetpoint: true }],
388
- [3, { name: 'heatpump', desc: 'Heat Pump', hasAddress: true }],
389
- [4, { name: 'ultratemp', desc: 'UltraTemp', hasAddress: true, hasCoolSetpoint: true }],
441
+ [2, { name: 'solar', desc: 'Solar Heater', hasAddress: false, hasCoolSetpoint: true, hasPreference: true }],
442
+ [3, { name: 'heatpump', desc: 'Heat Pump', hasAddress: true, hasPreference: true }],
443
+ [4, { name: 'ultratemp', desc: 'UltraTemp', hasAddress: true, hasCoolSetpoint: true, hasPreference: true }],
390
444
  [5, { name: 'hybrid', desc: 'Hybrid', hasAddress: true }],
391
- [6, { name: 'maxetherm', desc: 'Max-E-Therm', hasAddress: true }],
392
- [7, { name: 'mastertemp', desc: 'MasterTemp', hasAddress: true }]
445
+ [6, { name: 'mastertemp', desc: 'MasterTemp', hasAddress: true }],
446
+ [7, { name: 'maxetherm', desc: 'Max-E-Therm', hasAddress: true }],
393
447
  ]);
394
448
  public heatModes: byteValueMap = new byteValueMap([
395
449
  [0, { name: 'off', desc: 'Off' }],
@@ -408,7 +462,8 @@ export class byteValueMaps {
408
462
  [0, { name: 'off', desc: 'Off' }],
409
463
  [1, { name: 'heater', desc: 'Heater' }],
410
464
  [2, { name: 'solar', desc: 'Solar' }],
411
- [3, { name: 'cooling', desc: 'Cooling' }]
465
+ [3, { name: 'cooling', desc: 'Cooling' }],
466
+ [128, { name: 'cooldown', desc: 'Cooldown' }]
412
467
  ]);
413
468
  public pumpStatus: byteValueMap = new byteValueMap([
414
469
  [0, { name: 'off', desc: 'Off' }], // When the pump is disconnected or has no power then we simply report off as the status. This is not the recommended wiring
@@ -507,13 +562,12 @@ export class byteValueMaps {
507
562
  [0, { name: 'standard', desc: 'Standard' }],
508
563
  [1, { name: 'intellivalve', desc: 'IntelliValve' }]
509
564
  ]);
510
- public intellibriteActions: byteValueMap = new byteValueMap([
511
- [0, { name: 'ready', desc: 'Ready' }],
512
- [1, { name: 'sync', desc: 'Synchronizing' }],
513
- [2, { name: 'set', desc: 'Sequencing Set Operation' }],
514
- [3, { name: 'swim', desc: 'Sequencing Swim Operation' }],
515
- [4, { name: 'color', desc: 'Sequencing Theme/Color Operation' }],
516
- [5, { name: 'other', desc: 'Sequencing Save/Recall Operation' }]
565
+ public valveModes: byteValueMap = new byteValueMap([
566
+ [0, { name: 'off', desc: 'Off' }],
567
+ [1, { name: 'pool', desc: 'Pool' }],
568
+ [2, { name: 'spa', dest: 'Spa' }],
569
+ [3, { name: 'spillway', desc: 'Spillway' }],
570
+ [4, { name: 'spadrain', desc: 'Spa Drain' }]
517
571
  ]);
518
572
  public msgBroadcastActions: byteValueMap = new byteValueMap([
519
573
  [2, { name: 'status', desc: 'Equipment Status' }],
@@ -564,6 +618,18 @@ export class byteValueMaps {
564
618
  [0, { name: 'base', desc: 'Base pH+' }],
565
619
  [1, { name: 'acid', desc: 'Acid pH-' }]
566
620
  ]);
621
+ public phDoserTypes: byteValueMap = new byteValueMap([
622
+ [0, { name: 'none', desc: 'No Doser Attached' }],
623
+ [1, { name: 'extrelay', desc: 'External Relay' }],
624
+ [2, { name: 'co2', desc: 'CO2 Tank' }],
625
+ [3, { name: 'intrelay', desc: 'Internal Relay'}]
626
+ ]);
627
+ public orpDoserTypes: byteValueMap = new byteValueMap([
628
+ [0, { name: 'none', desc: 'No Doser Attached' }],
629
+ [1, { name: 'extrelay', desc: 'External Relay' }],
630
+ [2, { name: 'chlorinator', desc: 'Chlorinator'}],
631
+ [3, { name: 'intrelay', desc: 'Internal Relay'}]
632
+ ])
567
633
  public volumeUnits: byteValueMap = new byteValueMap([
568
634
  [0, { name: '', desc: 'No Units' }],
569
635
  [1, { name: 'gal', desc: 'Gallons' }],
@@ -603,7 +669,8 @@ export class byteValueMaps {
603
669
  [64, { name: 'orptankempty', desc: 'orp Tank Empty' }],
604
670
  [128, { name: 'probefault', desc: 'Probe Fault' }],
605
671
  [129, { name: 'phtanklow', desc: 'pH Tank Low' }],
606
- [130, { name: 'orptanklow', desc: 'orp Tank Low' }]
672
+ [130, { name: 'orptanklow', desc: 'orp Tank Low' }],
673
+ [131, { name: 'freezeprotect', desc: 'Freeze Protection Lockout'}]
607
674
  ]);
608
675
  public chemControllerHardwareFaults: byteValueMap = new byteValueMap([
609
676
  [0, { name: 'ok', desc: 'Ok - No Faults' }],
@@ -614,6 +681,7 @@ export class byteValueMaps {
614
681
  [5, { name: 'chlormismatch', desc: 'Chlorinator body mismatch' }],
615
682
  [6, { name: 'invalidbody', desc: 'Body capacity not valid' }],
616
683
  [7, { name: 'flowsensor', desc: 'Flow Sensor Fault' }]
684
+
617
685
  ]);
618
686
  public chemControllerWarnings: byteValueMap = new byteValueMap([
619
687
  [0, { name: 'ok', desc: 'Ok - No Warning' }],
@@ -749,7 +817,7 @@ export class SystemBoard {
749
817
  // turn off chlor
750
818
  console.log(`Stopping sys`);
751
819
  //sys.board.virtualChlorinatorController.stop();
752
- if (sys.controllerType === ControllerType.Virtual) this.turnOffAllCircuits();
820
+ if (sys.controllerType === ControllerType.Nixie) this.turnOffAllCircuits();
753
821
  // sys.board.virtualChemControllers.stop();
754
822
  this.killStatusCheck();
755
823
  await ncp.closeAsync();
@@ -758,16 +826,20 @@ export class SystemBoard {
758
826
  public async turnOffAllCircuits() {
759
827
  // turn off all circuits/features
760
828
  for (let i = 0; i < state.circuits.length; i++) {
761
- state.circuits.getItemByIndex(i).isOn = false;
829
+ let s = state.circuits.getItemByIndex(i);
830
+ await sys.board.circuits.setCircuitStateAsync(s.id, false);
762
831
  }
763
832
  for (let i = 0; i < state.features.length; i++) {
764
- state.features.getItemByIndex(i).isOn = false;
833
+ let s = state.features.getItemByIndex(i);
834
+ await sys.board.features.setFeatureStateAsync(s.id, false);
765
835
  }
766
836
  for (let i = 0; i < state.lightGroups.length; i++) {
767
- state.lightGroups.getItemByIndex(i).isOn = false;
837
+ let s = state.lightGroups.getItemByIndex(i);
838
+ await sys.board.circuits.setCircuitStateAsync(s.id, false);
768
839
  }
769
840
  for (let i = 0; i < state.temps.bodies.length; i++) {
770
- state.temps.bodies.getItemByIndex(i).isOn = false;
841
+ let s = state.temps.bodies.getItemByIndex(i);
842
+ await sys.board.circuits.setCircuitStateAsync(s.id, false);
771
843
  }
772
844
  // sys.board.virtualPumpControllers.setTargetSpeed();
773
845
  state.emitEquipmentChanges();
@@ -989,14 +1061,15 @@ export class SystemCommands extends BoardCommands {
989
1061
  ctx.filters = await sys.board.filters.validateRestore(rest);
990
1062
  ctx.schedules = await sys.board.schedules.validateRestore(rest);
991
1063
  }
992
- else ctx.board.errors.push(`Panel Types do not match cannot restore bakup from ${sys.controllerType} to ${rest.poolConfig.controllerType}`);
1064
+ else ctx.board.errors.push(`Panel Types do not match cannot restore backup from ${sys.controllerType} to ${rest.poolConfig.controllerType}`);
993
1065
 
994
1066
  return ctx;
995
1067
 
996
- } catch (err) { logger.error(`Error validating restore file: ${err.message}`); return Promise.reject(err);}
1068
+ } catch (err) { logger.error(`Error validating restore file: ${err.message}`); return Promise.reject(err); }
997
1069
 
998
1070
  }
999
1071
  public cancelDelay(): Promise<any> { state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); }
1072
+ public setManualOperationPriority(id: number): Promise<any> { return Promise.resolve(); }
1000
1073
  public setDateTimeAsync(obj: any): Promise<any> { return Promise.resolve(); }
1001
1074
  public keepManualTime() {
1002
1075
  try {
@@ -1019,12 +1092,11 @@ export class SystemCommands extends BoardCommands {
1019
1092
  }
1020
1093
  public getDOW() { return this.board.valueMaps.scheduleDays.toArray(); }
1021
1094
  public async setGeneralAsync(obj: any): Promise<General> {
1022
- let general = sys.general.get();
1023
1095
  if (typeof obj.alias === 'string') sys.general.alias = obj.alias;
1024
1096
  if (typeof obj.options !== 'undefined') await sys.board.system.setOptionsAsync(obj.options);
1025
1097
  if (typeof obj.location !== 'undefined') await sys.board.system.setLocationAsync(obj.location);
1026
1098
  if (typeof obj.owner !== 'undefined') await sys.board.system.setOwnerAsync(obj.owner);
1027
- return new Promise<General>(function (resolve, reject) { resolve(sys.general); });
1099
+ return sys.general;
1028
1100
  }
1029
1101
  public async setTempSensorsAsync(obj: any): Promise<TempSensorCollection> {
1030
1102
  if (typeof obj.waterTempAdj1 != 'undefined' && obj.waterTempAdj1 !== sys.equipment.tempSensors.getCalibration('water1')) {
@@ -1054,7 +1126,7 @@ export class SystemCommands extends BoardCommands {
1054
1126
  if (typeof obj.airTempAdj != 'undefined' && obj.airTempAdj !== sys.equipment.tempSensors.getCalibration('air')) {
1055
1127
  sys.equipment.tempSensors.setCalibration('air', parseFloat(obj.airTempAdj));
1056
1128
  }
1057
- return new Promise<TempSensorCollection>((resolve, reject) => { resolve(sys.equipment.tempSensors); });
1129
+ return sys.equipment.tempSensors;
1058
1130
  }
1059
1131
  public async setOptionsAsync(obj: any): Promise<Options> {
1060
1132
  if (obj.clockSource === 'server') sys.board.system.setTZ();
@@ -1063,15 +1135,15 @@ export class SystemCommands extends BoardCommands {
1063
1135
  let bodyUnits = sys.general.options.units === 0 ? 1 : 2;
1064
1136
  for (let i = 0; i < sys.bodies.length; i++) sys.bodies.getItemByIndex(i).capacityUnits = bodyUnits;
1065
1137
  state.temps.units = sys.general.options.units === 0 ? 1 : 4;
1066
- return new Promise<Options>(function (resolve, reject) { resolve(sys.general.options); });
1138
+ return sys.general.options;
1067
1139
  }
1068
1140
  public async setLocationAsync(obj: any): Promise<Location> {
1069
1141
  sys.general.location.set(obj);
1070
- return new Promise<Location>(function (resolve, reject) { resolve(sys.general.location); });
1142
+ return sys.general.location;
1071
1143
  }
1072
1144
  public async setOwnerAsync(obj: any): Promise<Owner> {
1073
1145
  sys.general.owner.set(obj);
1074
- return new Promise<Owner>(function (resolve, reject) { resolve(sys.general.owner); });
1146
+ return sys.general.owner;
1075
1147
  }
1076
1148
  public async setTempsAsync(obj: any): Promise<TemperatureState> {
1077
1149
  return new Promise<TemperatureState>((resolve, reject) => {
@@ -1330,7 +1402,12 @@ export class BodyCommands extends BoardCommands {
1330
1402
  let freeze = utils.makeBool(state.freeze);
1331
1403
  if (sys.controllerType === ControllerType.Nixie) {
1332
1404
  // If we are a Nixie controller we need to evaluate the current freeze settings against the air temperature.
1333
- if (typeof state.temps.air !== 'undefined') freeze = state.temps.air <= sys.general.options.freezeThreshold;
1405
+ if (typeof state.temps.air !== 'undefined') {
1406
+ // Start freeze protection when the temperature is <= the threshold but don't stop it until we are 2 degrees above the threshold. This
1407
+ // makes for a 3 degree offset.
1408
+ if (state.temps.air <= sys.general.options.freezeThreshold) freeze = true;
1409
+ else if (state.freeze && state.temps.air - 2 > sys.general.options.freezeThreshold) freeze = false;
1410
+ }
1334
1411
  else freeze = false;
1335
1412
 
1336
1413
  // We need to know when we first turned the freeze protection on. This is because we will be rotating between pool and spa
@@ -1422,7 +1499,7 @@ export class BodyCommands extends BoardCommands {
1422
1499
  else if (freeze && !cstate.isOn) {
1423
1500
  // This circuit should be on because we are freezing.
1424
1501
  cstate.freezeProtect = true;
1425
- await sys.board.features.setFeatureStateAsync(circ.id, true);
1502
+ await sys.board.circuits.setCircuitStateAsync(circ.id, true);
1426
1503
  }
1427
1504
  else if (!freeze && cstate.freezeProtect) {
1428
1505
  // This feature was turned on by freeze protection. We need to turn it off because it has warmed up.
@@ -1431,7 +1508,7 @@ export class BodyCommands extends BoardCommands {
1431
1508
  }
1432
1509
  }
1433
1510
  }
1434
- catch (err) { logger.error(`syncFreezeProtection: Error synchronizing freeze protection states`); }
1511
+ catch (err) { logger.error(`syncFreezeProtection: Error synchronizing freeze protection states: ${err.message}`); }
1435
1512
  }
1436
1513
 
1437
1514
  public async initFilters() {
@@ -1477,7 +1554,10 @@ export class BodyCommands extends BoardCommands {
1477
1554
  let id = parseInt(obj.id, 10); 1
1478
1555
  if (isNaN(id)) reject(new InvalidEquipmentIdError('Body Id has not been defined', obj.id, 'Body'));
1479
1556
  let body = sys.bodies.getItemById(id, false);
1557
+ let sbody = state.temps.bodies.getItemById(id, false);
1480
1558
  body.set(obj);
1559
+ sbody.name = body.name;
1560
+ sbody.showInDashboard = body.showInDashboard;
1481
1561
  resolve(body);
1482
1562
  });
1483
1563
  }
@@ -1570,54 +1650,57 @@ export class BodyCommands extends BoardCommands {
1570
1650
  sys.board.heaters.syncHeaterStates();
1571
1651
  return Promise.resolve(bstate);
1572
1652
  }
1573
- public getHeatSources(bodyId: number) {
1574
- let heatSources = [];
1575
- let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1576
- heatSources.push(this.board.valueMaps.heatSources.transformByName('nochange'));
1577
- if (heatTypes.total > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('off'));
1578
- if (heatTypes.gas > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('heater'));
1579
- if (heatTypes.mastertemp > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('mastertemp'));
1580
- if (heatTypes.solar > 0) {
1581
- let hm = this.board.valueMaps.heatSources.transformByName('solar');
1582
- heatSources.push(hm);
1583
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('solarpref'));
1584
- }
1585
- if (heatTypes.heatpump > 0) {
1586
- let hm = this.board.valueMaps.heatSources.transformByName('heatpump');
1587
- heatSources.push(hm);
1588
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('heatpumppref'));
1589
- }
1590
- if (heatTypes.ultratemp > 0) {
1591
- let hm = this.board.valueMaps.heatSources.transformByName('ultratemp');
1592
- heatSources.push(hm);
1593
- if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('ultratemppref'));
1594
- }
1595
- return heatSources;
1653
+ public getHeatSources(bodyId: number) {
1654
+ let heatSources = [];
1655
+ let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1656
+ heatSources.push(this.board.valueMaps.heatSources.transformByName('nochange'));
1657
+ if (heatTypes.total > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('off'));
1658
+ if (heatTypes.gas > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('heater'));
1659
+ if (heatTypes.mastertemp > 0) heatSources.push(this.board.valueMaps.heatSources.transformByName('mastertemp'));
1660
+ if (heatTypes.solar > 0) {
1661
+ let hm = this.board.valueMaps.heatSources.transformByName('solar');
1662
+ heatSources.push(hm);
1663
+ if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('solarpref'));
1596
1664
  }
1597
- public getHeatModes(bodyId: number) {
1598
- let heatModes = [];
1599
- // RKS: 09-26-20 This will need to be overloaded in IntelliCenterBoard when the other heater types are identified. (e.g. ultratemp, hybrid, maxetherm, and mastertemp)
1600
- heatModes.push(this.board.valueMaps.heatModes.transformByName('off')); // In IC fw 1.047 off is no longer 0.
1601
- let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1602
- if (heatTypes.gas > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('heater'));
1603
- if (heatTypes.mastertemp > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('mastertemp'));
1604
- if (heatTypes.solar > 0) {
1605
- let hm = this.board.valueMaps.heatModes.transformByName('solar');
1606
- heatModes.push(hm);
1607
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('solarpref'));
1608
- }
1609
- if (heatTypes.heatpump > 0) {
1610
- let hm = this.board.valueMaps.heatModes.transformByName('heatpump');
1611
- heatModes.push(hm);
1612
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('heatpumppref'));
1613
- }
1614
- if (heatTypes.ultratemp > 0) {
1615
- let hm = this.board.valueMaps.heatModes.transformByName('ultratemp');
1616
- heatModes.push(hm);
1617
- if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('ultratemppref'));
1618
- }
1619
- return heatModes;
1665
+ if (heatTypes.heatpump > 0) {
1666
+ let hm = this.board.valueMaps.heatSources.transformByName('heatpump');
1667
+ heatSources.push(hm);
1668
+ if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('heatpumppref'));
1669
+ }
1670
+ if (heatTypes.ultratemp > 0) {
1671
+ let hm = this.board.valueMaps.heatSources.transformByName('ultratemp');
1672
+ heatSources.push(hm);
1673
+ if (heatTypes.total > 1) heatSources.push(this.board.valueMaps.heatSources.transformByName('ultratemppref'));
1674
+ }
1675
+ return heatSources;
1676
+ }
1677
+ public getHeatModes(bodyId: number) {
1678
+ let heatModes = [];
1679
+ sys.board.heaters.updateHeaterServices();
1680
+
1681
+ // RKS: 09-26-20 This will need to be overloaded in IntelliCenterBoard when the other heater types are identified. (e.g. ultratemp, hybrid, maxetherm, and mastertemp)
1682
+ heatModes.push(this.board.valueMaps.heatModes.transformByName('off')); // In IC fw 1.047 off is no longer 0.
1683
+ let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId);
1684
+ if (heatTypes.hybrid > 0) heatModes = this.board.valueMaps.heatModes.toArray();
1685
+ if (heatTypes.gas > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('heater'));
1686
+ if (heatTypes.mastertemp > 0) heatModes.push(this.board.valueMaps.heatModes.transformByName('mtheater'));
1687
+ if (heatTypes.solar > 0) {
1688
+ let hm = this.board.valueMaps.heatModes.transformByName('solar');
1689
+ heatModes.push(hm);
1690
+ if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('solarpref'));
1691
+ }
1692
+ if (heatTypes.heatpump > 0) {
1693
+ let hm = this.board.valueMaps.heatModes.transformByName('heatpump');
1694
+ heatModes.push(hm);
1695
+ if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('heatpumppref'));
1696
+ }
1697
+ if (heatTypes.ultratemp > 0) {
1698
+ let hm = this.board.valueMaps.heatModes.transformByName('ultratemp');
1699
+ heatModes.push(hm);
1700
+ if (heatTypes.total > 1) heatModes.push(this.board.valueMaps.heatModes.transformByName('ultratemppref'));
1620
1701
  }
1702
+ return heatModes;
1703
+ }
1621
1704
  public getPoolStates(): BodyTempState[] {
1622
1705
  let arrPools = [];
1623
1706
  for (let i = 0; i < state.temps.bodies.length; i++) {
@@ -1676,8 +1759,9 @@ export class BodyCommands extends BoardCommands {
1676
1759
  case 'body4':
1677
1760
  return state.temps.bodies.getItemById(4).isOn;
1678
1761
  case 'poolspa':
1679
- if (sys.equipment.shared && sys.equipment.maxBodies >= 2)
1680
- return state.temps.bodies.getItemById(1).isOn || state.temps.bodies.getItemById(2).isOn;
1762
+ if (sys.equipment.shared && sys.equipment.maxBodies >= 2) {
1763
+ return state.temps.bodies.getItemById(1).isOn === true || state.temps.bodies.getItemById(2).isOn === true;
1764
+ }
1681
1765
  else
1682
1766
  return state.temps.bodies.getItemById(1).isOn;
1683
1767
  }
@@ -1857,6 +1941,7 @@ export class PumpCommands extends BoardCommands {
1857
1941
  _availCircuits.push({ type: 'none', id: 255, name: 'Remove' });
1858
1942
  return _availCircuits;
1859
1943
  }
1944
+ public setPumpValveDelays(circuitIds: number[], delay?: number) {}
1860
1945
  }
1861
1946
  export class CircuitCommands extends BoardCommands {
1862
1947
  public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
@@ -1918,7 +2003,7 @@ export class CircuitCommands extends BoardCommands {
1918
2003
  res.addModuleSuccess('circuitGroup', `Update: ${c.id}-${c.name}`);
1919
2004
  } catch (err) { res.addModuleError('circuitGroup', `Update: ${c.id}-${c.name}: ${err.message}`); }
1920
2005
  }
1921
- for (let i = 0; i < ctx.lightGroups.add.length; i++) {
2006
+ for (let i = 0; i < ctx.lightGroups.update.length; i++) {
1922
2007
  let c = ctx.lightGroups.update[i];
1923
2008
  try {
1924
2009
  await sys.board.circuits.setLightGroupAsync(c);
@@ -1982,14 +2067,14 @@ export class CircuitCommands extends BoardCommands {
1982
2067
  for (let i = 0; i < sys.circuits.length; i++) {
1983
2068
  let c = sys.circuits.getItemByIndex(i);
1984
2069
  let cstate = state.circuits.getItemByIndex(i);
1985
- if (!cstate.isActive || !cstate.isOn) continue;
2070
+ if (!cstate.isActive || !cstate.isOn || typeof cstate.endTime === 'undefined') continue;
1986
2071
  if (c.master === 1) {
1987
2072
  await ncp.circuits.checkCircuitEggTimerExpirationAsync(cstate);
1988
2073
  }
1989
2074
  }
1990
2075
  for (let i = 0; i < sys.features.length; i++) {
1991
2076
  let fstate = state.features.getItemByIndex(i);
1992
- if (!fstate.isActive || !fstate.isOn) continue;
2077
+ if (!fstate.isActive || !fstate.isOn || typeof fstate.endTime === 'undefined') continue;
1993
2078
  if (fstate.endTime.toDate() < new Timestamp().toDate()) {
1994
2079
  await sys.board.circuits.setCircuitStateAsync(fstate.id, false);
1995
2080
  fstate.emitEquipmentChange();
@@ -1997,7 +2082,7 @@ export class CircuitCommands extends BoardCommands {
1997
2082
  }
1998
2083
  for (let i = 0; i < sys.circuitGroups.length; i++) {
1999
2084
  let cgstate = state.circuitGroups.getItemByIndex(i);
2000
- if (!cgstate.isActive || !cgstate.isOn) continue;
2085
+ if (!cgstate.isActive || !cgstate.isOn || typeof cgstate.endTime === 'undefined') continue;
2001
2086
  if (cgstate.endTime.toDate() < new Timestamp().toDate()) {
2002
2087
  await sys.board.circuits.setCircuitGroupStateAsync(cgstate.id, false);
2003
2088
  cgstate.emitEquipmentChange();
@@ -2005,7 +2090,7 @@ export class CircuitCommands extends BoardCommands {
2005
2090
  }
2006
2091
  for (let i = 0; i < sys.lightGroups.length; i++) {
2007
2092
  let lgstate = state.lightGroups.getItemByIndex(i);
2008
- if (!lgstate.isActive || !lgstate.isOn) continue;
2093
+ if (!lgstate.isActive || !lgstate.isOn || typeof lgstate.endTime === 'undefined') continue;
2009
2094
  if (lgstate.endTime.toDate() < new Timestamp().toDate()) {
2010
2095
  await sys.board.circuits.setLightGroupStateAsync(lgstate.id, false);
2011
2096
  lgstate.emitEquipmentChange();
@@ -2050,8 +2135,13 @@ export class CircuitCommands extends BoardCommands {
2050
2135
  }
2051
2136
  if (!remove) {
2052
2137
  // Determine whether the pool heater is on.
2053
- for (let j = 0; j < poolStates.length; j++)
2054
- if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'heater') bState = true;
2138
+ for (let j = 0; j < poolStates.length; j++) {
2139
+ if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'heater') {
2140
+ // In this instance we may have a delay underway.
2141
+ let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name !== 'solar');
2142
+ bState = typeof hstate === 'undefined';
2143
+ }
2144
+ }
2055
2145
  }
2056
2146
  break;
2057
2147
  case 'spaHeater':
@@ -2062,6 +2152,34 @@ export class CircuitCommands extends BoardCommands {
2062
2152
  if (!remove) {
2063
2153
  // Determine whether the spa heater is on.
2064
2154
  for (let j = 0; j < spaStates.length; j++) {
2155
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') {
2156
+ // In this instance we may have a delay underway.
2157
+ let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name !== 'solar');
2158
+ bState = typeof hstate === 'undefined';
2159
+ }
2160
+ }
2161
+ //for (let j = 0; j < spaStates.length; j++) {
2162
+ // if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') bState = true;
2163
+ //}
2164
+ }
2165
+ break;
2166
+ case 'heater':
2167
+ // If heater is on for any body
2168
+ // RSG 5-3-22: Heater will now refer to any poolHeater or spaHeater but not solar or other types. anyHeater now takes that role.
2169
+ remove = true;
2170
+ for (let j = 0; j < poolStates.length; j++) {
2171
+ if (poolStates[j].heaterOptions.solar + poolStates[j].heaterOptions.heatpump > 0) remove = false;
2172
+ }
2173
+ if (remove) {
2174
+ for (let j = 0; j < spaStates.length; j++) {
2175
+ if (spaStates[j].heaterOptions.solar + spaStates[j].heaterOptions.heatpump > 0) remove = false;
2176
+ }
2177
+ }
2178
+ if (!remove) {
2179
+ for (let j = 0; j < poolStates.length && !bState; j++) {
2180
+ if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'heater') bState = true;
2181
+ }
2182
+ for (let j = 0; j < spaStates.length && !bState; j++) {
2065
2183
  if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'heater') bState = true;
2066
2184
  }
2067
2185
  }
@@ -2100,7 +2218,110 @@ export class CircuitCommands extends BoardCommands {
2100
2218
  }
2101
2219
  }
2102
2220
  break;
2103
- case 'heater':
2221
+ case 'solar1':
2222
+ remove = true;
2223
+ for (let j = 0; j < poolStates.length; j++) {
2224
+ if (poolStates[j].id === 1 && poolStates[j].heaterOptions.solar) {
2225
+ remove = false;
2226
+ vc.desc = `${poolStates[j].name} Solar`;
2227
+ if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') {
2228
+ // In this instance we may have a delay underway.
2229
+ let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name === 'solar');
2230
+ bState = typeof hstate === 'undefined';
2231
+ }
2232
+ }
2233
+ }
2234
+ for (let j = 0; j < spaStates.length; j++) {
2235
+ if (spaStates[j].id === 1 && spaStates[j].heaterOptions.solar) {
2236
+ remove = false;
2237
+ vc.desc = `${spaStates[j].name} Solar`;
2238
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2239
+ // In this instance we may have a delay underway.
2240
+ let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name === 'solar');
2241
+ bState = typeof hstate === 'undefined';
2242
+ }
2243
+ }
2244
+ }
2245
+
2246
+ break;
2247
+ case 'solar2':
2248
+ remove = true;
2249
+ for (let j = 0; j < poolStates.length; j++) {
2250
+ if (poolStates[j].id === 2 && poolStates[j].heaterOptions.solar) {
2251
+ remove = false;
2252
+ vc.desc = `${poolStates[j].name} Solar`;
2253
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2254
+ // In this instance we may have a delay underway.
2255
+ let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name === 'solar');
2256
+ bState = typeof hstate === 'undefined';
2257
+ }
2258
+ }
2259
+ }
2260
+ for (let j = 0; j < spaStates.length; j++) {
2261
+ if (spaStates[j].id === 2 && spaStates[j].heaterOptions.solar) {
2262
+ remove = false;
2263
+ vc.desc = `${spaStates[j].name} Solar`;
2264
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2265
+ // In this instance we may have a delay underway.
2266
+ let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name === 'solar');
2267
+ bState = typeof hstate === 'undefined';
2268
+ }
2269
+ }
2270
+ }
2271
+ break;
2272
+ case 'solar3':
2273
+ remove = true;
2274
+ for (let j = 0; j < poolStates.length; j++) {
2275
+ if (poolStates[j].id === 3 && poolStates[j].heaterOptions.solar) {
2276
+ remove = false;
2277
+ vc.desc = `${poolStates[j].name} Solar`;
2278
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2279
+ // In this instance we may have a delay underway.
2280
+ let hstate = state.heaters.find(x => x.bodyId === 3 && x.startupDelay === true && x.type.name === 'solar');
2281
+ bState = typeof hstate === 'undefined';
2282
+ }
2283
+ }
2284
+ }
2285
+ for (let j = 0; j < spaStates.length; j++) {
2286
+ if (spaStates[j].id === 3 && spaStates[j].heaterOptions.solar) {
2287
+ remove = false;
2288
+ vc.desc = `${spaStates[j].name} Solar`;
2289
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2290
+ // In this instance we may have a delay underway.
2291
+ let hstate = state.heaters.find(x => x.bodyId === 3 && x.startupDelay === true && x.type.name === 'solar');
2292
+ bState = typeof hstate === 'undefined';
2293
+ }
2294
+ }
2295
+ }
2296
+
2297
+ break;
2298
+ case 'solar4':
2299
+ remove = true;
2300
+ for (let j = 0; j < poolStates.length; j++) {
2301
+ if (poolStates[j].id === 4 && poolStates[j].heaterOptions.solar) {
2302
+ remove = false;
2303
+ vc.desc = `${poolStates[j].name} Solar`;
2304
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2305
+ // In this instance we may have a delay underway.
2306
+ let hstate = state.heaters.find(x => x.bodyId === 4 && x.startupDelay === true && x.type.name === 'solar');
2307
+ bState = typeof hstate === 'undefined';
2308
+ }
2309
+ }
2310
+ }
2311
+ for (let j = 0; j < spaStates.length; j++) {
2312
+ if (spaStates[j].id === 4 && spaStates[j].heaterOptions.solar) {
2313
+ remove = false;
2314
+ vc.desc = `${spaStates[j].name} Solar`;
2315
+ if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') {
2316
+ // In this instance we may have a delay underway.
2317
+ let hstate = state.heaters.find(x => x.bodyId === 4 && x.startupDelay === true && x.type.name === 'solar');
2318
+ bState = typeof hstate === 'undefined';
2319
+ }
2320
+ }
2321
+ }
2322
+ break;
2323
+ case 'anyHeater':
2324
+ // RSG 5-3-22 anyHeater now represents any solar, gas, etc heater. This replaces 'heater' which now refers to only gas heaters.
2104
2325
  remove = true;
2105
2326
  for (let j = 0; j < poolStates.length; j++) {
2106
2327
  if (poolStates[j].heaterOptions.total > 0) remove = false;
@@ -2125,10 +2346,17 @@ export class CircuitCommands extends BoardCommands {
2125
2346
  remove = true;
2126
2347
  break;
2127
2348
  }
2128
- if (remove)
2349
+ if (remove) {
2350
+ if (state.virtualCircuits.exists(x => vc.val === x.id)) {
2351
+ cstate = state.virtualCircuits.getItemById(vc.val, true);
2352
+ cstate.isActive = false;
2353
+ cstate.emitEquipmentChange();
2354
+ }
2129
2355
  state.virtualCircuits.removeItemById(vc.val);
2356
+ }
2130
2357
  else {
2131
2358
  cstate = state.virtualCircuits.getItemById(vc.val, true);
2359
+ cstate.isActive = true;
2132
2360
  if (cstate !== null) {
2133
2361
  cstate.isOn = bState;
2134
2362
  cstate.type = vc.val;
@@ -2138,7 +2366,7 @@ export class CircuitCommands extends BoardCommands {
2138
2366
  }
2139
2367
  } catch (err) { logger.error(`Error syncronizing virtual circuits`); }
2140
2368
  }
2141
- public async setCircuitStateAsync(id: number, val: boolean): Promise<ICircuitState> {
2369
+ public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
2142
2370
  sys.board.suspendStatus(true);
2143
2371
  try {
2144
2372
  // We need to do some routing here as it is now critical that circuits, groups, and features
@@ -2185,17 +2413,133 @@ export class CircuitCommands extends BoardCommands {
2185
2413
  let circ = state.circuits.getInterfaceById(id);
2186
2414
  return await this.setCircuitStateAsync(id, !(circ.isOn || false));
2187
2415
  }
2188
- public async setLightThemeAsync(id: number, theme: number) {
2416
+ public async runLightGroupCommandAsync(obj: any): Promise<ICircuitState> {
2417
+ // Do all our validation.
2418
+ try {
2419
+ let id = parseInt(obj.id, 10);
2420
+ let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightGroupCommands.findItem(obj.command) : { val: 0, name: 'undefined' };
2421
+ if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light group command ${cmd.name} does not exist`, 'runLightGroupCommandAsync'));
2422
+ if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light group ${id} does not exist`, 'runLightGroupCommandAsync'));
2423
+ let grp = sys.lightGroups.getItemById(id);
2424
+ let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name);
2425
+ let sgrp = state.lightGroups.getItemById(grp.id);
2426
+ sgrp.action = nop;
2427
+ sgrp.emitEquipmentChange();
2428
+ // So here we are now we can run the command against all lights in the group that match the command so get a list of the lights.
2429
+ let arrCircs = [];
2430
+ for (let i = 0; i < grp.circuits.length; i++) {
2431
+ let circ = sys.circuits.getItemById(grp.circuits.getItemByIndex(i).circuit);
2432
+ let type = sys.board.valueMaps.circuitFunctions.transform(circ.type);
2433
+ if (type.isLight && cmd.types.includes(type.theme)) arrCircs.push(circ);
2434
+ }
2435
+ // So now we should hav a complete list of the lights that are part of the command list so start them off on their sequence. We want all the lights
2436
+ // to be doing their thing at the same time so in the lieu of threads we will ceate a promise all.
2437
+ let proms = [];
2438
+ for (let i = 0; i < arrCircs.length; i++) {
2439
+ await ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence);
2440
+ //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence));
2441
+ }
2442
+ for (let i = 0; i < arrCircs.length; i++) {
2443
+ await sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, false);
2444
+ //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence));
2445
+ }
2446
+ await utils.sleep(10000);
2447
+ for (let i = 0; i < arrCircs.length; i++) {
2448
+ await sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, true);
2449
+ //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence));
2450
+ }
2451
+
2452
+ //if (proms.length > 0) {
2453
+ // //await Promise.all(proms);
2454
+ // // Let it simmer for 6 seconds then turn it off and back on.
2455
+ // proms.length = 0;
2456
+ // for (let i = 0; i < arrCircs.length; i++) {
2457
+ // proms.push(sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, false));
2458
+ // }
2459
+ // await Promise.all(proms);
2460
+ // // Let it be off for 3 seconds then turn it back on.
2461
+ // await utils.sleep(10000);
2462
+ // proms.length = 0;
2463
+ // for (let i = 0; i < arrCircs.length; i++) {
2464
+ // proms.push(sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, true));
2465
+ // }
2466
+ // await Promise.all(proms);
2467
+ //}
2468
+ sgrp.action = 0;
2469
+ sgrp.emitEquipmentChange();
2470
+ return state.lightGroups.getItemById(id);
2471
+ }
2472
+ catch (err) { return Promise.reject(`Error runLightGroupCommandAsync ${err.message}`); }
2473
+ }
2474
+ public async runLightCommandAsync(obj: any): Promise<ICircuitState> {
2475
+ // Do all our validation.
2476
+ try {
2477
+ let id = parseInt(obj.id, 10);
2478
+ let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightCommands.findItem(obj.command) : { val: 0, name: 'undefined' };
2479
+ if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light command ${cmd.name} does not exist`, 'runLightCommandAsync'));
2480
+ if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light ${id} does not exist`, 'runLightCommandAsync'));
2481
+ let circ = sys.circuits.getItemById(id);
2482
+ if (!circ.isActive) return Promise.reject(new InvalidOperationError(`Light circuit #${id} is not active`, 'runLightCommandAsync'));
2483
+ let type = sys.board.valueMaps.circuitFunctions.transform(circ.type);
2484
+ if (!type.isLight) return Promise.reject(new InvalidOperationError(`Circuit #${id} is not a light`, 'runLightCommandAsync'));
2485
+ let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name);
2486
+ let slight = state.circuits.getItemById(circ.id);
2487
+ slight.action = nop;
2488
+ console.log(nop);
2489
+ slight.emitEquipmentChange();
2490
+ await ncp.circuits.sendOnOffSequenceAsync(circ.id, cmd.sequence);
2491
+ await utils.sleep(7000);
2492
+ await sys.board.circuits.setCircuitStateAsync(circ.id, false);
2493
+ await sys.board.circuits.setCircuitStateAsync(circ.id, true);
2494
+ slight.action = 0;
2495
+ slight.emitEquipmentChange();
2496
+ return slight;
2497
+ }
2498
+ catch (err) { return Promise.reject(`Error runLightCommandAsync ${err.message}`); }
2499
+ }
2500
+ public async setLightThemeAsync(id: number, theme: number): Promise<ICircuitState> {
2189
2501
  let cstate = state.circuits.getItemById(id);
2190
2502
  let circ = sys.circuits.getItemById(id);
2191
2503
  let thm = sys.board.valueMaps.lightThemes.findItem(theme);
2192
- if (typeof thm !== 'undefined' && typeof thm.sequence !== 'undefined' && circ.master === 1) {
2504
+ let nop = sys.board.valueMaps.circuitActions.getValue('lighttheme');
2505
+ cstate.action = nop;
2506
+ cstate.emitEquipmentChange();
2507
+ try {
2508
+ if (typeof thm !== 'undefined' && typeof thm.sequence !== 'undefined' && circ.master === 1) {
2509
+ await sys.board.circuits.setCircuitStateAsync(id, true);
2510
+ await ncp.circuits.sendOnOffSequenceAsync(id, thm.sequence);
2511
+ }
2512
+ cstate.lightingTheme = theme;
2513
+ return cstate;
2514
+ } catch (err) { return Promise.reject(new InvalidOperationError(err.message, 'setLightThemeAsync')); }
2515
+ finally { cstate.action = 0; cstate.emitEquipmentChange(); }
2516
+ }
2517
+ public async setColorHoldAsync(id: number): Promise<ICircuitState> {
2518
+ try {
2519
+ let circ = sys.circuits.getItemById(id);
2520
+ if (!circ.isActive) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id ${id}`, id, 'circuit'));
2521
+ let cstate = state.circuits.getItemById(circ.id);
2522
+ let cmd = sys.board.valueMaps.lightCommands.findItem('colorhold');
2523
+ await sys.board.circuits.setCircuitStateAsync(id, true);
2524
+ if (circ.master === 1) await ncp.circuits.sendOnOffSequenceAsync(id, cmd.sequence);
2525
+ return cstate;
2526
+ }
2527
+ catch (err) { return Promise.reject(`Nixie: Error setColorHoldAsync ${err.message}`); }
2528
+ }
2529
+ public async setColorRecallAsync(id: number): Promise<ICircuitState> {
2530
+ try {
2531
+ let circ = sys.circuits.getItemById(id);
2532
+ if (!circ.isActive) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id ${id}`, id, 'circuit'));
2533
+ let cstate = state.circuits.getItemById(circ.id);
2534
+ let cmd = sys.board.valueMaps.lightCommands.findItem('colorrecall');
2193
2535
  await sys.board.circuits.setCircuitStateAsync(id, true);
2194
- await ncp.circuits.sendOnOffSequenceAsync(id, thm.sequence);
2536
+ if (circ.master === 1) await ncp.circuits.sendOnOffSequenceAsync(id, cmd.sequence);
2537
+ return cstate;
2195
2538
  }
2196
- cstate.lightingTheme = theme;
2197
- return Promise.resolve(cstate as ICircuitState);
2539
+ catch (err) { return Promise.reject(`Nixie: Error setColorHoldAsync ${err.message}`); }
2198
2540
  }
2541
+ public async setLightThumperAsync(id: number): Promise<ICircuitState> { return state.circuits.getItemById(id); }
2542
+
2199
2543
  public setDimmerLevelAsync(id: number, level: number): Promise<ICircuitState> {
2200
2544
  let circ = state.circuits.getItemById(id);
2201
2545
  circ.level = level;
@@ -2250,7 +2594,11 @@ export class CircuitCommands extends BoardCommands {
2250
2594
  return arrRefs;
2251
2595
  }
2252
2596
  public getLightThemes(type?: number) { return sys.board.valueMaps.lightThemes.toArray(); }
2253
- public getCircuitFunctions() { return sys.board.valueMaps.circuitFunctions.toArray(); }
2597
+ public getCircuitFunctions() {
2598
+ let cf = sys.board.valueMaps.circuitFunctions.toArray();
2599
+ if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' });
2600
+ return cf;
2601
+ }
2254
2602
  public getCircuitNames() { return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()]; }
2255
2603
  public async setCircuitAsync(data: any): Promise<ICircuit> {
2256
2604
  try {
@@ -2529,20 +2877,20 @@ export class CircuitCommands extends BoardCommands {
2529
2877
  }
2530
2878
  catch (err) { return Promise.reject(err); }
2531
2879
  }
2532
- public sequenceLightGroupAsync(id: number, operation: string): Promise<LightGroupState> {
2880
+ public async sequenceLightGroupAsync(id: number, operation: string): Promise<LightGroupState> {
2533
2881
  let sgroup = state.lightGroups.getItemById(id);
2534
- let nop = sys.board.valueMaps.intellibriteActions.getValue(operation);
2535
- if (nop > 0) {
2536
- sgroup.action = nop;
2537
- sgroup.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
2538
- state.emitEquipmentChanges();
2539
- setTimeout(function () {
2882
+ // This is the default action which really does nothing.
2883
+ try {
2884
+ let nop = sys.board.valueMaps.circuitActions.getValue(operation);
2885
+ if (nop > 0) {
2886
+ sgroup.action = nop;
2887
+ sgroup.emitEquipmentChange();
2888
+ await utils.sleep(10000);
2540
2889
  sgroup.action = 0;
2541
- sgroup.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
2542
- state.emitEquipmentChanges();
2543
- }, 20000); // It takes 20 seconds to sequence.
2544
- }
2545
- return Promise.resolve(sgroup);
2890
+ state.emitAllEquipmentChanges();
2891
+ }
2892
+ return sgroup;
2893
+ } catch (err) { return Promise.reject(new InvalidOperationError(`Error sequencing light group ${err.message}`, 'sequenceLightGroupAsync')); }
2546
2894
  }
2547
2895
  public async setCircuitGroupStateAsync(id: number, val: boolean): Promise<ICircuitGroupState> {
2548
2896
  let grp = sys.circuitGroups.getItemById(id, false, { isActive: false });
@@ -2567,7 +2915,7 @@ export class CircuitCommands extends BoardCommands {
2567
2915
  public async setLightGroupStateAsync(id: number, val: boolean): Promise<ICircuitGroupState> {
2568
2916
  return sys.board.circuits.setCircuitGroupStateAsync(id, val);
2569
2917
  }
2570
- public setEndTime(thing: ICircuit, thingState: ICircuitState, isOn: boolean, bForce: boolean= false) {
2918
+ public setEndTime(thing: ICircuit, thingState: ICircuitState, isOn: boolean, bForce: boolean = false) {
2571
2919
  /*
2572
2920
  this is a generic fn for circuits, features, circuitGroups, lightGroups
2573
2921
  to set the end time based on the egg timer.
@@ -2601,25 +2949,29 @@ export class CircuitCommands extends BoardCommands {
2601
2949
  eggTimerEndTime = state.time.clone().addHours(0, thing.eggTimer);
2602
2950
  }
2603
2951
  // egg timers don't come into play if a schedule will control the circuit
2604
- for (let i = 0; i < sys.schedules.length; i++) {
2605
- let sched = sys.schedules.getItemByIndex(i);
2606
- let ssched = state.schedules.getItemById(sched.id);
2607
- if (sched.isActive && sys.board.schedules.includesCircuit(sched, thing.id)) {
2608
- let nearestStartTime = sys.board.schedules.getNearestStartTime(sched);
2609
- let nearestEndTime = sys.board.schedules.getNearestEndTime(sched);
2610
- // if the schedule doesn't have an end date (eg no days)...
2611
- if (nearestEndTime.getTime() === 0) continue;
2612
- if (ssched.isOn) {
2613
- if (typeof endTime === 'undefined' || nearestEndTime.getTime() < endTime.getTime()) {
2614
- endTime = nearestEndTime.clone();
2615
- eggTimerEndTime = undefined;
2952
+ // schedules don't come into play if the circuit is in manualPriority
2953
+ if (!thingState.manualPriorityActive) {
2954
+
2955
+ for (let i = 0; i < sys.schedules.length; i++) {
2956
+ let sched = sys.schedules.getItemByIndex(i);
2957
+ let ssched = state.schedules.getItemById(sched.id);
2958
+ if (sched.isActive && sys.board.schedules.includesCircuit(sched, thing.id)) {
2959
+ let nearestStartTime = sys.board.schedules.getNearestStartTime(sched);
2960
+ let nearestEndTime = sys.board.schedules.getNearestEndTime(sched);
2961
+ // if the schedule doesn't have an end date (eg no days)...
2962
+ if (nearestEndTime.getTime() === 0) continue;
2963
+ if (ssched.isOn) {
2964
+ if (typeof endTime === 'undefined' || nearestEndTime.getTime() < endTime.getTime()) {
2965
+ endTime = nearestEndTime.clone();
2966
+ eggTimerEndTime = undefined;
2967
+ }
2616
2968
  }
2617
- }
2618
- else {
2619
- if (typeof eggTimerEndTime !== 'undefined' && eggTimerEndTime.getTime() < nearestStartTime.getTime()) {
2620
- if (typeof endTime === 'undefined' || eggTimerEndTime.getTime() < endTime.getTime()) endTime = eggTimerEndTime.clone();
2969
+ else {
2970
+ if (typeof eggTimerEndTime !== 'undefined' && eggTimerEndTime.getTime() < nearestStartTime.getTime()) {
2971
+ if (typeof endTime === 'undefined' || eggTimerEndTime.getTime() < endTime.getTime()) endTime = eggTimerEndTime.clone();
2972
+ }
2973
+ else if (typeof endTime === 'undefined' || nearestEndTime.getTime() < endTime.getTime()) endTime = nearestEndTime.clone();
2621
2974
  }
2622
- else if (typeof endTime === 'undefined' || nearestEndTime.getTime() < endTime.getTime()) endTime = nearestEndTime.clone();
2623
2975
  }
2624
2976
  }
2625
2977
  }
@@ -2631,8 +2983,85 @@ export class CircuitCommands extends BoardCommands {
2631
2983
  logger.error(`Error setting end time for ${thing.id}: ${err}`)
2632
2984
  }
2633
2985
  }
2986
+ public async turnOffDrainCircuits(ignoreDelays: boolean) {
2987
+ try {
2988
+ {
2989
+ let drt = sys.board.valueMaps.circuitFunctions.getValue('spadrain');
2990
+ let drains = sys.circuits.filter(x => { return x.type === drt });
2991
+ for (let i = 0; i < drains.length; i++) {
2992
+ let drain = drains.getItemByIndex(i);
2993
+ let sdrain = state.circuits.getItemById(drain.id);
2994
+ if (sdrain.isOn) await sys.board.circuits.setCircuitStateAsync(drain.id, false, ignoreDelays);
2995
+ sdrain.startDelay = false;
2996
+ sdrain.stopDelay = false;
2997
+ }
2998
+ }
2999
+ {
3000
+ let drt = sys.board.valueMaps.featureFunctions.getValue('spadrain');
3001
+ let drains = sys.features.filter(x => { return x.type === drt });
3002
+ for (let i = 0; i < drains.length; i++) {
3003
+ let drain = drains.getItemByIndex(i);
3004
+ let sdrain = state.features.getItemById(drain.id);
3005
+ if (sdrain.isOn) await sys.board.features.setFeatureStateAsync(drain.id, false, ignoreDelays);
3006
+ }
3007
+ }
3008
+
3009
+ } catch (err) { return Promise.reject(new BoardProcessError(`turnOffDrainCircuits: ${err.message}`)); }
3010
+ }
3011
+ public async turnOffCleanerCircuits(bstate: BodyTempState, ignoreDelays?: boolean) {
3012
+ try {
3013
+ // First we have to get all the cleaner circuits that are associated with the
3014
+ // body. To do this we get the circuit functions for all cleaner types associated with the body.
3015
+ //
3016
+ // Cleaner ciruits can always be turned off. However, they cannot always be turned on.
3017
+ let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('cleaner') !== -1 && x.body === bstate.id; });
3018
+ let cleaners = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
3019
+ // So now we should have all the cleaner circuits so lets make sure they are off.
3020
+ for (let i = 0; i < cleaners.length; i++) {
3021
+ let cleaner = cleaners.getItemByIndex(i);
3022
+ if (cleaner.isActive) {
3023
+ let cstate = state.circuits.getItemById(cleaner.id, true);
3024
+ if (cstate.isOn || cstate.startDelay) await sys.board.circuits.setCircuitStateAsync(cleaner.id, false, ignoreDelays);
3025
+ }
3026
+ }
3027
+ } catch (err) { return Promise.reject(new BoardProcessError(`turnOffCleanerCircuits: ${err.message}`)); }
3028
+ }
3029
+ public async turnOffSpillwayCircuits(ignoreDelays?: boolean) {
3030
+ try {
3031
+ {
3032
+ let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('spillway') !== -1 });
3033
+ let spillways = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
3034
+ // So now we should have all the cleaner circuits so lets make sure they are off.
3035
+ for (let i = 0; i < spillways.length; i++) {
3036
+ let spillway = spillways.getItemByIndex(i);
3037
+ if (spillway.isActive) {
3038
+ let cstate = state.circuits.getItemById(spillway.id, true);
3039
+ if (cstate.isOn || cstate.startDelay) await sys.board.circuits.setCircuitStateAsync(spillway.id, false, ignoreDelays);
3040
+ }
3041
+ }
3042
+ }
3043
+ {
3044
+ let arrTypes = sys.board.valueMaps.featureFunctions.toArray().filter(x => { return x.name.indexOf('spillway') !== -1 });
3045
+ let spillways = sys.features.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 });
3046
+ // So now we should have all the cleaner features so lets make sure they are off.
3047
+ for (let i = 0; i < spillways.length; i++) {
3048
+ let spillway = spillways.getItemByIndex(i);
3049
+ if (spillway.isActive) {
3050
+ let cstate = state.features.getItemById(spillway.id, true);
3051
+ if (cstate.isOn) await sys.board.features.setFeatureStateAsync(spillway.id, false, ignoreDelays);
3052
+ }
3053
+ }
3054
+ }
3055
+ } catch (err) { return Promise.reject(new BoardProcessError(`turnOffSpillwayCircuits: ${err.message}`)); }
3056
+ }
2634
3057
  }
2635
3058
  export class FeatureCommands extends BoardCommands {
3059
+ public getFeatureFunctions() {
3060
+ let cf = sys.board.valueMaps.featureFunctions.toArray();
3061
+ if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' });
3062
+ return cf;
3063
+ }
3064
+
2636
3065
  public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
2637
3066
  try {
2638
3067
  // First delete the features that should be removed.
@@ -2727,7 +3156,7 @@ export class FeatureCommands extends BoardCommands {
2727
3156
  else
2728
3157
  Promise.reject(new InvalidEquipmentIdError('Feature id has not been defined', undefined, 'Feature'));
2729
3158
  }
2730
- public async setFeatureStateAsync(id: number, val: boolean): Promise<ICircuitState> {
3159
+ public async setFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
2731
3160
  try {
2732
3161
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
2733
3162
  if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
@@ -2852,12 +3281,77 @@ export class ChlorinatorCommands extends BoardCommands {
2852
3281
  public async setChlorAsync(obj: any): Promise<ChlorinatorState> {
2853
3282
  try {
2854
3283
  let id = parseInt(obj.id, 10);
2855
- if (isNaN(id) || id <= 0) id = 1;
2856
- let cchlor = sys.chlorinators.getItemById(id, true);
2857
- await ncp.chlorinators.setChlorinatorAsync(cchlor, obj);
2858
- let schlor = state.chlorinators.getItemById(cchlor.id, true);
3284
+ let chlor: Chlorinator;
3285
+ let master = parseInt(obj.master, 10);
3286
+ let portId = typeof obj.portId !== 'undefined' ? parseInt(obj.portId, 10) : 0;
3287
+ if (isNaN(master)) master = 1; // NCP to control.
3288
+ if (isNaN(id) || id <= 0) {
3289
+ let body = sys.board.bodies.mapBodyAssociation(typeof obj.body !== 'undefined' ? parseInt(obj.body, 10) : 0);
3290
+ if (typeof body === 'undefined') {
3291
+ if (sys.equipment.shared) body = 32;
3292
+ else if (!sys.equipment.dual) body = 1;
3293
+ else return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body association is not valid: ${body}`, 'chlorinator', body));
3294
+ }
3295
+ let poolSetpoint = typeof obj.poolSetpoint !== 'undefined' ? parseInt(obj.poolSetpoint, 10) : 50;
3296
+ let spaSetpoint = typeof obj.spaSetpoint !== 'undefined' ? parseInt(obj.spaSetpoint, 10) : 10;
3297
+ if (isNaN(poolSetpoint) || poolSetpoint > 100 || poolSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator poolSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', poolSetpoint));
3298
+ if (isNaN(spaSetpoint) || spaSetpoint > 100 || spaSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator spaSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', spaSetpoint));
3299
+ if (master === 2) {
3300
+ // We can add as many external chlorinators as we want.
3301
+ id = sys.chlorinators.count(elem => elem.master === 2) + 50;
3302
+ chlor = sys.chlorinators.getItemById(id, true, { id: id, master: parseInt(obj.master, 10) });
3303
+ }
3304
+ else {
3305
+ if (portId === 0 && sys.controllerType !== ControllerType.Nixie) return Promise.reject(new InvalidEquipmentDataError(`You may not install a chlorinator on an ${sys.controllerType} system that is assigned to the Primary Port`, 'Chlorinator', portId));
3306
+ if (sys.chlorinators.count(elem => elem.portId === portId && elem.master !== 2) > 0) return Promise.reject(new InvalidEquipmentDataError(`There is already a chlorinator using port #${portId}. Only one chlorinator may be installed per port.`, 'Chlorinator', portId));
3307
+ // We are adding so we need to see if there is another chlorinator that is not external.
3308
+ 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));
3309
+ id = sys.chlorinators.getMaxId(false, 0) + 1;
3310
+ chlor = sys.chlorinators.getItemById(id, true, { id: id, master: 1 });
3311
+ }
3312
+ }
3313
+ else chlor = sys.chlorinators.getItemById(id, false);
3314
+
3315
+ if (chlor.master === 1)
3316
+ await ncp.chlorinators.setChlorinatorAsync(chlor, obj);
3317
+ else {
3318
+ let body = sys.board.bodies.mapBodyAssociation(typeof obj.body !== 'undefined' ? parseInt(obj.body, 10) : chlor.body);
3319
+ if (typeof body === 'undefined') {
3320
+ if (sys.equipment.shared) body = 32;
3321
+ else if (!sys.equipment.dual) body = 1;
3322
+ else return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body association is not valid: ${body}`, 'chlorinator', body));
3323
+ }
3324
+ let poolSetpoint = typeof obj.poolSetpoint !== 'undefined' ? parseInt(obj.poolSetpoint, 10) : isNaN(chlor.poolSetpoint) ? 50 : chlor.poolSetpoint;
3325
+ let spaSetpoint = typeof obj.spaSetpoint !== 'undefined' ? parseInt(obj.spaSetpoint, 10) : isNaN(chlor.spaSetpoint) ? 10 : chlor.spaSetpoint;
3326
+ if (poolSetpoint > 100 || poolSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator poolSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.poolSetpoint));
3327
+ if (spaSetpoint > 100 || spaSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator spaSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.spaSetpoint));
3328
+
3329
+ chlor = sys.chlorinators.getItemById(id, true);
3330
+ let schlor = state.chlorinators.getItemById(chlor.id, true);
3331
+ chlor.name = schlor.name = obj.name || chlor.name || 'Chlorinator --' + id;
3332
+ chlor.superChlorHours = schlor.superChlorHours = typeof obj.superChlorHours !== 'undefined' ? parseInt(obj.superChlorHours, 10) : isNaN(chlor.superChlorHours) ? 8 : chlor.superChlorHours;
3333
+ chlor.superChlor = schlor.superChlor = typeof obj.superChlorinate !== 'undefined' ? utils.makeBool(obj.superChlorinate) : chlor.superChlor;
3334
+ chlor.superChlor = schlor.superChlor = typeof obj.superChlor !== 'undefined' ? utils.makeBool(obj.superChlor) : chlor.superChlor;
3335
+
3336
+ chlor.isDosing = typeof obj.isDosing !== 'undefined' ? utils.makeBool(obj.isDosing) : chlor.isDosing || false;
3337
+ chlor.disabled = typeof obj.disabled !== 'undefined' ? utils.makeBool(obj.disabled) : chlor.disabled || false;
3338
+ schlor.model = chlor.model = typeof obj.model !== 'undefined' ? sys.board.valueMaps.chlorinatorModel.encode(obj.model) : chlor.model;
3339
+ chlor.type = schlor.type = typeof obj.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(obj.type) : chlor.type || 0;
3340
+ chlor.body = schlor.body = body.val;
3341
+ schlor.poolSetpoint = chlor.poolSetpoint = poolSetpoint;
3342
+ schlor.spaSetpoint = chlor.spaSetpoint = spaSetpoint;
3343
+ chlor.ignoreSaltReading = typeof obj.ignoreSaltReading !== 'undefined' ? utils.makeBool(obj.ignoreSaltReading) : utils.makeBool(chlor.ignoreSaltReading);
3344
+ schlor.isActive = chlor.isActive = typeof obj.isActive !== 'undefined' ? utils.makeBool(obj.isActive) : typeof chlor.isActive !== 'undefined' ? utils.makeBool(chlor.isActive) : true;
3345
+ chlor.master = 2;
3346
+ schlor.currentOutput = typeof obj.currentOutput !== 'undefined' ? parseInt(obj.currentOutput, 10) : schlor.currentOutput;
3347
+ schlor.lastComm = typeof obj.lastComm !== 'undefined' ? obj.lastComm : schlor.lastComm || Date.now();
3348
+ schlor.status = typeof obj.status !== 'undefined' ? sys.board.valueMaps.chlorinatorStatus.encode(obj.status) : sys.board.valueMaps.chlorinatorStatus.encode(schlor.status || 0);
3349
+ if (typeof obj.superChlorRemaining !== 'undefined') schlor.superChlorRemaining = parseInt(obj.superChlorRemaining, 10);
3350
+ schlor.targetOutput = typeof obj.targetOutput !== 'undefined' ? parseInt(obj.targetOutput, 10) : schlor.targetOutput;
3351
+ schlor.saltLevel = typeof obj.saltLevel !== 'undefined' ? parseInt(obj.saltLevel, 10) : schlor.saltLevel;
3352
+ }
2859
3353
  state.emitEquipmentChanges();
2860
- return Promise.resolve(schlor);
3354
+ return Promise.resolve(state.chlorinators.getItemById(id));
2861
3355
  }
2862
3356
  catch (err) {
2863
3357
  logger.error(`Error setting chlorinator: ${err}`)
@@ -3043,6 +3537,7 @@ export class ScheduleCommands extends BoardCommands {
3043
3537
  let schedDays = sys.board.schedules.transformDays(typeof data.scheduleDays !== 'undefined' ? data.scheduleDays : sched.scheduleDays);
3044
3538
  let changeHeatSetpoint = typeof (data.changeHeatSetpoint !== 'undefined') ? data.changeHeatSetpoint : false;
3045
3539
  let display = typeof data.display !== 'undefined' ? data.display : sched.display || 0;
3540
+ let disabled = typeof data.disabled !== 'undefined' ? utils.makeBool(data.disabled) : sched.disabled;
3046
3541
 
3047
3542
  // Ensure all the defaults.
3048
3543
  if (isNaN(startDate.getTime())) startDate = new Date();
@@ -3079,12 +3574,16 @@ export class ScheduleCommands extends BoardCommands {
3079
3574
  sched.startYear = startDate.getFullYear();
3080
3575
  sched.startMonth = startDate.getMonth() + 1;
3081
3576
  sched.startDay = startDate.getDate();
3082
- sched.isActive = sched.startTime !== 0;
3083
-
3577
+ ssched.isActive = sched.isActive = true;
3578
+ ssched.disabled = sched.disabled = disabled;
3084
3579
  ssched.display = sched.display = display;
3085
3580
  if (typeof sched.startDate === 'undefined')
3086
3581
  sched.master = 1;
3087
3582
  await ncp.schedules.setScheduleAsync(sched, data);
3583
+ // update end time in case sched is changed while circuit is on
3584
+ let cstate = state.circuits.getInterfaceById(sched.circuit);
3585
+ sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(sched.circuit), cstate, cstate.isOn, true);
3586
+ cstate.emitEquipmentChange();
3088
3587
  ssched.emitEquipmentChange();
3089
3588
  return sched;
3090
3589
  }
@@ -3113,7 +3612,8 @@ export class ScheduleCommands extends BoardCommands {
3113
3612
  let schedIsOn: boolean;
3114
3613
  let ssched = state.schedules.getItemByIndex(i);
3115
3614
  let scirc = state.circuits.getInterfaceById(ssched.circuit);
3116
- if (scirc.isOn &&
3615
+ let mOP = sys.board.schedules.manualPriorityActive(ssched); //sys.board.schedules.manualPriorityActiveByProxy(scirc.id);
3616
+ if (scirc.isOn && !mOP &&
3117
3617
  (ssched.scheduleDays & dayVal) > 0 &&
3118
3618
  ts >= ssched.startTime && ts <= ssched.endTime) schedIsOn = true
3119
3619
  else schedIsOn = false;
@@ -3173,484 +3673,684 @@ export class ScheduleCommands extends BoardCommands {
3173
3673
  }
3174
3674
  return nearestStartTime;
3175
3675
  }
3176
- }
3177
- export class HeaterCommands extends BoardCommands {
3178
- public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
3179
- try {
3180
- // First delete the heaters that should be removed.
3181
- for (let i = 0; i < ctx.heaters.remove.length; i++) {
3182
- let h = ctx.heaters.remove[i];
3183
- try {
3184
- await sys.board.heaters.deleteHeaterAsync(h);
3185
- res.addModuleSuccess('heater', `Remove: ${h.id}-${h.name}`);
3186
- } catch (err) { res.addModuleError('heater', `Remove: ${h.id}-${h.name}: ${err.message}`); }
3187
- }
3188
- for (let i = 0; i < ctx.heaters.update.length; i++) {
3189
- let h = ctx.heaters.update[i];
3190
- try {
3191
- await sys.board.heaters.setHeaterAsync(h);
3192
- res.addModuleSuccess('heater', `Update: ${h.id}-${h.name}`);
3193
- } catch (err) { res.addModuleError('heater', `Update: ${h.id}-${h.name}: ${err.message}`); }
3194
- }
3195
- for (let i = 0; i < ctx.heaters.add.length; i++) {
3196
- let h = ctx.heaters.add[i];
3197
- try {
3198
- // pull a little trick to first add the data then perform the update. This way we won't get a new id or
3199
- // it won't error out.
3200
- sys.heaters.getItemById(h, true);
3201
- await sys.board.heaters.setHeaterAsync(h);
3202
- res.addModuleSuccess('heater', `Add: ${h.id}-${h.name}`);
3203
- } catch (err) { res.addModuleError('heater', `Add: ${h.id}-${h.name}: ${err.message}`); }
3204
- }
3205
- return true;
3206
- } catch (err) { logger.error(`Error restoring heaters: ${err.message}`); res.addModuleError('system', `Error restoring heaters: ${err.message}`); return false; }
3207
- }
3208
- public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
3209
- try {
3210
- let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
3211
- // Look at heaters.
3212
- let cfg = rest.poolConfig;
3213
- for (let i = 0; i < cfg.heaters.length; i++) {
3214
- let r = cfg.heaters[i];
3215
- let c = sys.heaters.find(elem => r.id === elem.id);
3216
- if (typeof c === 'undefined') ctx.add.push(r);
3217
- else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
3218
- }
3219
- for (let i = 0; i < sys.heaters.length; i++) {
3220
- let c = sys.heaters.getItemByIndex(i);
3221
- let r = cfg.heaters.find(elem => elem.id == c.id);
3222
- if (typeof r === 'undefined') ctx.remove.push(c.get(true));
3223
- }
3224
- return ctx;
3225
- } catch (err) { logger.error(`Error validating heaters for restore: ${err.message}`); }
3676
+ public manualPriorityForThisCircuit(circuit: number): boolean {
3677
+ // This fn will test if this circuit/light group has any circuit group circuits that have manual priority active
3678
+ let grp: ICircuitGroup;
3679
+ let cgc: ICircuitGroupCircuit[] = [];
3680
+ if (sys.board.equipmentIds.circuitGroups.isInRange(circuit) || sys.board.equipmentIds.features.isInRange(circuit))
3681
+ grp = sys.circuitGroups.getInterfaceById(circuit);
3682
+ if (state.circuitGroups.getInterfaceById(circuit).manualPriorityActive) return true;
3683
+ if (grp && grp.isActive) cgc = grp.circuits.toArray();
3684
+ for (let i = 0; i < cgc.length; i++) {
3685
+ let c = state.circuits.getInterfaceById(cgc[i].id);
3686
+ if (c.manualPriorityActive) return true;
3226
3687
  }
3688
+ return false;
3689
+ }
3690
+ public manualPriorityActive(schedule: ScheduleState): boolean {
3691
+ // This method will look at all other schedules. If any of them have been resumed,
3692
+ // and manualPriority (global setting) is on, and this schedule would otherwise impact
3693
+ // that circuit, then we declared this schedule as being delayed due to manual override
3694
+ // priority (mOP).
3695
+ // We only need to check this if shouldBeOn = true; if that's false, exit.
3696
+ // Rules:
3697
+ // 1. If the circuit id for this schedule is in manual priority, then true
3698
+ // 2. If the other schedule will turn on a body in a shared body, and it will affect
3699
+ // this circuit id, return true
3700
+ // 3. If this is a circuit/light group schedule, check to see if any member circuit/lights have mOP active
3701
+ // 4. If this is a circuit/light/feature, is there another group that has this same id with mOP active
3227
3702
 
3228
- public getInstalledHeaterTypes(body?: number): any {
3229
- let heaters = sys.heaters.get();
3230
- let types = sys.board.valueMaps.heaterTypes.toArray();
3231
- let inst = { total: 0 };
3232
- for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
3233
- for (let i = 0; i < heaters.length; i++) {
3234
- let heater = heaters[i];
3235
- if (typeof body !== 'undefined' && heater.body !== 'undefined') {
3236
- if ((heater.body !== 32 && body !== heater.body + 1) || (heater.body === 32 && body > 2)) continue;
3237
- }
3238
- let type = types.find(elem => elem.val === heater.type);
3239
- if (typeof type !== 'undefined') {
3240
- if (inst[type.name] === 'undefined') inst[type.name] = 0;
3241
- inst[type.name] = inst[type.name] + 1;
3242
- if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
3243
- inst.total++;
3244
- }
3245
- }
3246
- return inst;
3247
- }
3248
- public isSolarInstalled(body?: number): boolean {
3249
- let heaters = sys.heaters.get();
3250
- let types = sys.board.valueMaps.heaterTypes.toArray();
3251
- for (let i = 0; i < heaters.length; i++) {
3252
- let heater = heaters[i];
3253
- if (typeof body !== 'undefined' && body !== heater.body) continue;
3254
- let type = types.find(elem => elem.val === heater.type);
3255
- if (typeof type !== 'undefined') {
3256
- switch (type.name) {
3257
- case 'solar':
3258
- return true;
3703
+ if (schedule.isActive === false) return false;
3704
+ if (schedule.disabled) return false;
3705
+ if (!sys.general.options.manualPriority) return false;
3706
+
3707
+ let currGrp: ICircuitGroup;
3708
+ let currSchedGrpCircs = [];
3709
+ if (sys.board.equipmentIds.circuitGroups.isInRange(schedule.circuit) || sys.board.equipmentIds.features.isInRange(schedule.circuit))
3710
+ currGrp = sys.circuitGroups.getInterfaceById(schedule.circuit);
3711
+ if (currGrp && currGrp.isActive) currSchedGrpCircs = currGrp.circuits.toArray();
3712
+ let circuitGrps: ICircuitGroup[] = sys.circuitGroups.toArray();
3713
+ let lightGrps: ICircuitGroup[] = sys.lightGroups.toArray();
3714
+ let currManualPriorityByProxy = sys.board.schedules.manualPriorityForThisCircuit(schedule.circuit);
3715
+ // check this circuit
3716
+ if (state.circuits.getInterfaceById(schedule.circuit).manualPriorityActive) return true;
3717
+ // check this group, if present
3718
+ if (currManualPriorityByProxy) return true;
3719
+
3720
+ let schedules: ScheduleState[] = state.schedules.get(true);
3721
+ for (let i = 0; i < schedules.length; i++) {
3722
+ let sched = schedules[i];
3723
+ // if the id of another circuit is the same as this, we should delay
3724
+ let schedCState = state.circuits.getInterfaceById(sched.circuit);
3725
+ if (schedule.circuit === schedCState.id && schedCState.manualPriorityActive) return true;
3726
+ // if OCP includes a shared body, and this schedule affects the shared body,
3727
+ // and this body is still on, we should delay
3728
+ if (sys.equipment.shared && schedCState.dataName === 'circuit') {
3729
+ let otherBody = sys.bodies.find(elem => elem.circuit === sched.circuit);
3730
+ // let otherBodyIsOn = state.circuits.getInterfaceById(sched.circuit).isOn;
3731
+ let thisBody = sys.bodies.find(elem => elem.circuit === schedule.circuit);
3732
+ if (typeof otherBody !== 'undefined' && typeof thisBody !== 'undefined' && schedCState.manualPriorityActive) return true;
3733
+ }
3734
+ // if other circuit/schedule groups have this circ id, and it's mOP, return true
3735
+ if (schedCState.dataName === 'circuitGroup') {
3736
+ for (let i = 0; i < circuitGrps.length; i++) {
3737
+ let grp: ICircuitGroup = circuitGrps[i];
3738
+ let sgrp: ICircuitGroupState = state.circuitGroups.getInterfaceById(grp.id);
3739
+ let circuits = grp.circuits.toArray();
3740
+ if (grp.isActive) {
3741
+ let manualPriorityByProxy = sys.board.schedules.manualPriorityForThisCircuit(grp.id);
3742
+ for (let j = 0; j < circuits.length; j++) {
3743
+ let cgc = grp.circuits.getItemByIndex(j);
3744
+ let scgc = state.circuits.getInterfaceById(cgc.circuit);
3745
+ // if the circuit id's match and mOP is active, we delay
3746
+ if (scgc.id === schedule.circuit && scgc.manualPriorityActive) return true;
3747
+ // check all the other cgc against this cgc
3748
+ // note: circuit/light groups cannot be part of a group themselves
3749
+ for (let k = 0; k < currSchedGrpCircs.length; k++) {
3750
+ let currCircGrpCirc = state.circuits.getInterfaceById(currSchedGrpCircs[k].circuit);
3751
+ // if either circuit in either group has mOP then delay
3752
+ if (currManualPriorityByProxy || manualPriorityByProxy) {
3753
+ if (currCircGrpCirc.id === schedCState.id) return true;
3754
+ if (currCircGrpCirc.id === scgc.id) return true;
3259
3755
  }
3756
+ }
3260
3757
  }
3758
+ }
3261
3759
  }
3262
- }
3263
- public isHeatPumpInstalled(body?: number): boolean {
3264
- let heaters = sys.heaters.get();
3265
- let types = sys.board.valueMaps.heaterTypes.toArray();
3266
- for (let i = 0; i < heaters.length; i++) {
3267
- let heater = heaters[i];
3268
- if (typeof body !== 'undefined' && body !== heater.body) continue;
3269
- let type = types.find(elem => elem.val === heater.type);
3270
- if (typeof type !== 'undefined') {
3271
- switch (type.name) {
3272
- case 'heatpump':
3273
- return true;
3760
+ }
3761
+ if (schedCState.dataName === 'lightGroup') {
3762
+ for (let i = 0; i < lightGrps.length; i++) {
3763
+ let grp: ICircuitGroup = lightGrps[i];
3764
+ let sgrp: ICircuitGroupState = state.circuitGroups.getInterfaceById(grp.id);
3765
+ let circuits = grp.circuits.toArray();
3766
+ if (grp.isActive) {
3767
+ let manualPriorityByProxy = sys.board.schedules.manualPriorityForThisCircuit(grp.id);
3768
+ for (let j = 0; j < circuits.length; j++) {
3769
+ let cgc = grp.circuits.getItemByIndex(j);
3770
+ let scgc = state.circuits.getInterfaceById(cgc.circuit);
3771
+ // if the circuit id's match and mOP is active, we delay
3772
+ if (scgc.id === schedule.circuit && scgc.manualPriorityActive) return true;
3773
+ // check all the other cgc against this cgc
3774
+ // note: circuit/light groups cannot be part of a group themselves
3775
+ for (let k = 0; k < currSchedGrpCircs.length; k++) {
3776
+ let currCircGrpCirc = state.circuits.getInterfaceById(currSchedGrpCircs[k].circuit);
3777
+ // if either circuit in either group has mOP then delay
3778
+ if (currManualPriorityByProxy || manualPriorityByProxy) {
3779
+ if (currCircGrpCirc.id === schedCState.id) return true;
3780
+ if (currCircGrpCirc.id === scgc.id) return true;
3274
3781
  }
3782
+ }
3275
3783
  }
3784
+ }
3276
3785
  }
3786
+ }
3277
3787
  }
3278
- public setHeater(heater: Heater, obj?: any) {
3279
- if (typeof obj !== undefined) {
3280
- for (var s in obj)
3281
- heater[s] = obj[s];
3282
- }
3283
- }
3284
- public async setHeaterAsync(obj: any): Promise<Heater> {
3788
+ // if we make it this far, nothing is impacting us
3789
+ return false;
3790
+ }
3791
+ }
3792
+ export class HeaterCommands extends BoardCommands {
3793
+ public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
3794
+ try {
3795
+ // First delete the heaters that should be removed.
3796
+ for (let i = 0; i < ctx.heaters.remove.length; i++) {
3797
+ let h = ctx.heaters.remove[i];
3285
3798
  try {
3286
- let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
3287
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
3288
- else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Virtual Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
3289
- let heater: Heater;
3290
- if (id <= 0) {
3291
- // We are adding a heater. In this case all heaters are virtual.
3292
- let vheaters = sys.heaters.filter(h => h.master === 1);
3293
- id = vheaters.length + 256;
3294
- }
3295
- heater = sys.heaters.getItemById(id, true);
3296
- if (typeof obj !== undefined) {
3297
- for (var s in obj) {
3298
- if (s === 'id') continue;
3299
- heater[s] = obj[s];
3300
- }
3301
- }
3302
- let hstate = state.heaters.getItemById(id, true);
3303
- //hstate.isVirtual = heater.isVirtual = true;
3304
- hstate.name = heater.name;
3305
- hstate.type = heater.type;
3306
- heater.master = 1;
3307
- if (heater.master === 1) await ncp.heaters.setHeaterAsync(heater, obj);
3308
- await sys.board.heaters.updateHeaterServices();
3309
- await sys.board.heaters.syncHeaterStates();
3310
- return heater;
3311
- } catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
3312
- }
3313
- public async deleteHeaterAsync(obj: any): Promise<Heater> {
3799
+ await sys.board.heaters.deleteHeaterAsync(h);
3800
+ res.addModuleSuccess('heater', `Remove: ${h.id}-${h.name}`);
3801
+ } catch (err) { res.addModuleError('heater', `Remove: ${h.id}-${h.name}: ${err.message}`); }
3802
+ }
3803
+ for (let i = 0; i < ctx.heaters.update.length; i++) {
3804
+ let h = ctx.heaters.update[i];
3314
3805
  try {
3315
- let id = parseInt(obj.id, 10);
3316
- if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
3317
- let heater = sys.heaters.getItemById(id);
3318
- heater.isActive = false;
3319
- if (heater.master === 1) await ncp.heaters.deleteHeaterAsync(heater.id);
3320
- sys.heaters.removeItemById(id);
3321
- state.heaters.removeItemById(id);
3322
- sys.board.heaters.updateHeaterServices();
3323
- sys.board.heaters.syncHeaterStates();
3324
- return heater;
3325
- } catch (err) { return Promise.reject(`Error deleting heater: ${err.message}`) }
3806
+ await sys.board.heaters.setHeaterAsync(h);
3807
+ res.addModuleSuccess('heater', `Update: ${h.id}-${h.name}`);
3808
+ } catch (err) { res.addModuleError('heater', `Update: ${h.id}-${h.name}: ${err.message}`); }
3809
+ }
3810
+ for (let i = 0; i < ctx.heaters.add.length; i++) {
3811
+ let h = ctx.heaters.add[i];
3812
+ try {
3813
+ // pull a little trick to first add the data then perform the update. This way we won't get a new id or
3814
+ // it won't error out.
3815
+ sys.heaters.getItemById(h.id, true);
3816
+ await sys.board.heaters.setHeaterAsync(h);
3817
+ res.addModuleSuccess('heater', `Add: ${h.id}-${h.name}`);
3818
+ } catch (err) { res.addModuleError('heater', `Add: ${h.id}-${h.name}: ${err.message}`); }
3819
+ }
3820
+ return true;
3821
+ } catch (err) { logger.error(`Error restoring heaters: ${err.message}`); res.addModuleError('system', `Error restoring heaters: ${err.message}`); return false; }
3822
+ }
3823
+ public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> {
3824
+ try {
3825
+ let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] };
3826
+ // Look at heaters.
3827
+ let cfg = rest.poolConfig;
3828
+ for (let i = 0; i < cfg.heaters.length; i++) {
3829
+ let r = cfg.heaters[i];
3830
+ let c = sys.heaters.find(elem => r.id === elem.id);
3831
+ if (typeof c === 'undefined') ctx.add.push(r);
3832
+ else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r);
3833
+ }
3834
+ for (let i = 0; i < sys.heaters.length; i++) {
3835
+ let c = sys.heaters.getItemByIndex(i);
3836
+ let r = cfg.heaters.find(elem => elem.id == c.id);
3837
+ if (typeof r === 'undefined') ctx.remove.push(c.get(true));
3838
+ }
3839
+ return ctx;
3840
+ } catch (err) { logger.error(`Error validating heaters for restore: ${err.message}`); }
3841
+ }
3842
+ public getHeatersByCircuitId(circuitId: number): Heater[] {
3843
+ let heaters: Heater[] = [];
3844
+ let bodyId = circuitId === 6 ? 1 : circuitId === 1 ? 2 : 0;
3845
+ if (bodyId > 0) {
3846
+ for (let i = 0; i < sys.heaters.length; i++) {
3847
+ let heater = sys.heaters.getItemByIndex(i);
3848
+ if (!heater.isActive) continue;
3849
+ if (bodyId === heater.body || sys.equipment.shared && heater.body === 32) heaters.push(heater);
3850
+ }
3326
3851
  }
3327
- public updateHeaterServices() {
3328
- let htypes = sys.board.heaters.getInstalledHeaterTypes();
3329
- let solarInstalled = htypes.solar > 0;
3330
- let heatPumpInstalled = htypes.heatpump > 0;
3331
- let gasHeaterInstalled = htypes.gas > 0;
3332
-
3333
- if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
3334
- if (gasHeaterInstalled) sys.board.valueMaps.heatSources.set(3, { name: 'heater', desc: 'Heater' });
3335
- if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
3336
- else if (solarInstalled) sys.board.valueMaps.heatSources.set(5, { name: 'solar', desc: 'Solar' });
3337
- if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
3338
- else if (heatPumpInstalled) sys.board.valueMaps.heatSources.set(9, { name: 'heatpump', desc: 'Heat Pump' });
3339
- sys.board.valueMaps.heatSources.set(32, { name: 'nochange', desc: 'No Change' });
3340
-
3341
- sys.board.valueMaps.heatModes = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
3342
- if (gasHeaterInstalled) sys.board.valueMaps.heatModes.set(3, { name: 'heater', desc: 'Heater' });
3343
- if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
3344
- else if (solarInstalled) sys.board.valueMaps.heatModes.set(5, { name: 'solar', desc: 'Solar' });
3345
- if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
3346
- else if (heatPumpInstalled) sys.board.valueMaps.heatModes.set(9, { name: 'heatpump', desc: 'Heat Pump' });
3347
- // Now set the body data.
3348
- for (let i = 0; i < sys.bodies.length; i++) {
3349
- let body = sys.bodies.getItemByIndex(i);
3350
- let btemp = state.temps.bodies.getItemById(body.id, body.isActive !== false);
3351
- let opts = sys.board.heaters.getInstalledHeaterTypes(body.id);
3352
- btemp.heaterOptions = opts;
3353
- }
3354
- this.setActiveTempSensors();
3852
+ return heaters;
3853
+ }
3854
+ public getInstalledHeaterTypes(body?: number): any {
3855
+ let heaters = sys.heaters.get();
3856
+ let types = sys.board.valueMaps.heaterTypes.toArray();
3857
+ let inst = { total: 0 };
3858
+ for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
3859
+ for (let i = 0; i < heaters.length; i++) {
3860
+ let heater = heaters[i];
3861
+ if (typeof body !== 'undefined' && heater.body !== 'undefined') {
3862
+ if ((heater.body !== 32 && body !== heater.body + 1) || (heater.body === 32 && body > 2)) continue;
3863
+ }
3864
+ let type = types.find(elem => elem.val === heater.type);
3865
+ if (typeof type !== 'undefined') {
3866
+ if (inst[type.name] === 'undefined') inst[type.name] = 0;
3867
+ inst[type.name] = inst[type.name] + 1;
3868
+ if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
3869
+ inst.total++;
3870
+ }
3355
3871
  }
3356
- public initTempSensors() {
3357
- // Add in the potential sensors and delete the ones that shouldn't exist.
3358
- let maxPairs = sys.equipment.maxBodies + (sys.equipment.shared ? -1 : 0);
3359
- sys.equipment.tempSensors.getItemById('air', true, { id: 'air', isActive: true, calibration: 0 }).name = 'Air';
3360
- sys.equipment.tempSensors.getItemById('water1', true, { id: 'water1', isActive: true, calibration: 0 }).name = maxPairs == 1 ? 'Water' : 'Body 1';
3361
- sys.equipment.tempSensors.getItemById('solar1', true, { id: 'solar1', isActive: false, calibration: 0 }).name = maxPairs == 1 ? 'Solar' : 'Solar 1';
3362
- if (maxPairs > 1) {
3363
- sys.equipment.tempSensors.getItemById('water2', true, { id: 'water2', isActive: false, calibration: 0 }).name = 'Body 2';
3364
- sys.equipment.tempSensors.getItemById('solar2', true, { id: 'solar2', isActive: false, calibration: 0 }).name = 'Solar 2';
3365
- }
3366
- else {
3367
- sys.equipment.tempSensors.removeItemById('water2');
3368
- sys.equipment.tempSensors.removeItemById('solar2');
3369
- }
3370
- if (maxPairs > 2) {
3371
- sys.equipment.tempSensors.getItemById('water3', true, { id: 'water3', isActive: false, calibration: 0 }).name = 'Body 3';
3372
- sys.equipment.tempSensors.getItemById('solar3', true, { id: 'solar3', isActive: false, calibration: 0 }).name = 'Solar 3';
3373
- }
3374
- else {
3375
- sys.equipment.tempSensors.removeItemById('water3');
3376
- sys.equipment.tempSensors.removeItemById('solar3');
3872
+ return inst;
3873
+ }
3874
+ public isSolarInstalled(body?: number): boolean {
3875
+ let heaters = sys.heaters.get();
3876
+ let types = sys.board.valueMaps.heaterTypes.toArray();
3877
+ for (let i = 0; i < heaters.length; i++) {
3878
+ let heater = heaters[i];
3879
+ if (typeof body !== 'undefined' && body !== heater.body) continue;
3880
+ let type = types.find(elem => elem.val === heater.type);
3881
+ if (typeof type !== 'undefined') {
3882
+ switch (type.name) {
3883
+ case 'solar':
3884
+ return true;
3377
3885
  }
3378
- if (maxPairs > 3) {
3379
- sys.equipment.tempSensors.getItemById('water4', true, { id: 'water4', isActive: false, calibration: 0 }).name = 'Body 4';
3380
- sys.equipment.tempSensors.getItemById('solar4', true, { id: 'solar4', isActive: false, calibration: 0 }).name = 'Solar 4';
3886
+ }
3887
+ }
3888
+ }
3889
+ public isHeatPumpInstalled(body?: number): boolean {
3890
+ let heaters = sys.heaters.get();
3891
+ let types = sys.board.valueMaps.heaterTypes.toArray();
3892
+ for (let i = 0; i < heaters.length; i++) {
3893
+ let heater = heaters[i];
3894
+ if (typeof body !== 'undefined' && body !== heater.body) continue;
3895
+ let type = types.find(elem => elem.val === heater.type);
3896
+ if (typeof type !== 'undefined') {
3897
+ switch (type.name) {
3898
+ case 'heatpump':
3899
+ return true;
3381
3900
  }
3382
- else {
3383
- sys.equipment.tempSensors.removeItemById('water4');
3384
- sys.equipment.tempSensors.removeItemById('solar4');
3901
+ }
3902
+ }
3903
+ }
3904
+ public setHeater(heater: Heater, obj?: any) {
3905
+ if (typeof obj !== undefined) {
3906
+ for (var s in obj)
3907
+ heater[s] = obj[s];
3908
+ }
3909
+ }
3910
+ public async setHeaterAsync(obj: any): Promise<Heater> {
3911
+ try {
3912
+ let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
3913
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
3914
+ else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Virtual Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
3915
+ let heater: Heater;
3916
+ if (id <= 0) {
3917
+ // We are adding a heater. In this case all heaters are virtual.
3918
+ let vheaters = sys.heaters.filter(h => h.master === 1);
3919
+ id = vheaters.length + 256;
3920
+ }
3921
+ heater = sys.heaters.getItemById(id, true);
3922
+ if (typeof obj !== undefined) {
3923
+ for (var s in obj) {
3924
+ if (s === 'id') continue;
3925
+ heater[s] = obj[s];
3385
3926
  }
3927
+ }
3928
+ let hstate = state.heaters.getItemById(id, true);
3929
+ //hstate.isVirtual = heater.isVirtual = true;
3930
+ hstate.name = heater.name;
3931
+ hstate.type = heater.type;
3932
+ heater.master = 1;
3933
+ if (heater.master === 1) await ncp.heaters.setHeaterAsync(heater, obj);
3934
+ await sys.board.heaters.updateHeaterServices();
3935
+ await sys.board.heaters.syncHeaterStates();
3936
+ return heater;
3937
+ } catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
3938
+ }
3939
+ public async deleteHeaterAsync(obj: any): Promise<Heater> {
3940
+ try {
3941
+ let id = parseInt(obj.id, 10);
3942
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
3943
+ let heater = sys.heaters.getItemById(id);
3944
+ heater.isActive = false;
3945
+ if (heater.master === 1) await ncp.heaters.deleteHeaterAsync(heater.id);
3946
+ sys.heaters.removeItemById(id);
3947
+ state.heaters.removeItemById(id);
3948
+ sys.board.heaters.updateHeaterServices();
3949
+ sys.board.heaters.syncHeaterStates();
3950
+ return heater;
3951
+ } catch (err) { return Promise.reject(`Error deleting heater: ${err.message}`) }
3952
+ }
3953
+ public updateHeaterServices() {
3954
+ let htypes = sys.board.heaters.getInstalledHeaterTypes();
3955
+ let solarInstalled = htypes.solar > 0;
3956
+ let heatPumpInstalled = htypes.heatpump > 0;
3957
+ let gasHeaterInstalled = htypes.gas > 0;
3958
+
3959
+ if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
3960
+ if (gasHeaterInstalled) sys.board.valueMaps.heatSources.set(3, { name: 'heater', desc: 'Heater' });
3961
+ if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
3962
+ else if (solarInstalled) sys.board.valueMaps.heatSources.set(5, { name: 'solar', desc: 'Solar' });
3963
+ if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
3964
+ else if (heatPumpInstalled) sys.board.valueMaps.heatSources.set(9, { name: 'heatpump', desc: 'Heat Pump' });
3965
+ sys.board.valueMaps.heatSources.set(32, { name: 'nochange', desc: 'No Change' });
3386
3966
 
3967
+ sys.board.valueMaps.heatModes = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
3968
+ if (gasHeaterInstalled) sys.board.valueMaps.heatModes.set(3, { name: 'heater', desc: 'Heater' });
3969
+ if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'solar', desc: 'Solar Only' }], [21, { name: 'solarpref', desc: 'Solar Preferred' }]]);
3970
+ else if (solarInstalled) sys.board.valueMaps.heatModes.set(5, { name: 'solar', desc: 'Solar' });
3971
+ if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
3972
+ else if (heatPumpInstalled) sys.board.valueMaps.heatModes.set(9, { name: 'heatpump', desc: 'Heat Pump' });
3973
+ // Now set the body data.
3974
+ for (let i = 0; i < sys.bodies.length; i++) {
3975
+ let body = sys.bodies.getItemByIndex(i);
3976
+ let btemp = state.temps.bodies.getItemById(body.id, body.isActive !== false);
3977
+ let opts = sys.board.heaters.getInstalledHeaterTypes(body.id);
3978
+ btemp.heaterOptions = opts;
3387
3979
  }
3388
- // Sets the active temp sensors based upon the installed equipment. At this point all
3389
- // detectable temp sensors should exist.
3390
- public setActiveTempSensors() {
3391
- let htypes;
3392
- // We are iterating backwards through the sensors array on purpose. We do this just in case we need
3393
- // to remove a sensor during the iteration. This way the index values will not be impacted and we can
3394
- // safely remove from the array we are iterating.
3395
- for (let i = sys.equipment.tempSensors.length - 1; i >= 0; i--) {
3396
- let sensor = sys.equipment.tempSensors.getItemByIndex(i);
3397
- // The names are normalized in this array.
3398
- switch (sensor.id) {
3399
- case 'air':
3400
- sensor.isActive = true;
3401
- break;
3402
- case 'water1':
3403
- sensor.isActive = sys.equipment.maxBodies > 0;
3404
- break;
3405
- case 'water2':
3406
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 2 : sys.equipment.maxBodies > 1;
3407
- break;
3408
- case 'water3':
3409
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 3 : sys.equipment.maxBodies > 2;
3410
- break;
3411
- case 'water4':
3412
- // It's a little weird but technically you should be able to install 3 expansions and a i10D personality
3413
- // board. If this situation ever comes up we will see if it works. Whether it reports is another story
3414
- // since the 2 message is short a byte for this.
3415
- sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 4 : sys.equipment.maxBodies > 3;
3416
- break;
3417
- // Solar sensors are funny ducks. This is because they are for both heatpumps and solar and the equipment
3418
- // can be installed on specific bodies. This will be true for heaters installed in expansion panels for *Touch, dual body systems,
3419
- // and any IntelliCenter with more than one body. At some point simply implementing the multi-body functions for touch will make
3420
- // this all work. This will only be with i10D or expansion panels.
3421
- case 'solar1':
3422
- // The first solar sensor is a funny duck in that it should be active for shared systems
3423
- // if either body has an active solar heater or heatpump.
3424
- htypes = sys.board.heaters.getInstalledHeaterTypes(1);
3425
- if ('solar' in htypes || 'heatpump' in htypes) sensor.isActive = true;
3426
- else if (sys.equipment.shared) {
3427
- htypes = sys.board.heaters.getInstalledHeaterTypes(2);
3428
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
3980
+ this.setActiveTempSensors();
3981
+ }
3982
+ public initTempSensors() {
3983
+ // Add in the potential sensors and delete the ones that shouldn't exist.
3984
+ let maxPairs = sys.equipment.maxBodies + (sys.equipment.shared ? -1 : 0);
3985
+ sys.equipment.tempSensors.getItemById('air', true, { id: 'air', isActive: true, calibration: 0 }).name = 'Air';
3986
+ sys.equipment.tempSensors.getItemById('water1', true, { id: 'water1', isActive: true, calibration: 0 }).name = maxPairs == 1 ? 'Water' : 'Body 1';
3987
+ sys.equipment.tempSensors.getItemById('solar1', true, { id: 'solar1', isActive: false, calibration: 0 }).name = maxPairs == 1 ? 'Solar' : 'Solar 1';
3988
+ if (maxPairs > 1) {
3989
+ sys.equipment.tempSensors.getItemById('water2', true, { id: 'water2', isActive: false, calibration: 0 }).name = 'Body 2';
3990
+ sys.equipment.tempSensors.getItemById('solar2', true, { id: 'solar2', isActive: false, calibration: 0 }).name = 'Solar 2';
3991
+ }
3992
+ else {
3993
+ sys.equipment.tempSensors.removeItemById('water2');
3994
+ sys.equipment.tempSensors.removeItemById('solar2');
3995
+ }
3996
+ if (maxPairs > 2) {
3997
+ sys.equipment.tempSensors.getItemById('water3', true, { id: 'water3', isActive: false, calibration: 0 }).name = 'Body 3';
3998
+ sys.equipment.tempSensors.getItemById('solar3', true, { id: 'solar3', isActive: false, calibration: 0 }).name = 'Solar 3';
3999
+ }
4000
+ else {
4001
+ sys.equipment.tempSensors.removeItemById('water3');
4002
+ sys.equipment.tempSensors.removeItemById('solar3');
4003
+ }
4004
+ if (maxPairs > 3) {
4005
+ sys.equipment.tempSensors.getItemById('water4', true, { id: 'water4', isActive: false, calibration: 0 }).name = 'Body 4';
4006
+ sys.equipment.tempSensors.getItemById('solar4', true, { id: 'solar4', isActive: false, calibration: 0 }).name = 'Solar 4';
4007
+ }
4008
+ else {
4009
+ sys.equipment.tempSensors.removeItemById('water4');
4010
+ sys.equipment.tempSensors.removeItemById('solar4');
4011
+ }
4012
+
4013
+ }
4014
+ // Sets the active temp sensors based upon the installed equipment. At this point all
4015
+ // detectable temp sensors should exist.
4016
+ public setActiveTempSensors() {
4017
+ let htypes;
4018
+ // We are iterating backwards through the sensors array on purpose. We do this just in case we need
4019
+ // to remove a sensor during the iteration. This way the index values will not be impacted and we can
4020
+ // safely remove from the array we are iterating.
4021
+ for (let i = sys.equipment.tempSensors.length - 1; i >= 0; i--) {
4022
+ let sensor = sys.equipment.tempSensors.getItemByIndex(i);
4023
+ // The names are normalized in this array.
4024
+ switch (sensor.id) {
4025
+ case 'air':
4026
+ sensor.isActive = true;
4027
+ break;
4028
+ case 'water1':
4029
+ sensor.isActive = sys.equipment.maxBodies > 0;
4030
+ break;
4031
+ case 'water2':
4032
+ sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 2 : sys.equipment.maxBodies > 1;
4033
+ break;
4034
+ case 'water3':
4035
+ sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 3 : sys.equipment.maxBodies > 2;
4036
+ break;
4037
+ case 'water4':
4038
+ // It's a little weird but technically you should be able to install 3 expansions and a i10D personality
4039
+ // board. If this situation ever comes up we will see if it works. Whether it reports is another story
4040
+ // since the 2 message is short a byte for this.
4041
+ sensor.isActive = sys.equipment.shared ? sys.equipment.maxBodies > 4 : sys.equipment.maxBodies > 3;
4042
+ break;
4043
+ // Solar sensors are funny ducks. This is because they are for both heatpumps and solar and the equipment
4044
+ // can be installed on specific bodies. This will be true for heaters installed in expansion panels for *Touch, dual body systems,
4045
+ // and any IntelliCenter with more than one body. At some point simply implementing the multi-body functions for touch will make
4046
+ // this all work. This will only be with i10D or expansion panels.
4047
+ case 'solar1':
4048
+ // The first solar sensor is a funny duck in that it should be active for shared systems
4049
+ // if either body has an active solar heater or heatpump.
4050
+ htypes = sys.board.heaters.getInstalledHeaterTypes(1);
4051
+ if ('solar' in htypes || 'heatpump' in htypes) sensor.isActive = true;
4052
+ else if (sys.equipment.shared) {
4053
+ htypes = sys.board.heaters.getInstalledHeaterTypes(2);
4054
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4055
+ }
4056
+ else sensor.isActive = false;
4057
+ break;
4058
+ case 'solar2':
4059
+ if (sys.equipment.maxBodies > 1 + (sys.equipment.shared ? 1 : 0)) {
4060
+ htypes = sys.board.heaters.getInstalledHeaterTypes(2 + (sys.equipment.shared ? 1 : 0));
4061
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4062
+ }
4063
+ else sensor.isActive = false;
4064
+ break;
4065
+ case 'solar3':
4066
+ if (sys.equipment.maxBodies > 2 + (sys.equipment.shared ? 1 : 0)) {
4067
+ htypes = sys.board.heaters.getInstalledHeaterTypes(3 + (sys.equipment.shared ? 1 : 0));
4068
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4069
+ }
4070
+ else sensor.isActive = false;
4071
+ break;
4072
+ case 'solar4':
4073
+ if (sys.equipment.maxBodies > 3 + (sys.equipment.shared ? 1 : 0)) {
4074
+ htypes = sys.board.heaters.getInstalledHeaterTypes(4 + (sys.equipment.shared ? 1 : 0));
4075
+ sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4076
+ }
4077
+ else sensor.isActive = false;
4078
+ break;
4079
+ default:
4080
+ if (typeof sensor.id === 'undefined') sys.equipment.tempSensors.removeItemByIndex(i);
4081
+ break;
4082
+ }
4083
+ }
4084
+ }
4085
+ // This updates the heater states based upon the installed heaters. This is true for heaters that are tied to the OCP
4086
+ // and those that are not.
4087
+ public syncHeaterStates() {
4088
+ try {
4089
+ // Go through the installed heaters and bodies to determine whether they should be on. If there is a
4090
+ // heater that is not controlled by the OCP then we need to determine whether it should be on.
4091
+ let heaters = sys.heaters.toArray();
4092
+ let bodies = state.temps.bodies.toArray();
4093
+ let hon = [];
4094
+ for (let i = 0; i < bodies.length; i++) {
4095
+ let body: BodyTempState = bodies[i];
4096
+ let cfgBody: Body = sys.bodies.getItemById(body.id);
4097
+ let isHeating = false;
4098
+ let isCooling = false;
4099
+ let hstatus = sys.board.valueMaps.heatStatus.getName(body.heatStatus);
4100
+ let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
4101
+ if (body.isOn) {
4102
+ if (typeof body.temp === 'undefined' && heaters.length > 0) logger.warn(`The body temperature for ${body.name} cannot be determined. Heater status for this body cannot be calculated.`);
4103
+ // Now get all the heaters associated with the body in an array.
4104
+ let bodyHeaters: Heater[] = [];
4105
+ for (let j = 0; j < heaters.length; j++) {
4106
+ let heater: Heater = heaters[j];
4107
+ if (heater.isActive === false) continue;
4108
+ if (heater.body === body.id) bodyHeaters.push(heater);
4109
+ else {
4110
+ let b = sys.board.valueMaps.bodies.transform(heater.body);
4111
+ switch (b.name) {
4112
+ case 'body1':
4113
+ case 'pool':
4114
+ if (body.id === 1) bodyHeaters.push(heater);
4115
+ break;
4116
+ case 'body2':
4117
+ case 'spa':
4118
+ if (body.id === 2) bodyHeaters.push(heater);
4119
+ break;
4120
+ case 'poolspa':
4121
+ if (body.id === 1 || body.id === 2) bodyHeaters.push(heater);
4122
+ break;
4123
+ case 'body3':
4124
+ if (body.id === 3) bodyHeaters.push(heater);
4125
+ break;
4126
+ case 'body4':
4127
+ if (body.id === 4) bodyHeaters.push(heater);
4128
+ break;
4129
+ }
4130
+ }
4131
+ }
4132
+ // Alright we have all the body heaters so sort them in a way that will make our heater preferences work. Solar, heatpumps, and ultratemp should be in the list first
4133
+ // so that if we have a heater preference set up then we do not have to evaluate the other heater.
4134
+ let heaterTypes = sys.board.valueMaps.heaterTypes;
4135
+ bodyHeaters.sort((a, b) => {
4136
+ if (heaterTypes.transform(a.type).hasPreference) return -1;
4137
+ else if (heaterTypes.transform(b.type).hasPreference) return 1;
4138
+ return 0;
4139
+ });
4140
+
4141
+ // Alright so now we should have a sorted array that has preference type heaters first.
4142
+ for (let j = 0; j < bodyHeaters.length; j++) {
4143
+ let heater: Heater = bodyHeaters[j];
4144
+ let isOn = false;
4145
+ let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
4146
+ let hstate = state.heaters.getItemById(heater.id, true);
4147
+ if (heater.master === 1) {
4148
+ if (hstatus !== 'cooldown') {
4149
+ // We need to do our own calculation as to whether it is on. This is for Nixie heaters.
4150
+ switch (htype.name) {
4151
+ case 'solar':
4152
+ if (mode === 'solar' || mode === 'solarpref') {
4153
+ // Measure up against start and stop temp deltas for effective solar heating.
4154
+ if (body.temp < cfgBody.heatSetpoint &&
4155
+ state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
4156
+ isOn = true;
4157
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
4158
+ isHeating = true;
4159
+ }
4160
+ else if (heater.coolingEnabled && body.temp > cfgBody.coolSetpoint && state.heliotrope.isNight &&
4161
+ state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
4162
+ isOn = true;
4163
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
4164
+ isHeating = true;
4165
+ isCooling = true;
4166
+ }
3429
4167
  }
3430
- else sensor.isActive = false;
3431
4168
  break;
3432
- case 'solar2':
3433
- if (sys.equipment.maxBodies > 1 + (sys.equipment.shared ? 1 : 0)) {
3434
- htypes = sys.board.heaters.getInstalledHeaterTypes(2 + (sys.equipment.shared ? 1 : 0));
3435
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4169
+ case 'ultratemp':
4170
+ // There is a temperature differential setting on UltraTemp. This is how
4171
+ // much the water temperature needs to drop below the set temperature, for the heater
4172
+ // to start up again. For instance, if the set temperature and the water temperature is 82 and then the
4173
+ // heater will shut off and not turn on again until the water temperature = setpoint - differentialTemperature.
4174
+ // This is the default operation on IntelliCenter and it appears to simply not start on the setpoint. We can do better
4175
+ // than this by heating 1 degree past the setpoint then applying this rule for 30 minutes. This allows for a more
4176
+ // responsive heater.
4177
+ //
4178
+ // For Ultratemp we need to determine whether the differential temp
4179
+ // is within range. The other thing that needs to be calculated here is
4180
+ // whether Ultratemp can effeciently heat the pool.
4181
+ if (mode === 'ultratemp' || mode === 'ultratemppref') {
4182
+ if (hstate.isOn) {
4183
+ // For the preference mode we will try to reach the setpoint for a period of time then
4184
+ // switch over to the gas heater. Our algorithm for this is to check the rate of
4185
+ // change when the heater first kicks on. If we go for longer than an hour and still
4186
+ // haven't reached the setpoint then we will switch to gas.
4187
+ if (mode === 'ultratemppref' &&
4188
+ typeof hstate.startTime !== 'undefined' &&
4189
+ hstate.startTime.getTime() < new Date().getTime() - (60 * 60 * 1000))
4190
+ break;
4191
+ // If the heater is already on we will heat to 1 degree past the setpoint.
4192
+ if (body.temp - 1 < cfgBody.heatSetpoint) {
4193
+ isOn = true;
4194
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
4195
+ isHeating = true;
4196
+ isCooling = false;
4197
+ }
4198
+ else if (body.temp + 1 > cfgBody.coolSetpoint && heater.coolingEnabled) {
4199
+ isOn = true;
4200
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
4201
+ isHeating = false;
4202
+ isCooling = true;
4203
+ }
4204
+ }
4205
+ else {
4206
+ let delayStart = typeof hstate.endTime !== 'undefined' ? (hstate.endTime.getTime() + (30 * 60 * 1000)) > new Date().getTime() : false;
4207
+ // The heater is not currently on lets turn it on if we pass all the criteria.
4208
+ if ((body.temp < cfgBody.heatSetpoint && !delayStart)
4209
+ || body.temp + heater.differentialTemp < cfgBody.heatSetpoint) {
4210
+ isOn = true;
4211
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
4212
+ isHeating = true;
4213
+ isCooling = false;
4214
+ }
4215
+ else if (body.temp > cfgBody.coolSetpoint && heater.coolingEnabled) {
4216
+ if (!delayStart || body.temp - heater.differentialTemp > cfgBody.coolSetpoint) {
4217
+ isOn = true;
4218
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
4219
+ isHeating = false;
4220
+ isCooling = true;
4221
+ }
4222
+ }
4223
+ }
3436
4224
  }
3437
- else sensor.isActive = false;
3438
4225
  break;
3439
- case 'solar3':
3440
- if (sys.equipment.maxBodies > 2 + (sys.equipment.shared ? 1 : 0)) {
3441
- htypes = sys.board.heaters.getInstalledHeaterTypes(3 + (sys.equipment.shared ? 1 : 0));
3442
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4226
+ case 'mastertemp':
4227
+ // If we make it here, the other heater is not heating the body.
4228
+ if (mode === 'mtheater' || mode === 'heatpumppref' || mode === 'ultratemppref' || mode === 'solarpref') {
4229
+ if (body.temp < cfgBody.setPoint) {
4230
+ isOn = true;
4231
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('mtheat');
4232
+ isHeating = true;
4233
+ }
3443
4234
  }
3444
- else sensor.isActive = false;
3445
4235
  break;
3446
- case 'solar4':
3447
- if (sys.equipment.maxBodies > 3 + (sys.equipment.shared ? 1 : 0)) {
3448
- htypes = sys.board.heaters.getInstalledHeaterTypes(4 + (sys.equipment.shared ? 1 : 0));
3449
- sensor.isActive = ('solar' in htypes || 'heatpump' in htypes);
4236
+ case 'maxetherm':
4237
+ case 'gas':
4238
+ // If we make it here, the other heater is not heating the body.
4239
+ if (mode === 'heater' || mode === 'solarpref' || mode === 'heatpumppref' || mode === 'ultratemppref') {
4240
+ // Heat past the setpoint for the heater but only if the heater is currently on.
4241
+ if ((body.temp - (hstate.isOn ? heater.stopTempDelta : 0)) < cfgBody.setPoint) {
4242
+ isOn = true;
4243
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
4244
+ isHeating = true;
4245
+ }
3450
4246
  }
3451
- else sensor.isActive = false;
3452
- break;
3453
- default:
3454
- if (typeof sensor.id === 'undefined') sys.equipment.tempSensors.removeItemByIndex(i);
3455
4247
  break;
3456
- }
3457
- }
3458
- }
3459
- // This updates the heater states based upon the installed heaters. This is true for heaters that are tied to the OCP
3460
- // and those that are not.
3461
- public syncHeaterStates() {
3462
- try {
3463
- // Go through the installed heaters and bodies to determine whether they should be on. If there is a
3464
- // heater that is not controlled by the OCP then we need to determine whether it should be on.
3465
- let heaters = sys.heaters.toArray();
3466
- let bodies = state.temps.bodies.toArray();
3467
- let hon = [];
3468
- for (let i = 0; i < bodies.length; i++) {
3469
- let body: BodyTempState = bodies[i];
3470
- let cfgBody: Body = sys.bodies.getItemById(body.id);
3471
- let isHeating = false;
3472
- if (body.isOn) {
3473
- if (typeof body.temp === 'undefined' && heaters.length > 0) logger.warn(`The body temperature for ${body.name} cannot be determined. Heater status for this body cannot be calculated.`);
3474
- for (let j = 0; j < heaters.length; j++) {
3475
- let heater: Heater = heaters[j];
3476
- if (heater.isActive === false) continue;
3477
- let isOn = false;
3478
- let isCooling = false;
3479
- let sensorTemp = state.temps.waterSensor1;
3480
- if (body.id === 4) sensorTemp = state.temps.waterSensor4;
3481
- if (body.id === 3) sensorTemp = state.temps.waterSensor3;
3482
- if (body.id === 2 && !sys.equipment.shared) sensorTemp = state.temps.waterSensor2;
3483
-
3484
- // Determine whether the heater can be used on this body.
3485
- let isAssociated = false;
3486
- let b = sys.board.valueMaps.bodies.transform(heater.body);
3487
- switch (b.name) {
3488
- case 'body1':
3489
- case 'pool':
3490
- if (body.id === 1) isAssociated = true;
3491
- break;
3492
- case 'body2':
3493
- case 'spa':
3494
- if (body.id === 2) isAssociated = true;
3495
- break;
3496
- case 'poolspa':
3497
- if (body.id === 1 || body.id === 2) isAssociated = true;
3498
- break;
3499
- case 'body3':
3500
- if (body.id === 3) isAssociated = true;
3501
- break;
3502
- case 'body4':
3503
- if (body.id === 4) isAssociated = true;
3504
- break;
4248
+ case 'heatpump':
4249
+ if (mode === 'heatpump' || mode === 'heatpumppref') {
4250
+ if (hstate.isOn) {
4251
+ // If the heater is already on we will heat to 1 degree past the setpoint.
4252
+ if (body.temp - 1 < cfgBody.heatSetpoint) {
4253
+ isOn = true;
4254
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
4255
+ isHeating = true;
4256
+ isCooling = false;
3505
4257
  }
3506
- // logger.silly(`Heater ${heater.name} is ${isAssociated === true ? '' : 'not '}associated with ${body.name}`);
3507
- if (isAssociated) {
3508
- let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
3509
- let status = sys.board.valueMaps.heatStatus.transform(body.heatStatus);
3510
- let hstate = state.heaters.getItemById(heater.id, true);
3511
- if (heater.master === 1) {
3512
- // We need to do our own calculation as to whether it is on. This is for Nixie heaters.
3513
- let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
3514
- switch (htype.name) {
3515
- case 'solar':
3516
- if (mode === 'solar' || mode === 'solarpref') {
3517
- // Measure up against start and stop temp deltas for effective solar heating.
3518
- if (body.temp < cfgBody.heatSetpoint &&
3519
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
3520
- isOn = true;
3521
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
3522
- isHeating = true;
3523
- }
3524
- else if (heater.coolingEnabled && body.temp > cfgBody.coolSetpoint && state.heliotrope.isNight &&
3525
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
3526
- isOn = true;
3527
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
3528
- isHeating = true;
3529
- isCooling = true;
3530
- }
3531
- }
3532
- break;
3533
- case 'ultratemp':
3534
- // We need to determine whether we are going to use the air temp or the solar temp
3535
- // for the sensor.
3536
- let deltaTemp = Math.max(state.temps.air, state.temps.solar || 0);
3537
- if (mode === 'ultratemp' || mode === 'ultratemppref') {
3538
- if (body.temp < cfgBody.heatSetpoint &&
3539
- deltaTemp > body.temp + heater.differentialTemp || 0) {
3540
- isOn = true;
3541
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
3542
- isHeating = true;
3543
- isCooling = false;
3544
- }
3545
- else if (body.temp > cfgBody.coolSetpoint && heater.coolingEnabled) {
3546
- isOn = true;
3547
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
3548
- isHeating = true;
3549
- isCooling = true;
3550
- }
3551
- }
3552
- break;
3553
- case 'mastertemp':
3554
- if (mode === 'mtheater') {
3555
- if (body.temp < cfgBody.setPoint) {
3556
- isOn = true;
3557
- body.heatStatus = sys.board.valueMaps.heaterTypes.getValue('mtheater');
3558
- isHeating = true;
3559
- }
3560
- }
3561
- break;
3562
- case 'maxetherm':
3563
- case 'gas':
3564
- if (mode === 'heater') {
3565
- if (body.temp < cfgBody.setPoint) {
3566
- isOn = true;
3567
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
3568
- isHeating = true;
3569
- }
3570
- }
3571
- else if (mode === 'solarpref' || mode === 'heatpumppref') {
3572
- // If solar should be running gas heater should be off.
3573
- if (body.temp < cfgBody.setPoint &&
3574
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) isOn = false;
3575
- else if (body.temp < cfgBody.setPoint) {
3576
- isOn = true;
3577
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
3578
- isHeating = true;
3579
- }
3580
- }
3581
- break;
3582
- case 'heatpump':
3583
- if (mode === 'heatpump' || mode === 'heatpumppref') {
3584
- if (body.temp < cfgBody.setPoint &&
3585
- state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) {
3586
- isOn = true;
3587
- body.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
3588
- isHeating = true;
3589
- }
3590
- }
3591
- break;
3592
- default:
3593
- isOn = utils.makeBool(hstate.isOn);
3594
- break;
3595
- }
3596
- logger.debug(`Heater Type: ${htype.name} Mode:${mode} Temp: ${body.temp} Setpoint: ${cfgBody.setPoint} Status: ${body.heatStatus}`);
3597
- }
3598
- else {
3599
- let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
3600
- switch (htype.name) {
3601
- case 'mastertemp':
3602
- if (status === 'mtheater') isHeating = isOn = true;
3603
- break;
3604
- case 'maxetherm':
3605
- case 'gas':
3606
- if (status === 'heater') isHeating = isOn = true;
3607
- break;
3608
- case 'hybrid':
3609
- case 'ultratemp':
3610
- case 'heatpump':
3611
- if (mode === 'ultratemp' || mode === 'ultratemppref' || mode === 'heatpump' || mode === 'heatpumppref') {
3612
- if (status === 'heater') isHeating = isOn = true;
3613
- else if (status === 'cooling') isCooling = isOn = true;
3614
- }
3615
- break;
3616
- case 'solar':
3617
- if (mode === 'solar' || mode === 'solarpref') {
3618
- if (status === 'solar') isHeating = isOn = true;
3619
- else if (status === 'cooling') isCooling = isOn = true;
3620
- }
3621
- break;
3622
- }
3623
- }
3624
- if (isOn === true && typeof hon.find(elem => elem === heater.id) === 'undefined') {
3625
- hon.push(heater.id);
3626
- if (heater.master === 1 && isOn) (async () => {
3627
- try {
3628
- await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
3629
- } catch (err) { logger.error(err.message); }
3630
- })();
3631
- else hstate.isOn = isOn;
3632
- }
4258
+ }
4259
+ else {
4260
+ // The heater is not currently on lets turn it on if we pass all the criteria.
4261
+ if ((body.temp < cfgBody.heatSetpoint && hstate.endTime.getTime() < new Date().getTime() + (30 * 60 * 1000))
4262
+ || body.temp + heater.differentialTemp < cfgBody.heatSetpoint) {
4263
+ isOn = true;
4264
+ body.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpcool');
4265
+ isHeating = true;
4266
+ isCooling = false;
3633
4267
  }
4268
+ }
3634
4269
  }
4270
+ break;
4271
+ default:
4272
+ isOn = utils.makeBool(hstate.isOn);
4273
+ break;
3635
4274
  }
3636
- // When the controller is a virtual one we need to control the heat status ourselves.
3637
- if (!isHeating && (sys.controllerType === ControllerType.Nixie)) body.heatStatus = 0;
4275
+ logger.debug(`Heater Type: ${htype.name} Mode:${mode} Temp: ${body.temp} Setpoint: ${cfgBody.setPoint} Status: ${body.heatStatus}`);
4276
+ }
3638
4277
  }
3639
- // Turn off any heaters that should be off. The code above only turns heaters on.
3640
- for (let i = 0; i < heaters.length; i++) {
3641
- let heater: Heater = heaters[i];
3642
- if (typeof hon.find(elem => elem === heater.id) === 'undefined') {
3643
- let hstate = state.heaters.getItemById(heater.id, true);
3644
- if (heater.master === 1) (async () => {
3645
- try {
3646
- await ncp.heaters.setHeaterStateAsync(hstate, false, false);
3647
- } catch (err) { logger.error(err.message); }
3648
- })();
3649
- else hstate.isOn = false;
3650
- }
4278
+ else {
4279
+ let mode = sys.board.valueMaps.heatModes.getName(body.heatMode);
4280
+ switch (htype.name) {
4281
+ case 'mastertemp':
4282
+ if (hstatus === 'mtheat') isHeating = isOn = true;
4283
+ break;
4284
+ case 'maxetherm':
4285
+ case 'gas':
4286
+ if (hstatus === 'heater') isHeating = isOn = true;
4287
+ break;
4288
+ case 'hybrid':
4289
+ if (hstatus === 'mtheat' || hstatus === 'heater' || hstatus === 'dual') isHeating = isOn = true;
4290
+ break;
4291
+ case 'ultratemp':
4292
+ case 'heatpump':
4293
+ if (mode === 'ultratemp' || mode === 'ultratemppref' || mode === 'heatpump' || mode === 'heatpumppref') {
4294
+ if (hstatus === 'heater') isHeating = isOn = true;
4295
+ else if (hstatus === 'cooling') isCooling = isOn = true;
4296
+ }
4297
+ break;
4298
+ case 'solar':
4299
+ if (mode === 'solar' || mode === 'solarpref') {
4300
+ if (hstatus === 'solar') isHeating = isOn = true;
4301
+ else if (hstatus === 'cooling') isCooling = isOn = true;
4302
+ }
4303
+ break;
4304
+ }
3651
4305
  }
3652
- } catch (err) { logger.error(`Error synchronizing heater states`); }
3653
- }
4306
+ if (isOn === true && typeof hon.find(elem => elem === heater.id) === 'undefined') {
4307
+ hon.push(heater.id);
4308
+ if (heater.master === 1 && isOn) (async () => {
4309
+ try {
4310
+ hstate.bodyId = body.id;
4311
+ if (sys.board.valueMaps.heatStatus.getName(body.heatStatus) === 'cooldown')
4312
+ await ncp.heaters.setHeaterStateAsync(hstate, false, false);
4313
+ else if (isOn) {
4314
+ hstate.bodyId = body.id;
4315
+ await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
4316
+ }
4317
+ else if (hstate.isOn !== isOn || hstate.isCooling !== isCooling) {
4318
+ await ncp.heaters.setHeaterStateAsync(hstate, isOn, isCooling);
4319
+ }
4320
+ } catch (err) { logger.error(err.message); }
4321
+ })();
4322
+ else {
4323
+ hstate.isOn = isOn;
4324
+ hstate.bodyId = body.id;
4325
+ }
4326
+ }
4327
+ // If there is a heater on for the body we need break out of the loop. This will make sure for instance a gas heater
4328
+ // isn't started when one of the more economical methods are.
4329
+ if (isOn === true) break;
4330
+ }
4331
+ }
4332
+ if (sys.controllerType === ControllerType.Nixie && !isHeating && !isCooling && hstatus !== 'cooldown') body.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
4333
+ //else if (sys.controllerType === ControllerType.Nixie) body.heatStatus = 0;
4334
+ }
4335
+ // Turn off any heaters that should be off. The code above only turns heaters on.
4336
+ for (let i = 0; i < heaters.length; i++) {
4337
+ let heater: Heater = heaters[i];
4338
+ if (typeof hon.find(elem => elem === heater.id) === 'undefined') {
4339
+ let hstate = state.heaters.getItemById(heater.id, true);
4340
+ if (heater.master === 1) (async () => {
4341
+ try {
4342
+ await ncp.heaters.setHeaterStateAsync(hstate, false, false);
4343
+ hstate.bodyId = 0;
4344
+ } catch (err) { logger.error(err.message); }
4345
+ })();
4346
+ else {
4347
+ hstate.isOn = false;
4348
+ hstate.bodyId = 0;
4349
+ }
4350
+ }
4351
+ }
4352
+ } catch (err) { logger.error(`Error synchronizing heater states: ${err.message}`); }
4353
+ }
3654
4354
  }
3655
4355
  export class ValveCommands extends BoardCommands {
3656
4356
  public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
@@ -3750,6 +4450,22 @@ export class ValveCommands extends BoardCommands {
3750
4450
  }
3751
4451
  public async syncValveStates() {
3752
4452
  try {
4453
+ // Check to see if there is a drain circuit or feature on. If it is on then the intake will be diverted no mater what.
4454
+ let drain = sys.equipment.shared ? typeof state.circuits.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spadrain' && elem.isOn === true) !== 'undefined' ||
4455
+ typeof state.features.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spadrain' && elem.isOn === true) !== 'undefined' : false;
4456
+ // Check to see if there is a spillway circuit or feature on. If it is on then the return will be diverted no mater what.
4457
+ let spillway = sys.equipment.shared ? typeof state.circuits.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined' ||
4458
+ typeof state.features.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined' : false;
4459
+ let spa = sys.equipment.shared ? state.circuits.getItemById(1).isOn : false;
4460
+ let pool = sys.equipment.shared ? state.circuits.getItemById(6).isOn : false;
4461
+ // Set the valve mode.
4462
+ if (!sys.equipment.shared) state.valveMode = sys.board.valueMaps.valveModes.getValue('off');
4463
+ else if (drain) state.valveMode = sys.board.valueMaps.valveModes.getValue('spadrain');
4464
+ else if (spillway) state.valveMode = sys.board.valueMaps.valveModes.getValue('spillway');
4465
+ else if (spa) state.valveMode = sys.board.valueMaps.valveModes.getValue('spa');
4466
+ else if (pool) state.valveMode = sys.board.valueMaps.valveModes.getValue('pool');
4467
+ else state.valveMode = sys.board.valueMaps.valveModes.getValue('off');
4468
+
3753
4469
  for (let i = 0; i < sys.valves.length; i++) {
3754
4470
  // Run through all the valves to see whether they should be triggered or not.
3755
4471
  let valve = sys.valves.getItemByIndex(i);
@@ -3757,13 +4473,21 @@ export class ValveCommands extends BoardCommands {
3757
4473
  let vstate = state.valves.getItemById(valve.id, true);
3758
4474
  let isDiverted = vstate.isDiverted;
3759
4475
  if (typeof valve.circuit !== 'undefined' && valve.circuit > 0) {
3760
- if (sys.equipment.shared && valve.isIntake === true)
3761
- isDiverted = utils.makeBool(state.circuits.getItemById(1).isOn); // If the spa is on then the intake is diverted.
4476
+ if (sys.equipment.shared && valve.isIntake === true) {
4477
+ // Valve Diverted Positions
4478
+ // Spa: Y
4479
+ // Drain: Y
4480
+ // Spillway: N
4481
+ // Pool: N
4482
+ isDiverted = utils.makeBool(spa || drain); // If the spa is on then the intake is diverted.
4483
+ }
3762
4484
  else if (sys.equipment.shared && valve.isReturn === true) {
3763
- // Check to see if there is a spillway circuit or feature on. If it is on then the return will be diverted no mater what.
3764
- let spillway = typeof state.circuits.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined' ||
3765
- typeof state.features.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined';
3766
- isDiverted = utils.makeBool(spillway || state.circuits.getItemById(1).isOn);
4485
+ // Valve Diverted Positions
4486
+ // Spa: Y
4487
+ // Drain: N
4488
+ // Spillway: Y
4489
+ // Pool: N
4490
+ isDiverted = utils.makeBool((spa || spillway) && !drain);
3767
4491
  }
3768
4492
  else {
3769
4493
  let circ = state.circuits.getInterfaceById(valve.circuit);
@@ -3779,6 +4503,35 @@ export class ValveCommands extends BoardCommands {
3779
4503
  }
3780
4504
  } catch (err) { logger.error(`syncValveStates: Error synchronizing valves ${err.message}`); }
3781
4505
  }
4506
+ public getBodyValveCircuitIds(isOn?: boolean): number[] {
4507
+ let arrIds: number[] = [];
4508
+ if (sys.equipment.shared !== true) return arrIds;
4509
+
4510
+ {
4511
+ let dtype = sys.board.valueMaps.circuitFunctions.getValue('spadrain');
4512
+ let stype = sys.board.valueMaps.circuitFunctions.getValue('spillway');
4513
+ let ptype = sys.board.valueMaps.circuitFunctions.getValue('pool');
4514
+ let sptype = sys.board.valueMaps.circuitFunctions.getValue('spa');
4515
+ for (let i = 0; i < state.circuits.length; i++) {
4516
+ let cstate = state.circuits.getItemByIndex(i);
4517
+ if (typeof isOn === 'undefined' || cstate.isOn === isOn) {
4518
+ if (cstate.id === 1 || cstate.id === 6) arrIds.push(cstate.id);
4519
+ if (cstate.type === dtype || cstate.type === stype || cstate.type === ptype || cstate.type === sptype) arrIds.push(cstate.id);
4520
+ }
4521
+ }
4522
+ }
4523
+ {
4524
+ let dtype = sys.board.valueMaps.featureFunctions.getValue('spadrain');
4525
+ let stype = sys.board.valueMaps.featureFunctions.getValue('spillway');
4526
+ for (let i = 0; i < state.features.length; i++) {
4527
+ let fstate = state.features.getItemByIndex(i);
4528
+ if (typeof isOn === 'undefined' || fstate.isOn === isOn) {
4529
+ if (fstate.type === dtype || fstate.type === stype) arrIds.push(fstate.id);
4530
+ }
4531
+ }
4532
+ }
4533
+ return arrIds;
4534
+ }
3782
4535
  }
3783
4536
  export class ChemControllerCommands extends BoardCommands {
3784
4537
  public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise<boolean> {
@@ -3910,7 +4663,7 @@ export class ChemControllerCommands extends BoardCommands {
3910
4663
  protected async setIntelliChemAsync(data: any): Promise<ChemController> {
3911
4664
  try {
3912
4665
  let chem = sys.chemControllers.getItemById(data.id);
3913
- return await ncp.chemControllers.setControllerAsync(chem, data);
4666
+ return chem.master === 1 ? await ncp.chemControllers.setControllerAsync(chem, data) : chem;
3914
4667
  } catch (err) { return Promise.reject(err); }
3915
4668
  }
3916
4669
  public findChemController(data: any) {
@@ -3935,11 +4688,11 @@ export class ChemControllerCommands extends BoardCommands {
3935
4688
  let type = sys.board.valueMaps.chemControllerTypes.encode(isAdd ? data.type : chem.type);
3936
4689
  if (typeof type === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`The chem controller type could not be determined ${data.type || type}`, 'chemController', type));
3937
4690
  if (isAdd && sys.equipment.maxChemControllers <= sys.chemControllers.length) return Promise.reject(new InvalidEquipmentDataError(`The maximum number of chem controllers have been added to your controller`, 'chemController', sys.equipment.maxChemControllers));
3938
- let address = parseInt(data.address, 10);
4691
+ let address = typeof data.address !== 'undefined' ? parseInt(data.address, 10) : isAdd ? undefined : chem.address;
3939
4692
  let t = sys.board.valueMaps.chemControllerTypes.transform(type);
3940
4693
  if (t.hasAddress) {
3941
4694
  // First lets make sure the user supplied an address.
3942
- if (isNaN(address)) return Promise.reject(new InvalidEquipmentDataError(`${type.desc} chem controllers require a valid address`, 'chemController', data.address));
4695
+ if (isNaN(address)) return Promise.reject(new InvalidEquipmentDataError(`${t.desc} chem controllers require a valid address`, 'chemController', data.address));
3943
4696
  if (typeof sys.chemControllers.find(x => x.address === address && x.id !== (isAdd ? -1 : chem.id)) !== 'undefined') return Promise.reject(new InvalidEquipmentDataError(`${type.desc} chem controller addresses must be unique`, 'chemController', data.address));
3944
4697
  }
3945
4698
  if (isAdd) {
@@ -3952,8 +4705,10 @@ export class ChemControllerCommands extends BoardCommands {
3952
4705
  chem.isActive = true;
3953
4706
  // So here is the thing. If you have an OCP then the IntelliChem must be controlled by that.
3954
4707
  // the messages on the bus will talk back to the OCP so if you do not do this mayhem will ensue.
3955
- if (type.name === 'intellichem')
3956
- await this.setIntelliChemAsync(data);
4708
+ if (t.name === 'intellichem') {
4709
+ logger.info(`${chem.name} - ${chem.id} routing IntelliChem to OCP`);
4710
+ await sys.board.chemControllers.setIntelliChemAsync(data);
4711
+ }
3957
4712
  else
3958
4713
  await ncp.chemControllers.setControllerAsync(chem, data);
3959
4714
  return Promise.resolve(chem);
@@ -3966,7 +4721,8 @@ export class ChemControllerCommands extends BoardCommands {
3966
4721
  let chem = sys.board.chemControllers.findChemController(data);
3967
4722
  if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`A valid chem controller could not be found for id:${data.id} or address ${data.address}`, data.id || data.address, 'chemController'));
3968
4723
  data.id = chem.id;
3969
- if (chem.master !== 0) await ncp.chemControllers.setControllerAsync(chem, data);
4724
+ logger.info(`Setting ${chem.name} data ${chem.master}`);
4725
+ if (chem.master === 1) await ncp.chemControllers.setControllerAsync(chem, data);
3970
4726
  else await sys.board.chemControllers.setChemControllerAsync(data);
3971
4727
  let schem = state.chemControllers.getItemById(chem.id, true);
3972
4728
  return Promise.resolve(schem);
@@ -4079,7 +4835,7 @@ export class FilterCommands extends BoardCommands {
4079
4835
  if (f.type !== 0) fon = true;
4080
4836
  // Check to see if this feature is used on a valve. This will make it
4081
4837
  // not include this pressure either. We do not care whether the valve is diverted or not.
4082
- if (typeof sys.valves.find(elem => elem.circuitId === f.id) !== 'undefined')
4838
+ if (typeof sys.valves.find(elem => elem.circuit === f.id) !== 'undefined')
4083
4839
  fon = true;
4084
4840
  else {
4085
4841
  // Finally if the feature happens to be used on a pump then we don't want it either.