homebridge 2.0.0-beta.3 → 2.0.0-beta.31
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/README.md +16 -17
- package/bin/homebridge.js +22 -0
- package/config-sample.json +3 -3
- package/dist/api.d.ts +457 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +221 -0
- package/dist/api.js.map +1 -0
- package/{lib → dist}/bridgeService.d.ts +19 -10
- package/dist/bridgeService.d.ts.map +1 -0
- package/{lib → dist}/bridgeService.js +85 -117
- package/dist/bridgeService.js.map +1 -0
- package/dist/childBridgeFork.d.ts +65 -0
- package/dist/childBridgeFork.d.ts.map +1 -0
- package/dist/childBridgeFork.js +531 -0
- package/dist/childBridgeFork.js.map +1 -0
- package/{lib → dist}/childBridgeService.d.ts +30 -7
- package/dist/childBridgeService.d.ts.map +1 -0
- package/{lib → dist}/childBridgeService.js +127 -69
- package/dist/childBridgeService.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +90 -0
- package/dist/cli.js.map +1 -0
- package/dist/externalPortService.d.ts +54 -0
- package/dist/externalPortService.d.ts.map +1 -0
- package/dist/externalPortService.js +125 -0
- package/dist/externalPortService.js.map +1 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/{lib → dist}/ipcService.d.ts +22 -5
- package/dist/ipcService.d.ts.map +1 -0
- package/{lib → dist}/ipcService.js +12 -12
- package/dist/ipcService.js.map +1 -0
- package/{lib → dist}/logger.d.ts +12 -6
- package/dist/logger.d.ts.map +1 -0
- package/{lib → dist}/logger.js +27 -28
- package/dist/logger.js.map +1 -0
- package/dist/matter/index.d.ts +123 -0
- package/dist/matter/index.d.ts.map +1 -0
- package/dist/matter/index.js +19 -0
- package/dist/matter/index.js.map +1 -0
- package/dist/matter/matterAccessoryCache.d.ts +96 -0
- package/dist/matter/matterAccessoryCache.d.ts.map +1 -0
- package/dist/matter/matterAccessoryCache.js +192 -0
- package/dist/matter/matterAccessoryCache.js.map +1 -0
- package/dist/matter/matterBehaviors.d.ts +194 -0
- package/dist/matter/matterBehaviors.d.ts.map +1 -0
- package/dist/matter/matterBehaviors.js +665 -0
- package/dist/matter/matterBehaviors.js.map +1 -0
- package/dist/matter/matterConfigValidator.d.ts +81 -0
- package/dist/matter/matterConfigValidator.d.ts.map +1 -0
- package/dist/matter/matterConfigValidator.js +240 -0
- package/dist/matter/matterConfigValidator.js.map +1 -0
- package/dist/matter/matterErrorHandler.d.ts +106 -0
- package/dist/matter/matterErrorHandler.d.ts.map +1 -0
- package/dist/matter/matterErrorHandler.js +495 -0
- package/dist/matter/matterErrorHandler.js.map +1 -0
- package/dist/matter/matterLogFormatter.d.ts +19 -0
- package/dist/matter/matterLogFormatter.d.ts.map +1 -0
- package/dist/matter/matterLogFormatter.js +144 -0
- package/dist/matter/matterLogFormatter.js.map +1 -0
- package/dist/matter/matterNetworkMonitor.d.ts +68 -0
- package/dist/matter/matterNetworkMonitor.d.ts.map +1 -0
- package/dist/matter/matterNetworkMonitor.js +249 -0
- package/dist/matter/matterNetworkMonitor.js.map +1 -0
- package/dist/matter/matterServer.d.ts +656 -0
- package/dist/matter/matterServer.d.ts.map +1 -0
- package/dist/matter/matterServer.js +1690 -0
- package/dist/matter/matterServer.js.map +1 -0
- package/dist/matter/matterServerHelpers.d.ts +81 -0
- package/dist/matter/matterServerHelpers.d.ts.map +1 -0
- package/dist/matter/matterServerHelpers.js +323 -0
- package/dist/matter/matterServerHelpers.js.map +1 -0
- package/dist/matter/matterSharedTypes.d.ts +170 -0
- package/dist/matter/matterSharedTypes.d.ts.map +1 -0
- package/dist/matter/matterSharedTypes.js +52 -0
- package/dist/matter/matterSharedTypes.js.map +1 -0
- package/dist/matter/matterStorage.d.ts +128 -0
- package/dist/matter/matterStorage.d.ts.map +1 -0
- package/dist/matter/matterStorage.js +415 -0
- package/dist/matter/matterStorage.js.map +1 -0
- package/dist/matter/matterTypes.d.ts +745 -0
- package/dist/matter/matterTypes.d.ts.map +1 -0
- package/dist/matter/matterTypes.js +174 -0
- package/dist/matter/matterTypes.js.map +1 -0
- package/{lib → dist}/platformAccessory.d.ts +8 -6
- package/dist/platformAccessory.d.ts.map +1 -0
- package/{lib → dist}/platformAccessory.js +19 -16
- package/dist/platformAccessory.js.map +1 -0
- package/{lib → dist}/plugin.d.ts +2 -3
- package/dist/plugin.d.ts.map +1 -0
- package/{lib → dist}/plugin.js +39 -51
- package/dist/plugin.js.map +1 -0
- package/{lib → dist}/pluginManager.d.ts +3 -3
- package/dist/pluginManager.d.ts.map +1 -0
- package/{lib → dist}/pluginManager.js +76 -81
- package/dist/pluginManager.js.map +1 -0
- package/{lib → dist}/server.d.ts +23 -1
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +811 -0
- package/dist/server.js.map +1 -0
- package/{lib → dist}/storageService.d.ts.map +1 -1
- package/dist/storageService.js +41 -0
- package/dist/storageService.js.map +1 -0
- package/{lib → dist}/user.d.ts +1 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +32 -0
- package/dist/user.js.map +1 -0
- package/{lib → dist}/util/mac.d.ts +1 -0
- package/dist/util/mac.d.ts.map +1 -0
- package/dist/util/mac.js +14 -0
- package/dist/util/mac.js.map +1 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -0
- package/package.json +48 -49
- package/bin/homebridge +0 -17
- package/lib/api.d.ts +0 -210
- package/lib/api.d.ts.map +0 -1
- package/lib/api.js +0 -155
- package/lib/api.js.map +0 -1
- package/lib/bridgeService.d.ts.map +0 -1
- package/lib/bridgeService.js.map +0 -1
- package/lib/childBridgeFork.d.ts +0 -37
- package/lib/childBridgeFork.d.ts.map +0 -1
- package/lib/childBridgeFork.js +0 -244
- package/lib/childBridgeFork.js.map +0 -1
- package/lib/childBridgeService.d.ts.map +0 -1
- package/lib/childBridgeService.js.map +0 -1
- package/lib/cli.d.ts +0 -4
- package/lib/cli.d.ts.map +0 -1
- package/lib/cli.js +0 -111
- package/lib/cli.js.map +0 -1
- package/lib/externalPortService.d.ts +0 -33
- package/lib/externalPortService.d.ts.map +0 -1
- package/lib/externalPortService.js +0 -64
- package/lib/externalPortService.js.map +0 -1
- package/lib/index.d.ts +0 -76
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js +0 -72
- package/lib/index.js.map +0 -1
- package/lib/ipcService.d.ts.map +0 -1
- package/lib/ipcService.js.map +0 -1
- package/lib/logger.d.ts.map +0 -1
- package/lib/logger.js.map +0 -1
- package/lib/platformAccessory.d.ts.map +0 -1
- package/lib/platformAccessory.js.map +0 -1
- package/lib/plugin.d.ts.map +0 -1
- package/lib/plugin.js.map +0 -1
- package/lib/pluginManager.d.ts.map +0 -1
- package/lib/pluginManager.js.map +0 -1
- package/lib/server.d.ts.map +0 -1
- package/lib/server.js +0 -457
- package/lib/server.js.map +0 -1
- package/lib/storageService.js +0 -70
- package/lib/storageService.js.map +0 -1
- package/lib/user.d.ts.map +0 -1
- package/lib/user.js +0 -36
- package/lib/user.js.map +0 -1
- package/lib/util/mac.d.ts.map +0 -1
- package/lib/util/mac.js +0 -20
- package/lib/util/mac.js.map +0 -1
- package/lib/version.d.ts.map +0 -1
- package/lib/version.js +0 -21
- package/lib/version.js.map +0 -1
- /package/{lib → dist}/storageService.d.ts +0 -0
- /package/{lib → dist}/version.d.ts +0 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import qrcode from 'qrcode-terminal';
|
|
5
|
+
import { HomebridgeAPI } from './api.js';
|
|
6
|
+
import { BridgeService } from './bridgeService.js';
|
|
7
|
+
import { ChildBridgeService } from './childBridgeService.js';
|
|
8
|
+
import { ExternalPortService } from './externalPortService.js';
|
|
9
|
+
import { IpcService } from './ipcService.js';
|
|
10
|
+
import { Logger } from './logger.js';
|
|
11
|
+
import { MatterConfigValidator, MatterServer } from './matter/index.js';
|
|
12
|
+
import { PluginManager } from './pluginManager.js';
|
|
13
|
+
import { User } from './user.js';
|
|
14
|
+
import { generate, validMacAddress } from './util/mac.js';
|
|
15
|
+
const log = Logger.internal;
|
|
16
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
17
|
+
export var ServerStatus;
|
|
18
|
+
(function (ServerStatus) {
|
|
19
|
+
/**
|
|
20
|
+
* When the server is starting up
|
|
21
|
+
*/
|
|
22
|
+
ServerStatus["PENDING"] = "pending";
|
|
23
|
+
/**
|
|
24
|
+
* When the server is online and has published the main bridge
|
|
25
|
+
*/
|
|
26
|
+
ServerStatus["OK"] = "ok";
|
|
27
|
+
/**
|
|
28
|
+
* When the server is shutting down
|
|
29
|
+
*/
|
|
30
|
+
ServerStatus["DOWN"] = "down";
|
|
31
|
+
})(ServerStatus || (ServerStatus = {}));
|
|
32
|
+
export class Server {
|
|
33
|
+
options;
|
|
34
|
+
api;
|
|
35
|
+
pluginManager;
|
|
36
|
+
bridgeService;
|
|
37
|
+
ipcService;
|
|
38
|
+
externalPortService;
|
|
39
|
+
config;
|
|
40
|
+
// used to keep track of child bridges
|
|
41
|
+
// Key is HAP username (MAC address)
|
|
42
|
+
childBridges = new Map();
|
|
43
|
+
// Matter server instance for main bridge (if enabled)
|
|
44
|
+
matterServer;
|
|
45
|
+
// External Matter servers for accessories that need their own bridge
|
|
46
|
+
// Key is accessory UUID, value is MatterServer instance
|
|
47
|
+
externalMatterServers = new Map();
|
|
48
|
+
// current server status
|
|
49
|
+
serverStatus = "pending" /* ServerStatus.PENDING */;
|
|
50
|
+
constructor(options = {}) {
|
|
51
|
+
this.options = options;
|
|
52
|
+
this.config = Server.loadConfig();
|
|
53
|
+
// object we feed to Plugins and BridgeService
|
|
54
|
+
this.api = new HomebridgeAPI();
|
|
55
|
+
this.ipcService = new IpcService();
|
|
56
|
+
this.externalPortService = new ExternalPortService(this.config.ports, this.config.matterPorts);
|
|
57
|
+
// set status to pending
|
|
58
|
+
this.setServerStatus("pending" /* ServerStatus.PENDING */);
|
|
59
|
+
// create new plugin manager
|
|
60
|
+
const pluginManagerOptions = {
|
|
61
|
+
activePlugins: this.config.plugins,
|
|
62
|
+
disabledPlugins: this.config.disabledPlugins,
|
|
63
|
+
customPluginPath: options.customPluginPath,
|
|
64
|
+
strictPluginResolution: options.strictPluginResolution,
|
|
65
|
+
};
|
|
66
|
+
this.pluginManager = new PluginManager(this.api, pluginManagerOptions);
|
|
67
|
+
// create new bridge service
|
|
68
|
+
const bridgeConfig = {
|
|
69
|
+
cachedAccessoriesDir: User.cachedAccessoryPath(),
|
|
70
|
+
cachedAccessoriesItemName: 'cachedAccessories',
|
|
71
|
+
};
|
|
72
|
+
// shallow copy the homebridge options to the bridge options object
|
|
73
|
+
Object.assign(bridgeConfig, this.options);
|
|
74
|
+
this.bridgeService = new BridgeService(this.api, this.pluginManager, this.externalPortService, bridgeConfig, this.config.bridge);
|
|
75
|
+
// Handle platform accessory registration
|
|
76
|
+
this.api.on("registerPlatformAccessories" /* InternalAPIEvent.REGISTER_PLATFORM_ACCESSORIES */, this.handleRegisterPlatformAccessories.bind(this));
|
|
77
|
+
this.api.on("unregisterPlatformAccessories" /* InternalAPIEvent.UNREGISTER_PLATFORM_ACCESSORIES */, this.handleUnregisterPlatformAccessories.bind(this));
|
|
78
|
+
// Handle external accessories (cameras, etc.)
|
|
79
|
+
this.api.on("publishExternalAccessories" /* InternalAPIEvent.PUBLISH_EXTERNAL_ACCESSORIES */, this.handlePublishExternalAccessories.bind(this));
|
|
80
|
+
// Handle Matter accessory registration (matching HAP pattern)
|
|
81
|
+
this.api.on("publishExternalMatterAccessories" /* InternalAPIEvent.PUBLISH_EXTERNAL_MATTER_ACCESSORIES */, this.handlePublishExternalMatterAccessories.bind(this));
|
|
82
|
+
this.api.on("registerMatterPlatformAccessories" /* InternalAPIEvent.REGISTER_MATTER_PLATFORM_ACCESSORIES */, this.handleRegisterMatterPlatformAccessories.bind(this));
|
|
83
|
+
this.api.on("unregisterMatterPlatformAccessories" /* InternalAPIEvent.UNREGISTER_MATTER_PLATFORM_ACCESSORIES */, this.handleUnregisterMatterPlatformAccessories.bind(this));
|
|
84
|
+
this.api.on("updateMatterAccessoryState" /* InternalAPIEvent.UPDATE_MATTER_ACCESSORY_STATE */, this.handleUpdateMatterAccessoryState.bind(this));
|
|
85
|
+
// watch bridge events to check when server is online
|
|
86
|
+
this.bridgeService.bridge.on("advertised" /* AccessoryEventTypes.ADVERTISED */, () => {
|
|
87
|
+
this.setServerStatus("ok" /* ServerStatus.OK */);
|
|
88
|
+
});
|
|
89
|
+
// watch for the paired event to update the server status
|
|
90
|
+
this.bridgeService.bridge.on("paired" /* AccessoryEventTypes.PAIRED */, () => {
|
|
91
|
+
this.setServerStatus(this.serverStatus);
|
|
92
|
+
});
|
|
93
|
+
// watch for the unpaired event to update the server status
|
|
94
|
+
this.bridgeService.bridge.on("unpaired" /* AccessoryEventTypes.UNPAIRED */, () => {
|
|
95
|
+
this.setServerStatus(this.serverStatus);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Set the current server status and update parent via IPC
|
|
100
|
+
* @param status
|
|
101
|
+
*/
|
|
102
|
+
setServerStatus(status) {
|
|
103
|
+
this.serverStatus = status;
|
|
104
|
+
const statusUpdate = {
|
|
105
|
+
status: this.serverStatus,
|
|
106
|
+
paired: this.bridgeService?.bridge?._accessoryInfo?.paired() ?? null,
|
|
107
|
+
setupUri: this.bridgeService?.bridge?.setupURI() ?? null,
|
|
108
|
+
name: this.config.bridge.name,
|
|
109
|
+
username: this.config.bridge.username,
|
|
110
|
+
pin: this.config.bridge.pin,
|
|
111
|
+
matter: {
|
|
112
|
+
enabled: false,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
// Include Matter commissioning info if Matter is enabled
|
|
116
|
+
if (this.matterServer) {
|
|
117
|
+
const commissioningInfo = this.matterServer.getCommissioningInfo();
|
|
118
|
+
statusUpdate.matter = {
|
|
119
|
+
enabled: true,
|
|
120
|
+
port: this.config.bridge.matter?.port,
|
|
121
|
+
setupUri: commissioningInfo.qrCode,
|
|
122
|
+
pin: commissioningInfo.manualPairingCode,
|
|
123
|
+
serialNumber: commissioningInfo.serialNumber,
|
|
124
|
+
commissioned: commissioningInfo.commissioned || false,
|
|
125
|
+
deviceCount: this.matterServer.getAccessories().length,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
else if (this.config.bridge.matter) {
|
|
129
|
+
// Matter is configured but not yet started (or failed to start)
|
|
130
|
+
statusUpdate.matter = {
|
|
131
|
+
enabled: false,
|
|
132
|
+
port: this.config.bridge.matter?.port,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
this.ipcService.sendMessage("serverStatusUpdate" /* IpcOutgoingEvent.SERVER_STATUS_UPDATE */, statusUpdate);
|
|
136
|
+
}
|
|
137
|
+
async start() {
|
|
138
|
+
if (this.config.bridge.disableIpc !== true) {
|
|
139
|
+
this.initializeIpcEventHandlers();
|
|
140
|
+
}
|
|
141
|
+
const promises = [];
|
|
142
|
+
// load the cached accessories
|
|
143
|
+
await this.bridgeService.loadCachedPlatformAccessoriesFromDisk();
|
|
144
|
+
// initialize plugins
|
|
145
|
+
await this.pluginManager.initializeInstalledPlugins();
|
|
146
|
+
// Initialize Matter server for main bridge if enabled
|
|
147
|
+
await this.initializeMatterServer();
|
|
148
|
+
if (this.config.platforms.length > 0) {
|
|
149
|
+
promises.push(...this.loadPlatforms());
|
|
150
|
+
}
|
|
151
|
+
if (this.config.accessories.length > 0) {
|
|
152
|
+
this.loadAccessories();
|
|
153
|
+
}
|
|
154
|
+
// start child bridges
|
|
155
|
+
for (const childBridge of this.childBridges.values()) {
|
|
156
|
+
childBridge.start();
|
|
157
|
+
}
|
|
158
|
+
// restore cached accessories
|
|
159
|
+
this.bridgeService.restoreCachedPlatformAccessories();
|
|
160
|
+
this.restoreCachedMatterAccessories();
|
|
161
|
+
this.api.signalFinished();
|
|
162
|
+
// wait for all platforms to publish their accessories before we publish the bridge
|
|
163
|
+
await Promise.all(promises)
|
|
164
|
+
.then(() => this.publishBridge());
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Initialize Matter server for main bridge if enabled
|
|
168
|
+
*/
|
|
169
|
+
async initializeMatterServer() {
|
|
170
|
+
// Check if main bridge has matter configuration
|
|
171
|
+
if (!this.config.bridge.matter) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Declare matterPort outside try block so it's accessible in catch
|
|
175
|
+
let matterPort;
|
|
176
|
+
try {
|
|
177
|
+
log.info('Initializing Matter server for main bridge...');
|
|
178
|
+
// Allocate port from pool if not explicitly configured
|
|
179
|
+
matterPort = this.config.bridge.matter.port;
|
|
180
|
+
if (!matterPort) {
|
|
181
|
+
matterPort = await this.externalPortService.requestPort(`${this.config.bridge.username}:MATTER`);
|
|
182
|
+
if (!matterPort) {
|
|
183
|
+
matterPort = 5540; // Default Matter port
|
|
184
|
+
log.warn('No port available from pool for main Matter bridge, using default port 5540');
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
log.info(`Allocated port ${matterPort} from pool for main Matter bridge`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Create Matter server instance with config inheritance from main bridge
|
|
191
|
+
const serialNumber = this.config.bridge.username.replace(/:/g, '');
|
|
192
|
+
// Normalize bind config to array format
|
|
193
|
+
const networkInterfaces = this.config.bridge.bind
|
|
194
|
+
? Array.isArray(this.config.bridge.bind)
|
|
195
|
+
? this.config.bridge.bind
|
|
196
|
+
: [this.config.bridge.bind]
|
|
197
|
+
: undefined;
|
|
198
|
+
this.matterServer = new MatterServer({
|
|
199
|
+
storagePath: User.matterPath(),
|
|
200
|
+
port: matterPort,
|
|
201
|
+
uniqueId: serialNumber,
|
|
202
|
+
manufacturer: this.config.bridge.manufacturer,
|
|
203
|
+
model: this.config.bridge.model,
|
|
204
|
+
firmwareRevision: this.config.bridge.firmwareRevision,
|
|
205
|
+
serialNumber,
|
|
206
|
+
debugModeEnabled: this.options.debugModeEnabled,
|
|
207
|
+
networkInterfaces,
|
|
208
|
+
});
|
|
209
|
+
// Start the Matter server
|
|
210
|
+
await this.matterServer.start();
|
|
211
|
+
log.info('Matter server initialized for main bridge');
|
|
212
|
+
// Inform the API that Matter is enabled
|
|
213
|
+
this.api._setMatterEnabled(true);
|
|
214
|
+
// Set the Matter server reference for API methods like getAccessoryState
|
|
215
|
+
this.api._setMatterServer(this.matterServer);
|
|
216
|
+
// Listen for Matter commissioning events to update status
|
|
217
|
+
this.matterServer.on('commissioning-changed', (commissioned, fabricCount) => {
|
|
218
|
+
log.info(`Matter commissioning state changed: commissioned=${commissioned}, fabricCount=${fabricCount}`);
|
|
219
|
+
this.setServerStatus(this.serverStatus);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
log.error('Failed to initialize Matter server for main bridge:', error);
|
|
224
|
+
// Provide user-friendly guidance for common errors
|
|
225
|
+
if (error.message && error.message.includes('corrupted')) {
|
|
226
|
+
log.error('');
|
|
227
|
+
log.error('╔════════════════════════════════════════════════════════════════════════════╗');
|
|
228
|
+
log.error('║ MATTER STORAGE CORRUPTED ║');
|
|
229
|
+
log.error('╠════════════════════════════════════════════════════════════════════════════╣');
|
|
230
|
+
log.error('║ Your Matter storage has become corrupted. This can happen when: ║');
|
|
231
|
+
log.error('║ • Matter.js library version changes ║');
|
|
232
|
+
log.error('║ • Storage format upgrades occur ║');
|
|
233
|
+
log.error('║ • Incomplete writes during shutdown ║');
|
|
234
|
+
log.error('║ ║');
|
|
235
|
+
log.error('║ To fix this, delete the corrupted storage directory: ║');
|
|
236
|
+
log.error(`║ rm -rf ~/.homebridge/matter/${this.config.bridge.username} ║`);
|
|
237
|
+
log.error('║ ║');
|
|
238
|
+
log.error('║ Note: You will need to re-pair your Matter devices after deletion. ║');
|
|
239
|
+
log.error('╚════════════════════════════════════════════════════════════════════════════╝');
|
|
240
|
+
log.error('');
|
|
241
|
+
}
|
|
242
|
+
else if (error.code === 'EADDRINUSE' || (error.message && error.message.includes('address already in use'))) {
|
|
243
|
+
log.error('');
|
|
244
|
+
log.error('╔════════════════════════════════════════════════════════════════════════════╗');
|
|
245
|
+
log.error('║ MATTER PORT ALREADY IN USE ║');
|
|
246
|
+
log.error('╠════════════════════════════════════════════════════════════════════════════╣');
|
|
247
|
+
log.error(`║ Port ${matterPort} is already in use by another application. ║`);
|
|
248
|
+
log.error('║ ║');
|
|
249
|
+
log.error('║ To fix this: ║');
|
|
250
|
+
log.error('║ 1. Stop the application using this port, or ║');
|
|
251
|
+
log.error('║ 2. Configure a different port in your config.json: ║');
|
|
252
|
+
log.error('║ "bridge": { ║');
|
|
253
|
+
log.error('║ "matter": { ║');
|
|
254
|
+
log.error('║ "port": <different-port> ║');
|
|
255
|
+
log.error('║ } ║');
|
|
256
|
+
log.error('║ } ║');
|
|
257
|
+
log.error('╚════════════════════════════════════════════════════════════════════════════╝');
|
|
258
|
+
log.error('');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async teardown() {
|
|
263
|
+
this.bridgeService.teardown();
|
|
264
|
+
// Stop main Matter server if running
|
|
265
|
+
if (this.matterServer) {
|
|
266
|
+
try {
|
|
267
|
+
await this.matterServer.stop();
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
log.error('Failed to stop Matter server:', error);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Stop all external Matter servers
|
|
274
|
+
for (const [uuid, matterServer] of this.externalMatterServers) {
|
|
275
|
+
try {
|
|
276
|
+
await matterServer.stop();
|
|
277
|
+
log.debug(`Stopped external Matter server for ${uuid}`);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
log.error(`Failed to stop external Matter server for ${uuid}:`, error);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.externalMatterServers.clear();
|
|
284
|
+
// Child bridge Matter servers are stopped by their own forked processes
|
|
285
|
+
this.setServerStatus("down" /* ServerStatus.DOWN */);
|
|
286
|
+
}
|
|
287
|
+
publishBridge() {
|
|
288
|
+
this.bridgeService.publishBridge();
|
|
289
|
+
this.printSetupInfo(this.config.bridge.pin);
|
|
290
|
+
}
|
|
291
|
+
handlePublishExternalAccessories(accessories) {
|
|
292
|
+
log.info(`Publishing ${accessories.length} external accessories`);
|
|
293
|
+
// External accessories are published via HAP
|
|
294
|
+
// Plugins should use api.matter to register Matter accessories explicitly
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Handle external Matter accessories - each gets its own dedicated Matter server
|
|
298
|
+
* This is required for devices like Robotic Vacuum Cleaners that Apple Home
|
|
299
|
+
* requires to be on their own bridge.
|
|
300
|
+
*/
|
|
301
|
+
async handlePublishExternalMatterAccessories(accessories) {
|
|
302
|
+
log.info(`Publishing ${accessories.length} external Matter accessor${accessories.length === 1 ? 'y' : 'ies'}`);
|
|
303
|
+
for (const accessory of accessories) {
|
|
304
|
+
try {
|
|
305
|
+
// Validate accessory has required fields
|
|
306
|
+
if (!accessory.uuid) {
|
|
307
|
+
log.error('External Matter accessory missing UUID - skipping');
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (!accessory.displayName) {
|
|
311
|
+
log.error(`External Matter accessory ${accessory.uuid} missing displayName - skipping`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
// Check if already published
|
|
315
|
+
if (this.externalMatterServers.has(accessory.uuid)) {
|
|
316
|
+
log.warn(`External Matter accessory ${accessory.displayName} (${accessory.uuid}) is already published`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
// Generate deterministic MAC address from UUID (same pattern as HAP external accessories)
|
|
320
|
+
const advertiseAddress = generate(accessory.uuid);
|
|
321
|
+
// For Matter, use the MAC without colons as uniqueId
|
|
322
|
+
const uniqueId = advertiseAddress.replace(/:/g, '');
|
|
323
|
+
// Allocate Matter port for the external Matter server
|
|
324
|
+
const port = await this.externalPortService.requestMatterPort(uniqueId);
|
|
325
|
+
if (!port) {
|
|
326
|
+
log.error(`Failed to allocate Matter port for external Matter accessory ${accessory.displayName}`);
|
|
327
|
+
log.error('Please configure matterPorts in config.json or free up ports in the default range (5530-5541)');
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
log.info(`Allocated port ${port} for external Matter accessory: ${accessory.displayName}`);
|
|
331
|
+
// Normalize bind config to array format (inherit from main bridge)
|
|
332
|
+
const networkInterfaces = this.config.bridge.bind
|
|
333
|
+
? Array.isArray(this.config.bridge.bind)
|
|
334
|
+
? this.config.bridge.bind
|
|
335
|
+
: [this.config.bridge.bind]
|
|
336
|
+
: undefined;
|
|
337
|
+
// Create dedicated Matter server for this accessory
|
|
338
|
+
const matterServer = new MatterServer({
|
|
339
|
+
port,
|
|
340
|
+
uniqueId,
|
|
341
|
+
storagePath: User.matterPath(),
|
|
342
|
+
manufacturer: accessory.manufacturer,
|
|
343
|
+
model: accessory.model,
|
|
344
|
+
firmwareRevision: accessory.firmwareRevision,
|
|
345
|
+
serialNumber: accessory.serialNumber || uniqueId, // use uniqueId as fallback serial number
|
|
346
|
+
debugModeEnabled: this.options.debugModeEnabled,
|
|
347
|
+
externalAccessory: true, // external accessory, added before server runs
|
|
348
|
+
networkInterfaces,
|
|
349
|
+
});
|
|
350
|
+
// Start the Matter server (but don't run it yet due to externalAccessory mode)
|
|
351
|
+
await matterServer.start();
|
|
352
|
+
// Get plugin identifier from accessory
|
|
353
|
+
const pluginIdentifier = accessory._associatedPlugin || 'unknown';
|
|
354
|
+
// Register the accessory to this dedicated server
|
|
355
|
+
await matterServer.registerPlatformAccessories(pluginIdentifier, 'ExternalMatter', [accessory]);
|
|
356
|
+
// Now run the server with the device already attached (required for external accessories)
|
|
357
|
+
await matterServer.runServer();
|
|
358
|
+
// Store the server instance
|
|
359
|
+
this.externalMatterServers.set(accessory.uuid, matterServer);
|
|
360
|
+
log.info(`✓ External Matter accessory published: ${accessory.displayName} on port ${port}`);
|
|
361
|
+
// Log commissioning info
|
|
362
|
+
const commissioningInfo = matterServer.getCommissioningInfo();
|
|
363
|
+
if (commissioningInfo.qrCode && commissioningInfo.manualPairingCode) {
|
|
364
|
+
log.info(`📱 Commissioning codes for ${accessory.displayName}:`);
|
|
365
|
+
log.info(` QR Code: ${commissioningInfo.qrCode}`);
|
|
366
|
+
log.info(` Manual Code: ${commissioningInfo.manualPairingCode}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
log.error(`Failed to publish external Matter accessory ${accessory.displayName}:`, error);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
handleRegisterPlatformAccessories(accessories) {
|
|
375
|
+
// Route to HAP bridge
|
|
376
|
+
this.bridgeService.handleRegisterPlatformAccessories(accessories);
|
|
377
|
+
}
|
|
378
|
+
handleUnregisterPlatformAccessories(accessories) {
|
|
379
|
+
// Route to HAP bridge
|
|
380
|
+
this.bridgeService.handleUnregisterPlatformAccessories(accessories);
|
|
381
|
+
}
|
|
382
|
+
handleRegisterMatterPlatformAccessories(pluginIdentifier, platformName, accessories) {
|
|
383
|
+
if (!this.matterServer) {
|
|
384
|
+
log.warn('Cannot register Matter accessories - Matter server is not running');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.matterServer.registerPlatformAccessories(pluginIdentifier, platformName, accessories).catch((error) => {
|
|
388
|
+
log.error(`Failed to register Matter accessories for ${pluginIdentifier}:`, error);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
handleUnregisterMatterPlatformAccessories(pluginIdentifier, platformName, accessories) {
|
|
392
|
+
if (!this.matterServer) {
|
|
393
|
+
log.warn('Cannot unregister Matter accessories - Matter server is not running');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
this.matterServer.unregisterPlatformAccessories(pluginIdentifier, platformName, accessories).catch((error) => {
|
|
397
|
+
log.error(`Failed to unregister Matter accessories for ${pluginIdentifier}:`, error);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
handleUpdateMatterAccessoryState(uuid, cluster, attributes, partId) {
|
|
401
|
+
if (!this.matterServer) {
|
|
402
|
+
log.warn('Cannot update Matter accessory state - Matter server is not running');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
this.matterServer.updateAccessoryState(uuid, cluster, attributes, partId).catch((error) => {
|
|
406
|
+
log.error(`Failed to update Matter accessory state for ${uuid}:`, error);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Restore cached Matter accessories (matching HAP pattern)
|
|
411
|
+
*/
|
|
412
|
+
restoreCachedMatterAccessories() {
|
|
413
|
+
if (!this.matterServer) {
|
|
414
|
+
log.debug('Matter server not available for restoring cached accessories');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const cachedAccessories = this.matterServer.getAllCachedAccessories();
|
|
418
|
+
log.debug(`Restoring ${cachedAccessories.length} cached Matter accessories`);
|
|
419
|
+
for (const cachedAccessory of cachedAccessories) {
|
|
420
|
+
let plugin = this.pluginManager.getPlugin(cachedAccessory.plugin);
|
|
421
|
+
if (!plugin) {
|
|
422
|
+
try {
|
|
423
|
+
// Try to find plugin by platform name (handles plugin renames)
|
|
424
|
+
plugin = this.pluginManager.getPluginByActiveDynamicPlatform(cachedAccessory.platform);
|
|
425
|
+
if (plugin) {
|
|
426
|
+
log.info(`When searching for the associated plugin of the Matter accessory '${cachedAccessory.displayName}' `
|
|
427
|
+
+ `it seems like the plugin name changed from '${cachedAccessory.plugin}' to '${plugin.getPluginIdentifier()}'. Plugin association is now being transformed!`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
log.warn(`Could not find the associated plugin for the Matter accessory '${cachedAccessory.displayName}'. `
|
|
432
|
+
+ `Tried to find the plugin by the platform name but ${error.message}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const platformPlugin = plugin && plugin.getActiveDynamicPlatform(cachedAccessory.platform);
|
|
436
|
+
if (!platformPlugin) {
|
|
437
|
+
log.warn(`Failed to find plugin to handle Matter accessory ${cachedAccessory.displayName} (plugin: ${cachedAccessory.plugin}, platform: ${cachedAccessory.platform})`);
|
|
438
|
+
// Note: Matter accessories are not added to the bridge here - they're registered via plugin's didFinishLaunching
|
|
439
|
+
// The plugin can check if this accessory still exists and re-register or remove it
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// Call configureMatterAccessory if the plugin implements it
|
|
443
|
+
if (platformPlugin.configureMatterAccessory) {
|
|
444
|
+
log.debug(`Calling configureMatterAccessory for ${cachedAccessory.displayName}`);
|
|
445
|
+
platformPlugin.configureMatterAccessory(cachedAccessory);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
log.debug(`Platform ${cachedAccessory.platform} does not implement configureMatterAccessory`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
static loadConfig() {
|
|
454
|
+
// Look for the configuration file
|
|
455
|
+
const configPath = User.configPath();
|
|
456
|
+
const defaultBridge = {
|
|
457
|
+
name: 'Homebridge',
|
|
458
|
+
username: 'CC:22:3D:E3:CE:30',
|
|
459
|
+
pin: '031-45-154',
|
|
460
|
+
};
|
|
461
|
+
if (!existsSync(configPath)) {
|
|
462
|
+
log.warn('config.json (%s) not found.', configPath);
|
|
463
|
+
return {
|
|
464
|
+
bridge: defaultBridge,
|
|
465
|
+
accessories: [],
|
|
466
|
+
platforms: [],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
let config;
|
|
470
|
+
try {
|
|
471
|
+
config = JSON.parse(readFileSync(configPath, { encoding: 'utf8' }));
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
log.error('There was a problem reading your config.json file.');
|
|
475
|
+
log.error('Please try pasting your config.json file here to validate it: https://jsonlint.com');
|
|
476
|
+
log.error('');
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
if (config.ports !== undefined) {
|
|
480
|
+
if (config.ports.start && config.ports.end) {
|
|
481
|
+
if (config.ports.start > config.ports.end) {
|
|
482
|
+
log.error('Invalid port pool configuration. End should be greater than or equal to start.');
|
|
483
|
+
config.ports = undefined;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
log.error('Invalid configuration for \'ports\'. Missing \'start\' and \'end\' properties! Ignoring it!');
|
|
488
|
+
config.ports = undefined;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (config.matterPorts !== undefined) {
|
|
492
|
+
if (config.matterPorts.start && config.matterPorts.end) {
|
|
493
|
+
if (config.matterPorts.start > config.matterPorts.end) {
|
|
494
|
+
log.error('Invalid Matter port pool configuration. End should be greater than or equal to start.');
|
|
495
|
+
config.matterPorts = undefined;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
log.error('Invalid configuration for \'matterPorts\'. Missing \'start\' and \'end\' properties! Ignoring it!');
|
|
500
|
+
config.matterPorts = undefined;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const bridge = config.bridge || defaultBridge;
|
|
504
|
+
bridge.name = bridge.name || defaultBridge.name;
|
|
505
|
+
bridge.username = bridge.username || defaultBridge.username;
|
|
506
|
+
bridge.pin = bridge.pin || defaultBridge.pin;
|
|
507
|
+
config.bridge = bridge;
|
|
508
|
+
const username = config.bridge.username;
|
|
509
|
+
if (!validMacAddress(username)) {
|
|
510
|
+
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.`);
|
|
511
|
+
}
|
|
512
|
+
config.accessories = config.accessories || [];
|
|
513
|
+
config.platforms = config.platforms || [];
|
|
514
|
+
if (!Array.isArray(config.accessories)) {
|
|
515
|
+
log.error('Value provided for accessories must be an array[]');
|
|
516
|
+
config.accessories = [];
|
|
517
|
+
}
|
|
518
|
+
if (!Array.isArray(config.platforms)) {
|
|
519
|
+
log.error('Value provided for platforms must be an array[]');
|
|
520
|
+
config.platforms = [];
|
|
521
|
+
}
|
|
522
|
+
log.info('Loaded config.json with %s accessories and %s platforms.', config.accessories.length, config.platforms.length);
|
|
523
|
+
// Validate Matter configuration for port conflicts
|
|
524
|
+
if (config.bridge.matter || config.platforms.some((p) => p._bridge?.matter) || config.accessories.some((a) => a._bridge?.matter)) {
|
|
525
|
+
// Validate main bridge Matter config
|
|
526
|
+
if (config.bridge.matter) {
|
|
527
|
+
const validation = MatterConfigValidator.validate(config.bridge.matter);
|
|
528
|
+
if (!validation.isValid) {
|
|
529
|
+
log.error('Main bridge Matter configuration is invalid. Matter will not be enabled for the main bridge.');
|
|
530
|
+
delete config.bridge.matter;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Validate all child bridge Matter configs and check for port conflicts
|
|
534
|
+
const childMatterValidation = MatterConfigValidator.validateAllChildMatterConfigs(config.platforms, config.accessories);
|
|
535
|
+
if (!childMatterValidation.isValid) {
|
|
536
|
+
log.error('Some child bridge Matter configurations are invalid. Check the errors above.');
|
|
537
|
+
}
|
|
538
|
+
// Additionally, check for conflicts between main bridge Matter port and child bridge ports
|
|
539
|
+
if (config.bridge.matter?.port) {
|
|
540
|
+
const mainMatterPort = config.bridge.matter.port;
|
|
541
|
+
const childMatterPorts = [];
|
|
542
|
+
for (const platform of config.platforms) {
|
|
543
|
+
if (platform._bridge?.matter?.port) {
|
|
544
|
+
childMatterPorts.push(platform._bridge.matter.port);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
for (const accessory of config.accessories) {
|
|
548
|
+
if (accessory._bridge?.matter?.port) {
|
|
549
|
+
childMatterPorts.push(accessory._bridge.matter.port);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (childMatterPorts.includes(mainMatterPort)) {
|
|
553
|
+
log.error(`Main bridge Matter port ${mainMatterPort} conflicts with a child bridge Matter port. Please use unique ports.`);
|
|
554
|
+
}
|
|
555
|
+
// Check for conflict with main bridge HAP port
|
|
556
|
+
if (config.bridge.port && Math.abs(config.bridge.port - mainMatterPort) < 10) {
|
|
557
|
+
log.warn(`Main bridge HAP port ${config.bridge.port} and Matter port ${mainMatterPort} are very close. Consider spacing them further apart.`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (config.bridge.advertiser) {
|
|
562
|
+
if (![
|
|
563
|
+
"bonjour-hap" /* MDNSAdvertiser.BONJOUR */,
|
|
564
|
+
"ciao" /* MDNSAdvertiser.CIAO */,
|
|
565
|
+
"avahi" /* MDNSAdvertiser.AVAHI */,
|
|
566
|
+
"resolved" /* MDNSAdvertiser.RESOLVED */,
|
|
567
|
+
].includes(config.bridge.advertiser)) {
|
|
568
|
+
config.bridge.advertiser = undefined;
|
|
569
|
+
log.error('Value provided in bridge.advertiser is not valid, reverting to platform default.');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
config.bridge.advertiser = undefined;
|
|
574
|
+
}
|
|
575
|
+
return config;
|
|
576
|
+
}
|
|
577
|
+
loadAccessories() {
|
|
578
|
+
log.info(`Loading ${this.config.accessories.length} accessories...`);
|
|
579
|
+
this.config.accessories.forEach((accessoryConfig, index) => {
|
|
580
|
+
if (!accessoryConfig.accessory) {
|
|
581
|
+
log.warn('Your config.json contains an illegal accessory configuration object at position %d. '
|
|
582
|
+
+ 'Missing property \'accessory\'. Skipping entry...', index + 1); // we rather count from 1 for the normal people?
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const accessoryIdentifier = accessoryConfig.accessory;
|
|
586
|
+
const displayName = accessoryConfig.name;
|
|
587
|
+
if (!displayName) {
|
|
588
|
+
log.warn('Could not load accessory %s at position %d as it is missing the required \'name\' property!', accessoryIdentifier, index + 1);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
let plugin;
|
|
592
|
+
let constructor;
|
|
593
|
+
try {
|
|
594
|
+
plugin = this.pluginManager.getPluginForAccessory(accessoryIdentifier);
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
log.error(error.message);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
// check the plugin is not disabled
|
|
601
|
+
if (plugin.disabled) {
|
|
602
|
+
log.warn(`Ignoring config for the accessory "${accessoryIdentifier}" in your config.json as the plugin "${plugin.getPluginIdentifier()}" has been disabled.`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
constructor = plugin.getAccessoryConstructor(accessoryIdentifier);
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
log.error(`Error loading the accessory "${accessoryIdentifier}" requested in your config.json at position ${index + 1} - this is likely an issue with the "${plugin.getPluginIdentifier()}" plugin.`);
|
|
610
|
+
log.error(error); // error message contains more information and full stack trace
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const logger = Logger.withPrefix(displayName);
|
|
614
|
+
logger('Initializing %s accessory...', accessoryIdentifier);
|
|
615
|
+
if (accessoryConfig._bridge) {
|
|
616
|
+
// ensure the username is always uppercase
|
|
617
|
+
accessoryConfig._bridge.username = accessoryConfig._bridge.username.toUpperCase();
|
|
618
|
+
try {
|
|
619
|
+
this.validateChildBridgeConfig("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, accessoryConfig._bridge);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
log.error(error.message);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
let childBridge;
|
|
626
|
+
if (this.childBridges.has(accessoryConfig._bridge.username)) {
|
|
627
|
+
childBridge = this.childBridges.get(accessoryConfig._bridge.username);
|
|
628
|
+
logger(`Adding to existing child bridge ${accessoryConfig._bridge.username}`);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
logger(`Initializing child bridge ${accessoryConfig._bridge.username}`);
|
|
632
|
+
childBridge = new ChildBridgeService("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, plugin, accessoryConfig._bridge, this.config, this.options, this.api, this.ipcService, this.externalPortService);
|
|
633
|
+
this.childBridges.set(accessoryConfig._bridge.username, childBridge);
|
|
634
|
+
}
|
|
635
|
+
// add config to child bridge service
|
|
636
|
+
childBridge.addConfig(accessoryConfig);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const accessoryInstance = new constructor(logger, accessoryConfig, this.api);
|
|
640
|
+
// pass accessoryIdentifier for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation
|
|
641
|
+
const accessory = this.bridgeService.createHAPAccessory(plugin, accessoryInstance, displayName, accessoryIdentifier, accessoryConfig.uuid_base);
|
|
642
|
+
if (accessory) {
|
|
643
|
+
try {
|
|
644
|
+
this.bridgeService.bridge.addBridgedAccessory(accessory);
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
logger.error(`Error loading the accessory "${accessoryIdentifier}" from "${plugin.getPluginIdentifier()}" requested in your config.json:`, error.message);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
logger.info('Accessory %s returned empty set of services; not adding it to the bridge.', accessoryIdentifier);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
loadPlatforms() {
|
|
656
|
+
log.info(`Loading ${this.config.platforms.length} platforms...`);
|
|
657
|
+
const promises = [];
|
|
658
|
+
this.config.platforms.forEach((platformConfig, index) => {
|
|
659
|
+
if (!platformConfig.platform) {
|
|
660
|
+
log.warn('Your config.json contains an illegal platform configuration object at position %d. '
|
|
661
|
+
+ 'Missing property \'platform\'. Skipping entry...', index + 1); // we rather count from 1 for the normal people?
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const platformIdentifier = platformConfig.platform;
|
|
665
|
+
const displayName = platformConfig.name || platformIdentifier;
|
|
666
|
+
let plugin;
|
|
667
|
+
let constructor;
|
|
668
|
+
// do not load homebridge-config-ui-x when running in service mode
|
|
669
|
+
if (platformIdentifier === 'config' && process.env.UIX_SERVICE_MODE === '1') {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
plugin = this.pluginManager.getPluginForPlatform(platformIdentifier);
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
log.error(error.message);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
// check the plugin is not disabled
|
|
680
|
+
if (plugin.disabled) {
|
|
681
|
+
log.warn(`Ignoring config for the platform "${platformIdentifier}" in your config.json as the plugin "${plugin.getPluginIdentifier()}" has been disabled.`);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
constructor = plugin.getPlatformConstructor(platformIdentifier);
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
log.error(`Error loading the platform "${platformIdentifier}" requested in your config.json at position ${index + 1} - this is likely an issue with the "${plugin.getPluginIdentifier()}" plugin.`);
|
|
689
|
+
log.error(error); // error message contains more information and full stack trace
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const logger = Logger.withPrefix(displayName);
|
|
693
|
+
logger('Initializing %s platform...', platformIdentifier);
|
|
694
|
+
if (platformConfig._bridge) {
|
|
695
|
+
// ensure the username is always uppercase
|
|
696
|
+
platformConfig._bridge.username = platformConfig._bridge.username.toUpperCase();
|
|
697
|
+
try {
|
|
698
|
+
this.validateChildBridgeConfig("platform" /* PluginType.PLATFORM */, platformIdentifier, platformConfig._bridge);
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
log.error(error.message);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
logger(`Initializing child bridge ${platformConfig._bridge.username}`);
|
|
705
|
+
const childBridge = new ChildBridgeService("platform" /* PluginType.PLATFORM */, platformIdentifier, plugin, platformConfig._bridge, this.config, this.options, this.api, this.ipcService, this.externalPortService);
|
|
706
|
+
this.childBridges.set(platformConfig._bridge.username, childBridge);
|
|
707
|
+
// add config to child bridge service
|
|
708
|
+
childBridge.addConfig(platformConfig);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const platform = new constructor(logger, platformConfig, this.api);
|
|
712
|
+
if (HomebridgeAPI.isDynamicPlatformPlugin(platform)) {
|
|
713
|
+
plugin.assignDynamicPlatform(platformIdentifier, platform);
|
|
714
|
+
}
|
|
715
|
+
else if (HomebridgeAPI.isStaticPlatformPlugin(platform)) { // Plugin 1.0, load accessories
|
|
716
|
+
promises.push(this.bridgeService.loadPlatformAccessories(plugin, platform, platformIdentifier, logger));
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
// otherwise it's a IndependentPlatformPlugin which doesn't expose any methods at all.
|
|
720
|
+
// We just call the constructor and let it be enabled.
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
return promises;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Validate an external bridge config
|
|
727
|
+
*/
|
|
728
|
+
validateChildBridgeConfig(type, identifier, bridgeConfig) {
|
|
729
|
+
// All child bridges require username
|
|
730
|
+
if (!bridgeConfig.username) {
|
|
731
|
+
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
732
|
+
+ 'Missing required field "_bridge.username".');
|
|
733
|
+
}
|
|
734
|
+
if (!validMacAddress(bridgeConfig.username)) {
|
|
735
|
+
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
736
|
+
+ `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.`);
|
|
737
|
+
}
|
|
738
|
+
if (this.childBridges.has(bridgeConfig.username)) {
|
|
739
|
+
const childBridge = this.childBridges.get(bridgeConfig.username);
|
|
740
|
+
if (type === "platform" /* PluginType.PLATFORM */) {
|
|
741
|
+
// only a single platform can exist on one child bridge
|
|
742
|
+
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
743
|
+
+ `Duplicate username found in _bridge.username: "${bridgeConfig.username}". Each platform child bridge must have it's own unique username.`);
|
|
744
|
+
}
|
|
745
|
+
else if (childBridge?.identifier !== identifier) {
|
|
746
|
+
// only accessories of the same type can be added to the same child bridge
|
|
747
|
+
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
748
|
+
+ `Duplicate username found in _bridge.username: "${bridgeConfig.username}". You can only group accessories of the same type in a child bridge.`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (bridgeConfig.username === this.config.bridge.username.toUpperCase()) {
|
|
752
|
+
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
753
|
+
+ `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.`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Takes care of the IPC Events sent to Homebridge
|
|
758
|
+
*/
|
|
759
|
+
initializeIpcEventHandlers() {
|
|
760
|
+
// start ipc service
|
|
761
|
+
this.ipcService.start();
|
|
762
|
+
// handle restart child bridge event
|
|
763
|
+
this.ipcService.on("restartChildBridge" /* IpcIncomingEvent.RESTART_CHILD_BRIDGE */, (username) => {
|
|
764
|
+
// noinspection SuspiciousTypeOfGuard
|
|
765
|
+
if (typeof username === 'string') {
|
|
766
|
+
const childBridge = this.childBridges.get(username.toUpperCase());
|
|
767
|
+
childBridge?.restartChildBridge();
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
// handle stop child bridge event
|
|
771
|
+
this.ipcService.on("stopChildBridge" /* IpcIncomingEvent.STOP_CHILD_BRIDGE */, (username) => {
|
|
772
|
+
// noinspection SuspiciousTypeOfGuard
|
|
773
|
+
if (typeof username === 'string') {
|
|
774
|
+
const childBridge = this.childBridges.get(username.toUpperCase());
|
|
775
|
+
childBridge?.stopChildBridge();
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
// handle start child bridge event
|
|
779
|
+
this.ipcService.on("startChildBridge" /* IpcIncomingEvent.START_CHILD_BRIDGE */, (username) => {
|
|
780
|
+
// noinspection SuspiciousTypeOfGuard
|
|
781
|
+
if (typeof username === 'string') {
|
|
782
|
+
const childBridge = this.childBridges.get(username.toUpperCase());
|
|
783
|
+
childBridge?.startChildBridge();
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
this.ipcService.on("childBridgeMetadataRequest" /* IpcIncomingEvent.CHILD_BRIDGE_METADATA_REQUEST */, () => {
|
|
787
|
+
this.ipcService.sendMessage("childBridgeMetadataResponse" /* IpcOutgoingEvent.CHILD_BRIDGE_METADATA_RESPONSE */, Array.from(this.childBridges.values()).map(x => x.getMetadata()));
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
printSetupInfo(pin) {
|
|
791
|
+
/* eslint-disable no-console */
|
|
792
|
+
console.log('Setup Payload:');
|
|
793
|
+
console.log(this.bridgeService.bridge.setupURI());
|
|
794
|
+
if (!this.options.hideQRCode) {
|
|
795
|
+
console.log('Scan this code with your HomeKit app on your iOS device to pair with Homebridge:');
|
|
796
|
+
qrcode.setErrorLevel('M'); // HAP specifies level M or higher for ECC
|
|
797
|
+
qrcode.generate(this.bridgeService.bridge.setupURI());
|
|
798
|
+
console.log('Or enter this code with your HomeKit app on your iOS device to pair with Homebridge:');
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
console.log('Enter this code with your HomeKit app on your iOS device to pair with Homebridge:');
|
|
802
|
+
}
|
|
803
|
+
console.log(chalk.black.bgWhite(' '));
|
|
804
|
+
console.log(chalk.black.bgWhite(' ┌────────────┐ '));
|
|
805
|
+
console.log(chalk.black.bgWhite(` │ ${pin} │ `));
|
|
806
|
+
console.log(chalk.black.bgWhite(' └────────────┘ '));
|
|
807
|
+
console.log(chalk.black.bgWhite(' '));
|
|
808
|
+
/* eslint-enable no-console */
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
//# sourceMappingURL=server.js.map
|