Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Child Bridge Support #2786

Merged
merged 18 commits into from Feb 1, 2021
Merged

Child Bridge Support #2786

merged 18 commits into from Feb 1, 2021

Conversation

oznu
Copy link
Member

@oznu oznu commented Jan 30, 2021

♻️ Current situation

Currently all Homebridge platform and accessory plugins are exposed to a single bridge (with the exception to external accessories). This results in the bridge only responding as fast as the slowest plugin, and can cause the entire bridge to crash if a single platform or accessory throws an unhandled exception.

💡 Proposed solution

This feature allows any Homebridge platform or accessory to optionally run as it's own independent accessory, seperate from the main bridge, and in an isolated process. There are several reasons / benefits from doing this:

  • Isolate plugin code from the main bridge - in this mode the plugin will run in it's own child process, preventing it from ever crashing the main bridge if a fatal exception occurs.
  • If the plugin process does crash, Homebridge will automatically restart it, without impacting the main bridge or other plugins.
  • The plugin is protected from dependency polution caused by other plugins (rarely happens).
  • Isolate slow plugins, preventing them from slowing down the main bridge or other plugins - HomeKit requests the status of all the accessories on a single bridge when the Home app is opened, as a result the response time is only as fast as your slowest plugin. Loading your accessory / platform as a seperate bridge will allow HomeKit to make concurrent requests.
  • Easily work around the 149 accessory limitation of a bridge without having to run multiple instances.
  • Run multiple instances of platform-based plugins (for example, connecting two different Ring accounts using the homebridge-ring plugin).
  • Prevent static platform plugins from blocking the main bridge or other plugins while it initialises (for example, homebridge-hue while it is trying to discover the Hue/Deconz bridge)
  • Gain all the benefits of running multiple instances of Homebridge without the management overhead.

This will work with all existing plugins without requiring then to perform any code changes.

Users will be able to use the Homebridge UI to enable the feature:

image

Users can also manually add the require config to each platform/accessory config block you want to be exposed as a seperate bridge:

Make sure the username is unique! It must not be the same as the main bridge or another other bridged plugin!

"_bridge": {
    "username": "0E:B8:2B:20:76:36"
}

The following can also optionally set:

  • pin - optional - defaults to the same as the main bridge
  • port - optional - defaults to a random unused port assigned by HAPNode.js
  • name - optional - defaults to accessory/platform name
  • model - optional - defaults to same as main bridge
  • manufacturer - optional - defaults to same as main bridge
  • setupID - optional - default to unique setup id generated by HAPNode.js (so we can have different QR codes for each child bridge eventually)

An IPC service has also been added to allow tools such as the Homebridge UI to restart individual child bridges. Currently this just uses Node.js parent/child process IPC, at some point in the future we may swap to using node-ipc to allow control from a non-parent process.

Problem that is solved

See above.

Implications

This is an opt-in feature. Existing setups should not be impacted.

Testing

I have tested this with accessories, static and dynamic platform plugins.

Reviewer Nudging

@Supereg

@oznu oznu requested a review from Supereg January 30, 2021 04:00
Copy link
Member

@Supereg Supereg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work here 🚀 I think this will be nice addition to the 1.3.0 release 👍

Just had some smaller comments.

package-lock.json Outdated Show resolved Hide resolved
const filePath = path.resolve(this.baseDirectory, itemName);
return fs.readJsonSync(filePath);
} catch (e) {
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK getItemSync is currently used in loadCachedPlatformAccessoriesFromDisk().
What I currently don't like that errors in the StorageService are complexity silenced.

Additionally loadCachedPlatformAccessoriesFromDisk() will just do nothing if getItemSync returns null.

Am I missing anything that this "empty" catch makes sense?
If we encounter an IO error, we would first of all never the error message, and homebridge would just not load cached accessories silently.

I would argue for a solution where homebridge fails to init if the file can't be loaded (maybe including some logic to repair permissions, which are no 1. issue I think).

Same applies to the Promise based method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reasons this would throw would be if the file does not exist yet or if the file is corrupted (not valid json) - in either case we would want to write a new cache file.

We could check for the files existence before attempting to read it so we can log an error if the file is corrupted or we don't have permissions to read it - that just requires one more stat call.

I wouldn't prevent the bridge from starting if the JSON isn't valid - just re-write and move on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. I would suggest to make that explicit then. So comment those individual scenarios in the catch clause (maybe adding a debug log statement of the logger?).

And for the corrupted file scenario, I would propose that we try as much as possible to recover (e.g. ensure permissions set) and outer wise overwrite as you suggest, though with logging a warning. So we can easily debug why cachedAccessories was reset.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes have been made - red error log messages will explain that the cached accessories either cannot be saved or cannot be loaded and the implications of this. It will not prevent Homebridge from loading.

Also updated the loadCachedPlatformAccessoriesFromDisk to use the async methods, as the only places we use this is in async functions, and async is better so why not 😄

src/childBridgeFork.ts Show resolved Hide resolved
@oznu oznu merged commit b8528db into beta Feb 1, 2021
@oznu oznu deleted the preview/forked-external-bridges branch February 1, 2021 22:18
@beatpaul
Copy link

beatpaul commented Feb 6, 2021

What a great Feature, Thx !!
Since probably everyone who uses many plugins separated them in multiple instances I have a Question:
Is there a way to merge multiple instances and using the Child Bridge-feature without breaking an old installation (not having to newly add accessories, not breaking old automations, e.g.)
This would probably be really helpful for lots of users :)

@ebaauw
Copy link
Contributor

ebaauw commented Feb 6, 2021

This would be a manual process, as it's not supported by the UI. Basically you need to:

  • Shutdown the old installation;
  • Note the username under the bridge section in config.json;
  • Copy the AccessoryInfo.XXXXXXXXXXXX.json and IdentifierCache.XXXXXXXXXXXX.json files from the persist directory of the old installation to the persist directory of new installation, where XXXXXXXXXXXX is the username from config.json, without the :s.
  • Copy the cachedAccessories from the accessories directory of the old installation to cachedAccessories.XXXXXXXXXXXX in the accessories directory of the new installation;
  • Copy over any Eve history files;
  • Edit config.json of the new installation, adding the _bridge object to the platform object of the platforms array, using the username from the old installation.
  • Restart the new installation.

You only have one chance to get this right. If you start the (child) bridge with the old username without exposing the accessories, HomeKit will delete them. Next time you re-expose them, HomeKit will treat them as new accessories, losing any associations to HomeKit rooms, groups, scenes, and automations. Best play around with child bridge setup first, so you get a feel for how it's configured.

@beatpaul
Copy link

beatpaul commented Feb 6, 2021

Great ! (a bit scary though...)
Is there a way to also merge accessories (not platforms)?
What if more than one plugin was running on another instance ?

I will try to merge one instance when the final release 1.3 of Homebridge is published

Thanks for your help!

@donavanbecker
Copy link
Contributor

Great ! (a bit scary though...)

Is there a way to also merge accessories (not platforms)?

What if more than one plugin was running on another instance ?

I will try to merge one instance when the final release 1.3 of Homebridge is published

Thanks for your help!

I believe you should be able copy the same cached accessories for each plugin. Then when the bridge loads up it will be remove the cache accessories that aren't needed.

I can test this and let you know.

@ebaauw
Copy link
Contributor

ebaauw commented Feb 6, 2021

HomeKit identifies accessories by uuid and bridge. So you can restore cached accessories to another bridge by copying/renaming cachedAccessories, but if you expose them through a bridge with a different username, you’ll stil lose your rooms, groups, scenes, and automations.

@donavanbecker
Copy link
Contributor

HomeKit identifies accessories by uuid and bridge. So you can restore cached accessories to another bridge by copying/renaming cachedAccessories, but if you expose them through a bridge with a different username, you’ll stil lose your rooms, groups, scenes, and automations.

So you could potentially change the username in the cacheAccessories.

@ebaauw
Copy link
Contributor

ebaauw commented Feb 7, 2021

It’s the username of the bridge, that’s not even in cachedAccessories.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants