nodejs-poolcontroller 8.4.0 → 8.4.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/workflows/ghcr-publish.yml +1 -1
- package/157_issues.md +101 -0
- package/AGENTS.md +17 -1
- package/README.md +13 -2
- package/controller/Equipment.ts +49 -0
- package/controller/State.ts +8 -0
- package/controller/boards/AquaLinkBoard.ts +174 -2
- package/controller/boards/EasyTouchBoard.ts +44 -0
- package/controller/boards/IntelliCenterBoard.ts +360 -172
- package/controller/boards/NixieBoard.ts +7 -4
- package/controller/boards/SunTouchBoard.ts +1 -0
- package/controller/boards/SystemBoard.ts +39 -4
- package/controller/comms/Comms.ts +9 -3
- package/controller/comms/messages/Messages.ts +218 -24
- package/controller/comms/messages/config/EquipmentMessage.ts +34 -0
- package/controller/comms/messages/config/ExternalMessage.ts +1051 -989
- package/controller/comms/messages/config/GeneralMessage.ts +65 -0
- package/controller/comms/messages/config/OptionsMessage.ts +15 -2
- package/controller/comms/messages/config/PumpMessage.ts +427 -421
- package/controller/comms/messages/config/SecurityMessage.ts +37 -13
- package/controller/comms/messages/status/EquipmentStateMessage.ts +0 -218
- package/controller/comms/messages/status/HeaterStateMessage.ts +27 -15
- package/controller/comms/messages/status/NeptuneModbusStateMessage.ts +217 -0
- package/controller/comms/messages/status/VersionMessage.ts +67 -18
- package/controller/nixie/chemistry/ChemController.ts +65 -33
- package/controller/nixie/heaters/Heater.ts +10 -1
- package/controller/nixie/pumps/Pump.ts +145 -2
- package/docker-compose.yml +1 -0
- package/logger/Logger.ts +75 -64
- package/package.json +1 -1
- package/tsconfig.json +2 -1
- package/web/Server.ts +3 -1
- package/web/services/config/Config.ts +150 -1
- package/web/services/state/State.ts +21 -0
- package/web/services/state/StateSocket.ts +28 -0
|
@@ -23,7 +23,7 @@ import { Protocol, Outbound, Inbound, Message, Response } from '../comms/message
|
|
|
23
23
|
import { conn } from '../comms/Comms';
|
|
24
24
|
import { logger } from '../../logger/Logger';
|
|
25
25
|
import { state, ChlorinatorState, LightGroupState, VirtualCircuitState, ICircuitState, BodyTempState, CircuitGroupState, ICircuitGroupState, ChemControllerState } from '../State';
|
|
26
|
-
import { utils } from '../../controller/Constants';
|
|
26
|
+
import { utils, ControllerType } from '../../controller/Constants';
|
|
27
27
|
import { InvalidEquipmentIdError, InvalidEquipmentDataError, EquipmentNotFoundError, MessageError, InvalidOperationError } from '../Errors';
|
|
28
28
|
import { ncp } from '../nixie/Nixie';
|
|
29
29
|
import { Timestamp } from "../Constants"
|
|
@@ -340,6 +340,7 @@ export class IntelliCenterBoard extends SystemBoard {
|
|
|
340
340
|
const out: Outbound = Outbound.create({
|
|
341
341
|
dest: 16, // MUST send to OCP (16), not broadcast (15)
|
|
342
342
|
action: 251,
|
|
343
|
+
scope: 'v3Registration',
|
|
343
344
|
payload: [
|
|
344
345
|
Message.pluginAddress, // [0] Device address (33)
|
|
345
346
|
0, // [1] Reserved
|
|
@@ -351,7 +352,8 @@ export class IntelliCenterBoard extends SystemBoard {
|
|
|
351
352
|
1, 7, 11 // [19-21] Unknown (copied from wireless remote)
|
|
352
353
|
],
|
|
353
354
|
retries: 3,
|
|
354
|
-
|
|
355
|
+
// Action 253 comes from OCP (src=16) to broadcast (dest=15).
|
|
356
|
+
response: Response.create({ source: 16, dest: 15, action: 253 })
|
|
355
357
|
});
|
|
356
358
|
await out.sendAsync();
|
|
357
359
|
logger.silly('Device registration request sent, awaiting confirmation via Action 217');
|
|
@@ -389,6 +391,9 @@ export class IntelliCenterBoard extends SystemBoard {
|
|
|
389
391
|
logger.warn(`checkConfiguration failed (port may not be open yet): ${err.message}`);
|
|
390
392
|
}
|
|
391
393
|
}
|
|
394
|
+
public isConfigQueueProcessing(): boolean {
|
|
395
|
+
return this._configQueue._processing;
|
|
396
|
+
}
|
|
392
397
|
public requestConfiguration(ver: ConfigVersion) {
|
|
393
398
|
if (this.needsConfigChanges) {
|
|
394
399
|
logger.info(`Requesting IntelliCenter configuration`);
|
|
@@ -419,9 +424,13 @@ export class IntelliCenterBoard extends SystemBoard {
|
|
|
419
424
|
const verReq = Outbound.create({
|
|
420
425
|
dest,
|
|
421
426
|
action: 228,
|
|
427
|
+
scope: sys.equipment.isIntellicenterV3 ? 'v3VersionSync' : undefined,
|
|
422
428
|
payload: [0],
|
|
423
429
|
retries: 3,
|
|
424
|
-
|
|
430
|
+
// v3.004+: require the version response (164) to be addressed to us (not to Wireless).
|
|
431
|
+
response: sys.equipment.isIntellicenterV3
|
|
432
|
+
? Response.create({ dest: Message.pluginAddress, action: 164 })
|
|
433
|
+
: Response.create({ action: 164 })
|
|
425
434
|
});
|
|
426
435
|
await verReq.sendAsync();
|
|
427
436
|
if (sys.equipment.isIntellicenterV3) {
|
|
@@ -776,7 +785,7 @@ export class IntelliCenterBoard extends SystemBoard {
|
|
|
776
785
|
}
|
|
777
786
|
}
|
|
778
787
|
public get commandSourceAddress(): number { return Message.pluginAddress; }
|
|
779
|
-
public get commandDestAddress(): number { return 15; }
|
|
788
|
+
public get commandDestAddress(): number { return sys.equipment.isIntellicenterV3 ? 16 : 15; }
|
|
780
789
|
public static getAckResponse(action: number, source?: number, dest?: number): Response { return Response.create({ source: source, dest: dest || sys.board.commandSourceAddress, action: 1, payload: [action] }); }
|
|
781
790
|
}
|
|
782
791
|
class IntelliCenterConfigRequest extends ConfigRequest {
|
|
@@ -793,10 +802,53 @@ class IntelliCenterConfigQueue extends ConfigQueue {
|
|
|
793
802
|
public _processing: boolean = false;
|
|
794
803
|
public _newRequest: boolean = false;
|
|
795
804
|
public _failed: boolean = false;
|
|
805
|
+
private static readonly WATCHDOG_TIMEOUT_MS = 120000;
|
|
806
|
+
private static readonly WATCHDOG_POLL_MS = 5000;
|
|
807
|
+
private _watchdogTimer?: NodeJS.Timeout;
|
|
808
|
+
private _lastProgressMs: number = 0;
|
|
809
|
+
public close() {
|
|
810
|
+
this.stopWatchdog();
|
|
811
|
+
this._processing = false;
|
|
812
|
+
super.close();
|
|
813
|
+
}
|
|
814
|
+
private markProgress(): void {
|
|
815
|
+
if (!this._processing) return;
|
|
816
|
+
this._lastProgressMs = Date.now();
|
|
817
|
+
if (!this._watchdogTimer) {
|
|
818
|
+
this._watchdogTimer = setInterval(() => this.checkWatchdog(), IntelliCenterConfigQueue.WATCHDOG_POLL_MS);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
private stopWatchdog(): void {
|
|
822
|
+
if (this._watchdogTimer) {
|
|
823
|
+
clearInterval(this._watchdogTimer);
|
|
824
|
+
this._watchdogTimer = undefined;
|
|
825
|
+
}
|
|
826
|
+
this._lastProgressMs = 0;
|
|
827
|
+
}
|
|
828
|
+
private checkWatchdog(): void {
|
|
829
|
+
if (!this._processing || this.closed) {
|
|
830
|
+
this.stopWatchdog();
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const elapsed = Date.now() - this._lastProgressMs;
|
|
834
|
+
if (elapsed < IntelliCenterConfigQueue.WATCHDOG_TIMEOUT_MS) return;
|
|
835
|
+
logger.warn(`Config queue watchdog timed out after ${elapsed}ms; forcing recovery (${this.remainingItems} items remaining)`);
|
|
836
|
+
this.queue.length = 0;
|
|
837
|
+
this.curr = null;
|
|
838
|
+
this.totalItems = 0;
|
|
839
|
+
this._processing = false;
|
|
840
|
+
this._failed = false;
|
|
841
|
+
this._newRequest = false;
|
|
842
|
+
this.stopWatchdog();
|
|
843
|
+
state.status = 1;
|
|
844
|
+
state.emitControllerChange();
|
|
845
|
+
setTimeout(() => { sys.board.checkConfiguration(); }, 250);
|
|
846
|
+
}
|
|
796
847
|
public processNext(msg?: Outbound) {
|
|
797
848
|
if (this.closed) return;
|
|
798
849
|
let self = this;
|
|
799
850
|
if (typeof msg !== 'undefined' && msg !== null) {
|
|
851
|
+
this.markProgress();
|
|
800
852
|
if (!msg.failed) {
|
|
801
853
|
// Remove all references to future items. We got it so we don't need it again.
|
|
802
854
|
this.removeItem(msg.payload[0], msg.payload[1]);
|
|
@@ -847,14 +899,16 @@ class IntelliCenterConfigQueue extends ConfigQueue {
|
|
|
847
899
|
let out = Outbound.create({
|
|
848
900
|
// v1: broadcast (15). v3: wireless/ICP unicasts to OCP (16).
|
|
849
901
|
dest,
|
|
902
|
+
scope: sys.equipment.isIntellicenterV3 ? 'v3ConfigQueue' : undefined,
|
|
850
903
|
action: 222, payload: [this.curr.category, itm], retries: 5,
|
|
851
904
|
// v3.004+: some config requests can yield an Action 30 with an empty payload (length=0).
|
|
852
905
|
// Those packets still indicate "done" for the requested item, but cannot be matched by payload prefix.
|
|
853
906
|
response: sys.equipment.isIntellicenterV3
|
|
854
|
-
? Response.create({ dest:
|
|
907
|
+
? Response.create({ dest: Message.pluginAddress, action: 30 })
|
|
855
908
|
: Response.create({ dest: -1, action: 30, payload: [this.curr.category, itm] })
|
|
856
909
|
});
|
|
857
910
|
logger.verbose(`Requesting config for: ${ConfigCategories[this.curr.category]} - Item: ${itm}`);
|
|
911
|
+
this.markProgress();
|
|
858
912
|
// setTimeout(() => { conn.queueSendMessage(out) }, 50);
|
|
859
913
|
out.sendAsync()
|
|
860
914
|
.then(() => {
|
|
@@ -872,6 +926,7 @@ class IntelliCenterConfigQueue extends ConfigQueue {
|
|
|
872
926
|
state.status = 1;
|
|
873
927
|
this.curr = null;
|
|
874
928
|
this._processing = false;
|
|
929
|
+
this.stopWatchdog();
|
|
875
930
|
if (this._failed) setTimeout(() => { sys.checkConfiguration(); }, 100);
|
|
876
931
|
logger.info(`Configuration Complete`);
|
|
877
932
|
sys.board.heaters.updateHeaterServices();
|
|
@@ -903,14 +958,34 @@ class IntelliCenterConfigQueue extends ConfigQueue {
|
|
|
903
958
|
console.log('WE ARE ALREADY PROCESSING CHANGES...')
|
|
904
959
|
return;
|
|
905
960
|
}
|
|
961
|
+
// IMPORTANT: Only enter "processing" mode if there are actual version changes.
|
|
962
|
+
// If we set `_processing=true` and then return early, the UI can get stuck showing a partial
|
|
963
|
+
// percent (e.g., 87%) because no further progress/completion events will be emitted.
|
|
964
|
+
if (!curr.hasChanges(ver)) {
|
|
965
|
+
// Ensure controller status returns to ready and queue state is not wedged.
|
|
966
|
+
this._processing = false;
|
|
967
|
+
this._failed = false;
|
|
968
|
+
this._newRequest = false;
|
|
969
|
+
this.stopWatchdog();
|
|
970
|
+
state.status = 1;
|
|
971
|
+
state.emitControllerChange();
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// New run: reset per-run accounting so percent reflects ONLY this run.
|
|
976
|
+
// Do NOT call `ConfigQueue.reset()` here because it also mutates `closed`.
|
|
977
|
+
// We only want to reset per-run counters/queues.
|
|
978
|
+
this.queue.length = 0;
|
|
979
|
+
this.curr = null;
|
|
980
|
+
this.totalItems = 0;
|
|
906
981
|
this._processing = true;
|
|
907
982
|
this._failed = false;
|
|
983
|
+
this.markProgress();
|
|
908
984
|
let self = this;
|
|
909
|
-
if (!curr.hasChanges(ver)) return;
|
|
910
985
|
sys.configVersion.lastUpdated = new Date();
|
|
911
986
|
// Tell the system we are loading.
|
|
912
987
|
state.status = sys.board.valueMaps.controllerStatus.transform(2, 0);
|
|
913
|
-
this.maybeQueueItems(curr.equipment, ver.equipment, ConfigCategories.equipment, [0, 1, 2, 3]);
|
|
988
|
+
this.maybeQueueItems(curr.equipment, ver.equipment, ConfigCategories.equipment, [0, 1, 2, 3, 12, 13, 14, 15]);
|
|
914
989
|
this.maybeQueueItems(curr.options, ver.options, ConfigCategories.options, [0, 1]);
|
|
915
990
|
if (this.compareVersions(curr.circuits, ver.circuits)) {
|
|
916
991
|
let req = new IntelliCenterConfigRequest(ConfigCategories.circuits, ver.circuits, [0, 1, 2],
|
|
@@ -1028,6 +1103,7 @@ class IntelliCenterConfigQueue extends ConfigQueue {
|
|
|
1028
1103
|
if (this.remainingItems > 0) setTimeout(() => { self.processNext(); }, 50);
|
|
1029
1104
|
else {
|
|
1030
1105
|
this._processing = false;
|
|
1106
|
+
this.stopWatchdog();
|
|
1031
1107
|
if (this._newRequest) {
|
|
1032
1108
|
this._newRequest = false;
|
|
1033
1109
|
setTimeout(() => { sys.board.checkConfiguration(); }, 250);
|
|
@@ -1112,6 +1188,21 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1112
1188
|
}
|
|
1113
1189
|
public async setOptionsAsync(obj?: any): Promise<Options> {
|
|
1114
1190
|
let fnToByte = function (num) { return num < 0 ? Math.abs(num) | 0x80 : Math.abs(num) || 0; }
|
|
1191
|
+
const isIntellicenterV3 = (sys.controllerType === ControllerType.IntelliCenter && sys.equipment.isIntellicenterV3);
|
|
1192
|
+
const encodeFreezeOverride = (minutes: number): number => {
|
|
1193
|
+
if (isNaN(minutes)) return 0;
|
|
1194
|
+
// v3.008 appears to encode Frz Override as compact steps: 30 + (raw * 60).
|
|
1195
|
+
if (minutes <= 30) return 0;
|
|
1196
|
+
return Math.max(0, Math.min(3, Math.round((minutes - 30) / 60)));
|
|
1197
|
+
};
|
|
1198
|
+
const freezeCycleTime = parseInt((sys.general.options.freezeCycleTime || 15).toString(), 10) || 15;
|
|
1199
|
+
const freezeOverrideRaw = encodeFreezeOverride(parseInt((sys.general.options.freezeOverride || 30).toString(), 10) || 30);
|
|
1200
|
+
const pool = sys.bodies.getItemById(1, false);
|
|
1201
|
+
const spa = sys.bodies.getItemById(2, false);
|
|
1202
|
+
const manualPriorityPayloadIndex = isIntellicenterV3 ? 28 : 39;
|
|
1203
|
+
const manualHeatPayloadIndex = isIntellicenterV3 ? 31 : 40;
|
|
1204
|
+
const pumpDelayPayloadIndex = isIntellicenterV3 ? 29 : 30;
|
|
1205
|
+
const cooldownDelayPayloadIndex = isIntellicenterV3 ? 30 : 31;
|
|
1115
1206
|
|
|
1116
1207
|
let payload = [0, 0, 0,
|
|
1117
1208
|
fnToByte(sys.equipment.tempSensors.getCalibration('water2')),
|
|
@@ -1130,20 +1221,36 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1130
1221
|
0, 0,
|
|
1131
1222
|
sys.general.options.clockSource === 'internet' ? 1 : 0, // 17
|
|
1132
1223
|
3, 0, 0,
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1224
|
+
// For v3.008+, Action 168 full options blocks place pool/spa setpoints at [20..23]
|
|
1225
|
+
// and modes at [24..25]. Keep legacy layout for pre-v3 controllers.
|
|
1226
|
+
...(isIntellicenterV3
|
|
1227
|
+
? [
|
|
1228
|
+
pool.setPoint || 100, pool.coolSetpoint || (pool.setPoint || 100),
|
|
1229
|
+
spa.setPoint || 100, spa.coolSetpoint || (spa.setPoint || 100),
|
|
1230
|
+
pool.heatMode || 0, spa.heatMode || 0,
|
|
1231
|
+
freezeCycleTime, freezeOverrideRaw,
|
|
1232
|
+
sys.general.options.manualPriority ? 1 : 0,
|
|
1233
|
+
sys.general.options.pumpDelay ? 1 : 0,
|
|
1234
|
+
sys.general.options.cooldownDelay ? 1 : 0,
|
|
1235
|
+
sys.general.options.manualHeat ? 1 : 0,
|
|
1236
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0
|
|
1237
|
+
]
|
|
1238
|
+
: [
|
|
1239
|
+
sys.bodies.getItemById(1, false).setPoint || 100, // 21
|
|
1240
|
+
sys.bodies.getItemById(3, false).setPoint || 100,
|
|
1241
|
+
sys.bodies.getItemById(2, false).setPoint || 100,
|
|
1242
|
+
sys.bodies.getItemById(4, false).setPoint || 100,
|
|
1243
|
+
sys.bodies.getItemById(1, false).heatMode || 0,
|
|
1244
|
+
sys.bodies.getItemById(2, false).heatMode || 0,
|
|
1245
|
+
sys.bodies.getItemById(3, false).heatMode || 0,
|
|
1246
|
+
sys.bodies.getItemById(4, false).heatMode || 0,
|
|
1247
|
+
15,
|
|
1248
|
+
sys.general.options.pumpDelay ? 1 : 0, // 30
|
|
1249
|
+
sys.general.options.cooldownDelay ? 1 : 0,
|
|
1250
|
+
0, 0, 100, 0, 0, 0, 0,
|
|
1251
|
+
sys.general.options.manualPriority ? 1 : 0, // 39
|
|
1252
|
+
sys.general.options.manualHeat ? 1 : 0
|
|
1253
|
+
])];
|
|
1147
1254
|
let arr = [];
|
|
1148
1255
|
try {
|
|
1149
1256
|
if (typeof obj.waterTempAdj1 != 'undefined' && obj.waterTempAdj1 !== sys.equipment.tempSensors.getCalibration('water1')) {
|
|
@@ -1257,12 +1364,13 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1257
1364
|
}
|
|
1258
1365
|
if ((typeof obj.clockMode !== 'undefined' && obj.clockMode !== sys.general.options.clockMode) ||
|
|
1259
1366
|
(typeof obj.adjustDST !== 'undefined' && obj.adjustDST !== sys.general.options.adjustDST)) {
|
|
1260
|
-
|
|
1367
|
+
const effectiveClockSource = (typeof obj.clockSource === 'string') ? obj.clockSource : sys.general.options.clockSource;
|
|
1368
|
+
let byte = 0x10 | (effectiveClockSource === 'internet' ? 0x20 : 0x00);
|
|
1261
1369
|
if (typeof obj.clockMode === 'undefined') byte |= sys.general.options.clockMode === 24 ? 0x40 : 0x00;
|
|
1262
1370
|
else byte |= obj.clockMode === 24 ? 0x40 : 0x00;
|
|
1263
1371
|
if (typeof obj.adjustDST === 'undefined') byte |= sys.general.options.adjustDST ? 0x80 : 0x00;
|
|
1264
1372
|
else byte |= obj.adjustDST ? 0x80 : 0x00;
|
|
1265
|
-
payload[2] = 11;
|
|
1373
|
+
payload[2] = isIntellicenterV3 ? 0 : 11;
|
|
1266
1374
|
payload[14] = byte;
|
|
1267
1375
|
let out = Outbound.create({
|
|
1268
1376
|
action: 168,
|
|
@@ -1272,11 +1380,11 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1272
1380
|
});
|
|
1273
1381
|
await out.sendAsync();
|
|
1274
1382
|
if (typeof obj.clockMode !== 'undefined') sys.general.options.clockMode = obj.clockMode === 24 ? 24 : 12;
|
|
1275
|
-
if (typeof obj.adjustDST !== 'undefined'
|
|
1383
|
+
if (typeof obj.adjustDST !== 'undefined') sys.general.options.adjustDST = obj.adjustDST ? true : false;
|
|
1276
1384
|
}
|
|
1277
1385
|
|
|
1278
1386
|
if (typeof obj.clockSource != 'undefined' && obj.clockSource !== sys.general.options.clockSource) {
|
|
1279
|
-
payload[2] = 11;
|
|
1387
|
+
payload[2] = isIntellicenterV3 ? 0 : 11;
|
|
1280
1388
|
payload[17] = obj.clockSource === 'internet' ? 0x01 : 0x00;
|
|
1281
1389
|
let out = Outbound.create({
|
|
1282
1390
|
action: 168,
|
|
@@ -1289,9 +1397,41 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1289
1397
|
sys.general.options.clockSource = obj.clockSource;
|
|
1290
1398
|
sys.board.system.setTZ();
|
|
1291
1399
|
}
|
|
1400
|
+
if (isIntellicenterV3) {
|
|
1401
|
+
const requestedFreezeCycleTime = typeof obj.freezeCycleTime !== 'undefined'
|
|
1402
|
+
? parseInt(obj.freezeCycleTime, 10)
|
|
1403
|
+
: (typeof obj.frzCycleTime !== 'undefined' ? parseInt(obj.frzCycleTime, 10) : undefined);
|
|
1404
|
+
if (typeof requestedFreezeCycleTime !== 'undefined' && !isNaN(requestedFreezeCycleTime) && requestedFreezeCycleTime !== sys.general.options.freezeCycleTime) {
|
|
1405
|
+
payload[2] = isIntellicenterV3 ? 0 : 26;
|
|
1406
|
+
payload[26] = Math.max(0, Math.min(255, requestedFreezeCycleTime));
|
|
1407
|
+
let out = Outbound.create({
|
|
1408
|
+
action: 168,
|
|
1409
|
+
retries: 5,
|
|
1410
|
+
payload: payload,
|
|
1411
|
+
response: IntelliCenterBoard.getAckResponse(168)
|
|
1412
|
+
});
|
|
1413
|
+
await out.sendAsync();
|
|
1414
|
+
sys.general.options.freezeCycleTime = payload[26];
|
|
1415
|
+
}
|
|
1416
|
+
const requestedFreezeOverride = typeof obj.freezeOverride !== 'undefined'
|
|
1417
|
+
? parseInt(obj.freezeOverride, 10)
|
|
1418
|
+
: (typeof obj.frzOverride !== 'undefined' ? parseInt(obj.frzOverride, 10) : undefined);
|
|
1419
|
+
if (typeof requestedFreezeOverride !== 'undefined' && !isNaN(requestedFreezeOverride) && requestedFreezeOverride !== sys.general.options.freezeOverride) {
|
|
1420
|
+
payload[2] = isIntellicenterV3 ? 0 : 27;
|
|
1421
|
+
payload[27] = encodeFreezeOverride(requestedFreezeOverride);
|
|
1422
|
+
let out = Outbound.create({
|
|
1423
|
+
action: 168,
|
|
1424
|
+
retries: 5,
|
|
1425
|
+
payload: payload,
|
|
1426
|
+
response: IntelliCenterBoard.getAckResponse(168)
|
|
1427
|
+
});
|
|
1428
|
+
await out.sendAsync();
|
|
1429
|
+
sys.general.options.freezeOverride = requestedFreezeOverride;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1292
1432
|
if (typeof obj.pumpDelay !== 'undefined' && obj.pumpDelay !== sys.general.options.pumpDelay) {
|
|
1293
|
-
payload[2] = 27;
|
|
1294
|
-
payload[
|
|
1433
|
+
payload[2] = isIntellicenterV3 ? 0 : 27;
|
|
1434
|
+
payload[pumpDelayPayloadIndex] = obj.pumpDelay ? 0x01 : 0x00;
|
|
1295
1435
|
let out = Outbound.create({
|
|
1296
1436
|
action: 168,
|
|
1297
1437
|
retries: 5,
|
|
@@ -1302,8 +1442,8 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1302
1442
|
sys.general.options.pumpDelay = obj.pumpDelay ? true : false;
|
|
1303
1443
|
}
|
|
1304
1444
|
if (typeof obj.cooldownDelay !== 'undefined' && obj.cooldownDelay !== sys.general.options.cooldownDelay) {
|
|
1305
|
-
payload[2] = 28;
|
|
1306
|
-
payload[
|
|
1445
|
+
payload[2] = isIntellicenterV3 ? 0 : 28;
|
|
1446
|
+
payload[cooldownDelayPayloadIndex] = obj.cooldownDelay ? 0x01 : 0x00;
|
|
1307
1447
|
let out = Outbound.create({
|
|
1308
1448
|
action: 168,
|
|
1309
1449
|
retries: 5,
|
|
@@ -1314,8 +1454,8 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1314
1454
|
sys.general.options.cooldownDelay = obj.cooldownDelay ? true : false;
|
|
1315
1455
|
}
|
|
1316
1456
|
if (typeof obj.manualPriority !== 'undefined' && obj.manualPriority !== sys.general.options.manualPriority) {
|
|
1317
|
-
payload[2] = 36;
|
|
1318
|
-
payload[
|
|
1457
|
+
payload[2] = isIntellicenterV3 ? 0 : 36;
|
|
1458
|
+
payload[manualPriorityPayloadIndex] = obj.manualPriority ? 0x01 : 0x00;
|
|
1319
1459
|
let out = Outbound.create({
|
|
1320
1460
|
action: 168,
|
|
1321
1461
|
retries: 5,
|
|
@@ -1326,8 +1466,8 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1326
1466
|
sys.general.options.manualPriority = obj.manualPriority ? true : false;
|
|
1327
1467
|
}
|
|
1328
1468
|
if (typeof obj.manualHeat !== 'undefined' && obj.manualHeat !== sys.general.options.manualHeat) {
|
|
1329
|
-
payload[2] = 37;
|
|
1330
|
-
payload[
|
|
1469
|
+
payload[2] = isIntellicenterV3 ? 0 : 37;
|
|
1470
|
+
payload[manualHeatPayloadIndex] = obj.manualHeat ? 0x01 : 0x00;
|
|
1331
1471
|
let out = Outbound.create({
|
|
1332
1472
|
action: 168,
|
|
1333
1473
|
retries: 5,
|
|
@@ -1406,12 +1546,12 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1406
1546
|
action: 168,
|
|
1407
1547
|
retries: 5,
|
|
1408
1548
|
payload: [12, 0, 11,
|
|
1409
|
-
|
|
1410
|
-
|
|
1549
|
+
lat % 256,
|
|
1550
|
+
Math.floor(lat / 256)],
|
|
1411
1551
|
response: IntelliCenterBoard.getAckResponse(168)
|
|
1412
1552
|
});
|
|
1413
1553
|
await out.sendAsync();
|
|
1414
|
-
sys.general.location.
|
|
1554
|
+
sys.general.location.latitude = Math.round(obj.latitude * 100) / 100;
|
|
1415
1555
|
}
|
|
1416
1556
|
if (typeof obj.longitude === 'number' && obj.longitude !== sys.general.location.longitude) {
|
|
1417
1557
|
let lon = Math.round(Math.abs(obj.longitude) * 100);
|
|
@@ -1419,12 +1559,12 @@ class IntelliCenterSystemCommands extends SystemCommands {
|
|
|
1419
1559
|
action: 168,
|
|
1420
1560
|
retries: 5,
|
|
1421
1561
|
payload: [12, 0, 12,
|
|
1422
|
-
|
|
1423
|
-
|
|
1562
|
+
lon % 256,
|
|
1563
|
+
Math.floor(lon / 256)],
|
|
1424
1564
|
response: IntelliCenterBoard.getAckResponse(168)
|
|
1425
1565
|
});
|
|
1426
1566
|
await out.sendAsync();
|
|
1427
|
-
sys.general.location.longitude =
|
|
1567
|
+
sys.general.location.longitude = Math.round(obj.longitude * 100) / 100;
|
|
1428
1568
|
}
|
|
1429
1569
|
if (typeof obj.timeZone === 'number' && obj.timeZone !== sys.general.location.timeZone) {
|
|
1430
1570
|
let out = Outbound.create({
|
|
@@ -1510,47 +1650,6 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
1510
1650
|
// Key: circuit/feature ID, Value: intended state (true=on, false=off)
|
|
1511
1651
|
private pendingStates: Map<number, boolean> = new Map();
|
|
1512
1652
|
|
|
1513
|
-
private findActiveTargetIdOwners(targetId: number, excludeCircuitId?: number): ICircuit[] {
|
|
1514
|
-
const owners: ICircuit[] = [];
|
|
1515
|
-
if (typeof targetId !== 'number' || targetId <= 0) return owners;
|
|
1516
|
-
for (let i = 0; i < sys.circuits.length; i++) {
|
|
1517
|
-
const circ = sys.circuits.getItemByIndex(i);
|
|
1518
|
-
if (!circ || !circ.isActive) continue;
|
|
1519
|
-
if (typeof excludeCircuitId === 'number' && circ.id === excludeCircuitId) continue;
|
|
1520
|
-
// targetId may be undefined if not learned yet
|
|
1521
|
-
if (typeof (circ as any).targetId === 'number' && (circ as any).targetId === targetId) owners.push(circ);
|
|
1522
|
-
}
|
|
1523
|
-
return owners;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
public seedKnownV3TargetIds(): void {
|
|
1527
|
-
if (!sys.equipment.isIntellicenterV3) return;
|
|
1528
|
-
// Known fixed body circuits on IntelliCenter:
|
|
1529
|
-
// - Circuit ID 1 = Spa
|
|
1530
|
-
// - Circuit ID 6 = Pool
|
|
1531
|
-
//
|
|
1532
|
-
// We seed these as defaults ONLY when missing, and we never overwrite an existing learned value.
|
|
1533
|
-
// This also has a safety benefit: it prevents other circuits from accidentally learning/reusing
|
|
1534
|
-
// the Spa/Pool targetIds.
|
|
1535
|
-
// Additional observed mapping (NOT guaranteed across all installations):
|
|
1536
|
-
// - Circuit ID 2 targetId observed as 0xC490 in captures. We seed it as a best-effort default
|
|
1537
|
-
// for users without a Wireless/indoor panel, but it will be overridden when we learn the real
|
|
1538
|
-
// mapping from the bus (and cleared quickly if readback indicates it’s wrong).
|
|
1539
|
-
const seeds: Array<{ circuitId: number, targetId: number }> = [
|
|
1540
|
-
{ circuitId: 1, targetId: 0xA8ED }, // 168,237
|
|
1541
|
-
{ circuitId: 6, targetId: 0x6CE1 }, // 108,225
|
|
1542
|
-
{ circuitId: 2, targetId: 0xC490 } // 196,144
|
|
1543
|
-
];
|
|
1544
|
-
for (const s of seeds) {
|
|
1545
|
-
const circ = sys.circuits.getItemById(s.circuitId, false, { isActive: false });
|
|
1546
|
-
if (!circ || circ.isActive === false) continue;
|
|
1547
|
-
if (typeof (circ as any).targetId === 'number' && (circ as any).targetId > 0) continue;
|
|
1548
|
-
const owners = this.findActiveTargetIdOwners(s.targetId, s.circuitId);
|
|
1549
|
-
if (owners.length > 0) continue; // don't introduce duplicates
|
|
1550
|
-
(circ as any).targetId = s.targetId;
|
|
1551
|
-
logger.debug(`v3.004+ seedKnownV3TargetIds: Seeded circuitId=${s.circuitId} (index=${s.circuitId - 1}) with targetId=${s.targetId}`);
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
1653
|
|
|
1555
1654
|
// Add a pending state change (called before sending command)
|
|
1556
1655
|
public addPendingState(id: number, isOn: boolean): void {
|
|
@@ -1600,6 +1699,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
1600
1699
|
let eggHrs = Math.floor(eggTimer / 60);
|
|
1601
1700
|
let eggMins = eggTimer - (eggHrs * 60);
|
|
1602
1701
|
let type = typeof data.type !== 'undefined' ? parseInt(data.type, 10) : circuit.type;
|
|
1702
|
+
this.assertSinglePoolSpaType(id, type);
|
|
1603
1703
|
let theme = typeof data.lightingTheme !== 'undefined' ? data.lightingTheme : circuit.lightingTheme;
|
|
1604
1704
|
if (circuit.type === 9) theme = typeof data.level !== 'undefined' ? data.level : circuit.level;
|
|
1605
1705
|
if (typeof theme === 'undefined') theme = 0;
|
|
@@ -2200,6 +2300,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
2200
2300
|
let out = Outbound.create({
|
|
2201
2301
|
dest,
|
|
2202
2302
|
action: 222,
|
|
2303
|
+
scope: sys.equipment.isIntellicenterV3 ? 'v3CommandReadback' : undefined,
|
|
2203
2304
|
retries: 3,
|
|
2204
2305
|
payload: payload,
|
|
2205
2306
|
response: Response.create({ dest: -1, action: 30, payload: payload })
|
|
@@ -2210,6 +2311,12 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
2210
2311
|
|
|
2211
2312
|
}
|
|
2212
2313
|
public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
|
|
2314
|
+
// v3.004+ features: dashPanel (and other callers) may route feature toggles through the "circuit" path.
|
|
2315
|
+
// IntelliCenter features live in a different Action 184 channel (0xE89D), so delegate feature IDs here.
|
|
2316
|
+
if (sys.equipment.isIntellicenterV3 && sys.board.equipmentIds.features.isInRange(id)) {
|
|
2317
|
+
logger.info(`v3.004+ setCircuitStateAsync: ID ${id} is a feature; delegating to setFeatureStateAsync -> ${val ? 'ON' : 'OFF'}`);
|
|
2318
|
+
return await this.board.features.setFeatureStateAsync(id, val, ignoreDelays);
|
|
2319
|
+
}
|
|
2213
2320
|
let c = sys.circuits.getInterfaceById(id);
|
|
2214
2321
|
if (c.master !== 0) return await super.setCircuitStateAsync(id, val);
|
|
2215
2322
|
// As of 1.047 there is a sequence to this.
|
|
@@ -2256,42 +2363,41 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
2256
2363
|
return circ;
|
|
2257
2364
|
}
|
|
2258
2365
|
|
|
2259
|
-
// v3.004
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
}
|
|
2366
|
+
// v3.004+ non-body circuits: Use indexed Action 184 (Wireless-style)
|
|
2367
|
+
if (sys.equipment.isIntellicenterV3) {
|
|
2368
|
+
const circuit = sys.circuits.getItemById(id, false);
|
|
2369
|
+
logger.info(`v3.004+ setCircuitStateAsync: Using indexed Action 184 for circuit ${id} (${circuit?.name || 'unnamed'}) -> ${val ? 'ON' : 'OFF'}`);
|
|
2370
|
+
/**
|
|
2371
|
+
* v3.004+ Indexed Circuit Control (Wireless-style).
|
|
2372
|
+
* Action 184 is the native circuit control message used by the Wireless remote.
|
|
2373
|
+
*
|
|
2374
|
+
* Payload structure (10 bytes):
|
|
2375
|
+
* Bytes 0-1: Channel (0x688F for circuits)
|
|
2376
|
+
* Byte 2: Index (circuitId - 1 or featureId - 1)
|
|
2377
|
+
* Byte 3: Format (255 = command mode)
|
|
2378
|
+
* Bytes 4-5: Target (0xA8ED = control primitive)
|
|
2379
|
+
* Byte 6: State (0=OFF, 1=ON)
|
|
2380
|
+
* Bytes 7-9: Reserved (0,0,0)
|
|
2381
|
+
*/
|
|
2382
|
+
const idx = Math.max(0, Math.min(255, (id | 0) - 1));
|
|
2383
|
+
const out = Outbound.createMessage(184, [
|
|
2384
|
+
104, 143, // Channel 0x688F (circuits)
|
|
2385
|
+
idx, // Index (circuitId - 1)
|
|
2386
|
+
255, // Format (command)
|
|
2387
|
+
168, 237, // Target 0xA8ED (control primitive)
|
|
2388
|
+
val ? 1 : 0, // State
|
|
2389
|
+
0, 0, 0
|
|
2390
|
+
], 3);
|
|
2391
|
+
out.dest = 16; // Send to OCP
|
|
2392
|
+
out.scope = `circuitState${id}`;
|
|
2393
|
+
out.retries = 5;
|
|
2394
|
+
out.response = IntelliCenterBoard.getAckResponse(184);
|
|
2395
|
+
await out.sendAsync();
|
|
2396
|
+
// Request updated config to confirm state change
|
|
2397
|
+
await this.getConfigAsync([15, 0]);
|
|
2398
|
+
let circ = state.circuits.getInterfaceById(id);
|
|
2399
|
+
state.emitEquipmentChanges();
|
|
2400
|
+
return circ;
|
|
2295
2401
|
}
|
|
2296
2402
|
|
|
2297
2403
|
// v1.x or v3.004+ without known targetId: Use Action 168 (original method)
|
|
@@ -2672,37 +2778,6 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
2672
2778
|
}
|
|
2673
2779
|
return out;
|
|
2674
2780
|
}
|
|
2675
|
-
/**
|
|
2676
|
-
* Creates an Action 184 message for v3.004+ IntelliCenter circuit control.
|
|
2677
|
-
* Action 184 is the native circuit control message used by the Wireless remote.
|
|
2678
|
-
*
|
|
2679
|
-
* Payload structure (10 bytes):
|
|
2680
|
-
* Bytes 0-1: Channel ID (104,143 = 0x688F = default channel)
|
|
2681
|
-
* Byte 2: Sequence number (0)
|
|
2682
|
-
* Byte 3: Format (255 = command mode)
|
|
2683
|
-
* Bytes 4-5: Target ID (circuit's unique identifier, learned from OCP broadcasts)
|
|
2684
|
-
* Byte 6: State (0=OFF, 1=ON)
|
|
2685
|
-
* Bytes 7-9: Reserved (0,0,0)
|
|
2686
|
-
*
|
|
2687
|
-
* @param targetId The circuit's unique Target ID (hi*256 + lo)
|
|
2688
|
-
* @param isOn True to turn circuit ON, false for OFF
|
|
2689
|
-
* @returns Outbound message ready to send
|
|
2690
|
-
*/
|
|
2691
|
-
public createAction184Message(targetId: number, isOn: boolean): Outbound {
|
|
2692
|
-
const targetIdHi = Math.floor(targetId / 256);
|
|
2693
|
-
const targetIdLo = targetId % 256;
|
|
2694
|
-
// Default channel 104,143 (0x688F), seq=0, format=255 (command)
|
|
2695
|
-
let out = Outbound.createMessage(184, [
|
|
2696
|
-
104, 143, // Channel ID (default)
|
|
2697
|
-
0, // Sequence number
|
|
2698
|
-
255, // Format (command mode)
|
|
2699
|
-
targetIdHi, // Target ID high byte
|
|
2700
|
-
targetIdLo, // Target ID low byte
|
|
2701
|
-
isOn ? 1 : 0, // State (1=ON, 0=OFF)
|
|
2702
|
-
0, 0, 0 // Reserved
|
|
2703
|
-
], 3);
|
|
2704
|
-
return out;
|
|
2705
|
-
}
|
|
2706
2781
|
|
|
2707
2782
|
public async setDimmerLevelAsync(id: number, level: number): Promise<ICircuitState> {
|
|
2708
2783
|
let circuit = sys.circuits.getItemById(id);
|
|
@@ -2729,14 +2804,84 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
|
|
|
2729
2804
|
catch (err) { return Promise.reject(err); }
|
|
2730
2805
|
}
|
|
2731
2806
|
public async toggleCircuitStateAsync(id: number): Promise<ICircuitState> {
|
|
2807
|
+
// v3.004+ features: dashPanel may attempt to toggle features via the circuit endpoint.
|
|
2808
|
+
if (sys.equipment.isIntellicenterV3 && sys.board.equipmentIds.features.isInRange(id)) {
|
|
2809
|
+
return await this.board.features.toggleFeatureStateAsync(id);
|
|
2810
|
+
}
|
|
2732
2811
|
let circ = state.circuits.getInterfaceById(id);
|
|
2733
2812
|
return sys.board.circuits.setCircuitStateAsync(id, !circ.isOn);
|
|
2734
2813
|
}
|
|
2735
2814
|
}
|
|
2736
2815
|
class IntelliCenterFeatureCommands extends FeatureCommands {
|
|
2737
2816
|
declare board: IntelliCenterBoard;
|
|
2738
|
-
|
|
2739
|
-
|
|
2817
|
+
|
|
2818
|
+
private async getConfigAsync(payload: number[]): Promise<boolean> {
|
|
2819
|
+
const dest = sys.equipment.isIntellicenterV3 ? 16 : 15;
|
|
2820
|
+
let out = Outbound.create({
|
|
2821
|
+
dest,
|
|
2822
|
+
action: 222,
|
|
2823
|
+
scope: sys.equipment.isIntellicenterV3 ? 'v3CommandReadback' : undefined,
|
|
2824
|
+
retries: 3,
|
|
2825
|
+
payload: payload,
|
|
2826
|
+
response: Response.create({ dest: -1, action: 30, payload: payload })
|
|
2827
|
+
});
|
|
2828
|
+
await out.sendAsync();
|
|
2829
|
+
// Do NOT ACK(30). Wireless captures show config succeeds without ACK(30), and v1 queue avoids ACK(30).
|
|
2830
|
+
return true;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
public async setFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
|
|
2834
|
+
// v3.004+: Features are controlled via Action 184 channel 0xE89D (232,157), not the circuits channel.
|
|
2835
|
+
if (sys.equipment.isIntellicenterV3) {
|
|
2836
|
+
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
|
|
2837
|
+
if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
|
|
2838
|
+
|
|
2839
|
+
const feature = sys.features.getItemById(id, false, { isActive: false });
|
|
2840
|
+
logger.info(`v3.004+ setFeatureStateAsync: Using indexed Action 184 for feature ${id} (${feature?.name || 'unnamed'}) -> ${val ? 'ON' : 'OFF'}`);
|
|
2841
|
+
|
|
2842
|
+
/**
|
|
2843
|
+
* v3.004+ Indexed Feature Control (Wireless-style).
|
|
2844
|
+
* Action 184 is the native feature control message used by the Wireless remote (channel 0xE89D).
|
|
2845
|
+
*
|
|
2846
|
+
* Payload structure (10 bytes):
|
|
2847
|
+
* Bytes 0-1: Channel (0xE89D for features)
|
|
2848
|
+
* Byte 2: Index (featureId - 1)
|
|
2849
|
+
* Byte 3: Format/mode (observed 0 in replays 132/138 feature toggles)
|
|
2850
|
+
* Bytes 4-5: Target (0xA8ED = control primitive)
|
|
2851
|
+
* Byte 6: State (0=OFF, 1=ON)
|
|
2852
|
+
* Bytes 7-9: Reserved (0,0,0)
|
|
2853
|
+
*/
|
|
2854
|
+
const idx = Math.max(0, Math.min(255, (id | 0) - 1));
|
|
2855
|
+
const out = Outbound.createMessage(184, [
|
|
2856
|
+
232, 157, // Channel 0xE89D (features)
|
|
2857
|
+
idx, // Index (featureId - 1)
|
|
2858
|
+
0, // Format/mode (observed)
|
|
2859
|
+
168, 237, // Target 0xA8ED (control primitive)
|
|
2860
|
+
val ? 1 : 0, // State
|
|
2861
|
+
0, 0, 0
|
|
2862
|
+
], 3);
|
|
2863
|
+
out.dest = 16; // Send to OCP
|
|
2864
|
+
out.scope = `featureState${id}`;
|
|
2865
|
+
out.retries = 5;
|
|
2866
|
+
out.response = IntelliCenterBoard.getAckResponse(184);
|
|
2867
|
+
await out.sendAsync();
|
|
2868
|
+
|
|
2869
|
+
// Request updated system state to confirm feature change (authoritative source for v3 features).
|
|
2870
|
+
await this.getConfigAsync([15, 0]);
|
|
2871
|
+
|
|
2872
|
+
const fstate = state.features.getItemById(id, true);
|
|
2873
|
+
state.emitEquipmentChanges();
|
|
2874
|
+
return fstate;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
// Legacy behavior (v1.x): delegate to circuit state setter.
|
|
2878
|
+
return sys.board.circuits.setCircuitStateAsync(id, val);
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
public async toggleFeatureStateAsync(id: number): Promise<ICircuitState> {
|
|
2882
|
+
const feat = state.features.getItemById(id);
|
|
2883
|
+
return this.setFeatureStateAsync(id, !(feat.isOn || false));
|
|
2884
|
+
}
|
|
2740
2885
|
public syncGroupStates() { } // Do nothing and let IntelliCenter do it.
|
|
2741
2886
|
public async setFeatureAsync(data: any): Promise<Feature> {
|
|
2742
2887
|
|
|
@@ -3010,42 +3155,64 @@ class IntelliCenterPumpCommands extends PumpCommands {
|
|
|
3010
3155
|
// supplied then we will use what we already have. This will make sure the information is valid and any change can be applied without the complete
|
|
3011
3156
|
// definition of the pump. This is important since additional attributes may be added in the future and this keeps us current no matter what
|
|
3012
3157
|
// the endpoint capability is.
|
|
3013
|
-
|
|
3158
|
+
const isV3 = sys.equipment.isIntellicenterV3;
|
|
3159
|
+
const dest = isV3 ? 16 : 15;
|
|
3160
|
+
let outc = Outbound.create({ dest, action: 168, payload: [4, 0, id - 1, ntype, 0] });
|
|
3014
3161
|
outc.appendPayloadByte(parseInt(data.address, 10), id + 95); // 5
|
|
3015
|
-
|
|
3016
|
-
|
|
3162
|
+
// v3.004+ uses big-endian for 16-bit speed/flow values
|
|
3163
|
+
if (isV3) {
|
|
3164
|
+
outc.appendPayloadIntBE(parseInt(data.minSpeed, 10), pump.minSpeed); // 6
|
|
3165
|
+
outc.appendPayloadIntBE(parseInt(data.maxSpeed, 10), pump.maxSpeed); // 8
|
|
3166
|
+
} else {
|
|
3167
|
+
outc.appendPayloadInt(parseInt(data.minSpeed, 10), pump.minSpeed); // 6
|
|
3168
|
+
outc.appendPayloadInt(parseInt(data.maxSpeed, 10), pump.maxSpeed); // 8
|
|
3169
|
+
}
|
|
3017
3170
|
outc.appendPayloadByte(parseInt(data.minFlow, 10), pump.minFlow); // 10
|
|
3018
3171
|
outc.appendPayloadByte(parseInt(data.maxFlow, 10), pump.maxFlow); // 11
|
|
3019
3172
|
outc.appendPayloadByte(parseInt(data.flowStepSize, 10), pump.flowStepSize || 1); // 12
|
|
3020
|
-
|
|
3173
|
+
if (isV3) {
|
|
3174
|
+
outc.appendPayloadIntBE(parseInt(data.primingSpeed, 10), pump.primingSpeed || 2500); // 13
|
|
3175
|
+
} else {
|
|
3176
|
+
outc.appendPayloadInt(parseInt(data.primingSpeed, 10), pump.primingSpeed || 2500); // 13
|
|
3177
|
+
}
|
|
3021
3178
|
outc.appendPayloadByte(typeof data.speedStepSize !== 'undefined' ? parseInt(data.speedStepSize, 10) / 10 : pump.speedStepSize / 10, 1); // 15
|
|
3022
3179
|
outc.appendPayloadByte(parseInt(data.primingTime, 10), pump.primingTime || 0); // 17
|
|
3023
3180
|
outc.appendPayloadByte(255); //
|
|
3024
3181
|
outc.appendPayloadBytes(255, 8); // 18
|
|
3025
3182
|
outc.appendPayloadBytes(0, 8); // 26
|
|
3026
|
-
let outn = Outbound.create({ action: 168, payload: [4, 1, id - 1] });
|
|
3183
|
+
let outn = Outbound.create({ dest, action: 168, payload: [4, 1, id - 1] });
|
|
3027
3184
|
outn.appendPayloadBytes(0, 16);
|
|
3028
3185
|
outn.appendPayloadString(data.name, 16, pump.name || type.name);
|
|
3029
3186
|
if (type.name === 'ss') {
|
|
3030
3187
|
outc.setPayloadByte(5, 0); // Clear the pump address
|
|
3031
3188
|
|
|
3032
3189
|
// At some point we may add these to the pump model.
|
|
3033
|
-
|
|
3034
|
-
|
|
3190
|
+
// v3.004+ uses big-endian for 16-bit speed/flow values
|
|
3191
|
+
if (isV3) {
|
|
3192
|
+
outc.setPayloadIntBE(6, type.minSpeed, 450);
|
|
3193
|
+
outc.setPayloadIntBE(8, type.maxSpeed, 3450);
|
|
3194
|
+
} else {
|
|
3195
|
+
outc.setPayloadInt(6, type.minSpeed, 450);
|
|
3196
|
+
outc.setPayloadInt(8, type.maxSpeed, 3450);
|
|
3197
|
+
}
|
|
3035
3198
|
outc.setPayloadByte(10, type.minFlow, 0);
|
|
3036
3199
|
outc.setPayloadByte(11, type.maxFlow, 130);
|
|
3037
3200
|
outc.setPayloadByte(12, 1);
|
|
3038
|
-
|
|
3201
|
+
if (isV3) {
|
|
3202
|
+
outc.setPayloadIntBE(13, type.primingSpeed, 2500);
|
|
3203
|
+
} else {
|
|
3204
|
+
outc.setPayloadInt(13, type.primingSpeed, 2500);
|
|
3205
|
+
}
|
|
3039
3206
|
outc.setPayloadByte(15, 10);
|
|
3040
3207
|
outc.setPayloadByte(16, 1);
|
|
3041
3208
|
outc.setPayloadByte(17, 5);
|
|
3042
3209
|
outc.setPayloadByte(18, data.body, pump.body);
|
|
3043
3210
|
outc.setPayloadByte(26, 0);
|
|
3044
|
-
outn.setPayloadInt(3, 0);
|
|
3211
|
+
if (isV3) outn.setPayloadIntBE(3, 0); else outn.setPayloadInt(3, 0);
|
|
3045
3212
|
for (let i = 1; i < 8; i++) {
|
|
3046
3213
|
outc.setPayloadByte(i + 18, 255);
|
|
3047
3214
|
outc.setPayloadByte(i + 26, 0);
|
|
3048
|
-
outn.setPayloadInt((i * 2) + 3, 1000);
|
|
3215
|
+
if (isV3) outn.setPayloadIntBE((i * 2) + 3, 1000); else outn.setPayloadInt((i * 2) + 3, 1000);
|
|
3049
3216
|
}
|
|
3050
3217
|
}
|
|
3051
3218
|
else {
|
|
@@ -3067,13 +3234,13 @@ class IntelliCenterPumpCommands extends PumpCommands {
|
|
|
3067
3234
|
// The incoming data does not include this circuit so we will set it to 255.
|
|
3068
3235
|
outc.setPayloadByte(i + 18, 255);
|
|
3069
3236
|
if (typeof type.minSpeed !== 'undefined')
|
|
3070
|
-
outn.setPayloadInt((i * 2) + 3, type.minSpeed);
|
|
3237
|
+
isV3 ? outn.setPayloadIntBE((i * 2) + 3, type.minSpeed) : outn.setPayloadInt((i * 2) + 3, type.minSpeed);
|
|
3071
3238
|
else if (typeof type.minFlow !== 'undefined') {
|
|
3072
|
-
outn.setPayloadInt((i * 2) + 3, type.minFlow);
|
|
3239
|
+
isV3 ? outn.setPayloadIntBE((i * 2) + 3, type.minFlow) : outn.setPayloadInt((i * 2) + 3, type.minFlow);
|
|
3073
3240
|
outc.setPayloadByte(i + 26, 1);
|
|
3074
3241
|
}
|
|
3075
3242
|
else
|
|
3076
|
-
outn.setPayloadInt((i * 2) + 3, 0);
|
|
3243
|
+
isV3 ? outn.setPayloadIntBE((i * 2) + 3, 0) : outn.setPayloadInt((i * 2) + 3, 0);
|
|
3077
3244
|
}
|
|
3078
3245
|
else {
|
|
3079
3246
|
let c = data.circuits[i];
|
|
@@ -3088,11 +3255,11 @@ class IntelliCenterPumpCommands extends PumpCommands {
|
|
|
3088
3255
|
outc.setPayloadByte(i + 18, circuit - 1, circ.circuit - 1);
|
|
3089
3256
|
if (typeof type.minSpeed !== 'undefined' && (parseInt(c.units, 10) === 0 || isNaN(parseInt(c.units, 10)))) {
|
|
3090
3257
|
outc.setPayloadByte(i + 26, 0); // Set to rpm
|
|
3091
|
-
outn.setPayloadInt((i * 2) + 3, Math.max(speed, type.minSpeed), circ.speed);
|
|
3258
|
+
isV3 ? outn.setPayloadIntBE((i * 2) + 3, Math.max(speed, type.minSpeed), circ.speed) : outn.setPayloadInt((i * 2) + 3, Math.max(speed, type.minSpeed), circ.speed);
|
|
3092
3259
|
}
|
|
3093
3260
|
else if (typeof type.minFlow !== 'undefined' && (parseInt(c.units, 10) === 1 || isNaN(parseInt(c.units, 10)))) {
|
|
3094
3261
|
outc.setPayloadByte(i + 26, 1); // Set to gpm
|
|
3095
|
-
outn.setPayloadInt((i * 2) + 3, Math.max(flow, type.minFlow), circ.flow);
|
|
3262
|
+
isV3 ? outn.setPayloadIntBE((i * 2) + 3, Math.max(flow, type.minFlow), circ.flow) : outn.setPayloadInt((i * 2) + 3, Math.max(flow, type.minFlow), circ.flow);
|
|
3096
3263
|
}
|
|
3097
3264
|
}
|
|
3098
3265
|
}
|
|
@@ -3212,14 +3379,25 @@ class IntelliCenterPumpCommands extends PumpCommands {
|
|
|
3212
3379
|
if (pump.master === 1) return super.deletePumpAsync(data);
|
|
3213
3380
|
|
|
3214
3381
|
if (typeof pump.type === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Pump #${data.id} does not exist in configuration`, data.id, 'Schedule'));
|
|
3382
|
+
const isV3 = sys.equipment.isIntellicenterV3;
|
|
3215
3383
|
let outc = Outbound.create({ action: 168, payload: [4, 0, id - 1, 0, 0, id + 95] });
|
|
3216
|
-
|
|
3217
|
-
|
|
3384
|
+
if (isV3) {
|
|
3385
|
+
outc.appendPayloadIntBE(450); // 6
|
|
3386
|
+
outc.appendPayloadIntBE(3450); // 8
|
|
3387
|
+
} else {
|
|
3388
|
+
outc.appendPayloadInt(450); // 6
|
|
3389
|
+
outc.appendPayloadInt(3450); // 8
|
|
3390
|
+
}
|
|
3218
3391
|
outc.appendPayloadByte(15); // 10
|
|
3219
3392
|
outc.appendPayloadByte(130); // 11
|
|
3220
3393
|
outc.appendPayloadByte(1); // 12
|
|
3221
|
-
|
|
3222
|
-
|
|
3394
|
+
if (isV3) {
|
|
3395
|
+
outc.appendPayloadIntBE(1000); // 13
|
|
3396
|
+
outc.appendPayloadIntBE(10); // 15
|
|
3397
|
+
} else {
|
|
3398
|
+
outc.appendPayloadInt(1000); // 13
|
|
3399
|
+
outc.appendPayloadInt(10); // 15
|
|
3400
|
+
}
|
|
3223
3401
|
outc.appendPayloadByte(5); // 17
|
|
3224
3402
|
outc.appendPayloadBytes(255, 8); // 18
|
|
3225
3403
|
outc.appendPayloadBytes(0, 8); // 26
|
|
@@ -3686,14 +3864,20 @@ class IntelliCenterScheduleCommands extends ScheduleCommands {
|
|
|
3686
3864
|
if (endTimeType !== 0) runOnce |= (1 << (endTimeType + 3));
|
|
3687
3865
|
// This was always the cooling setpoint for ultratemp.
|
|
3688
3866
|
//let flags = (circuit === 1 || circuit === 6) ? 81 : 100;
|
|
3867
|
+
// v3.004+ uses big-endian for 16-bit time values
|
|
3868
|
+
let startTimeLo = startTime - Math.floor(startTime / 256) * 256;
|
|
3869
|
+
let startTimeHi = Math.floor(startTime / 256);
|
|
3870
|
+
let endTimeLo = endTime - Math.floor(endTime / 256) * 256;
|
|
3871
|
+
let endTimeHi = Math.floor(endTime / 256);
|
|
3872
|
+
let isV3 = sys.equipment.isIntellicenterV3;
|
|
3689
3873
|
let out = Outbound.createMessage(168, [
|
|
3690
3874
|
3
|
|
3691
3875
|
, 0
|
|
3692
3876
|
, id - 1 // IntelliCenter schedules start at 0.
|
|
3693
|
-
,
|
|
3694
|
-
,
|
|
3695
|
-
,
|
|
3696
|
-
,
|
|
3877
|
+
, isV3 ? startTimeHi : startTimeLo
|
|
3878
|
+
, isV3 ? startTimeLo : startTimeHi
|
|
3879
|
+
, isV3 ? endTimeHi : endTimeLo
|
|
3880
|
+
, isV3 ? endTimeLo : endTimeHi
|
|
3697
3881
|
, circuit - 1
|
|
3698
3882
|
, runOnce
|
|
3699
3883
|
, schedDays
|
|
@@ -4149,6 +4333,9 @@ export class IntelliCenterChemControllerCommands extends ChemControllerCommands
|
|
|
4149
4333
|
let cyanuricAcid = typeof data.cyanuricAcid !== 'undefined' ? parseInt(data.cyanuricAcid, 10) : chem.cyanuricAcid;
|
|
4150
4334
|
let alkalinity = typeof data.alkalinity !== 'undefined' ? parseInt(data.alkalinity, 10) : chem.alkalinity;
|
|
4151
4335
|
let borates = typeof data.borates !== 'undefined' ? parseInt(data.borates, 10) : chem.borates || 0;
|
|
4336
|
+
let intellichemStandalone = sys.controllerType === ControllerType.Nixie
|
|
4337
|
+
? (typeof data.intellichemStandalone !== 'undefined' ? utils.makeBool(data.intellichemStandalone) : chem.intellichemStandalone)
|
|
4338
|
+
: false;
|
|
4152
4339
|
let body = sys.board.bodies.mapBodyAssociation(typeof data.body === 'undefined' ? chem.body : data.body);
|
|
4153
4340
|
if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid body assignment`, 'chemController', data.body || chem.body));
|
|
4154
4341
|
// Do a final validation pass so we dont send this off in a mess.
|
|
@@ -4237,6 +4424,7 @@ export class IntelliCenterChemControllerCommands extends ChemControllerCommands
|
|
|
4237
4424
|
chem.alkalinity = alkalinity;
|
|
4238
4425
|
chem.borates = borates;
|
|
4239
4426
|
chem.body = schem.body = body;
|
|
4427
|
+
chem.intellichemStandalone = intellichemStandalone;
|
|
4240
4428
|
schem.isActive = chem.isActive = true;
|
|
4241
4429
|
chem.lsiRange.enabled = lsiRange.enabled;
|
|
4242
4430
|
chem.lsiRange.low = lsiRange.low;
|