nodejs-poolcontroller 7.5.1 → 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.
@@ -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,22 +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 }],
54
- [14, { name: 'colorlogic', desc: 'ColorLogic', isLight: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'}]
55
63
  ]);
56
64
  this.valueMaps.pumpTypes = new byteValueMap([
57
65
  [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody: true, maxRelays: 1 }],
@@ -132,10 +140,13 @@ export class NixieBoard extends SystemBoard {
132
140
  [245, { name: 'spaHeater', desc: 'Spa Heater' }],
133
141
  [246, { name: 'freeze', desc: 'Freeze' }],
134
142
  [247, { name: 'poolSpa', desc: 'Pool/Spa' }],
135
- [248, { name: 'solarHeat', desc: 'Solar Heat' }],
136
143
  [251, { name: 'heater', desc: 'Heater' }],
137
144
  [252, { name: 'solar', desc: 'Solar' }],
138
- [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' }]
139
150
  ]);
140
151
  this.valueMaps.scheduleTimeTypes.merge([
141
152
  [1, { name: 'sunrise', desc: 'Sunrise' }],
@@ -144,35 +155,35 @@ export class NixieBoard extends SystemBoard {
144
155
 
145
156
  this.valueMaps.lightThemes = new byteValueMap([
146
157
  // IntelliBrite Themes
147
- [0, { name: 'white', desc: 'White', type: 'intellibrite', sequence: 11 }],
148
- [1, { name: 'green', desc: 'Green', type: 'intellibrite', sequence: 9 }],
149
- [2, { name: 'blue', desc: 'Blue', type: 'intellibrite', sequence: 8 }],
150
- [3, { name: 'magenta', desc: 'Magenta', type: 'intellibrite', sequence: 12 }],
151
- [4, { name: 'red', desc: 'Red', type: 'intellibrite', sequence: 10 }],
152
- [5, { name: 'sam', desc: 'SAm Mode', type: 'intellibrite', sequence: 1 }],
153
- [6, { name: 'party', desc: 'Party', type: 'intellibrite', sequence: 2 }],
154
- [7, { name: 'romance', desc: 'Romance', type: 'intellibrite', sequence: 3 }],
155
- [8, { name: 'caribbean', desc: 'Caribbean', type: 'intellibrite', sequence: 4 }],
156
- [9, { name: 'american', desc: 'American', type: 'intellibrite', sequence: 5 }],
157
- [10, { name: 'sunset', desc: 'Sunset', type: 'intellibrite', sequence: 6 }],
158
- [11, { name: 'royal', desc: 'Royal', type: 'intellibrite', sequence: 7 }],
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 }],
159
170
  // ColorLogic Themes
160
- [20, { name: 'cloudwhite', desc: 'Cloud White', type: 'colorlogic', sequence: 7 }],
161
- [21, { name: 'deepsea', desc: 'Deep Sea', type: 'colorlogic', sequence: 2 }],
162
- [22, { name: 'royalblue', desc: 'Royal Blue', type: 'colorlogic', sequence: 3 }],
163
- [23, { name: 'afternoonskies', desc: 'Afternoon Skies', type: 'colorlogic', sequence: 4 }],
164
- [24, { name: 'aquagreen', desc: 'Aqua Green', type: 'colorlogic', sequence: 5 }],
165
- [25, { name: 'emerald', desc: 'Emerald', type: 'colorlogic', sequence: 6 }],
166
- [26, { name: 'warmred', desc: 'Warm Red', type: 'colorlogic', sequence: 8 }],
167
- [27, { name: 'flamingo', desc: 'Flamingo', type: 'colorlogic', sequence: 9 }],
168
- [28, { name: 'vividviolet', desc: 'Vivid Violet', type: 'colorlogic', sequence: 10 }],
169
- [29, { name: 'sangria', desc: 'Sangria', type: 'colorlogic', sequence: 11 }],
170
- [30, { name: 'twilight', desc: 'Twilight', type: 'colorlogic', sequence: 12 }],
171
- [31, { name: 'tranquility', desc: 'Tranquility', type: 'colorlogic', sequence: 13 }],
172
- [32, { name: 'gemstone', desc: 'Gemstone', type: 'colorlogic', sequence: 14 }],
173
- [33, { name: 'usa', desc: 'USA', type: 'colorlogic', sequence: 15 }],
174
- [34, { name: 'mardigras', desc: 'Mardi Gras', type: 'colorlogic', sequence: 16 }],
175
- [35, { name: 'coolcabaret', desc: 'Cabaret', type: 'colorlogic', sequence: 17 }],
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 }],
176
187
 
177
188
  [255, { name: 'none', desc: 'None' }]
178
189
  ]);
@@ -202,8 +213,10 @@ export class NixieBoard extends SystemBoard {
202
213
  [1, { name: 'heater', desc: 'Heater' }],
203
214
  [2, { name: 'solar', desc: 'Solar' }],
204
215
  [3, { name: 'cooling', desc: 'Cooling' }],
216
+ [6, { name: 'mtheat', desc: 'Heater' }],
205
217
  [4, { name: 'hpheat', desc: 'Heating' }],
206
- [8, { name: 'hpcool', desc: 'Cooling' }]
218
+ [8, { name: 'hpcool', desc: 'Cooling' }],
219
+ [128, {name: 'cooldown', desc: 'Cooldown'}]
207
220
  ]);
208
221
  this.valueMaps.scheduleTypes = new byteValueMap([
209
222
  [0, { name: 'runonce', desc: 'Run Once', startDate: true, startTime: true, endTime: true, days: false, heatSource: true, heatSetpoint: true }],
@@ -429,7 +442,7 @@ export class NixieBoard extends SystemBoard {
429
442
  //public chlorinator: NixieChlorinatorCommands = new NixieChlorinatorCommands(this);
430
443
  public bodies: NixieBodyCommands = new NixieBodyCommands(this);
431
444
  public filters: NixieFilterCommands = new NixieFilterCommands(this);
432
- //public pumps: NixiePumpCommands = new NixiePumpCommands(this);
445
+ public pumps: NixiePumpCommands = new NixiePumpCommands(this);
433
446
  //public schedules: NixieScheduleCommands = new NixieScheduleCommands(this);
434
447
  public heaters: NixieHeaterCommands = new NixieHeaterCommands(this);
435
448
  public valves: NixieValveCommands = new NixieValveCommands(this);
@@ -456,12 +469,19 @@ export class NixieFilterCommands extends FilterCommands {
456
469
  try {
457
470
  await ncp.filters.setFilterStateAsync(fstate, isOn);
458
471
  }
459
- catch (err) { return Promise.reject(`Nixie: Error setFiterStateAsync ${err.message}`); }
472
+ catch (err) { return Promise.reject(new BoardProcessError(`Nixie: Error setFiterStateAsync ${err.message}`, 'setFilterStateAsync')); }
460
473
  }
461
474
  }
462
475
 
463
476
  export class NixieSystemCommands extends SystemCommands {
464
- 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
+ }
465
485
  public setDateTimeAsync(obj: any): Promise<any> { return Promise.resolve(); }
466
486
  public getDOW() { return this.board.valueMaps.scheduleDays.toArray(); }
467
487
  public async setGeneralAsync(obj: any): Promise<General> {
@@ -480,7 +500,20 @@ export class NixieSystemCommands extends SystemCommands {
480
500
  }
481
501
  }
482
502
  export class NixieCircuitCommands extends CircuitCommands {
483
- 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> {
484
517
  sys.board.suspendStatus(true);
485
518
  try {
486
519
  // We need to do some routing here as it is now critical that circuits, groups, and features
@@ -490,48 +523,340 @@ export class NixieCircuitCommands extends CircuitCommands {
490
523
  else if (sys.board.equipmentIds.features.isInRange(id))
491
524
  return await sys.board.features.setFeatureStateAsync(id, val);
492
525
 
526
+
493
527
  let circuit: ICircuit = sys.circuits.getInterfaceById(id, false, { isActive: false });
494
528
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit or Feature id ${id} not valid`, id, 'Circuit'));
495
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
+ }
496
536
  let newState = utils.makeBool(val);
497
- // 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
498
- // to turn off the other body first.
499
- //[12, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
500
- //[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }]
501
- 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}`);
502
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
+ }
503
672
  // If we are shared we need to turn off the other circuit.
504
- let offType = circ.type === 12 ? 13 : 12;
673
+ let offType = circuit.type === 12 ? 13 : 12;
505
674
  let off = sys.circuits.get().filter(elem => elem.type === offType);
675
+ let delayCooldown = false;
506
676
  // Turn the circuits off that are part of the shared system. We are going back to the board
507
677
  // just in case we got here for a circuit that isn't on the current defined panel.
508
678
  for (let i = 0; i < off.length; i++) {
509
679
  let coff = off[i];
510
- logger.info(`Turning off shared body ${coff.name} circuit`);
511
- 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);
512
739
  }
513
740
  }
514
- //sys.board.virtualChlorinatorController.start();
741
+ await ncp.circuits.setCircuitStateAsync(cstate, val);
742
+ bstate.isOn = val;
515
743
  }
516
- if (id === 6) state.temps.bodies.getItemById(1, true).isOn = val;
517
- else if (id === 1) state.temps.bodies.getItemById(2, true).isOn = val;
518
- // Let the main nixie controller set the circuit state and affect the relays if it needs to.
519
- await ncp.circuits.setCircuitStateAsync(circ, newState);
520
- await sys.board.processStatusAsync();
521
- return state.circuits.getInterfaceById(circ.id);
522
- }
523
- catch (err) { return Promise.reject(`Nixie: Error setCircuitStateAsync ${err.message}`); }
524
- finally {
525
- state.emitEquipmentChanges();
526
- ncp.pumps.syncPumpStates();
527
- sys.board.suspendStatus(false);
528
- }
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')); }
529
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')); }
849
+ }
850
+
530
851
  public toggleCircuitStateAsync(id: number): Promise<ICircuitState> {
531
852
  let circ = state.circuits.getInterfaceById(id);
532
853
  return this.setCircuitStateAsync(id, !(circ.isOn || false));
533
854
  }
534
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
+ }
535
860
  let cstate = state.circuits.getItemById(id);
536
861
  let circ = sys.circuits.getItemById(id);
537
862
  let thm = sys.board.valueMaps.lightThemes.findItem(theme);
@@ -605,7 +930,11 @@ export class NixieCircuitCommands extends CircuitCommands {
605
930
  }
606
931
  return arr;
607
932
  }
608
- public getCircuitFunctions() { return sys.board.valueMaps.circuitFunctions.toArray(); }
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
+ }
609
938
  public getCircuitNames() {
610
939
  return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()];
611
940
  }
@@ -810,42 +1139,32 @@ export class NixieCircuitCommands extends CircuitCommands {
810
1139
  public async setLightGroupThemeAsync(id: number, theme: number): Promise<ICircuitState> {
811
1140
  const grp = sys.lightGroups.getItemById(id);
812
1141
  const sgrp = state.lightGroups.getItemById(id);
813
- grp.lightingTheme = sgrp.lightingTheme = theme;
814
- for (let i = 0; i < grp.circuits.length; i++) {
815
- let c = grp.circuits.getItemByIndex(i);
816
- let cstate = state.circuits.getItemById(c.circuit);
817
- // if theme is 'off' light groups should not turn on
818
- 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);
819
1151
  await sys.board.circuits.setCircuitStateAsync(c.circuit, false);
820
- 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;
821
1162
  }
822
- sgrp.isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false : true;
823
- // If we truly want to support themes in lightGroups we probably need to program
824
- // the specific on/off toggles to enable that. For now this will go through the motions but it's just a pretender.
825
- switch (theme) {
826
- case 0: // off
827
- case 1: // on
828
- break;
829
- case 128: // sync
830
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'sync'); });
831
- break;
832
- case 144: // swim
833
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'swim'); });
834
- break;
835
- case 160: // swim
836
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'set'); });
837
- break;
838
- case 190: // save
839
- case 191: // recall
840
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'other'); });
841
- break;
842
- default:
843
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'color'); });
844
- // 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;
845
1167
  }
846
- sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
847
- state.emitEquipmentChanges();
848
- return Promise.resolve(sgrp);
849
1168
  }
850
1169
  public async setLightGroupAttribsAsync(group: LightGroup): Promise<LightGroup> {
851
1170
  let grp = sys.lightGroups.getItemById(group.id);
@@ -861,20 +1180,41 @@ export class NixieCircuitCommands extends CircuitCommands {
861
1180
  }
862
1181
  catch (err) { return Promise.reject(err); }
863
1182
  }
864
- public sequenceLightGroupAsync(id: number, operation: string): Promise<LightGroupState> {
1183
+ public async sequenceLightGroupAsync(id: number, operation: string): Promise<LightGroupState> {
865
1184
  let sgroup = state.lightGroups.getItemById(id);
1185
+ let grp = sys.lightGroups.getItemById(id);
866
1186
  let nop = sys.board.valueMaps.intellibriteActions.getValue(operation);
867
- if (nop > 0) {
868
- sgroup.action = nop;
869
- sgroup.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
870
- state.emitEquipmentChanges();
871
- setTimeout(function () {
872
- sgroup.action = 0;
873
- sgroup.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
874
- state.emitEquipmentChanges();
875
- }, 20000); // It takes 20 seconds to sequence.
876
- }
877
- 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(); }
878
1218
  }
879
1219
  public async setCircuitGroupStateAsync(id: number, val: boolean): Promise<ICircuitGroupState> {
880
1220
  let grp = sys.circuitGroups.getItemById(id, false, { isActive: false });
@@ -963,20 +1303,95 @@ export class NixieFeatureCommands extends FeatureCommands {
963
1303
  else
964
1304
  Promise.reject(new InvalidEquipmentIdError('Feature id has not been defined', undefined, 'Feature'));
965
1305
  }
966
- public async setFeatureStateAsync(id: number, val: boolean): Promise<ICircuitState> {
1306
+ public async setFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
967
1307
  try {
968
1308
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
969
1309
  if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
970
1310
  let feature = sys.features.getItemById(id);
971
1311
  let fstate = state.features.getItemById(feature.id, feature.isActive !== false);
972
- sys.board.circuits.setEndTime(feature, fstate, val);
973
- 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);
974
1326
  sys.board.valves.syncValveStates();
975
1327
  ncp.pumps.syncPumpStates();
976
1328
  state.emitEquipmentChanges();
977
1329
  return fstate;
978
1330
  } catch (err) { return Promise.reject(new Error(`Error setting feature state ${err.message}`)); }
979
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
+
980
1395
  public async toggleFeatureStateAsync(id: number): Promise<ICircuitState> {
981
1396
  let feat = state.features.getItemById(id);
982
1397
  return this.setFeatureStateAsync(id, !(feat.isOn || false));
@@ -1027,7 +1442,50 @@ export class NixieFeatureCommands extends FeatureCommands {
1027
1442
  }
1028
1443
  state.emitEquipmentChanges();
1029
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);
1030
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
+ }
1031
1489
  }
1032
1490
  export class NixieValveCommands extends ValveCommands {
1033
1491
  public async setValveAsync(obj: any): Promise<Valve> {
@@ -1080,12 +1538,13 @@ export class NixieHeaterCommands extends HeaterCommands {
1080
1538
  try {
1081
1539
  let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
1082
1540
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
1083
- 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'));
1084
1542
  let heater: Heater;
1085
1543
  if (id <= 0) {
1086
1544
  // We are adding a heater. In this case all heaters are virtual.
1087
1545
  let vheaters = sys.heaters.filter(h => h.master === 1);
1088
- id = vheaters.length + 256;
1546
+ id = Math.max(vheaters.getMaxId() + 1, vheaters.length + 256);
1547
+ logger.info(`Adding a new heater with id ${id}`);
1089
1548
  }
1090
1549
  heater = sys.heaters.getItemById(id, true);
1091
1550
  if (typeof obj !== undefined) {
@@ -1105,16 +1564,16 @@ export class NixieHeaterCommands extends HeaterCommands {
1105
1564
  } catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
1106
1565
  }
1107
1566
  public async deleteHeaterAsync(obj: any): Promise<Heater> {
1108
- return new Promise<Heater>((resolve, reject) => {
1567
+ try {
1109
1568
  let id = parseInt(obj.id, 10);
1110
- 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'));
1111
1570
  let heater = sys.heaters.getItemById(id);
1112
1571
  heater.isActive = false;
1113
1572
  sys.heaters.removeItemById(id);
1114
1573
  state.heaters.removeItemById(id);
1115
1574
  sys.board.heaters.updateHeaterServices();
1116
- resolve(heater);
1117
- });
1575
+ return heater;
1576
+ } catch (err) { return Promise.reject(new BoardProcessError(err.message, 'deleteHeaterAsync')); }
1118
1577
  }
1119
1578
  public updateHeaterServices() {
1120
1579
  let htypes = sys.board.heaters.getInstalledHeaterTypes();
@@ -1122,7 +1581,7 @@ export class NixieHeaterCommands extends HeaterCommands {
1122
1581
  let heatPumpInstalled = htypes.heatpump > 0;
1123
1582
  let gasHeaterInstalled = htypes.gas > 0;
1124
1583
  let ultratempInstalled = htypes.ultratemp > 0;
1125
-
1584
+ let mastertempInstalled = htypes.mastertemp > 0;
1126
1585
  // The heat mode options are
1127
1586
  // 1 = Off
1128
1587
  // 2 = Gas Heater
@@ -1142,8 +1601,10 @@ export class NixieHeaterCommands extends HeaterCommands {
1142
1601
  // 3 = Solar Heater
1143
1602
  // 4 = Solar Preferred
1144
1603
  // 5 = Heat Pump
1604
+
1145
1605
  if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]);
1146
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' }]]);
1147
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 }]]);
1148
1609
  else if (solarInstalled) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar', hasCoolsetpoint: htypes.hasCoolSetpoint }]]);
1149
1610
  if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Pref' }]]);
@@ -1154,11 +1615,12 @@ export class NixieHeaterCommands extends HeaterCommands {
1154
1615
 
1155
1616
  sys.board.valueMaps.heatModes = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]);
1156
1617
  if (gasHeaterInstalled) sys.board.valueMaps.heatModes.merge([[2, { name: 'heater', desc: 'Heater' }]]);
1157
- 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' }]]);
1158
1620
  else if (solarInstalled) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar' }]]);
1159
- 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' }]]);
1160
1622
  else if (ultratempInstalled) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp' }]]);
1161
- 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' }]]);
1162
1624
  else if (heatPumpInstalled) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heat Pump' }]]);
1163
1625
  // Now set the body data.
1164
1626
  for (let i = 0; i < sys.bodies.length; i++) {