mirror of
https://github.com/home-assistant/core.git
synced 2025-08-03 04:35:11 +02:00
Fix matter websocket reconnect (#84192)
This commit is contained in:
@@ -729,7 +729,6 @@ omit =
|
|||||||
homeassistant/components/mastodon/notify.py
|
homeassistant/components/mastodon/notify.py
|
||||||
homeassistant/components/matrix/*
|
homeassistant/components/matrix/*
|
||||||
homeassistant/components/matter/__init__.py
|
homeassistant/components/matter/__init__.py
|
||||||
homeassistant/components/matter/entity.py
|
|
||||||
homeassistant/components/meater/__init__.py
|
homeassistant/components/meater/__init__.py
|
||||||
homeassistant/components/meater/const.py
|
homeassistant/components/meater/const.py
|
||||||
homeassistant/components/meater/sensor.py
|
homeassistant/components/meater/sensor.py
|
||||||
|
@@ -11,10 +11,11 @@ from matter_server.client.exceptions import (
|
|||||||
FailedCommand,
|
FailedCommand,
|
||||||
InvalidServerVersion,
|
InvalidServerVersion,
|
||||||
)
|
)
|
||||||
|
from matter_server.common.models.error import MatterError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
@@ -32,6 +33,10 @@ from .addon import get_addon_manager
|
|||||||
from .api import async_register_api
|
from .api import async_register_api
|
||||||
from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN, LOGGER
|
from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN, LOGGER
|
||||||
from .device_platform import DEVICE_PLATFORM
|
from .device_platform import DEVICE_PLATFORM
|
||||||
|
from .helpers import MatterEntryData, get_matter
|
||||||
|
|
||||||
|
CONNECT_TIMEOUT = 10
|
||||||
|
LISTEN_READY_TIMEOUT = 30
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
@@ -41,8 +46,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
matter_client = MatterClient(entry.data[CONF_URL], async_get_clientsession(hass))
|
matter_client = MatterClient(entry.data[CONF_URL], async_get_clientsession(hass))
|
||||||
try:
|
try:
|
||||||
await matter_client.connect()
|
async with async_timeout.timeout(CONNECT_TIMEOUT):
|
||||||
except CannotConnect as err:
|
await matter_client.connect()
|
||||||
|
except (CannotConnect, asyncio.TimeoutError) as err:
|
||||||
raise ConfigEntryNotReady("Failed to connect to matter server") from err
|
raise ConfigEntryNotReady("Failed to connect to matter server") from err
|
||||||
except InvalidServerVersion as err:
|
except InvalidServerVersion as err:
|
||||||
if use_addon:
|
if use_addon:
|
||||||
@@ -60,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
raise ConfigEntryNotReady(f"Invalid server version: {err}") from err
|
raise ConfigEntryNotReady(f"Invalid server version: {err}") from err
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
matter_client.logger.exception("Failed to connect to matter server")
|
LOGGER.exception("Failed to connect to matter server")
|
||||||
raise ConfigEntryNotReady(
|
raise ConfigEntryNotReady(
|
||||||
"Unknown error connecting to the Matter server"
|
"Unknown error connecting to the Matter server"
|
||||||
) from err
|
) from err
|
||||||
@@ -75,16 +81,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
||||||
)
|
)
|
||||||
|
|
||||||
# register websocket api
|
|
||||||
async_register_api(hass)
|
async_register_api(hass)
|
||||||
|
|
||||||
# launch the matter client listen task in the background
|
# launch the matter client listen task in the background
|
||||||
# use the init_ready event to keep track if it did initialize successfully
|
# use the init_ready event to wait until initialization is done
|
||||||
init_ready = asyncio.Event()
|
init_ready = asyncio.Event()
|
||||||
listen_task = asyncio.create_task(matter_client.start_listening(init_ready))
|
listen_task = asyncio.create_task(
|
||||||
|
_client_listen(hass, entry, matter_client, init_ready)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with async_timeout.timeout(30):
|
async with async_timeout.timeout(LISTEN_READY_TIMEOUT):
|
||||||
await init_ready.wait()
|
await init_ready.wait()
|
||||||
except asyncio.TimeoutError as err:
|
except asyncio.TimeoutError as err:
|
||||||
listen_task.cancel()
|
listen_task.cancel()
|
||||||
@@ -94,27 +101,58 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass.data[DOMAIN] = {}
|
hass.data[DOMAIN] = {}
|
||||||
_async_init_services(hass)
|
_async_init_services(hass)
|
||||||
|
|
||||||
# we create an intermediate layer (adapter) which keeps track of our nodes
|
# create an intermediate layer (adapter) which keeps track of the nodes
|
||||||
# and discovery of platform entities from the node's attributes
|
# and discovery of platform entities from the node attributes
|
||||||
matter = MatterAdapter(hass, matter_client, entry)
|
matter = MatterAdapter(hass, matter_client, entry)
|
||||||
hass.data[DOMAIN][entry.entry_id] = matter
|
hass.data[DOMAIN][entry.entry_id] = MatterEntryData(matter, listen_task)
|
||||||
|
|
||||||
# forward platform setup to all platforms in the discovery schema
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, DEVICE_PLATFORM)
|
await hass.config_entries.async_forward_entry_setups(entry, DEVICE_PLATFORM)
|
||||||
|
await matter.setup_nodes()
|
||||||
|
|
||||||
# start discovering of node entities as task
|
# If the listen task is already failed, we need to raise ConfigEntryNotReady
|
||||||
asyncio.create_task(matter.setup_nodes())
|
if listen_task.done() and (listen_error := listen_task.exception()) is not None:
|
||||||
|
await hass.config_entries.async_unload_platforms(entry, DEVICE_PLATFORM)
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
try:
|
||||||
|
await matter_client.disconnect()
|
||||||
|
finally:
|
||||||
|
raise ConfigEntryNotReady(listen_error) from listen_error
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def _client_listen(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
matter_client: MatterClient,
|
||||||
|
init_ready: asyncio.Event,
|
||||||
|
) -> None:
|
||||||
|
"""Listen with the client."""
|
||||||
|
try:
|
||||||
|
await matter_client.start_listening(init_ready)
|
||||||
|
except MatterError as err:
|
||||||
|
if entry.state != ConfigEntryState.LOADED:
|
||||||
|
raise
|
||||||
|
LOGGER.error("Failed to listen: %s", err)
|
||||||
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
# We need to guard against unknown exceptions to not crash this task.
|
||||||
|
LOGGER.exception("Unexpected exception: %s", err)
|
||||||
|
if entry.state != ConfigEntryState.LOADED:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not hass.is_stopping:
|
||||||
|
LOGGER.debug("Disconnected from server. Reloading integration")
|
||||||
|
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, DEVICE_PLATFORM)
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, DEVICE_PLATFORM)
|
||||||
|
|
||||||
if unload_ok:
|
if unload_ok:
|
||||||
matter: MatterAdapter = hass.data[DOMAIN].pop(entry.entry_id)
|
matter_entry_data: MatterEntryData = hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
await matter.matter_client.disconnect()
|
matter_entry_data.listen_task.cancel()
|
||||||
|
await matter_entry_data.adapter.matter_client.disconnect()
|
||||||
|
|
||||||
if entry.data.get(CONF_USE_ADDON) and entry.disabled_by:
|
if entry.data.get(CONF_USE_ADDON) and entry.disabled_by:
|
||||||
addon_manager: AddonManager = get_addon_manager(hass)
|
addon_manager: AddonManager = get_addon_manager(hass)
|
||||||
@@ -165,26 +203,17 @@ async def async_remove_config_entry_device(
|
|||||||
if not unique_id:
|
if not unique_id:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id]
|
matter_entry_data: MatterEntryData = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
matter_client = matter_entry_data.adapter.matter_client
|
||||||
|
|
||||||
for node in await matter.matter_client.get_nodes():
|
for node in await matter_client.get_nodes():
|
||||||
if node.unique_id == unique_id:
|
if node.unique_id == unique_id:
|
||||||
await matter.matter_client.remove_node(node.node_id)
|
await matter_client.remove_node(node.node_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def get_matter(hass: HomeAssistant) -> MatterAdapter:
|
|
||||||
"""Return MatterAdapter instance."""
|
|
||||||
# NOTE: This assumes only one Matter connection/fabric can exist.
|
|
||||||
# Shall we support connecting to multiple servers in the client or by config entries?
|
|
||||||
# In case of the config entry we need to fix this.
|
|
||||||
matter: MatterAdapter = next(iter(hass.data[DOMAIN].values()))
|
|
||||||
return matter
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_init_services(hass: HomeAssistant) -> None:
|
def _async_init_services(hass: HomeAssistant) -> None:
|
||||||
"""Init services."""
|
"""Init services."""
|
||||||
|
@@ -13,7 +13,7 @@ from homeassistant.components.websocket_api import ActiveConnection
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
from .adapter import MatterAdapter
|
from .adapter import MatterAdapter
|
||||||
from .const import DOMAIN
|
from .helpers import get_matter
|
||||||
|
|
||||||
ID = "id"
|
ID = "id"
|
||||||
TYPE = "type"
|
TYPE = "type"
|
||||||
@@ -36,7 +36,7 @@ def async_get_matter_adapter(func: Callable) -> Callable:
|
|||||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict
|
hass: HomeAssistant, connection: ActiveConnection, msg: dict
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Provide the Matter client to the function."""
|
"""Provide the Matter client to the function."""
|
||||||
matter: MatterAdapter = next(iter(hass.data[DOMAIN].values()))
|
matter = get_matter(hass)
|
||||||
|
|
||||||
await func(hass, connection, msg, matter)
|
await func(hass, connection, msg, matter)
|
||||||
|
|
||||||
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from chip.clusters import Objects as clusters
|
from chip.clusters import Objects as clusters
|
||||||
from matter_server.common.models import device_types
|
from matter_server.common.models import device_types
|
||||||
@@ -18,11 +17,8 @@ from homeassistant.const import Platform
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
|
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
|
||||||
|
from .helpers import get_matter
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .adapter import MatterAdapter
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -31,7 +27,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Matter binary sensor from Config Entry."""
|
"""Set up Matter binary sensor from Config Entry."""
|
||||||
matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id]
|
matter = get_matter(hass)
|
||||||
matter.register_platform_handler(Platform.BINARY_SENSOR, async_add_entities)
|
matter.register_platform_handler(Platform.BINARY_SENSOR, async_add_entities)
|
||||||
|
|
||||||
|
|
||||||
|
31
homeassistant/components/matter/helpers.py
Normal file
31
homeassistant/components/matter/helpers.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"""Provide integration helpers that are aware of the matter integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .adapter import MatterAdapter
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MatterEntryData:
|
||||||
|
"""Hold Matter data for the config entry."""
|
||||||
|
|
||||||
|
adapter: MatterAdapter
|
||||||
|
listen_task: asyncio.Task
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def get_matter(hass: HomeAssistant) -> MatterAdapter:
|
||||||
|
"""Return MatterAdapter instance."""
|
||||||
|
# NOTE: This assumes only one Matter connection/fabric can exist.
|
||||||
|
# Shall we support connecting to multiple servers in the client or by config entries?
|
||||||
|
# In case of the config entry we need to fix this.
|
||||||
|
matter_entry_data: MatterEntryData = next(iter(hass.data[DOMAIN].values()))
|
||||||
|
return matter_entry_data.adapter
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import Any
|
||||||
|
|
||||||
from chip.clusters import Objects as clusters
|
from chip.clusters import Objects as clusters
|
||||||
from matter_server.common.models import device_types
|
from matter_server.common.models import device_types
|
||||||
@@ -19,13 +19,10 @@ from homeassistant.const import Platform
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
|
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
|
||||||
|
from .helpers import get_matter
|
||||||
from .util import renormalize
|
from .util import renormalize
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .adapter import MatterAdapter
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -33,7 +30,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Matter Light from Config Entry."""
|
"""Set up Matter Light from Config Entry."""
|
||||||
matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id]
|
matter = get_matter(hass)
|
||||||
matter.register_platform_handler(Platform.LIGHT, async_add_entities)
|
matter.register_platform_handler(Platform.LIGHT, async_add_entities)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import Any
|
||||||
|
|
||||||
from chip.clusters import Objects as clusters
|
from chip.clusters import Objects as clusters
|
||||||
from chip.clusters.Types import Nullable, NullValue
|
from chip.clusters.Types import Nullable, NullValue
|
||||||
@@ -29,11 +29,8 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
|
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
|
||||||
|
from .helpers import get_matter
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .adapter import MatterAdapter
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -42,7 +39,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Matter sensors from Config Entry."""
|
"""Set up Matter sensors from Config Entry."""
|
||||||
matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id]
|
matter = get_matter(hass)
|
||||||
matter.register_platform_handler(Platform.SENSOR, async_add_entities)
|
matter.register_platform_handler(Platform.SENSOR, async_add_entities)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import Any
|
||||||
|
|
||||||
from chip.clusters import Objects as clusters
|
from chip.clusters import Objects as clusters
|
||||||
from matter_server.common.models import device_types
|
from matter_server.common.models import device_types
|
||||||
@@ -18,11 +18,8 @@ from homeassistant.const import Platform
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
|
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
|
||||||
|
from .helpers import get_matter
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .adapter import MatterAdapter
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -31,7 +28,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Matter switches from Config Entry."""
|
"""Set up Matter switches from Config Entry."""
|
||||||
matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id]
|
matter = get_matter(hass)
|
||||||
matter.register_platform_handler(Platform.SWITCH, async_add_entities)
|
matter.register_platform_handler(Platform.SWITCH, async_add_entities)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -33,6 +33,9 @@ async def matter_client_fixture() -> AsyncGenerator[MagicMock, None]:
|
|||||||
"""Mock listen."""
|
"""Mock listen."""
|
||||||
if init_ready is not None:
|
if init_ready is not None:
|
||||||
init_ready.set()
|
init_ready.set()
|
||||||
|
listen_block = asyncio.Event()
|
||||||
|
await listen_block.wait()
|
||||||
|
assert False, "Listen was not cancelled!"
|
||||||
|
|
||||||
client.connect = AsyncMock(side_effect=connect)
|
client.connect = AsyncMock(side_effect=connect)
|
||||||
client.start_listening = AsyncMock(side_effect=listen)
|
client.start_listening = AsyncMock(side_effect=listen)
|
||||||
|
@@ -2,20 +2,211 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import AsyncMock, MagicMock, call
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, call, patch
|
||||||
|
|
||||||
from matter_server.client.exceptions import InvalidServerVersion
|
from matter_server.client.exceptions import CannotConnect, InvalidServerVersion
|
||||||
|
from matter_server.common.helpers.util import dataclass_from_dict
|
||||||
|
from matter_server.common.models.error import MatterError
|
||||||
|
from matter_server.common.models.node import MatterNode
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.hassio import HassioAPIError
|
from homeassistant.components.hassio import HassioAPIError
|
||||||
from homeassistant.components.matter.const import DOMAIN
|
from homeassistant.components.matter.const import DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
|
||||||
|
from homeassistant.const import STATE_UNAVAILABLE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import issue_registry as ir
|
from homeassistant.helpers import issue_registry as ir
|
||||||
|
|
||||||
|
from .common import load_and_parse_node_fixture
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="connect_timeout")
|
||||||
|
def connect_timeout_fixture() -> Generator[int, None, None]:
|
||||||
|
"""Mock the connect timeout."""
|
||||||
|
with patch("homeassistant.components.matter.CONNECT_TIMEOUT", new=0) as timeout:
|
||||||
|
yield timeout
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="listen_ready_timeout")
|
||||||
|
def listen_ready_timeout_fixture() -> Generator[int, None, None]:
|
||||||
|
"""Mock the listen ready timeout."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.matter.LISTEN_READY_TIMEOUT", new=0
|
||||||
|
) as timeout:
|
||||||
|
yield timeout
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entry_setup_unload(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
matter_client: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test the integration set up and unload."""
|
||||||
|
node_data = load_and_parse_node_fixture("onoff-light")
|
||||||
|
node = dataclass_from_dict(
|
||||||
|
MatterNode,
|
||||||
|
node_data,
|
||||||
|
)
|
||||||
|
matter_client.get_nodes.return_value = [node]
|
||||||
|
matter_client.get_node.return_value = node
|
||||||
|
entry = MockConfigEntry(domain="matter", data={"url": "ws://localhost:5580/ws"})
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert matter_client.connect.call_count == 1
|
||||||
|
assert entry.state == ConfigEntryState.LOADED
|
||||||
|
entity_state = hass.states.get("light.mock_onoff_light")
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state != STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
|
||||||
|
assert matter_client.disconnect.call_count == 1
|
||||||
|
assert entry.state == ConfigEntryState.NOT_LOADED
|
||||||
|
entity_state = hass.states.get("light.mock_onoff_light")
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
|
||||||
|
async def test_home_assistant_stop(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
matter_client: MagicMock,
|
||||||
|
integration: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test clean up on home assistant stop."""
|
||||||
|
await hass.async_stop()
|
||||||
|
|
||||||
|
assert matter_client.disconnect.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error", [CannotConnect("Boom"), Exception("Boom")])
|
||||||
|
async def test_connect_failed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
matter_client: MagicMock,
|
||||||
|
error: Exception,
|
||||||
|
) -> None:
|
||||||
|
"""Test failure during client connection."""
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data={"url": "ws://localhost:5580/ws"})
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
matter_client.connect.side_effect = error
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connect_timeout(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
matter_client: MagicMock,
|
||||||
|
connect_timeout: int,
|
||||||
|
) -> None:
|
||||||
|
"""Test timeout during client connection."""
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data={"url": "ws://localhost:5580/ws"})
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error", [MatterError("Boom"), Exception("Boom")])
|
||||||
|
async def test_listen_failure_timeout(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
listen_ready_timeout: int,
|
||||||
|
matter_client: MagicMock,
|
||||||
|
error: Exception,
|
||||||
|
) -> None:
|
||||||
|
"""Test client listen errors during the first timeout phase."""
|
||||||
|
|
||||||
|
async def start_listening(listen_ready: asyncio.Event) -> None:
|
||||||
|
"""Mock the client start_listening method."""
|
||||||
|
# Set the connect side effect to stop an endless loop on reload.
|
||||||
|
matter_client.connect.side_effect = MatterError("Boom")
|
||||||
|
raise error
|
||||||
|
|
||||||
|
matter_client.start_listening.side_effect = start_listening
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data={"url": "ws://localhost:5580/ws"})
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error", [MatterError("Boom"), Exception("Boom")])
|
||||||
|
async def test_listen_failure_config_entry_not_loaded(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
matter_client: MagicMock,
|
||||||
|
error: Exception,
|
||||||
|
) -> None:
|
||||||
|
"""Test client listen errors during the final phase before config entry loaded."""
|
||||||
|
listen_block = asyncio.Event()
|
||||||
|
|
||||||
|
async def start_listening(listen_ready: asyncio.Event) -> None:
|
||||||
|
"""Mock the client start_listening method."""
|
||||||
|
listen_ready.set()
|
||||||
|
await listen_block.wait()
|
||||||
|
# Set the connect side effect to stop an endless loop on reload.
|
||||||
|
matter_client.connect.side_effect = MatterError("Boom")
|
||||||
|
raise error
|
||||||
|
|
||||||
|
async def get_nodes() -> list[MagicMock]:
|
||||||
|
"""Mock the client get_nodes method."""
|
||||||
|
listen_block.set()
|
||||||
|
return []
|
||||||
|
|
||||||
|
matter_client.start_listening.side_effect = start_listening
|
||||||
|
matter_client.get_nodes.side_effect = get_nodes
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data={"url": "ws://localhost:5580/ws"})
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
assert matter_client.disconnect.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error", [MatterError("Boom"), Exception("Boom")])
|
||||||
|
async def test_listen_failure_config_entry_loaded(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
matter_client: MagicMock,
|
||||||
|
error: Exception,
|
||||||
|
) -> None:
|
||||||
|
"""Test client listen errors after config entry is loaded."""
|
||||||
|
listen_block = asyncio.Event()
|
||||||
|
|
||||||
|
async def start_listening(listen_ready: asyncio.Event) -> None:
|
||||||
|
"""Mock the client start_listening method."""
|
||||||
|
listen_ready.set()
|
||||||
|
await listen_block.wait()
|
||||||
|
# Set the connect side effect to stop an endless loop on reload.
|
||||||
|
matter_client.connect.side_effect = MatterError("Boom")
|
||||||
|
raise error
|
||||||
|
|
||||||
|
matter_client.start_listening.side_effect = start_listening
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data={"url": "ws://localhost:5580/ws"})
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state == ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
listen_block.set()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state == ConfigEntryState.SETUP_RETRY
|
||||||
|
assert matter_client.disconnect.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_raise_addon_task_in_progress(
|
async def test_raise_addon_task_in_progress(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
addon_not_installed: AsyncMock,
|
addon_not_installed: AsyncMock,
|
||||||
|
Reference in New Issue
Block a user