UNPKG

homebridge

Version:
347 lines 17.7 kB
/** * Child Bridge Matter Manager * * Manages Matter server lifecycle and accessories for child bridges. * This class extracts Matter-specific logic from childBridgeFork.ts to minimize changes to core files. */ import process from 'node:process'; import { DEFAULT_BRIDGE_DEFAULTS } from '../bridgeService.js'; import { Logger } from '../logger.js'; import { User } from '../user.js'; import getVersion from '../version.js'; import { BaseMatterManager } from './BaseMatterManager.js'; import { publishExternalMatterAccessory } from './ExternalMatterAccessoryPublisher.js'; import { MatterServer } from './server.js'; import { appendUsernameSuffix, getMatterJsVersion, normalizeBindConfig } from './utils.js'; const log = Logger.withPrefix('Matter/ChildManager'); const COLON_RE = /:/g; /** * Manages Matter server and accessories for a child bridge */ export class ChildBridgeMatterManager extends BaseMatterManager { bridgeConfig; bridgeOptions; api; externalPortService; // Matter configuration from bridge config matterConfig; // Stored serial number for status updates matterSerialNumber; constructor(bridgeConfig, bridgeOptions, api, externalPortService, pluginManager) { super(pluginManager); this.bridgeConfig = bridgeConfig; this.bridgeOptions = bridgeOptions; this.api = api; this.externalPortService = externalPortService; this.matterConfig = bridgeConfig.matter; } // Stored listener references so they can be removed in teardown() _onPublishExternalMatterAccessories = (accessories, registrationId) => { this.handlePublishExternalAccessories(accessories, registrationId).catch((error) => { log.error('Failed to publish external Matter accessories:', error); this.api._resolveExternalRegistration(registrationId); }); }; _onRegisterMatterPlatformAccessories = (pluginIdentifier, platformName, accessories) => { this.handleRegisterPlatformAccessories(pluginIdentifier, platformName, accessories).catch((error) => { log.error(`Failed to register Matter accessories for ${pluginIdentifier}:`, error); }); }; _onUpdateMatterPlatformAccessories = (accessories) => { this.handleUpdatePlatformAccessories(accessories).catch((error) => { log.error('Failed to update Matter platform accessories:', error); }); }; _onUnregisterMatterPlatformAccessories = (pluginIdentifier, platformName, accessories) => { this.handleUnregisterPlatformAccessories(pluginIdentifier, platformName, accessories).catch((error) => { log.error(`Failed to unregister Matter accessories for ${pluginIdentifier}:`, error); }); }; _onUnregisterExternalMatterAccessories = (accessories) => { this.handleUnregisterExternalAccessories(accessories).catch((error) => { log.error('Failed to unregister external Matter accessories:', error); }); }; _onUpdateMatterAccessoryState = (uuid, cluster, attributes, partId) => { this.handleUpdateAccessoryState(uuid, cluster, attributes, partId).catch((error) => { log.error(`Failed to update Matter accessory state for ${uuid}:`, error); }); }; // Stored references so listeners can be removed in teardown() _onMatterServerStateChange = ({ uuid, cluster, state, partId }) => { if (process.send) { process.send({ id: 'matterEvent', data: { type: 'accessoryUpdate', data: { uuid, cluster, state, partId }, }, }); } }; _onCommissioningStatusChanged; /** * Initialize Matter server for child bridge if enabled * @param onCommissioningChanged Optional callback when commissioning status changes */ async initialize(onCommissioningChanged) { // Check if Matter is configured if (!this.matterConfig) { return; } log.debug(`Child bridge ${this.bridgeConfig.username} has Matter config (Combined HAP+Matter), starting Matter server`); // If Matter doesn't have a port configured, allocate one if (!this.matterConfig.port) { // Generate a unique username for Matter port allocation const matterUsername = appendUsernameSuffix(this.bridgeConfig.username, 'MATTER'); const matterPort = await this.externalPortService.requestPort(matterUsername); if (!matterPort) { throw new Error('Failed to allocate Matter port for child bridge. ' + 'Please specify a port manually in the _bridge.matter configuration, or free up ports in the configured range.'); } this.matterConfig.port = matterPort; log.debug(`Allocated Matter port: ${this.matterConfig.port} (HAP port: ${this.bridgeConfig.port})`); } // Start Matter server await this.startMatterServer(this.matterConfig); // Listen for commissioning status changes to update parent process if (onCommissioningChanged && this.matterServer) { this._onCommissioningStatusChanged = (commissioned, fabricCount) => { log.info(`Matter commissioning status changed for child bridge ${this.bridgeConfig.username}: commissioned=${commissioned}, fabricCount=${fabricCount}`); onCommissioningChanged(); }; this.matterServer.on('commissioning-status-changed', this._onCommissioningStatusChanged); } // Listen for state changes and forward to parent process if (this.matterServer) { this.matterServer.on('stateChange', this._onMatterServerStateChange); } } /** * Start Matter server for child bridge */ async startMatterServer(matterConfig) { // Log Matter.js version and startup info const matterJsVersion = await getMatterJsVersion(); log.success('Homebridge v%s (Matter.js v%s) (%s) is running on port %s.', getVersion(), matterJsVersion, this.bridgeConfig.name, matterConfig.port); log.debug(`Starting Matter server for child bridge ${this.bridgeConfig.username}`); // Create Matter server with the provided configuration const serialNumber = this.bridgeConfig.username.replace(COLON_RE, ''); // Normalize bind config to array format const networkInterfaces = normalizeBindConfig(this.bridgeConfig.bind); this.matterServer = new MatterServer({ port: matterConfig.port || 5540, uniqueId: serialNumber, storagePath: User.matterPath(), displayName: this.bridgeConfig.name || 'Child Bridge', debugModeEnabled: this.bridgeOptions.debugModeEnabled, manufacturer: this.bridgeConfig.manufacturer || DEFAULT_BRIDGE_DEFAULTS.manufacturer, model: this.bridgeConfig.model || DEFAULT_BRIDGE_DEFAULTS.model, firmwareRevision: this.bridgeConfig.firmwareRevision || getVersion(), serialNumber, networkInterfaces, }); await this.matterServer.start(); // Inform the API that Matter is enabled this.api._setMatterEnabled(true); // Set the Matter server reference for API methods like getAccessoryState this.api._setMatterServer(this.matterServer); const commissioningInfo = this.matterServer.getCommissioningInfo(); log.info(`Matter server started for child bridge ${this.bridgeConfig.username} with commissioning info:`, commissioningInfo); // Store the serial number for status updates this.matterSerialNumber = commissioningInfo.serialNumber; // Set up event listeners for Matter API calls this.setupEventListeners(); } /** * Set up Matter API event listeners */ setupEventListeners() { this.api.on("publishExternalMatterAccessories" /* InternalAPIEvent.PUBLISH_EXTERNAL_MATTER_ACCESSORIES */, this._onPublishExternalMatterAccessories); this.api.on("registerMatterPlatformAccessories" /* InternalAPIEvent.REGISTER_MATTER_PLATFORM_ACCESSORIES */, this._onRegisterMatterPlatformAccessories); this.api.on("updateMatterPlatformAccessories" /* InternalAPIEvent.UPDATE_MATTER_PLATFORM_ACCESSORIES */, this._onUpdateMatterPlatformAccessories); this.api.on("unregisterMatterPlatformAccessories" /* InternalAPIEvent.UNREGISTER_MATTER_PLATFORM_ACCESSORIES */, this._onUnregisterMatterPlatformAccessories); this.api.on("unregisterExternalMatterAccessories" /* InternalAPIEvent.UNREGISTER_EXTERNAL_MATTER_ACCESSORIES */, this._onUnregisterExternalMatterAccessories); this.api.on("updateMatterAccessoryState" /* InternalAPIEvent.UPDATE_MATTER_ACCESSORY_STATE */, this._onUpdateMatterAccessoryState); } /** * Handle external Matter accessories - each gets its own dedicated Matter server * This is required for devices like Robotic Vacuum Cleaners that Apple Home * requires to be on their own bridge. */ async handlePublishExternalAccessories(accessories, registrationId) { log.info(`Publishing ${accessories.length} external Matter accessor${accessories.length === 1 ? 'y' : 'ies'} from child bridge ${this.bridgeConfig.username}`); try { // Normalize bind config to array format (inherit from bridge) const networkInterfaces = normalizeBindConfig(this.bridgeConfig.bind); for (const accessory of accessories) { try { // Check if already published if (this.externalMatterServers.has(accessory.UUID)) { log.warn(`External Matter accessory ${accessory.displayName} (${accessory.UUID}) is already published`); continue; } // Publish the accessory using shared helper const result = await publishExternalMatterAccessory(accessory, { portService: this.externalPortService, networkInterfaces, debugModeEnabled: this.bridgeOptions.debugModeEnabled, }); if (!result) { // Validation or publishing failed (errors already logged by helper) continue; } // Store the server instance this.externalMatterServers.set(accessory.UUID, result.server); // Listen for state changes and forward to parent process // (same pattern as the child bridge server listener in initialize()) result.server.on('stateChange', this._onMatterServerStateChange); // Register the external bridge username with parent process for routing // Send via IPC to parent - parent will register in externalMatterBridgeRegistry if (process.send) { process.send({ id: 'matterEvent', data: { type: 'externalBridgeRegistration', data: { externalBridgeUsername: result.username, childBridgeUsername: this.bridgeConfig.username, }, }, }); } // Log commissioning info if (result.commissioningInfo.qrCode && result.commissioningInfo.manualPairingCode) { log.info(`📱 Commissioning codes for ${accessory.displayName}:`); log.info(` QR Code: ${result.commissioningInfo.qrCode}`); log.info(` Manual Code: ${result.commissioningInfo.manualPairingCode}`); } } catch (error) { log.error(`Failed to publish external Matter accessory ${accessory.displayName}:`, error); } } } finally { // Notify that registration is complete (whether successful or not) this.api._resolveExternalRegistration(registrationId); } } /** * Get Matter status information for IPC communication * Returns undefined if Matter is not enabled for this child bridge */ getMatterStatusInfo() { if (!this.matterConfig || !this.matterServer) { return undefined; } const commissioningInfo = this.matterServer.getCommissioningInfo(); return { qrCode: commissioningInfo.qrCode, manualPairingCode: commissioningInfo.manualPairingCode, serialNumber: this.matterSerialNumber || commissioningInfo.serialNumber, commissioned: commissioningInfo.commissioned || false, deviceCount: this.matterServer.getAccessories().length, }; } /** * Check if Matter is enabled for this child bridge */ isMatterEnabled() { return this.matterServer !== undefined; } /** * Enable state monitoring on all Matter servers * Override to add bridge-specific logging */ enableStateMonitoring() { log.debug(`Enabling Matter state monitoring for child bridge ${this.bridgeConfig.username}`); super.enableStateMonitoring(); } /** * Disable state monitoring on all Matter servers * Override to add bridge-specific logging */ disableStateMonitoring() { log.debug(`Disabling Matter state monitoring for child bridge ${this.bridgeConfig.username}`); super.disableStateMonitoring(); } /** * Collect all Matter accessories for UI display */ collectAllAccessories() { const accessories = []; if (this.matterServer) { const serverAccessories = this.matterServer.collectAccessories(this.bridgeConfig.username, 'child', this.bridgeConfig.name || 'Child Bridge'); accessories.push(...serverAccessories); } // Collect from external servers for (const server of this.externalMatterServers.values()) { const externalAccessories = server.collectAccessories(server.username, 'external', server.bridgeName); accessories.push(...externalAccessories); } return accessories; } /** * Get detailed info for a specific Matter accessory * * @param uuid - Accessory UUID * @returns Accessory info or undefined if not found */ getAccessoryInfo(uuid) { // Check main server if (this.matterServer) { const info = this.matterServer.getAccessoryInfo(uuid); if (info) { return info; } } // Check external servers for (const server of this.externalMatterServers.values()) { const info = server.getAccessoryInfo(uuid); if (info) { return info; } } return undefined; } /** * Teardown Matter servers */ async teardown() { // Remove API event listeners to prevent retention of this manager after teardown this.api.removeListener("publishExternalMatterAccessories" /* InternalAPIEvent.PUBLISH_EXTERNAL_MATTER_ACCESSORIES */, this._onPublishExternalMatterAccessories); this.api.removeListener("registerMatterPlatformAccessories" /* InternalAPIEvent.REGISTER_MATTER_PLATFORM_ACCESSORIES */, this._onRegisterMatterPlatformAccessories); this.api.removeListener("updateMatterPlatformAccessories" /* InternalAPIEvent.UPDATE_MATTER_PLATFORM_ACCESSORIES */, this._onUpdateMatterPlatformAccessories); this.api.removeListener("unregisterMatterPlatformAccessories" /* InternalAPIEvent.UNREGISTER_MATTER_PLATFORM_ACCESSORIES */, this._onUnregisterMatterPlatformAccessories); this.api.removeListener("unregisterExternalMatterAccessories" /* InternalAPIEvent.UNREGISTER_EXTERNAL_MATTER_ACCESSORIES */, this._onUnregisterExternalMatterAccessories); this.api.removeListener("updateMatterAccessoryState" /* InternalAPIEvent.UPDATE_MATTER_ACCESSORY_STATE */, this._onUpdateMatterAccessoryState); // Stop main Matter server if it was initialized if (this.matterServer) { log.debug(`Stopping Matter server for child bridge ${this.bridgeConfig.username}`); try { this.matterServer.removeListener('stateChange', this._onMatterServerStateChange); if (this._onCommissioningStatusChanged) { this.matterServer.removeListener('commissioning-status-changed', this._onCommissioningStatusChanged); } await this.matterServer.stop(); } catch (error) { log.error('Error stopping Matter server:', error); } } // Stop all external Matter servers for (const [uuid, matterServer] of this.externalMatterServers) { log.debug(`Stopping external Matter server for ${uuid}`); try { matterServer.removeListener('stateChange', this._onMatterServerStateChange); await matterServer.stop(); } catch (error) { log.error(`Error stopping external Matter server for ${uuid}:`, error); } } this.externalMatterServers.clear(); } } //# sourceMappingURL=ChildBridgeMatterManager.js.map