homebridge 2.0.0-alpha.41 → 2.0.0-alpha.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +81 -20
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +71 -11
- package/dist/api.js.map +1 -1
- package/dist/bridgeService.d.ts +14 -12
- package/dist/bridgeService.d.ts.map +1 -1
- package/dist/bridgeService.js +17 -3
- package/dist/bridgeService.js.map +1 -1
- package/dist/bridgeTypes.d.ts +54 -0
- package/dist/bridgeTypes.d.ts.map +1 -0
- package/dist/bridgeTypes.js +8 -0
- package/dist/bridgeTypes.js.map +1 -0
- package/dist/childBridgeFork.d.ts +23 -0
- package/dist/childBridgeFork.d.ts.map +1 -1
- package/dist/childBridgeFork.js +240 -4
- package/dist/childBridgeFork.js.map +1 -1
- package/dist/childBridgeService.d.ts +47 -7
- package/dist/childBridgeService.d.ts.map +1 -1
- package/dist/childBridgeService.js +67 -2
- package/dist/childBridgeService.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/ipcService.d.ts +3 -1
- package/dist/ipcService.d.ts.map +1 -1
- package/dist/ipcService.js +2 -0
- package/dist/ipcService.js.map +1 -1
- package/dist/matter/index.d.ts +4 -6
- package/dist/matter/index.d.ts.map +1 -1
- package/dist/matter/index.js +4 -5
- package/dist/matter/index.js.map +1 -1
- package/dist/matter/matterConfigValidator.d.ts +2 -3
- package/dist/matter/matterConfigValidator.d.ts.map +1 -1
- package/dist/matter/matterConfigValidator.js +47 -38
- package/dist/matter/matterConfigValidator.js.map +1 -1
- package/dist/matter/matterErrorHandler.d.ts +6 -25
- package/dist/matter/matterErrorHandler.d.ts.map +1 -1
- package/dist/matter/matterErrorHandler.js +89 -99
- package/dist/matter/matterErrorHandler.js.map +1 -1
- package/dist/matter/matterServer.d.ts +126 -39
- package/dist/matter/matterServer.d.ts.map +1 -1
- package/dist/matter/matterServer.js +528 -226
- package/dist/matter/matterServer.js.map +1 -1
- package/dist/matter/matterSharedTypes.d.ts +16 -38
- package/dist/matter/matterSharedTypes.d.ts.map +1 -1
- package/dist/matter/matterSharedTypes.js +3 -4
- package/dist/matter/matterSharedTypes.js.map +1 -1
- package/dist/matter/matterStorage.d.ts +116 -0
- package/dist/matter/matterStorage.d.ts.map +1 -0
- package/dist/matter/matterStorage.js +442 -0
- package/dist/matter/matterStorage.js.map +1 -0
- package/dist/matter/matterTypes.d.ts +148 -20
- package/dist/matter/matterTypes.d.ts.map +1 -1
- package/dist/matter/matterTypes.js +91 -263
- package/dist/matter/matterTypes.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +4 -2
- package/dist/plugin.js.map +1 -1
- package/dist/server.d.ts +18 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +376 -349
- package/dist/server.js.map +1 -1
- package/dist/user.d.ts +1 -0
- package/dist/user.d.ts.map +1 -1
- package/dist/user.js +3 -0
- package/dist/user.js.map +1 -1
- package/package.json +6 -7
- package/dist/childMatterBridgeFork.d.ts +0 -108
- package/dist/childMatterBridgeFork.d.ts.map +0 -1
- package/dist/childMatterBridgeFork.js +0 -330
- package/dist/childMatterBridgeFork.js.map +0 -1
- package/dist/childMatterBridgeService.d.ts +0 -166
- package/dist/childMatterBridgeService.d.ts.map +0 -1
- package/dist/childMatterBridgeService.js +0 -623
- package/dist/childMatterBridgeService.js.map +0 -1
- package/dist/matter/matterBridge.d.ts +0 -64
- package/dist/matter/matterBridge.d.ts.map +0 -1
- package/dist/matter/matterBridge.js +0 -154
- package/dist/matter/matterBridge.js.map +0 -1
- package/dist/matter/matterDevice.d.ts +0 -107
- package/dist/matter/matterDevice.d.ts.map +0 -1
- package/dist/matter/matterDevice.js +0 -913
- package/dist/matter/matterDevice.js.map +0 -1
- package/dist/matter/portAllocator.d.ts +0 -85
- package/dist/matter/portAllocator.d.ts.map +0 -1
- package/dist/matter/portAllocator.js +0 -296
- package/dist/matter/portAllocator.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -5,11 +5,10 @@ import qrcode from 'qrcode-terminal';
|
|
|
5
5
|
import { HomebridgeAPI } from './api.js';
|
|
6
6
|
import { BridgeService } from './bridgeService.js';
|
|
7
7
|
import { ChildBridgeService } from './childBridgeService.js';
|
|
8
|
-
import { ChildMatterBridgeService } from './childMatterBridgeService.js';
|
|
9
8
|
import { ExternalPortService } from './externalPortService.js';
|
|
10
9
|
import { IpcService } from './ipcService.js';
|
|
11
10
|
import { Logger } from './logger.js';
|
|
12
|
-
import { MatterConfigValidator } from './matter/
|
|
11
|
+
import { MatterConfigValidator, MatterServer } from './matter/index.js';
|
|
13
12
|
import { PluginManager } from './pluginManager.js';
|
|
14
13
|
import { User } from './user.js';
|
|
15
14
|
import { validMacAddress } from './util/mac.js';
|
|
@@ -39,8 +38,12 @@ export class Server {
|
|
|
39
38
|
externalPortService;
|
|
40
39
|
config;
|
|
41
40
|
// used to keep track of child bridges
|
|
41
|
+
// Key is either HAP username (MAC address) or Matter-only identifier (e.g., "matter-plugin-platform")
|
|
42
42
|
childBridges = new Map();
|
|
43
|
-
|
|
43
|
+
// Matter server instance for main bridge (if enabled)
|
|
44
|
+
matterServer;
|
|
45
|
+
// Cache for Matter bridge identifier -> ChildBridgeService lookups
|
|
46
|
+
matterBridgeCache = new Map();
|
|
44
47
|
// Track platform configurations for routing accessories
|
|
45
48
|
platformConfigs = new Map();
|
|
46
49
|
// current server status
|
|
@@ -70,12 +73,15 @@ export class Server {
|
|
|
70
73
|
// shallow copy the homebridge options to the bridge options object
|
|
71
74
|
Object.assign(bridgeConfig, this.options);
|
|
72
75
|
this.bridgeService = new BridgeService(this.api, this.pluginManager, this.externalPortService, bridgeConfig, this.config.bridge, this.config);
|
|
73
|
-
//
|
|
74
|
-
// Intercept platform accessory registration to route to Matter bridges if needed
|
|
76
|
+
// Handle platform accessory registration
|
|
75
77
|
this.api.on("registerPlatformAccessories" /* InternalAPIEvent.REGISTER_PLATFORM_ACCESSORIES */, this.handleRegisterPlatformAccessories.bind(this));
|
|
76
78
|
this.api.on("unregisterPlatformAccessories" /* InternalAPIEvent.UNREGISTER_PLATFORM_ACCESSORIES */, this.handleUnregisterPlatformAccessories.bind(this));
|
|
77
|
-
// Handle external accessories (cameras, etc.)
|
|
79
|
+
// Handle external accessories (cameras, etc.)
|
|
78
80
|
this.api.on("publishExternalAccessories" /* InternalAPIEvent.PUBLISH_EXTERNAL_ACCESSORIES */, this.handlePublishExternalAccessories.bind(this));
|
|
81
|
+
// Handle Matter accessory registration
|
|
82
|
+
this.api.on("registerMatterAccessory" /* InternalAPIEvent.REGISTER_MATTER_ACCESSORY */, this.handleRegisterMatterAccessory.bind(this));
|
|
83
|
+
this.api.on("unregisterMatterAccessory" /* InternalAPIEvent.UNREGISTER_MATTER_ACCESSORY */, this.handleUnregisterMatterAccessory.bind(this));
|
|
84
|
+
this.api.on("updateMatterAccessoryState" /* InternalAPIEvent.UPDATE_MATTER_ACCESSORY_STATE */, this.handleUpdateMatterAccessoryState.bind(this));
|
|
79
85
|
// watch bridge events to check when server is online
|
|
80
86
|
this.bridgeService.bridge.on("advertised" /* AccessoryEventTypes.ADVERTISED */, () => {
|
|
81
87
|
this.setServerStatus("ok" /* ServerStatus.OK */);
|
|
@@ -95,14 +101,6 @@ export class Server {
|
|
|
95
101
|
*/
|
|
96
102
|
setServerStatus(status) {
|
|
97
103
|
this.serverStatus = status;
|
|
98
|
-
// Collect child Matter bridge statuses
|
|
99
|
-
const childMatterBridgeStatuses = Array.from(this.childMatterBridges.values()).map(bridge => bridge.getMetadata());
|
|
100
|
-
// Get Matter bridges status (child bridges only)
|
|
101
|
-
const matterStatus = childMatterBridgeStatuses.length > 0
|
|
102
|
-
? {
|
|
103
|
-
children: childMatterBridgeStatuses,
|
|
104
|
-
}
|
|
105
|
-
: undefined;
|
|
106
104
|
this.ipcService.sendMessage("serverStatusUpdate" /* IpcOutgoingEvent.SERVER_STATUS_UPDATE */, {
|
|
107
105
|
type: 'hap', // Main bridge is HAP
|
|
108
106
|
status: this.serverStatus,
|
|
@@ -111,78 +109,29 @@ export class Server {
|
|
|
111
109
|
name: this.bridgeService?.bridge?.displayName || this.config.bridge.name,
|
|
112
110
|
username: this.config.bridge.username,
|
|
113
111
|
pin: this.config.bridge.pin,
|
|
114
|
-
matterStatus,
|
|
115
112
|
});
|
|
116
113
|
}
|
|
117
114
|
async start() {
|
|
118
115
|
if (this.config.bridge.disableIpc !== true) {
|
|
119
116
|
this.initializeIpcEventHandlers();
|
|
120
117
|
}
|
|
121
|
-
// Track existing Matter bridges before loading new configuration
|
|
122
|
-
const existingMatterBridgeIds = new Set(this.childMatterBridges.keys());
|
|
123
|
-
// Validate child Matter configurations
|
|
124
|
-
const matterValidation = MatterConfigValidator.validateAllChildMatterConfigs(this.config.platforms, this.config.accessories);
|
|
125
|
-
if (!matterValidation.isValid) {
|
|
126
|
-
log.error('Child Matter configuration validation failed. Please fix the errors above.');
|
|
127
|
-
// Continue anyway but child Matter bridges may not work properly
|
|
128
|
-
}
|
|
129
118
|
const promises = [];
|
|
130
119
|
// load the cached accessories
|
|
131
120
|
await this.bridgeService.loadCachedPlatformAccessoriesFromDisk();
|
|
132
121
|
// initialize plugins
|
|
133
122
|
await this.pluginManager.initializeInstalledPlugins();
|
|
123
|
+
// Initialize Matter server for main bridge if enabled
|
|
124
|
+
await this.initializeMatterServer();
|
|
134
125
|
if (this.config.platforms.length > 0) {
|
|
135
126
|
promises.push(...this.loadPlatforms());
|
|
136
127
|
}
|
|
137
128
|
if (this.config.accessories.length > 0) {
|
|
138
129
|
this.loadAccessories();
|
|
139
130
|
}
|
|
140
|
-
// Track which Matter bridges are still in the configuration
|
|
141
|
-
const currentMatterBridgeIds = new Set();
|
|
142
|
-
// Check platforms for Matter bridges
|
|
143
|
-
this.config.platforms.forEach((platformConfig) => {
|
|
144
|
-
if (platformConfig._matter && platformConfig.platform) {
|
|
145
|
-
try {
|
|
146
|
-
const plugin = this.pluginManager.getPluginForPlatform(platformConfig.platform);
|
|
147
|
-
const matterIdentifier = `${plugin.getPluginIdentifier()}-${platformConfig.platform}`;
|
|
148
|
-
currentMatterBridgeIds.add(matterIdentifier);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
// Plugin not found, skip
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
// Check accessories for Matter bridges
|
|
156
|
-
this.config.accessories.forEach((accessoryConfig) => {
|
|
157
|
-
if (accessoryConfig._matter && accessoryConfig.accessory) {
|
|
158
|
-
try {
|
|
159
|
-
const plugin = this.pluginManager.getPluginForAccessory(accessoryConfig.accessory);
|
|
160
|
-
const matterIdentifier = `${plugin.getPluginIdentifier()}-${accessoryConfig.accessory}`;
|
|
161
|
-
currentMatterBridgeIds.add(matterIdentifier);
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// Plugin not found, skip
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
// Clean up Matter bridges that were removed from configuration
|
|
169
|
-
for (const bridgeId of existingMatterBridgeIds) {
|
|
170
|
-
if (!currentMatterBridgeIds.has(bridgeId) && this.childMatterBridges.has(bridgeId)) {
|
|
171
|
-
const removedBridge = this.childMatterBridges.get(bridgeId);
|
|
172
|
-
log.info(`Matter bridge "${bridgeId}" was removed from configuration, cleaning up...`);
|
|
173
|
-
// Stop and clean up the removed bridge (with permanent flag)
|
|
174
|
-
await removedBridge.teardown(true);
|
|
175
|
-
this.childMatterBridges.delete(bridgeId);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
131
|
// start child HAP bridges
|
|
179
132
|
for (const childBridge of this.childBridges.values()) {
|
|
180
133
|
childBridge.start();
|
|
181
134
|
}
|
|
182
|
-
// start child Matter bridges
|
|
183
|
-
for (const childMatterBridge of this.childMatterBridges.values()) {
|
|
184
|
-
promises.push(childMatterBridge.start());
|
|
185
|
-
}
|
|
186
135
|
// restore cached accessories
|
|
187
136
|
this.bridgeService.restoreCachedPlatformAccessories();
|
|
188
137
|
this.api.signalFinished();
|
|
@@ -191,157 +140,144 @@ export class Server {
|
|
|
191
140
|
.then(() => this.publishBridge());
|
|
192
141
|
}
|
|
193
142
|
/**
|
|
194
|
-
*
|
|
143
|
+
* Initialize Matter server for main bridge if enabled
|
|
195
144
|
*/
|
|
196
|
-
|
|
197
|
-
|
|
145
|
+
async initializeMatterServer() {
|
|
146
|
+
// Check if main bridge has matter configuration
|
|
147
|
+
if (!this.config.bridge.matter) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
log.info('Initializing Matter server for main bridge...');
|
|
152
|
+
// Allocate port from pool if not explicitly configured
|
|
153
|
+
let matterPort = this.config.bridge.matter.port;
|
|
154
|
+
if (!matterPort) {
|
|
155
|
+
matterPort = await this.externalPortService.requestPort(`${this.config.bridge.username}:MATTER`);
|
|
156
|
+
if (!matterPort) {
|
|
157
|
+
matterPort = 5540; // Default Matter port
|
|
158
|
+
log.warn('No port available from pool for main Matter bridge, using default port 5540');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
log.info(`Allocated port ${matterPort} from pool for main Matter bridge`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Create Matter server instance
|
|
165
|
+
this.matterServer = new MatterServer({
|
|
166
|
+
storagePath: User.matterPath(),
|
|
167
|
+
port: matterPort,
|
|
168
|
+
uniqueId: 'main-bridge',
|
|
169
|
+
name: this.config.bridge.matter.name || (this.config.bridge.name ? `${this.config.bridge.name} (Matter)` : 'Homebridge Matter Bridge'),
|
|
170
|
+
});
|
|
171
|
+
// Start the Matter server
|
|
172
|
+
await this.matterServer.start();
|
|
173
|
+
log.info('Matter server initialized for main bridge');
|
|
174
|
+
// Inform the API that Matter is enabled
|
|
175
|
+
this.api._setMatterEnabled(true);
|
|
176
|
+
// Send Matter status update to notify UI that Matter server is ready
|
|
177
|
+
if (this.config.bridge.disableIpc !== true) {
|
|
178
|
+
this.sendMainBridgeMatterStatusUpdate();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
log.error('Failed to initialize Matter server for main bridge:', error);
|
|
183
|
+
// Provide user-friendly guidance for common errors
|
|
184
|
+
if (error.message && error.message.includes('corrupted')) {
|
|
185
|
+
log.error('');
|
|
186
|
+
log.error('╔════════════════════════════════════════════════════════════════════════════╗');
|
|
187
|
+
log.error('║ MATTER STORAGE CORRUPTED ║');
|
|
188
|
+
log.error('╠════════════════════════════════════════════════════════════════════════════╣');
|
|
189
|
+
log.error('║ Your Matter storage has become corrupted. This can happen when: ║');
|
|
190
|
+
log.error('║ • Matter.js library version changes ║');
|
|
191
|
+
log.error('║ • Storage format upgrades occur ║');
|
|
192
|
+
log.error('║ • Incomplete writes during shutdown ║');
|
|
193
|
+
log.error('║ ║');
|
|
194
|
+
log.error('║ To fix this, delete the corrupted storage directory: ║');
|
|
195
|
+
log.error('║ rm -rf ~/.homebridge/matter/main-bridge ║');
|
|
196
|
+
log.error('║ ║');
|
|
197
|
+
log.error('║ Note: You will need to re-pair your Matter devices after deletion. ║');
|
|
198
|
+
log.error('╚════════════════════════════════════════════════════════════════════════════╝');
|
|
199
|
+
log.error('');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Send Matter status update for main bridge
|
|
205
|
+
*/
|
|
206
|
+
sendMainBridgeMatterStatusUpdate() {
|
|
207
|
+
if (!this.matterServer || !this.matterServer.isServerRunning()) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const commissioningInfo = this.matterServer.getCommissioningInfo();
|
|
211
|
+
// Transform property names to match UI expectations
|
|
212
|
+
const statusUpdate = {
|
|
213
|
+
type: 'matter',
|
|
214
|
+
status: 'ok',
|
|
215
|
+
port: this.config.bridge.matter?.port || 5540,
|
|
216
|
+
setupUri: commissioningInfo.qrCode, // Map qrCode -> setupUri for UI
|
|
217
|
+
pin: commissioningInfo.manualPairingCode, // Map manualPairingCode -> pin for UI
|
|
218
|
+
serialNumber: commissioningInfo.serialNumber,
|
|
219
|
+
passcode: commissioningInfo.passcode,
|
|
220
|
+
discriminator: commissioningInfo.discriminator,
|
|
221
|
+
name: this.config.bridge.matter?.name || (this.config.bridge.name ? `${this.config.bridge.name} (Matter)` : 'Homebridge Matter Bridge'),
|
|
222
|
+
plugin: 'main-bridge',
|
|
223
|
+
identifier: 'main-bridge',
|
|
224
|
+
deviceCount: this.matterServer.getAccessories().length,
|
|
225
|
+
commissioned: commissioningInfo.commissioned,
|
|
226
|
+
};
|
|
227
|
+
this.ipcService.sendMessage("matterBridgeStatusUpdate" /* IpcOutgoingEvent.MATTER_BRIDGE_STATUS_UPDATE */, statusUpdate);
|
|
198
228
|
}
|
|
199
|
-
teardown() {
|
|
229
|
+
async teardown() {
|
|
200
230
|
this.bridgeService.teardown();
|
|
201
|
-
// Stop
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
231
|
+
// Stop main Matter server if running
|
|
232
|
+
if (this.matterServer) {
|
|
233
|
+
try {
|
|
234
|
+
await this.matterServer.stop();
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
log.error('Failed to stop Matter server:', error);
|
|
238
|
+
}
|
|
206
239
|
}
|
|
240
|
+
// Child bridge Matter servers are stopped by their own forked processes
|
|
207
241
|
this.setServerStatus("down" /* ServerStatus.DOWN */);
|
|
208
242
|
}
|
|
209
243
|
publishBridge() {
|
|
210
244
|
this.bridgeService.publishBridge();
|
|
245
|
+
// Main bridge always has a pin (validated in loadConfig)
|
|
211
246
|
this.printSetupInfo(this.config.bridge.pin);
|
|
212
247
|
}
|
|
213
|
-
// Matter accessories are managed via _matter configuration, not API methods
|
|
214
248
|
handlePublishExternalAccessories(accessories) {
|
|
215
249
|
log.info(`Publishing ${accessories.length} external accessories`);
|
|
216
|
-
// External accessories are
|
|
217
|
-
//
|
|
218
|
-
const accessoriesByPlugin = new Map();
|
|
219
|
-
accessories.forEach((accessory) => {
|
|
220
|
-
const pluginIdentifier = accessory._associatedPlugin;
|
|
221
|
-
if (!pluginIdentifier) {
|
|
222
|
-
log.warn(`External accessory "${accessory.displayName}" has no plugin identifier`);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (!accessoriesByPlugin.has(pluginIdentifier)) {
|
|
226
|
-
accessoriesByPlugin.set(pluginIdentifier, []);
|
|
227
|
-
}
|
|
228
|
-
accessoriesByPlugin.get(pluginIdentifier).push(accessory);
|
|
229
|
-
});
|
|
230
|
-
// Process each plugin's external accessories
|
|
231
|
-
accessoriesByPlugin.forEach((pluginAccessories, pluginIdentifier) => {
|
|
232
|
-
// Check if there's already an external Matter bridge for this plugin
|
|
233
|
-
const externalMatterBridgeId = `${pluginIdentifier}-external`;
|
|
234
|
-
let externalMatterBridge = this.childMatterBridges.get(externalMatterBridgeId);
|
|
235
|
-
if (!externalMatterBridge) {
|
|
236
|
-
// Check if this plugin has any Matter-enabled platform config
|
|
237
|
-
let hasMatterConfig = false;
|
|
238
|
-
let baseMatterConfig = {};
|
|
239
|
-
// Look for any platform with Matter config from this plugin
|
|
240
|
-
for (const [bridgeId, childBridge] of this.childMatterBridges) {
|
|
241
|
-
if (bridgeId.startsWith(`${pluginIdentifier}-`) && childBridge.type === "platform" /* PluginType.PLATFORM */) {
|
|
242
|
-
hasMatterConfig = true;
|
|
243
|
-
// Use existing Matter bridge configuration as template
|
|
244
|
-
baseMatterConfig = {
|
|
245
|
-
// Port will be auto-allocated
|
|
246
|
-
};
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// If plugin has Matter-enabled platforms, create external bridge automatically
|
|
251
|
-
if (hasMatterConfig) {
|
|
252
|
-
log.info(`Automatically creating external Matter bridge for ${pluginIdentifier}`);
|
|
253
|
-
const plugin = this.pluginManager.getPlugin(pluginIdentifier);
|
|
254
|
-
if (!plugin) {
|
|
255
|
-
log.warn(`Could not find plugin ${pluginIdentifier} to create external Matter bridge`);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
// Create synthetic config for external bridge
|
|
259
|
-
const externalConfig = {
|
|
260
|
-
platform: '__external__',
|
|
261
|
-
name: `${pluginIdentifier} External`,
|
|
262
|
-
_matter: {
|
|
263
|
-
...baseMatterConfig,
|
|
264
|
-
name: `${pluginIdentifier} External Matter`,
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
externalMatterBridge = new ChildMatterBridgeService("platform" /* PluginType.PLATFORM */, plugin, externalConfig._matter, externalConfig, this.api, this.externalPortService, this.options, this.ipcService, this.config);
|
|
268
|
-
this.childMatterBridges.set(externalMatterBridgeId, externalMatterBridge);
|
|
269
|
-
// Start the external bridge
|
|
270
|
-
externalMatterBridge.start().catch((error) => {
|
|
271
|
-
log.error(`Failed to start external Matter bridge for ${pluginIdentifier}:`, error);
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
// Publish accessories to appropriate bridge
|
|
276
|
-
if (externalMatterBridge) {
|
|
277
|
-
pluginAccessories.forEach((accessory) => {
|
|
278
|
-
externalMatterBridge.addAccessory(accessory);
|
|
279
|
-
log.info(`External accessory "${accessory.displayName}" added to automatic external Matter bridge`);
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
// Check if any child Matter bridge from this plugin can handle it
|
|
284
|
-
let publishedToChildBridge = false;
|
|
285
|
-
for (const [bridgeId, childBridge] of this.childMatterBridges) {
|
|
286
|
-
if (bridgeId.startsWith(`${pluginIdentifier}-`)) {
|
|
287
|
-
pluginAccessories.forEach((accessory) => {
|
|
288
|
-
childBridge.addAccessory(accessory);
|
|
289
|
-
log.info(`External accessory "${accessory.displayName}" added to plugin's Matter bridge`);
|
|
290
|
-
});
|
|
291
|
-
publishedToChildBridge = true;
|
|
292
|
-
break;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// External accessories are only published to child Matter bridges, not main bridge
|
|
296
|
-
if (!publishedToChildBridge) {
|
|
297
|
-
pluginAccessories.forEach((accessory) => {
|
|
298
|
-
log.debug(`External accessory "${accessory.displayName}" not published to Matter - configure _matter property for Matter support`);
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
});
|
|
250
|
+
// External accessories are published via HAP
|
|
251
|
+
// Plugins should use api.matter to register Matter accessories explicitly
|
|
303
252
|
}
|
|
304
253
|
handleRegisterPlatformAccessories(accessories) {
|
|
305
|
-
// Route to HAP bridge
|
|
254
|
+
// Route to HAP bridge
|
|
306
255
|
this.bridgeService.handleRegisterPlatformAccessories(accessories);
|
|
307
|
-
// Check if we need to also route to Matter bridges
|
|
308
|
-
for (const accessory of accessories) {
|
|
309
|
-
const platformKey = `${accessory._associatedPlugin}-${accessory._associatedPlatform}`;
|
|
310
|
-
const platformConfig = this.platformConfigs.get(platformKey);
|
|
311
|
-
if (platformConfig) {
|
|
312
|
-
// Check if platform has child Matter bridge
|
|
313
|
-
if (platformConfig._matter && typeof platformConfig._matter === 'object') {
|
|
314
|
-
const matterIdentifier = `${accessory._associatedPlugin}-${accessory._associatedPlatform}`;
|
|
315
|
-
const childMatterBridge = this.childMatterBridges.get(matterIdentifier);
|
|
316
|
-
if (childMatterBridge) {
|
|
317
|
-
childMatterBridge.addAccessory(accessory).catch((error) => {
|
|
318
|
-
log.error(`Failed to add accessory "${accessory.displayName}" to child Matter bridge:`, error);
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
256
|
}
|
|
325
257
|
handleUnregisterPlatformAccessories(accessories) {
|
|
326
|
-
// Route to HAP bridge
|
|
258
|
+
// Route to HAP bridge
|
|
327
259
|
this.bridgeService.handleUnregisterPlatformAccessories(accessories);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
260
|
+
}
|
|
261
|
+
handleRegisterMatterAccessory(accessory) {
|
|
262
|
+
if (!this.matterServer) {
|
|
263
|
+
log.warn('Cannot register Matter accessory - Matter server is not running');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this.matterServer.registerAccessory(accessory);
|
|
267
|
+
}
|
|
268
|
+
handleUnregisterMatterAccessory(uuid) {
|
|
269
|
+
if (!this.matterServer) {
|
|
270
|
+
log.warn('Cannot unregister Matter accessory - Matter server is not running');
|
|
271
|
+
return;
|
|
344
272
|
}
|
|
273
|
+
this.matterServer.unregisterAccessory(uuid);
|
|
274
|
+
}
|
|
275
|
+
handleUpdateMatterAccessoryState(uuid, cluster, attributes) {
|
|
276
|
+
if (!this.matterServer) {
|
|
277
|
+
log.warn('Cannot update Matter accessory state - Matter server is not running');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.matterServer.updateAccessoryState(uuid, cluster, attributes);
|
|
345
281
|
}
|
|
346
282
|
static loadConfig() {
|
|
347
283
|
// Look for the configuration file
|
|
@@ -386,6 +322,7 @@ export class Server {
|
|
|
386
322
|
bridge.username = bridge.username || defaultBridge.username;
|
|
387
323
|
bridge.pin = bridge.pin || defaultBridge.pin;
|
|
388
324
|
config.bridge = bridge;
|
|
325
|
+
// Main bridge username is always required and must be valid
|
|
389
326
|
const username = config.bridge.username;
|
|
390
327
|
if (!validMacAddress(username)) {
|
|
391
328
|
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.`);
|
|
@@ -401,6 +338,48 @@ export class Server {
|
|
|
401
338
|
config.platforms = [];
|
|
402
339
|
}
|
|
403
340
|
log.info('Loaded config.json with %s accessories and %s platforms.', config.accessories.length, config.platforms.length);
|
|
341
|
+
// Validate Matter configuration for port conflicts
|
|
342
|
+
if (config.bridge.matter || config.platforms.some((p) => p._bridge?.matter) || config.accessories.some((a) => a._bridge?.matter)) {
|
|
343
|
+
// Validate main bridge Matter config
|
|
344
|
+
if (config.bridge.matter) {
|
|
345
|
+
// Apply default name before validation if not set
|
|
346
|
+
if (!config.bridge.matter.name) {
|
|
347
|
+
config.bridge.matter.name = config.bridge.name ? `${config.bridge.name} (Matter)` : 'Homebridge Matter Bridge';
|
|
348
|
+
}
|
|
349
|
+
const validation = MatterConfigValidator.validate(config.bridge.matter);
|
|
350
|
+
if (!validation.isValid) {
|
|
351
|
+
log.error('Main bridge Matter configuration is invalid. Matter will not be enabled for the main bridge.');
|
|
352
|
+
delete config.bridge.matter;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Validate all child bridge Matter configs and check for port conflicts
|
|
356
|
+
const childMatterValidation = MatterConfigValidator.validateAllChildMatterConfigs(config.platforms, config.accessories);
|
|
357
|
+
if (!childMatterValidation.isValid) {
|
|
358
|
+
log.error('Some child bridge Matter configurations are invalid. Check the errors above.');
|
|
359
|
+
}
|
|
360
|
+
// Additionally, check for conflicts between main bridge Matter port and child bridge ports
|
|
361
|
+
if (config.bridge.matter?.port) {
|
|
362
|
+
const mainMatterPort = config.bridge.matter.port;
|
|
363
|
+
const childMatterPorts = [];
|
|
364
|
+
for (const platform of config.platforms) {
|
|
365
|
+
if (platform._bridge?.matter?.port) {
|
|
366
|
+
childMatterPorts.push(platform._bridge.matter.port);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const accessory of config.accessories) {
|
|
370
|
+
if (accessory._bridge?.matter?.port) {
|
|
371
|
+
childMatterPorts.push(accessory._bridge.matter.port);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (childMatterPorts.includes(mainMatterPort)) {
|
|
375
|
+
log.error(`Main bridge Matter port ${mainMatterPort} conflicts with a child bridge Matter port. Please use unique ports.`);
|
|
376
|
+
}
|
|
377
|
+
// Check for conflict with main bridge HAP port
|
|
378
|
+
if (config.bridge.port && Math.abs(config.bridge.port - mainMatterPort) < 10) {
|
|
379
|
+
log.warn(`Main bridge HAP port ${config.bridge.port} and Matter port ${mainMatterPort} are very close. Consider spacing them further apart.`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
404
383
|
if (config.bridge.advertiser) {
|
|
405
384
|
if (![
|
|
406
385
|
"bonjour-hap" /* MDNSAdvertiser.BONJOUR */,
|
|
@@ -455,63 +434,37 @@ export class Server {
|
|
|
455
434
|
}
|
|
456
435
|
const logger = Logger.withPrefix(displayName);
|
|
457
436
|
logger('Initializing %s accessory...', accessoryIdentifier);
|
|
437
|
+
// Handle child bridge (HAP, Matter-only, or HAP+Matter)
|
|
458
438
|
if (accessoryConfig._bridge) {
|
|
459
|
-
//
|
|
460
|
-
|
|
439
|
+
// Ensure the username is always uppercase if it exists (HAP bridges only)
|
|
440
|
+
if (accessoryConfig._bridge.username) {
|
|
441
|
+
accessoryConfig._bridge.username = accessoryConfig._bridge.username.toUpperCase();
|
|
442
|
+
}
|
|
443
|
+
// Generate unique key for this child bridge
|
|
444
|
+
const bridgeKey = this.getChildBridgeKey("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, plugin.getPluginIdentifier(), accessoryConfig._bridge);
|
|
461
445
|
try {
|
|
462
|
-
this.validateChildBridgeConfig("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, accessoryConfig._bridge);
|
|
446
|
+
this.validateChildBridgeConfig("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, plugin.getPluginIdentifier(), accessoryConfig._bridge, bridgeKey);
|
|
463
447
|
}
|
|
464
448
|
catch (error) {
|
|
465
449
|
log.error(error.message);
|
|
466
450
|
return;
|
|
467
451
|
}
|
|
468
452
|
let childBridge;
|
|
469
|
-
if (this.childBridges.has(
|
|
470
|
-
childBridge = this.childBridges.get(
|
|
471
|
-
logger(`Adding to existing child bridge ${
|
|
453
|
+
if (this.childBridges.has(bridgeKey)) {
|
|
454
|
+
childBridge = this.childBridges.get(bridgeKey);
|
|
455
|
+
logger(`Adding to existing child bridge ${bridgeKey}`);
|
|
472
456
|
}
|
|
473
457
|
else {
|
|
474
|
-
|
|
458
|
+
const bridgeType = accessoryConfig._bridge.matterOnly ? 'Matter-only' : 'HAP';
|
|
459
|
+
logger(`Initializing ${bridgeType} child bridge ${bridgeKey}`);
|
|
475
460
|
childBridge = new ChildBridgeService("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, plugin, accessoryConfig._bridge, this.config, this.options, this.api, this.ipcService, this.externalPortService);
|
|
476
|
-
this.childBridges.set(
|
|
461
|
+
this.childBridges.set(bridgeKey, childBridge);
|
|
477
462
|
}
|
|
478
463
|
// add config to child bridge service
|
|
479
464
|
childBridge.addConfig(accessoryConfig);
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
// Handle child Matter bridge for accessories
|
|
486
|
-
if (accessoryConfig._matter && typeof accessoryConfig._matter === 'object') {
|
|
487
|
-
const matterIdentifier = `${plugin.getPluginIdentifier()}-${accessoryIdentifier}`;
|
|
488
|
-
logger(`Initializing child Matter bridge for accessory ${accessoryIdentifier}`);
|
|
489
|
-
// Ensure Matter config has proper name
|
|
490
|
-
// Try to get a human-friendly name from the accessory
|
|
491
|
-
// Convert CamelCase accessory identifier to spaces
|
|
492
|
-
const formattedAccessoryId = accessoryIdentifier.replace(/([A-Z])/g, ' $1').trim();
|
|
493
|
-
// Get plugin name without 'homebridge-' prefix and replace dashes with spaces
|
|
494
|
-
const pluginDisplayName = plugin.getPluginIdentifier()
|
|
495
|
-
.replace('homebridge-', '')
|
|
496
|
-
.replace(/-/g, ' ')
|
|
497
|
-
.split(' ')
|
|
498
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
499
|
-
.join(' ');
|
|
500
|
-
const defaultName = accessoryConfig.name || formattedAccessoryId || pluginDisplayName;
|
|
501
|
-
const matterConfig = {
|
|
502
|
-
...accessoryConfig._matter,
|
|
503
|
-
name: accessoryConfig._matter.name || defaultName,
|
|
504
|
-
};
|
|
505
|
-
const childMatterBridge = new ChildMatterBridgeService("accessory" /* PluginType.ACCESSORY */, plugin, matterConfig, accessoryConfig, this.api, this.externalPortService, this.options, this.ipcService);
|
|
506
|
-
this.childMatterBridges.set(matterIdentifier, childMatterBridge);
|
|
507
|
-
// If _bridge is also defined, we're done - accessory uses both protocols
|
|
508
|
-
if (accessoryConfig._bridge) {
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
// If both _bridge and _matter are defined, the accessory is handled by child bridges
|
|
513
|
-
if (accessoryConfig._bridge || accessoryConfig._matter) {
|
|
514
|
-
return;
|
|
465
|
+
// Matter for child bridges is handled inside the forked child process (childBridgeFork.ts)
|
|
466
|
+
// to maintain isolation - each child process manages its own Matter server
|
|
467
|
+
return; // Done - child bridge
|
|
515
468
|
}
|
|
516
469
|
const accessoryInstance = new constructor(logger, accessoryConfig, this.api);
|
|
517
470
|
// pass accessoryIdentifier for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation
|
|
@@ -568,59 +521,29 @@ export class Server {
|
|
|
568
521
|
}
|
|
569
522
|
const logger = Logger.withPrefix(displayName);
|
|
570
523
|
logger('Initializing %s platform...', platformIdentifier);
|
|
571
|
-
//
|
|
572
|
-
const platformKey = `${plugin.getPluginIdentifier()}-${platformIdentifier}`;
|
|
573
|
-
this.platformConfigs.set(platformKey, platformConfig);
|
|
574
|
-
// Handle child HAP bridge
|
|
524
|
+
// Handle child bridge (HAP, Matter-only, or HAP+Matter)
|
|
575
525
|
if (platformConfig._bridge) {
|
|
576
|
-
//
|
|
577
|
-
|
|
526
|
+
// Ensure the username is always uppercase if it exists (HAP bridges only)
|
|
527
|
+
if (platformConfig._bridge.username) {
|
|
528
|
+
platformConfig._bridge.username = platformConfig._bridge.username.toUpperCase();
|
|
529
|
+
}
|
|
530
|
+
// Generate unique key for this child bridge
|
|
531
|
+
const bridgeKey = this.getChildBridgeKey("platform" /* PluginType.PLATFORM */, platformIdentifier, plugin.getPluginIdentifier(), platformConfig._bridge);
|
|
578
532
|
try {
|
|
579
|
-
this.validateChildBridgeConfig("platform" /* PluginType.PLATFORM */, platformIdentifier, platformConfig._bridge);
|
|
533
|
+
this.validateChildBridgeConfig("platform" /* PluginType.PLATFORM */, platformIdentifier, plugin.getPluginIdentifier(), platformConfig._bridge, bridgeKey);
|
|
580
534
|
}
|
|
581
535
|
catch (error) {
|
|
582
536
|
log.error(error.message);
|
|
583
537
|
return;
|
|
584
538
|
}
|
|
585
|
-
|
|
539
|
+
const bridgeType = platformConfig._bridge.matterOnly ? 'Matter-only' : 'HAP';
|
|
540
|
+
logger(`Initializing ${bridgeType} child bridge ${bridgeKey}`);
|
|
586
541
|
const childBridge = new ChildBridgeService("platform" /* PluginType.PLATFORM */, platformIdentifier, plugin, platformConfig._bridge, this.config, this.options, this.api, this.ipcService, this.externalPortService);
|
|
587
|
-
this.childBridges.set(
|
|
542
|
+
this.childBridges.set(bridgeKey, childBridge);
|
|
588
543
|
// add config to child bridge service
|
|
589
544
|
childBridge.addConfig(platformConfig);
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
// Handle child Matter bridge
|
|
596
|
-
if (platformConfig._matter && typeof platformConfig._matter === 'object') {
|
|
597
|
-
const matterIdentifier = `${plugin.getPluginIdentifier()}-${platformIdentifier}`;
|
|
598
|
-
logger(`Initializing child Matter bridge for ${platformIdentifier}`);
|
|
599
|
-
// Ensure Matter config has proper name
|
|
600
|
-
// Try to get a human-friendly name from the platform
|
|
601
|
-
// Convert CamelCase platform identifier to spaces (e.g., "Ring" stays "Ring", "MySmartHome" becomes "My Smart Home")
|
|
602
|
-
const formattedPlatformId = platformIdentifier.replace(/([A-Z])/g, ' $1').trim();
|
|
603
|
-
// Get plugin name without 'homebridge-' prefix and replace dashes with spaces
|
|
604
|
-
const pluginDisplayName = plugin.getPluginIdentifier()
|
|
605
|
-
.replace('homebridge-', '')
|
|
606
|
-
.replace(/-/g, ' ')
|
|
607
|
-
.split(' ')
|
|
608
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
609
|
-
.join(' ');
|
|
610
|
-
const defaultName = platformConfig.name || formattedPlatformId || pluginDisplayName;
|
|
611
|
-
const matterConfig = {
|
|
612
|
-
...platformConfig._matter,
|
|
613
|
-
name: platformConfig._matter.name || defaultName,
|
|
614
|
-
};
|
|
615
|
-
const childMatterBridge = new ChildMatterBridgeService("platform" /* PluginType.PLATFORM */, plugin, matterConfig, platformConfig, this.api, this.externalPortService, this.options, this.ipcService);
|
|
616
|
-
this.childMatterBridges.set(matterIdentifier, childMatterBridge);
|
|
617
|
-
// If _bridge is also defined, we're done - platform uses both protocols
|
|
618
|
-
if (platformConfig._bridge) {
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
// If both _bridge and _matter are defined, the platform is handled by child bridges
|
|
623
|
-
if (platformConfig._bridge || platformConfig._matter) {
|
|
545
|
+
// Matter for child bridges is handled inside the forked child process (childBridgeFork.ts)
|
|
546
|
+
// to maintain isolation - each child process manages its own Matter server
|
|
624
547
|
return;
|
|
625
548
|
}
|
|
626
549
|
const platform = new constructor(logger, platformConfig, this.api);
|
|
@@ -637,16 +560,48 @@ export class Server {
|
|
|
637
560
|
});
|
|
638
561
|
return promises;
|
|
639
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Generate a unique identifier for a child bridge
|
|
565
|
+
* - For HAP bridges: returns the uppercase MAC address
|
|
566
|
+
* - For Matter-only bridges: returns "matter-{plugin}-{identifier}"
|
|
567
|
+
*/
|
|
568
|
+
getChildBridgeKey(type, identifier, pluginName, bridgeConfig) {
|
|
569
|
+
if (bridgeConfig.matterOnly) {
|
|
570
|
+
// Matter-only bridge: generate identifier from plugin and platform/accessory name
|
|
571
|
+
return `matter-${pluginName}-${identifier}`;
|
|
572
|
+
}
|
|
573
|
+
// HAP bridge: use MAC address (already validated and uppercased)
|
|
574
|
+
return bridgeConfig.username.toUpperCase();
|
|
575
|
+
}
|
|
640
576
|
/**
|
|
641
577
|
* Validate an external bridge config
|
|
642
578
|
*/
|
|
643
|
-
validateChildBridgeConfig(type, identifier, bridgeConfig) {
|
|
579
|
+
validateChildBridgeConfig(type, identifier, pluginName, bridgeConfig, bridgeKey) {
|
|
580
|
+
// Matter-only bridges don't require HAP fields
|
|
581
|
+
if (bridgeConfig.matterOnly) {
|
|
582
|
+
if (!bridgeConfig.matter) {
|
|
583
|
+
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
584
|
+
+ 'Matter-only bridge must have a "matter" configuration block in _bridge.matter.');
|
|
585
|
+
}
|
|
586
|
+
// Check for duplicate Matter-only bridges
|
|
587
|
+
if (this.childBridges.has(bridgeKey)) {
|
|
588
|
+
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
589
|
+
+ 'A Matter-only bridge with this plugin and platform/accessory combination already exists. Each Matter-only bridge must be unique.');
|
|
590
|
+
}
|
|
591
|
+
// Matter-only bridges don't need username/pin validation
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
// HAP bridges (or HAP+Matter bridges) require username and pin
|
|
595
|
+
if (!bridgeConfig.username) {
|
|
596
|
+
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
597
|
+
+ 'Missing required field "_bridge.username". Set "matterOnly": true if you want a Matter-only bridge.');
|
|
598
|
+
}
|
|
644
599
|
if (!validMacAddress(bridgeConfig.username)) {
|
|
645
600
|
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
646
601
|
+ `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.`);
|
|
647
602
|
}
|
|
648
|
-
if (this.childBridges.has(
|
|
649
|
-
const childBridge = this.childBridges.get(
|
|
603
|
+
if (this.childBridges.has(bridgeKey)) {
|
|
604
|
+
const childBridge = this.childBridges.get(bridgeKey);
|
|
650
605
|
if (type === "platform" /* PluginType.PLATFORM */) {
|
|
651
606
|
// only a single platform can exist on one child bridge
|
|
652
607
|
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
@@ -658,6 +613,8 @@ export class Server {
|
|
|
658
613
|
+ `Duplicate username found in _bridge.username: "${bridgeConfig.username}". You can only group accessories of the same type in a child bridge.`);
|
|
659
614
|
}
|
|
660
615
|
}
|
|
616
|
+
// Check that child bridge username doesn't conflict with main bridge
|
|
617
|
+
// Main bridge always has a username (validated in loadConfig)
|
|
661
618
|
if (bridgeConfig.username === this.config.bridge.username.toUpperCase()) {
|
|
662
619
|
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
663
620
|
+ `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.`);
|
|
@@ -674,7 +631,9 @@ export class Server {
|
|
|
674
631
|
// noinspection SuspiciousTypeOfGuard
|
|
675
632
|
if (typeof username === 'string') {
|
|
676
633
|
const childBridge = this.childBridges.get(username.toUpperCase());
|
|
677
|
-
childBridge
|
|
634
|
+
if (childBridge) {
|
|
635
|
+
childBridge.restartChildBridge();
|
|
636
|
+
}
|
|
678
637
|
}
|
|
679
638
|
});
|
|
680
639
|
// handle stop child bridge event
|
|
@@ -682,7 +641,9 @@ export class Server {
|
|
|
682
641
|
// noinspection SuspiciousTypeOfGuard
|
|
683
642
|
if (typeof username === 'string') {
|
|
684
643
|
const childBridge = this.childBridges.get(username.toUpperCase());
|
|
685
|
-
childBridge
|
|
644
|
+
if (childBridge) {
|
|
645
|
+
childBridge.stopChildBridge();
|
|
646
|
+
}
|
|
686
647
|
}
|
|
687
648
|
});
|
|
688
649
|
// handle start child bridge event
|
|
@@ -690,110 +651,176 @@ export class Server {
|
|
|
690
651
|
// noinspection SuspiciousTypeOfGuard
|
|
691
652
|
if (typeof username === 'string') {
|
|
692
653
|
const childBridge = this.childBridges.get(username.toUpperCase());
|
|
693
|
-
childBridge
|
|
654
|
+
if (childBridge) {
|
|
655
|
+
childBridge.startChildBridge();
|
|
656
|
+
}
|
|
694
657
|
}
|
|
695
658
|
});
|
|
696
659
|
this.ipcService.on("childBridgeMetadataRequest" /* IpcIncomingEvent.CHILD_BRIDGE_METADATA_REQUEST */, () => {
|
|
697
|
-
|
|
660
|
+
const childBridgeMetadata = Array.from(this.childBridges.values()).map(x => x.getMetadata());
|
|
661
|
+
this.ipcService.sendMessage("childBridgeMetadataResponse" /* IpcOutgoingEvent.CHILD_BRIDGE_METADATA_RESPONSE */, childBridgeMetadata);
|
|
698
662
|
});
|
|
699
663
|
// Matter bridge IPC handlers
|
|
664
|
+
// Main bridge Matter server runs in this process alongside HAP
|
|
665
|
+
// Child bridge Matter servers run in forked child processes (see childBridgeFork.ts)
|
|
700
666
|
this.ipcService.on("restartMatterBridge" /* IpcIncomingEvent.RESTART_MATTER_BRIDGE */, (matterBridgeId) => {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
667
|
+
if (matterBridgeId === 'main-bridge') {
|
|
668
|
+
// Main Matter bridge runs in the main process, so restart the entire Homebridge instance
|
|
669
|
+
log.info('Restarting Homebridge (Matter bridge restart requested)...');
|
|
670
|
+
process.kill(process.pid, 'SIGTERM');
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
// Handle child bridge using cache for efficient lookup
|
|
674
|
+
const childBridge = this.matterBridgeCache.get(matterBridgeId);
|
|
675
|
+
if (childBridge) {
|
|
676
|
+
childBridge.restartChildBridge();
|
|
707
677
|
}
|
|
708
678
|
else {
|
|
709
|
-
log.warn(`
|
|
710
|
-
log.debug('Available Matter bridges:', Array.from(this.childMatterBridges.keys()));
|
|
679
|
+
log.warn(`Child bridge ${matterBridgeId} not found for Matter restart`);
|
|
711
680
|
}
|
|
712
681
|
}
|
|
713
682
|
});
|
|
714
683
|
this.ipcService.on("stopMatterBridge" /* IpcIncomingEvent.STOP_MATTER_BRIDGE */, (matterBridgeId) => {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
684
|
+
if (matterBridgeId === 'main-bridge') {
|
|
685
|
+
log.warn('Stop requested for main Matter bridge - this is not supported');
|
|
686
|
+
log.info('The main Matter bridge runs alongside the HAP bridge and cannot be stopped independently');
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
// Handle child bridge using cache for efficient lookup
|
|
690
|
+
const childBridge = this.matterBridgeCache.get(matterBridgeId);
|
|
691
|
+
if (childBridge) {
|
|
692
|
+
childBridge.stopChildBridge();
|
|
721
693
|
}
|
|
722
694
|
else {
|
|
723
|
-
log.warn(`
|
|
695
|
+
log.warn(`Child bridge ${matterBridgeId} not found for Matter stop`);
|
|
724
696
|
}
|
|
725
697
|
}
|
|
726
698
|
});
|
|
727
699
|
this.ipcService.on("startMatterBridge" /* IpcIncomingEvent.START_MATTER_BRIDGE */, (matterBridgeId) => {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
700
|
+
if (matterBridgeId === 'main-bridge') {
|
|
701
|
+
log.warn('Start requested for main Matter bridge - this is not supported');
|
|
702
|
+
log.info('The main Matter bridge starts automatically with Homebridge');
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
// Handle child bridge using cache for efficient lookup
|
|
706
|
+
const childBridge = this.matterBridgeCache.get(matterBridgeId);
|
|
707
|
+
if (childBridge) {
|
|
708
|
+
childBridge.startChildBridge();
|
|
734
709
|
}
|
|
735
710
|
else {
|
|
736
|
-
log.warn(`
|
|
711
|
+
log.warn(`Child bridge ${matterBridgeId} not found for Matter start`);
|
|
737
712
|
}
|
|
738
713
|
}
|
|
739
714
|
});
|
|
740
715
|
this.ipcService.on("matterBridgeMetadataRequest" /* IpcIncomingEvent.MATTER_BRIDGE_METADATA_REQUEST */, () => {
|
|
741
|
-
|
|
742
|
-
|
|
716
|
+
const matterBridges = [];
|
|
717
|
+
// Add main bridge Matter metadata if enabled
|
|
718
|
+
if (this.matterServer && this.matterServer.isServerRunning()) {
|
|
719
|
+
const commissioningInfo = this.matterServer.getCommissioningInfo();
|
|
720
|
+
matterBridges.push({
|
|
721
|
+
type: 'matter',
|
|
722
|
+
status: 'ok',
|
|
723
|
+
port: this.config.bridge.matter?.port || 5540,
|
|
724
|
+
setupUri: commissioningInfo.qrCode,
|
|
725
|
+
pin: commissioningInfo.manualPairingCode,
|
|
726
|
+
serialNumber: commissioningInfo.serialNumber,
|
|
727
|
+
name: this.config.bridge.name,
|
|
728
|
+
plugin: 'main-bridge',
|
|
729
|
+
identifier: 'main-bridge',
|
|
730
|
+
deviceCount: this.matterServer.getAccessories().length,
|
|
731
|
+
commissioned: commissioningInfo.commissioned,
|
|
732
|
+
matterEnabled: true,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
// Add child bridge Matter metadata
|
|
736
|
+
for (const childBridge of this.childBridges.values()) {
|
|
737
|
+
const matterInfo = childBridge.getCommissioningInfo();
|
|
738
|
+
const matterConfig = childBridge.getMatterConfig();
|
|
739
|
+
if (matterInfo && matterConfig) {
|
|
740
|
+
const metadata = childBridge.getMetadata();
|
|
741
|
+
// Use plugin-identifier format to match storage
|
|
742
|
+
const childIdentifier = `${metadata.plugin}-${childBridge.identifier}`;
|
|
743
|
+
// Populate cache for efficient lookups
|
|
744
|
+
this.matterBridgeCache.set(childIdentifier, childBridge);
|
|
745
|
+
matterBridges.push({
|
|
746
|
+
type: 'matter',
|
|
747
|
+
status: metadata.status === 'ok' ? 'ok' : 'pending',
|
|
748
|
+
port: matterConfig.port,
|
|
749
|
+
setupUri: matterInfo.qrCode,
|
|
750
|
+
pin: matterInfo.manualPairingCode,
|
|
751
|
+
serialNumber: matterInfo.serialNumber,
|
|
752
|
+
name: matterConfig.name || metadata.name,
|
|
753
|
+
plugin: metadata.plugin,
|
|
754
|
+
identifier: childIdentifier,
|
|
755
|
+
commissioned: matterInfo.commissioned,
|
|
756
|
+
username: metadata.username,
|
|
757
|
+
matterEnabled: true,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
this.ipcService.sendMessage("matterBridgeMetadataResponse" /* IpcOutgoingEvent.MATTER_BRIDGE_METADATA_RESPONSE */, matterBridges);
|
|
743
762
|
});
|
|
744
763
|
this.ipcService.on("matterAccessoriesRequest" /* IpcIncomingEvent.MATTER_ACCESSORIES_REQUEST */, () => {
|
|
745
|
-
//
|
|
764
|
+
// Return Matter accessories from all bridges
|
|
746
765
|
const allMatterAccessories = {
|
|
747
766
|
children: {},
|
|
748
767
|
};
|
|
749
|
-
// Add
|
|
750
|
-
|
|
751
|
-
allMatterAccessories.children[
|
|
768
|
+
// Add main bridge accessories
|
|
769
|
+
if (this.matterServer) {
|
|
770
|
+
allMatterAccessories.children['main-bridge'] = this.matterServer.getAccessories();
|
|
752
771
|
}
|
|
772
|
+
// Child bridge accessories are managed by child processes
|
|
753
773
|
this.ipcService.sendMessage("matterAccessoriesResponse" /* IpcOutgoingEvent.MATTER_ACCESSORIES_RESPONSE */, allMatterAccessories);
|
|
754
774
|
});
|
|
755
|
-
// Handle new Matter IPC events
|
|
756
|
-
this.ipcService.on("toggleMatterDevice" /* IpcIncomingEvent.TOGGLE_MATTER_DEVICE */, (data) => {
|
|
757
|
-
if (data && data.uuid) {
|
|
758
|
-
// This would toggle Matter for a specific device
|
|
759
|
-
// Implementation depends on how devices are managed
|
|
760
|
-
log.debug(`Toggle Matter for device ${data.uuid}: ${data.enabled}`);
|
|
761
|
-
// Send status update
|
|
762
|
-
this.ipcService.sendMessage("matterDeviceStatusUpdate" /* IpcOutgoingEvent.MATTER_DEVICE_STATUS_UPDATE */, {
|
|
763
|
-
uuid: data.uuid,
|
|
764
|
-
matterEnabled: data.enabled,
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
});
|
|
768
775
|
this.ipcService.on("matterCommissioningInfoRequest" /* IpcIncomingEvent.MATTER_COMMISSIONING_INFO_REQUEST */, (matterBridgeId) => {
|
|
769
|
-
let
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
776
|
+
let rawInfo = { commissioned: false };
|
|
777
|
+
// Handle main bridge
|
|
778
|
+
if (matterBridgeId === 'main-bridge') {
|
|
779
|
+
if (this.matterServer && this.matterServer.isServerRunning()) {
|
|
780
|
+
rawInfo = this.matterServer.getCommissioningInfo();
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
log.debug(`[Matter] Server not available when commissioning info requested for: ${matterBridgeId}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
// Handle child bridge using plugin-identifier format (e.g., homebridge-virtual-accessories-VirtualAccessoriesForHomebridge)
|
|
788
|
+
// Use cache for efficient O(1) lookup
|
|
789
|
+
const childBridge = this.matterBridgeCache.get(matterBridgeId);
|
|
790
|
+
if (childBridge) {
|
|
791
|
+
rawInfo = childBridge.getCommissioningInfo() || { commissioned: false };
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
log.debug(`[Matter] Child bridge ${matterBridgeId} not found when commissioning info requested`);
|
|
795
|
+
}
|
|
774
796
|
}
|
|
775
|
-
|
|
797
|
+
// Transform property names to match MatterCommissioningInfo interface
|
|
798
|
+
const commissioningInfo = {
|
|
799
|
+
commissioned: rawInfo.commissioned || false,
|
|
800
|
+
pin: rawInfo.manualPairingCode, // Map manualPairingCode -> pin
|
|
801
|
+
setupUri: rawInfo.qrCode, // Map qrCode -> setupUri
|
|
802
|
+
serialNumber: rawInfo.serialNumber,
|
|
803
|
+
};
|
|
804
|
+
this.ipcService.sendMessage("matterCommissioningInfoResponse" /* IpcOutgoingEvent.MATTER_COMMISSIONING_INFO_RESPONSE */, commissioningInfo);
|
|
776
805
|
});
|
|
777
806
|
}
|
|
778
807
|
printSetupInfo(pin) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
console.log(this.bridgeService.bridge.setupURI());
|
|
808
|
+
log.info('Setup Payload:');
|
|
809
|
+
log.info(this.bridgeService.bridge.setupURI());
|
|
782
810
|
if (!this.options.hideQRCode) {
|
|
783
|
-
|
|
811
|
+
log.info('Scan this code with your HomeKit app on your iOS device to pair with Homebridge:');
|
|
784
812
|
qrcode.setErrorLevel('M'); // HAP specifies level M or higher for ECC
|
|
785
813
|
qrcode.generate(this.bridgeService.bridge.setupURI());
|
|
786
|
-
|
|
814
|
+
log.info('Or enter this code with your HomeKit app on your iOS device to pair with Homebridge:');
|
|
787
815
|
}
|
|
788
816
|
else {
|
|
789
|
-
|
|
817
|
+
log.info('Enter this code with your HomeKit app on your iOS device to pair with Homebridge:');
|
|
790
818
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
/* eslint-enable no-console */
|
|
819
|
+
log.info(chalk.black.bgWhite(' '));
|
|
820
|
+
log.info(chalk.black.bgWhite(' ┌────────────┐ '));
|
|
821
|
+
log.info(chalk.black.bgWhite(` │ ${pin} │ `));
|
|
822
|
+
log.info(chalk.black.bgWhite(' └────────────┘ '));
|
|
823
|
+
log.info(chalk.black.bgWhite(' '));
|
|
797
824
|
}
|
|
798
825
|
}
|
|
799
826
|
//# sourceMappingURL=server.js.map
|