homebridge
Version:
HomeKit support for the impatient
192 lines • 7.3 kB
JavaScript
/**
* 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