Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Matter integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from typing import Any
7 
8 from matter_server.client import MatterClient
9 from matter_server.client.exceptions import CannotConnect, InvalidServerVersion
10 import voluptuous as vol
11 
13  AddonError,
14  AddonInfo,
15  AddonManager,
16  AddonState,
17 )
18 from homeassistant.components.onboarding import async_is_onboarded
19 from homeassistant.components.zeroconf import ZeroconfServiceInfo
20 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
21 from homeassistant.const import CONF_URL
22 from homeassistant.core import HomeAssistant
23 from homeassistant.data_entry_flow import AbortFlow
24 from homeassistant.exceptions import HomeAssistantError
25 from homeassistant.helpers import aiohttp_client
26 from homeassistant.helpers.hassio import is_hassio
27 from homeassistant.helpers.service_info.hassio import HassioServiceInfo
28 
29 from .addon import get_addon_manager
30 from .const import (
31  ADDON_SLUG,
32  CONF_INTEGRATION_CREATED_ADDON,
33  CONF_USE_ADDON,
34  DOMAIN,
35  LOGGER,
36 )
37 
38 ADDON_SETUP_TIMEOUT = 5
39 ADDON_SETUP_TIMEOUT_ROUNDS = 40
40 DEFAULT_URL = "ws://localhost:5580/ws"
41 DEFAULT_TITLE = "Matter"
42 ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool})
43 
44 
45 def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema:
46  """Return a schema for the manual step."""
47  default_url = user_input.get(CONF_URL, DEFAULT_URL)
48  return vol.Schema({vol.Required(CONF_URL, default=default_url): str})
49 
50 
51 async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
52  """Validate the user input allows us to connect."""
53  client = MatterClient(data[CONF_URL], aiohttp_client.async_get_clientsession(hass))
54  await client.connect()
55 
56 
57 def build_ws_address(host: str, port: int) -> str:
58  """Return the websocket address."""
59  return f"ws://{host}:{port}/ws"
60 
61 
62 class MatterConfigFlow(ConfigFlow, domain=DOMAIN):
63  """Handle a config flow for Matter."""
64 
65  VERSION = 1
66 
67  def __init__(self) -> None:
68  """Set up flow instance."""
69  self._running_in_background_running_in_background = False
70  self.ws_addressws_address: str | None = None
71  # If we install the add-on we should uninstall it on entry remove.
72  self.integration_created_addonintegration_created_addon = False
73  self.install_taskinstall_task: asyncio.Task | None = None
74  self.start_taskstart_task: asyncio.Task | None = None
75  self.use_addonuse_addon = False
76 
78  self, user_input: dict[str, Any] | None = None
79  ) -> ConfigFlowResult:
80  """Install Matter Server add-on."""
81  if not self.install_taskinstall_task:
82  self.install_taskinstall_task = self.hass.async_create_task(self._async_install_addon_async_install_addon())
83 
84  if not self._running_in_background_running_in_background and not self.install_taskinstall_task.done():
85  return self.async_show_progressasync_show_progress(
86  step_id="install_addon",
87  progress_action="install_addon",
88  progress_task=self.install_taskinstall_task,
89  )
90 
91  try:
92  await self.install_taskinstall_task
93  except AddonError as err:
94  LOGGER.error(err)
95  if self._running_in_background_running_in_background:
96  return await self.async_step_install_failedasync_step_install_failed()
97  return self.async_show_progress_doneasync_show_progress_done(next_step_id="install_failed")
98  finally:
99  self.install_taskinstall_task = None
100 
101  self.integration_created_addonintegration_created_addon = True
102 
103  if self._running_in_background_running_in_background:
104  return await self.async_step_start_addonasync_step_start_addon()
105  return self.async_show_progress_doneasync_show_progress_done(next_step_id="start_addon")
106 
108  self, user_input: dict[str, Any] | None = None
109  ) -> ConfigFlowResult:
110  """Add-on installation failed."""
111  return self.async_abortasync_abortasync_abort(reason="addon_install_failed")
112 
113  async def _async_install_addon(self) -> None:
114  """Install the Matter Server add-on."""
115  addon_manager: AddonManager = get_addon_manager(self.hass)
116  await addon_manager.async_schedule_install_addon()
117 
118  async def _async_get_addon_discovery_info(self) -> dict:
119  """Return add-on discovery info."""
120  addon_manager: AddonManager = get_addon_manager(self.hass)
121  try:
122  discovery_info_config = await addon_manager.async_get_addon_discovery_info()
123  except AddonError as err:
124  LOGGER.error(err)
125  raise AbortFlow("addon_get_discovery_info_failed") from err
126 
127  return discovery_info_config
128 
130  self, user_input: dict[str, Any] | None = None
131  ) -> ConfigFlowResult:
132  """Start Matter Server add-on."""
133  if not self.start_taskstart_task:
134  self.start_taskstart_task = self.hass.async_create_task(self._async_start_addon_async_start_addon())
135  if not self._running_in_background_running_in_background and not self.start_taskstart_task.done():
136  return self.async_show_progressasync_show_progress(
137  step_id="start_addon",
138  progress_action="start_addon",
139  progress_task=self.start_taskstart_task,
140  )
141 
142  try:
143  await self.start_taskstart_task
144  except (FailedConnect, AddonError, AbortFlow) as err:
145  LOGGER.error(err)
146  if self._running_in_background_running_in_background:
147  return await self.async_step_start_failedasync_step_start_failed()
148  return self.async_show_progress_doneasync_show_progress_done(next_step_id="start_failed")
149  finally:
150  self.start_taskstart_task = None
151 
152  if self._running_in_background_running_in_background:
153  return await self.async_step_finish_addon_setupasync_step_finish_addon_setup()
154  return self.async_show_progress_doneasync_show_progress_done(next_step_id="finish_addon_setup")
155 
157  self, user_input: dict[str, Any] | None = None
158  ) -> ConfigFlowResult:
159  """Add-on start failed."""
160  return self.async_abortasync_abortasync_abort(reason="addon_start_failed")
161 
162  async def _async_start_addon(self) -> None:
163  """Start the Matter Server add-on."""
164  addon_manager: AddonManager = get_addon_manager(self.hass)
165 
166  await addon_manager.async_schedule_start_addon()
167  # Sleep some seconds to let the add-on start properly before connecting.
168  for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS):
169  await asyncio.sleep(ADDON_SETUP_TIMEOUT)
170  try:
171  if not (ws_address := self.ws_addressws_address):
172  discovery_info = await self._async_get_addon_discovery_info_async_get_addon_discovery_info()
173  ws_address = self.ws_addressws_address = build_ws_address(
174  discovery_info["host"], discovery_info["port"]
175  )
176  await validate_input(self.hass, {CONF_URL: ws_address})
177  except (AbortFlow, CannotConnect) as err:
178  LOGGER.debug(
179  "Add-on not ready yet, waiting %s seconds: %s",
180  ADDON_SETUP_TIMEOUT,
181  err,
182  )
183  else:
184  break
185  else:
186  raise FailedConnect("Failed to start Matter Server add-on: timeout")
187 
188  async def _async_get_addon_info(self) -> AddonInfo:
189  """Return Matter Server add-on info."""
190  addon_manager: AddonManager = get_addon_manager(self.hass)
191  try:
192  addon_info: AddonInfo = await addon_manager.async_get_addon_info()
193  except AddonError as err:
194  LOGGER.error(err)
195  raise AbortFlow("addon_info_failed") from err
196 
197  return addon_info
198 
199  async def async_step_user(
200  self, user_input: dict[str, Any] | None = None
201  ) -> ConfigFlowResult:
202  """Handle the initial step."""
203  if is_hassio(self.hass):
204  return await self.async_step_on_supervisorasync_step_on_supervisor()
205 
206  return await self.async_step_manualasync_step_manual()
207 
208  async def async_step_manual(
209  self, user_input: dict[str, Any] | None = None
210  ) -> ConfigFlowResult:
211  """Handle a manual configuration."""
212  if user_input is None:
213  return self.async_show_formasync_show_formasync_show_form(
214  step_id="manual", data_schema=get_manual_schema({})
215  )
216 
217  errors = {}
218 
219  try:
220  await validate_input(self.hass, user_input)
221  except CannotConnect:
222  errors["base"] = "cannot_connect"
223  except InvalidServerVersion:
224  errors["base"] = "invalid_server_version"
225  except Exception: # noqa: BLE001
226  LOGGER.exception("Unexpected exception")
227  errors["base"] = "unknown"
228  else:
229  self.ws_addressws_address = user_input[CONF_URL]
230 
231  return await self._async_create_entry_or_abort_async_create_entry_or_abort()
232 
233  return self.async_show_formasync_show_formasync_show_form(
234  step_id="manual", data_schema=get_manual_schema(user_input), errors=errors
235  )
236 
238  self, discovery_info: ZeroconfServiceInfo
239  ) -> ConfigFlowResult:
240  """Handle zeroconf discovery."""
241  if not async_is_onboarded(self.hass) and is_hassio(self.hass):
242  await self._async_handle_discovery_without_unique_id_async_handle_discovery_without_unique_id()
243  self._running_in_background_running_in_background = True
244  return await self.async_step_on_supervisorasync_step_on_supervisor(
245  user_input={CONF_USE_ADDON: True}
246  )
247  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
248 
249  async def async_step_hassio(
250  self, discovery_info: HassioServiceInfo
251  ) -> ConfigFlowResult:
252  """Receive configuration from add-on discovery info.
253 
254  This flow is triggered by the Matter Server add-on.
255  """
256  if discovery_info.slug != ADDON_SLUG:
257  return self.async_abortasync_abortasync_abort(reason="not_matter_addon")
258 
259  await self._async_handle_discovery_without_unique_id_async_handle_discovery_without_unique_id()
260 
261  self.ws_addressws_address = build_ws_address(
262  discovery_info.config["host"], discovery_info.config["port"]
263  )
264 
265  return await self.async_step_hassio_confirmasync_step_hassio_confirm()
266 
268  self, user_input: dict[str, Any] | None = None
269  ) -> ConfigFlowResult:
270  """Confirm the add-on discovery."""
271  if user_input is not None:
272  return await self.async_step_on_supervisorasync_step_on_supervisor(
273  user_input={CONF_USE_ADDON: True}
274  )
275 
276  return self.async_show_formasync_show_formasync_show_form(step_id="hassio_confirm")
277 
279  self, user_input: dict[str, Any] | None = None
280  ) -> ConfigFlowResult:
281  """Handle logic when on Supervisor host."""
282  if user_input is None:
283  return self.async_show_formasync_show_formasync_show_form(
284  step_id="on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA
285  )
286  if not user_input[CONF_USE_ADDON]:
287  return await self.async_step_manualasync_step_manual()
288 
289  self.use_addonuse_addon = True
290 
291  addon_info = await self._async_get_addon_info_async_get_addon_info()
292 
293  if addon_info.state == AddonState.RUNNING:
294  return await self.async_step_finish_addon_setupasync_step_finish_addon_setup()
295 
296  if addon_info.state == AddonState.NOT_RUNNING:
297  return await self.async_step_start_addonasync_step_start_addon()
298 
299  return await self.async_step_install_addonasync_step_install_addon()
300 
302  self, user_input: dict[str, Any] | None = None
303  ) -> ConfigFlowResult:
304  """Prepare info needed to complete the config entry."""
305  if not self.ws_addressws_address:
306  discovery_info = await self._async_get_addon_discovery_info_async_get_addon_discovery_info()
307  ws_address = self.ws_addressws_address = build_ws_address(
308  discovery_info["host"], discovery_info["port"]
309  )
310  # Check that we can connect to the address.
311  try:
312  await validate_input(self.hass, {CONF_URL: ws_address})
313  except CannotConnect:
314  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
315 
316  return await self._async_create_entry_or_abort_async_create_entry_or_abort()
317 
318  async def _async_create_entry_or_abort(self) -> ConfigFlowResult:
319  """Return a config entry for the flow or abort if already configured."""
320  assert self.ws_addressws_address is not None
321 
322  if existing_config_entries := self._async_current_entries_async_current_entries():
323  config_entry = existing_config_entries[0]
324  self.hass.config_entries.async_update_entry(
325  config_entry,
326  data={
327  **config_entry.data,
328  CONF_URL: self.ws_addressws_address,
329  CONF_USE_ADDON: self.use_addonuse_addon,
330  CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addonintegration_created_addon,
331  },
332  title=DEFAULT_TITLE,
333  )
334  await self.hass.config_entries.async_reload(config_entry.entry_id)
335  raise AbortFlow("reconfiguration_successful")
336 
337  # Abort any other flows that may be in progress
338  for progress in self._async_in_progress_async_in_progress():
339  self.hass.config_entries.flow.async_abort(progress["flow_id"])
340 
341  return self.async_create_entryasync_create_entryasync_create_entry(
342  title=DEFAULT_TITLE,
343  data={
344  CONF_URL: self.ws_addressws_address,
345  CONF_USE_ADDON: self.use_addonuse_addon,
346  CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addonintegration_created_addon,
347  },
348  )
349 
350 
352  """Failed to connect to the Matter Server."""
ConfigFlowResult async_step_hassio(self, HassioServiceInfo discovery_info)
Definition: config_flow.py:251
ConfigFlowResult async_step_zeroconf(self, ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:239
ConfigFlowResult async_step_install_failed(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:109
ConfigFlowResult async_step_install_addon(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:79
ConfigFlowResult async_step_finish_addon_setup(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:303
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:201
ConfigFlowResult async_step_start_failed(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:158
ConfigFlowResult async_step_manual(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:210
ConfigFlowResult async_step_on_supervisor(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:280
ConfigFlowResult async_step_hassio_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:269
ConfigFlowResult async_step_start_addon(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:131
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
list[ConfigFlowResult] _async_in_progress(self, bool include_uninitialized=False, dict[str, Any]|None match_context=None)
ConfigFlowResult _async_step_discovery_without_unique_id(self)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_show_progress(self, *str|None step_id=None, str progress_action, Mapping[str, str]|None description_placeholders=None, asyncio.Task[Any]|None progress_task=None)
_FlowResultT async_show_progress_done(self, *str next_step_id)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
bool is_hassio(HomeAssistant hass)
Definition: __init__.py:302
str build_ws_address(str host, int port)
Definition: config_flow.py:57
None validate_input(HomeAssistant hass, dict[str, Any] data)
Definition: config_flow.py:51
vol.Schema get_manual_schema(dict[str, Any] user_input)
Definition: config_flow.py:45
bool async_is_onboarded(HomeAssistant hass)
Definition: __init__.py:68
AddonManager get_addon_manager(HomeAssistant hass, str slug)
Definition: config_flow.py:44