homebridge
Version:
HomeKit support for the impatient
412 lines • 18.6 kB
JavaScript
/* global NodeJS */
import process from 'node:process';
import { HAPStorage } from '@homebridge/hap-nodejs';
import { HomebridgeAPI } from './api.js';
import { BridgeService } from './bridgeService.js';
import { ChildBridgeExternalPortService } from './externalPortService.js';
import { Logger } from './logger.js';
import { ChildBridgeMatterMessageHandler } from './matter/ChildBridgeMatterMessageHandler.js';
import { PluginManager } from './pluginManager.js';
import { User } from './user.js';
import 'source-map-support/register.js';
/**
* This is a standalone script executed as a child process fork
*/
process.title = 'homebridge: child bridge';
const matterLogger = Logger.withPrefix('Matter/ChildManager');
export class ChildBridgeFork {
bridgeService;
api;
pluginManager;
externalPortService;
// Matter bridge manager (handles Matter server lifecycle)
matterManager;
// Matter message handler (delegates Matter IPC handling)
matterMessageHandler;
type;
plugin;
identifier;
pluginConfig;
bridgeConfig;
bridgeOptions;
portRequestCallback = new Map();
constructor() {
// tell the parent process we are ready to accept plugin config
this.sendMessage("ready" /* ChildProcessMessageEventType.READY */);
}
sendMessage(type, data) {
if (process.send) {
process.send({
id: type,
data,
});
}
}
async loadPlugin(data) {
// set data
this.type = data.type;
this.identifier = data.identifier;
this.pluginConfig = data.pluginConfig;
this.bridgeConfig = data.bridgeConfig;
this.bridgeOptions = data.bridgeOptions;
// remove the _bridge key (some plugins do not like unknown config)
for (const config of this.pluginConfig) {
delete config._bridge;
}
// set bridge settings (inherited from main bridge)
if (this.bridgeOptions.noLogTimestamps) {
Logger.setTimestampEnabled(false);
}
if (this.bridgeOptions.debugModeEnabled) {
Logger.setDebugEnabled(true);
}
if (this.bridgeOptions.forceColourLogging) {
Logger.forceColor();
}
if (this.bridgeOptions.customStoragePath) {
User.setStoragePath(this.bridgeOptions.customStoragePath);
}
// Initialize HAP-NodeJS with a custom persist directory
HAPStorage.setCustomStoragePath(User.persistPath());
// load api
this.api = new HomebridgeAPI();
this.pluginManager = new PluginManager(this.api);
this.externalPortService = new ChildBridgeExternalPortService(this);
// Eagerly load the MatterAPI facade BEFORE plugin init when Matter is
// configured for this child bridge, so api.matter is defined when the
// plugin's initializer runs. The heavy ChildBridgeMatterManager init
// still happens later in startBridge(). Matter is unsupported on
// accessory-style child bridges, so skip there.
if (this.bridgeConfig.matter && this.type !== "accessory" /* PluginType.ACCESSORY */) {
await this.api.loadMatterAPI();
}
// load plugin
this.plugin = this.pluginManager.loadPlugin(data.pluginPath);
await this.plugin.load();
await this.pluginManager.initializePlugin(this.plugin, data.identifier);
// change process title to include plugin name
process.title = `homebridge: ${this.plugin.getPluginIdentifier()}`;
this.sendMessage("loaded" /* ChildProcessMessageEventType.LOADED */, {
version: this.plugin.version,
});
}
async startBridge() {
// Conditionally load Matter support only if this child bridge has Matter configured
// This prevents loading heavy Matter.js libraries for child bridges that don't use it
if (this.bridgeConfig.matter && this.type === "accessory" /* PluginType.ACCESSORY */) {
matterLogger.warn('Matter is not supported on accessory child bridges. Ignoring matter configuration.');
}
if (this.bridgeConfig.matter && this.type !== "accessory" /* PluginType.ACCESSORY */) {
matterLogger.info('Loading Matter support for child bridge...');
// Note: api.loadMatterAPI() was already called at the start of loadPlugin()
// so api.matter is already defined by the time the plugin's initializer ran.
// Dynamically import Matter manager only when needed
const { ChildBridgeMatterManager } = await import('./matter/index.js');
// Create Matter bridge manager
this.matterManager = new ChildBridgeMatterManager(this.bridgeConfig, this.bridgeOptions, this.api, this.externalPortService, this.pluginManager);
// Set manager reference on API for getAccessoryState
this.api._setMatterManager(this.matterManager);
// Initialize Matter server if configured
// Pass callback to send status updates when commissioning changes
await this.matterManager.initialize(() => {
this.sendPairedStatusEvent();
});
// Create Matter message handler to delegate IPC handling
matterLogger.debug('Creating ChildBridgeMatterMessageHandler...');
this.matterMessageHandler = new ChildBridgeMatterMessageHandler(this.matterManager, this.bridgeConfig.username, (type, data) => this.sendMessage(type, data));
matterLogger.debug(`Matter message handler created for child bridge ${this.bridgeConfig.username}`);
}
else {
matterLogger.debug('Matter not configured for this child bridge, skipping Matter setup');
}
this.bridgeService = new BridgeService(this.api, this.pluginManager, this.externalPortService, this.bridgeOptions, this.bridgeConfig);
// watch bridge events to check when server is online
this.bridgeService.bridge.on("advertised" /* AccessoryEventTypes.ADVERTISED */, () => {
this.sendPairedStatusEvent();
});
// watch for the paired event to update the server status
this.bridgeService.bridge.on("paired" /* AccessoryEventTypes.PAIRED */, () => {
this.sendPairedStatusEvent();
});
// watch for the unpaired event to update the server status
this.bridgeService.bridge.on("unpaired" /* AccessoryEventTypes.UNPAIRED */, () => {
this.sendPairedStatusEvent();
});
// load the cached accessories
await this.bridgeService.loadCachedPlatformAccessoriesFromDisk();
for (const config of this.pluginConfig) {
if (this.type === "platform" /* PluginType.PLATFORM */) {
const plugin = this.pluginManager.getPluginForPlatform(this.identifier);
const displayName = config.name || plugin.getPluginIdentifier();
const logger = Logger.withPrefix(displayName);
const constructor = plugin.getPlatformConstructor(this.identifier);
const platform = new constructor(logger, config, this.api);
if (HomebridgeAPI.isDynamicPlatformPlugin(platform)) {
plugin.assignDynamicPlatform(this.identifier, platform);
}
else if (HomebridgeAPI.isStaticPlatformPlugin(platform)) { // Plugin 1.0, load accessories
await this.bridgeService.loadPlatformAccessories(plugin, platform, this.identifier, 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.
}
}
else if (this.type === "accessory" /* PluginType.ACCESSORY */) {
const plugin = this.pluginManager.getPluginForAccessory(this.identifier);
const displayName = config.name;
if (!displayName) {
Logger.internal.warn('Could not load accessory %s as it is missing the required \'name\' property!', this.identifier);
return;
}
const logger = Logger.withPrefix(displayName);
const constructor = plugin.getAccessoryConstructor(this.identifier);
const accessoryInstance = new constructor(logger, config, 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, this.identifier, config.uuid_base);
if (accessory) {
this.bridgeService.bridge.addBridgedAccessory(accessory);
}
else {
logger('Accessory %s returned empty set of services. Won\'t adding it to the bridge!', this.identifier);
}
}
}
// restore the cached accessories
this.bridgeService.restoreCachedPlatformAccessories();
// Restore Matter accessories if Matter is enabled for this bridge
if (this.matterManager) {
this.matterManager.restoreCachedAccessories(this.bridgeOptions.keepOrphanedCachedAccessories ?? false);
}
// Publish HAP only when not opted out via bridgeConfig.hap=false. The
// protocol-enablement check in Server.validateChildBridgeConfig has
// already guaranteed at least one of HAP or Matter is on for this bridge.
if (this.bridgeConfig.hap !== false) {
this.bridgeService.publishBridge();
}
else {
Logger.internal.info('HAP is disabled for this child bridge (bridgeConfig.hap=false); skipping HAP publish.');
}
this.api.signalFinished();
// Send initial status update with HAP and Matter info BEFORE telling parent we're online
// This ensures the parent's cache is populated before any UI status updates
this.sendPairedStatusEvent();
// tell the parent we are online
this.sendMessage("online" /* ChildProcessMessageEventType.ONLINE */);
}
/**
* Request the next available external HAP port from the parent process
* @param username
*/
async requestExternalPort(username) {
return new Promise((resolve) => {
const requestTimeout = setTimeout(() => {
Logger.internal.warn('Parent process did not respond to port allocation request within 5 seconds - assigning random port.');
this.portRequestCallback.delete(username);
resolve(undefined);
}, 5000);
// setup callback
const callback = (port) => {
clearTimeout(requestTimeout);
this.portRequestCallback.delete(username);
resolve(port);
};
this.portRequestCallback.set(username, callback);
// send port request
this.sendMessage("portRequest" /* ChildProcessMessageEventType.PORT_REQUEST */, { username });
});
}
/**
* Request the next available Matter port from the parent process
* @param uniqueId - MAC-derived identifier (without colons)
*/
async requestMatterPort(uniqueId) {
return new Promise((resolve) => {
// Use uniqueId as the key for the callback map
const mac = uniqueId;
const requestTimeout = setTimeout(() => {
matterLogger.warn('Parent process did not respond to Matter port allocation request within 5 seconds - assigning random port.');
this.portRequestCallback.delete(mac);
resolve(undefined);
}, 5000);
// setup callback
const callback = (port) => {
clearTimeout(requestTimeout);
this.portRequestCallback.delete(mac);
resolve(port);
};
this.portRequestCallback.set(mac, callback);
// send Matter port request
this.sendMessage("portRequest" /* ChildProcessMessageEventType.PORT_REQUEST */, {
username: mac,
portType: 'matter',
});
});
}
/**
* Handles the port allocation response message from the parent process
* @param data
*/
handleExternalResponse(data) {
const callback = this.portRequestCallback.get(data.username);
if (callback) {
callback(data.port);
}
}
/**
* Sends the current pairing status of the child bridge to the parent process
*/
sendPairedStatusEvent() {
// Get Matter commissioning info if Matter is enabled
const matterInfo = this.matterManager?.getMatterStatusInfo();
const isPublished = !!this.bridgeService?.bridge?._accessoryInfo;
this.sendMessage("status" /* ChildProcessMessageEventType.STATUS_UPDATE */, {
paired: isPublished ? (this.bridgeService?.bridge?._accessoryInfo?.paired() ?? null) : null,
setupUri: isPublished ? (this.bridgeService?.bridge?.setupURI() ?? null) : null,
// Include Matter commissioning info in unified message
...(matterInfo && { matter: matterInfo }),
});
}
/**
* Handle start Matter monitoring request from parent process
*/
handleStartMatterMonitoring() {
this.matterMessageHandler?.handleStartMatterMonitoring();
}
/**
* Handle stop Matter monitoring request from parent process
*/
handleStopMatterMonitoring() {
this.matterMessageHandler?.handleStopMatterMonitoring();
}
/**
* Handle get Matter accessories request from parent process
*/
handleGetMatterAccessories() {
if (!this.matterMessageHandler) {
// Matter not initialized yet or not configured - send empty response
// This can happen during startup before Matter finishes initializing
if (!this.bridgeConfig) {
// Bridge config not loaded yet, too early to respond
return;
}
const event = {
type: 'accessoriesData',
data: {
bridgeUsername: this.bridgeConfig.username,
accessories: [],
},
};
this.sendMessage("matterEvent" /* ChildProcessMessageEventType.MATTER_EVENT */, event);
return;
}
this.matterMessageHandler.handleGetMatterAccessories();
}
/**
* Handle get Matter accessory info request from parent process
*/
handleGetMatterAccessoryInfo(data) {
this.matterMessageHandler?.handleGetMatterAccessoryInfo(data);
}
/**
* Handle Matter accessory control request from parent process
*/
handleMatterAccessoryControl(data) {
this.matterMessageHandler?.handleMatterAccessoryControl(data);
}
shutdown() {
this.bridgeService.teardown();
// Teardown Matter servers (main bridge and external accessories)
if (this.matterManager) {
this.matterManager.teardown().catch((error) => {
matterLogger.error('Error tearing down Matter manager:', error);
});
}
}
}
/**
* Start Self
*/
const childPluginFork = new ChildBridgeFork();
/**
* Handle incoming IPC messages from the parent Homebridge process
*/
process.on('message', (message) => {
if (typeof message !== 'object' || !message.id) {
return;
}
switch (message.id) {
case "load" /* ChildProcessMessageEventType.LOAD */: {
childPluginFork.loadPlugin(message.data).catch((error) => {
Logger.internal.error('Child bridge failed to load plugin:', error);
process.exit(1);
});
break;
}
case "start" /* ChildProcessMessageEventType.START */: {
childPluginFork.startBridge().catch((error) => {
Logger.internal.error('Child bridge failed to start:', error);
process.exit(1);
});
break;
}
case "portAllocated" /* ChildProcessMessageEventType.PORT_ALLOCATED */: {
childPluginFork.handleExternalResponse(message.data);
break;
}
case "startMatterMonitoring" /* ChildProcessMessageEventType.START_MATTER_MONITORING */: {
childPluginFork.handleStartMatterMonitoring();
break;
}
case "stopMatterMonitoring" /* ChildProcessMessageEventType.STOP_MATTER_MONITORING */: {
childPluginFork.handleStopMatterMonitoring();
break;
}
case "getMatterAccessories" /* ChildProcessMessageEventType.GET_MATTER_ACCESSORIES */: {
childPluginFork.handleGetMatterAccessories();
break;
}
case "getMatterAccessoryInfo" /* ChildProcessMessageEventType.GET_MATTER_ACCESSORY_INFO */: {
childPluginFork.handleGetMatterAccessoryInfo(message.data);
break;
}
case "matterAccessoryControl" /* ChildProcessMessageEventType.MATTER_ACCESSORY_CONTROL */: {
childPluginFork.handleMatterAccessoryControl(message.data);
break;
}
default: {
Logger.internal.warn(`Received unknown message type from parent process: ${message.id}`);
break;
}
}
});
/**
* Handle the sigterm shutdown signals
*/
let shuttingDown = false;
function signalHandler(signal, signalNum) {
if (shuttingDown) {
return;
}
shuttingDown = true;
Logger.internal.info('Got %s, shutting down child bridge process...', signal);
try {
childPluginFork.shutdown();
}
catch (error) {
Logger.internal.error('Error during child bridge shutdown:', error);
}
setTimeout(() => process.exit(128 + signalNum), 5000).unref();
}
process.on('SIGINT', signalHandler.bind(undefined, 'SIGINT', 2));
process.on('SIGTERM', signalHandler.bind(undefined, 'SIGTERM', 15));
/**
* Ensure orphaned processes are cleaned up
*/
setInterval(() => {
if (!process.connected) {
Logger.internal.info('Parent process not connected, terminating process...');
process.exit(1);
}
}, 5000);
//# sourceMappingURL=childBridgeFork.js.map