280 lines
8.9 KiB
Python
280 lines
8.9 KiB
Python
|
"""The Matter integration."""
|
||
|
from __future__ import annotations
|
||
|
|
||
|
import asyncio
|
||
|
from typing import cast
|
||
|
|
||
|
import async_timeout
|
||
|
from matter_server.client import MatterClient
|
||
|
from matter_server.client.exceptions import (
|
||
|
CannotConnect,
|
||
|
FailedCommand,
|
||
|
InvalidServerVersion,
|
||
|
)
|
||
|
import voluptuous as vol
|
||
|
|
||
|
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||
|
from homeassistant.config_entries import ConfigEntry
|
||
|
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
||
|
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||
|
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 homeassistant.helpers.service import async_register_admin_service
|
||
|
|
||
|
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 .device_platform import DEVICE_PLATFORM
|
||
|
|
||
|
|
||
|
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:
|
||
|
await matter_client.connect()
|
||
|
except CannotConnect as err:
|
||
|
raise ConfigEntryNotReady("Failed to connect to matter server") from err
|
||
|
except InvalidServerVersion as err:
|
||
|
if use_addon:
|
||
|
addon_manager = _get_addon_manager(hass)
|
||
|
addon_manager.async_schedule_update_addon(catch_error=True)
|
||
|
else:
|
||
|
async_create_issue(
|
||
|
hass,
|
||
|
DOMAIN,
|
||
|
"invalid_server_version",
|
||
|
is_fixable=False,
|
||
|
severity=IssueSeverity.ERROR,
|
||
|
translation_key="invalid_server_version",
|
||
|
)
|
||
|
raise ConfigEntryNotReady(f"Invalid server version: {err}") from err
|
||
|
|
||
|
except Exception as err:
|
||
|
matter_client.logger.exception("Failed to connect to matter server")
|
||
|
raise ConfigEntryNotReady(
|
||
|
"Unknown error connecting to the Matter server"
|
||
|
) from err
|
||
|
else:
|
||
|
async_delete_issue(hass, DOMAIN, "invalid_server_version")
|
||
|
|
||
|
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)
|
||
|
)
|
||
|
|
||
|
# register websocket api
|
||
|
async_register_api(hass)
|
||
|
|
||
|
# launch the matter client listen task in the background
|
||
|
# use the init_ready event to keep track if it did initialize successfully
|
||
|
init_ready = asyncio.Event()
|
||
|
listen_task = asyncio.create_task(matter_client.start_listening(init_ready))
|
||
|
|
||
|
try:
|
||
|
async with async_timeout.timeout(30):
|
||
|
await init_ready.wait()
|
||
|
except asyncio.TimeoutError as err:
|
||
|
listen_task.cancel()
|
||
|
raise ConfigEntryNotReady("Matter client not ready") from err
|
||
|
|
||
|
if DOMAIN not in hass.data:
|
||
|
hass.data[DOMAIN] = {}
|
||
|
_async_init_services(hass)
|
||
|
|
||
|
# we create an intermediate layer (adapter) which keeps track of our nodes
|
||
|
# and discovery of platform entities from the node's attributes
|
||
|
matter = MatterAdapter(hass, matter_client, entry)
|
||
|
hass.data[DOMAIN][entry.entry_id] = matter
|
||
|
|
||
|
# forward platform setup to all platforms in the discovery schema
|
||
|
await hass.config_entries.async_forward_entry_setups(entry, DEVICE_PLATFORM)
|
||
|
|
||
|
# start discovering of node entities as task
|
||
|
asyncio.create_task(matter.setup_nodes())
|
||
|
|
||
|
return True
|
||
|
|
||
|
|
||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||
|
"""Unload a config entry."""
|
||
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, DEVICE_PLATFORM)
|
||
|
|
||
|
if unload_ok:
|
||
|
matter: MatterAdapter = hass.data[DOMAIN].pop(entry.entry_id)
|
||
|
await matter.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."""
|
||
|
unique_id = None
|
||
|
|
||
|
for ident in device_entry.identifiers:
|
||
|
if ident[0] == DOMAIN:
|
||
|
unique_id = ident[1]
|
||
|
break
|
||
|
|
||
|
if not unique_id:
|
||
|
return True
|
||
|
|
||
|
matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id]
|
||
|
|
||
|
for node in await matter.matter_client.get_nodes():
|
||
|
if node.unique_id == unique_id:
|
||
|
await matter.matter_client.remove_node(node.node_id)
|
||
|
break
|
||
|
|
||
|
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
|
||
|
def _async_init_services(hass: HomeAssistant) -> None:
|
||
|
"""Init services."""
|
||
|
|
||
|
async def _node_id_from_ha_device_id(ha_device_id: str) -> int | None:
|
||
|
"""Get node id from ha device id."""
|
||
|
dev_reg = dr.async_get(hass)
|
||
|
device = dev_reg.async_get(ha_device_id)
|
||
|
|
||
|
if device is None:
|
||
|
return None
|
||
|
|
||
|
matter_id = next(
|
||
|
(
|
||
|
identifier
|
||
|
for identifier in device.identifiers
|
||
|
if identifier[0] == DOMAIN
|
||
|
),
|
||
|
None,
|
||
|
)
|
||
|
|
||
|
if not matter_id:
|
||
|
return None
|
||
|
|
||
|
unique_id = matter_id[1]
|
||
|
|
||
|
matter_client = get_matter(hass).matter_client
|
||
|
|
||
|
# This could be more efficient
|
||
|
for node in await matter_client.get_nodes():
|
||
|
if node.unique_id == unique_id:
|
||
|
return cast(int, node.node_id)
|
||
|
|
||
|
return None
|
||
|
|
||
|
async def open_commissioning_window(call: ServiceCall) -> None:
|
||
|
"""Open commissioning window on specific node."""
|
||
|
node_id = await _node_id_from_ha_device_id(call.data["device_id"])
|
||
|
|
||
|
if node_id is None:
|
||
|
raise HomeAssistantError("This is not a Matter device")
|
||
|
|
||
|
matter_client = get_matter(hass).matter_client
|
||
|
|
||
|
# We are sending device ID .
|
||
|
|
||
|
try:
|
||
|
await matter_client.open_commissioning_window(node_id)
|
||
|
except FailedCommand as err:
|
||
|
raise HomeAssistantError(str(err)) from err
|
||
|
|
||
|
async_register_admin_service(
|
||
|
hass,
|
||
|
DOMAIN,
|
||
|
"open_commissioning_window",
|
||
|
open_commissioning_window,
|
||
|
vol.Schema({"device_id": str}),
|
||
|
)
|
||
|
|
||
|
|
||
|
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
|