Better handling of EADDRINUSE for Govee light (#117943)
This commit is contained in:
parent
88257c9c42
commit
767d971c5f
9 changed files with 144 additions and 19 deletions
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue