@@ -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 > {
0 commit comments