Skip to content

Commit 690b47c

Browse files
Refactor IntelliCenterBoard and related message handling for v3.004+ support. Update response handling to ensure correct destination addressing for Action 164 and enhance ACK processing for Actions 168 and 184. Streamline circuit control messaging and improve target ID management to prevent conflicts. Remove deprecated code related to pending Wireless commands and clarify logging for better debugging. #1090
1 parent 2032ad7 commit 690b47c

4 files changed

Lines changed: 129 additions & 333 deletions

File tree

controller/boards/IntelliCenterBoard.ts

Lines changed: 55 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,10 @@ export class IntelliCenterBoard extends SystemBoard {
421421
action: 228,
422422
payload: [0],
423423
retries: 3,
424-
response: Response.create({ action: 164 })
424+
// v3.004+: require the version response (164) to be addressed to us (not to Wireless).
425+
response: sys.equipment.isIntellicenterV3
426+
? Response.create({ dest: Message.pluginAddress, action: 164 })
427+
: Response.create({ action: 164 })
425428
});
426429
await verReq.sendAsync();
427430
if (sys.equipment.isIntellicenterV3) {
@@ -851,7 +854,7 @@ class IntelliCenterConfigQueue extends ConfigQueue {
851854
// v3.004+: some config requests can yield an Action 30 with an empty payload (length=0).
852855
// Those packets still indicate "done" for the requested item, but cannot be matched by payload prefix.
853856
response: sys.equipment.isIntellicenterV3
854-
? Response.create({ dest: -1, action: 30 })
857+
? Response.create({ dest: Message.pluginAddress, action: 30 })
855858
: Response.create({ dest: -1, action: 30, payload: [this.curr.category, itm] })
856859
});
857860
logger.verbose(`Requesting config for: ${ConfigCategories[this.curr.category]} - Item: ${itm}`);
@@ -903,10 +906,28 @@ class IntelliCenterConfigQueue extends ConfigQueue {
903906
console.log('WE ARE ALREADY PROCESSING CHANGES...')
904907
return;
905908
}
909+
// IMPORTANT: Only enter "processing" mode if there are actual version changes.
910+
// If we set `_processing=true` and then return early, the UI can get stuck showing a partial
911+
// percent (e.g., 87%) because no further progress/completion events will be emitted.
912+
if (!curr.hasChanges(ver)) {
913+
// Ensure controller status returns to ready and queue state is not wedged.
914+
this._processing = false;
915+
this._failed = false;
916+
this._newRequest = false;
917+
state.status = 1;
918+
state.emitControllerChange();
919+
return;
920+
}
921+
922+
// New run: reset per-run accounting so percent reflects ONLY this run.
923+
// Do NOT call `ConfigQueue.reset()` here because it also mutates `closed`.
924+
// We only want to reset per-run counters/queues.
925+
this.queue.length = 0;
926+
this.curr = null;
927+
this.totalItems = 0;
906928
this._processing = true;
907929
this._failed = false;
908930
let self = this;
909-
if (!curr.hasChanges(ver)) return;
910931
sys.configVersion.lastUpdated = new Date();
911932
// Tell the system we are loading.
912933
state.status = sys.board.valueMaps.controllerStatus.transform(2, 0);
@@ -1510,47 +1531,6 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
15101531
// Key: circuit/feature ID, Value: intended state (true=on, false=off)
15111532
private pendingStates: Map<number, boolean> = new Map();
15121533

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-
}
15541534

15551535
// Add a pending state change (called before sending command)
15561536
public addPendingState(id: number, isOn: boolean): void {
@@ -2256,42 +2236,23 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
22562236
return circ;
22572237
}
22582238

2259-
// v3.004+: Use Action 184 if we have a learned targetId for this circuit.
2260-
// Action 184 is the native circuit control message that the Wireless remote uses.
2261-
// OCP accepts this format and doesn't revert the state like it does with Action 168.
2262-
const circuit = sys.circuits.getItemById(id, false);
2263-
if (sys.equipment.isIntellicenterV3 && circuit && typeof circuit.targetId === 'number' && circuit.targetId > 0) {
2264-
// Safety: A targetId must be unique per circuit. If duplicates exist, Action 184 could toggle the wrong circuit.
2265-
const dupOwners = this.findActiveTargetIdOwners(circuit.targetId, id);
2266-
if (dupOwners.length > 0) {
2267-
logger.error(
2268-
`v3.004+ setCircuitStateAsync: Circuit ${id} (${circuit.name || 'unnamed'}) has duplicate targetId ${circuit.targetId} also used by ` +
2269-
dupOwners.map(o => `${o.id}(${o.name || 'unnamed'})`).join(', ') +
2270-
`. Clearing targetId and falling back to Action 168.`
2271-
);
2272-
circuit.targetId = 0;
2273-
} else {
2274-
logger.info(`v3.004+ setCircuitStateAsync: Using Action 184 with targetId ${circuit.targetId} for circuit ${id} (${circuit.name || 'unnamed'})`);
2275-
let out = this.createAction184Message(circuit.targetId, val);
2276-
out.dest = 16; // Send to OCP
2277-
out.scope = `circuitState${id}`;
2278-
out.retries = 5;
2279-
out.response = IntelliCenterBoard.getAckResponse(184);
2280-
await out.sendAsync();
2281-
// Request updated config to confirm state change
2282-
await this.getConfigAsync([15, 0]);
2283-
let circ = state.circuits.getInterfaceById(id);
2284-
// If readback doesn't match, assume this targetId is incorrect (or rejected) and clear it to prevent repeats.
2285-
if (typeof circ?.isOn === 'boolean' && circ.isOn !== val) {
2286-
logger.warn(
2287-
`v3.004+ setCircuitStateAsync: Action 184 readback mismatch for circuit ${id} (${circuit.name || 'unnamed'}). ` +
2288-
`Requested ${val ? 'ON' : 'OFF'} but OCP reports ${circ.isOn ? 'ON' : 'OFF'}. Clearing targetId ${circuit.targetId}.`
2289-
);
2290-
circuit.targetId = 0;
2291-
}
2292-
state.emitEquipmentChanges();
2293-
return circ;
2294-
}
2239+
// v3.004+ non-body circuits: Use indexed Action 184 (Wireless-style)
2240+
// Protocol: channel=0x688F, byte[2]=circuitId-1, target=0xA8ED, byte[6]=state
2241+
// This is the native control method observed from the Wireless remote.
2242+
if (sys.equipment.isIntellicenterV3) {
2243+
const circuit = sys.circuits.getItemById(id, false);
2244+
logger.info(`v3.004+ setCircuitStateAsync: Using indexed Action 184 for circuit ${id} (${circuit?.name || 'unnamed'}) -> ${val ? 'ON' : 'OFF'}`);
2245+
let out = this.createAction184IndexedCircuitMessage(id, val);
2246+
out.dest = 16; // Send to OCP
2247+
out.scope = `circuitState${id}`;
2248+
out.retries = 5;
2249+
out.response = IntelliCenterBoard.getAckResponse(184);
2250+
await out.sendAsync();
2251+
// Request updated config to confirm state change
2252+
await this.getConfigAsync([15, 0]);
2253+
let circ = state.circuits.getInterfaceById(id);
2254+
state.emitEquipmentChanges();
2255+
return circ;
22952256
}
22962257

22972258
// v1.x or v3.004+ without known targetId: Use Action 168 (original method)
@@ -2673,35 +2634,31 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
26732634
return out;
26742635
}
26752636
/**
2676-
* Creates an Action 184 message for v3.004+ IntelliCenter circuit control.
2637+
* v3.004+ Indexed Circuit Control (Wireless-style).
26772638
* Action 184 is the native circuit control message used by the Wireless remote.
26782639
*
26792640
* Payload structure (10 bytes):
2680-
* Bytes 0-1: Channel ID (104,143 = 0x688F = default channel)
2681-
* Byte 2: Sequence number (0)
2641+
* Bytes 0-1: Channel (0x688F for circuits, 0xE89D for features)
2642+
* Byte 2: Index (circuitId - 1 or featureId - 1)
26822643
* Byte 3: Format (255 = command mode)
2683-
* Bytes 4-5: Target ID (circuit's unique identifier, learned from OCP broadcasts)
2644+
* Bytes 4-5: Target (0xA8ED = control primitive)
26842645
* Byte 6: State (0=OFF, 1=ON)
26852646
* Bytes 7-9: Reserved (0,0,0)
26862647
*
2687-
* @param targetId The circuit's unique Target ID (hi*256 + lo)
2648+
* @param circuitId The circuit ID (1-40)
26882649
* @param isOn True to turn circuit ON, false for OFF
26892650
* @returns Outbound message ready to send
26902651
*/
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
2652+
public createAction184IndexedCircuitMessage(circuitId: number, isOn: boolean): Outbound {
2653+
const idx = Math.max(0, Math.min(255, (circuitId | 0) - 1));
2654+
return Outbound.createMessage(184, [
2655+
104, 143, // Channel 0x688F (circuits)
2656+
idx, // Index (circuitId - 1)
2657+
255, // Format (command)
2658+
168, 237, // Target 0xA8ED (control primitive)
2659+
isOn ? 1 : 0, // State
2660+
0, 0, 0
27032661
], 3);
2704-
return out;
27052662
}
27062663

27072664
public async setDimmerLevelAsync(id: number, level: number): Promise<ICircuitState> {

controller/comms/messages/Messages.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,13 @@ export class Inbound extends Message {
762762
case ControllerType.IntelliCenter:
763763
switch (this.action) {
764764
case 1: // ACK
765-
VersionMessage.processAction168Ack(this);
765+
// v3.004+ piggyback: only route ACKs we care about (168/184) into a single handler
766+
// to avoid doing extra work on every ACK frame.
767+
if (this.payload.length === 1 && (this.payload[0] === 168 || this.payload[0] === 184)) {
768+
VersionMessage.processActionAck(this);
769+
} else {
770+
this.isProcessed = true;
771+
}
766772
break;
767773
case 2:
768774
case 204:
@@ -1317,21 +1323,12 @@ export class Response extends OutboundCommon {
13171323
if (msgIn.protocol !== msgOut.protocol) { return false; }
13181324
if (typeof msgIn === 'undefined') { return false; } // getting here on msg send failure
13191325

1320-
// if these properties were set on the Response (this) object via creation,
1321-
// then use the passed in values. Otherwise, use the msgIn/msgOut matching rules
1322-
// IntelliCenter config queue uses (action,payload-prefix) matching for Action 30 responses.
1323-
// Keep this stricter prefix matching scoped to IntelliCenter to avoid unintended effects on other controllers.
1324-
if (this.action > 0 && sys.controllerType === ControllerType.IntelliCenter) {
1325-
if (this.action !== msgIn.action) return false;
1326-
// If no payload prefix is provided, action match is sufficient (e.g. v3 Action 30 with empty payload).
1327-
if (this.payload.length === 0) return true;
1328-
if (msgIn.payload.length < this.payload.length) return false;
1329-
for (let i = 0; i < this.payload.length; i++) {
1330-
if (this.payload[i] !== msgIn.payload[i]) return false;
1331-
}
1332-
return true;
1333-
}
1334-
else if (msgOut.protocol === Protocol.Pump) {
1326+
// If these properties were set on the Response (this) object via creation,
1327+
// then use the passed in values. Otherwise, use the msgIn/msgOut matching rules.
1328+
//
1329+
// NOTE: IntelliCenter response matching is handled in the IntelliCenter-specific block below
1330+
// to keep the logic in one place.
1331+
if (msgOut.protocol === Protocol.Pump) {
13351332
switch (msgIn.action) {
13361333
case 7:
13371334
// Scenario 1. Request for pump status.
@@ -1404,6 +1401,20 @@ export class Response extends OutboundCommon {
14041401
}
14051402
else if (sys.controllerType === ControllerType.IntelliCenter) {
14061403
// intellicenter packets
1404+
// IntelliCenter config queue uses (action,payload-prefix) matching for Action 30 responses.
1405+
// Keep this scoped to IntelliCenter to avoid unintended effects on other controllers.
1406+
if (sys.equipment.isIntellicenterV3 && this.action > 0) {
1407+
if (this.action !== msgIn.action) return false;
1408+
// If a destination was specified on the Response, enforce it (critical for v3 unicast flows).
1409+
if (this.dest >= 0 && msgIn.dest !== this.dest) return false;
1410+
// If no payload prefix is provided, action match is sufficient (e.g. v3 Action 30 with empty payload).
1411+
if (this.payload.length === 0) return true;
1412+
if (msgIn.payload.length < this.payload.length) return false;
1413+
for (let i = 0; i < this.payload.length; i++) {
1414+
if (msgIn.payload[i] !== this.payload[i]) return false;
1415+
}
1416+
return true;
1417+
}
14071418
if (this.dest >= 0 && msgIn.dest !== this.dest) return false;
14081419
for (let i = 0; i < this.payload.length; i++) {
14091420
if (i > msgIn.payload.length - 1)

0 commit comments

Comments
 (0)