Bonjour Discovery Across Subnets

· antonio's blog

#blog

In home automation, bonjour services can make one's life extremely simple. Plug your device in, connect it to your network and load up your favorite home automation application, and poof! You have access to it automatically!

This is the best case scenario, but there are times where you can't necessarily connect the device directly to your network. This could be due to wanting added security between smart home devices and other networked devices (separate VLANs/subnets) or you don't actually manage the device (a thermostat installed by your landlord). For these reasons, auto discovery may not be possible. This leaves you with the inability to benefit from automated setup and could prevent you from using said device.

Maybe the platform you're using allows you to connect to the device directly over IP, but sometimes this is more of a headache than warranted. Instead, you could use a tool to share bonjour broadcasts between separate networks. That's where bonjourproxy comes in.

bonjourproxy is a small tool that allows you to broadcast the relevant information required for autodiscovery to your separate network. It can be setup to broadcast any number of devices and can be configured for the type of information to broadcast.

For example, let's say you have a smart thermostat on a separate network but you want it to be discoverable by something like Home Assistant that exists on your main network. You can setup a service using a config like so:

 1[[proxyservice]]
 2name = "ecobee"
 3servicetype = "_hap._tcp"
 4domain = ""
 5port = 1200
 6host = "ecobee"
 7ip = "192.168.0.1"
 8textdata = [
 9    "MFG=ecobee Inc."
10]

which broadcasts the relevant Bonjour service to be discovered. The beauty of this system is it can be setup on any linux/mac device and can be used from a single multi-arch docker image. Meaning you can easily run it with something like:

1docker run -itd \
2    --restart=always \
3    --net=host \
4    --name=bonjourproxy \
5    -v /mnt/data/supervisor/share/bonjourproxy/services.toml:/app/services.toml:ro \
6    antoniomika/bonjourproxy:latest

If you're using Home Assistant, there are some changes that you may need to make in order for this to work. Namely, these changes add retry logic to the pairing mechanism.

Here's a diff of the changes I introduced using a custom component:

 1--- a/homeassistant/components/homekit_controller/config_flow.py
 2+++ b/config/custom_components/homekit_controller/config_flow.py
 3@@ -313,6 +313,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
 4         # volatile. We do cache it, but not against the config entry.
 5         # So copy the pairing data and mutate the copy.
 6         pairing_data = pairing.pairing_data.copy()
 7+        _LOGGER.debug(pairing_data)
 8
 9         # Use the accessories data from the pairing operation if it is
10         # available. Otherwise request a fresh copy from the API.
11@@ -320,9 +321,19 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
12         # the same time.
13         accessories = pairing_data.pop("accessories", None)
14         if not accessories:
15-            accessories = await pairing.list_accessories_and_characteristics()
16+            _LOGGER.debug("Attempting to load accessories and characteristics")
17+            while True:
18+                try:
19+                    accessories = await pairing.list_accessories_and_characteristics()
20+                except Exception:
21+                    _LOGGER.exception("Handled exception and retrying")
22+                    continue
23+                break
24
25         bridge_info = get_bridge_information(accessories)
26         name = get_accessory_name(bridge_info)
27
28+        _LOGGER.debug(bridge_info)
29+        _LOGGER.debug(name)
30+
31         return self.async_create_entry(title=name, data=pairing_data)
32diff --git a/homeassistant/components/homekit_controller/connection.py b/config/custom_components/homekit_controller/connection.py
33index 9d8eb00b54..61626310d4 100644
34--- a/homeassistant/components/homekit_controller/connection.py
35+++ b/config/custom_components/homekit_controller/connection.py
36@@ -140,7 +140,12 @@ class HKDevice:
37
38     async def async_setup(self):
39         """Prepare to use a paired HomeKit device in Home Assistant."""
40+        _LOGGER.debug(self)
41+
42         cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id)
43+
44+        _LOGGER.debug(cache)
45+
46         if not cache:
47             if await self.async_refresh_entity_map(self.config_num):
48                 self._polling_interval_remover = async_track_time_interval(
49@@ -152,6 +157,8 @@ class HKDevice:
50         self.accessories = cache["accessories"]
51         self.config_num = cache["config_num"]
52
53+        _LOGGER.debug(cache)
54+
55         self.entity_map = Accessories.from_list(self.accessories)
56
57         self._polling_interval_remover = async_track_time_interval(
58@@ -208,7 +215,13 @@ class HKDevice:
59     async def async_refresh_entity_map(self, config_num):
60         """Handle setup of a HomeKit accessory."""
61         try:
62-            self.accessories = await self.pairing.list_accessories_and_characteristics()
63+            while True:
64+                try:
65+                    self.accessories = await self.pairing.list_accessories_and_characteristics()
66+                except Exception:
67+                    _LOGGER.exception("Setting up the accessory")
68+                    continue
69+                break
70         except AccessoryDisconnectedError:
71             # If we fail to refresh this data then we will naturally retry
72             # later when Bonjour spots c# is still not up to date.