UNPKG

homebridge

Version:
1,037 lines 54.6 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, isHapConfigEnabled, isHapExternalsOnly, validateHapConfig } from './bridgeService.js';
import { ChildBridgeService } from './childBridgeService.js';
import { ExternalPortService } from './externalPortService.js';
import { IpcService } from './ipcService.js';
import { Logger } from './logger.js';
import { isMatterActive, isMatterConfigEnabled, MatterConfigCollector } from './matter/config.js';
import { PluginManager } from './pluginManager.js';
import { User } from './user.js';
import { validMacAddress } from './util/mac.js';
// HAP specifies QR error-correction level M or higher for ECC. Set once at
// module load — qrcode-terminal stores this on the (process-global) module,
// so re-setting it on every printSetupInfo call was redundant.
qrcode.setErrorLevel('M');
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;
    // Fallback timers for child-bridge Matter accessory lookups. Keyed by uuid
    // so that a child's accessoryInfoData (success or error) can cancel the
    // timer before it fires a spurious "Timed out" event at the UI.
    pendingMatterAccessoryInfoLookups = new Map();
    // The same fallback, for control requests broadcast to the child bridges.
    // Keyed by correlationId: a child that does not own the accessory stays
    // silent by design (every matter child is asked, only the owner answers), so
    // without this a control for an unknown uuid produced no response at all.
    pendingMatterControlRequests = new Map();
    // 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',
            externalAccessoriesItemName: 'externalAccessories',
        };
        // 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 (or externalsOnly mode is set). The bridge ADVERTISED
            // listener won't fire for the bridge itself, so move server status to OK
            // explicitly. Matter may or may not be up — if both protocols are
            // suppressed the bridge simply advertises nothing of its own.
            if (isHapExternalsOnly(this.config.bridge.hap)) {
                log.info('HAP externalsOnly mode for the main bridge; bridge accessory will not publish but external accessories will.');
            }
            else {
                log.info('HAP is disabled for the main bridge (bridge.hap.enabled=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();
        // Cancel any in-flight Matter accessory info fallback timers so they
        // don't fire `accessoryInfoData` events at the IPC channel after the
        // service has stopped. The timers are already unref()'d so they don't
        // hold the loop open — this is for tidiness, not a real leak.
        for (const timer of this.pendingMatterAccessoryInfoLookups.values()) {
            clearTimeout(timer);
        }
        this.pendingMatterAccessoryInfoLookups.clear();
        for (const timer of this.pendingMatterControlRequests.values()) {
            clearTimeout(timer);
        }
        this.pendingMatterControlRequests.clear();
        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.enabled: false`.
     * In externalsOnly mode the bridge accessory itself is not published, so
     * this returns false there too — externals are handled separately by
     * BridgeService.
     */
    static isHapEnabled(bridgeConfig) {
        return isHapConfigEnabled(bridgeConfig.hap) && !isHapExternalsOnly(bridgeConfig.hap);
    }
    /**
     * Whether Matter is enabled for the given bridge.
     * Matter is opt-in: a `bridge.matter` block must be present and not
     * explicitly disabled via `bridge.matter.enabled: false`.
     */
    static isMatterEnabledForBridge(bridgeConfig) {
        return isMatterConfigEnabled(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;
        // 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);
        // Normalise the main bridge username to uppercase so downstream comparisons
        // (validMacAddress, registry lookups, child-bridge dedup) stay case-consistent.
        // Guarded so a malformed (non-string) value falls through to `validMacAddress`
        // below and produces the proper "Not a valid username" error rather than a
        // raw TypeError from calling toUpperCase on a number/boolean.
        if (typeof config.bridge.username === 'string') {
            config.bridge.username = config.bridge.username.toUpperCase();
        }
        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.`);
        }
        // Validate the main bridge HAP config (shape + externalsOnly/enabled coherence).
        validateHapConfig(config.bridge, { bridgeLabel: 'main bridge' });
        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);
                    // Cancel the parent-side fallback timer when this child answers a lookup
                    childBridge.onAccessoryInfoResponse = this.cancelPendingMatterAccessoryInfoLookup.bind(this);
                    childBridge.onAccessoryControlResponse = this.cancelPendingMatterControlRequest.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);
                // Cancel the parent-side fallback timer when this child answers a lookup
                childBridge.onAccessoryInfoResponse = this.cancelPendingMatterAccessoryInfoLookup.bind(this);
                childBridge.onAccessoryControlResponse = this.cancelPendingMatterControlRequest.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".');
        }
        // Normalise the child username to uppercase, mirroring the main bridge
        // (loadConfig). validMacAddress only accepts A-F, so without this a lowercase
        // MAC in _bridge.username would be rejected here even though the identical
        // value is accepted on the main bridge. Guarded so a non-string value still
        // falls through to the proper "not a valid username" error below.
        if (typeof bridgeConfig.username === 'string') {
            bridgeConfig.username = bridgeConfig.username.toUpperCase();
        }
        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.`);
            }
        }
        // Both usernames are normalised to uppercase (main in loadConfig, child
        // above), so a direct comparison is case-consistent.
        if (bridgeConfig.username === this.config.bridge.username) {
            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.`);
        }
        // Validate the child bridge HAP config (shape + externalsOnly/enabled coherence).
        // For accessory child bridges, `hap.externalsOnly` is stripped with a warning
        // since externals are not supported via the accessory plugin API.
        validateHapConfig(bridgeConfig, {
            bridgeLabel: `${type} "${identifier}" child bridge`,
            isAccessoryPlugin: type === "accessory" /* PluginType.ACCESSORY */,
        });
    }
    /**
     * 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 */, (data) => {
            this.handleStartMatterMonitoring(data);
        });
        this.ipcService.on("stopMatterMonitoring" /* IpcIncomingEvent.STOP_MATTER_MONITORING */, (data) => {
            this.handleStopMatterMonitoring(data);
        });
        this.ipcService.on("getMatterAccessories" /* IpcIncomingEvent.GET_MATTER_ACCESSORIES */, (data) => {
            void this.handleGetMatterAccessories(data);
        });
        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.
     *
     * The UI parks each `startMatterMonitoring` request under a `correlationId`
     * so it can route the ack back to the matching waiter and gate its first
     * `getMatterAccessories` on it; echo it on the reply so the UI's dispatcher
     * (which drops events without a correlationId) can deliver it.
     */
    handleStartMatterMonitoring(data) {
        const correlationId = data?.correlationId;
        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',
                correlationId,
                data: { success: true },
            };
            this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event);
        }
        else {
            // Already monitoring, just acknowledge
            const event = {
                type: 'monitoringStarted',
                correlationId,
                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.
     *
     * Echo the request's `correlationId` for the same reason as
     * `handleStartMatterMonitoring`.
     */
    handleStopMatterMonitoring(data) {
        const correlationId = data?.correlationId;
        if (this.matterMonitoringClients <= 0) {
            // Nothing to do, but still acknowledge so the UI doesn't sit waiting
            // for a confirmation event that never comes.
            const event = {
                type: 'monitoringStopped',
                correlationId,
                data: { success: true, alreadyStopped: true },
            };
            this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event);
            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',
                correlationId,
                data: { success: true },
            };
            this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event);
        }
        else {
            // Other clients still monitoring
            const event = {
                type: 'monitoringStopped',
                correlationId,
                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);
    }
    /**
     * Cancel the pending fallback timer for a forwarded Matter accessory lookup.
     * Called by ChildBridgeService when a child responds with accessoryInfoData
     * so the 2s "Timed out" event isn't sent after a successful response.
     */
    cancelPendingMatterAccessoryInfoLookup(uuid) {
        const timer = this.pendingMatterAccessoryInfoLookups.get(uuid);
        if (timer) {
            clearTimeout(timer);
            this.pendingMatterAccessoryInfoLookups.delete(uuid);
        }
    }
    /**
     * Cancel the fallback for a broadcast control request, because the child
     * that owns the accessory has answered it.
     */
    cancelPendingMatterControlRequest(correlationId) {
        const timer = this.pendingMatterControlRequests.get(correlationId);
        if (timer) {
            clearTimeout(timer);
            this.pendingMatterControlRequests.delete(correlationId);
        }
    }
    /**
     * Get Matter accessories for a specific bridge or all bridges.
     *
     * The UI parks each request under a `correlationId` and routes responses
     * back to the matching waiter; events without the original correlationId
     * are dropped, so every emitted `accessoriesData` event must echo it.
     */
    async handleGetMatterAccessories(data) {
        const bridgeUsername = data?.bridgeUsername;
        const correlationId = data?.correlationId;
        // Check if monitoring is active
        if (!this.matterMonitoringActive) {
            matterLogger.warn('Matter monitoring not active - cannot get accessories');
            const event = {
                type: 'accessoriesData',
                correlationId,
                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',
                correlationId,
                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',
                correlationId,
                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',
                correlationId,
                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 on main bridge, forward to child bridges whose Matter is
            // actually active. A child with `matter: { enabled: false }` still carries
            // a matterConfig block but never starts a Matter message handler, so it
            // would never answer — forwarding to it would only make the UI wait out
            // the 2s fallback instead of getting an immediate "not found". Gate on
            // isMatterActive (enabled or externalsOnly), which mirrors the condition
            // under which the child actually creates its Matter handler.
            // The matching child responds directly to the UI via the existing
            // MATTER_EVENT forwarding path; schedule a fallback error so the UI
            // doesn't hang if no child knows the UUID either.
            let forwardedToChildren = false;
            for (const childBridge of this.childBridges.values()) {
                if (isMatterActive(childBridge.getMetadata().matterConfig)) {
                    childBridge.getMatterAccessoryInfo(uuid);
                    forwardedToChildren = true;
                }
            }
            if (!forwardedToChildren) {
                this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, {
                    type: 'accessoryInfoData',
                    data: { error: `Accessory ${uuid} not found`, uuid },
                });
                return;
            }
            // 2s is comfortably longer than a healthy child response and short
            // enough that the UI doesn't feel stuck. Use unref() so a late
            // shutdown doesn't wait on this timer. The timer is registered in
            // pendingMatterAccessoryInfoLookups so a child's accessoryInfoData
            // response (routed via ChildBridgeService.onAccessoryInfoResponse) can
            // cancel it before it fires a spurious timed-out event. A second
            // concurrent request for the same uuid replaces the existing timer.
            const existing = this.pendingMatterAccessoryInfoLookups.get(uuid);
            if (existing) {
                clearTimeout(existing);
            }
            const fallback = setTimeout(() => {
                this.pendingMatterAccessoryInfoLookups.delete(uuid);
                this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, {
                    type: 'accessoryInfoData',
                    data: {
                        error: `Timed out looking up Matter accessory ${uuid}; it may not be registered.`,
                        uuid,
                        timedOut: true,
                    },
                });
            }, 2000);
            fallback.unref();
            this.pendingMatterAccessoryInfoLookups.set(uuid, fallback);
        }
        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
     */
    /**
     * Send an `accessoryControlResponse` back over IPC, echoing the request's
     * `correlationId` when one was given. The UI parks each control request
     * under its correlationId and its dispatcher only delivers events that
     * carry it — a response without one is dropped, so the request would sit
     * through a 10s timeout, be retried (running the command a second time),
     * and then be reported as failed even though the control succeeded.
     */
    sendMatterControlResponse(data, correlationId) {
        this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, {
            type: 'accessoryControlResponse',
            correlationId,
            data,
        });
    }
    async handleMatterAccessoryControl(data) {
        matterLogger.debug(`Matter control request: uuid=${data?.uuid}, cluster=${data?.cluster}, bridge=${data?.bridgeUsername || 'auto'}, part=${data?.partId || 'main'}`);
        const correlationId = data?.correlationId;
        if (!data?.uuid || !data?.cluster || !data?.attributes) {
            matterLogger.error('Missing required parameters for Matter control');
            this.sendMatterControlResponse({
                success: false,
                error: 'Missing required parameters',
            }, correlationId);
            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.sendMatterControlResponse({ success: true, uuid: data.uuid }, correlationId);
                }
                catch (error) {
                    matterLogger.error(`Main bridge failed to control ${data.uuid}: ${error.message}`);
                    this.sendMatterControlResponse({ success: false, error: error.message, uuid: data.uuid }, correlationId);
                }
                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.sendMatterControlResponse({ success: true, uuid: data.uuid }, correlationId);
                    }
                    catch (error) {
                        matterLogger.error(`Main bridge failed to control external accessory ${data.uuid}: ${error.message}`);
                        this.sendMatterControlResponse({ success: false, error: error.message, uuid: data.uuid }, correlationId);
                    }
                }
                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.sendMatterControlResponse({ success: false, error: `Owner bridge ${ownerUsername} not found`, uuid: data.uuid }, correlationId);
                    }
                }
                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.sendMatterControlResponse({ success: false, error: `Bridge ${targetUsername} not found`, uuid: data.uuid }, correlationId);
            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.sendMatterControlResponse({ success: true, uuid: data.uuid }, correlationId);
        }
        catch (error) {
            // Main bridge doesn't have accessory - forward to child bridges whose
            // Matter is actually active. A child with `matter: { enabled: false }`
            // still carries a matterConfig block but never starts a Matter handler,
            // so forwarding a control request to it would just be dropped. Gate on
            // isMatterActive (enabled or externalsOnly) — the same condition under
            // which the child creates its Matter handler.
            const matterChildBridges = [...this.childBridges.values()].filter(bridge => isMatterActive(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);
                }
                // Every Matter child is asked and only the owner answers - a child that
                // does not have the accessory returns silently, by design. So when no
                // child owns it nothing is sent back at all, and the UI sat through its
                // own 10s timeout, retried the command, then reported a failure. Mirror
                // the accessoryInfoData fallback: arm a short timer that the owner's
                // response cancels (via ChildBridgeService.onAccessoryControlResponse).
                //
                // Only meaningful with a correlationId — the UI drops any response
                // without one, so there would be nothing to rescue.
                if (correlationId) {
                    const existing = this.pendingMatterControlRequests.get(correlationId);
                    if (existing) {
                        clearTimeout(existing);
                    }
                    const fallback = setTimeout(() => {
                        this.pendingMatterControlRequests.delete(correlationId);
                        matterLogger.warn(`Accessory ${data.uuid} not found - no child bridge with Matter claimed it`);
                        this.sendMatterControlResponse({ success: false, error: 'Accessory not found', uuid: data.uuid }, correlationId);
                    }, 2000);
                    fallback.unref();
                    this.pendingMatterControlRequests.set(correlationId, fallback);
                }
            }
            else {
                matterLogger.warn(`Accessory ${data.uuid} not found - not on main bridge and no child bridges with Matter available`);
                this.sendMatterControlResponse({ success: false, error: 'Accessory not found', uuid: data.uuid }, correlationId);
            }
        }
    }
    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.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