UNPKG

homebridge

Version:
906 lines 45.7 kB
import { existsSync, readFileSync } from 'node:fs'; import process from 'node:process'; import chalk from 'chalk'; import qrcode from 'qrcode-terminal'; import { HomebridgeAPI } from './api.js'; import { BridgeService } from './bridgeService.js'; import { ChildBridgeService } from './childBridgeService.js'; import { ExternalPortService } from './externalPortService.js'; import { IpcService } from './ipcService.js'; import { Logger } from './logger.js'; import { MatterConfigCollector } from './matter/config.js'; import { PluginManager } from './pluginManager.js'; import { User } from './user.js'; import { validMacAddress } from './util/mac.js'; const log = Logger.internal; const matterLogger = Logger.withPrefix('Matter/MainManager'); // eslint-disable-next-line no-restricted-syntax export var ServerStatus; (function (ServerStatus) { /** * When the server is starting up */ ServerStatus["PENDING"] = "pending"; /** * When the server is online and has published the main bridge */ ServerStatus["OK"] = "ok"; /** * When the server is shutting down */ ServerStatus["DOWN"] = "down"; })(ServerStatus || (ServerStatus = {})); export class Server { options; api; pluginManager; bridgeService; externalPortService; ipcService; config; // used to keep track of child bridges // Key is HAP username (MAC address) childBridges = new Map(); // Matter bridge manager (handles Matter server lifecycle) // Lazy-loaded only when Matter is configured to avoid loading heavy Matter.js libraries matterManager; // Registry of external Matter bridge usernames to their owning bridge // Key: external Matter bridge username (e.g., CE:65:F2:E2:D5:98) // Value: owner bridge username (main bridge or child bridge MAC address) externalMatterBridgeRegistry = new Map(); // Matter monitoring state (for UI accessories page) matterMonitoringActive = false; matterMonitoringClients = 0; // current server status serverStatus = "pending" /* ServerStatus.PENDING */; constructor(options = {}) { this.options = options; this.config = Server.loadConfig(); // object we feed to Plugins and BridgeService this.api = new HomebridgeAPI(); this.ipcService = new IpcService(); // Collect all configured Matter ports to avoid conflicts const configuredMatterPorts = MatterConfigCollector.collectConfiguredMatterPorts(this.config); this.externalPortService = new ExternalPortService(this.config.ports, this.config.matterPorts, configuredMatterPorts); // set status to pending this.setServerStatus("pending" /* ServerStatus.PENDING */); // create new plugin manager const pluginManagerOptions = { activePlugins: this.config.plugins, disabledPlugins: this.config.disabledPlugins, customPluginPath: options.customPluginPath, strictPluginResolution: options.strictPluginResolution, }; this.pluginManager = new PluginManager(this.api, pluginManagerOptions); // create new bridge service const bridgeConfig = { cachedAccessoriesDir: User.cachedAccessoryPath(), cachedAccessoriesItemName: 'cachedAccessories', }; // shallow copy the homebridge options to the bridge options object Object.assign(bridgeConfig, this.options); this.bridgeService = new BridgeService(this.api, this.pluginManager, this.externalPortService, bridgeConfig, this.config.bridge); // Note: MatterBridgeManager creation is deferred to start() to avoid loading // heavy Matter.js libraries during construction when Matter may not be configured // Watch bridge events to check when server is online this.bridgeService.bridge.on("advertised" /* AccessoryEventTypes.ADVERTISED */, () => { this.setServerStatus("ok" /* ServerStatus.OK */); }); // watch for the paired event to update the server status this.bridgeService.bridge.on("paired" /* AccessoryEventTypes.PAIRED */, () => { this.setServerStatus(this.serverStatus); }); // watch for the unpaired event to update the server status this.bridgeService.bridge.on("unpaired" /* AccessoryEventTypes.UNPAIRED */, () => { this.setServerStatus(this.serverStatus); }); } /** * Set the current server status and update parent via IPC * @param status */ setServerStatus(status) { this.serverStatus = status; // setupURI() asserts the accessory is published. _accessoryInfo is only // set post-publish, so use it as the guard — covers both the HAP-disabled // case and the teardown path, where the bridge has been torn down. const bridge = this.bridgeService?.bridge; const isPublished = !!bridge?._accessoryInfo; const statusUpdate = { status: this.serverStatus, paired: isPublished ? (bridge?._accessoryInfo?.paired() ?? null) : null, setupUri: isPublished ? (bridge?.setupURI() ?? null) : null, name: this.config.bridge.name, username: this.config.bridge.username, pin: this.config.bridge.pin, matter: this.matterManager?.getMatterStatus() ?? { enabled: false }, }; this.ipcService.sendMessage("serverStatusUpdate" /* IpcOutgoingEvent.SERVER_STATUS_UPDATE */, statusUpdate); } async start() { if (this.config.bridge.disableIpc !== true) { this.initializeIpcEventHandlers(); } const promises = []; // load the cached accessories await this.bridgeService.loadCachedPlatformAccessoriesFromDisk(); // Validate Matter configuration up front so we know whether to expose // api.matter to plugins. Validator may strip invalid entries, so re-check // after. Caching the result avoids two more hasMatterConfig calls below. let matterIsConfigured = MatterConfigCollector.hasMatterConfig(this.config); if (matterIsConfigured) { await MatterConfigCollector.validateMatterConfig(this.config); matterIsConfigured = MatterConfigCollector.hasMatterConfig(this.config); } // Eagerly load the MatterAPI facade before plugins initialize, so api.matter // is defined when plugin code runs on Matter-enabled bridges. The heavy // MatterBridgeManager init still happens after plugins load (below) — only // the API surface needs to be ready early. if (matterIsConfigured) { await this.api.loadMatterAPI(); } // initialize plugins await this.pluginManager.initializeInstalledPlugins(); // Initialize Matter manager only if configured. Heavy Matter.js libraries // are loaded here (async), avoiding sync blocking during construction. if (matterIsConfigured) { // Dynamically import MatterBridgeManager only when needed // This prevents loading heavy Matter.js libraries when Matter is not configured const { MatterBridgeManager } = await import('./matter/MatterBridgeManager.js'); // Create the manager this.matterManager = new MatterBridgeManager(this.config, this.api, this.externalPortService, this.pluginManager, this.options, this); // Set manager reference on API for getAccessoryState this.api._setMatterManager(this.matterManager); // Initialize Matter server for main bridge if enabled await this.matterManager.initialize(); } if (this.config.platforms.length > 0) { promises.push(...this.loadPlatforms()); } if (this.config.accessories.length > 0) { this.loadAccessories(); } // start child bridges for (const childBridge of this.childBridges.values()) { childBridge.start(); } // restore cached accessories this.bridgeService.restoreCachedPlatformAccessories(); this.matterManager?.restoreCachedAccessories(this.options.keepOrphanedCachedAccessories ?? false); this.api.signalFinished(); // wait for all platforms to publish their accessories before we publish the bridge await Promise.all(promises); if (Server.isHapEnabled(this.config.bridge)) { this.publishBridge(); } else { // HAP is opted out. The bridge ADVERTISED listener won't fire, so move // server status to OK explicitly — Matter is the only protocol up here. log.info('HAP is disabled for the main bridge (bridge.hap=false); skipping HAP publish.'); this.setServerStatus("ok" /* ServerStatus.OK */); } } async teardown() { this.bridgeService.teardown(); // Teardown Matter servers (main bridge and external accessories) await this.matterManager?.teardown(); this.ipcService.stop(); this.setServerStatus("down" /* ServerStatus.DOWN */); } publishBridge() { this.bridgeService.publishBridge(); this.printSetupInfo(this.config.bridge.pin); } /** * Handle Matter command trigger from IPC (for UI control) * This is called by IPC handlers, not API events */ async handleTriggerMatterCommand(uuid, cluster, attributes, partId) { if (!this.matterManager) { throw new Error('Matter manager not initialized'); } await this.matterManager.handleTriggerCommand(uuid, cluster, attributes, partId); } /** * Whether HAP should be published for the given bridge configuration. * HAP is on by default; users opt out via `bridge.hap: false`. */ static isHapEnabled(bridgeConfig) { return bridgeConfig.hap !== false; } /** * Whether Matter is configured for the given bridge. * Matter is opt-in: a `bridge.matter` block must be present. */ static isMatterEnabledForBridge(bridgeConfig) { return !!bridgeConfig.matter; } static loadConfig() { // Look for the configuration file const configPath = User.configPath(); const defaultBridge = { name: 'Homebridge', username: 'CC:22:3D:E3:CE:30', pin: '031-45-154', }; if (!existsSync(configPath)) { log.warn('config.json (%s) not found.', configPath); return { bridge: defaultBridge, accessories: [], platforms: [], }; } let config; try { config = JSON.parse(readFileSync(configPath, { encoding: 'utf8' })); } catch (error) { log.error('There was a problem reading your config.json file.'); log.error('Please try pasting your config.json file here to validate it: https://jsonlint.com'); log.error(''); throw error; } if (config.ports !== undefined) { if (config.ports.start && config.ports.end) { if (config.ports.start > config.ports.end) { log.error('Invalid port pool configuration. End should be greater than or equal to start.'); config.ports = undefined; } } else { log.error('Invalid configuration for \'ports\'. Missing \'start\' and \'end\' properties! Ignoring it!'); config.ports = undefined; } } const bridge = config.bridge || defaultBridge; bridge.name = bridge.name || defaultBridge.name; bridge.username = bridge.username || defaultBridge.username; bridge.pin = bridge.pin || defaultBridge.pin; config.bridge = bridge; // Protocol-enablement validation: at least one of HAP or Matter must be on. // HAP is enabled by default; users opt out via `bridge.hap: false`. // Matter is enabled when `bridge.matter` is configured. if (!Server.isHapEnabled(config.bridge) && !Server.isMatterEnabledForBridge(config.bridge)) { throw new Error('At least one protocol (HAP or Matter) must be enabled. ' + 'Set `bridge.hap` to true or add a `bridge.matter` configuration.'); } // Validate Matter port pool configuration. Must run after bridge defaults // are filled in, since the cast to HomebridgeConfig only becomes honest at // that point. MatterConfigCollector.validateMatterPortsPool(config); const username = config.bridge.username; if (!validMacAddress(username)) { throw new Error(`Not a valid username: ${username}. Must be 6 pairs of colon-separated hexadecimal chars (A-F 0-9), like a MAC address.`); } config.accessories = config.accessories || []; config.platforms = config.platforms || []; if (!Array.isArray(config.accessories)) { log.error('Value provided for accessories must be an array[]'); config.accessories = []; } if (!Array.isArray(config.platforms)) { log.error('Value provided for platforms must be an array[]'); config.platforms = []; } log.info('Loaded config.json with %s accessories and %s platforms.', config.accessories.length, config.platforms.length); if (config.bridge.advertiser) { if (![ "bonjour-hap" /* MDNSAdvertiser.BONJOUR */, "ciao" /* MDNSAdvertiser.CIAO */, "avahi" /* MDNSAdvertiser.AVAHI */, "resolved" /* MDNSAdvertiser.RESOLVED */, ].includes(config.bridge.advertiser)) { config.bridge.advertiser = undefined; log.error('Value provided in bridge.advertiser is not valid, reverting to platform default.'); } } else { config.bridge.advertiser = undefined; } return config; } loadAccessories() { log.info(`Loading ${this.config.accessories.length} accessories...`); this.config.accessories.forEach((accessoryConfig, index) => { if (!accessoryConfig.accessory) { log.warn('Your config.json contains an illegal accessory configuration object at position %d. ' + 'Missing property \'accessory\'. Skipping entry...', index + 1); // we rather count from 1 for the normal people? return; } const accessoryIdentifier = accessoryConfig.accessory; const displayName = accessoryConfig.name; if (!displayName) { log.warn('Could not load accessory %s at position %d as it is missing the required \'name\' property!', accessoryIdentifier, index + 1); return; } let plugin; let constructor; try { plugin = this.pluginManager.getPluginForAccessory(accessoryIdentifier); } catch (error) { log.error(error.message); return; } // check the plugin is not disabled if (plugin.disabled) { log.warn(`Ignoring config for the accessory "${accessoryIdentifier}" in your config.json as the plugin "${plugin.getPluginIdentifier()}" has been disabled.`); return; } try { constructor = plugin.getAccessoryConstructor(accessoryIdentifier); } catch (error) { log.error(`Error loading the accessory "${accessoryIdentifier}" requested in your config.json at position ${index + 1} - this is likely an issue with the "${plugin.getPluginIdentifier()}" plugin.`); log.error(error); // error message contains more information and full stack trace return; } const logger = Logger.withPrefix(displayName); logger('Initializing %s accessory...', accessoryIdentifier); if (accessoryConfig._bridge) { // ensure the username is always uppercase accessoryConfig._bridge.username = accessoryConfig._bridge.username.toUpperCase(); try { this.validateChildBridgeConfig("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, accessoryConfig._bridge); } catch (error) { log.error(error.message); return; } let childBridge; if (this.childBridges.has(accessoryConfig._bridge.username)) { childBridge = this.childBridges.get(accessoryConfig._bridge.username); logger(`Adding to existing child bridge ${accessoryConfig._bridge.username}`); } else { logger(`Initializing child bridge ${accessoryConfig._bridge.username}`); childBridge = new ChildBridgeService("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, plugin, accessoryConfig._bridge, this.config, this.options, this.api, this.ipcService, this.externalPortService); // Set callback for external Matter bridge registration childBridge.onExternalBridgeRegistered = this.registerExternalMatterBridge.bind(this); this.childBridges.set(accessoryConfig._bridge.username, childBridge); } // add config to child bridge service childBridge.addConfig(accessoryConfig); return; } const accessoryInstance = new constructor(logger, accessoryConfig, this.api); // pass accessoryIdentifier for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation const accessory = this.bridgeService.createHAPAccessory(plugin, accessoryInstance, displayName, accessoryIdentifier, accessoryConfig.uuid_base); if (accessory) { try { this.bridgeService.bridge.addBridgedAccessory(accessory); } catch (error) { logger.error(`Error loading the accessory "${accessoryIdentifier}" from "${plugin.getPluginIdentifier()}" requested in your config.json:`, error.message); } } else { logger.info('Accessory %s returned empty set of services; not adding it to the bridge.', accessoryIdentifier); } }); } loadPlatforms() { log.info(`Loading ${this.config.platforms.length} platforms...`); const promises = []; this.config.platforms.forEach((platformConfig, index) => { if (!platformConfig.platform) { log.warn('Your config.json contains an illegal platform configuration object at position %d. ' + 'Missing property \'platform\'. Skipping entry...', index + 1); // we rather count from 1 for the normal people? return; } const platformIdentifier = platformConfig.platform; const displayName = platformConfig.name || platformIdentifier; let plugin; let constructor; // do not load homebridge-config-ui-x when running in service mode if (platformIdentifier === 'config' && process.env.UIX_SERVICE_MODE === '1') { return; } try { plugin = this.pluginManager.getPluginForPlatform(platformIdentifier); } catch (error) { log.error(error.message); return; } // check the plugin is not disabled if (plugin.disabled) { log.warn(`Ignoring config for the platform "${platformIdentifier}" in your config.json as the plugin "${plugin.getPluginIdentifier()}" has been disabled.`); return; } try { constructor = plugin.getPlatformConstructor(platformIdentifier); } catch (error) { log.error(`Error loading the platform "${platformIdentifier}" requested in your config.json at position ${index + 1} - this is likely an issue with the "${plugin.getPluginIdentifier()}" plugin.`); log.error(error); // error message contains more information and full stack trace return; } const logger = Logger.withPrefix(displayName); logger('Initializing %s platform...', platformIdentifier); if (platformConfig._bridge) { // ensure the username is always uppercase platformConfig._bridge.username = platformConfig._bridge.username.toUpperCase(); try { this.validateChildBridgeConfig("platform" /* PluginType.PLATFORM */, platformIdentifier, platformConfig._bridge); } catch (error) { log.error(error.message); return; } logger(`Initializing child bridge ${platformConfig._bridge.username}`); const childBridge = new ChildBridgeService("platform" /* PluginType.PLATFORM */, platformIdentifier, plugin, platformConfig._bridge, this.config, this.options, this.api, this.ipcService, this.externalPortService); // Set callback for external Matter bridge registration childBridge.onExternalBridgeRegistered = this.registerExternalMatterBridge.bind(this); this.childBridges.set(platformConfig._bridge.username, childBridge); // add config to child bridge service childBridge.addConfig(platformConfig); return; } const platform = new constructor(logger, platformConfig, this.api); if (HomebridgeAPI.isDynamicPlatformPlugin(platform)) { plugin.assignDynamicPlatform(platformIdentifier, platform); } else if (HomebridgeAPI.isStaticPlatformPlugin(platform)) { // Plugin 1.0, load accessories promises.push(this.bridgeService.loadPlatformAccessories(plugin, platform, platformIdentifier, logger)); } else { // otherwise it's a IndependentPlatformPlugin which doesn't expose any methods at all. // We just call the constructor and let it be enabled. } }); return promises; } /** * Validate an external bridge config */ validateChildBridgeConfig(type, identifier, bridgeConfig) { // All child bridges require username if (!bridgeConfig.username) { throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - ` + 'Missing required field "_bridge.username".'); } // At least one of HAP or Matter must be enabled per child bridge. // Note: Matter is unsupported on accessory-style child bridges (warned about // in childBridgeFork.ts), so for ACCESSORY child bridges only HAP counts. const hapOk = Server.isHapEnabled(bridgeConfig); const matterOk = type === "platform" /* PluginType.PLATFORM */ && Server.isMatterEnabledForBridge(bridgeConfig); if (!hapOk && !matterOk) { throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - ` + 'at least one protocol must be enabled on this child bridge. ' + 'Set `_bridge.hap` to true or add a `_bridge.matter` configuration.'); } if (!validMacAddress(bridgeConfig.username)) { throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - ` + `not a valid username in _bridge.username: "${bridgeConfig.username}". Must be 6 pairs of colon-separated hexadecimal chars (A-F 0-9), like a MAC address.`); } if (this.childBridges.has(bridgeConfig.username)) { const childBridge = this.childBridges.get(bridgeConfig.username); if (type === "platform" /* PluginType.PLATFORM */) { // only a single platform can exist on one child bridge throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - ` + `Duplicate username found in _bridge.username: "${bridgeConfig.username}". Each platform child bridge must have it's own unique username.`); } else if (childBridge?.identifier !== identifier) { // only accessories of the same type can be added to the same child bridge throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - ` + `Duplicate username found in _bridge.username: "${bridgeConfig.username}". You can only group accessories of the same type in a child bridge.`); } } if (bridgeConfig.username === this.config.bridge.username.toUpperCase()) { throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - ` + `Username found in _bridge.username: "${bridgeConfig.username}" is the same as the main bridge. Each child bridge platform/accessory must have it's own unique username.`); } } /** * Takes care of the IPC Events sent to Homebridge */ initializeIpcEventHandlers() { // start ipc service this.ipcService.start(); // handle restart child bridge event this.ipcService.on("restartChildBridge" /* IpcIncomingEvent.RESTART_CHILD_BRIDGE */, (username) => { // noinspection SuspiciousTypeOfGuard if (typeof username === 'string') { const childBridge = this.childBridges.get(username.toUpperCase()); childBridge?.restartChildBridge(); } }); // handle stop child bridge event this.ipcService.on("stopChildBridge" /* IpcIncomingEvent.STOP_CHILD_BRIDGE */, (username) => { // noinspection SuspiciousTypeOfGuard if (typeof username === 'string') { const childBridge = this.childBridges.get(username.toUpperCase()); childBridge?.stopChildBridge(); } }); // handle start child bridge event this.ipcService.on("startChildBridge" /* IpcIncomingEvent.START_CHILD_BRIDGE */, (username) => { // noinspection SuspiciousTypeOfGuard if (typeof username === 'string') { const childBridge = this.childBridges.get(username.toUpperCase()); childBridge?.startChildBridge(); } }); this.ipcService.on("childBridgeMetadataRequest" /* IpcIncomingEvent.CHILD_BRIDGE_METADATA_REQUEST */, () => { this.ipcService.sendMessage("childBridgeMetadataResponse" /* IpcOutgoingEvent.CHILD_BRIDGE_METADATA_RESPONSE */, Array.from(this.childBridges.values(), x => x.getMetadata())); }); // Matter monitoring lifecycle handlers this.ipcService.on("startMatterMonitoring" /* IpcIncomingEvent.START_MATTER_MONITORING */, () => { this.handleStartMatterMonitoring(); }); this.ipcService.on("stopMatterMonitoring" /* IpcIncomingEvent.STOP_MATTER_MONITORING */, () => { this.handleStopMatterMonitoring(); }); this.ipcService.on("getMatterAccessories" /* IpcIncomingEvent.GET_MATTER_ACCESSORIES */, (data) => { void this.handleGetMatterAccessories(data?.bridgeUsername); }); this.ipcService.on("getMatterAccessoryInfo" /* IpcIncomingEvent.GET_MATTER_ACCESSORY_INFO */, (data) => { this.handleGetMatterAccessoryInfo(data?.uuid); }); this.ipcService.on("matterAccessoryControl" /* IpcIncomingEvent.MATTER_ACCESSORY_CONTROL */, (data) => { void this.handleMatterAccessoryControl(data); }); } /** * Handle start Matter monitoring request from UI * Only starts monitoring if this is the first client */ handleStartMatterMonitoring() { this.matterMonitoringClients++; // Only setup monitoring if this is the first client if (this.matterMonitoringClients === 1) { this.matterMonitoringActive = true; // Enable monitoring on main bridge Matter servers this.matterManager?.enableStateMonitoring(); // Enable monitoring on all child bridges for (const childBridge of this.childBridges.values()) { childBridge.startMatterMonitoring(); } const event = { type: 'monitoringStarted', data: { success: true }, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); } else { // Already monitoring, just acknowledge const event = { type: 'monitoringStarted', data: { success: true, alreadyActive: true }, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); } } /** * Handle stop Matter monitoring request from UI * Only stops monitoring when no more clients */ handleStopMatterMonitoring() { if (this.matterMonitoringClients <= 0) { return; } this.matterMonitoringClients--; // Only stop monitoring when no more clients if (this.matterMonitoringClients === 0) { this.matterMonitoringActive = false; // Disable monitoring on main bridge Matter servers this.matterManager?.disableStateMonitoring(); // Disable monitoring on all child bridges for (const childBridge of this.childBridges.values()) { childBridge.stopMatterMonitoring(); } const event = { type: 'monitoringStopped', data: { success: true }, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); } else { // Other clients still monitoring const event = { type: 'monitoringStopped', data: { success: true, othersActive: true }, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); } } /** * Register an external Matter bridge (e.g., robot vacuum with own bridge) * This allows routing control commands directly to the correct owner * @param externalBridgeUsername - Username of the external Matter bridge * @param ownerUsername - Username of the bridge that owns it (main bridge or child bridge username) */ registerExternalMatterBridge(externalBridgeUsername, ownerUsername) { const normalizedExternal = externalBridgeUsername.toUpperCase(); const normalizedOwner = ownerUsername.toUpperCase(); matterLogger.debug(`Registering external Matter bridge ${normalizedExternal} → owner: ${normalizedOwner}`); this.externalMatterBridgeRegistry.set(normalizedExternal, normalizedOwner); } /** * Get Matter accessories for a specific bridge or all bridges * @param bridgeUsername - Optional: specific bridge username (MAC format) */ async handleGetMatterAccessories(bridgeUsername) { // Check if monitoring is active if (!this.matterMonitoringActive) { matterLogger.warn('Matter monitoring not active - cannot get accessories'); const event = { type: 'accessoriesData', data: { bridgeUsername, error: 'Matter monitoring not active', }, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); return; } // Check if Matter is enabled on main bridge if (!this.api.isMatterEnabled() && this.childBridges.size === 0) { const event = { type: 'accessoriesData', data: { bridgeUsername, accessories: [], }, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); return; } try { // Get accessories from main bridge const allAccessories = this.matterManager?.collectAllAccessories(bridgeUsername) || []; // Request from child bridges and wait for responses (with timeout) if (this.childBridges.size > 0) { const results = await Promise.allSettled(Array.from(this.childBridges.values(), childBridge => childBridge.requestMatterAccessories())); for (const result of results) { if (result.status === 'fulfilled' && result.value?.accessories) { allAccessories.push(...result.value.accessories); } } } const event = { type: 'accessoriesData', data: { bridgeUsername: bridgeUsername || 'all', accessories: allAccessories, }, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); } catch (error) { matterLogger.error('Failed to get Matter accessories:', error); const event = { type: 'accessoriesData', data: { bridgeUsername, error: error instanceof Error ? error.message : 'Unknown error', }, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); } } /** * Get detailed info for a specific Matter accessory */ handleGetMatterAccessoryInfo(uuid) { if (!uuid) { const event = { type: 'accessoryInfoData', data: { error: 'UUID is required', }, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); return; } try { // Try to get from main bridge first const accessoryInfo = this.matterManager?.getAccessoryInfo(uuid); if (accessoryInfo) { const event = { type: 'accessoryInfoData', data: accessoryInfo, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); return; } // If not found in main bridge, forward to child bridges with Matter enabled. // Child bridges will respond directly if they have the accessory for (const childBridge of this.childBridges.values()) { // Only forward to bridges with Matter enabled if (childBridge.getMetadata().matterConfig) { childBridge.getMatterAccessoryInfo(uuid); } } // If no child bridge responds, we'll send error after a timeout // For now, assume child bridges will handle it } catch (error) { matterLogger.error('Failed to get Matter accessory info:', error); const event = { type: 'accessoryInfoData', data: { error: error instanceof Error ? error.message : 'Unknown error', }, }; this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event); } } /** * Handle Matter accessory control command */ async handleMatterAccessoryControl(data) { matterLogger.debug(`Matter control request: uuid=${data?.uuid}, cluster=${data?.cluster}, bridge=${data?.bridgeUsername || 'auto'}, part=${data?.partId || 'main'}`); if (!data?.uuid || !data?.cluster || !data?.attributes) { matterLogger.error('Missing required parameters for Matter control'); this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, { type: 'accessoryControlResponse', data: { success: false, error: 'Missing required parameters', }, }); return; } // If bridge username is provided, route directly to that bridge if (data.bridgeUsername) { const targetUsername = data.bridgeUsername.toUpperCase(); // Check if it's the main bridge if (targetUsername === this.config.bridge.username.toUpperCase()) { matterLogger.debug(`Routing to main bridge (${targetUsername})`); try { await this.handleTriggerMatterCommand(data.uuid, data.cluster, data.attributes, data.partId); matterLogger.debug(`Main bridge successfully controlled accessory ${data.uuid}`); this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, { type: 'accessoryControlResponse', data: { success: true, uuid: data.uuid, }, }); } catch (error) { matterLogger.error(`Main bridge failed to control ${data.uuid}: ${error.message}`); this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, { type: 'accessoryControlResponse', data: { success: false, error: error.message, uuid: data.uuid, }, }); } return; } // Check if it's a specific child bridge for (const childBridge of this.childBridges.values()) { if (childBridge.getMetadata().username.toUpperCase() === targetUsername) { matterLogger.debug(`Routing to child bridge ${childBridge.identifier} (${targetUsername})`); childBridge.controlMatterAccessory(data); return; } } // Check if it's an external Matter bridge (e.g., robot vacuum with own bridge) // Use registry for efficient direct routing const ownerUsername = this.externalMatterBridgeRegistry.get(targetUsername); if (ownerUsername) { matterLogger.debug(`Found external bridge ${targetUsername} in registry, owned by ${ownerUsername}`); if (ownerUsername === this.config.bridge.username.toUpperCase()) { // External accessory on main bridge matterLogger.debug(`Routing to main bridge's external accessories for ${data.uuid}`); try { await this.handleTriggerMatterCommand(data.uuid, data.cluster, data.attributes, data.partId); matterLogger.debug(`External accessory ${data.uuid} successfully controlled via main bridge`); this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, { type: 'accessoryControlResponse', data: { success: true, uuid: data.uuid, }, }); } catch (error) { matterLogger.error(`Main bridge failed to control external accessory ${data.uuid}: ${error.message}`); this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, { type: 'accessoryControlResponse', data: { success: false, error: error.message, uuid: data.uuid, }, }); } } else { // External accessory on child bridge - lookup by username const childBridge = this.childBridges.get(ownerUsername); if (childBridge) { matterLogger.debug(`Routing to child bridge ${childBridge.identifier} (${ownerUsername}) for external accessory ${data.uuid}`); childBridge.controlMatterAccessory(data); } else { matterLogger.error(`Owner bridge ${ownerUsername} not found for external bridge ${targetUsername}`); this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, { type: 'accessoryControlResponse', data: { success: false, error: `Owner bridge ${ownerUsername} not found`, uuid: data.uuid, }, }); } } return; } // Bridge username provided but not found anywhere // With registry, we should always be able to find the bridge if the data is correct matterLogger.error(`Bridge ${targetUsername} not found in main/child bridges or registry`); this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, { type: 'accessoryControlResponse', data: { success: false, error: `Bridge ${targetUsername} not found`, uuid: data.uuid, }, }); return; } // No bridge username provided - broadcast mode (try main, then all children) matterLogger.debug(`Broadcast mode: trying main bridge for accessory ${data.uuid}`); try { await this.handleTriggerMatterCommand(data.uuid, data.cluster, data.attributes, data.partId); matterLogger.debug(`Main bridge successfully controlled accessory ${data.uuid}`); this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, { type: 'accessoryControlResponse', data: { success: true, uuid: data.uuid, }, }); } catch (error) { // Main bridge doesn't have accessory - forward to child bridges with Matter enabled const matterChildBridges = [...this.childBridges.values()].filter(bridge => bridge.getMetadata().matterConfig); if (matterChildBridges.length > 0) { matterLogger.debug(`Main bridge doesn't have accessory ${data.uuid}, forwarding to ${matterChildBridges.length} child bridge(s) with Matter enabled`); for (const childBridge of matterChildBridges) { childBridge.controlMatterAccessory(data); } } else { matterLogger.warn(`Accessory ${data.uuid} not found - not on main bridge and no child bridges with Matter available`); this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, { type: 'accessoryControlResponse', data: { success: false, error: 'Accessory not found', uuid: data.uuid, }, }); } } } printSetupInfo(pin) { /* eslint-disable no-console */ console.log('Setup Payload:'); console.log(this.bridgeService.bridge.setupURI()); if (!this.options.hideQRCode) { console.log('Scan this code with your HomeKit app on your iOS device to pair with Homebridge:'); qrcode.setErrorLevel('M'); // HAP specifies level M or higher for ECC qrcode.generate(this.bridgeService.bridge.setupURI()); console.log('Or enter this code with your HomeKit app on your iOS device to pair with Homebridge:'); } else { console.log('Enter this code with your HomeKit app on your iOS device to pair with Homebridge:'); } console.log(chalk.black.bgWhite(' ')); console.log(chalk.black.bgWhite(' ┌────────────┐ ')); console.log(chalk.black.bgWhite(` │ ${pin} │ `)); console.log(chalk.black.bgWhite(' └────────────┘ ')); console.log(chalk.black.bgWhite(' ')); /* eslint-enable no-console */ } } //# sourceMappingURL=server.js.map