Skip to content

Commit f720ab4

Browse files
committed
multiple accessories in a child bridge
1 parent 63d43df commit f720ab4

File tree

4 files changed

+143
-80
lines changed

4 files changed

+143
-80
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "homebridge",
33
"description": "HomeKit support for the impatient",
44
"version": "1.3.1",
5-
"betaVersion": "1.3.1",
5+
"betaVersion": "1.3.2",
66
"main": "lib/index.js",
77
"types": "lib/index.d.ts",
88
"license": "Apache-2.0",

src/childBridgeFork.ts

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class ChildBridgeFork {
3939
private type!: PluginType;
4040
private plugin!: Plugin;
4141
private identifier!: string;
42-
private pluginConfig!: PlatformConfig | AccessoryConfig;
42+
private pluginConfig!: Array<PlatformConfig | AccessoryConfig>;
4343
private bridgeConfig!: BridgeConfiguration;
4444
private bridgeOptions!: BridgeOptions;
4545
private homebridgeConfig!: HomebridgeConfig;
@@ -70,7 +70,9 @@ export class ChildBridgeFork {
7070
this.homebridgeConfig = data.homebridgeConfig;
7171

7272
// remove the _bridge key (some plugins do not like unknown config)
73-
delete this.pluginConfig._bridge;
73+
for (const config of this.pluginConfig) {
74+
delete config._bridge;
75+
}
7476

7577
// set bridge settings (inherited from main bridge)
7678
if (this.bridgeOptions.noLogTimestamps) {
@@ -123,42 +125,45 @@ export class ChildBridgeFork {
123125
// load the cached accessories
124126
await this.bridgeService.loadCachedPlatformAccessoriesFromDisk();
125127

126-
if (this.type === PluginType.PLATFORM) {
127-
const plugin = this.pluginManager.getPluginForPlatform(this.identifier);
128-
const displayName = this.pluginConfig?.name || plugin.getPluginIdentifier();
129-
const logger = Logger.withPrefix(displayName);
130-
const constructor = plugin.getPlatformConstructor(this.identifier);
131-
const platform: PlatformPlugin = new constructor(logger, this.pluginConfig as PlatformConfig, this.api);
132-
133-
if (HomebridgeAPI.isDynamicPlatformPlugin(platform)) {
134-
plugin.assignDynamicPlatform(this.identifier, platform);
135-
} else if (HomebridgeAPI.isStaticPlatformPlugin(platform)) { // Plugin 1.0, load accessories
136-
await this.bridgeService.loadPlatformAccessories(plugin, platform, this.identifier, logger);
137-
} else {
138-
// otherwise it's a IndependentPlatformPlugin which doesn't expose any methods at all.
139-
// We just call the constructor and let it be enabled.
140-
}
141-
142-
} else if (this.type === PluginType.ACCESSORY) {
143-
const plugin = this.pluginManager.getPluginForAccessory(this.identifier);
144-
const displayName = this.pluginConfig.name;
145-
146-
if (!displayName) {
147-
Logger.internal.warn("Could not load accessory %s as it is missing the required 'name' property!", this.identifier);
148-
return;
149-
}
150-
151-
const logger = Logger.withPrefix(displayName);
152-
const constructor = plugin.getAccessoryConstructor(this.identifier);
153-
const accessoryInstance: AccessoryPlugin = new constructor(logger, this.pluginConfig as AccessoryConfig, this.api);
154-
155-
//pass accessoryIdentifier for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation
156-
const accessory = this.bridgeService.createHAPAccessory(plugin, accessoryInstance, displayName, this.identifier, this.pluginConfig.uuid_base);
157-
158-
if (accessory) {
159-
this.bridgeService.bridge.addBridgedAccessory(accessory);
160-
} else {
161-
logger("Accessory %s returned empty set of services. Won't adding it to the bridge!", this.identifier);
128+
for (const config of this.pluginConfig) {
129+
130+
if (this.type === PluginType.PLATFORM) {
131+
const plugin = this.pluginManager.getPluginForPlatform(this.identifier);
132+
const displayName = config.name || plugin.getPluginIdentifier();
133+
const logger = Logger.withPrefix(displayName);
134+
const constructor = plugin.getPlatformConstructor(this.identifier);
135+
const platform: PlatformPlugin = new constructor(logger, config as PlatformConfig, this.api);
136+
137+
if (HomebridgeAPI.isDynamicPlatformPlugin(platform)) {
138+
plugin.assignDynamicPlatform(this.identifier, platform);
139+
} else if (HomebridgeAPI.isStaticPlatformPlugin(platform)) { // Plugin 1.0, load accessories
140+
await this.bridgeService.loadPlatformAccessories(plugin, platform, this.identifier, logger);
141+
} else {
142+
// otherwise it's a IndependentPlatformPlugin which doesn't expose any methods at all.
143+
// We just call the constructor and let it be enabled.
144+
}
145+
146+
} else if (this.type === PluginType.ACCESSORY) {
147+
const plugin = this.pluginManager.getPluginForAccessory(this.identifier);
148+
const displayName = config.name;
149+
150+
if (!displayName) {
151+
Logger.internal.warn("Could not load accessory %s as it is missing the required 'name' property!", this.identifier);
152+
return;
153+
}
154+
155+
const logger = Logger.withPrefix(displayName);
156+
const constructor = plugin.getAccessoryConstructor(this.identifier);
157+
const accessoryInstance: AccessoryPlugin = new constructor(logger, config as AccessoryConfig, this.api);
158+
159+
//pass accessoryIdentifier for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation
160+
const accessory = this.bridgeService.createHAPAccessory(plugin, accessoryInstance, displayName, this.identifier, config.uuid_base);
161+
162+
if (accessory) {
163+
this.bridgeService.bridge.addBridgedAccessory(accessory);
164+
} else {
165+
logger("Accessory %s returned empty set of services. Won't adding it to the bridge!", this.identifier);
166+
}
162167
}
163168
}
164169

src/childBridgeService.ts

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { IpcOutgoingEvent, IpcService } from "./ipcService";
77
import { ExternalPortService } from "./externalPortService";
88
import { HomebridgeAPI, PluginType } from "./api";
99
import { HomebridgeOptions } from "./server";
10-
import { Logger } from "./logger";
10+
import { Logger, Logging } from "./logger";
1111
import { Plugin } from "./plugin";
1212
import { User } from "./user";
1313
import {
@@ -81,7 +81,7 @@ export interface ChildProcessLoadEventData {
8181
type: PluginType;
8282
identifier: string;
8383
pluginPath: string;
84-
pluginConfig: PlatformConfig | AccessoryConfig;
84+
pluginConfig: Array<PlatformConfig | AccessoryConfig>;
8585
bridgeConfig: BridgeConfiguration;
8686
homebridgeConfig: HomebridgeConfig;
8787
bridgeOptions: BridgeOptions;
@@ -115,26 +115,24 @@ export interface ChildMetadata {
115115
*/
116116
export class ChildBridgeService {
117117
private child?: child_process.ChildProcess;
118-
private log = Logger.withPrefix(this.pluginConfig?.name || this.plugin.getPluginIdentifier());
119118
private args: string[] = [];
120119
private shuttingDown = false;
121120
private lastBridgeStatus: ChildBridgeStatus = ChildBridgeStatus.PENDING;
121+
private pluginConfig: Array<PlatformConfig | AccessoryConfig> = [];
122+
private log: Logging = Logger.withPrefix(this.plugin.getPluginIdentifier());
123+
private displayName?: string;
122124

123125
constructor(
124-
private type: PluginType,
125-
private identifier: string,
126+
public type: PluginType,
127+
public identifier: string,
126128
private plugin: Plugin,
127-
private pluginConfig: PlatformConfig | AccessoryConfig,
128129
private bridgeConfig: BridgeConfiguration,
129130
private homebridgeConfig: HomebridgeConfig,
130131
private homebridgeOptions: HomebridgeOptions,
131132
private api: HomebridgeAPI,
132133
private ipcService: IpcService,
133134
private externalPortService: ExternalPortService,
134135
) {
135-
this.setProcessFlags();
136-
this.startChildProcess();
137-
138136
this.api.on("shutdown", () => {
139137
this.shuttingDown = true;
140138
this.teardown();
@@ -144,6 +142,33 @@ export class ChildBridgeService {
144142
this.api.setMaxListeners(this.api.getMaxListeners() + 1);
145143
}
146144

145+
/**
146+
* Start the child bridge service
147+
*/
148+
public start(): void {
149+
this.setProcessFlags();
150+
this.startChildProcess();
151+
152+
// set display name
153+
if (this.pluginConfig.length > 1 || this.pluginConfig.length === 0) {
154+
this.displayName = this.plugin.getPluginIdentifier();
155+
} else {
156+
this.displayName = this.pluginConfig[0]?.name || this.plugin.getPluginIdentifier();
157+
}
158+
159+
// re-configured log with display name
160+
this.log = Logger.withPrefix(this.displayName);
161+
}
162+
163+
/**
164+
* Add a config block to a child bridge.
165+
* Platform child bridges can only contain one config block.
166+
* @param config
167+
*/
168+
public addConfig(config: PlatformConfig | AccessoryConfig): void {
169+
this.pluginConfig.push(config);
170+
}
171+
147172
private get bridgeStatus(): ChildBridgeStatus {
148173
return this.lastBridgeStatus;
149174
}
@@ -193,13 +218,17 @@ export class ChildBridgeService {
193218

194219
switch(message.id) {
195220
case ChildProcessMessageEventType.READY: {
196-
this.log(`Launched external bridge with PID ${this.child?.pid}`);
221+
this.log(`Launched child bridge with PID ${this.child?.pid}`);
197222
this.loadPlugin();
198223
break;
199224
}
200225
case ChildProcessMessageEventType.LOADED: {
201226
const version = (message.data as ChildProcessPluginLoadedEventData).version;
202-
this.log(`Loaded ${this.plugin.getPluginIdentifier()} v${version} successfully`);
227+
if (this.pluginConfig.length > 1) {
228+
this.log(`Loaded ${this.plugin.getPluginIdentifier()} v${version} child bridge successfully with ${this.pluginConfig.length} accessories`);
229+
} else {
230+
this.log(`Loaded ${this.plugin.getPluginIdentifier()} v${version} child bridge successfully`);
231+
}
203232
this.startBridge();
204233
break;
205234
}
@@ -284,7 +313,7 @@ export class ChildBridgeService {
284313
*/
285314
private loadPlugin(): void {
286315
const bridgeConfig: BridgeConfiguration = {
287-
name: this.bridgeConfig.name || this.pluginConfig.name || this.plugin.getPluginIdentifier(),
316+
name: this.bridgeConfig.name || this.displayName || this.plugin.getPluginIdentifier(),
288317
port: this.bridgeConfig.port,
289318
username: this.bridgeConfig.username,
290319
advertiser: this.homebridgeConfig.bridge.advertiser,
@@ -366,18 +395,18 @@ export class ChildBridgeService {
366395
const homebridgeConfig: HomebridgeConfig = await fs.readJson(User.configPath());
367396

368397
if (this.type === PluginType.PLATFORM) {
369-
const config = homebridgeConfig.platforms?.find(x => x.platform === this.identifier && x._bridge?.username === this.bridgeConfig.username);
370-
if (config) {
398+
const config = homebridgeConfig.platforms?.filter(x => x.platform === this.identifier && x._bridge?.username === this.bridgeConfig.username);
399+
if (config.length) {
371400
this.pluginConfig = config;
372-
this.bridgeConfig = this.pluginConfig._bridge || this.bridgeConfig;
401+
this.bridgeConfig = this.pluginConfig[0]._bridge || this.bridgeConfig;
373402
} else {
374403
this.log.warn("Platform config could not be found, using existing config.");
375404
}
376405
} else if (this.type === PluginType.ACCESSORY) {
377-
const config = homebridgeConfig.accessories?.find(x => x.accessory === this.identifier && x._bridge?.username === this.bridgeConfig.username);
378-
if (config) {
406+
const config = homebridgeConfig.accessories?.filter(x => x.accessory === this.identifier && x._bridge?.username === this.bridgeConfig.username);
407+
if (config.length) {
379408
this.pluginConfig = config;
380-
this.bridgeConfig = this.pluginConfig._bridge || this.bridgeConfig;
409+
this.bridgeConfig = this.pluginConfig[0]._bridge || this.bridgeConfig;
381410
} else {
382411
this.log.warn("Accessory config could not be found, using existing config.");
383412
}
@@ -395,7 +424,7 @@ export class ChildBridgeService {
395424
return {
396425
status: this.bridgeStatus,
397426
username: this.bridgeConfig.username,
398-
name: this.bridgeConfig.name || this.pluginConfig.name || this.plugin.getPluginIdentifier(),
427+
name: this.bridgeConfig.name || this.displayName || this.plugin.getPluginIdentifier(),
399428
plugin: this.plugin.getPluginIdentifier(),
400429
identifier: this.identifier,
401430
pid: this.child?.pid,

src/server.ts

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ export class Server {
119119
this.loadAccessories();
120120
}
121121

122+
// start child bridges
123+
for (const childBridge of this.childBridges.values()) {
124+
childBridge.start();
125+
}
126+
122127
// restore cached accessories
123128
this.bridgeService.restoreCachedPlatformAccessories();
124129

@@ -264,26 +269,37 @@ export class Server {
264269
accessoryConfig._bridge.username = accessoryConfig._bridge.username.toUpperCase();
265270

266271
try {
267-
this.validateChildBridgeConfig(PluginType.PLATFORM, accessoryIdentifier, accessoryConfig._bridge);
272+
this.validateChildBridgeConfig(PluginType.ACCESSORY, accessoryIdentifier, accessoryConfig._bridge);
268273
} catch (error) {
269274
log.error(error.message);
270275
return;
271276
}
272277

273-
const childBridge = new ChildBridgeService(
274-
PluginType.ACCESSORY,
275-
accessoryIdentifier,
276-
plugin,
277-
accessoryConfig,
278-
accessoryConfig._bridge,
279-
this.config,
280-
this.options,
281-
this.api,
282-
this.ipcService,
283-
this.externalPortService,
284-
);
278+
let childBridge: ChildBridgeService;
279+
280+
if (this.childBridges.has(accessoryConfig._bridge.username)) {
281+
childBridge = this.childBridges.get(accessoryConfig._bridge.username)!;
282+
logger(`Adding to existing child bridge ${accessoryConfig._bridge.username}`);
283+
} else {
284+
logger(`Initializing child bridge ${accessoryConfig._bridge.username}`);
285+
childBridge = new ChildBridgeService(
286+
PluginType.ACCESSORY,
287+
accessoryIdentifier,
288+
plugin,
289+
accessoryConfig._bridge,
290+
this.config,
291+
this.options,
292+
this.api,
293+
this.ipcService,
294+
this.externalPortService,
295+
);
296+
297+
this.childBridges.set(accessoryConfig._bridge.username, childBridge);
298+
}
299+
300+
// add config to child bridge service
301+
childBridge.addConfig(accessoryConfig);
285302

286-
this.childBridges.set(accessoryConfig._bridge.username, childBridge);
287303
return;
288304
}
289305

@@ -356,12 +372,12 @@ export class Server {
356372
log.error(error.message);
357373
return;
358374
}
359-
375+
376+
logger(`Initializing child bridge ${platformConfig._bridge.username}`);
360377
const childBridge = new ChildBridgeService(
361378
PluginType.PLATFORM,
362379
platformIdentifier,
363380
plugin,
364-
platformConfig,
365381
platformConfig._bridge,
366382
this.config,
367383
this.options,
@@ -371,6 +387,9 @@ export class Server {
371387
);
372388

373389
this.childBridges.set(platformConfig._bridge.username, childBridge);
390+
391+
// add config to child bridge service
392+
childBridge.addConfig(platformConfig);
374393
return;
375394
}
376395

@@ -401,16 +420,26 @@ export class Server {
401420
}
402421

403422
if (this.childBridges.has(bridgeConfig.username)) {
404-
throw new Error(
405-
`Error loading the ${identifier} "${identifier}" requested in your config.json - ` +
406-
`Duplicate username found in _bridge.username: "${bridgeConfig.username}". Each external platform/accessory must have it's own unique username.`,
407-
);
423+
const childBridge = this.childBridges.get(bridgeConfig.username);
424+
if (type === PluginType.PLATFORM) {
425+
// only a single platform can exist on one child bridge
426+
throw new Error(
427+
`Error loading the ${type} "${identifier}" requested in your config.json - ` +
428+
`Duplicate username found in _bridge.username: "${bridgeConfig.username}". Each platform child bridge must have it's own unique username.`,
429+
);
430+
} else if (childBridge?.identifier !== identifier) {
431+
// only accessories of the same type can be added to the same child bridge
432+
throw new Error(
433+
`Error loading the ${type} "${identifier}" requested in your config.json - ` +
434+
`Duplicate username found in _bridge.username: "${bridgeConfig.username}". You can only group accessories of the same type in a child bridge.`,
435+
);
436+
}
408437
}
409438

410439
if (bridgeConfig.username === this.config.bridge.username.toUpperCase()) {
411440
throw new Error(
412-
`Error loading the ${identifier} "${identifier}" requested in your config.json - ` +
413-
`Username found in _bridge.username: "${bridgeConfig.username}" is the same as the main bridge. Each external platform/accessory must have it's own unique username.`,
441+
`Error loading the ${type} "${identifier}" requested in your config.json - ` +
442+
`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.`,
414443
);
415444
}
416445
}

0 commit comments

Comments
 (0)