Home Assistant Unofficial Reference 2024.12.1
discovery.py
Go to the documentation of this file.
1 """Implement the services discovery feature from Hass.io for Add-ons."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import logging
7 from typing import Any
8 from uuid import UUID
9 
10 from aiohasupervisor import SupervisorError
11 from aiohasupervisor.models import Discovery
12 from aiohttp import web
13 from aiohttp.web_exceptions import HTTPServiceUnavailable
14 
15 from homeassistant import config_entries
16 from homeassistant.components.http import HomeAssistantView
17 from homeassistant.const import ATTR_SERVICE, EVENT_HOMEASSISTANT_START
18 from homeassistant.core import Event, HomeAssistant, callback
19 from homeassistant.helpers import discovery_flow
20 from homeassistant.helpers.dispatcher import async_dispatcher_connect
21 from homeassistant.helpers.service_info.hassio import HassioServiceInfo
22 
23 from .const import ATTR_ADDON, ATTR_UUID, DOMAIN
24 from .handler import HassIO, get_supervisor_client
25 
26 _LOGGER = logging.getLogger(__name__)
27 
28 
29 @callback
30 def async_setup_discovery_view(hass: HomeAssistant, hassio: HassIO) -> None:
31  """Discovery setup."""
32  hassio_discovery = HassIODiscovery(hass, hassio)
33  supervisor_client = get_supervisor_client(hass)
34  hass.http.register_view(hassio_discovery)
35 
36  # Handle exists discovery messages
37  async def _async_discovery_start_handler(event: Event) -> None:
38  """Process all exists discovery on startup."""
39  try:
40  data = await supervisor_client.discovery.list()
41  except SupervisorError as err:
42  _LOGGER.error("Can't read discover info: %s", err)
43  return
44 
45  jobs = [
46  asyncio.create_task(hassio_discovery.async_process_new(discovery))
47  for discovery in data
48  ]
49  if jobs:
50  await asyncio.wait(jobs)
51 
52  hass.bus.async_listen_once(
53  EVENT_HOMEASSISTANT_START, _async_discovery_start_handler
54  )
55 
56  async def _handle_config_entry_removed(
57  entry: config_entries.ConfigEntry,
58  ) -> None:
59  """Handle config entry changes."""
60  for disc_key in entry.discovery_keys[DOMAIN]:
61  if disc_key.version != 1 or not isinstance(key := disc_key.key, str):
62  continue
63  uuid = key
64  _LOGGER.debug("Rediscover addon %s", uuid)
65  await hassio_discovery.async_rediscover(uuid)
66 
68  hass,
69  config_entries.signal_discovered_config_entry_removed(DOMAIN),
70  _handle_config_entry_removed,
71  )
72 
73 
74 class HassIODiscovery(HomeAssistantView):
75  """Hass.io view to handle base part."""
76 
77  name = "api:hassio_push:discovery"
78  url = "/api/hassio_push/discovery/{uuid}"
79 
80  def __init__(self, hass: HomeAssistant, hassio: HassIO) -> None:
81  """Initialize WebView."""
82  self.hasshass = hass
83  self.hassiohassio = hassio
84  self._supervisor_client_supervisor_client = get_supervisor_client(hass)
85 
86  async def post(self, request: web.Request, uuid: str) -> web.Response:
87  """Handle new discovery requests."""
88  # Fetch discovery data and prevent injections
89  try:
90  data = await self._supervisor_client_supervisor_client.discovery.get(UUID(uuid))
91  except SupervisorError as err:
92  _LOGGER.error("Can't read discovery data: %s", err)
93  raise HTTPServiceUnavailable from None
94 
95  await self.async_process_newasync_process_new(data)
96  return web.Response()
97 
98  async def delete(self, request: web.Request, uuid: str) -> web.Response:
99  """Handle remove discovery requests."""
100  data: dict[str, Any] = await request.json()
101 
102  await self.async_process_delasync_process_del(data)
103  return web.Response()
104 
105  async def async_rediscover(self, uuid: str) -> None:
106  """Rediscover add-on when config entry is removed."""
107  try:
108  data = await self._supervisor_client_supervisor_client.discovery.get(UUID(uuid))
109  except SupervisorError as err:
110  _LOGGER.debug("Can't read discovery data: %s", err)
111  else:
112  await self.async_process_newasync_process_new(data)
113 
114  async def async_process_new(self, data: Discovery) -> None:
115  """Process add discovery entry."""
116  # Read additional Add-on info
117  try:
118  addon_info = await self._supervisor_client_supervisor_client.addons.addon_info(data.addon)
119  except SupervisorError as err:
120  _LOGGER.error("Can't read add-on info: %s", err)
121  return
122 
123  data.config[ATTR_ADDON] = addon_info.name
124 
125  # Use config flow
126  discovery_flow.async_create_flow(
127  self.hasshass,
128  data.service,
129  context={"source": config_entries.SOURCE_HASSIO},
130  data=HassioServiceInfo(
131  config=data.config,
132  name=addon_info.name,
133  slug=data.addon,
134  uuid=data.uuid.hex,
135  ),
136  discovery_key=discovery_flow.DiscoveryKey(
137  domain=DOMAIN,
138  key=data.uuid.hex,
139  version=1,
140  ),
141  )
142 
143  async def async_process_del(self, data: dict[str, Any]) -> None:
144  """Process remove discovery entry."""
145  service: str = data[ATTR_SERVICE]
146  uuid: str = data[ATTR_UUID]
147 
148  # Check if really deletet / prevent injections
149  try:
150  await self._supervisor_client_supervisor_client.discovery.get(UUID(uuid))
151  except SupervisorError:
152  pass
153  else:
154  _LOGGER.warning("Retrieve wrong unload for %s", service)
155  return
156 
157  # Use config flow
158  for entry in self.hasshass.config_entries.async_entries(service):
159  if entry.source != config_entries.SOURCE_HASSIO or entry.unique_id != uuid:
160  continue
161  await self.hasshass.config_entries.async_remove(entry.entry_id)
None async_process_del(self, dict[str, Any] data)
Definition: discovery.py:143
web.Response delete(self, web.Request request, str uuid)
Definition: discovery.py:98
None __init__(self, HomeAssistant hass, HassIO hassio)
Definition: discovery.py:80
web.Response post(self, web.Request request, str uuid)
Definition: discovery.py:86
None async_setup_discovery_view(HomeAssistant hass, HassIO hassio)
Definition: discovery.py:30
SupervisorClient get_supervisor_client(HomeAssistant hass)
Definition: handler.py:344
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103