diff --git a/homeassistant/components/govee_light_local/__init__.py b/homeassistant/components/govee_light_local/__init__.py index d2537fb5c9b..088f9bae22b 100644 --- a/homeassistant/components/govee_light_local/__init__.py +++ b/homeassistant/components/govee_light_local/__init__.py @@ -3,6 +3,11 @@ from __future__ import annotations import asyncio +from contextlib import suppress +from errno import EADDRINUSE +import logging + +from govee_local_api.controller import LISTENING_PORT from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -14,14 +19,29 @@ from .coordinator import GoveeLocalApiCoordinator PLATFORMS: list[Platform] = [Platform.LIGHT] +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Govee light local from a config entry.""" coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator(hass=hass) - entry.async_on_unload(coordinator.cleanup) - await coordinator.start() + async def await_cleanup(): + cleanup_complete: asyncio.Event = coordinator.cleanup() + with suppress(TimeoutError): + await asyncio.wait_for(cleanup_complete.wait(), 1) + + entry.async_on_unload(await_cleanup) + + try: + await coordinator.start() + except OSError as ex: + if ex.errno != EADDRINUSE: + _LOGGER.error("Start failed, errno: %d", ex.errno) + return False + _LOGGER.error("Port %s already in use", LISTENING_PORT) + raise ConfigEntryNotReady from ex await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/govee_light_local/config_flow.py b/homeassistant/components/govee_light_local/config_flow.py index d31bfed0579..da70d44688b 100644 --- a/homeassistant/components/govee_light_local/config_flow.py +++ b/homeassistant/components/govee_light_local/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from contextlib import suppress import logging from govee_local_api import GoveeController @@ -39,7 +40,11 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: update_enabled=False, ) - await controller.start() + try: + await controller.start() + except OSError as ex: + _LOGGER.error("Start failed, errno: %d", ex.errno) + return False try: async with asyncio.timeout(delay=DISCOVERY_TIMEOUT): @@ -49,7 +54,9 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: _LOGGER.debug("No devices found") devices_count = len(controller.devices) - controller.cleanup() + cleanup_complete: asyncio.Event = controller.cleanup() + with suppress(TimeoutError): + await asyncio.wait_for(cleanup_complete.wait(), 1) return devices_count > 0 diff --git a/homeassistant/components/govee_light_local/coordinator.py b/homeassistant/components/govee_light_local/coordinator.py index 79b572e89ae..64119f1871c 100644 --- a/homeassistant/components/govee_light_local/coordinator.py +++ b/homeassistant/components/govee_light_local/coordinator.py @@ -1,5 +1,6 @@ """Coordinator for Govee light local.""" +import asyncio from collections.abc import Callable import logging @@ -54,9 +55,9 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]): """Set discovery callback for automatic Govee light discovery.""" self._controller.set_device_discovered_callback(callback) - def cleanup(self) -> None: + def cleanup(self) -> asyncio.Event: """Stop and cleanup the cooridinator.""" - self._controller.cleanup() + return self._controller.cleanup() async def turn_on(self, device: GoveeDevice) -> None: """Turn on the light.""" diff --git a/homeassistant/components/govee_light_local/manifest.json b/homeassistant/components/govee_light_local/manifest.json index df72a082190..93a19408182 100644 --- a/homeassistant/components/govee_light_local/manifest.json +++ b/homeassistant/components/govee_light_local/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/govee_light_local", "iot_class": "local_push", - "requirements": ["govee-local-api==1.4.5"] + "requirements": ["govee-local-api==1.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cd2e79e3b31..db344424cd9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -986,7 +986,7 @@ gotailwind==0.2.3 govee-ble==0.31.2 # homeassistant.components.govee_light_local -govee-local-api==1.4.5 +govee-local-api==1.5.0 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d42fa0be924..592ad3ff8b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -809,7 +809,7 @@ gotailwind==0.2.3 govee-ble==0.31.2 # homeassistant.components.govee_light_local -govee-local-api==1.4.5 +govee-local-api==1.5.0 # homeassistant.components.gpsd gps3==0.33.3 diff --git a/tests/components/govee_light_local/conftest.py b/tests/components/govee_light_local/conftest.py index 5976d3c1b74..1c0f678e485 100644 --- a/tests/components/govee_light_local/conftest.py +++ b/tests/components/govee_light_local/conftest.py @@ -1,7 +1,8 @@ """Tests configuration for Govee Local API.""" +from asyncio import Event from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from govee_local_api import GoveeLightCapability import pytest @@ -14,6 +15,8 @@ def fixture_mock_govee_api(): """Set up Govee Local API fixture.""" mock_api = AsyncMock(spec=GoveeController) mock_api.start = AsyncMock() + mock_api.cleanup = MagicMock(return_value=Event()) + mock_api.cleanup.return_value.set() mock_api.turn_on_off = AsyncMock() mock_api.set_brightness = AsyncMock() mock_api.set_color = AsyncMock() diff --git a/tests/components/govee_light_local/test_config_flow.py b/tests/components/govee_light_local/test_config_flow.py index 1f935f18530..2e7144fae3a 100644 --- a/tests/components/govee_light_local/test_config_flow.py +++ b/tests/components/govee_light_local/test_config_flow.py @@ -1,5 +1,6 @@ """Test Govee light local config flow.""" +from errno import EADDRINUSE from unittest.mock import AsyncMock, patch from govee_local_api import GoveeDevice @@ -12,6 +13,18 @@ from homeassistant.data_entry_flow import FlowResultType from .conftest import DEFAULT_CAPABILITEIS +def _get_devices(mock_govee_api: AsyncMock) -> list[GoveeDevice]: + return [ + GoveeDevice( + controller=mock_govee_api, + ip="192.168.1.100", + fingerprint="asdawdqwdqwd1", + sku="H615A", + capabilities=DEFAULT_CAPABILITEIS, + ) + ] + + async def test_creating_entry_has_no_devices( hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_govee_api: AsyncMock ) -> None: @@ -52,15 +65,7 @@ async def test_creating_entry_has_with_devices( ) -> None: """Test setting up Govee with devices.""" - mock_govee_api.devices = [ - GoveeDevice( - controller=mock_govee_api, - ip="192.168.1.100", - fingerprint="asdawdqwdqwd1", - sku="H615A", - capabilities=DEFAULT_CAPABILITEIS, - ) - ] + mock_govee_api.devices = _get_devices(mock_govee_api) with patch( "homeassistant.components.govee_light_local.config_flow.GoveeController", @@ -80,3 +85,35 @@ async def test_creating_entry_has_with_devices( mock_govee_api.start.assert_awaited_once() mock_setup_entry.assert_awaited_once() + + +async def test_creating_entry_errno( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_govee_api: AsyncMock, +) -> None: + """Test setting up Govee with devices.""" + + e = OSError() + e.errno = EADDRINUSE + mock_govee_api.start.side_effect = e + mock_govee_api.devices = _get_devices(mock_govee_api) + + with patch( + "homeassistant.components.govee_light_local.config_flow.GoveeController", + return_value=mock_govee_api, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] is FlowResultType.ABORT + + await hass.async_block_till_done() + + assert mock_govee_api.start.call_count == 1 + mock_setup_entry.assert_not_awaited() diff --git a/tests/components/govee_light_local/test_light.py b/tests/components/govee_light_local/test_light.py index 3bc9da77fe5..4a1125643fa 100644 --- a/tests/components/govee_light_local/test_light.py +++ b/tests/components/govee_light_local/test_light.py @@ -1,5 +1,6 @@ """Test Govee light local.""" +from errno import EADDRINUSE, ENETDOWN from unittest.mock import AsyncMock, MagicMock, patch from govee_local_api import GoveeDevice @@ -138,6 +139,62 @@ async def test_light_setup_retry( assert entry.state is ConfigEntryState.SETUP_RETRY +async def test_light_setup_retry_eaddrinuse( + hass: HomeAssistant, mock_govee_api: AsyncMock +) -> None: + """Test adding an unknown device.""" + + mock_govee_api.start.side_effect = OSError() + mock_govee_api.start.side_effect.errno = EADDRINUSE + mock_govee_api.devices = [ + GoveeDevice( + controller=mock_govee_api, + ip="192.168.1.100", + fingerprint="asdawdqwdqwd", + sku="H615A", + capabilities=DEFAULT_CAPABILITEIS, + ) + ] + + with patch( + "homeassistant.components.govee_light_local.coordinator.GoveeController", + return_value=mock_govee_api, + ): + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_light_setup_error( + hass: HomeAssistant, mock_govee_api: AsyncMock +) -> None: + """Test adding an unknown device.""" + + mock_govee_api.start.side_effect = OSError() + mock_govee_api.start.side_effect.errno = ENETDOWN + mock_govee_api.devices = [ + GoveeDevice( + controller=mock_govee_api, + ip="192.168.1.100", + fingerprint="asdawdqwdqwd", + sku="H615A", + capabilities=DEFAULT_CAPABILITEIS, + ) + ] + + with patch( + "homeassistant.components.govee_light_local.coordinator.GoveeController", + return_value=mock_govee_api, + ): + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_ERROR + + async def test_light_on_off(hass: HomeAssistant, mock_govee_api: MagicMock) -> None: """Test adding a known device."""