Home Assistant Unofficial Reference 2024.12.1
handler.py
Go to the documentation of this file.
1 """Handler for Hass.io."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Callable, Coroutine
7 from http import HTTPStatus
8 import logging
9 import os
10 from typing import Any
11 
12 from aiohasupervisor import SupervisorClient
13 import aiohttp
14 from yarl import URL
15 
16 from homeassistant.auth.models import RefreshToken
18  CONF_SERVER_HOST,
19  CONF_SERVER_PORT,
20  CONF_SSL_CERTIFICATE,
21 )
22 from homeassistant.const import SERVER_PORT
23 from homeassistant.core import HomeAssistant
24 from homeassistant.helpers.singleton import singleton
25 from homeassistant.loader import bind_hass
26 
27 from .const import ATTR_MESSAGE, ATTR_RESULT, DOMAIN, X_HASS_SOURCE
28 
29 _LOGGER = logging.getLogger(__name__)
30 
31 KEY_SUPERVISOR_CLIENT = "supervisor_client"
32 
33 
34 class HassioAPIError(RuntimeError):
35  """Return if a API trow a error."""
36 
37 
38 def _api_bool[**_P](
39  funct: Callable[_P, Coroutine[Any, Any, dict[str, Any]]],
40 ) -> Callable[_P, Coroutine[Any, Any, bool]]:
41  """Return a boolean."""
42 
43  async def _wrapper(*argv: _P.args, **kwargs: _P.kwargs) -> bool:
44  """Wrap function."""
45  try:
46  data = await funct(*argv, **kwargs)
47  return data["result"] == "ok"
48  except HassioAPIError:
49  return False
50 
51  return _wrapper
52 
53 
54 def api_data[**_P](
55  funct: Callable[_P, Coroutine[Any, Any, dict[str, Any]]],
56 ) -> Callable[_P, Coroutine[Any, Any, Any]]:
57  """Return data of an api."""
58 
59  async def _wrapper(*argv: _P.args, **kwargs: _P.kwargs) -> Any:
60  """Wrap function."""
61  data = await funct(*argv, **kwargs)
62  if data["result"] == "ok":
63  return data["data"]
64  raise HassioAPIError(data["message"])
65 
66  return _wrapper
67 
68 
69 @bind_hass
70 async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> bool:
71  """Update Supervisor diagnostics toggle.
72 
73  The caller of the function should handle HassioAPIError.
74  """
75  hassio: HassIO = hass.data[DOMAIN]
76  return await hassio.update_diagnostics(diagnostics)
77 
78 
79 @bind_hass
80 @api_data
82  hass: HomeAssistant, payload: dict, partial: bool = False
83 ) -> dict:
84  """Create a full or partial backup.
85 
86  The caller of the function should handle HassioAPIError.
87  """
88  hassio: HassIO = hass.data[DOMAIN]
89  backup_type = "partial" if partial else "full"
90  command = f"/backups/new/{backup_type}"
91  return await hassio.send_command(command, payload=payload, timeout=None)
92 
93 
94 @api_data
95 async def async_get_green_settings(hass: HomeAssistant) -> dict[str, bool]:
96  """Return settings specific to Home Assistant Green."""
97  hassio: HassIO = hass.data[DOMAIN]
98  return await hassio.send_command("/os/boards/green", method="get")
99 
100 
101 @api_data
103  hass: HomeAssistant, settings: dict[str, bool]
104 ) -> dict:
105  """Set settings specific to Home Assistant Green.
106 
107  Returns an empty dict.
108  """
109  hassio: HassIO = hass.data[DOMAIN]
110  return await hassio.send_command(
111  "/os/boards/green", method="post", payload=settings
112  )
113 
114 
115 @api_data
116 async def async_get_yellow_settings(hass: HomeAssistant) -> dict[str, bool]:
117  """Return settings specific to Home Assistant Yellow."""
118  hassio: HassIO = hass.data[DOMAIN]
119  return await hassio.send_command("/os/boards/yellow", method="get")
120 
121 
122 @api_data
124  hass: HomeAssistant, settings: dict[str, bool]
125 ) -> dict:
126  """Set settings specific to Home Assistant Yellow.
127 
128  Returns an empty dict.
129  """
130  hassio: HassIO = hass.data[DOMAIN]
131  return await hassio.send_command(
132  "/os/boards/yellow", method="post", payload=settings
133  )
134 
135 
136 @api_data
137 async def async_reboot_host(hass: HomeAssistant) -> dict:
138  """Reboot the host.
139 
140  Returns an empty dict.
141  """
142  hassio: HassIO = hass.data[DOMAIN]
143  return await hassio.send_command("/host/reboot", method="post", timeout=60)
144 
145 
146 class HassIO:
147  """Small API wrapper for Hass.io."""
148 
149  def __init__(
150  self,
151  loop: asyncio.AbstractEventLoop,
152  websession: aiohttp.ClientSession,
153  ip: str,
154  ) -> None:
155  """Initialize Hass.io API."""
156  self.looploop = loop
157  self.websessionwebsession = websession
158  self._ip_ip = ip
159  base_url = f"http://{ip}"
160  self._base_url_base_url = URL(base_url)
161 
162  @property
163  def base_url(self) -> URL:
164  """Return base url for Supervisor."""
165  return self._base_url_base_url
166 
167  @api_data
168  def get_info(self) -> Coroutine:
169  """Return generic Supervisor information.
170 
171  This method returns a coroutine.
172  """
173  return self.send_commandsend_command("/info", method="get")
174 
175  @api_data
176  def get_host_info(self) -> Coroutine:
177  """Return data for Host.
178 
179  This method returns a coroutine.
180  """
181  return self.send_commandsend_command("/host/info", method="get")
182 
183  @api_data
184  def get_os_info(self) -> Coroutine:
185  """Return data for the OS.
186 
187  This method returns a coroutine.
188  """
189  return self.send_commandsend_command("/os/info", method="get")
190 
191  @api_data
192  def get_core_info(self) -> Coroutine:
193  """Return data for Home Asssistant Core.
194 
195  This method returns a coroutine.
196  """
197  return self.send_commandsend_command("/core/info", method="get")
198 
199  @api_data
200  def get_supervisor_info(self) -> Coroutine:
201  """Return data for the Supervisor.
202 
203  This method returns a coroutine.
204  """
205  return self.send_commandsend_command("/supervisor/info", method="get")
206 
207  @api_data
208  def get_network_info(self) -> Coroutine:
209  """Return data for the Host Network.
210 
211  This method returns a coroutine.
212  """
213  return self.send_commandsend_command("/network/info", method="get")
214 
215  @api_data
216  def get_core_stats(self) -> Coroutine:
217  """Return stats for the core.
218 
219  This method returns a coroutine.
220  """
221  return self.send_commandsend_command("/core/stats", method="get")
222 
223  @api_data
224  def get_supervisor_stats(self) -> Coroutine:
225  """Return stats for the supervisor.
226 
227  This method returns a coroutine.
228  """
229  return self.send_commandsend_command("/supervisor/stats", method="get")
230 
231  @api_data
232  def get_ingress_panels(self) -> Coroutine:
233  """Return data for Add-on ingress panels.
234 
235  This method returns a coroutine.
236  """
237  return self.send_commandsend_command("/ingress/panels", method="get")
238 
239  @_api_bool
240  async def update_hass_api(
241  self, http_config: dict[str, Any], refresh_token: RefreshToken
242  ):
243  """Update Home Assistant API data on Hass.io."""
244  port = http_config.get(CONF_SERVER_PORT) or SERVER_PORT
245  options = {
246  "ssl": CONF_SSL_CERTIFICATE in http_config,
247  "port": port,
248  "refresh_token": refresh_token.token,
249  }
250 
251  if http_config.get(CONF_SERVER_HOST) is not None:
252  options["watchdog"] = False
253  _LOGGER.warning(
254  "Found incompatible HTTP option 'server_host'. Watchdog feature"
255  " disabled"
256  )
257 
258  return await self.send_commandsend_command("/homeassistant/options", payload=options)
259 
260  @_api_bool
261  def update_hass_timezone(self, timezone: str) -> Coroutine:
262  """Update Home-Assistant timezone data on Hass.io.
263 
264  This method returns a coroutine.
265  """
266  return self.send_commandsend_command("/supervisor/options", payload={"timezone": timezone})
267 
268  @_api_bool
269  def update_diagnostics(self, diagnostics: bool) -> Coroutine:
270  """Update Supervisor diagnostics setting.
271 
272  This method returns a coroutine.
273  """
274  return self.send_commandsend_command(
275  "/supervisor/options", payload={"diagnostics": diagnostics}
276  )
277 
278  async def send_command(
279  self,
280  command: str,
281  method: str = "post",
282  payload: Any | None = None,
283  timeout: int | None = 10,
284  return_text: bool = False,
285  *,
286  source: str = "core.handler",
287  ) -> Any:
288  """Send API command to Hass.io.
289 
290  This method is a coroutine.
291  """
292  joined_url = self._base_url_base_url.with_path(command)
293  # This check is to make sure the normalized URL string
294  # is the same as the URL string that was passed in. If
295  # they are different, then the passed in command URL
296  # contained characters that were removed by the normalization
297  # such as ../../../../etc/passwd
298  if joined_url.raw_path != command:
299  _LOGGER.error("Invalid request %s", command)
300  raise HassioAPIError
301 
302  try:
303  response = await self.websessionwebsession.request(
304  method,
305  joined_url,
306  json=payload,
307  headers={
308  aiohttp.hdrs.AUTHORIZATION: (
309  f"Bearer {os.environ.get('SUPERVISOR_TOKEN', '')}"
310  ),
311  X_HASS_SOURCE: source,
312  },
313  timeout=aiohttp.ClientTimeout(total=timeout),
314  )
315 
316  if response.status != HTTPStatus.OK:
317  error = await response.json(encoding="utf-8")
318  if error.get(ATTR_RESULT) == "error":
319  raise HassioAPIError(error.get(ATTR_MESSAGE))
320 
321  _LOGGER.error(
322  "Request to %s method %s returned with code %d",
323  command,
324  method,
325  response.status,
326  )
327  raise HassioAPIError
328 
329  if return_text:
330  return await response.text(encoding="utf-8")
331 
332  return await response.json(encoding="utf-8")
333 
334  except TimeoutError:
335  _LOGGER.error("Timeout on %s request", command)
336 
337  except aiohttp.ClientError as err:
338  _LOGGER.error("Client error on %s request %s", command, err)
339 
340  raise HassioAPIError
341 
342 
343 @singleton(KEY_SUPERVISOR_CLIENT)
344 def get_supervisor_client(hass: HomeAssistant) -> SupervisorClient:
345  """Return supervisor client."""
346  hassio: HassIO = hass.data[DOMAIN]
347  return SupervisorClient(
348  str(hassio.base_url),
349  os.environ.get("SUPERVISOR_TOKEN", ""),
350  session=hassio.websession,
351  )
Coroutine update_hass_timezone(self, str timezone)
Definition: handler.py:261
Any send_command(self, str command, str method="post", Any|None payload=None, int|None timeout=10, bool return_text=False, *str source="core.handler")
Definition: handler.py:287
Coroutine update_diagnostics(self, bool diagnostics)
Definition: handler.py:269
def update_hass_api(self, dict[str, Any] http_config, RefreshToken refresh_token)
Definition: handler.py:242
None __init__(self, asyncio.AbstractEventLoop loop, aiohttp.ClientSession websession, str ip)
Definition: handler.py:154
dict async_set_yellow_settings(HomeAssistant hass, dict[str, bool] settings)
Definition: handler.py:125
dict async_reboot_host(HomeAssistant hass)
Definition: handler.py:137
dict[str, bool] async_get_yellow_settings(HomeAssistant hass)
Definition: handler.py:116
SupervisorClient get_supervisor_client(HomeAssistant hass)
Definition: handler.py:344
bool async_update_diagnostics(HomeAssistant hass, bool diagnostics)
Definition: handler.py:70
dict async_create_backup(HomeAssistant hass, dict payload, bool partial=False)
Definition: handler.py:83
dict async_set_green_settings(HomeAssistant hass, dict[str, bool] settings)
Definition: handler.py:104
dict[str, bool] async_get_green_settings(HomeAssistant hass)
Definition: handler.py:95