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
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)
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()

View file

@ -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,
)
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

View file

@ -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."""

View file

@ -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"]
}

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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."""