nodejs-poolcontroller 7.3.0 → 7.6.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 (60) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/Changelog +23 -0
  3. package/README.md +5 -5
  4. package/app.ts +2 -0
  5. package/config/Config.ts +3 -0
  6. package/config/VersionCheck.ts +8 -4
  7. package/controller/Constants.ts +88 -0
  8. package/controller/Equipment.ts +246 -66
  9. package/controller/Errors.ts +24 -1
  10. package/controller/Lockouts.ts +423 -0
  11. package/controller/State.ts +314 -54
  12. package/controller/boards/EasyTouchBoard.ts +107 -59
  13. package/controller/boards/IntelliCenterBoard.ts +186 -125
  14. package/controller/boards/IntelliTouchBoard.ts +104 -30
  15. package/controller/boards/NixieBoard.ts +721 -159
  16. package/controller/boards/SystemBoard.ts +2370 -1108
  17. package/controller/comms/Comms.ts +85 -10
  18. package/controller/comms/messages/Messages.ts +10 -4
  19. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -4
  20. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  21. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  22. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  23. package/controller/comms/messages/config/ExternalMessage.ts +44 -26
  24. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  25. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  26. package/controller/comms/messages/config/HeaterMessage.ts +15 -9
  27. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  28. package/controller/comms/messages/config/OptionsMessage.ts +13 -1
  29. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  30. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  31. package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
  32. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  33. package/controller/comms/messages/config/ValveMessage.ts +13 -3
  34. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
  35. package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
  36. package/controller/comms/messages/status/HeaterStateMessage.ts +42 -9
  37. package/controller/comms/messages/status/IntelliChemStateMessage.ts +37 -26
  38. package/controller/nixie/Nixie.ts +18 -16
  39. package/controller/nixie/bodies/Body.ts +4 -1
  40. package/controller/nixie/chemistry/ChemController.ts +80 -77
  41. package/controller/nixie/chemistry/Chlorinator.ts +9 -8
  42. package/controller/nixie/circuits/Circuit.ts +55 -6
  43. package/controller/nixie/heaters/Heater.ts +192 -32
  44. package/controller/nixie/pumps/Pump.ts +146 -84
  45. package/controller/nixie/schedules/Schedule.ts +3 -2
  46. package/controller/nixie/valves/Valve.ts +1 -1
  47. package/defaultConfig.json +32 -1
  48. package/issue_template.md +1 -1
  49. package/logger/DataLogger.ts +37 -22
  50. package/package.json +20 -18
  51. package/web/Server.ts +520 -29
  52. package/web/bindings/influxDB.json +96 -8
  53. package/web/bindings/mqtt.json +151 -40
  54. package/web/bindings/mqttAlt.json +114 -4
  55. package/web/interfaces/httpInterface.ts +2 -0
  56. package/web/interfaces/influxInterface.ts +36 -19
  57. package/web/interfaces/mqttInterface.ts +14 -3
  58. package/web/services/config/Config.ts +171 -44
  59. package/web/services/state/State.ts +49 -5
  60. package/web/services/state/StateSocket.ts +18 -1
@@ -17,14 +17,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
  import * as extend from 'extend';
18
18
  import { EventEmitter } from 'events';
19
19
  import { ncp } from "../nixie/Nixie";
20
+ import { NixieHeaterBase } from "../nixie/heaters/Heater";
20
21
  import { utils, Heliotrope, Timestamp } from '../Constants';
21
22
  import {SystemBoard, byteValueMap, ConfigQueue, ConfigRequest, BodyCommands, FilterCommands, PumpCommands, SystemCommands, CircuitCommands, FeatureCommands, ValveCommands, HeaterCommands, ChlorinatorCommands, ChemControllerCommands, EquipmentIdRange} from './SystemBoard';
22
23
  import { logger } from '../../logger/Logger';
23
- import { state, ChlorinatorState, ChemControllerState, TemperatureState, VirtualCircuitState, ICircuitState, ICircuitGroupState, LightGroupState, ValveState, FilterState } from '../State';
24
+ import { state, ChlorinatorState, ChemControllerState, TemperatureState, VirtualCircuitState, CircuitState, ICircuitState, ICircuitGroupState, LightGroupState, ValveState, FilterState, BodyTempState, FeatureState } from '../State';
24
25
  import { sys, Equipment, Options, Owner, Location, CircuitCollection, TempSensorCollection, General, PoolSystem, Body, Pump, CircuitGroupCircuit, CircuitGroup, ChemController, Circuit, Feature, Valve, ICircuit, Heater, LightGroup, LightGroupCircuit, ControllerType, Filter } from '../Equipment';
25
26
  import { Protocol, Outbound, Message, Response } from '../comms/messages/Messages';
26
- import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ParameterOutOfRangeError } from '../Errors';
27
- import {conn} from '../comms/Comms';
27
+ import { BoardProcessError, EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ParameterOutOfRangeError } from '../Errors';
28
+ import { conn } from '../comms/Comms';
29
+ import { delayMgr } from '../Lockouts';
28
30
  export class NixieBoard extends SystemBoard {
29
31
  constructor (system: PoolSystem){
30
32
  super(system);
@@ -36,21 +38,28 @@ export class NixieBoard extends SystemBoard {
36
38
  this.equipmentIds.features.start = 129;
37
39
  this.equipmentIds.circuitGroups.start = 193;
38
40
  this.equipmentIds.virtualCircuits.start = 237;
41
+ this.valueMaps.featureFunctions = new byteValueMap([
42
+ [0, { name: 'generic', desc: 'Generic' }],
43
+ [1, { name: 'spillway', desc: 'Spillway' }],
44
+ [2, { name: 'spadrain', desc: 'Spa Drain' }]
45
+ ]);
39
46
  this.valueMaps.circuitFunctions = new byteValueMap([
40
47
  [0, { name: 'generic', desc: 'Generic' }],
41
48
  [1, { name: 'spillway', desc: 'Spillway' }],
42
- [2, { name: 'mastercleaner', desc: 'Master Cleaner' }],
49
+ [2, { name: 'mastercleaner', desc: 'Master Cleaner', body: 1 }],
43
50
  [3, { name: 'chemrelay', desc: 'Chem Relay' }],
44
51
  [4, { name: 'light', desc: 'Light', isLight: true }],
45
- [5, { name: 'intellibrite', desc: 'Intellibrite', isLight: true }],
46
- [6, { name: 'globrite', desc: 'GloBrite', isLight: true }],
52
+ [5, { name: 'intellibrite', desc: 'Intellibrite', isLight: true, theme: 'intellibrite' }],
53
+ [6, { name: 'globrite', desc: 'GloBrite', isLight: true, theme: 'intellibrite' }],
47
54
  [7, { name: 'globritewhite', desc: 'GloBrite White', isLight: true }],
48
- [8, { name: 'magicstream', desc: 'Magicstream', isLight: true }],
55
+ [8, { name: 'magicstream', desc: 'Magicstream', isLight: true, theme: 'magicstream' }],
49
56
  [9, { name: 'dimmer', desc: 'Dimmer', isLight: true }],
50
- [10, { name: 'colorcascade', desc: 'ColorCascade', isLight: true }],
51
- [11, { name: 'mastercleaner2', desc: 'Master Cleaner 2' }],
52
- [12, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
53
- [13, { name: 'spa', desc: 'Spa', hasHeatSource: true }]
57
+ [10, { name: 'colorcascade', desc: 'ColorCascade', isLight: true, theme: 'intellibrite' }],
58
+ [11, { name: 'mastercleaner2', desc: 'Master Cleaner 2', body: 2 }],
59
+ [12, { name: 'pool', desc: 'Pool', hasHeatSource: true, body: 1 }],
60
+ [13, { name: 'spa', desc: 'Spa', hasHeatSource: true, body: 2 }],
61
+ [14, { name: 'colorlogic', desc: 'ColorLogic', isLight: true, theme: 'colorlogic' }],
62
+ [15, { name: 'spadrain', desc: 'Spa Drain'}]
54
63
  ]);
55
64
  this.valueMaps.pumpTypes = new byteValueMap([
56
65
  [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody: true, maxRelays: 1 }],
@@ -81,6 +90,20 @@ export class NixieBoard extends SystemBoard {
81
90
  [2, { name: 'off', desc: 'Off' }],
82
91
  [3, { name: 'ignore', desc: 'Ignore' }]
83
92
  ]);
93
+ this.valueMaps.chlorinatorModel = new byteValueMap([
94
+ [0, { name: 'unknown', desc: 'unknown', capacity: 0, chlorinePerDay: 0, chlorinePerSec: 0 }],
95
+ [1, { name: 'intellichlor--15', desc: 'IntelliChlor IC15', capacity: 15000, chlorinePerDay: 0.60, chlorinePerSec: 0.60 / 86400 }],
96
+ [2, { name: 'intellichlor--20', desc: 'IntelliChlor IC20', capacity: 20000, chlorinePerDay: 0.70, chlorinePerSec: 0.70 / 86400 }],
97
+ [3, { name: 'intellichlor--40', desc: 'IntelliChlor IC40', capacity: 40000, chlorinePerDay: 1.40, chlorinePerSec: 1.4 / 86400 }],
98
+ [4, { name: 'intellichlor--60', desc: 'IntelliChlor IC60', capacity: 60000, chlorinePerDay: 2, chlorinePerSec: 2 / 86400 }],
99
+ [5, { name: 'aquarite-t15', desc: 'AquaRite T15', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }],
100
+ [6, { name: 'aquarite-t9', desc: 'AquaRite T9', capacity: 30000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }],
101
+ [7, { name: 'aquarite-t5', desc: 'AquaRite T5', capacity: 20000, chlorinePerDay: 0.735, chlorinePerSec: 0.735 / 86400 }],
102
+ [8, { name: 'aquarite-t3', desc: 'AquaRite T3', capacity: 15000, chlorinePerDay: 0.53, chlorinePerSec: 0.53 / 86400 }],
103
+ [9, { name: 'aquarite-925', desc: 'AquaRite 925', capacity: 25000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }],
104
+ [10, { name: 'aquarite-940', desc: 'AquaRite 940', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }]
105
+ ]);
106
+
84
107
 
85
108
  // Keep this around for now so I can fart with the custom names array.
86
109
  //this.valueMaps.customNames = new byteValueMap(
@@ -117,28 +140,51 @@ export class NixieBoard extends SystemBoard {
117
140
  [245, { name: 'spaHeater', desc: 'Spa Heater' }],
118
141
  [246, { name: 'freeze', desc: 'Freeze' }],
119
142
  [247, { name: 'poolSpa', desc: 'Pool/Spa' }],
120
- [248, { name: 'solarHeat', desc: 'Solar Heat' }],
121
143
  [251, { name: 'heater', desc: 'Heater' }],
122
144
  [252, { name: 'solar', desc: 'Solar' }],
123
- [255, { name: 'poolHeatEnable', desc: 'Pool Heat Enable' }]
145
+ [253, { name: 'solar1', desc: 'Solar Body 1' }],
146
+ [254, { name: 'solar2', desc: 'Solar Body 2' }],
147
+ [255, { name: 'solar3', desc: 'Solar Body 3' }],
148
+ [256, { name: 'solar4', desc: 'Solar Body 4' }],
149
+ [257, { name: 'poolHeatEnable', desc: 'Pool Heat Enable' }]
124
150
  ]);
125
151
  this.valueMaps.scheduleTimeTypes.merge([
126
152
  [1, { name: 'sunrise', desc: 'Sunrise' }],
127
153
  [2, { name: 'sunset', desc: 'Sunset' }]
128
154
  ]);
155
+
129
156
  this.valueMaps.lightThemes = new byteValueMap([
130
- [0, { name: 'white', desc: 'White', type: 'intellibrite', sequence: 11 }],
131
- [1, { name: 'green', desc: 'Green', type: 'intellibrite', sequence: 9 }],
132
- [2, { name: 'blue', desc: 'Blue', type: 'intellibrite', sequence: 8 }],
133
- [3, { name: 'magenta', desc: 'Magenta', type: 'intellibrite', sequence: 12 }],
134
- [4, { name: 'red', desc: 'Red', type: 'intellibrite', sequence: 10 }],
135
- [5, { name: 'sam', desc: 'SAm Mode', type: 'intellibrite', sequence: 1 }],
136
- [6, { name: 'party', desc: 'Party', type: 'intellibrite', sequence: 2 }],
137
- [7, { name: 'romance', desc: 'Romance', type: 'intellibrite', sequence: 3 }],
138
- [8, { name: 'caribbean', desc: 'Caribbean', type: 'intellibrite', sequence: 4 }],
139
- [9, { name: 'american', desc: 'American', type: 'intellibrite', sequence: 5 }],
140
- [10, { name: 'sunset', desc: 'Sunset', type: 'intellibrite', sequence: 6 }],
141
- [11, { name: 'royal', desc: 'Royal', type: 'intellibrite', sequence: 7 }],
157
+ // IntelliBrite Themes
158
+ [0, { name: 'white', desc: 'White', types: ['intellibrite', 'magicstream'], sequence: 11 }],
159
+ [1, { name: 'green', desc: 'Green', types: ['intellibrite', 'magicstream'], sequence: 9 }],
160
+ [2, { name: 'blue', desc: 'Blue', types: ['intellibrite', 'magicstream'], sequence: 8 }],
161
+ [3, { name: 'magenta', desc: 'Magenta', types: ['intellibrite', 'magicstream'], sequence: 12 }],
162
+ [4, { name: 'red', desc: 'Red', types: ['intellibrite', 'magicstream'], sequence: 10 }],
163
+ [5, { name: 'sam', desc: 'SAm Mode', types: ['intellibrite', 'magicstream'], sequence: 1 }],
164
+ [6, { name: 'party', desc: 'Party', types: ['intellibrite', 'magicstream'], sequence: 2 }],
165
+ [7, { name: 'romance', desc: 'Romance', types: ['intellibrite', 'magicstream'], sequence: 3 }],
166
+ [8, { name: 'caribbean', desc: 'Caribbean', types: ['intellibrite', 'magicstream'], sequence: 4 }],
167
+ [9, { name: 'american', desc: 'American', types: ['intellibrite', 'magicstream'], sequence: 5 }],
168
+ [10, { name: 'sunset', desc: 'Sunset', types: ['intellibrite', 'magicstream'], sequence: 6 }],
169
+ [11, { name: 'royal', desc: 'Royal', types: ['intellibrite', 'magicstream'], sequence: 7 }],
170
+ // ColorLogic Themes
171
+ [20, { name: 'cloudwhite', desc: 'Cloud White', types: ['colorlogic'], sequence: 7 }],
172
+ [21, { name: 'deepsea', desc: 'Deep Sea', types: ['colorlogic'], sequence: 2 }],
173
+ [22, { name: 'royalblue', desc: 'Royal Blue', types: ['colorlogic'], sequence: 3 }],
174
+ [23, { name: 'afternoonskies', desc: 'Afternoon Skies', types: ['colorlogic'], sequence: 4 }],
175
+ [24, { name: 'aquagreen', desc: 'Aqua Green', types: ['colorlogic'], sequence: 5 }],
176
+ [25, { name: 'emerald', desc: 'Emerald', types: ['colorlogic'], sequence: 6 }],
177
+ [26, { name: 'warmred', desc: 'Warm Red', types: ['colorlogic'], sequence: 8 }],
178
+ [27, { name: 'flamingo', desc: 'Flamingo', types: ['colorlogic'], sequence: 9 }],
179
+ [28, { name: 'vividviolet', desc: 'Vivid Violet', types: ['colorlogic'], sequence: 10 }],
180
+ [29, { name: 'sangria', desc: 'Sangria', types: ['colorlogic'], sequence: 11 }],
181
+ [30, { name: 'twilight', desc: 'Twilight', types: ['colorlogic'], sequence: 12 }],
182
+ [31, { name: 'tranquility', desc: 'Tranquility', types: ['colorlogic'], sequence: 13 }],
183
+ [32, { name: 'gemstone', desc: 'Gemstone', types: ['colorlogic'], sequence: 14 }],
184
+ [33, { name: 'usa', desc: 'USA', types: ['colorlogic'], sequence: 15 }],
185
+ [34, { name: 'mardigras', desc: 'Mardi Gras', types: ['colorlogic'], sequence: 16 }],
186
+ [35, { name: 'coolcabaret', desc: 'Cabaret', types: ['colorlogic'], sequence: 17 }],
187
+
142
188
  [255, { name: 'none', desc: 'None' }]
143
189
  ]);
144
190
  this.valueMaps.lightColors = new byteValueMap([
@@ -167,8 +213,10 @@ export class NixieBoard extends SystemBoard {
167
213
  [1, { name: 'heater', desc: 'Heater' }],
168
214
  [2, { name: 'solar', desc: 'Solar' }],
169
215
  [3, { name: 'cooling', desc: 'Cooling' }],
216
+ [6, { name: 'mtheat', desc: 'Heater' }],
170
217
  [4, { name: 'hpheat', desc: 'Heating' }],
171
- [8, { name: 'hpcool', desc: 'Cooling' }]
218
+ [8, { name: 'hpcool', desc: 'Cooling' }],
219
+ [128, {name: 'cooldown', desc: 'Cooldown'}]
172
220
  ]);
173
221
  this.valueMaps.scheduleTypes = new byteValueMap([
174
222
  [0, { name: 'runonce', desc: 'Run Once', startDate: true, startTime: true, endTime: true, days: false, heatSource: true, heatSetpoint: true }],
@@ -225,7 +273,8 @@ export class NixieBoard extends SystemBoard {
225
273
  sys.equipment.maxCustomNames = 0;
226
274
  state.equipment.model = type.desc;
227
275
  state.equipment.maxBodies = sys.equipment.maxBodies = type.bodies;
228
-
276
+ let bodyUnits = sys.general.options.units === 0 ? 1 : 2;
277
+
229
278
  if (typeof state.temps.units === 'undefined' || state.temps.units < 0) state.temps.units = sys.general.options.units;
230
279
  if (type.bodies > 0) {
231
280
  let pool = sys.bodies.getItemById(1, true);
@@ -237,6 +286,7 @@ export class NixieBoard extends SystemBoard {
237
286
  pool.circuit = 6;
238
287
  pool.isActive = true;
239
288
  pool.master = 1;
289
+ pool.capacityUnits = bodyUnits;
240
290
  sbody.name = pool.name;
241
291
  sbody.setPoint = pool.setPoint;
242
292
  sbody.circuit = pool.circuit;
@@ -272,6 +322,7 @@ export class NixieBoard extends SystemBoard {
272
322
  sbody.setPoint = spa.setPoint;
273
323
  sbody.circuit = spa.circuit;
274
324
  sbody.type = spa.type;
325
+ spa.capacityUnits = bodyUnits;
275
326
  scirc = state.circuits.getItemById(1, true);
276
327
  scirc.showInFeatures = circ.showInFeatures;
277
328
  scirc.type = circ.type;
@@ -295,6 +346,7 @@ export class NixieBoard extends SystemBoard {
295
346
  sys.circuits.removeItemById(6);
296
347
  state.circuits.removeItemById(6);
297
348
  }
349
+
298
350
  sys.equipment.setEquipmentIds();
299
351
  sys.board.bodies.initFilters();
300
352
  state.status = sys.board.valueMaps.controllerStatus.transform(2, 0);
@@ -390,7 +442,7 @@ export class NixieBoard extends SystemBoard {
390
442
  //public chlorinator: NixieChlorinatorCommands = new NixieChlorinatorCommands(this);
391
443
  public bodies: NixieBodyCommands = new NixieBodyCommands(this);
392
444
  public filters: NixieFilterCommands = new NixieFilterCommands(this);
393
- //public pumps: NixiePumpCommands = new NixiePumpCommands(this);
445
+ public pumps: NixiePumpCommands = new NixiePumpCommands(this);
394
446
  //public schedules: NixieScheduleCommands = new NixieScheduleCommands(this);
395
447
  public heaters: NixieHeaterCommands = new NixieHeaterCommands(this);
396
448
  public valves: NixieValveCommands = new NixieValveCommands(this);
@@ -417,12 +469,19 @@ export class NixieFilterCommands extends FilterCommands {
417
469
  try {
418
470
  await ncp.filters.setFilterStateAsync(fstate, isOn);
419
471
  }
420
- catch (err) { return Promise.reject(`Nixie: Error setFiterStateAsync ${err.message}`); }
472
+ catch (err) { return Promise.reject(new BoardProcessError(`Nixie: Error setFiterStateAsync ${err.message}`, 'setFilterStateAsync')); }
421
473
  }
422
474
  }
423
475
 
424
476
  export class NixieSystemCommands extends SystemCommands {
425
- public cancelDelay(): Promise<any> { state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); }
477
+ public cancelDelay(): Promise<any> {
478
+ delayMgr.cancelPumpValveDelays();
479
+ delayMgr.cancelHeaterCooldownDelays();
480
+ delayMgr.cancelHeaterStartupDelays();
481
+ delayMgr.cancelCleanerStartDelays();
482
+ state.delay = sys.board.valueMaps.delay.getValue('nodelay');
483
+ return Promise.resolve(state.data.delay);
484
+ }
426
485
  public setDateTimeAsync(obj: any): Promise<any> { return Promise.resolve(); }
427
486
  public getDOW() { return this.board.valueMaps.scheduleDays.toArray(); }
428
487
  public async setGeneralAsync(obj: any): Promise<General> {
@@ -441,7 +500,20 @@ export class NixieSystemCommands extends SystemCommands {
441
500
  }
442
501
  }
443
502
  export class NixieCircuitCommands extends CircuitCommands {
444
- public async setCircuitStateAsync(id: number, val: boolean): Promise<ICircuitState> {
503
+ // This is our poll loop for circuit relay states.
504
+ public async syncCircuitRelayStates() {
505
+ try {
506
+ for (let i = 0; i < sys.circuits.length; i++) {
507
+ // Run through all the controlled circuits to see whether they should be triggered or not.
508
+ let circ = sys.circuits.getItemByIndex(i);
509
+ if (circ.master === 1 && circ.isActive) {
510
+ let cstate = state.circuits.getItemById(circ.id);
511
+ if (cstate.isOn) await ncp.circuits.setCircuitStateAsync(cstate, cstate.isOn);
512
+ }
513
+ }
514
+ } catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); }
515
+ }
516
+ public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
445
517
  sys.board.suspendStatus(true);
446
518
  try {
447
519
  // We need to do some routing here as it is now critical that circuits, groups, and features
@@ -451,50 +523,348 @@ export class NixieCircuitCommands extends CircuitCommands {
451
523
  else if (sys.board.equipmentIds.features.isInRange(id))
452
524
  return await sys.board.features.setFeatureStateAsync(id, val);
453
525
 
526
+
454
527
  let circuit: ICircuit = sys.circuits.getInterfaceById(id, false, { isActive: false });
455
528
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit or Feature id ${id} not valid`, id, 'Circuit'));
456
529
  let circ = state.circuits.getInterfaceById(id, circuit.isActive !== false);
530
+ if (circ.stopDelay) {
531
+ // Send this off so that the relays are properly set. In the end we cannot change right now. If this
532
+ // happens to be a body circuit then the relay state will be skipped anyway.
533
+ await ncp.circuits.setCircuitStateAsync(circ, circ.isOn);
534
+ return circ;
535
+ }
457
536
  let newState = utils.makeBool(val);
458
- // First, if we are turning the circuit on, lets determine whether the circuit is a pool or spa circuit and if this is a shared system then we need
459
- // to turn off the other body first.
460
- //[12, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
461
- //[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }]
462
- if (newState && (circuit.type === 12 || circuit.type === 13)) {
537
+ let ctype = sys.board.valueMaps.circuitFunctions.getName(circ.type);
538
+ // Filter out any special circuit types.
539
+ switch (ctype) {
540
+ case 'pool':
541
+ case 'spa':
542
+ await this.setBodyCircuitStateAsync(id, newState, ignoreDelays);
543
+ break;
544
+ case 'mastercleaner':
545
+ case 'mastercleaner2':
546
+ await this.setCleanerCircuitStateAsync(id, newState, ignoreDelays);
547
+ break;
548
+ case 'spillway':
549
+ await this.setSpillwayCircuitStateAsync(id, newState, ignoreDelays);
550
+ break;
551
+ case 'spadrain':
552
+ await this.setDrainCircuitStateAsync(id, newState, ignoreDelays);
553
+ break;
554
+ default:
555
+ await ncp.circuits.setCircuitStateAsync(circ, newState);
556
+ await sys.board.processStatusAsync();
557
+ break;
558
+ }
559
+ // Let the main nixie controller set the circuit state and affect the relays if it needs to.
560
+ return state.circuits.getInterfaceById(circ.id);
561
+ }
562
+ catch (err) { logger.error(`Nixie: setCircuitState ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setCircuitStateAsync ${err.message}`, 'setCircuitState')); }
563
+ finally {
564
+ state.emitEquipmentChanges();
565
+ ncp.pumps.syncPumpStates();
566
+ sys.board.suspendStatus(false);
567
+ }
568
+ }
569
+ protected async setCleanerCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
570
+ try {
571
+ let cstate = state.circuits.getItemById(id);
572
+ let circuit = sys.circuits.getItemById(id);
573
+ // We know which body the cleaner belongs to by an attribute on the circuit function.
574
+ let ctype = sys.board.valueMaps.circuitFunctions.get(circuit.type);
575
+ let bstate = state.temps.bodies.getItemById(ctype.body || 1);
576
+ // Cleaner lockout should occur when
577
+ // 1. The body circuit is off.
578
+ // 2. The spillway mode is running.
579
+
580
+ // Optional modes include
581
+ // 1. The current body is heating with solar.
582
+
583
+ // Lockouts are cleared when
584
+ // 1. The above conditions are no longer true.
585
+ // 2. The user requests the circuit to be off.
586
+ if (!val) {
587
+ // We can always turn a cleaner circuit off. Even if a delay is underway.
588
+ delayMgr.clearCleanerStartDelays(bstate.id);
589
+ await ncp.circuits.setCircuitStateAsync(cstate, false);
590
+ }
591
+ else if (val) {
592
+ logger.info(`Setting cleaner circuit ${cstate.name} to ${val}`);
593
+ // Alright we are turning the cleaner on.
594
+ // To turn on the cleaner circuit we must first ensure the body is on. If it is not then we abort.
595
+ if (!bstate.isOn) {
596
+ logger.info(`Cannot turn on cleaner circuit ${cstate.name}. ${bstate.name} is not running`);
597
+ await ncp.circuits.setCircuitStateAsync(cstate, false);
598
+ return cstate;
599
+ }
600
+ // If there is a drain circuit going shut that thing off.
601
+ await this.turnOffDrainCircuits(ignoreDelays);
602
+ // If solar is currently on and the cleaner solar delay is set then we need to calculate a delay
603
+ // to turn on the cleaner.
604
+ let delayTime = 0;
605
+ let dtNow = new Date().getTime();
606
+ if (typeof ignoreDelays === 'undefined' || !ignoreDelays) {
607
+ if (sys.general.options.cleanerSolarDelay && sys.general.options.cleanerSolarDelayTime > 0) {
608
+ let circBody = state.circuits.getItemById(bstate.circuit);
609
+ // If the body has not been on or the solar heater has not been on long enough then we need to delay the startup.
610
+ if (sys.board.valueMaps.heatStatus.getName(bstate.heatStatus) === 'solar') {
611
+ // Check for the solar delay. We need to know when the heater first kicked in. A cleaner and solar
612
+ // heater can run at the same time but the heater must be on long enough for the timer to expire.
613
+
614
+ // The reasoning behind this is so that the booster pump can be assured that there is sufficient pressure
615
+ // for it to start and any air from the solar has had time to purge through the system.
616
+ let heaters = sys.heaters.getSolarHeaters(bstate.id);
617
+ let startTime = 0;
618
+ for (let i = 0; i < heaters.length; i++) {
619
+ let heater = heaters.getItemByIndex(i);
620
+ let hstate = state.heaters.getItemById(heater.id);
621
+ startTime = Math.max(startTime, hstate.startTime.getTime());
622
+ }
623
+ // Lets see if we have a solar start delay.
624
+ delayTime = Math.max(Math.round(((sys.general.options.cleanerSolarDelayTime * 1000) - (dtNow - startTime))) / 1000, delayTime);
625
+ }
626
+ }
627
+ if (sys.general.options.cleanerStartDelay && sys.general.options.cleanerStartDelayTime) {
628
+ let bcstate = state.circuits.getItemById(bstate.circuit);
629
+ // So we should be started. Lets determine whethere there should be any delay.
630
+ delayTime = Math.max(Math.round(((sys.general.options.cleanerStartDelayTime * 1000) - (dtNow - bcstate.startTime.getTime())) / 1000), delayTime);
631
+ logger.info(`Cleaner delay time calculated to ${delayTime}`);
632
+ }
633
+ }
634
+ if (delayTime > 5) delayMgr.setCleanerStartDelay(cstate, bstate.id, delayTime);
635
+ else await ncp.circuits.setCircuitStateAsync(cstate, true);
636
+ }
637
+ return cstate;
638
+ } catch (err) { return Promise.reject(new BoardProcessError(`Nixie: Error setting cleaner circuit state: ${err.message}`, 'setCleanerCircuitStateAsync')); }
639
+ }
640
+ protected async setBodyCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<CircuitState> {
641
+ try {
642
+ let cstate = state.circuits.getItemById(id);
643
+ let circuit = sys.circuits.getItemById(id);
644
+ let bstate = state.temps.bodies.getBodyByCircuitId(id);
645
+ if (val) {
646
+ // We are turning on a body circuit.
647
+ logger.verbose(`Turning on a body circuit ${bstate.name}`);
463
648
  if (sys.equipment.shared === true) {
649
+ // If we are turning on and this is a shared system it means that we need to turn off
650
+ // the other circuit.
651
+ let delayPumps = false;
652
+ await this.turnOffDrainCircuits(ignoreDelays);
653
+ if (bstate.id === 2) await this.turnOffSpillwayCircuits();
654
+ if (sys.general.options.pumpDelay === true && ignoreDelays !== true) {
655
+ // Now that this is off check the valve positions. If they are not currently in the correct position we need to delay any attached pump
656
+ // so that it does not come on while the valve is rotating. Default 30 seconds.
657
+ let iValves = sys.valves.getIntake();
658
+ for (let i = 0; i < iValves.length && !delayPumps; i++) {
659
+ let vstate = state.valves.getItemById(iValves[i].id);
660
+ if (vstate.isDiverted === true && circuit.type === 12) delayPumps = true;
661
+ else if (vstate.isDiverted === false && circuit.type === 13) delayPumps = true;
662
+ }
663
+ if (!delayPumps) {
664
+ let rValves = sys.valves.getReturn();
665
+ for (let i = 0; i < rValves.length && !delayPumps; i++) {
666
+ let vstate = state.valves.getItemById(rValves[i].id);
667
+ if (vstate.isDiverted === true && circuit.type === 12) delayPumps = true;
668
+ else if (vstate.isDiverted === false && circuit.type === 13) delayPumps = true;
669
+ }
670
+ }
671
+ }
464
672
  // If we are shared we need to turn off the other circuit.
465
- let offType = circ.type === 12 ? 13 : 12;
673
+ let offType = circuit.type === 12 ? 13 : 12;
466
674
  let off = sys.circuits.get().filter(elem => elem.type === offType);
675
+ let delayCooldown = false;
467
676
  // Turn the circuits off that are part of the shared system. We are going back to the board
468
677
  // just in case we got here for a circuit that isn't on the current defined panel.
469
678
  for (let i = 0; i < off.length; i++) {
470
679
  let coff = off[i];
471
- logger.info(`Turning off shared body ${coff.name} circuit`);
472
- await sys.board.circuits.setCircuitStateAsync(coff.id, false);
680
+ let bsoff = state.temps.bodies.getBodyByCircuitId(coff.id);
681
+ let csoff = state.circuits.getItemById(coff.id);
682
+ // Ensure the cleaner circuits for this body are off.
683
+ await this.turnOffCleanerCircuits(bsoff);
684
+ if (csoff.isOn) {
685
+ logger.verbose(`Turning off shared body ${coff.name} circuit`);
686
+ delayMgr.clearBodyStartupDelay(bsoff);
687
+ if (bsoff.heaterCooldownDelay && ignoreDelays !== true) {
688
+ // In this condition we are requesting that the shared body start when the cooldown delay
689
+ // has finished. This will add this request to the cooldown delay code. The setHeaterCooldownDelay
690
+ // code is expected to be re-entrant and checks the id so that it does not clear
691
+ // the original request if it is asked for again.
692
+
693
+ // NOTE: There is room for improvement here. For instance, if the result
694
+ // of turning on the circuit is that the heater(s) requiring cooldown will result in being on
695
+ // then why not cancel the current cooldown cycle and let the user get on with it.
696
+ // Consider:
697
+ // 1. Check each heater attached to the off body to see if it is also attached to the on body.
698
+ // 2. If the heater is attached check to see if there is any cooldown time left on it.
699
+ // 3. If the above conditions are true cancel the cooldown cycle.
700
+ logger.verbose(`${bsoff.name} is already in Cooldown mode`);
701
+ delayMgr.setHeaterCooldownDelay(bsoff, bstate);
702
+ delayCooldown = true;
703
+ }
704
+ else {
705
+ // We need to deal with heater cooldown delays here since you cannot turn off the body while the heater is
706
+ // cooling down. This means we need to check to see if the heater requires cooldown then set a delay for it
707
+ // if it does. The delay manager will shut the body off and start the new body when it is done.
708
+ let heaters = sys.board.heaters.getHeatersByCircuitId(circuit.id);
709
+ let cooldownTime = 0;
710
+ if (ignoreDelays !== true) {
711
+ for (let j = 0; j < heaters.length; j++) {
712
+ let nheater = ncp.heaters.find(x => x.id === heaters[j].id) as NixieHeaterBase;
713
+ cooldownTime = Math.max(nheater.getCooldownTime(), cooldownTime);
714
+ }
715
+ }
716
+ if (cooldownTime > 0) {
717
+ // We need do start a cooldown cycle for the body. If there is already
718
+ // a cooldown underway this will append the on to it.
719
+ delayMgr.setHeaterCooldownDelay(bsoff, bstate, cooldownTime * 1000);
720
+ delayCooldown = true;
721
+ }
722
+ else {
723
+ await ncp.circuits.setCircuitStateAsync(csoff, false);
724
+ bsoff.isOn = false;
725
+ }
726
+ }
727
+ }
728
+ }
729
+ if (delayCooldown) return cstate;
730
+ if (delayPumps === true) sys.board.pumps.setPumpValveDelays([id, bstate.circuit]);
731
+ }
732
+ // Now we need to set the startup delay for all the heaters. This is true whether
733
+ // the system is shared or not so lets get a list of all the associated heaters for the body in question.
734
+ if (sys.general.options.heaterStartDelay && sys.general.options.heaterStartDelayTime > 0) {
735
+ let heaters = sys.board.heaters.getHeatersByCircuitId(circuit.id);
736
+ for (let j = 0; j < heaters.length; j++) {
737
+ let hstate = state.heaters.getItemById(heaters[j].id);
738
+ delayMgr.setHeaterStartupDelay(hstate);
473
739
  }
474
740
  }
475
- //sys.board.virtualChlorinatorController.start();
741
+ await ncp.circuits.setCircuitStateAsync(cstate, val);
742
+ bstate.isOn = val;
476
743
  }
477
- if (id === 6) state.temps.bodies.getItemById(1, true).isOn = val;
478
- else if (id === 1) state.temps.bodies.getItemById(2, true).isOn = val;
479
- // Let the main nixie controller set the circuit state and affect the relays if it needs to.
480
- await ncp.circuits.setCircuitStateAsync(circ, newState);
481
- return state.circuits.getInterfaceById(circ.id);
482
- }
483
- catch (err) { return Promise.reject(`Nixie: Error setCircuitStateAsync ${err.message}`); }
484
- finally {
485
- state.emitEquipmentChanges();
486
- // sys.board.virtualPumpControllers.start();
487
- ncp.pumps.syncPumpStates();
488
- sys.board.suspendStatus(false);
489
- this.board.processStatusAsync();
490
- }
744
+ else if (!val) {
745
+ // Alright we are turning off a circuit that will result in a body shutting off. If this
746
+ // circuit is already under delay it should have been processed out earlier.
747
+ delayMgr.cancelPumpValveDelays();
748
+ delayMgr.cancelHeaterStartupDelays();
749
+ if (cstate.startDelay) delayMgr.clearBodyStartupDelay(bstate);
750
+ await this.turnOffCleanerCircuits(bstate);
751
+ if (sys.equipment.shared && bstate.id === 2) await this.turnOffDrainCircuits(ignoreDelays);
752
+ logger.verbose(`Turning off a body circuit ${circuit.name}`);
753
+ if (cstate.isOn) {
754
+
755
+ // Check to see if we have any heater cooldown delays that need to take place.
756
+ let heaters = sys.board.heaters.getHeatersByCircuitId(circuit.id);
757
+ let cooldownTime = 0;
758
+ for (let j = 0; j < heaters.length; j++) {
759
+ let nheater = ncp.heaters.find(x => x.id === heaters[j].id) as NixieHeaterBase;
760
+ cooldownTime = Math.max(nheater.getCooldownTime(), cooldownTime);
761
+ }
762
+ if (cooldownTime > 0) {
763
+ logger.info(`Starting a Cooldown Delay ${cooldownTime}sec`);
764
+ // We need do start a cooldown cycle for the body.
765
+ delayMgr.setHeaterCooldownDelay(bstate, undefined, cooldownTime * 1000);
766
+ }
767
+ else {
768
+ await ncp.circuits.setCircuitStateAsync(cstate, val);
769
+ bstate.isOn = val;
770
+ }
771
+ }
772
+ }
773
+ return cstate;
774
+ } catch (err) { logger.error(`Nixie: Error setBodyCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setBodyCircuitStateAsync ${err.message}`, 'setBodyCircuitStateAsync')); }
775
+ }
776
+ protected async setSpillwayCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<CircuitState> {
777
+ try {
778
+ let cstate = state.circuits.getItemById(id);
779
+ let delayPumps = false;
780
+ if (cstate.isOn !== val) {
781
+ if (sys.equipment.shared === true) {
782
+ // First we need to check to see if the pool is on.
783
+ if (val) {
784
+ let spastate = state.circuits.getItemById(1);
785
+ if (spastate.isOn) {
786
+ logger.warn(`Cannot turn ${cstate.name} on because ${spastate.name} is on`);
787
+ return cstate;
788
+ }
789
+ // If there are any drain circuits or features that are currently engaged we need to turn them off.
790
+ await this.turnOffDrainCircuits(ignoreDelays);
791
+ if (sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([6, id]);
792
+ }
793
+ else if (!val && !ignoreDelays) {
794
+ // If we are turning off and there is another circuit that ties to the same pumps then we need set a valve delay. This means
795
+ // that if the pool circuit is on then we need to delay the pumps. However, if there is no other circuit that needs
796
+ // the pump to be on, then no harm no foul a delay in the pump won't mean anything.
797
+
798
+ // Conditions where this should not delay.
799
+ // 1. Another spillway circuit or feature is on.
800
+ // 2. There is no other running circuit that will affect the intake or return.
801
+ let arrIds = sys.board.valves.getBodyValveCircuitIds(true);
802
+ if (arrIds.length > 1) {
803
+ if (sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) {
804
+ sys.board.pumps.setPumpValveDelays([6, id]);
805
+ }
806
+ }
807
+ }
808
+ }
809
+ }
810
+ logger.verbose(`Turning ${val ? 'on' : 'off'} a spillway circuit ${cstate.name}`);
811
+ await ncp.circuits.setCircuitStateAsync(cstate, val);
812
+ return cstate;
813
+ } catch (err) { logger.error(`Nixie: Error setSpillwayCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setSpillwayCircuitStateAsync ${err.message}`, 'setBodyCircuitStateAsync')); }
814
+ }
815
+ protected async setDrainCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<CircuitState> {
816
+ try {
817
+ // Drain circuits can be very bad. This is because they can be turned on then never turned off
818
+ // we may want to create some limits are to how long they can be on or even force them off
819
+ // if for instance the spa is not on.
820
+ // RULES FOR DRAIN CIRCUITS:
821
+ // 1. All spillway circuits must be off.
822
+ let cstate = state.circuits.getItemById(id);
823
+ let delayPumps = false;
824
+ if (cstate.isOn !== val) {
825
+ if (sys.equipment.shared === true) {
826
+ let spastate = state.temps.bodies.getItemById(2);
827
+ let poolstate = state.temps.bodies.getItemById(1);
828
+ // First we need to check to see if the pool is on.
829
+ if (val) {
830
+ if (spastate.isOn || spastate.startDelay || poolstate.isOn || poolstate.startDelay) {
831
+ logger.warn(`Cannot turn ${cstate.name} on because a body is on`);
832
+ return cstate;
833
+ }
834
+ // If there are any spillway circuits or features that are currently engaged we need to turn them off.
835
+ await this.turnOffSpillwayCircuits(true);
836
+ // If there are any cleaner circuits on for the main body turn them off.
837
+ await this.turnOffCleanerCircuits(state.temps.bodies.getItemById(1), true);
838
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]);
839
+ }
840
+ else if (!val && !ignoreDelays) {
841
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]);
842
+ }
843
+ }
844
+ }
845
+ logger.verbose(`Turning ${val ? 'on' : 'off'} a drain circuit ${cstate.name}`);
846
+ await ncp.circuits.setCircuitStateAsync(cstate, val);
847
+ return cstate;
848
+ } catch (err) { logger.error(`Nixie: Error setSpillwayCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setBodyCircuitStateAsync ${err.message}`, 'setBodyCircuitStateAsync')); }
491
849
  }
850
+
492
851
  public toggleCircuitStateAsync(id: number): Promise<ICircuitState> {
493
852
  let circ = state.circuits.getInterfaceById(id);
494
853
  return this.setCircuitStateAsync(id, !(circ.isOn || false));
495
854
  }
496
855
  public async setLightThemeAsync(id: number, theme: number) {
856
+ if (sys.board.equipmentIds.circuitGroups.isInRange(id)) {
857
+ await this.setLightGroupThemeAsync(id, theme);
858
+ return Promise.resolve(state.lightGroups.getItemById(id));
859
+ }
497
860
  let cstate = state.circuits.getItemById(id);
861
+ let circ = sys.circuits.getItemById(id);
862
+ let thm = sys.board.valueMaps.lightThemes.findItem(theme);
863
+ if (typeof thm !== 'undefined' && typeof thm.sequence !== 'undefined' && circ.master === 1) {
864
+ logger.info(`Setting light theme for ${circ.name} to ${thm.name} [${thm.sequence}]`);
865
+ await sys.board.circuits.setCircuitStateAsync(id, true);
866
+ await ncp.circuits.sendOnOffSequenceAsync(id, thm.sequence);
867
+ }
498
868
  cstate.lightingTheme = theme;
499
869
  return Promise.resolve(cstate as ICircuitState);
500
870
  }
@@ -551,8 +921,20 @@ export class NixieCircuitCommands extends CircuitCommands {
551
921
  }
552
922
  return arrRefs;
553
923
  }
554
- public getLightThemes(type?: number) { return sys.board.valueMaps.lightThemes.toArray(); }
555
- public getCircuitFunctions() { return sys.board.valueMaps.circuitFunctions.toArray(); }
924
+ public getLightThemes(type?: number) {
925
+ let tobj = (typeof type === 'undefined') ? sys.board.valueMaps.circuitFunctions.transformByName('intellibrite') : sys.board.valueMaps.circuitFunctions.transform(type);
926
+ let arrThemes = sys.board.valueMaps.lightThemes.toArray();
927
+ let arr = [];
928
+ for (let i = 0; i < arrThemes.length; i++) {
929
+ if (tobj.name === arrThemes[i].type) arr.push(arrThemes[i]);
930
+ }
931
+ return arr;
932
+ }
933
+ public getCircuitFunctions() {
934
+ let cf = sys.board.valueMaps.circuitFunctions.toArray();
935
+ if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' });
936
+ return cf;
937
+ }
556
938
  public getCircuitNames() {
557
939
  return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()];
558
940
  }
@@ -566,8 +948,8 @@ export class NixieCircuitCommands extends CircuitCommands {
566
948
  if (isNaN(id) || !sys.board.equipmentIds.circuits.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.id}`, data.id, 'Circuit'));
567
949
  let circuit = sys.circuits.getItemById(id, true);
568
950
  let scircuit = state.circuits.getItemById(id, true);
569
- circuit.isActive = true;
570
- scircuit.isOn = false;
951
+ scircuit.isActive = circuit.isActive = true;
952
+ circuit.master = 1;
571
953
  if (data.name) circuit.name = scircuit.name = data.name;
572
954
  else if (!circuit.name && !data.name) circuit.name = scircuit.name = Circuit.getIdName(id);
573
955
  if (typeof data.type !== 'undefined' || typeof circuit.type === 'undefined') circuit.type = scircuit.type = parseInt(data.type, 10) || 0;
@@ -588,8 +970,14 @@ export class NixieCircuitCommands extends CircuitCommands {
588
970
  let group: CircuitGroup = null;
589
971
  let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1;
590
972
  if (id <= 0) {
591
- // We are adding a circuit group.
592
- id = sys.circuitGroups.getNextEquipmentId(sys.board.equipmentIds.circuitGroups);
973
+ // We are adding a circuit group so we need to get the next equipment id. For circuit groups and light groups, they share ids.
974
+ let range = sys.board.equipmentIds.circuitGroups;
975
+ for (let i = range.start; i <= range.end; i++) {
976
+ if (!sys.lightGroups.find(elem => elem.id === i) && !sys.circuitGroups.find(elem => elem.id === i)) {
977
+ id = i;
978
+ break;
979
+ }
980
+ }
593
981
  }
594
982
  if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit group id exceeded`, id, 'CircuitGroup'));
595
983
  if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'CircuitGroup'));
@@ -621,7 +1009,7 @@ export class NixieCircuitCommands extends CircuitCommands {
621
1009
  //RKS: 09-26-20 There is no such thing as a lighting theme on a circuit group circuit. That is what lighGroups are for.
622
1010
  //if (typeof cobj.lightingTheme !== 'undefined') c.lightingTheme = parseInt(cobj.lightingTheme, 10);
623
1011
  }
624
- // group.circuits.length = obj.circuits.length; // RSG - removed as this will delete circuits that were not changed
1012
+ group.circuits.length = obj.circuits.length; // RSG - removed as this will delete circuits that were not changed
625
1013
  }
626
1014
  resolve(group);
627
1015
  });
@@ -631,14 +1019,21 @@ export class NixieCircuitCommands extends CircuitCommands {
631
1019
  let group: LightGroup = null;
632
1020
  let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1;
633
1021
  if (id <= 0) {
634
- // We are adding a circuit group.
635
- id = sys.circuitGroups.getNextEquipmentId(sys.board.equipmentIds.circuitGroups);
1022
+ // We are adding a circuit group so we need to get the next equipment id. For circuit groups and light groups, they share ids.
1023
+ let range = sys.board.equipmentIds.circuitGroups;
1024
+ for (let i = range.start; i <= range.end; i++) {
1025
+ if (!sys.lightGroups.find(elem => elem.id === i) && !sys.circuitGroups.find(elem => elem.id === i)) {
1026
+ id = i;
1027
+ break;
1028
+ }
1029
+ }
636
1030
  }
637
1031
  if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit light group id exceeded`, id, 'LightGroup'));
638
1032
  if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'LightGroup'));
639
1033
  group = sys.lightGroups.getItemById(id, true);
1034
+ let sgroup = state.lightGroups.getItemById(id, true);
640
1035
  return new Promise<LightGroup>((resolve, reject) => {
641
- if (typeof obj.name !== 'undefined') group.name = obj.name;
1036
+ if (typeof obj.name !== 'undefined') sgroup.name = group.name = obj.name;
642
1037
  if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440;
643
1038
  if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440);
644
1039
  group.dontStop = group.eggTimer === 1440;
@@ -656,7 +1051,11 @@ export class NixieCircuitCommands extends CircuitCommands {
656
1051
  if (typeof cobj.swimDelay !== 'undefined') c.swimDelay = parseInt(cobj.swimDelay, 10);
657
1052
  if (typeof cobj.position !== 'undefined') c.position = parseInt(cobj.position, 10);
658
1053
  }
1054
+ // RKS: 09-25-21 - This has to be here. Not sure the goal of not setting the entire circuit array when saving the group.
659
1055
  // group.circuits.length = obj.circuits.length; // RSG - removed as this will delete circuits that were not changed
1056
+ group.circuits.length = obj.circuits.length;
1057
+ sgroup.emitEquipmentChange();
1058
+
660
1059
  }
661
1060
  resolve(group);
662
1061
  });
@@ -698,24 +1097,30 @@ export class NixieCircuitCommands extends CircuitCommands {
698
1097
  return Promise.reject(new InvalidEquipmentIdError('Group id has not been defined', id, 'LightGroup'));
699
1098
  }
700
1099
  public async deleteCircuitAsync(data: any): Promise<ICircuit> {
701
- if (typeof data.id === 'undefined') return Promise.reject(new InvalidEquipmentIdError('You must provide an id to delete a circuit', data.id, 'Circuit'));
702
- let circuit = sys.circuits.getInterfaceById(data.id);
1100
+ let id = parseInt(data.id, 10);
1101
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.id}`, data.id, 'Circuit'));
1102
+ if (!sys.board.equipmentIds.circuits.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.id}`, data.id, 'Circuit'));
1103
+ let circuit = sys.circuits.getInterfaceById(id);
1104
+ let cstate = state.circuits.getInterfaceById(id);
703
1105
  if (circuit instanceof Circuit) {
704
- sys.circuits.removeItemById(data.id);
705
- state.circuits.removeItemById(data.id);
1106
+ sys.circuits.removeItemById(circuit.id);
1107
+ state.circuits.removeItemById(circuit.id);
1108
+ cstate.isActive = circuit.isActive = false;
706
1109
  }
707
1110
  if (circuit instanceof Feature) {
708
- sys.features.removeItemById(data.id);
709
- state.features.removeItemById(data.id);
1111
+ sys.features.removeItemById(circuit.id);
1112
+ state.features.removeItemById(circuit.id);
1113
+ cstate.isActive = circuit.isActive = false;
710
1114
  }
1115
+ cstate.emitEquipmentChange();
711
1116
  return new Promise<ICircuit>((resolve, reject) => { resolve(circuit); });
712
1117
  }
713
1118
  public deleteCircuit(data: any) {
714
1119
  if (typeof data.id !== 'undefined') {
715
1120
  let circuit = sys.circuits.getInterfaceById(data.id);
716
1121
  if (circuit instanceof Circuit) {
717
- sys.circuits.removeItemById(data.id);
718
- state.circuits.removeItemById(data.id);
1122
+ sys.circuits.removeItemById(circuit.id);
1123
+ state.circuits.removeItemById(circuit.id);
719
1124
  return;
720
1125
  }
721
1126
  if (circuit instanceof Feature) {
@@ -734,42 +1139,32 @@ export class NixieCircuitCommands extends CircuitCommands {
734
1139
  public async setLightGroupThemeAsync(id: number, theme: number): Promise<ICircuitState> {
735
1140
  const grp = sys.lightGroups.getItemById(id);
736
1141
  const sgrp = state.lightGroups.getItemById(id);
737
- grp.lightingTheme = sgrp.lightingTheme = theme;
738
- for (let i = 0; i < grp.circuits.length; i++) {
739
- let c = grp.circuits.getItemByIndex(i);
740
- let cstate = state.circuits.getItemById(c.circuit);
741
- // if theme is 'off' light groups should not turn on
742
- if (cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) === 'off')
1142
+ //grp.lightingTheme = sgrp.lightingTheme = theme;
1143
+ let thm = sys.board.valueMaps.lightThemes.transform(theme);
1144
+ sgrp.action = sys.board.valueMaps.intellibriteActions.getValue('color');
1145
+ try {
1146
+ // Go through and set the theme for all lights in the group.
1147
+ for (let i = 0; i < grp.circuits.length; i++) {
1148
+ let c = grp.circuits.getItemByIndex(i);
1149
+ //let cstate = state.circuits.getItemById(c.circuit);
1150
+ await sys.board.circuits.setLightThemeAsync(c.circuit, theme);
743
1151
  await sys.board.circuits.setCircuitStateAsync(c.circuit, false);
744
- else if (!cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) !== 'off') await sys.board.circuits.setCircuitStateAsync(c.circuit, true);
1152
+ }
1153
+ await utils.sleep(5000);
1154
+ // Turn the circuits all back on again.
1155
+ for (let i = 0; i < grp.circuits.length; i++) {
1156
+ let c = grp.circuits.getItemByIndex(i);
1157
+ //let cstate = state.circuits.getItemById(c.circuit);
1158
+ await sys.board.circuits.setCircuitStateAsync(c.circuit, true);
1159
+ }
1160
+ sgrp.lightingTheme = theme;
1161
+ return sgrp;
745
1162
  }
746
- sgrp.isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false : true;
747
- // If we truly want to support themes in lightGroups we probably need to program
748
- // the specific on/off toggles to enable that. For now this will go through the motions but it's just a pretender.
749
- switch (theme) {
750
- case 0: // off
751
- case 1: // on
752
- break;
753
- case 128: // sync
754
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'sync'); });
755
- break;
756
- case 144: // swim
757
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'swim'); });
758
- break;
759
- case 160: // swim
760
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'set'); });
761
- break;
762
- case 190: // save
763
- case 191: // recall
764
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'other'); });
765
- break;
766
- default:
767
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'color'); });
768
- // other themes for magicstream?
1163
+ catch (err) { return Promise.reject(err); }
1164
+ finally {
1165
+ sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
1166
+ sgrp.action = 0;
769
1167
  }
770
- sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
771
- state.emitEquipmentChanges();
772
- return Promise.resolve(sgrp);
773
1168
  }
774
1169
  public async setLightGroupAttribsAsync(group: LightGroup): Promise<LightGroup> {
775
1170
  let grp = sys.lightGroups.getItemById(group.id);
@@ -785,30 +1180,59 @@ export class NixieCircuitCommands extends CircuitCommands {
785
1180
  }
786
1181
  catch (err) { return Promise.reject(err); }
787
1182
  }
788
- public sequenceLightGroupAsync(id: number, operation: string): Promise<LightGroupState> {
1183
+ public async sequenceLightGroupAsync(id: number, operation: string): Promise<LightGroupState> {
789
1184
  let sgroup = state.lightGroups.getItemById(id);
1185
+ let grp = sys.lightGroups.getItemById(id);
790
1186
  let nop = sys.board.valueMaps.intellibriteActions.getValue(operation);
791
- if (nop > 0) {
792
- sgroup.action = nop;
793
- sgroup.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
794
- state.emitEquipmentChanges();
795
- setTimeout(function () {
796
- sgroup.action = 0;
797
- sgroup.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
798
- state.emitEquipmentChanges();
799
- }, 20000); // It takes 20 seconds to sequence.
800
- }
801
- return Promise.resolve(sgroup);
1187
+ try {
1188
+ switch (operation) {
1189
+ case 'sync':
1190
+ sgroup.action = nop;
1191
+ sgroup.emitEquipmentChange();
1192
+ for (let i = 0; i < grp.circuits.length; i++) {
1193
+ let c = grp.circuits.getItemByIndex(i);
1194
+ await sys.board.circuits.setCircuitStateAsync(c.circuit, false);
1195
+ }
1196
+ await utils.sleep(5000);
1197
+ // Turn the circuits all back on again.
1198
+ for (let i = 0; i < grp.circuits.length; i++) {
1199
+ let c = grp.circuits.getItemByIndex(i);
1200
+ //let cstate = state.circuits.getItemById(c.circuit);
1201
+ await sys.board.circuits.setCircuitStateAsync(c.circuit, true);
1202
+ }
1203
+ break;
1204
+ case 'set':
1205
+ sgroup.action = nop;
1206
+ sgroup.emitEquipmentChange();
1207
+ await utils.sleep(5000);
1208
+ break;
1209
+ case 'swim':
1210
+ sgroup.action = nop;
1211
+ sgroup.emitEquipmentChange();
1212
+ await utils.sleep(5000);
1213
+ break;
1214
+ }
1215
+ return sgroup;
1216
+ } catch (err) { return Promise.reject(err); }
1217
+ finally { sgroup.action = 0; sgroup.emitEquipmentChange(); }
802
1218
  }
803
1219
  public async setCircuitGroupStateAsync(id: number, val: boolean): Promise<ICircuitGroupState> {
804
1220
  let grp = sys.circuitGroups.getItemById(id, false, { isActive: false });
805
- let gstate = (grp.dataName === 'circuitGroupConfig') ? state.circuitGroups.getItemById(grp.id, grp.isActive !== false) : state.lightGroups.getItemById(grp.id, grp.isActive !== false);
1221
+ if (grp.dataName !== 'circuitGroupConfig') return await sys.board.circuits.setLightGroupStateAsync(id, val);
1222
+ let gstate = state.circuitGroups.getItemById(grp.id, grp.isActive !== false);
806
1223
  let circuits = grp.circuits.toArray();
1224
+ sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(gstate.id), gstate, val);
807
1225
  gstate.isOn = val;
808
1226
  let arr = [];
809
1227
  for (let i = 0; i < circuits.length; i++) {
810
1228
  let circuit = circuits[i];
811
- arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, val));
1229
+ // The desiredState will be as follows.
1230
+ // 1 = on, 2 = off, 3 = ignore.
1231
+ let cval = true;
1232
+ if (circuit.desiredState === 1) cval = val ? true : false;
1233
+ else if (circuit.desiredState === 2) cval = val ? false : true;
1234
+ else continue;
1235
+ arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, cval));
812
1236
  }
813
1237
  return new Promise<ICircuitGroupState>(async (resolve, reject) => {
814
1238
  await Promise.all(arr).catch((err) => { reject(err) });
@@ -816,16 +1240,22 @@ export class NixieCircuitCommands extends CircuitCommands {
816
1240
  });
817
1241
  }
818
1242
  public async setLightGroupStateAsync(id: number, val: boolean): Promise<ICircuitGroupState> {
819
- return sys.board.circuits.setCircuitGroupStateAsync(id, val);
1243
+ let grp = sys.circuitGroups.getItemById(id, false, { isActive: false });
1244
+ if (grp.dataName === 'circuitGroupConfig') return await sys.board.circuits.setCircuitGroupStateAsync(id, val);
1245
+ let gstate = state.lightGroups.getItemById(grp.id, grp.isActive !== false);
1246
+ let circuits = grp.circuits.toArray();
1247
+ sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(gstate.id), gstate, val);
1248
+ gstate.isOn = val;
1249
+ let arr = [];
1250
+ for (let i = 0; i < circuits.length; i++) {
1251
+ let circuit = circuits[i];
1252
+ arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, val));
1253
+ }
1254
+ return new Promise<ICircuitGroupState>(async (resolve, reject) => {
1255
+ await Promise.all(arr).catch((err) => { reject(err) });
1256
+ resolve(gstate);
1257
+ });
820
1258
  }
821
- /* public sequenceIntelliBrite(operation: string) {
822
- state.intellibrite.hasChanged = true;
823
- let nop = sys.board.valueMaps.intellibriteActions.getValue(operation);
824
- if (nop > 0) {
825
- state.intellibrite.action = nop;
826
- setTimeout(function() { state.intellibrite.action = 0; state.emitEquipmentChanges(); }, 20000); // It takes 20 seconds to sequence.
827
- }
828
- } */
829
1259
  }
830
1260
  export class NixieFeatureCommands extends FeatureCommands {
831
1261
  public async setFeatureAsync(obj: any): Promise<Feature> {
@@ -866,68 +1296,196 @@ export class NixieFeatureCommands extends FeatureCommands {
866
1296
  feature.isActive = false;
867
1297
  sfeature.isOn = false;
868
1298
  sfeature.showInFeatures = false;
1299
+ sfeature.isActive = false;
869
1300
  sfeature.emitEquipmentChange();
870
1301
  return new Promise<Feature>((resolve, reject) => { resolve(feature); });
871
1302
  }
872
1303
  else
873
1304
  Promise.reject(new InvalidEquipmentIdError('Feature id has not been defined', undefined, 'Feature'));
874
1305
  }
875
- public async setFeatureStateAsync(id: number, val: boolean): Promise<ICircuitState> {
1306
+ public async setFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
876
1307
  try {
877
1308
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
878
1309
  if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
879
1310
  let feature = sys.features.getItemById(id);
880
1311
  let fstate = state.features.getItemById(feature.id, feature.isActive !== false);
881
- fstate.isOn = val;
1312
+ feature.master = 1;
1313
+ let ftype = sys.board.valueMaps.featureFunctions.getName(feature.type);
1314
+ switch (ftype) {
1315
+ case 'spadrain':
1316
+ this.setDrainFeatureStateAsync(id, val, ignoreDelays);
1317
+ break;
1318
+ case 'spillway':
1319
+ this.setSpillwayFeatureStateAsync(id, val, ignoreDelays);
1320
+ break;
1321
+ default:
1322
+ fstate.isOn = val;
1323
+ break;
1324
+ }
1325
+ if(fstate.isOn === val) sys.board.circuits.setEndTime(feature, fstate, val);
882
1326
  sys.board.valves.syncValveStates();
883
- // sys.board.virtualPumpControllers.start();
884
1327
  ncp.pumps.syncPumpStates();
885
1328
  state.emitEquipmentChanges();
886
1329
  return fstate;
887
1330
  } catch (err) { return Promise.reject(new Error(`Error setting feature state ${err.message}`)); }
888
1331
  }
1332
+ protected async setSpillwayFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<FeatureState> {
1333
+ try {
1334
+ let cstate = state.features.getItemById(id);
1335
+ if (cstate.isOn !== val) {
1336
+ if (sys.equipment.shared === true) {
1337
+ let spastate = state.temps.bodies.getItemById(2);
1338
+ if (val) {
1339
+ if (spastate.isOn || spastate.startDelay) {
1340
+ logger.warn(`Cannot turn ${cstate.name} on because ${spastate.name} is on`);
1341
+ return cstate;
1342
+ }
1343
+ // If there are any drain circuits or features that are currently engaged we need to turn them off.
1344
+ await sys.board.circuits.turnOffDrainCircuits(ignoreDelays);
1345
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 6]);
1346
+ }
1347
+ else if (!val) {
1348
+ let arrIds = sys.board.valves.getBodyValveCircuitIds(true);
1349
+ if (arrIds.length > 1) {
1350
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 6]);
1351
+ }
1352
+ }
1353
+ }
1354
+ logger.verbose(`Turning ${val ? 'on' : 'off'} a spillway feature ${cstate.name}`);
1355
+ cstate.isOn = val;
1356
+ }
1357
+ return cstate;
1358
+ } catch (err) { logger.error(`Nixie: Error setSpillwayFeatureStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setSpillwayFeatureStateAsync ${err.message}`, 'setSpillwayFeatureStateAsync')); }
1359
+ }
1360
+ protected async setDrainFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<FeatureState> {
1361
+ try {
1362
+ // Drain circuits can be very bad. This is because they can be turned on then never turned off
1363
+ // we may want to create some limits are to how long they can be on or even force them off
1364
+ // if for instance the spa is not on.
1365
+ // RULES FOR DRAIN CIRCUITS:
1366
+ // 1. All spillway circuits must be off.
1367
+ let cstate = state.features.getItemById(id);
1368
+ if (cstate.isOn !== val) {
1369
+ if (sys.equipment.shared === true) {
1370
+ if (val) {
1371
+ // First we need to check to see if the pool is on.
1372
+ let poolstate = state.temps.bodies.getItemById(1);
1373
+ let spastate = state.temps.bodies.getItemById(2);
1374
+ if ((spastate.isOn || spastate.startDelay || poolstate.isOn || poolstate.startDelay) && val) {
1375
+ logger.warn(`Cannot turn ${cstate.name} on because a body circuit is on`);
1376
+ return cstate;
1377
+ }
1378
+ // If there are any spillway circuits or features that are currently engaged we need to turn them off.
1379
+ await sys.board.circuits.turnOffSpillwayCircuits(true);
1380
+ // If there are any cleaner circuits on for the main body turn them off.
1381
+ await sys.board.circuits.turnOffCleanerCircuits(state.temps.bodies.getItemById(1), true);
1382
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]);
1383
+ }
1384
+ else if (!val) {
1385
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]);
1386
+ }
1387
+ }
1388
+ logger.verbose(`Turning ${val ? 'on' : 'off'} a spa drain circuit ${cstate.name}`);
1389
+ cstate.isOn = val;
1390
+ }
1391
+ return cstate;
1392
+ } catch (err) { logger.error(`Nixie: Error setSpillwayCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setBodyCircuitStateAsync ${err.message}`, 'setBodyCircuitStateAsync')); }
1393
+ }
1394
+
889
1395
  public async toggleFeatureStateAsync(id: number): Promise<ICircuitState> {
890
1396
  let feat = state.features.getItemById(id);
891
1397
  return this.setFeatureStateAsync(id, !(feat.isOn || false));
892
1398
  }
893
1399
  public syncGroupStates() {
1400
+ // The way this should work is that when all of the states are met
1401
+ // the group should be on. Otherwise it should be off. That means that if
1402
+ // you turned on all the group circuits that should be on individually then
1403
+ // the group should be on.
894
1404
  for (let i = 0; i < sys.circuitGroups.length; i++) {
895
1405
  let grp: CircuitGroup = sys.circuitGroups.getItemByIndex(i);
896
1406
  let circuits = grp.circuits.toArray();
897
- let bIsOn = false;
898
1407
  if (grp.isActive) {
899
- for (let j = 0; j < circuits.length; j++) {
900
- let circuit: CircuitGroupCircuit = grp.circuits.getItemById(j);
1408
+ let bIsOn = true;
1409
+ // Iterate the circuits and break out should we find a condition
1410
+ // where the group should be off.
1411
+ for (let j = 0; j < circuits.length && bIsOn === true; j++) {
1412
+ let circuit: CircuitGroupCircuit = grp.circuits.getItemByIndex(j);
901
1413
  let cstate = state.circuits.getInterfaceById(circuit.circuit);
902
- if (circuit.desiredState === 1 || circuit.desiredState === 0) {
903
- if (cstate.isOn === utils.makeBool(circuit.desiredState)) bIsOn = true;
1414
+ if (circuit.desiredState === 1) { // The circuit should be on.
1415
+ if (!utils.makeBool(cstate.isOn)) bIsOn = false;
1416
+ }
1417
+ else if (circuit.desiredState === 0) { // The circuit should be off.
1418
+ if (utils.makeBool(cstate.isOn)) bIsOn = false;
904
1419
  }
905
1420
  }
1421
+ let sgrp = state.circuitGroups.getItemById(grp.id);
1422
+ sgrp.isOn = bIsOn;
906
1423
  }
907
- let sgrp = state.circuitGroups.getItemById(grp.id);
908
- sgrp.isOn = bIsOn && grp.isActive;
909
-
910
1424
  sys.board.valves.syncValveStates();
911
1425
  }
912
1426
  // I am guessing that there will only be one here but iterate
913
1427
  // just in case we expand.
914
1428
  for (let i = 0; i < sys.lightGroups.length; i++) {
915
1429
  let grp: LightGroup = sys.lightGroups.getItemByIndex(i);
916
- let bIsOn = false;
1430
+ let circuits = grp.circuits.toArray();
917
1431
  if (grp.isActive) {
918
- let circuits = grp.circuits.toArray();
919
- for (let j = 0; j < circuits.length; j++) {
920
- let circuit = grp.circuits.getItemByIndex(j).circuit;
921
- let cstate = state.circuits.getInterfaceById(circuit);
922
- if (cstate.isOn) bIsOn = true;
1432
+ let bIsOn = true;
1433
+ for (let j = 0; j < circuits.length && bIsOn === true; j++) {
1434
+ let circuit: LightGroupCircuit = grp.circuits.getItemByIndex(j);
1435
+ let cstate = state.circuits.getInterfaceById(circuit.circuit);
1436
+ if (!utils.makeBool(cstate.isOn)) bIsOn = false;
923
1437
  }
1438
+ let sgrp = state.lightGroups.getItemById(grp.id);
1439
+ sgrp.isOn = bIsOn;
924
1440
  }
925
- let sgrp = state.lightGroups.getItemById(grp.id);
926
- sgrp.isOn = bIsOn;
1441
+ sys.board.valves.syncValveStates();
927
1442
  }
928
1443
  state.emitEquipmentChanges();
929
1444
  }
1445
+ }
1446
+ export class NixiePumpCommands extends PumpCommands {
1447
+ public async setPumpValveDelays(circuitIds: number[], delay?: number) {
1448
+ try {
1449
+ logger.info(`Setting pump valve delays: ${JSON.stringify(circuitIds)}`);
1450
+ // Alright now we have to delay the pumps associated with the circuit. So lets iterate all our
1451
+ // pump states and see where we land.
1452
+ for (let i = 0; i < sys.pumps.length; i++) {
1453
+ let pump = sys.pumps.getItemByIndex(i);
1454
+ let pstate = state.pumps.getItemById(pump.id);
1455
+ let pt = sys.board.valueMaps.pumpTypes.get(pump.type);
930
1456
 
1457
+ // [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody: true, maxRelays: 1 }],
1458
+ // [2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 2 }],
1459
+ // [3, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true }],
1460
+ // [4, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
1461
+ // [5, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
1462
+ // [100, { name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1 }]
1463
+ switch (pt.name) {
1464
+ case 'ss':
1465
+ // If a single speed pump is designated it will be the filter pump but we need to map any settings
1466
+ // to bodies.
1467
+ console.log(`Body: ${pump.body} Pump: ${pump.name} Pool: ${circuitIds.includes(6)} `);
1468
+ if ((pump.body === 255 && (circuitIds.includes(6) || circuitIds.includes(1))) ||
1469
+ (pump.body === 0 && circuitIds.includes(6)) ||
1470
+ (pump.body === 101 && circuitIds.includes(1))) {
1471
+ delayMgr.setPumpValveDelay(pstate);
1472
+ }
1473
+ break;
1474
+ default:
1475
+ if (pt.maxCircuits > 0) {
1476
+ for (let j = 0; j < pump.circuits.length; j++) {
1477
+ let circ = pump.circuits.getItemByIndex(j);
1478
+ if (circuitIds.includes(circ.circuit)) {
1479
+ delayMgr.setPumpValveDelay(pstate);
1480
+ break;
1481
+ }
1482
+ }
1483
+ }
1484
+ break;
1485
+ }
1486
+ }
1487
+ } catch (err) { }
1488
+ }
931
1489
  }
932
1490
  export class NixieValveCommands extends ValveCommands {
933
1491
  public async setValveAsync(obj: any): Promise<Valve> {
@@ -948,7 +1506,7 @@ export class NixieValveCommands extends ValveCommands {
948
1506
  valve.deviceBinding = typeof obj.deviceBinding !== 'undefined' ? obj.deviceBinding : valve.deviceBinding;
949
1507
  valve.pinId = typeof obj.pinId !== 'undefined' ? obj.pinId : valve.pinId;
950
1508
  await ncp.valves.setValveAsync(valve, obj);
951
- sys.board.processStatusAsync();
1509
+ await sys.board.syncEquipmentItems();
952
1510
  return valve;
953
1511
  } catch (err) { logger.error(`Nixie: Error setting valve definition. ${err.message}`); return Promise.reject(err); }
954
1512
  }
@@ -980,12 +1538,13 @@ export class NixieHeaterCommands extends HeaterCommands {
980
1538
  try {
981
1539
  let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
982
1540
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
983
- else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Virtual Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
1541
+ else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Nixie Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
984
1542
  let heater: Heater;
985
1543
  if (id <= 0) {
986
1544
  // We are adding a heater. In this case all heaters are virtual.
987
- let vheaters = sys.heaters.filter(h => h.isVirtual === true);
988
- id = vheaters.length + 256;
1545
+ let vheaters = sys.heaters.filter(h => h.master === 1);
1546
+ id = Math.max(vheaters.getMaxId() + 1, vheaters.length + 256);
1547
+ logger.info(`Adding a new heater with id ${id}`);
989
1548
  }
990
1549
  heater = sys.heaters.getItemById(id, true);
991
1550
  if (typeof obj !== undefined) {
@@ -1005,16 +1564,16 @@ export class NixieHeaterCommands extends HeaterCommands {
1005
1564
  } catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
1006
1565
  }
1007
1566
  public async deleteHeaterAsync(obj: any): Promise<Heater> {
1008
- return new Promise<Heater>((resolve, reject) => {
1567
+ try {
1009
1568
  let id = parseInt(obj.id, 10);
1010
- if (isNaN(id)) return reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
1569
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
1011
1570
  let heater = sys.heaters.getItemById(id);
1012
1571
  heater.isActive = false;
1013
1572
  sys.heaters.removeItemById(id);
1014
1573
  state.heaters.removeItemById(id);
1015
1574
  sys.board.heaters.updateHeaterServices();
1016
- resolve(heater);
1017
- });
1575
+ return heater;
1576
+ } catch (err) { return Promise.reject(new BoardProcessError(err.message, 'deleteHeaterAsync')); }
1018
1577
  }
1019
1578
  public updateHeaterServices() {
1020
1579
  let htypes = sys.board.heaters.getInstalledHeaterTypes();
@@ -1022,7 +1581,7 @@ export class NixieHeaterCommands extends HeaterCommands {
1022
1581
  let heatPumpInstalled = htypes.heatpump > 0;
1023
1582
  let gasHeaterInstalled = htypes.gas > 0;
1024
1583
  let ultratempInstalled = htypes.ultratemp > 0;
1025
-
1584
+ let mastertempInstalled = htypes.mastertemp > 0;
1026
1585
  // The heat mode options are
1027
1586
  // 1 = Off
1028
1587
  // 2 = Gas Heater
@@ -1042,8 +1601,10 @@ export class NixieHeaterCommands extends HeaterCommands {
1042
1601
  // 3 = Solar Heater
1043
1602
  // 4 = Solar Preferred
1044
1603
  // 5 = Heat Pump
1604
+
1045
1605
  if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]);
1046
1606
  if (gasHeaterInstalled) sys.board.valueMaps.heatSources.merge([[2, { name: 'heater', desc: 'Heater' }]]);
1607
+ if (mastertempInstalled) sys.board.valueMaps.heatSources.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]);
1047
1608
  if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar Only', hasCoolSetpoint: htypes.hasCoolSetpoint }], [4, { name: 'solarpref', desc: 'Solar Preferred', hasCoolSetpoint: htypes.hasCoolSetpoint }]]);
1048
1609
  else if (solarInstalled) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar', hasCoolsetpoint: htypes.hasCoolSetpoint }]]);
1049
1610
  if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Pref' }]]);
@@ -1054,11 +1615,12 @@ export class NixieHeaterCommands extends HeaterCommands {
1054
1615
 
1055
1616
  sys.board.valueMaps.heatModes = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]);
1056
1617
  if (gasHeaterInstalled) sys.board.valueMaps.heatModes.merge([[2, { name: 'heater', desc: 'Heater' }]]);
1057
- if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar Only' }], [4, { name: 'solarpref', desc: 'Solar Preferred' }]]);
1618
+ if (mastertempInstalled) sys.board.valueMaps.heatModes.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]);
1619
+ if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar Only' }], [4, { name: 'solarpref', desc: 'Solar Preferred' }]]);
1058
1620
  else if (solarInstalled) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar' }]]);
1059
- if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only' }], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref' }]]);
1621
+ if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only' }], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref' }]]);
1060
1622
  else if (ultratempInstalled) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp' }]]);
1061
- if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
1623
+ if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
1062
1624
  else if (heatPumpInstalled) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heat Pump' }]]);
1063
1625
  // Now set the body data.
1064
1626
  for (let i = 0; i < sys.bodies.length; i++) {