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.
- package/Changelog +14 -0
- package/README.md +1 -1
- package/controller/Equipment.ts +107 -2
- package/controller/Errors.ts +10 -0
- package/controller/Lockouts.ts +423 -0
- package/controller/State.ts +112 -8
- package/controller/boards/EasyTouchBoard.ts +15 -13
- package/controller/boards/IntelliCenterBoard.ts +82 -87
- package/controller/boards/NixieBoard.ts +590 -128
- package/controller/boards/SystemBoard.ts +1264 -970
- package/controller/comms/messages/config/ExternalMessage.ts +1 -1
- package/controller/comms/messages/config/ValveMessage.ts +1 -1
- package/controller/comms/messages/status/HeaterStateMessage.ts +27 -3
- package/controller/nixie/bodies/Body.ts +3 -0
- package/controller/nixie/circuits/Circuit.ts +24 -8
- package/controller/nixie/heaters/Heater.ts +191 -31
- package/controller/nixie/pumps/Pump.ts +97 -60
- package/controller/nixie/valves/Valve.ts +1 -1
- package/package.json +1 -1
- package/web/bindings/mqtt.json +49 -38
- package/web/bindings/mqttAlt.json +12 -1
- package/web/services/config/Config.ts +6 -6
|
@@ -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
|
-
[
|
|
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',
|
|
148
|
-
[1, { name: 'green', desc: 'Green',
|
|
149
|
-
[2, { name: 'blue', desc: 'Blue',
|
|
150
|
-
[3, { name: 'magenta', desc: 'Magenta',
|
|
151
|
-
[4, { name: 'red', desc: 'Red',
|
|
152
|
-
[5, { name: 'sam', desc: 'SAm Mode',
|
|
153
|
-
[6, { name: 'party', desc: 'Party',
|
|
154
|
-
[7, { name: 'romance', desc: 'Romance',
|
|
155
|
-
[8, { name: 'caribbean', desc: 'Caribbean',
|
|
156
|
-
[9, { name: 'american', desc: 'American',
|
|
157
|
-
[10, { name: 'sunset', desc: 'Sunset',
|
|
158
|
-
[11, { name: 'royal', desc: 'Royal',
|
|
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',
|
|
161
|
-
[21, { name: 'deepsea', desc: 'Deep Sea',
|
|
162
|
-
[22, { name: 'royalblue', desc: 'Royal Blue',
|
|
163
|
-
[23, { name: 'afternoonskies', desc: 'Afternoon Skies',
|
|
164
|
-
[24, { name: 'aquagreen', desc: 'Aqua Green',
|
|
165
|
-
[25, { name: 'emerald', desc: 'Emerald',
|
|
166
|
-
[26, { name: 'warmred', desc: 'Warm Red',
|
|
167
|
-
[27, { name: 'flamingo', desc: 'Flamingo',
|
|
168
|
-
[28, { name: 'vividviolet', desc: 'Vivid Violet',
|
|
169
|
-
[29, { name: 'sangria', desc: 'Sangria',
|
|
170
|
-
[30, { name: 'twilight', desc: 'Twilight',
|
|
171
|
-
[31, { name: 'tranquility', desc: 'Tranquility',
|
|
172
|
-
[32, { name: 'gemstone', desc: 'Gemstone',
|
|
173
|
-
[33, { name: 'usa', desc: 'USA',
|
|
174
|
-
[34, { name: 'mardigras', desc: 'Mardi Gras',
|
|
175
|
-
[35, { name: 'coolcabaret', desc: 'Cabaret',
|
|
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
|
-
|
|
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> {
|
|
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
|
-
|
|
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
|
-
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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 =
|
|
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
|
-
|
|
511
|
-
|
|
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
|
-
|
|
741
|
+
await ncp.circuits.setCircuitStateAsync(cstate, val);
|
|
742
|
+
bstate.isOn = val;
|
|
515
743
|
}
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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() {
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
//
|
|
818
|
-
|
|
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
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
973
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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++) {
|