Skip to content

Commit eec718a

Browse files
Refactor features to use new [232,157] payload block.
Piggyback on features confirmations and request same in configqueue. Don't debounce config requests just delay them. #1090
1 parent 690b47c commit eec718a

3 files changed

Lines changed: 107 additions & 66 deletions

File tree

controller/boards/IntelliCenterBoard.ts

Lines changed: 88 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2237,12 +2237,30 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
22372237
}
22382238

22392239
// 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.
22422240
if (sys.equipment.isIntellicenterV3) {
22432241
const circuit = sys.circuits.getItemById(id, false);
22442242
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);
2243+
/**
2244+
* v3.004+ Indexed Circuit Control (Wireless-style).
2245+
* Action 184 is the native circuit control message used by the Wireless remote.
2246+
*
2247+
* Payload structure (10 bytes):
2248+
* Bytes 0-1: Channel (0x688F for circuits)
2249+
* Byte 2: Index (circuitId - 1 or featureId - 1)
2250+
* Byte 3: Format (255 = command mode)
2251+
* Bytes 4-5: Target (0xA8ED = control primitive)
2252+
* Byte 6: State (0=OFF, 1=ON)
2253+
* Bytes 7-9: Reserved (0,0,0)
2254+
*/
2255+
const idx = Math.max(0, Math.min(255, (id | 0) - 1));
2256+
const out = Outbound.createMessage(184, [
2257+
104, 143, // Channel 0x688F (circuits)
2258+
idx, // Index (circuitId - 1)
2259+
255, // Format (command)
2260+
168, 237, // Target 0xA8ED (control primitive)
2261+
val ? 1 : 0, // State
2262+
0, 0, 0
2263+
], 3);
22462264
out.dest = 16; // Send to OCP
22472265
out.scope = `circuitState${id}`;
22482266
out.retries = 5;
@@ -2633,33 +2651,6 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
26332651
}
26342652
return out;
26352653
}
2636-
/**
2637-
* v3.004+ Indexed Circuit Control (Wireless-style).
2638-
* Action 184 is the native circuit control message used by the Wireless remote.
2639-
*
2640-
* Payload structure (10 bytes):
2641-
* Bytes 0-1: Channel (0x688F for circuits, 0xE89D for features)
2642-
* Byte 2: Index (circuitId - 1 or featureId - 1)
2643-
* Byte 3: Format (255 = command mode)
2644-
* Bytes 4-5: Target (0xA8ED = control primitive)
2645-
* Byte 6: State (0=OFF, 1=ON)
2646-
* Bytes 7-9: Reserved (0,0,0)
2647-
*
2648-
* @param circuitId The circuit ID (1-40)
2649-
* @param isOn True to turn circuit ON, false for OFF
2650-
* @returns Outbound message ready to send
2651-
*/
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
2661-
], 3);
2662-
}
26632654

26642655
public async setDimmerLevelAsync(id: number, level: number): Promise<ICircuitState> {
26652656
let circuit = sys.circuits.getItemById(id);
@@ -2692,8 +2683,73 @@ class IntelliCenterCircuitCommands extends CircuitCommands {
26922683
}
26932684
class IntelliCenterFeatureCommands extends FeatureCommands {
26942685
declare board: IntelliCenterBoard;
2695-
public async setFeatureStateAsync(id, val): Promise<ICircuitState> { return sys.board.circuits.setCircuitStateAsync(id, val); }
2696-
public async toggleFeatureStateAsync(id): Promise<ICircuitState> { return sys.board.circuits.toggleCircuitStateAsync(id); }
2686+
2687+
private async getConfigAsync(payload: number[]): Promise<boolean> {
2688+
const dest = sys.equipment.isIntellicenterV3 ? 16 : 15;
2689+
let out = Outbound.create({
2690+
dest,
2691+
action: 222,
2692+
retries: 3,
2693+
payload: payload,
2694+
response: Response.create({ dest: -1, action: 30, payload: payload })
2695+
});
2696+
await out.sendAsync();
2697+
// Do NOT ACK(30). Wireless captures show config succeeds without ACK(30), and v1 queue avoids ACK(30).
2698+
return true;
2699+
}
2700+
2701+
public async setFeatureStateAsync(id: number, val: boolean): Promise<ICircuitState> {
2702+
// v3.004+: Features are controlled via Action 184 channel 0xE89D (232,157), not the circuits channel.
2703+
if (sys.equipment.isIntellicenterV3) {
2704+
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
2705+
if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
2706+
2707+
const feature = sys.features.getItemById(id, false, { isActive: false });
2708+
logger.info(`v3.004+ setFeatureStateAsync: Using indexed Action 184 for feature ${id} (${feature?.name || 'unnamed'}) -> ${val ? 'ON' : 'OFF'}`);
2709+
2710+
/**
2711+
* v3.004+ Indexed Feature Control (Wireless-style).
2712+
* Action 184 is the native feature control message used by the Wireless remote (channel 0xE89D).
2713+
*
2714+
* Payload structure (10 bytes):
2715+
* Bytes 0-1: Channel (0xE89D for features)
2716+
* Byte 2: Index (featureId - 1)
2717+
* Byte 3: Format/mode (observed 0 in replays 132/138 feature toggles)
2718+
* Bytes 4-5: Target (0xA8ED = control primitive)
2719+
* Byte 6: State (0=OFF, 1=ON)
2720+
* Bytes 7-9: Reserved (0,0,0)
2721+
*/
2722+
const idx = Math.max(0, Math.min(255, (id | 0) - 1));
2723+
const out = Outbound.createMessage(184, [
2724+
232, 157, // Channel 0xE89D (features)
2725+
idx, // Index (featureId - 1)
2726+
0, // Format/mode (observed)
2727+
168, 237, // Target 0xA8ED (control primitive)
2728+
val ? 1 : 0, // State
2729+
0, 0, 0
2730+
], 3);
2731+
out.dest = 16; // Send to OCP
2732+
out.scope = `featureState${id}`;
2733+
out.retries = 5;
2734+
out.response = IntelliCenterBoard.getAckResponse(184);
2735+
await out.sendAsync();
2736+
2737+
// Request updated system state to confirm feature change (authoritative source for v3 features).
2738+
await this.getConfigAsync([15, 0]);
2739+
2740+
const fstate = state.features.getItemById(id, true);
2741+
state.emitEquipmentChanges();
2742+
return fstate;
2743+
}
2744+
2745+
// Legacy behavior (v1.x): delegate to circuit state setter.
2746+
return sys.board.circuits.setCircuitStateAsync(id, val);
2747+
}
2748+
2749+
public async toggleFeatureStateAsync(id: number): Promise<ICircuitState> {
2750+
const feat = state.features.getItemById(id);
2751+
return this.setFeatureStateAsync(id, !(feat.isOn || false));
2752+
}
26972753
public syncGroupStates() { } // Do nothing and let IntelliCenter do it.
26982754
public async setFeatureAsync(data: any): Promise<Feature> {
26992755

controller/comms/messages/status/EquipmentStateMessage.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -620,38 +620,6 @@ export class EquipmentStateMessage {
620620
break;
621621
}
622622
case 184: {
623-
// v3.004+ Action 184 - Circuit/Feature control
624-
//
625-
// Payload structure (10 bytes):
626-
// Bytes 0-1: Channel (0x688F=circuits, 0xE89D=features)
627-
// Byte 2: Index (circuitId-1 or featureId-1)
628-
// Byte 3: Format (255=command, 0=status)
629-
// Bytes 4-5: Target (0xA8ED=control, 0xD4B6=body context)
630-
// Byte 6: State (0=OFF, 1=ON)
631-
// Bytes 7-9: Context data (for D4B6) or reserved (for A8ED)
632-
//
633-
// Control panel sources: Wireless(36), ICP(32) send commands to OCP(16)
634-
// OCP(16) broadcasts status to all(15)
635-
if (sys.controllerType === ControllerType.IntelliCenter &&
636-
sys.equipment.isIntellicenterV3 &&
637-
msg.payload.length >= 10) {
638-
639-
const channelId = msg.extractPayloadByte(0) * 256 + msg.extractPayloadByte(1);
640-
const index = msg.extractPayloadByte(2);
641-
const targetId = msg.extractPayloadByte(4) * 256 + msg.extractPayloadByte(5);
642-
const stateVal = msg.extractPayloadByte(6);
643-
644-
const isFromOCP = msg.source === 16;
645-
const isFromControlPanel = (msg.source === 36 || msg.source === 32) && msg.dest === 16;
646-
647-
if (isFromOCP || isFromControlPanel) {
648-
const sourceDesc = isFromOCP ? 'OCP' : (msg.source === 36 ? 'Wireless' : 'ICP');
649-
const channelName = channelId === 0x688F ? 'circuits' : (channelId === 0xE89D ? 'features' : `0x${channelId.toString(16)}`);
650-
const targetName = targetId === 0xA8ED ? 'control' : (targetId === 0xD4B6 ? 'body-ctx' : `0x${targetId.toString(16)}`);
651-
652-
logger.debug(`v3.004+ Action 184 (${sourceDesc}): channel=${channelName} idx=${index} target=${targetName} state=${stateVal}`);
653-
}
654-
}
655623
msg.isProcessed = true;
656624
break;
657625
}

controller/comms/messages/status/VersionMessage.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,29 @@ export class VersionMessage {
2323
// Debounce config refresh requests to avoid duplicate requests from overlapping triggers
2424
private static lastConfigRefreshTime: number = 0;
2525
private static readonly CONFIG_REFRESH_DEBOUNCE_MS = 2000; // 2 seconds
26+
private static pendingConfigRefreshTimer?: NodeJS.Timeout;
27+
private static pendingConfigRefreshSource?: string;
2628

2729
/**
2830
* Shared method to trigger a config refresh with debouncing.
2931
* Prevents duplicate requests when multiple triggers fire in quick succession.
3032
*/
3133
private static triggerConfigRefresh(source: string): void {
3234
const now = Date.now();
33-
if (now - this.lastConfigRefreshTime < this.CONFIG_REFRESH_DEBOUNCE_MS) {
34-
logger.silly(`v3.004+ ${source}: Skipping config refresh (debounced, last was ${now - this.lastConfigRefreshTime}ms ago)`);
35+
const elapsed = now - this.lastConfigRefreshTime;
36+
if (elapsed < this.CONFIG_REFRESH_DEBOUNCE_MS) {
37+
// Throttle-with-trailing: don't lose rapid toggle updates; schedule one refresh at end of window.
38+
const remainingMs = Math.max(0, this.CONFIG_REFRESH_DEBOUNCE_MS - elapsed);
39+
this.pendingConfigRefreshSource = source;
40+
if (!this.pendingConfigRefreshTimer) {
41+
this.pendingConfigRefreshTimer = setTimeout(() => {
42+
this.pendingConfigRefreshTimer = undefined;
43+
const src = this.pendingConfigRefreshSource ? `${this.pendingConfigRefreshSource} (trailing)` : 'Trailing';
44+
this.pendingConfigRefreshSource = undefined;
45+
this.triggerConfigRefresh(src);
46+
}, remainingMs);
47+
}
48+
logger.silly(`v3.004+ ${source}: Skipping immediate config refresh (debounced, last was ${elapsed}ms ago)`);
3549
return;
3650
}
3751
this.lastConfigRefreshTime = now;
@@ -41,6 +55,9 @@ export class VersionMessage {
4155
// OCP doesn't increment options version when heat mode/setpoints change,
4256
// so we force a refresh by clearing our cached version.
4357
sys.configVersion.options = 0;
58+
// v3.004+: OCP does NOT reliably increment systemState when features toggle (esp. rapid OFF/ON sequences).
59+
// Force a systemState refresh so queueChanges() will request category 15 (systemState), option [0] => Action 222 [15,0].
60+
sys.configVersion.systemState = 0;
4461
logger.silly(`v3.004+ ${source}: Sending Action 228`);
4562
Outbound.create({
4663
dest: 16, action: 228, payload: [0], retries: 2,

0 commit comments

Comments
 (0)