nodejs-poolcontroller 7.4.0 → 7.5.1
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 +3 -0
- package/README.md +2 -2
- package/app.ts +2 -0
- package/config/Config.ts +3 -0
- package/config/VersionCheck.ts +8 -4
- package/controller/Equipment.ts +89 -29
- package/controller/Errors.ts +14 -1
- package/controller/State.ts +75 -31
- package/controller/boards/EasyTouchBoard.ts +81 -36
- package/controller/boards/IntelliCenterBoard.ts +96 -32
- package/controller/boards/IntelliTouchBoard.ts +103 -29
- package/controller/boards/NixieBoard.ts +79 -27
- package/controller/boards/SystemBoard.ts +1552 -822
- package/controller/comms/Comms.ts +84 -9
- 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 +43 -25
- 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 +10 -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 +12 -2
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
- package/controller/comms/messages/status/EquipmentStateMessage.ts +74 -22
- package/controller/comms/messages/status/HeaterStateMessage.ts +15 -6
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +37 -26
- package/controller/nixie/Nixie.ts +18 -16
- package/controller/nixie/chemistry/ChemController.ts +57 -37
- package/controller/nixie/chemistry/Chlorinator.ts +7 -8
- package/controller/nixie/circuits/Circuit.ts +17 -0
- package/controller/nixie/pumps/Pump.ts +49 -24
- package/controller/nixie/schedules/Schedule.ts +1 -1
- package/defaultConfig.json +15 -0
- package/issue_template.md +1 -1
- package/logger/DataLogger.ts +37 -22
- package/package.json +3 -1
- package/web/Server.ts +515 -27
- package/web/bindings/influxDB.json +35 -0
- package/web/bindings/mqtt.json +62 -3
- package/web/bindings/mqttAlt.json +57 -4
- package/web/interfaces/httpInterface.ts +2 -0
- package/web/interfaces/influxInterface.ts +3 -2
- package/web/interfaces/mqttInterface.ts +12 -1
- package/web/services/config/Config.ts +162 -37
- package/web/services/state/State.ts +47 -3
- package/web/services/state/StateSocket.ts +1 -1
|
@@ -38,6 +38,12 @@ export class IntelliCenterBoard extends SystemBoard {
|
|
|
38
38
|
this.equipmentIds.features.start = 129;
|
|
39
39
|
this.equipmentIds.circuitGroups.start = 193;
|
|
40
40
|
this.equipmentIds.virtualCircuits.start = 237;
|
|
41
|
+
this.valueMaps.panelModes = new byteValueMap([
|
|
42
|
+
[0, { val: 0, name: 'auto', desc: 'Auto' }],
|
|
43
|
+
[1, { val: 1, name: 'service', desc: 'Service' }],
|
|
44
|
+
[8, { val: 8, name: 'freeze', desc: 'Freeze' }],
|
|
45
|
+
[255, { name: 'error', desc: 'System Error' }]
|
|
46
|
+
]);
|
|
41
47
|
this.valueMaps.circuitFunctions = new byteValueMap([
|
|
42
48
|
[0, { name: 'generic', desc: 'Generic' }],
|
|
43
49
|
[1, { name: 'spillway', desc: 'Spillway' }],
|
|
@@ -59,12 +65,12 @@ export class IntelliCenterBoard extends SystemBoard {
|
|
|
59
65
|
[2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody:true }],
|
|
60
66
|
[3, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true }],
|
|
61
67
|
[4, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
|
|
62
|
-
[5, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
|
|
68
|
+
[5, { name: 'vf', desc: 'Intelliflo VF', maxPrimingTime: 6, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
|
|
63
69
|
[100, {name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1}]
|
|
64
70
|
]);
|
|
65
71
|
// RSG - same as systemBoard definition; can delete.
|
|
66
72
|
this.valueMaps.heatModes = new byteValueMap([
|
|
67
|
-
[
|
|
73
|
+
[1, { name: 'off', desc: 'Off' }],
|
|
68
74
|
[3, { name: 'heater', desc: 'Heater' }],
|
|
69
75
|
[5, { name: 'solar', desc: 'Solar Only' }],
|
|
70
76
|
[12, { name: 'solarpref', desc: 'Solar Preferred' }]
|
|
@@ -207,8 +213,9 @@ export class IntelliCenterBoard extends SystemBoard {
|
|
|
207
213
|
[1, { name: 'heater', desc: 'Heater' }],
|
|
208
214
|
[2, { name: 'solar', desc: 'Solar' }],
|
|
209
215
|
[3, { name: 'cooling', desc: 'Cooling' }],
|
|
216
|
+
[6, { name: 'mtheat', desc: 'Heater' }],
|
|
210
217
|
[4, { name: 'hpheat', desc: 'Heating' }],
|
|
211
|
-
[8, { name: 'hpcool', desc: 'Cooling'}]
|
|
218
|
+
[8, { name: 'hpcool', desc: 'Cooling' }]
|
|
212
219
|
]);
|
|
213
220
|
this.valueMaps.scheduleTypes = new byteValueMap([
|
|
214
221
|
[0, { name: 'runonce', desc: 'Run Once', startDate: true, startTime: true, endTime: true, days: false, heatSource: true, heatSetpoint: true }],
|
|
@@ -1389,6 +1396,19 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1389
1396
|
}
|
|
1390
1397
|
class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
1391
1398
|
declare board: IntelliCenterBoard;
|
|
1399
|
+
// Need to override this as IntelliCenter manages all the egg timers for all circuit types.
|
|
1400
|
+
public async checkEggTimerExpirationAsync() {
|
|
1401
|
+
try {
|
|
1402
|
+
for (let i = 0; i < sys.circuits.length; i++) {
|
|
1403
|
+
let c = sys.circuits.getItemByIndex(i);
|
|
1404
|
+
let cstate = state.circuits.getItemByIndex(i);
|
|
1405
|
+
if (!cstate.isActive || !cstate.isOn) continue;
|
|
1406
|
+
if (c.master === 1) {
|
|
1407
|
+
await ncp.circuits.checkCircuitEggTimerExpirationAsync(cstate);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
} catch (err) { logger.error(`checkEggTimerExpiration: Error synchronizing circuit relays ${err.message}`); }
|
|
1411
|
+
}
|
|
1392
1412
|
public async setCircuitAsync(data: any): Promise<ICircuit> {
|
|
1393
1413
|
let id = parseInt(data.id, 10);
|
|
1394
1414
|
let circuit = sys.circuits.getItemById(id, false);
|
|
@@ -1405,7 +1425,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
1405
1425
|
let eggHrs = Math.floor(eggTimer / 60);
|
|
1406
1426
|
let eggMins = eggTimer - (eggHrs * 60);
|
|
1407
1427
|
let type = typeof data.type !== 'undefined' ? parseInt(data.type, 10) : circuit.type;
|
|
1408
|
-
let theme = typeof data.lightingTheme !== 'undefined' ? data.
|
|
1428
|
+
let theme = typeof data.lightingTheme !== 'undefined' ? data.lightingTheme : circuit.lightingTheme;
|
|
1409
1429
|
if (circuit.type === 9) theme = typeof data.level !== 'undefined' ? data.level : circuit.level;
|
|
1410
1430
|
if (typeof theme === 'undefined') theme = 0;
|
|
1411
1431
|
let out = Outbound.create({
|
|
@@ -1425,10 +1445,18 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
1425
1445
|
circuit.freeze = (typeof data.freeze !== 'undefined' ? utils.makeBool(data.freeze) : circuit.freeze);
|
|
1426
1446
|
circuit.showInFeatures = (typeof data.showInFeatures !== 'undefined' ? utils.makeBool(data.showInFeatures) : circuit.showInFeatures);
|
|
1427
1447
|
if (type === 9) scircuit.level = circuit.level = theme;
|
|
1428
|
-
else
|
|
1429
|
-
|
|
1448
|
+
else {
|
|
1449
|
+
let t = sys.board.valueMaps.circuitFunctions.transform(type);
|
|
1450
|
+
if (t.isLight == true) scircuit.lightingTheme = circuit.lightingTheme = theme;
|
|
1451
|
+
else {
|
|
1452
|
+
scircuit.lightingTheme = undefined;
|
|
1453
|
+
circuit.lightingTheme = 0;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
scircuit.name = circuit.name = typeof data.name !== 'undefined' ? data.name.toString().substring(0, 16) : circuit.name;
|
|
1430
1457
|
scircuit.type = circuit.type = type;
|
|
1431
1458
|
scircuit.isActive = circuit.isActive = true;
|
|
1459
|
+
circuit.master = 0;
|
|
1432
1460
|
resolve(circuit);
|
|
1433
1461
|
}
|
|
1434
1462
|
}
|
|
@@ -2057,6 +2085,14 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
2057
2085
|
// NOT SURE IF COINCIDENTAL: The ICP seems to respond immediately after action 2.
|
|
2058
2086
|
// 7. ICP Sends 168[15,0,... new options, 0,0,0,0]
|
|
2059
2087
|
// 8. OCP responds ACK(168)
|
|
2088
|
+
// i10D turn on pool
|
|
2089
|
+
// OCP
|
|
2090
|
+
// Schedule on
|
|
2091
|
+
// [255, 0, 255][165, 1, 15, 16, 168, 36][15, 0, 0, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 1][5, 226]
|
|
2092
|
+
// No schedules
|
|
2093
|
+
// [255, 0, 255][165, 1, 15, 16, 168, 36][15, 0, 0, 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 1, 0][5, 195]
|
|
2094
|
+
// njsPC
|
|
2095
|
+
// [255, 0, 255][165, 1, 15, 33, 168, 36][15, 0, 0, 33, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0][5, 216]
|
|
2060
2096
|
|
|
2061
2097
|
// The previous sequence is just additional noise on the bus. There is no need for it. We just
|
|
2062
2098
|
// need to send the set circuit message. It will reliably work 100% of the time but the ICP
|
|
@@ -2066,6 +2102,9 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
2066
2102
|
//if (b) b = await this.getConfigAsync([15, 0]);
|
|
2067
2103
|
return new Promise<ICircuitState>((resolve, reject) => {
|
|
2068
2104
|
let out = this.createCircuitStateMessage(id, val);
|
|
2105
|
+
//if (sys.equipment.dual && id === 6) out.setPayloadByte(35, 1);
|
|
2106
|
+
out.setPayloadByte(34, 1);
|
|
2107
|
+
out.source = 16;
|
|
2069
2108
|
out.onComplete = async (err, msg: Inbound) => {
|
|
2070
2109
|
if (err) reject(err);
|
|
2071
2110
|
else {
|
|
@@ -2299,7 +2338,19 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
2299
2338
|
let ndx = Math.floor(ordinal / 8);
|
|
2300
2339
|
let byte = out.payload[ndx + 15];
|
|
2301
2340
|
let bit = ordinal - (ndx * 8);
|
|
2302
|
-
|
|
2341
|
+
// Lets determine if this schedule should be on.
|
|
2342
|
+
if (sched.circuit === id) {
|
|
2343
|
+
if (isOn) {
|
|
2344
|
+
let dt = state.time.toDate();
|
|
2345
|
+
let dow = dt.getDay();
|
|
2346
|
+
// Convert the dow to the bit value.
|
|
2347
|
+
let sd = sys.board.valueMaps.scheduleDays.toArray().find(elem => elem.dow === dow);
|
|
2348
|
+
let dayVal = sd.bitVal || sd.val; // The bitval allows mask overrides.
|
|
2349
|
+
let ts = dt.getHours() * 60 + dt.getMinutes();
|
|
2350
|
+
if ((sched.scheduleDays & dayVal) > 0 && ts >= sched.startTime && ts <= sched.endTime) byte = byte | (1 << bit);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
else if (sched.isOn) byte = byte | (1 << bit);
|
|
2303
2354
|
out.payload[ndx + 15] = byte;
|
|
2304
2355
|
}
|
|
2305
2356
|
return out;
|
|
@@ -2429,12 +2480,10 @@ class IntelliCenterChlorinatorCommands extends ChlorinatorCommands {
|
|
|
2429
2480
|
public async setChlorAsync(obj: any): Promise<ChlorinatorState> {
|
|
2430
2481
|
let id = parseInt(obj.id, 10);
|
|
2431
2482
|
let isAdd = false;
|
|
2432
|
-
let isVirtual = false;
|
|
2433
|
-
if (id <= 0 || isNaN(id)) id = 1;
|
|
2434
2483
|
let chlor = sys.chlorinators.getItemById(id);
|
|
2435
|
-
if (id
|
|
2484
|
+
if (id <= 0 || isNaN(id)) {
|
|
2436
2485
|
isAdd = true;
|
|
2437
|
-
chlor.master = utils.makeBool(obj.
|
|
2486
|
+
chlor.master = utils.makeBool(obj.master) ? 1 : 0;
|
|
2438
2487
|
// Calculate an id for the chlorinator. The messed up part is that if a chlorinator is not attached to the OCP, its address
|
|
2439
2488
|
// cannot be set by the MUX. This will have to wait.
|
|
2440
2489
|
id = 1;
|
|
@@ -2443,14 +2492,20 @@ class IntelliCenterChlorinatorCommands extends ChlorinatorCommands {
|
|
|
2443
2492
|
//let chlor = extend(true, {}, sys.chlorinators.getItemById(id).get(), obj);
|
|
2444
2493
|
// If this is a virtual chlorinator then go to the base class and handle it from there.
|
|
2445
2494
|
if (chlor.master === 1) return super.setChlorAsync(obj);
|
|
2495
|
+
if (typeof chlor.master === 'undefined') chlor.master = 0;
|
|
2446
2496
|
let name = obj.name || chlor.name || 'IntelliChlor' + id;
|
|
2447
|
-
let poolSetpoint = parseInt(obj.poolSetpoint, 10);
|
|
2448
|
-
let spaSetpoint = parseInt(obj.spaSetpoint, 10);
|
|
2449
2497
|
let superChlorHours = parseInt(obj.superChlorHours, 10);
|
|
2450
2498
|
if (typeof obj.superChlorinate !== 'undefined') obj.superChlor = utils.makeBool(obj.superChlorinate);
|
|
2451
2499
|
let superChlorinate = typeof obj.superChlor === 'undefined' ? undefined : utils.makeBool(obj.superChlor);
|
|
2452
|
-
let disabled = typeof obj.disabled !== 'undefined' ? utils.makeBool(obj.disabled) : chlor.disabled;
|
|
2453
2500
|
let isDosing = typeof obj.isDosing !== 'undefined' ? utils.makeBool(obj.isDosing) : chlor.isDosing;
|
|
2501
|
+
let disabled = typeof obj.disabled !== 'undefined' ? utils.makeBool(obj.disabled) : chlor.disabled;
|
|
2502
|
+
// This should never never never modify the setpoints based upon the disabled or isDosing flags.
|
|
2503
|
+
//let poolSetpoint = isDosing ? 100 : disabled ? 0 : parseInt(obj.poolSetpoint, 10);
|
|
2504
|
+
//let spaSetpoint = isDosing ? 100 : disabled ? 0 : parseInt(obj.spaSetpoint, 10);
|
|
2505
|
+
let poolSetpoint = typeof obj.poolSetpoint !== 'undefined' ? parseInt(obj.poolSetpoint, 10) : chlor.poolSetpoint;
|
|
2506
|
+
let spaSetpoint = typeof obj.spaSetpoint !== 'undefined' ? parseInt(obj.spaSetpoint, 10) : chlor.spaSetpoint;
|
|
2507
|
+
if (poolSetpoint === 0) console.log(obj);
|
|
2508
|
+
|
|
2454
2509
|
let model = typeof obj.model !== 'undefined' ? obj.model : chlor.model;
|
|
2455
2510
|
let chlorType = typeof obj.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(obj.type) : chlor.type || 0;
|
|
2456
2511
|
if (isAdd) {
|
|
@@ -2476,7 +2531,10 @@ class IntelliCenterChlorinatorCommands extends ChlorinatorCommands {
|
|
|
2476
2531
|
return new Promise<ChlorinatorState>((resolve, reject) => {
|
|
2477
2532
|
let out = Outbound.create({
|
|
2478
2533
|
action: 168,
|
|
2479
|
-
payload: [7, 0, id - 1, body.val, 1,
|
|
2534
|
+
payload: [7, 0, id - 1, body.val, 1,
|
|
2535
|
+
disabled ? 0 : isDosing ? 100 : poolSetpoint,
|
|
2536
|
+
disabled ? 0 : isDosing ? 100 : spaSetpoint,
|
|
2537
|
+
superChlorinate ? 1 : 0, superChlorHours, 0, 1],
|
|
2480
2538
|
response: IntelliCenterBoard.getAckResponse(168),
|
|
2481
2539
|
retries: 5,
|
|
2482
2540
|
onComplete: (err, msg) => {
|
|
@@ -2504,21 +2562,22 @@ class IntelliCenterChlorinatorCommands extends ChlorinatorCommands {
|
|
|
2504
2562
|
}
|
|
2505
2563
|
public async deleteChlorAsync(obj: any): Promise<ChlorinatorState> {
|
|
2506
2564
|
let id = parseInt(obj.id, 10);
|
|
2507
|
-
if (isNaN(id)) obj.id
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
let
|
|
2565
|
+
if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator id is not valid: ${obj.id}`, 'chlorinator', obj.id));
|
|
2566
|
+
let chlor = sys.chlorinators.getItemById(id);
|
|
2567
|
+
if (chlor.master === 1) return await super.deleteChlorAsync(obj);
|
|
2568
|
+
let schlor = state.chlorinators.getItemById(id);
|
|
2511
2569
|
// Verify the data.
|
|
2512
2570
|
return new Promise<ChlorinatorState>((resolve, reject) => {
|
|
2513
2571
|
let out = Outbound.create({
|
|
2514
2572
|
action: 168,
|
|
2515
|
-
payload: [7, 0, id - 1,
|
|
2573
|
+
payload: [7, 0, id - 1, schlor.body || 0, 0, schlor.poolSetpoint || 0, schlor.spaSetpoint || 0, 0, schlor.superChlorHours || 0, 0, 0],
|
|
2516
2574
|
response: IntelliCenterBoard.getAckResponse(168),
|
|
2517
2575
|
retries: 5,
|
|
2518
2576
|
onComplete: (err, msg) => {
|
|
2519
2577
|
if (err) reject(err);
|
|
2520
2578
|
else {
|
|
2521
|
-
|
|
2579
|
+
ncp.chlorinators.deleteChlorinatorAsync(id).then(()=>{});
|
|
2580
|
+
schlor = state.chlorinators.getItemById(id, true);
|
|
2522
2581
|
state.chlorinators.removeItemById(id);
|
|
2523
2582
|
sys.chlorinators.removeItemById(id);
|
|
2524
2583
|
resolve(schlor);
|
|
@@ -2607,7 +2666,7 @@ class IntelliCenterPumpCommands extends PumpCommands {
|
|
|
2607
2666
|
let id = (typeof data.id === 'undefined' || data.id <= 0) ? sys.pumps.getNextEquipmentId(sys.board.equipmentIds.pumps) : parseInt(data.id, 10);
|
|
2608
2667
|
if (isNaN(id)) return Promise.reject(new Error(`Invalid pump id: ${data.id}`));
|
|
2609
2668
|
let pump = sys.pumps.getItemById(id, false);
|
|
2610
|
-
if (data.master > 0 || pump.master > 0
|
|
2669
|
+
if (data.master > 0 || pump.master > 0) return await super.setPumpAsync(data);
|
|
2611
2670
|
|
|
2612
2671
|
// 0 6 10 11 12 15
|
|
2613
2672
|
//[255, 0, 255][165, 63, 15, 16, 168, 34][4, 0, 0, 3, 0, 96, 194, 1, 122, 13, 15, 130, 1, 196, 9, 128, 2, 255, 5, 0, 251, 128, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0][11, 218]
|
|
@@ -3186,6 +3245,8 @@ class IntelliCenterScheduleCommands extends ScheduleCommands {
|
|
|
3186
3245
|
out.retries = 5;
|
|
3187
3246
|
out.onComplete = (err, msg) => {
|
|
3188
3247
|
if (!err) {
|
|
3248
|
+
sched = sys.schedules.getItemById(id, true);
|
|
3249
|
+
ssched = state.schedules.getItemById(id, true);
|
|
3189
3250
|
sched.circuit = ssched.circuit = circuit;
|
|
3190
3251
|
sched.scheduleDays = ssched.scheduleDays = schedDays;
|
|
3191
3252
|
sched.scheduleType = ssched.scheduleType = schedType;
|
|
@@ -3282,14 +3343,14 @@ class IntelliCenterHeaterCommands extends HeaterCommands {
|
|
|
3282
3343
|
conn.queueSendMessage(out);
|
|
3283
3344
|
}
|
|
3284
3345
|
public async setHeaterAsync(obj: any): Promise<Heater> {
|
|
3285
|
-
if (
|
|
3346
|
+
if (obj.master === 1 || parseInt(obj.id, 10) > 255) return super.setHeaterAsync(obj);
|
|
3286
3347
|
return new Promise<Heater>((resolve, reject) => {
|
|
3287
3348
|
let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
|
|
3288
3349
|
if (isNaN(id)) return reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
|
|
3289
3350
|
let heater: Heater;
|
|
3290
3351
|
if (id <= 0) {
|
|
3291
3352
|
// We are adding a heater. In this case all heaters are virtual.
|
|
3292
|
-
let vheaters = sys.heaters.filter(h => h.
|
|
3353
|
+
let vheaters = sys.heaters.filter(h => h.master === 1);
|
|
3293
3354
|
id = vheaters.length + 1;
|
|
3294
3355
|
}
|
|
3295
3356
|
heater = sys.heaters.getItemById(id, false);
|
|
@@ -3397,7 +3458,6 @@ class IntelliCenterHeaterCommands extends HeaterCommands {
|
|
|
3397
3458
|
heater.economyTime = economyTime;
|
|
3398
3459
|
heater.startTempDelta = startTempDelta;
|
|
3399
3460
|
heater.stopTempDelta = stopTempDelta;
|
|
3400
|
-
//hstate.isVirtual = heater.isVirtual = false;
|
|
3401
3461
|
heater.cooldownDelay = cooldownDelay;
|
|
3402
3462
|
sys.board.heaters.updateHeaterServices();
|
|
3403
3463
|
sys.board.heaters.syncHeaterStates();
|
|
@@ -3410,7 +3470,7 @@ class IntelliCenterHeaterCommands extends HeaterCommands {
|
|
|
3410
3470
|
});
|
|
3411
3471
|
}
|
|
3412
3472
|
public async deleteHeaterAsync(obj): Promise<Heater> {
|
|
3413
|
-
if (
|
|
3473
|
+
if (obj.master === 1 || parseInt(obj.id, 10) > 255) return await super.deleteHeaterAsync(obj);
|
|
3414
3474
|
return new Promise<Heater>((resolve, reject) => {
|
|
3415
3475
|
let id = parseInt(obj.id, 10);
|
|
3416
3476
|
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
|
|
@@ -3452,6 +3512,8 @@ class IntelliCenterHeaterCommands extends HeaterCommands {
|
|
|
3452
3512
|
let heatPumpInstalled = htypes.heatpump > 0;
|
|
3453
3513
|
let gasHeaterInstalled = htypes.gas > 0;
|
|
3454
3514
|
let ultratempInstalled = htypes.ultratemp > 0;
|
|
3515
|
+
let mastertempInstalled = htypes.mastertemp > 0;
|
|
3516
|
+
|
|
3455
3517
|
|
|
3456
3518
|
// RKS: 09-26-20 This is a hack to maintain backward compatability with fw versions 1.04 and below. Ultratemp is not
|
|
3457
3519
|
// supported on 1.04 and below.
|
|
@@ -3475,9 +3537,10 @@ class IntelliCenterHeaterCommands extends HeaterCommands {
|
|
|
3475
3537
|
// 3 = Solar Heater
|
|
3476
3538
|
// 4 = Solar Preferred
|
|
3477
3539
|
// 5 = Heat Pump
|
|
3478
|
-
|
|
3540
|
+
|
|
3479
3541
|
if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]);
|
|
3480
3542
|
if (gasHeaterInstalled) sys.board.valueMaps.heatSources.merge([[2, { name: 'heater', desc: 'Heater' }]]);
|
|
3543
|
+
if (mastertempInstalled) sys.board.valueMaps.heatSources.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]);
|
|
3481
3544
|
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 }]]);
|
|
3482
3545
|
else if (solarInstalled) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar', hasCoolsetpoint: htypes.hasCoolSetpoint }]]);
|
|
3483
3546
|
if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Pref' }]]);
|
|
@@ -3488,11 +3551,12 @@ class IntelliCenterHeaterCommands extends HeaterCommands {
|
|
|
3488
3551
|
|
|
3489
3552
|
sys.board.valueMaps.heatModes = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]);
|
|
3490
3553
|
if (gasHeaterInstalled) sys.board.valueMaps.heatModes.merge([[2, { name: 'heater', desc: 'Heater' }]]);
|
|
3491
|
-
if (
|
|
3554
|
+
if (mastertempInstalled) sys.board.valueMaps.heatModes.merge([11, { name: 'mtheater', desc: 'MasterTemp' }]);
|
|
3555
|
+
if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar Only' }], [4, { name: 'solarpref', desc: 'Solar Preferred' }]]);
|
|
3492
3556
|
else if (solarInstalled) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar' }]]);
|
|
3493
|
-
if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only'}], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref' }]]);
|
|
3557
|
+
if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only' }], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref' }]]);
|
|
3494
3558
|
else if (ultratempInstalled) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp' }]]);
|
|
3495
|
-
if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
|
|
3559
|
+
if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
|
|
3496
3560
|
else if (heatPumpInstalled) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heat Pump' }]]);
|
|
3497
3561
|
}
|
|
3498
3562
|
else {
|
|
@@ -3524,7 +3588,7 @@ class IntelliCenterHeaterCommands extends HeaterCommands {
|
|
|
3524
3588
|
}
|
|
3525
3589
|
class IntelliCenterValveCommands extends ValveCommands {
|
|
3526
3590
|
public async setValveAsync(obj?: any): Promise<Valve> {
|
|
3527
|
-
if (obj.
|
|
3591
|
+
if (obj.master === 1) return super.setValveAsync(obj);
|
|
3528
3592
|
let id = parseInt(obj.id, 10);
|
|
3529
3593
|
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Valve Id has not been defined', obj.id, 'Valve'));
|
|
3530
3594
|
let valve = sys.valves.getItemById(id);
|
|
@@ -3574,7 +3638,7 @@ export class IntelliCenterChemControllerCommands extends ChemControllerCommands
|
|
|
3574
3638
|
// Now lets do all our validation to the incoming chem controller data.
|
|
3575
3639
|
let name = typeof data.name !== 'undefined' ? data.name : chem.name || `IntelliChem - ${address - 143}`;
|
|
3576
3640
|
let type = sys.board.valueMaps.chemControllerTypes.transformByName('intellichem');
|
|
3577
|
-
// So now we are down to the nitty gritty setting the data for the REM
|
|
3641
|
+
// So now we are down to the nitty gritty setting the data for the REM Chem controller.
|
|
3578
3642
|
let calciumHardness = typeof data.calciumHardness !== 'undefined' ? parseInt(data.calciumHardness, 10) : chem.calciumHardness;
|
|
3579
3643
|
let cyanuricAcid = typeof data.cyanuricAcid !== 'undefined' ? parseInt(data.cyanuricAcid, 10) : chem.cyanuricAcid;
|
|
3580
3644
|
let alkalinity = typeof data.alkalinity !== 'undefined' ? parseInt(data.alkalinity, 10) : chem.alkalinity;
|
|
@@ -14,16 +14,17 @@ GNU Affero General Public License for more details.
|
|
|
14
14
|
You should have received a copy of the GNU Affero General Public License
|
|
15
15
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
16
|
*/
|
|
17
|
-
import {byteValueMap} from './SystemBoard';
|
|
18
|
-
import {logger} from '../../logger/Logger';
|
|
17
|
+
import { byteValueMap } from './SystemBoard';
|
|
18
|
+
import { logger } from '../../logger/Logger';
|
|
19
19
|
import { EasyTouchBoard, TouchConfigQueue, GetTouchConfigCategories, TouchCircuitCommands } from './EasyTouchBoard';
|
|
20
20
|
import { state, ICircuitGroupState } from '../State';
|
|
21
|
-
import { PoolSystem, sys, ExpansionPanel,
|
|
22
|
-
|
|
21
|
+
import { PoolSystem, sys, ExpansionPanel, Equipment } from '../Equipment';
|
|
22
|
+
|
|
23
|
+
import { conn } from '../comms/Comms';
|
|
24
|
+
import { InvalidEquipmentDataError } from '../Errors';
|
|
23
25
|
|
|
24
|
-
import {conn} from '../comms/Comms';
|
|
25
26
|
export class IntelliTouchBoard extends EasyTouchBoard {
|
|
26
|
-
constructor
|
|
27
|
+
constructor(system: PoolSystem) {
|
|
27
28
|
super(system);
|
|
28
29
|
this.equipmentIds.features.start = 41;
|
|
29
30
|
this.equipmentIds.features.end = 50;
|
|
@@ -32,10 +33,11 @@ export class IntelliTouchBoard extends EasyTouchBoard {
|
|
|
32
33
|
[0, { name: 'IT5', part: 'i5+3', desc: 'IntelliTouch i5+3', circuits: 6, shared: true }],
|
|
33
34
|
[1, { name: 'IT7', part: 'i7+3', desc: 'IntelliTouch i7+3', circuits: 8, shared: true }],
|
|
34
35
|
[2, { name: 'IT9', part: 'i9+3', desc: 'IntelliTouch i9+3', circuits: 10, shared: true }],
|
|
35
|
-
[3, { name: 'IT5S', part: 'i5+3S', desc: 'IntelliTouch i5+3S', circuits: 5, shared: false }],
|
|
36
|
-
[4, { name: 'IT9S', part: 'i9+3S', desc: 'IntelliTouch i9+3S', circuits: 9, shared: false }],
|
|
36
|
+
[3, { name: 'IT5S', part: 'i5+3S', desc: 'IntelliTouch i5+3S', circuits: 5, shared: false, bodies: 1, intakeReturnValves: false }],
|
|
37
|
+
[4, { name: 'IT9S', part: 'i9+3S', desc: 'IntelliTouch i9+3S', circuits: 9, shared: false, bodies: 1, intakeReturnValves: false }],
|
|
37
38
|
[5, { name: 'IT10D', part: 'i10D', desc: 'IntelliTouch i10D', circuits: 10, shared: false, dual: true }],
|
|
38
|
-
[32, { name: '
|
|
39
|
+
[32, { name: 'IT5X', part: 'i5X', desc: 'IntelliTouch i5X', circuits: 5 }],
|
|
40
|
+
[33, { name: 'IT10X', part: 'i10X', desc: 'IntelliTouch i10X', circuits: 10 }]
|
|
39
41
|
]);
|
|
40
42
|
}
|
|
41
43
|
public initExpansionModules(byte1: number, byte2: number) {
|
|
@@ -48,6 +50,7 @@ export class IntelliTouchBoard extends EasyTouchBoard {
|
|
|
48
50
|
mod.type = byte1;
|
|
49
51
|
mod.part = mt.part;
|
|
50
52
|
let eq = sys.equipment;
|
|
53
|
+
let bd = sys.board;
|
|
51
54
|
let md = mod.get();
|
|
52
55
|
|
|
53
56
|
eq.maxBodies = md.bodies = typeof mt.bodies !== 'undefined' ? mt.bodies : mt.shared || mt.dual ? 2 : 1;
|
|
@@ -57,16 +60,18 @@ export class IntelliTouchBoard extends EasyTouchBoard {
|
|
|
57
60
|
eq.maxPumps = md.maxPumps = typeof mt.pumps !== 'undefined' ? mt.pumps : 8;
|
|
58
61
|
eq.shared = mt.shared;
|
|
59
62
|
eq.dual = typeof mt.dual !== 'undefined' ? mt.dual : false;
|
|
63
|
+
eq.intakeReturnValves = md.intakeReturnValves = typeof mt.intakeReturnValves !== 'undefined' ? mt.intakeReturnValves : false;
|
|
60
64
|
eq.maxChlorinators = md.chlorinators = 1;
|
|
61
65
|
eq.maxChemControllers = md.chemControllers = 1;
|
|
62
66
|
eq.maxCustomNames = 20;
|
|
63
67
|
eq.maxCircuitGroups = 10; // Not sure why this is 10 other than to allow for those that we are in control of.
|
|
64
68
|
|
|
65
69
|
// Calculate out the invalid ids.
|
|
66
|
-
sys.board.equipmentIds.invalidIds.set([]);
|
|
67
|
-
if (!eq.shared) sys.board.equipmentIds.invalidIds.merge([1]);
|
|
70
|
+
// sys.board.equipmentIds.invalidIds.set([]);
|
|
68
71
|
// Add in all the invalid ids from the base personality board.
|
|
69
72
|
sys.board.equipmentIds.invalidIds.set([16, 17, 18]); // These appear to alway be invalid in IntelliTouch.
|
|
73
|
+
// RGS 10-7-21: Since single bodies have hi-temp/lo-temp we will always want ID 1.
|
|
74
|
+
// if (!eq.shared) sys.board.equipmentIds.invalidIds.merge([1]);
|
|
70
75
|
//if (eq.maxCircuits < 9) sys.board.equipmentIds.invalidIds.merge([9]);
|
|
71
76
|
for (let i = 7; i <= 10; i++) {
|
|
72
77
|
// This will add all the invalid ids between 7 and 10 that are omitted for IntelliTouch models.
|
|
@@ -74,26 +79,42 @@ export class IntelliTouchBoard extends EasyTouchBoard {
|
|
|
74
79
|
if (i > eq.maxCircuits) sys.board.equipmentIds.invalidIds.merge([i]);
|
|
75
80
|
}
|
|
76
81
|
// This code should be repeated if we ever see a panel with more than one expansion panel.
|
|
77
|
-
let
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
let emd = pnl.modules.getItemById(1, true).get();
|
|
87
|
-
eq.maxCircuits += emd.circuits = typeof emt.circuits !== 'undefined' ? emt.circuits : 0;
|
|
82
|
+
let pnl1: ExpansionPanel;
|
|
83
|
+
if ((byte2 & 0x40) === 64) {
|
|
84
|
+
// 64 indicates one expansion panel; SL defaults to i10x but it could also be i5x until we know better
|
|
85
|
+
pnl1 = sys.equipment.expansions.getItemById(1, true);
|
|
86
|
+
pnl1.type = 32;
|
|
87
|
+
let emt = this.valueMaps.expansionBoards.transform(pnl1.type);
|
|
88
|
+
pnl1.name = emt.desc;
|
|
89
|
+
pnl1.isActive = true;
|
|
90
|
+
eq.maxCircuits += emt.circuits;
|
|
88
91
|
}
|
|
89
|
-
else
|
|
90
|
-
|
|
92
|
+
else sys.equipment.expansions.removeItemById(1);
|
|
93
|
+
let pnl2: ExpansionPanel;
|
|
94
|
+
if ((byte2 & 0x80) === 128) {
|
|
95
|
+
// SL defaults to i5x but it could also be i10x until we know better
|
|
96
|
+
pnl2 = sys.equipment.expansions.getItemById(2, true);
|
|
97
|
+
pnl2.type = 32;
|
|
98
|
+
let emt = this.valueMaps.expansionBoards.transform(pnl2.type);
|
|
99
|
+
pnl2.name = emt.desc;
|
|
100
|
+
pnl2.isActive = true;
|
|
101
|
+
eq.maxCircuits += emt.circuits;
|
|
102
|
+
}
|
|
103
|
+
else sys.equipment.expansions.removeItemById(2);
|
|
104
|
+
let pnl3: ExpansionPanel;
|
|
105
|
+
if ((byte2 & 0xC0) === 192) {
|
|
106
|
+
// SL defaults to i5x but it could also be i10x until we know better
|
|
107
|
+
pnl3 = sys.equipment.expansions.getItemById(3, true);
|
|
108
|
+
pnl3.type = 32;
|
|
109
|
+
let emt = this.valueMaps.expansionBoards.transform(pnl3.type);
|
|
110
|
+
pnl3.name = emt.desc;
|
|
111
|
+
pnl3.isActive = true;
|
|
112
|
+
eq.maxCircuits += emt.circuits;
|
|
113
|
+
}
|
|
114
|
+
else sys.equipment.expansions.removeItemById(3);
|
|
91
115
|
if (byte1 !== 14) sys.board.equipmentIds.invalidIds.merge([10, 19]);
|
|
92
116
|
state.equipment.model = sys.equipment.model = mt.desc;
|
|
93
117
|
state.equipment.controllerType = 'intellitouch';
|
|
94
|
-
// The code above should be repeated if we ever see a panel with more than one expansion panel.
|
|
95
|
-
sys.equipment.expansions.getItemById(2, true).isActive = false;
|
|
96
|
-
sys.equipment.expansions.getItemById(3, true).isActive = false;
|
|
97
118
|
sys.equipment.shared ? sys.board.equipmentIds.circuits.start = 1 : sys.board.equipmentIds.circuits.start = 2;
|
|
98
119
|
this.initBodyDefaults();
|
|
99
120
|
this.initHeaterDefaults();
|
|
@@ -117,6 +138,59 @@ export class IntelliTouchBoard extends EasyTouchBoard {
|
|
|
117
138
|
state.emitControllerChange();
|
|
118
139
|
}
|
|
119
140
|
public circuits: ITTouchCircuitCommands = new ITTouchCircuitCommands(this);
|
|
141
|
+
public async setControllerType(obj): Promise<Equipment> {
|
|
142
|
+
try {
|
|
143
|
+
if (obj.controllerType !== sys.controllerType) {
|
|
144
|
+
return Promise.reject(new InvalidEquipmentDataError(`You may not change the controller type data for ${sys.controllerType} controllers`, 'controllerType', obj.controllerType));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let mod = sys.equipment.modules.getItemById(0);
|
|
148
|
+
let mt = this.valueMaps.expansionBoards.get(mod.type);
|
|
149
|
+
let _circuits = mt.circuits;
|
|
150
|
+
let pnl1 = sys.equipment.expansions.getItemById(1);
|
|
151
|
+
if (typeof obj.expansion1 !== 'undefined' && obj.expansion1 !== pnl1.type) {
|
|
152
|
+
let emt = this.valueMaps.expansionBoards.transform(obj.expansion1);
|
|
153
|
+
logger.info(`Changing expansion 1 to ${emt.desc}.`);
|
|
154
|
+
pnl1.type = emt.val;
|
|
155
|
+
pnl1.name = emt.desc;
|
|
156
|
+
pnl1.isActive = true;
|
|
157
|
+
}
|
|
158
|
+
let pnl2 = sys.equipment.expansions.getItemById(2);
|
|
159
|
+
if (typeof obj.expansion2 !== 'undefined' && obj.expansion2 !== pnl2.type) {
|
|
160
|
+
let emt = this.valueMaps.expansionBoards.transform(obj.expansion2);
|
|
161
|
+
logger.info(`Changing expansion 2 to ${emt.desc}.`);
|
|
162
|
+
pnl2.type = emt.val;
|
|
163
|
+
pnl2.name = emt.desc;
|
|
164
|
+
pnl2.isActive = true;
|
|
165
|
+
}
|
|
166
|
+
let pnl3 = sys.equipment.expansions.getItemById(3);
|
|
167
|
+
if (typeof obj.expansion3 !== 'undefined' && obj.expansion3 !== pnl3.type) {
|
|
168
|
+
let emt = this.valueMaps.expansionBoards.transform(obj.expansion3);
|
|
169
|
+
logger.info(`Changing expansion 3 to ${emt.desc}.`);
|
|
170
|
+
pnl3.type = emt.val;
|
|
171
|
+
pnl3.name = emt.desc;
|
|
172
|
+
pnl3.isActive = true;
|
|
173
|
+
}
|
|
174
|
+
let prevMaxCircuits = sys.equipment.maxCircuits;
|
|
175
|
+
if (pnl1.isActive) _circuits += this.valueMaps.expansionBoards.get(pnl1.type).circuits;
|
|
176
|
+
if (pnl2.isActive) _circuits += this.valueMaps.expansionBoards.get(pnl2.type).circuits;
|
|
177
|
+
if (pnl3.isActive) _circuits += this.valueMaps.expansionBoards.get(pnl3.type).circuits;
|
|
178
|
+
if (_circuits < prevMaxCircuits) {
|
|
179
|
+
// if we downsize expansions, remove circuits
|
|
180
|
+
for (let i = _circuits + 1; i <= prevMaxCircuits; i++) {
|
|
181
|
+
sys.circuits.removeItemById(i);
|
|
182
|
+
state.circuits.removeItemById(i);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (_circuits > prevMaxCircuits) {
|
|
186
|
+
this._configQueue.queueChanges();
|
|
187
|
+
}
|
|
188
|
+
sys.equipment.maxCircuits = _circuits;
|
|
189
|
+
return sys.equipment;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
logger.error(`Error setting expansion panels: ${err.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
120
194
|
}
|
|
121
195
|
class ITTouchConfigQueue extends TouchConfigQueue {
|
|
122
196
|
public queueChanges() {
|
|
@@ -148,7 +222,7 @@ class ITTouchConfigQueue extends TouchConfigQueue {
|
|
|
148
222
|
}
|
|
149
223
|
if (this.remainingItems > 0) {
|
|
150
224
|
var self = this;
|
|
151
|
-
setTimeout(() => {self.processNext();}, 50);
|
|
225
|
+
setTimeout(() => { self.processNext(); }, 50);
|
|
152
226
|
} else state.status = 1;
|
|
153
227
|
state.emitControllerChange();
|
|
154
228
|
}
|
|
@@ -164,5 +238,5 @@ class ITTouchCircuitCommands extends TouchCircuitCommands {
|
|
|
164
238
|
}
|
|
165
239
|
catch (err) { reject(err); }
|
|
166
240
|
});
|
|
167
|
-
}
|
|
241
|
+
}
|
|
168
242
|
}
|