diff --git a/homeassistant/components/govee_light_local/__init__.py b/homeassistant/components/govee_light_local/__init__.py index ee04dd81088..00f77189e2b 100644 --- a/homeassistant/components/govee_light_local/__init__.py +++ b/homeassistant/components/govee_light_local/__init__.py @@ -9,6 +9,7 @@ import logging from govee_local_api.controller import LISTENING_PORT +from homeassistant.components import network from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -23,12 +24,24 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool: """Set up Govee light local from a config entry.""" - coordinator = GoveeLocalApiCoordinator(hass, entry) + + # Get source IPs for all enabled adapters + source_ips = await network.async_get_enabled_source_ips(hass) + _LOGGER.debug("Enabled source IPs: %s", source_ips) + + coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator( + hass=hass, config_entry=entry, source_ips=source_ips + ) async def await_cleanup(): - cleanup_complete: asyncio.Event = coordinator.cleanup() + cleanup_complete_events: [asyncio.Event] = coordinator.cleanup() with suppress(TimeoutError): - await asyncio.wait_for(cleanup_complete.wait(), 1) + await asyncio.gather( + *[ + asyncio.wait_for(cleanup_complete_event.wait(), 1) + for cleanup_complete_event in cleanup_complete_events + ] + ) entry.async_on_unload(await_cleanup) diff --git a/homeassistant/components/govee_light_local/config_flow.py b/homeassistant/components/govee_light_local/config_flow.py index da70d44688b..67fa4b548cd 100644 --- a/homeassistant/components/govee_light_local/config_flow.py +++ b/homeassistant/components/govee_light_local/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from contextlib import suppress +from ipaddress import IPv4Address, IPv6Address import logging from govee_local_api import GoveeController @@ -23,15 +24,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def _async_has_devices(hass: HomeAssistant) -> bool: - """Return if there are devices that can be discovered.""" - - adapter = await network.async_get_source_ip(hass, network.PUBLIC_TARGET_IP) - +async def _async_discover( + hass: HomeAssistant, adapter_ip: IPv4Address | IPv6Address +) -> bool: controller: GoveeController = GoveeController( loop=hass.loop, logger=_LOGGER, - listening_address=adapter, + listening_address=str(adapter_ip), broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT, broadcast_port=CONF_TARGET_PORT_DEFAULT, listening_port=CONF_LISTENING_PORT_DEFAULT, @@ -41,9 +40,10 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: ) try: + _LOGGER.debug("Starting discovery with IP %s", adapter_ip) await controller.start() except OSError as ex: - _LOGGER.error("Start failed, errno: %d", ex.errno) + _LOGGER.error("Start failed on IP %s, errno: %d", adapter_ip, ex.errno) return False try: @@ -51,16 +51,34 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: while not controller.devices: await asyncio.sleep(delay=1) except TimeoutError: - _LOGGER.debug("No devices found") + _LOGGER.debug("No devices found with IP %s", adapter_ip) devices_count = len(controller.devices) - cleanup_complete: asyncio.Event = controller.cleanup() + cleanup_complete_events: list[asyncio.Event] = [] with suppress(TimeoutError): - await asyncio.wait_for(cleanup_complete.wait(), 1) + await asyncio.gather( + *[ + asyncio.wait_for(cleanup_complete_event.wait(), 1) + for cleanup_complete_event in cleanup_complete_events + ] + ) return devices_count > 0 +async def _async_has_devices(hass: HomeAssistant) -> bool: + """Return if there are devices that can be discovered.""" + + # Get source IPs for all enabled adapters + source_ips = await network.async_get_enabled_source_ips(hass) + _LOGGER.debug("Enabled source IPs: %s", source_ips) + + # Run discovery on every IPv4 address and gather results + results = await asyncio.gather(*[_async_discover(hass, ip) for ip in source_ips]) + + return any(results) + + config_entry_flow.register_discovery_flow( DOMAIN, "Govee light local", _async_has_devices ) diff --git a/homeassistant/components/govee_light_local/coordinator.py b/homeassistant/components/govee_light_local/coordinator.py index 530ade1f743..9e0792a132d 100644 --- a/homeassistant/components/govee_light_local/coordinator.py +++ b/homeassistant/components/govee_light_local/coordinator.py @@ -2,6 +2,7 @@ import asyncio from collections.abc import Callable +from ipaddress import IPv4Address, IPv6Address import logging from govee_local_api import GoveeController, GoveeDevice @@ -11,7 +12,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( - CONF_DISCOVERY_INTERVAL_DEFAULT, CONF_LISTENING_PORT_DEFAULT, CONF_MULTICAST_ADDRESS_DEFAULT, CONF_TARGET_PORT_DEFAULT, @@ -26,10 +26,11 @@ type GoveeLocalConfigEntry = ConfigEntry[GoveeLocalApiCoordinator] class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]): """Govee light local coordinator.""" - config_entry: GoveeLocalConfigEntry - def __init__( - self, hass: HomeAssistant, config_entry: GoveeLocalConfigEntry + self, + hass: HomeAssistant, + config_entry: GoveeLocalConfigEntry, + source_ips: list[IPv4Address | IPv6Address], ) -> None: """Initialize my coordinator.""" super().__init__( @@ -40,32 +41,40 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]): update_interval=SCAN_INTERVAL, ) - self._controller = GoveeController( - loop=hass.loop, - logger=_LOGGER, - broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT, - broadcast_port=CONF_TARGET_PORT_DEFAULT, - listening_port=CONF_LISTENING_PORT_DEFAULT, - discovery_enabled=True, - discovery_interval=CONF_DISCOVERY_INTERVAL_DEFAULT, - discovered_callback=None, - update_enabled=False, - ) + self._controllers: list[GoveeController] = [ + GoveeController( + loop=hass.loop, + logger=_LOGGER, + listening_address=str(source_ip), + broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT, + broadcast_port=CONF_TARGET_PORT_DEFAULT, + listening_port=CONF_LISTENING_PORT_DEFAULT, + discovery_enabled=True, + discovery_interval=1, + update_enabled=False, + ) + for source_ip in source_ips + ] async def start(self) -> None: """Start the Govee coordinator.""" - await self._controller.start() - self._controller.send_update_message() + + for controller in self._controllers: + await controller.start() + controller.send_update_message() async def set_discovery_callback( self, callback: Callable[[GoveeDevice, bool], bool] ) -> None: """Set discovery callback for automatic Govee light discovery.""" - self._controller.set_device_discovered_callback(callback) - def cleanup(self) -> asyncio.Event: - """Stop and cleanup the cooridinator.""" - return self._controller.cleanup() + for controller in self._controllers: + controller.set_device_discovered_callback(callback) + + def cleanup(self) -> list[asyncio.Event]: + """Stop and cleanup the coordinator.""" + + return [controller.cleanup() for controller in self._controllers] async def turn_on(self, device: GoveeDevice) -> None: """Turn on the light.""" @@ -96,8 +105,14 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]): @property def devices(self) -> list[GoveeDevice]: """Return a list of discovered Govee devices.""" - return self._controller.devices + + devices: list[GoveeDevice] = [] + for controller in self._controllers: + devices = devices + controller.devices + return devices async def _async_update_data(self) -> list[GoveeDevice]: - self._controller.send_update_message() - return self._controller.devices + for controller in self._controllers: + controller.send_update_message() + + return self.devices