homebridge
Version:
HomeKit support for the impatient
297 lines • 14.4 kB
JavaScript
/**
* Base class for Matter bridge managers
* Contains shared logic for handling Matter accessory control and state updates
*/
import { rmSync } from 'node:fs';
import path from 'node:path';
import { Logger } from '../logger.js';
import { User } from '../user.js';
import { generate } from '../util/mac.js';
import { mapAttributesToCommand } from './ClusterCommandMapper.js';
const log = Logger.withPrefix('Matter/BaseManager');
const COLON_RE = /:/g;
/**
* Base Matter Manager
* Provides common functionality for both main bridge and child bridge Matter managers
*/
export class BaseMatterManager {
matterServer;
externalMatterServers = new Map();
pluginManager;
constructor(pluginManager) {
this.pluginManager = pluginManager;
}
/**
* Get an external Matter server by accessory UUID
*
* @param uuid - Accessory UUID
* @returns Matter server instance or undefined if not found
*/
getExternalServer(uuid) {
return this.externalMatterServers.get(uuid);
}
/**
* Handle Matter accessory command (triggers user handlers)
* This is for UI/external control that should invoke plugin handlers
* Checks both external servers and bridge server
*/
async handleTriggerCommand(uuid, cluster, attributes, partId) {
// Map attributes to command using centralized mapper
const commandMapping = mapAttributesToCommand(cluster, attributes);
// Debug logging — pass objects as %j format args so JSON.stringify only
// runs when debug is enabled (Logger.log short-circuits on level first).
log.debug(`handleTriggerCommand: uuid=${uuid}, cluster=${cluster}, partId=${partId}`);
log.debug('Attributes: %j', attributes);
if (commandMapping) {
log.debug('Command mapping: %j', commandMapping);
}
else {
log.debug('Command mapping: null (state-only update)');
}
log.debug(`External servers count: ${this.externalMatterServers.size}`);
if (this.externalMatterServers.size > 0) {
// Pass the keys iterator as a %j arg so the spread+join only runs once
// util.format is reached (i.e. when debug is enabled).
log.debug('External server UUIDs: %j', [...this.externalMatterServers.keys()]);
}
// Check if this is an external accessory first
const externalServer = this.externalMatterServers.get(uuid);
if (externalServer) {
log.debug(`Found external server for ${uuid}`);
if (commandMapping) {
// Explicit command invocation
await externalServer.triggerCommand(uuid, cluster, commandMapping.command, commandMapping.args, partId);
// After a command, read back the current state and notify so the UI updates
this.notifyCurrentState(externalServer, uuid, cluster, partId);
}
else {
// State-only update (triggers change handlers automatically)
await externalServer.updateAccessoryState(uuid, cluster, attributes, partId);
}
return;
}
// Otherwise, try the bridge Matter server
if (!this.matterServer) {
// This is expected when accessory is on a different bridge - throw error for proper handling
log.debug(`No matterServer and external server not found for ${uuid}`);
throw new Error(`Accessory ${uuid} not found on this bridge`);
}
log.debug(`Trying matterServer for ${uuid}`);
if (commandMapping) {
// Explicit command invocation
await this.matterServer.triggerCommand(uuid, cluster, commandMapping.command, commandMapping.args, partId);
// After a command, read back the current state and notify so the UI updates
this.notifyCurrentState(this.matterServer, uuid, cluster, partId);
}
else {
// State-only update (triggers change handlers automatically)
await this.matterServer.updateAccessoryState(uuid, cluster, attributes, partId);
}
}
/**
* Handle Matter accessory state updates
* Checks both external servers and bridge server
*/
async handleUpdateAccessoryState(uuid, cluster, attributes, partId) {
// Check if this is an external accessory first
const externalServer = this.externalMatterServers.get(uuid);
if (externalServer) {
await externalServer.updateAccessoryState(uuid, cluster, attributes, partId);
return;
}
// Otherwise, try the bridge Matter server
if (!this.matterServer) {
// This is expected when accessory is on a different bridge - throw error for proper handling
throw new Error(`Accessory ${uuid} not found on this bridge`);
}
await this.matterServer.updateAccessoryState(uuid, cluster, attributes, partId);
}
/**
* Enable state monitoring on all Matter servers
*/
enableStateMonitoring() {
this.matterServer?.enableStateMonitoring();
for (const externalServer of this.externalMatterServers.values()) {
externalServer.enableStateMonitoring();
}
}
/**
* After a triggerCommand completes, read back the current cluster state
* and emit a state change notification. This ensures the UI receives
* the updated state (e.g., currentPositionLiftPercent100ths for window
* coverings) even if the behavior's own notification was not delivered.
*/
notifyCurrentState(server, uuid, cluster, partId) {
const currentState = server.getAccessoryState(uuid, cluster, partId);
if (currentState) {
server.notifyStateChange(uuid, cluster, currentState, partId);
}
}
/**
* Disable state monitoring on all Matter servers
*/
disableStateMonitoring() {
this.matterServer?.disableStateMonitoring();
for (const externalServer of this.externalMatterServers.values()) {
externalServer.disableStateMonitoring();
}
}
/**
* Restore cached Matter accessories (matching HAP pattern)
*/
restoreCachedAccessories(keepOrphaned) {
if (!this.matterServer) {
return;
}
const cachedAccessories = this.matterServer.getAllCachedAccessories();
log.debug(`Restoring ${cachedAccessories.length} cached Matter accessories`);
for (const cachedAccessory of cachedAccessories) {
let plugin = this.pluginManager.getPlugin(cachedAccessory.plugin);
if (!plugin) {
try {
// Try to find plugin by platform name (handles plugin renames)
plugin = this.pluginManager.getPluginByActiveDynamicPlatform(cachedAccessory.platform);
if (plugin) {
log.info(`When searching for the associated plugin of the Matter accessory '${cachedAccessory.displayName}' `
+ `it seems like the plugin name changed from '${cachedAccessory.plugin}' to '${plugin.getPluginIdentifier()}'. Plugin association is now being transformed!`);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.warn(`Could not find the associated plugin for the Matter accessory '${cachedAccessory.displayName}'. `
+ `Tried to find the plugin by the platform name but ${errorMessage}`);
}
}
const platformPlugin = plugin && plugin.getActiveDynamicPlatform(cachedAccessory.platform);
if (!platformPlugin) {
log.warn(`Failed to find plugin to handle Matter accessory ${cachedAccessory.displayName} (plugin: ${cachedAccessory.plugin}, platform: ${cachedAccessory.platform})`);
if (!keepOrphaned) {
log.info(`Removing orphaned Matter accessory ${cachedAccessory.displayName}`);
this.matterServer.unregisterAccessory(cachedAccessory.uuid).catch((error) => {
log.warn(`Failed to unregister orphaned Matter accessory ${cachedAccessory.displayName}:`, error);
});
}
}
else {
// Call configureMatterAccessory if the plugin implements it
if (platformPlugin.configureMatterAccessory) {
log.debug(`Calling configureMatterAccessory for ${cachedAccessory.displayName}`);
// Deserialize from cache format to MatterAccessory for plugin
const accessory = this.deserializeMatterAccessory(cachedAccessory);
platformPlugin.configureMatterAccessory(accessory);
}
else {
log.debug(`Platform ${cachedAccessory.platform} does not implement configureMatterAccessory`);
}
}
}
}
/**
* Handle registration of Matter platform accessories
*/
async handleRegisterPlatformAccessories(pluginIdentifier, platformName, accessories) {
if (!this.matterServer) {
log.warn('Cannot register Matter accessories - Matter server is not running');
return;
}
await this.matterServer.registerPlatformAccessories(pluginIdentifier, platformName, accessories);
}
/**
* Handle updating Matter platform accessories in the cache
* Checks both external servers and bridge server
*/
async handleUpdatePlatformAccessories(accessories) {
const bridgeAccessories = [];
// Route each accessory to the appropriate server
for (const accessory of accessories) {
const externalServer = this.externalMatterServers.get(accessory.UUID);
if (externalServer) {
// Update external accessory
await externalServer.updatePlatformAccessories([accessory]);
}
else {
// Collect accessories for bridge server
bridgeAccessories.push(accessory);
}
}
// Update accessories on bridge server if any
if (bridgeAccessories.length > 0) {
if (!this.matterServer) {
log.warn('Cannot update Matter platform accessories - Matter server is not running');
return;
}
await this.matterServer.updatePlatformAccessories(bridgeAccessories);
}
}
/**
* Handle unregistration of Matter platform accessories
*/
async handleUnregisterPlatformAccessories(pluginIdentifier, platformName, accessories) {
if (!this.matterServer) {
log.warn('Cannot unregister Matter accessories - Matter server is not running');
return;
}
await this.matterServer.unregisterPlatformAccessories(pluginIdentifier, platformName, accessories);
}
/**
* Handle unregistration of external Matter accessories
* Stops dedicated servers and cleans up storage
*/
async handleUnregisterExternalAccessories(accessories) {
log.info(`Unregistering ${accessories.length} external Matter accessor${accessories.length === 1 ? 'y' : 'ies'}`);
for (const accessory of accessories) {
try {
// Check if this external server exists
const matterServer = this.externalMatterServers.get(accessory.UUID);
if (!matterServer) {
log.warn(`External Matter accessory ${accessory.displayName} (${accessory.UUID}) is not registered`);
continue;
}
log.info(`Stopping external Matter server for ${accessory.displayName}`);
// Stop the Matter server
await matterServer.stop();
// Remove from the map
this.externalMatterServers.delete(accessory.UUID);
// Clean up storage folder
// Generate the same uniqueId that was used when creating the server
const advertiseAddress = generate(accessory.UUID);
const uniqueId = advertiseAddress.replace(COLON_RE, '');
const storagePath = path.join(User.matterPath(), uniqueId);
try {
log.debug(`Removing Matter storage for external accessory at: ${storagePath}`);
rmSync(storagePath, { recursive: true, force: true });
log.info(`✓ Cleaned up storage for external Matter accessory: ${accessory.displayName}`);
}
catch (error) {
log.error(`Failed to clean up storage for external Matter accessory ${accessory.displayName}:`, error);
}
log.info(`✓ External Matter accessory unregistered: ${accessory.displayName}`);
}
catch (error) {
log.error(`Failed to unregister external Matter accessory ${accessory.displayName}:`, error);
}
}
}
/**
* Deserialize SerializedMatterAccessory from cache to MatterAccessory for plugin use
* Converts internal cache format to the public API format plugins expect
*/
deserializeMatterAccessory(serialized) {
return {
UUID: serialized.uuid, // Convert lowercase uuid to uppercase UUID
displayName: serialized.displayName,
deviceType: serialized.deviceType, // Type info only (full EndpointType not restorable from cache)
serialNumber: serialized.serialNumber,
manufacturer: serialized.manufacturer,
model: serialized.model,
firmwareRevision: serialized.firmwareRevision,
hardwareRevision: serialized.hardwareRevision,
softwareVersion: serialized.softwareVersion,
context: serialized.context ?? {}, // Ensure non-optional context
clusters: serialized.clusters,
parts: serialized.parts, // Part types not fully restorable from cache
// Note: handlers and getState are not restored from cache - plugins must provide these
};
}
}
//# sourceMappingURL=BaseMatterManager.js.map