mirror of
https://github.com/home-assistant/core.git
synced 2025-11-06 17:40:11 +00:00
* Improve Matter Server version incompatibility handling Improve the handling of Matter Server version. Noteably fix the issues raised (add strings for the issue) and split the version check into two cases: One if the server is too old and one if the server is too new. * Bump Python Matter Server library to 6.2.0b1 * Address review feedback
289 lines
9.7 KiB
Python
289 lines
9.7 KiB
Python
"""The Matter integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from contextlib import suppress
|
|
from functools import cache
|
|
|
|
from matter_server.client import MatterClient
|
|
from matter_server.client.exceptions import (
|
|
CannotConnect,
|
|
InvalidServerVersion,
|
|
ServerVersionTooNew,
|
|
ServerVersionTooOld,
|
|
)
|
|
from matter_server.common.errors import MatterError, NodeNotExists
|
|
|
|
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
|
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
|
from homeassistant.core import Event, HomeAssistant, callback
|
|
from homeassistant.exceptions import ConfigEntryNotReady
|
|
from homeassistant.helpers import device_registry as dr
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.helpers.issue_registry import (
|
|
IssueSeverity,
|
|
async_create_issue,
|
|
async_delete_issue,
|
|
)
|
|
|
|
from .adapter import MatterAdapter
|
|
from .addon import get_addon_manager
|
|
from .api import async_register_api
|
|
from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN, LOGGER
|
|
from .discovery import SUPPORTED_PLATFORMS
|
|
from .helpers import (
|
|
MatterEntryData,
|
|
get_matter,
|
|
get_node_from_device_entry,
|
|
node_from_ha_device_id,
|
|
)
|
|
from .models import MatterDeviceInfo
|
|
|
|
CONNECT_TIMEOUT = 10
|
|
LISTEN_READY_TIMEOUT = 30
|
|
|
|
|
|
@callback
|
|
@cache
|
|
def get_matter_device_info(
|
|
hass: HomeAssistant, device_id: str
|
|
) -> MatterDeviceInfo | None:
|
|
"""Return Matter device info or None if device does not exist."""
|
|
# Test hass.data[DOMAIN] to ensure config entry is set up
|
|
if not hass.data.get(DOMAIN, False) or not (
|
|
node := node_from_ha_device_id(hass, device_id)
|
|
):
|
|
return None
|
|
|
|
return MatterDeviceInfo(
|
|
unique_id=node.device_info.uniqueID,
|
|
vendor_id=hex(node.device_info.vendorID),
|
|
product_id=hex(node.device_info.productID),
|
|
)
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Set up Matter from a config entry."""
|
|
if use_addon := entry.data.get(CONF_USE_ADDON):
|
|
await _async_ensure_addon_running(hass, entry)
|
|
|
|
matter_client = MatterClient(entry.data[CONF_URL], async_get_clientsession(hass))
|
|
try:
|
|
async with asyncio.timeout(CONNECT_TIMEOUT):
|
|
await matter_client.connect()
|
|
except (CannotConnect, TimeoutError) as err:
|
|
raise ConfigEntryNotReady("Failed to connect to matter server") from err
|
|
except InvalidServerVersion as err:
|
|
if isinstance(err, ServerVersionTooOld):
|
|
if use_addon:
|
|
addon_manager = _get_addon_manager(hass)
|
|
addon_manager.async_schedule_update_addon(catch_error=True)
|
|
else:
|
|
async_create_issue(
|
|
hass,
|
|
DOMAIN,
|
|
"server_version_version_too_old",
|
|
is_fixable=False,
|
|
severity=IssueSeverity.ERROR,
|
|
translation_key="server_version_version_too_old",
|
|
)
|
|
elif isinstance(err, ServerVersionTooNew):
|
|
async_create_issue(
|
|
hass,
|
|
DOMAIN,
|
|
"server_version_version_too_new",
|
|
is_fixable=False,
|
|
severity=IssueSeverity.ERROR,
|
|
translation_key="server_version_version_too_new",
|
|
)
|
|
raise ConfigEntryNotReady(f"Invalid server version: {err}") from err
|
|
|
|
except Exception as err:
|
|
LOGGER.exception("Failed to connect to matter server")
|
|
raise ConfigEntryNotReady(
|
|
"Unknown error connecting to the Matter server"
|
|
) from err
|
|
|
|
async_delete_issue(hass, DOMAIN, "server_version_version_too_old")
|
|
async_delete_issue(hass, DOMAIN, "server_version_version_too_new")
|
|
|
|
async def on_hass_stop(event: Event) -> None:
|
|
"""Handle incoming stop event from Home Assistant."""
|
|
await matter_client.disconnect()
|
|
|
|
entry.async_on_unload(
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
|
)
|
|
|
|
async_register_api(hass)
|
|
|
|
# launch the matter client listen task in the background
|
|
# use the init_ready event to wait until initialization is done
|
|
init_ready = asyncio.Event()
|
|
listen_task = asyncio.create_task(
|
|
_client_listen(hass, entry, matter_client, init_ready)
|
|
)
|
|
|
|
try:
|
|
async with asyncio.timeout(LISTEN_READY_TIMEOUT):
|
|
await init_ready.wait()
|
|
except TimeoutError as err:
|
|
listen_task.cancel()
|
|
raise ConfigEntryNotReady("Matter client not ready") from err
|
|
|
|
if DOMAIN not in hass.data:
|
|
hass.data[DOMAIN] = {}
|
|
|
|
# create an intermediate layer (adapter) which keeps track of the nodes
|
|
# and discovery of platform entities from the node attributes
|
|
matter = MatterAdapter(hass, matter_client, entry)
|
|
hass.data[DOMAIN][entry.entry_id] = MatterEntryData(matter, listen_task)
|
|
|
|
await hass.config_entries.async_forward_entry_setups(entry, SUPPORTED_PLATFORMS)
|
|
await matter.setup_nodes()
|
|
|
|
# If the listen task is already failed, we need to raise ConfigEntryNotReady
|
|
if listen_task.done() and (listen_error := listen_task.exception()) is not None:
|
|
await hass.config_entries.async_unload_platforms(entry, SUPPORTED_PLATFORMS)
|
|
hass.data[DOMAIN].pop(entry.entry_id)
|
|
try:
|
|
await matter_client.disconnect()
|
|
finally:
|
|
raise ConfigEntryNotReady(listen_error) from listen_error
|
|
|
|
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: # noqa: BLE001
|
|
# 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:
|
|
"""Unload a config entry."""
|
|
unload_ok = await hass.config_entries.async_unload_platforms(
|
|
entry, SUPPORTED_PLATFORMS
|
|
)
|
|
|
|
if unload_ok:
|
|
matter_entry_data: MatterEntryData = hass.data[DOMAIN].pop(entry.entry_id)
|
|
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:
|
|
addon_manager: AddonManager = get_addon_manager(hass)
|
|
LOGGER.debug("Stopping Matter Server add-on")
|
|
try:
|
|
await addon_manager.async_stop_addon()
|
|
except AddonError as err:
|
|
LOGGER.error("Failed to stop the Matter Server add-on: %s", err)
|
|
return False
|
|
|
|
return unload_ok
|
|
|
|
|
|
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
"""Config entry is being removed."""
|
|
|
|
if not entry.data.get(CONF_INTEGRATION_CREATED_ADDON):
|
|
return
|
|
|
|
addon_manager: AddonManager = get_addon_manager(hass)
|
|
try:
|
|
await addon_manager.async_stop_addon()
|
|
except AddonError as err:
|
|
LOGGER.error(err)
|
|
return
|
|
try:
|
|
await addon_manager.async_create_backup()
|
|
except AddonError as err:
|
|
LOGGER.error(err)
|
|
return
|
|
try:
|
|
await addon_manager.async_uninstall_addon()
|
|
except AddonError as err:
|
|
LOGGER.error(err)
|
|
|
|
|
|
async def async_remove_config_entry_device(
|
|
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
|
|
) -> bool:
|
|
"""Remove a config entry from a device."""
|
|
node = get_node_from_device_entry(hass, device_entry)
|
|
|
|
if node is None:
|
|
return True
|
|
|
|
if node.is_bridge_device:
|
|
device_registry = dr.async_get(hass)
|
|
devices = dr.async_entries_for_config_entry(
|
|
device_registry, config_entry.entry_id
|
|
)
|
|
for device in devices:
|
|
if device.via_device_id == device_entry.id:
|
|
device_registry.async_update_device(
|
|
device.id, remove_config_entry_id=config_entry.entry_id
|
|
)
|
|
|
|
matter = get_matter(hass)
|
|
with suppress(NodeNotExists):
|
|
# ignore if the server has already removed the node.
|
|
await matter.matter_client.remove_node(node.node_id)
|
|
|
|
return True
|
|
|
|
|
|
async def _async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
"""Ensure that Matter Server add-on is installed and running."""
|
|
addon_manager = _get_addon_manager(hass)
|
|
try:
|
|
addon_info = await addon_manager.async_get_addon_info()
|
|
except AddonError as err:
|
|
raise ConfigEntryNotReady(err) from err
|
|
|
|
addon_state = addon_info.state
|
|
|
|
if addon_state == AddonState.NOT_INSTALLED:
|
|
addon_manager.async_schedule_install_setup_addon(
|
|
addon_info.options,
|
|
catch_error=True,
|
|
)
|
|
raise ConfigEntryNotReady
|
|
|
|
if addon_state == AddonState.NOT_RUNNING:
|
|
addon_manager.async_schedule_start_addon(catch_error=True)
|
|
raise ConfigEntryNotReady
|
|
|
|
|
|
@callback
|
|
def _get_addon_manager(hass: HomeAssistant) -> AddonManager:
|
|
"""Ensure that Matter Server add-on is updated and running.
|
|
|
|
May only be used as part of async_setup_entry above.
|
|
"""
|
|
addon_manager: AddonManager = get_addon_manager(hass)
|
|
if addon_manager.task_in_progress():
|
|
raise ConfigEntryNotReady
|
|
return addon_manager
|