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