Skip to content

Commit 9b709d4

Browse files
authored
Add bridge device support with connectivity sensor and via_device linking (#379)
- Register bridge (Wi-Fi Hub) devices in Home Assistant with a connectivity binary sensor showing online/offline status - Link child devices (sprinklers, flood sensors) to their bridge via via_device using device_gateway_topic mapping - Expose MAC address on all devices via CONNECTION_NETWORK_MAC - Always include bridge devices regardless of user device selection - Exclude bridges from config flow device picker and device removal logic
1 parent 35e7790 commit 9b709d4

7 files changed

Lines changed: 348 additions & 20 deletions

File tree

custom_components/bhyve/__init__.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
)
1919
from homeassistant.helpers import device_registry as dr
2020
from homeassistant.helpers.aiohttp_client import async_get_clientsession
21-
from homeassistant.helpers.device_registry import DeviceInfo
21+
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
2222
from homeassistant.helpers.typing import ConfigType
2323
from homeassistant.helpers.update_coordinator import CoordinatorEntity
2424

2525
from custom_components.bhyve.pybhyve.typings import BHyveDevice
2626

2727
from .const import (
2828
CONF_DEVICES,
29+
DEVICE_BRIDGE,
2930
DOMAIN,
3031
EVENT_PROGRAM_CHANGED,
3132
LOGGER,
@@ -103,6 +104,17 @@ async def async_update_callback(data: dict) -> None:
103104
programs = await client.timer_programs
104105
devices = filter_configured_devices(entry, all_devices)
105106

107+
# Build a mapping from device_gateway_topic to bridge device ID
108+
# so child devices can reference their bridge via via_device.
109+
# Bridges are always included by filter_configured_devices.
110+
gateway_to_bridge: dict[str, str] = {}
111+
for device in devices:
112+
if device.get("type") == DEVICE_BRIDGE:
113+
gateway_topic = device.get("device_gateway_topic")
114+
if gateway_topic:
115+
gateway_to_bridge[gateway_topic] = device.get("id", "")
116+
coordinator.gateway_to_bridge = gateway_to_bridge
117+
106118
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
107119
"client": client,
108120
"coordinator": coordinator,
@@ -146,9 +158,12 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
146158
# Get currently configured device IDs
147159
configured_ids = set(entry.options.get(CONF_DEVICES, []))
148160

149-
# Find devices that were removed from configuration
150-
all_device_ids = {str(d["id"]) for d in all_devices}
151-
removed_device_ids = all_device_ids - configured_ids
161+
# Find leaf devices that were removed from configuration
162+
# (bridges are always included and not user-selectable)
163+
leaf_device_ids = {
164+
str(d["id"]) for d in all_devices if d.get("type") != DEVICE_BRIDGE
165+
}
166+
removed_device_ids = leaf_device_ids - configured_ids
152167

153168
if removed_device_ids:
154169
# Remove devices from Home Assistant
@@ -183,8 +198,16 @@ def __init__(
183198
self._mac_address = device.get("mac_address")
184199

185200
# Device info for grouping
186-
self._attr_device_info = DeviceInfo(
201+
connections: set[tuple[str, str]] = set()
202+
if self._mac_address:
203+
# Format raw MAC (e.g. "4467552a366e") with colons
204+
raw = self._mac_address.replace(":", "").replace("-", "").lower()
205+
formatted_mac = ":".join(raw[i : i + 2] for i in range(0, len(raw), 2))
206+
connections.add((CONNECTION_NETWORK_MAC, formatted_mac))
207+
208+
device_info = DeviceInfo(
187209
identifiers={(DOMAIN, self._device_id)},
210+
connections=connections,
188211
manufacturer=MANUFACTURER,
189212
configuration_url=f"https://techsupport.orbitbhyve.com/dashboard/support/device/{self._device_id}",
190213
name=self._device_name,
@@ -193,6 +216,16 @@ def __init__(
193216
sw_version=device.get("firmware_version"),
194217
)
195218

219+
# Link non-bridge devices to their bridge via device_gateway_topic
220+
if self._device_type != DEVICE_BRIDGE:
221+
gateway_topic = device.get("device_gateway_topic")
222+
gateway_to_bridge = getattr(coordinator, "gateway_to_bridge", {})
223+
bridge_id = gateway_to_bridge.get(gateway_topic) if gateway_topic else None
224+
if bridge_id:
225+
device_info["via_device"] = (DOMAIN, bridge_id)
226+
227+
self._attr_device_info = device_info
228+
196229
LOGGER.debug(
197230
"Creating %s: %s - %s",
198231
self.__class__.__name__,

custom_components/bhyve/binary_sensor.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from __future__ import annotations
44

5-
import logging
65
from dataclasses import dataclass
76
from typing import TYPE_CHECKING, Any
87

@@ -13,7 +12,7 @@
1312
)
1413

1514
from . import BHyveCoordinatorEntity
16-
from .const import DEVICE_FLOOD, DEVICE_SPRINKLER, DOMAIN
15+
from .const import DEVICE_BRIDGE, DEVICE_FLOOD, DEVICE_SPRINKLER, DOMAIN
1716

1817
if TYPE_CHECKING:
1918
from homeassistant.config_entries import ConfigEntry
@@ -23,8 +22,6 @@
2322
from .coordinator import BHyveDataUpdateCoordinator
2423
from .pybhyve.typings import BHyveDevice
2524

26-
_LOGGER = logging.getLogger(__name__)
27-
2825

2926
@dataclass(frozen=True, kw_only=True)
3027
class BHyveBinarySensorEntityDescription(BinarySensorEntityDescription):
@@ -84,6 +81,15 @@ class BHyveBinarySensorEntityDescription(BinarySensorEntityDescription):
8481
"station_faults": data.get("status", {}).get("station_faults", []),
8582
},
8683
),
84+
BHyveBinarySensorEntityDescription(
85+
key="connectivity",
86+
translation_key="connectivity",
87+
name="Connected",
88+
device_class=BinarySensorDeviceClass.CONNECTIVITY,
89+
unique_id_suffix="connectivity",
90+
device_types=(DEVICE_BRIDGE,),
91+
value_fn=lambda data: data.get("is_connected", False),
92+
),
8793
)
8894

8995

custom_components/bhyve/coordinator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def __init__(
5252
)
5353
self.client = client
5454
self.entry = entry
55+
self.gateway_to_bridge: dict[str, str] = {}
5556

5657
async def _async_update_data(self) -> dict[str, Any]:
5758
"""Fetch data from API (periodic polling)."""

custom_components/bhyve/strings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@
157157
"on": "Fault",
158158
"off": "OK"
159159
}
160+
},
161+
"connectivity": {
162+
"name": "Connected"
160163
}
161164
},
162165
"sensor": {

custom_components/bhyve/util.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from homeassistant.config_entries import ConfigEntry
66
from homeassistant.util import dt
77

8-
from .const import CONF_DEVICES
8+
from .const import CONF_DEVICES, DEVICE_BRIDGE
99

1010

1111
def orbit_time_to_local_time(timestamp: str | None) -> datetime | None:
@@ -18,9 +18,18 @@ def orbit_time_to_local_time(timestamp: str | None) -> datetime | None:
1818

1919

2020
def filter_configured_devices(entry: ConfigEntry, all_devices: list) -> list:
21-
"""Filter the device list to those that are enabled in options."""
21+
"""
22+
Filter the device list to those that are enabled in options.
23+
24+
Bridge devices are always included since they are not user-selectable
25+
but are needed to register the hub in Home Assistant.
26+
"""
2227
configured_devices = entry.options.get(CONF_DEVICES, [])
23-
filtered_devices = [d for d in all_devices if str(d["id"]) in configured_devices]
28+
filtered_devices = [
29+
d
30+
for d in all_devices
31+
if str(d["id"]) in configured_devices or d.get("type") == DEVICE_BRIDGE
32+
]
2433

2534
# Ensure that all devices have a name
2635
for device in filtered_devices:

0 commit comments

Comments
 (0)