Don't crash entire Matter integration setup when one node is failing (#126491)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
pull/126743/head
Marcel van der Veldt 2024-09-25 12:45:24 +02:00 committed by GitHub
parent a5b556b21b
commit 18766905f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 59 additions and 39 deletions

View File

@ -56,10 +56,6 @@ class MatterAdapter:
"""Set up all existing nodes and subscribe to new nodes."""
initialized_nodes: set[int] = set()
for node in self.matter_client.get_nodes():
if not node.available:
# ignore un-initialized nodes at startup
# catch them later when they become available.
continue
initialized_nodes.add(node.node_id)
self._setup_node(node)
@ -143,10 +139,18 @@ class MatterAdapter:
def _setup_node(self, node: MatterNode) -> None:
"""Set up an node."""
LOGGER.debug("Setting up entities for node %s", node.node_id)
for endpoint in node.endpoints.values():
# Node endpoints are translated into HA devices
self._setup_endpoint(endpoint)
try:
for endpoint in node.endpoints.values():
# Node endpoints are translated into HA devices
self._setup_endpoint(endpoint)
except Exception as err: # noqa: BLE001
# We don't want to crash the whole setup when a single node fails to setup
# for whatever reason, so we catch all exceptions here.
LOGGER.exception(
"Error setting up node %s: %s",
node.node_id,
err,
)
def _create_device_registry(
self,

View File

@ -34,15 +34,7 @@ async def setup_integration_with_node_fixture(
override_attributes: dict[str, Any] | None = None,
) -> MatterNode:
"""Set up Matter integration with fixture as node."""
node_data = load_and_parse_node_fixture(node_fixture)
if override_attributes:
node_data["attributes"].update(override_attributes)
node = MatterNode(
dataclass_from_dict(
MatterNodeData,
node_data,
)
)
node = create_node_from_fixture(node_fixture, override_attributes)
client.get_nodes.return_value = [node]
client.get_node.return_value = node
config_entry = MockConfigEntry(
@ -56,6 +48,21 @@ async def setup_integration_with_node_fixture(
return node
def create_node_from_fixture(
node_fixture: str, override_attributes: dict[str, Any] | None = None
) -> MatterNode:
"""Create a node from a fixture."""
node_data = load_and_parse_node_fixture(node_fixture)
if override_attributes:
node_data["attributes"].update(override_attributes)
return MatterNode(
dataclass_from_dict(
MatterNodeData,
node_data,
)
)
def set_node_attribute(
node: MatterNode,
endpoint: int,

View File

@ -4,9 +4,7 @@ from __future__ import annotations
from unittest.mock import MagicMock
from matter_server.client.models.node import MatterNode
from matter_server.common.helpers.util import dataclass_from_dict
from matter_server.common.models import EventType, MatterNodeData
from matter_server.common.models import EventType
import pytest
from homeassistant.components.matter.adapter import get_clean_name
@ -14,7 +12,9 @@ from homeassistant.components.matter.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .common import load_and_parse_node_fixture, setup_integration_with_node_fixture
from .common import create_node_from_fixture, setup_integration_with_node_fixture
from tests.common import MockConfigEntry
# This tests needs to be adjusted to remove lingering tasks
@ -156,13 +156,7 @@ async def test_node_added_subscription(
)
node_added_callback = matter_client.subscribe_events.call_args.kwargs["callback"]
node_data = load_and_parse_node_fixture("onoff-light")
node = MatterNode(
dataclass_from_dict(
MatterNodeData,
node_data,
)
)
node = create_node_from_fixture("onoff-light")
entity_state = hass.states.get("light.mock_onoff_light_light")
assert not entity_state
@ -218,3 +212,27 @@ async def test_get_clean_name_() -> None:
assert get_clean_name("") is None
assert get_clean_name("Mock device") == "Mock device"
assert get_clean_name("Mock device \x00") == "Mock device"
async def test_bad_node_not_crash_integration(
hass: HomeAssistant,
matter_client: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that a bad node does not crash the integration."""
good_node = create_node_from_fixture("onoff-light")
bad_node = create_node_from_fixture("onoff-light")
del bad_node.endpoints[0].node
matter_client.get_nodes.return_value = [good_node, bad_node]
config_entry = MockConfigEntry(
domain="matter", data={"url": "http://mock-matter-server-url"}
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert matter_client.get_nodes.call_count == 1
assert hass.states.get("light.mock_onoff_light_light") is not None
assert len(hass.states.async_all("light")) == 1
assert "Error setting up node" in caplog.text

View File

@ -12,10 +12,7 @@ from matter_server.client.exceptions import (
ServerVersionTooNew,
ServerVersionTooOld,
)
from matter_server.client.models.node import MatterNode
from matter_server.common.errors import MatterError
from matter_server.common.helpers.util import dataclass_from_dict
from matter_server.common.models import MatterNodeData
import pytest
from homeassistant.components.hassio import HassioAPIError
@ -30,7 +27,7 @@ from homeassistant.helpers import (
)
from homeassistant.setup import async_setup_component
from .common import load_and_parse_node_fixture, setup_integration_with_node_fixture
from .common import create_node_from_fixture, setup_integration_with_node_fixture
from tests.common import MockConfigEntry
from tests.typing import WebSocketGenerator
@ -57,13 +54,7 @@ async def test_entry_setup_unload(
matter_client: MagicMock,
) -> None:
"""Test the integration set up and unload."""
node_data = load_and_parse_node_fixture("onoff-light")
node = MatterNode(
dataclass_from_dict(
MatterNodeData,
node_data,
)
)
node = create_node_from_fixture("onoff-light")
matter_client.get_nodes.return_value = [node]
matter_client.get_node.return_value = node
entry = MockConfigEntry(domain="matter", data={"url": "ws://localhost:5580/ws"})