Home Assistant Unofficial Reference 2024.12.1
websocket_api.py
Go to the documentation of this file.
1 """The Hardware websocket API."""
2 
3 from __future__ import annotations
4 
5 import contextlib
6 from dataclasses import asdict, dataclass
7 from datetime import datetime, timedelta
8 from typing import Any
9 
10 import psutil_home_assistant as ha_psutil
11 import voluptuous as vol
12 
13 from homeassistant.components import websocket_api
14 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
15 from homeassistant.exceptions import HomeAssistantError
16 from homeassistant.helpers.event import async_track_time_interval
17 import homeassistant.util.dt as dt_util
18 
19 from .const import DOMAIN
20 from .hardware import async_process_hardware_platforms
21 from .models import HardwareProtocol
22 
23 
24 @dataclass(slots=True)
26  """System status."""
27 
28  ha_psutil: ha_psutil
29  remove_periodic_timer: CALLBACK_TYPE | None
30  subscribers: set[tuple[websocket_api.ActiveConnection, int]]
31 
32 
33 async def async_setup(hass: HomeAssistant) -> None:
34  """Set up the hardware websocket API."""
35  websocket_api.async_register_command(hass, ws_info)
36  websocket_api.async_register_command(hass, ws_subscribe_system_status)
37  hass.data[DOMAIN]["system_status"] = SystemStatus(
38  ha_psutil=await hass.async_add_executor_job(ha_psutil.PsutilWrapper),
39  remove_periodic_timer=None,
40  subscribers=set(),
41  )
42 
43 
44 @websocket_api.websocket_command( { vol.Required("type"): "hardware/info",
45  }
46 )
47 @websocket_api.async_response
48 async def ws_info(
49  hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
50 ) -> None:
51  """Return hardware info."""
52  hardware_info = []
53 
54  if "hardware_platform" not in hass.data[DOMAIN]:
56 
57  hardware_platform: dict[str, HardwareProtocol] = hass.data[DOMAIN][
58  "hardware_platform"
59  ]
60  for platform in hardware_platform.values():
61  if hasattr(platform, "async_info"):
62  with contextlib.suppress(HomeAssistantError):
63  hardware_info.extend([asdict(hw) for hw in platform.async_info(hass)])
64 
65  connection.send_result(msg["id"], {"hardware": hardware_info})
66 
67 
68 @callback
69 @websocket_api.websocket_command( { vol.Required("type"): "hardware/subscribe_system_status",
70  }
71 )
73  hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
74 ) -> None:
75  """Subscribe to system status updates."""
76 
77  system_status: SystemStatus = hass.data[DOMAIN]["system_status"]
78 
79  @callback
80  def async_update_status(now: datetime) -> None:
81  # Although cpu_percent and virtual_memory access files in the /proc vfs, those
82  # accesses do not block and we don't need to wrap the calls in an executor.
83  # https://elixir.bootlin.com/linux/v5.19.4/source/fs/proc/stat.c
84  # https://elixir.bootlin.com/linux/v5.19.4/source/fs/proc/meminfo.c#L32
85  cpu_percentage = round(
86  system_status.ha_psutil.psutil.cpu_percent(interval=None)
87  )
88  virtual_memory = system_status.ha_psutil.psutil.virtual_memory()
89  json_msg = {
90  "cpu_percent": cpu_percentage,
91  "memory_used_percent": virtual_memory.percent,
92  "memory_used_mb": round(
93  (virtual_memory.total - virtual_memory.available) / 1024**2, 1
94  ),
95  "memory_free_mb": round(virtual_memory.available / 1024**2, 1),
96  "timestamp": dt_util.utcnow().isoformat(),
97  }
98  for conn, msg_id in system_status.subscribers:
99  conn.send_message(websocket_api.event_message(msg_id, json_msg))
100 
101  if not system_status.subscribers:
102  system_status.remove_periodic_timer = async_track_time_interval(
103  hass, async_update_status, timedelta(seconds=5)
104  )
105 
106  system_status.subscribers.add((connection, msg["id"]))
107 
108  @callback
109  def cancel_subscription() -> None:
110  system_status.subscribers.remove((connection, msg["id"]))
111  if not system_status.subscribers and system_status.remove_periodic_timer:
112  system_status.remove_periodic_timer()
113  system_status.remove_periodic_timer = None
114 
115  connection.subscriptions[msg["id"]] = cancel_subscription
116 
117  connection.send_message(websocket_api.result_message(msg["id"]))
118 
None async_process_hardware_platforms(HomeAssistant hass)
Definition: hardware.py:15
None ws_info(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
None ws_subscribe_system_status(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Definition: event.py:1679