UNPKG

homebridge

Version:
192 lines 7.3 kB
/** * Fabric Manager * * Handles fabric info queries, commissioned status checks, * and fabric removal operations. */ import fs from 'node:fs'; import path from 'node:path'; import { Logger } from '../../logger.js'; import { MatterDeviceError } from '../types.js'; const log = Logger.withPrefix('Matter/Server'); export class FabricManager { getServerNode; getMatterStoragePath; constructor(getServerNode, getMatterStoragePath) { this.getServerNode = getServerNode; this.getMatterStoragePath = getMatterStoragePath; } /** * Get fabric information for commissioned controllers */ getFabricInfo() { try { const serverNode = this.getServerNode(); if (!serverNode) { return []; } // Use the server node's commissioning state to read fabric info const env = serverNode.env; if (!env) { return []; } // Try reading from the server node's state try { const serverState = serverNode; const fabrics = serverState?.state?.operationalCredentials?.fabrics; if (Array.isArray(fabrics) && fabrics.length > 0) { return fabrics.map((fabric) => ({ fabricIndex: fabric.fabricIndex || 0, fabricId: fabric.fabricId?.toString() || '', nodeId: fabric.nodeId?.toString() || '', rootVendorId: fabric.rootVendorId || 0, label: fabric.label || '', })); } } catch { // Fallback to checking storage } // Fallback: read from disk storage return this.readFabricsFromStorage(); } catch (error) { log.debug('Failed to get fabric info:', error); return []; } } /** * Read fabric information from storage files */ readFabricsFromStorage() { try { const storagePath = this.getMatterStoragePath(); if (!storagePath) { return []; } // Try to read the storage file synchronously for backwards compatibility // Look for JSON files that might contain fabric data const files = fs.readdirSync(storagePath).filter((f) => f.endsWith('.json')); for (const file of files) { try { const data = JSON.parse(fs.readFileSync(path.join(storagePath, file), 'utf-8')); // Check for fabrics data in various storage key formats const fabricsData = data?.['fabrics.fabrics'] || data?.fabrics?.fabrics; if (Array.isArray(fabricsData) && fabricsData.length > 0) { return fabricsData.map((fabric) => ({ fabricIndex: fabric.fabricIndex || 0, fabricId: fabric.fabricId?.value?.toString() || fabric.fabricId?.toString() || '', nodeId: fabric.nodeId?.value?.toString() || fabric.nodeId?.toString() || '', rootVendorId: fabric.rootVendorId || 0, label: fabric.label || '', })); } } catch { // Skip files that can't be parsed } } return []; } catch { return []; } } /** * Check if the server is commissioned */ isCommissioned() { try { const serverNode = this.getServerNode(); if (serverNode) { // Try to check commissioned state from the server node try { const serverState = serverNode; const commissioned = serverState?.state?.commissioning?.commissioned; if (commissioned === true) { return true; } } catch { // Fallback } } // Fallback to checking fabric count const fabrics = this.getFabricInfo(); return fabrics.length > 0; } catch (error) { log.debug('Failed to check commissioned status:', error); return false; } } /** * Get the number of commissioned fabrics */ getCommissionedFabricCount() { return this.getFabricInfo().length; } /** * Get commissioned/fabricCount/fabrics in a single pass. * * Coalesces what would otherwise be three separate getFabricInfo() calls * (one each from isCommissioned, getCommissionedFabricCount, getFabricInfo). * In the cold path that means one sync filesystem scan instead of three. * Preserves the serverNode.state fast-path that isCommissioned() uses. */ getCommissioningSnapshot() { const fabrics = this.getFabricInfo(); const fabricCount = fabrics.length; if (fabricCount > 0) { return { commissioned: true, fabricCount, fabrics }; } // Fabric list is empty — fall back to the serverNode commissioning flag in // case the state is reachable but fabric enumeration didn't return rows. let commissioned = false; try { const serverNode = this.getServerNode(); if (serverNode) { const serverState = serverNode; commissioned = serverState?.state?.commissioning?.commissioned === true; } } catch { // Treat any access failure as not-commissioned. } return { commissioned, fabricCount, fabrics }; } /** * Remove a specific fabric (controller) from the bridge */ async removeFabric(fabricIndex) { const serverNode = this.getServerNode(); if (!serverNode) { throw new MatterDeviceError('Matter server not started'); } try { log.info(`Removing fabric ${fabricIndex}...`); const serverState = serverNode; const removeFabric = serverState?.state?.commissioning?.removeFabric; if (typeof removeFabric !== 'function') { throw new MatterDeviceError('Fabric removal not supported by Matter.js version'); } await removeFabric(fabricIndex); log.info(`Fabric ${fabricIndex} removed successfully`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log.error(`Failed to remove fabric ${fabricIndex}:`, error); throw new MatterDeviceError(`Failed to remove fabric: ${errorMessage}`, { originalError: error instanceof Error ? error : undefined, }); } } /** * Check if a specific fabric exists */ hasFabric(fabricIndex) { const fabrics = this.getFabricInfo(); return fabrics.some(f => f.fabricIndex === fabricIndex); } } //# sourceMappingURL=FabricManager.js.map