Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for System health ."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Awaitable, Callable
7 import dataclasses
8 from datetime import datetime
9 import logging
10 from typing import Any, Protocol
11 
12 import aiohttp
13 import voluptuous as vol
14 
15 from homeassistant.components import websocket_api
16 from homeassistant.core import HomeAssistant, callback
17 from homeassistant.helpers import (
18  aiohttp_client,
19  config_validation as cv,
20  integration_platform,
21 )
22 from homeassistant.helpers.typing import ConfigType
23 from homeassistant.loader import bind_hass
24 
25 _LOGGER = logging.getLogger(__name__)
26 
27 DOMAIN = "system_health"
28 
29 INFO_CALLBACK_TIMEOUT = 5
30 
31 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
32 
33 
34 class SystemHealthProtocol(Protocol):
35  """Define the format of system_health platforms."""
36 
38  self, hass: HomeAssistant, register: SystemHealthRegistration
39  ) -> None:
40  """Register system health callbacks."""
41 
42 
43 @bind_hass
44 @callback
46  hass: HomeAssistant,
47  domain: str,
48  info_callback: Callable[[HomeAssistant], Awaitable[dict]],
49 ) -> None:
50  """Register an info callback.
51 
52  Deprecated.
53  """
54  _LOGGER.warning(
55  "Calling system_health.async_register_info is deprecated; Add a system_health"
56  " platform instead"
57  )
58  hass.data.setdefault(DOMAIN, {})
59  SystemHealthRegistration(hass, domain).async_register_info(info_callback)
60 
61 
62 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
63  """Set up the System Health component."""
64  websocket_api.async_register_command(hass, handle_info)
65  hass.data.setdefault(DOMAIN, {})
66 
67  await integration_platform.async_process_integration_platforms(
68  hass, DOMAIN, _register_system_health_platform
69  )
70 
71  return True
72 
73 
74 @callback
76  hass: HomeAssistant, integration_domain: str, platform: SystemHealthProtocol
77 ) -> None:
78  """Register a system health platform."""
79  platform.async_register(hass, SystemHealthRegistration(hass, integration_domain))
80 
81 
83  hass: HomeAssistant, registration: SystemHealthRegistration
84 ) -> dict[str, Any]:
85  """Get integration system health."""
86  try:
87  assert registration.info_callback
88  async with asyncio.timeout(INFO_CALLBACK_TIMEOUT):
89  data = await registration.info_callback(hass)
90  except TimeoutError:
91  data = {"error": {"type": "failed", "error": "timeout"}}
92  except Exception:
93  _LOGGER.exception("Error fetching info")
94  data = {"error": {"type": "failed", "error": "unknown"}}
95 
96  result: dict[str, Any] = {"info": data}
97 
98  if registration.manage_url:
99  result["manage_url"] = registration.manage_url
100 
101  return result
102 
103 
104 @callback
105 def _format_value(val: Any) -> Any:
106  """Format a system health value."""
107  if isinstance(val, datetime):
108  return {"value": val.isoformat(), "type": "date"}
109  return val
110 
111 
112 @websocket_api.websocket_command({vol.Required("type"): "system_health/info"})
113 @websocket_api.async_response
114 async def handle_info(
115  hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
116 ) -> None:
117  """Handle an info request via a subscription."""
118  registrations: dict[str, SystemHealthRegistration] = hass.data[DOMAIN]
119  data = {}
120  pending_info: dict[tuple[str, str], asyncio.Task] = {}
121 
122  for domain, domain_data in zip(
123  registrations,
124  await asyncio.gather(
125  *(
126  get_integration_info(hass, registration)
127  for registration in registrations.values()
128  )
129  ),
130  strict=False,
131  ):
132  for key, value in domain_data["info"].items():
133  if asyncio.iscoroutine(value):
134  value = asyncio.create_task(value)
135  if isinstance(value, asyncio.Task):
136  pending_info[(domain, key)] = value
137  domain_data["info"][key] = {"type": "pending"}
138  else:
139  domain_data["info"][key] = _format_value(value)
140 
141  data[domain] = domain_data
142 
143  # Confirm subscription
144  connection.send_result(msg["id"])
145 
146  stop_event = asyncio.Event()
147  connection.subscriptions[msg["id"]] = stop_event.set
148 
149  # Send initial data
150  connection.send_message(
151  websocket_api.messages.event_message(
152  msg["id"], {"type": "initial", "data": data}
153  )
154  )
155 
156  # If nothing pending, wrap it up.
157  if not pending_info:
158  connection.send_message(
159  websocket_api.messages.event_message(msg["id"], {"type": "finish"})
160  )
161  return
162 
163  tasks: set[asyncio.Task] = {
164  asyncio.create_task(stop_event.wait()),
165  *pending_info.values(),
166  }
167  pending_lookup = {val: key for key, val in pending_info.items()}
168 
169  # One task is the stop_event.wait() and is always there
170  while len(tasks) > 1 and not stop_event.is_set():
171  # Wait for first completed task
172  done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
173 
174  if stop_event.is_set():
175  for task in tasks:
176  task.cancel()
177  return
178 
179  # Update subscription of all finished tasks
180  for result in done:
181  domain, key = pending_lookup[result]
182  event_msg = {
183  "type": "update",
184  "domain": domain,
185  "key": key,
186  }
187 
188  if exception := result.exception():
189  _LOGGER.error(
190  "Error fetching system info for %s - %s",
191  domain,
192  key,
193  exc_info=(type(exception), exception, exception.__traceback__),
194  )
195  event_msg["success"] = False
196  event_msg["error"] = {"type": "failed", "error": "unknown"}
197  else:
198  event_msg["success"] = True
199  event_msg["data"] = _format_value(result.result())
200 
201  connection.send_message(
202  websocket_api.messages.event_message(msg["id"], event_msg)
203  )
204 
205  connection.send_message(
206  websocket_api.messages.event_message(msg["id"], {"type": "finish"})
207  )
208 
209 
210 @dataclasses.dataclass(slots=True)
212  """Helper class to track platform registration."""
213 
214  hass: HomeAssistant
215  domain: str
216  info_callback: Callable[[HomeAssistant], Awaitable[dict]] | None = None
217  manage_url: str | None = None
218 
219  @callback
221  self,
222  info_callback: Callable[[HomeAssistant], Awaitable[dict]],
223  manage_url: str | None = None,
224  ) -> None:
225  """Register an info callback."""
226  self.info_callbackinfo_callback = info_callback
227  self.manage_urlmanage_url = manage_url
228  self.hass.data[DOMAIN][self.domain] = self
229 
230 
232  hass: HomeAssistant, url: str, more_info: str | None = None
233 ) -> str | dict[str, str]:
234  """Test if the url can be reached."""
235  session = aiohttp_client.async_get_clientsession(hass)
236 
237  try:
238  await session.get(url, timeout=aiohttp.ClientTimeout(total=5))
239  except aiohttp.ClientError:
240  data = {"type": "failed", "error": "unreachable"}
241  except TimeoutError:
242  data = {"type": "failed", "error": "timeout"}
243  else:
244  return "ok"
245  if more_info is not None:
246  data["more_info"] = more_info
247  return data
None async_register(self, HomeAssistant hass, SystemHealthRegistration register)
Definition: __init__.py:39
None async_register_info(self, Callable[[HomeAssistant], Awaitable[dict]] info_callback, str|None manage_url=None)
Definition: __init__.py:224
None handle_info(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:116
dict[str, Any] get_integration_info(HomeAssistant hass, SystemHealthRegistration registration)
Definition: __init__.py:84
str|dict[str, str] async_check_can_reach_url(HomeAssistant hass, str url, str|None more_info=None)
Definition: __init__.py:233
None async_register_info(HomeAssistant hass, str domain, Callable[[HomeAssistant], Awaitable[dict]] info_callback)
Definition: __init__.py:49
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:62
None _register_system_health_platform(HomeAssistant hass, str integration_domain, SystemHealthProtocol platform)
Definition: __init__.py:77