#4009·homebridge

Matter: external accessory shown as 'Not Paired' after restart although the node still holds a fabric

Author: maksymenkomlCreated Sep 8, 2026Updated Sep 8, 2026

Environment: Homebridge 2.4.0, Config UI X 5.29.0, Node 22.19.0, Synology DSM. External Matter accessory (RoboticVacuumCleaner) published from a child bridge by homebridge-dreame-vacuum-matter 1.0.4.

What happened

  1. Apple Home commissioned the external node:
    [6:06:10 PM] [Matter/Server] Fabric added: index 2
    [6:06:10 PM] [Matter/GeneralCommissioningClusterHandler] Commissioned fabric: c35d23cad70731a8 (#2) node: 000000004db48fcf
    
  2. The child bridge was restarted (Loading existing commissioning credentials from storage, node came back online on port 5532).
  3. The node still held the fabric: mDNS advertised C35D23CAD70731A8-000000004DB48FCF._matter._tcp on port 5532 and no _matterc._udp record for it.
  4. Plugins → External Accessories showed Not Paired · Scan to add to HomeKit, and Settings → Reset Individual Bridges listed it under Non Child Bridges with "pairing information will remain unchanged".

Following the UI I kept scanning the code; Apple Home searched for ~10 minutes and failed each time because the node was not commissionable. Only removing the node's storage (matter/B8653CFC01E7) made it pairable again.

Expected: the dialog reports the node as paired (with the fabric count), so the owner knows to remove the fabric or reset the node instead of scanning.

Suspected cause

The UI reads commissioned from matter/<id>/commissioning.json. CommissioningManager.generateCommissioningInfo() rewrites that file on every start with fabricManager.isCommissioned(), and for external accessories this runs before runServer(). At that point serverNode.state.commissioning.commissioned is not set, so isCommissioned() falls back to FabricManager.readFabricsFromStorage(), which scans *.json files in the storage root for a fabrics.fabrics key. With the matter.js storage the fabrics live one level down, without a .json extension:

matter/B8653CFC01E7/commissioning.json      <- {"commissioned": false, ...} after restart
matter/B8653CFC01E7/credentials.json
matter/B8653CFC01E7/accessories.json
matter/B8653CFC01E7/B8653CFC01E7/fabrics.fabrics   <- [{"fabricIndex":3,"fabricId":...}]
matter/B8653CFC01E7/B8653CFC01E7/root.operationalCredentials.fabrics

So the scan returns [], commissioned: false is written, and it is only corrected by a later fabricsChanged / commissioned event, which never fires for a node that was already commissioned before the restart.

Possible fix: refresh commissioning.json once the node is online using the live operationalCredentials.fabrics list, or teach readFabricsFromStorage() about <id>/<id>/fabrics.fabrics.

Full logs available on request.