Better handling of EADDRINUSE for Govee light (#117943)

This commit is contained in:
Galorhallen 2024-05-23 08:45:49 +02:00 committed by GitHub
parent 88257c9c42
commit 767d971c5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 144 additions and 19 deletions

View File

@ -3,6 +3,11 @@
from __future__ import annotations from __future__ import annotations
import asyncio 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.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
@ -14,14 +19,29 @@ from .coordinator import GoveeLocalApiCoordinator
PLATFORMS: list[Platform] = [Platform.LIGHT] PLATFORMS: list[Platform] = [Platform.LIGHT]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Govee light local from a config entry.""" """Set up Govee light local from a config entry."""
coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator(hass=hass) coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator(hass=hass)
entry.async_on_unload(coordinator.cleanup)
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() 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() await coordinator.async_config_entry_first_refresh()

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from contextlib import suppress
import logging import logging
from govee_local_api import GoveeController from govee_local_api import GoveeController
@ -39,7 +40,11 @@ async def _async_has_devices(hass: HomeAssistant) -> bool:
update_enabled=False, update_enabled=False,
) )
try:
await controller.start() await controller.start()
except OSError as ex:
_LOGGER.error("Start failed, errno: %d", ex.errno)
return False
try: try:
async with asyncio.timeout(delay=DISCOVERY_TIMEOUT): async with asyncio.timeout(delay=DISCOVERY_TIMEOUT):
@ -49,7 +54,9 @@ async def _async_has_devices(hass: HomeAssistant) -> bool:
_LOGGER.debug("No devices found") _LOGGER.debug("No devices found")
devices_count = len(controller.devices) 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 return devices_count > 0

View File

@ -1,5 +1,6 @@
"""Coordinator for Govee light local.""" """Coordinator for Govee light local."""
import asyncio
from collections.abc import Callable from collections.abc import Callable
import logging import logging
@ -54,9 +55,9 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]):
"""Set discovery callback for automatic Govee light discovery.""" """Set discovery callback for automatic Govee light discovery."""
self._controller.set_device_discovered_callback(callback) self._controller.set_device_discovered_callback(callback)
def cleanup(self) -> None: def cleanup(self) -> asyncio.Event:
"""Stop and cleanup the cooridinator.""" """Stop and cleanup the cooridinator."""
self._controller.cleanup() return self._controller.cleanup()
async def turn_on(self, device: GoveeDevice) -> None: async def turn_on(self, device: GoveeDevice) -> None:
"""Turn on the light.""" """Turn on the light."""

View File

@ -6,5 +6,5 @@
"dependencies": ["network"], "dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local", "documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["govee-local-api==1.4.5"] "requirements": ["govee-local-api==1.5.0"]
} }

View File

@ -986,7 +986,7 @@ gotailwind==0.2.3
govee-ble==0.31.2 govee-ble==0.31.2
# homeassistant.components.govee_light_local # homeassistant.components.govee_light_local
govee-local-api==1.4.5 govee-local-api==1.5.0
# homeassistant.components.remote_rpi_gpio # homeassistant.components.remote_rpi_gpio
gpiozero==1.6.2 gpiozero==1.6.2

View File

@ -809,7 +809,7 @@ gotailwind==0.2.3
govee-ble==0.31.2 govee-ble==0.31.2
# homeassistant.components.govee_light_local # homeassistant.components.govee_light_local
govee-local-api==1.4.5 govee-local-api==1.5.0
# homeassistant.components.gpsd # homeassistant.components.gpsd
gps3==0.33.3 gps3==0.33.3

View File

@ -1,7 +1,8 @@
"""Tests configuration for Govee Local API.""" """Tests configuration for Govee Local API."""
from asyncio import Event
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from govee_local_api import GoveeLightCapability from govee_local_api import GoveeLightCapability
import pytest import pytest
@ -14,6 +15,8 @@ def fixture_mock_govee_api():
"""Set up Govee Local API fixture.""" """Set up Govee Local API fixture."""
mock_api = AsyncMock(spec=GoveeController) mock_api = AsyncMock(spec=GoveeController)
mock_api.start = AsyncMock() 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.turn_on_off = AsyncMock()
mock_api.set_brightness = AsyncMock() mock_api.set_brightness = AsyncMock()
mock_api.set_color = AsyncMock() mock_api.set_color = AsyncMock()

View File

@ -1,5 +1,6 @@
"""Test Govee light local config flow.""" """Test Govee light local config flow."""
from errno import EADDRINUSE
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from govee_local_api import GoveeDevice from govee_local_api import GoveeDevice
@ -12,6 +13,18 @@ from homeassistant.data_entry_flow import FlowResultType
from .conftest import DEFAULT_CAPABILITEIS 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( async def test_creating_entry_has_no_devices(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_govee_api: AsyncMock hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_govee_api: AsyncMock
) -> None: ) -> None:
@ -52,15 +65,7 @@ async def test_creating_entry_has_with_devices(
) -> None: ) -> None:
"""Test setting up Govee with devices.""" """Test setting up Govee with devices."""
mock_govee_api.devices = [ mock_govee_api.devices = _get_devices(mock_govee_api)
GoveeDevice(
controller=mock_govee_api,
ip="192.168.1.100",
fingerprint="asdawdqwdqwd1",
sku="H615A",
capabilities=DEFAULT_CAPABILITEIS,
)
]
with patch( with patch(
"homeassistant.components.govee_light_local.config_flow.GoveeController", "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_govee_api.start.assert_awaited_once()
mock_setup_entry.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()

View File

@ -1,5 +1,6 @@
"""Test Govee light local.""" """Test Govee light local."""
from errno import EADDRINUSE, ENETDOWN
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from govee_local_api import GoveeDevice from govee_local_api import GoveeDevice
@ -138,6 +139,62 @@ async def test_light_setup_retry(
assert entry.state is ConfigEntryState.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: async def test_light_on_off(hass: HomeAssistant, mock_govee_api: MagicMock) -> None:
"""Test adding a known device.""" """Test adding a known device."""