UNPKG

homebridge

Version:
258 lines 12.8 kB
/** * Lightweight Matter Configuration Utilities * * This module provides config collection and validation without importing * heavy Matter.js libraries. This ensures fast startup for users without * Matter configured. */ import { Logger } from '../logger.js'; const log = Logger.withPrefix('Matter/Config'); /** * Whether a Matter config block represents an *enabled* Matter setup. * The block being present means Matter is configured; `enabled: false` means * it is configured but intentionally turned off (storage + port preserved so * it can be re-enabled without re-commissioning). Missing `enabled` = enabled, * which keeps every pre-existing config working unchanged. * * Note: this does NOT distinguish "the main matter server should start" from * "the matter API surface should be exposed to plugins". Use * `shouldStartMatterServer` for the former — it additionally accounts for * externalsOnly mode, where the API is exposed but the bridge server is not * started. */ export function isMatterConfigEnabled(matter) { return !!matter && matter.enabled !== false; } /** * Whether the main Matter server should start for this bridge — i.e. Matter * is configured, enabled, and NOT in externalsOnly mode. In externalsOnly * mode the API is still loaded (so plugins can publish externals via their * own per-accessory MatterServer instances) but the bridge aggregator does * not come up. */ export function shouldStartMatterServer(matter) { return isMatterConfigEnabled(matter) && !matter?.externalsOnly; } /** * Whether Matter is "active" for this bridge in any form — either fully on, * or in externalsOnly mode (api.matter loaded, manager listening for external * publish events, main bridge server NOT started). Returns false only when * matter is missing or fully disabled (`enabled: false` without `externalsOnly`). * * Use this for gates like "should api.matter be loaded?" and "should the * matter manager be constructed?". Use `shouldStartMatterServer` for the * tighter gate of "should the bridge aggregator come up?". */ export function isMatterActive(matter) { return !!matter && (matter.enabled !== false || matter.externalsOnly === true); } /** * Normalise the coherence of `matter.externalsOnly` for a single bridge block. * * `externalsOnly: true` is meant to be paired with `enabled: false` (the two * flags together confirm "bridge node off, externals still publish"). If a * config sets `externalsOnly: true` on its own we honour the unambiguous * intent rather than failing the whole process — we warn and set * `enabled: false` in place so the block matches the canonical externalsOnly * form every downstream check expects. This mirrors the log-and-continue * behaviour of the port validators rather than taking the whole instance down * over one stray flag. * * For accessory child bridges, callers should use * `stripMatterExternalsOnlyForAccessory` instead — externals are not * supported on accessory plugins, so the field is dropped with a warning * before this check runs. * * Mutates the passed matter block in place. */ export function validateMatterExternalsOnly(matter, bridgeLabel) { if (matter.externalsOnly === true && matter.enabled !== false) { log.warn(`${bridgeLabel}: 'matter.externalsOnly: true' was set without 'matter.enabled: false'. Proceeding in externalsOnly mode (the bridge node will not start). Set 'matter.enabled: false' to confirm intent and silence this warning.`); matter.enabled = false; } } /** * Strip `matter.externalsOnly` (if set) from an accessory child bridge's * matter block, logging a warning. Externals are not supported via the * accessory plugin API, so the flag is meaningless there — mirrors the * accessory-side behaviour of `hap.externalsOnly`. * * Mutates the passed matter block in place. */ export function stripMatterExternalsOnlyForAccessory(matter, bridgeLabel) { if (matter.externalsOnly === true) { log.warn(`${bridgeLabel}: 'matter.externalsOnly' is not supported on accessory child bridges. Ignoring.`); delete matter.externalsOnly; } } /** * Lightweight config collector that doesn't require Matter.js imports */ export class MatterConfigCollector { /** * Check if any Matter configuration exists in the config */ static hasMatterConfig(config) { // Use isMatterActive so externalsOnly bridges are considered configured — // they still need api.matter loaded and the manager set up to attach // their external-publish listeners. return (isMatterActive(config.bridge.matter) || config.platforms.some((p) => isMatterActive(p._bridge?.matter)) || config.accessories.some((a) => isMatterActive(a._bridge?.matter))); } /** * Collect all configured Matter ports from config to avoid conflicts. * * Ports are collected from every bridge block that declares one, regardless * of `enabled`/`externalsOnly` state. This is deliberate: a disabled-in-place * bridge (`enabled: false`) keeps its configured port reserved so that * re-enabling it later reuses the same port (no re-commissioning) and the * allocator never hands that port to an automatic Matter/external allocation * in the meantime. The trade-off is that a disabled bridge's port stays * unavailable for auto-allocation even though no server currently binds it — * that is the cost of the "port preserved" contract on `enabled`. */ static collectConfiguredMatterPorts(config) { const configuredMatterPorts = []; if (config.bridge.matter?.port) { configuredMatterPorts.push(config.bridge.matter.port); } for (const platform of config.platforms) { if (platform._bridge?.matter?.port) { configuredMatterPorts.push(platform._bridge.matter.port); } } for (const accessory of config.accessories) { if (accessory._bridge?.matter?.port) { configuredMatterPorts.push(accessory._bridge.matter.port); } } return configuredMatterPorts; } /** * Validate the matterPorts pool configuration * Ensures start and end are defined and start <= end * * @param config - The Homebridge configuration */ static validateMatterPortsPool(config) { if (config.matterPorts !== undefined) { if (config.matterPorts.start && config.matterPorts.end) { if (config.matterPorts.start > config.matterPorts.end) { log.error('Invalid Matter port pool configuration. End should be greater than or equal to start.'); config.matterPorts = undefined; } } else { log.error('Invalid configuration for \'matterPorts\'. Missing \'start\' and \'end\' properties! Ignoring it!'); config.matterPorts = undefined; } } } /** * Validate Matter configuration (lazy-loads validator only when needed) * This function dynamically imports the full validator to avoid loading it * when Matter is not configured. */ static async validateMatterConfig(config) { // Only validate if Matter config exists if (!this.hasMatterConfig(config)) { return; } // externalsOnly coherence checks run first so misconfigurations throw with // a clear message before downstream validators get the chance to silently // strip the matter block on unrelated errors. Accessory child bridges have // externalsOnly stripped with a warning here too (mirrors hap.externalsOnly). if (config.bridge.matter) { validateMatterExternalsOnly(config.bridge.matter, 'main bridge'); } for (const platform of config.platforms) { if (platform._bridge?.matter) { validateMatterExternalsOnly(platform._bridge.matter, `platform "${platform.platform}" child bridge`); } } for (const accessory of config.accessories) { if (accessory._bridge?.matter) { stripMatterExternalsOnlyForAccessory(accessory._bridge.matter, `accessory "${accessory.accessory}" child bridge`); } } // Lazy-load the full validator (which has heavier dependencies) const { MatterConfigValidator } = await import('./configValidator.js'); // Validate the main bridge Matter config only when it will actually start a // server. A disabled (`enabled: false`) or externalsOnly main bridge is // preserved as-is (disabled-in-place) — mirroring the child validator — // since stripping it would lose config/storage the user expects to survive // so it can be re-enabled without re-commissioning. if (config.bridge.matter && shouldStartMatterServer(config.bridge.matter)) { const validation = MatterConfigValidator.validate(config.bridge.matter); if (!validation.isValid) { log.error('Main bridge Matter configuration is invalid. Matter will not be enabled for the main bridge.'); delete config.bridge.matter; } } // Reserve the main bridge's Matter port so the child validator catches // child↔main port collisions in the same pass — but only when the main // Matter server will actually bind it. A disabled or externalsOnly main // bridge never starts its bridge server, so its configured port must not // block a child bridge from legitimately using the same number. const reserved = new Set(); if (shouldStartMatterServer(config.bridge.matter) && config.bridge.matter?.port) { reserved.add(config.bridge.matter.port); } // Validate all child bridge Matter configs and check for port conflicts const childMatterValidation = MatterConfigValidator.validateAllChildMatterConfigs(config.platforms, config.accessories, reserved); if (!childMatterValidation.isValid) { log.error('Some child bridge Matter configurations were invalid and have been disabled. The remaining configuration will start as normal.'); // Surface the specific per-child errors (which platform/accessory and // which port) so the user knows what to fix. Previously these details // were collected into the result but never logged, leaving only the // generic line above — the user couldn't tell what had been disabled. for (const error of childMatterValidation.errors) { log.error(error); } } // Surface any non-fatal child Matter warnings too — also collected by the // validator but not previously logged by this caller. for (const warning of childMatterValidation.warnings) { log.warn(warning); } // Check for conflicts between main bridge Matter port and child bridge // ports — again only when the main server will actually bind its port, // so a disabled/externalsOnly main doesn't raise a spurious conflict. if (shouldStartMatterServer(config.bridge.matter) && config.bridge.matter?.port) { this.checkPortConflicts(config); } } /** * Check for port conflicts between main bridge and child bridges * * @param config - The Homebridge configuration */ static checkPortConflicts(config) { const mainMatterPort = config.bridge.matter?.port; if (!mainMatterPort) { return; } // Collect all child bridge Matter ports const childMatterPorts = []; for (const platform of config.platforms) { if (platform._bridge?.matter?.port) { childMatterPorts.push(platform._bridge.matter.port); } } for (const accessory of config.accessories) { if (accessory._bridge?.matter?.port) { childMatterPorts.push(accessory._bridge.matter.port); } } // Check for conflicts with child bridge Matter ports if (childMatterPorts.includes(mainMatterPort)) { log.error(`Main bridge Matter port ${mainMatterPort} conflicts with a child bridge Matter port. Please use unique ports.`); } // Check for conflict with main bridge HAP port if (config.bridge.port && Math.abs(config.bridge.port - mainMatterPort) < 10) { log.warn(`Main bridge HAP port ${config.bridge.port} and Matter port ${mainMatterPort} are very close. Consider spacing them further apart.`); } } } //# sourceMappingURL=config.js.map