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