Home Assistant Unofficial Reference 2024.12.1
router.py
Go to the documentation of this file.
1 """Represent the Freebox router and its devices and sensors."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Mapping
6 from contextlib import suppress
7 from datetime import datetime
8 import json
9 import logging
10 import os
11 from pathlib import Path
12 import re
13 from typing import Any
14 
15 from freebox_api import Freepybox
16 from freebox_api.api.call import Call
17 from freebox_api.api.home import Home
18 from freebox_api.api.wifi import Wifi
19 from freebox_api.exceptions import HttpRequestError, NotOpenError
20 
21 from homeassistant.config_entries import ConfigEntry
22 from homeassistant.const import CONF_HOST, CONF_PORT
23 from homeassistant.core import HomeAssistant
24 from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
25 from homeassistant.helpers.dispatcher import async_dispatcher_send
26 from homeassistant.helpers.storage import Store
27 from homeassistant.util import slugify
28 
29 from .const import (
30  API_VERSION,
31  APP_DESC,
32  CONNECTION_SENSORS_KEYS,
33  DOMAIN,
34  HOME_COMPATIBLE_CATEGORIES,
35  STORAGE_KEY,
36  STORAGE_VERSION,
37 )
38 
39 _LOGGER = logging.getLogger(__name__)
40 
41 
42 def is_json(json_str: str) -> bool:
43  """Validate if a String is a JSON value or not."""
44  try:
45  json.loads(json_str)
46  except (ValueError, TypeError) as err:
47  _LOGGER.error(
48  "Failed to parse JSON '%s', error '%s'",
49  json_str,
50  err,
51  )
52  return False
53  return True
54 
55 
56 async def get_api(hass: HomeAssistant, host: str) -> Freepybox:
57  """Get the Freebox API."""
58  freebox_path = Store(hass, STORAGE_VERSION, STORAGE_KEY).path
59 
60  if not os.path.exists(freebox_path):
61  await hass.async_add_executor_job(os.makedirs, freebox_path)
62 
63  token_file = Path(f"{freebox_path}/{slugify(host)}.conf")
64 
65  return Freepybox(APP_DESC, token_file, API_VERSION)
66 
67 
69  fbx_api: Freepybox,
70 ) -> tuple[bool, list[dict[str, Any]]]:
71  """Hosts list is not supported when freebox is configured in bridge mode."""
72  supports_hosts: bool = True
73  fbx_devices: list[dict[str, Any]] = []
74  try:
75  fbx_devices = await fbx_api.lan.get_hosts_list() or []
76  except HttpRequestError as err:
77  if (
78  (matcher := re.search(r"Request failed \‍(APIResponse: (.+)\‍)", str(err)))
79  and is_json(json_str := matcher.group(1))
80  and (json_resp := json.loads(json_str)).get("error_code") == "nodev"
81  ):
82  # No need to retry, Host list not available
83  supports_hosts = False
84  _LOGGER.debug(
85  "Host list is not available using bridge mode (%s)",
86  json_resp.get("msg"),
87  )
88 
89  else:
90  raise
91 
92  return supports_hosts, fbx_devices
93 
94 
96  """Representation of a Freebox router."""
97 
98  def __init__(
99  self,
100  hass: HomeAssistant,
101  entry: ConfigEntry,
102  api: Freepybox,
103  freebox_config: Mapping[str, Any],
104  ) -> None:
105  """Initialize a Freebox router."""
106  self.hasshass = hass
107  self._host_host = entry.data[CONF_HOST]
108  self._port_port = entry.data[CONF_PORT]
109 
110  self._api: Freepybox = api
111  self.name: str = freebox_config["model_info"]["pretty_name"]
112  self.mac: str = freebox_config["mac"]
113  self._sw_v: str = freebox_config["firmware_version"]
114  self._attrs_attrs: dict[str, Any] = {}
115 
116  self.supports_hostssupports_hosts = True
117  self.devices: dict[str, dict[str, Any]] = {}
118  self.disks: dict[int, dict[str, Any]] = {}
119  self.supports_raidsupports_raid = True
120  self.raids: dict[int, dict[str, Any]] = {}
121  self.sensors_temperature: dict[str, int] = {}
122  self.sensors_connection: dict[str, float] = {}
123  self.call_listcall_list: list[dict[str, Any]] = []
124  self.home_grantedhome_granted = True
125  self.home_devices: dict[str, Any] = {}
126  self.listeners: list[Callable[[], None]] = []
127 
128  async def update_all(self, now: datetime | None = None) -> None:
129  """Update all Freebox platforms."""
130  await self.update_device_trackersupdate_device_trackers()
131  await self.update_sensorsupdate_sensors()
132  await self.update_home_devicesupdate_home_devices()
133 
134  async def update_device_trackers(self) -> None:
135  """Update Freebox devices."""
136  new_device = False
137 
138  fbx_devices: list[dict[str, Any]] = []
139 
140  # Access to Host list not available in bridge mode, API return error_code 'nodev'
141  if self.supports_hostssupports_hosts:
142  self.supports_hostssupports_hosts, fbx_devices = await get_hosts_list_if_supported(
143  self._api
144  )
145 
146  # Adds the Freebox itself
147  fbx_devices.append(
148  {
149  "primary_name": self.name,
150  "l2ident": {"id": self.mac},
151  "vendor_name": "Freebox SAS",
152  "host_type": "router",
153  "active": True,
154  "attrs": self._attrs_attrs,
155  }
156  )
157 
158  for fbx_device in fbx_devices:
159  device_mac = fbx_device["l2ident"]["id"]
160 
161  if self.devices.get(device_mac) is None:
162  new_device = True
163 
164  self.devices[device_mac] = fbx_device
165 
166  async_dispatcher_send(self.hasshass, self.signal_device_updatesignal_device_update)
167 
168  if new_device:
169  async_dispatcher_send(self.hasshass, self.signal_device_newsignal_device_new)
170 
171  async def update_sensors(self) -> None:
172  """Update Freebox sensors."""
173 
174  # System sensors
175  syst_datas: dict[str, Any] = await self._api.system.get_config()
176 
177  # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree.
178  # Name and id of sensors may vary under Freebox devices.
179  for sensor in syst_datas["sensors"]:
180  self.sensors_temperature[sensor["name"]] = sensor.get("value")
181 
182  # Connection sensors
183  connection_datas: dict[str, Any] = await self._api.connection.get_status()
184  for sensor_key in CONNECTION_SENSORS_KEYS:
185  self.sensors_connection[sensor_key] = connection_datas[sensor_key]
186 
187  self._attrs_attrs = {
188  "IPv4": connection_datas.get("ipv4"),
189  "IPv6": connection_datas.get("ipv6"),
190  "connection_type": connection_datas["media"],
191  "uptime": datetime.fromtimestamp(
192  round(datetime.now().timestamp()) - syst_datas["uptime_val"]
193  ),
194  "firmware_version": self._sw_v,
195  "serial": syst_datas["serial"],
196  }
197 
198  self.call_listcall_list = await self._api.call.get_calls_log()
199 
200  await self._update_disks_sensors_update_disks_sensors()
201  await self._update_raids_sensors_update_raids_sensors()
202 
203  async_dispatcher_send(self.hasshass, self.signal_sensor_updatesignal_sensor_update)
204 
205  async def _update_disks_sensors(self) -> None:
206  """Update Freebox disks."""
207  # None at first request
208  fbx_disks: list[dict[str, Any]] = await self._api.storage.get_disks() or []
209 
210  for fbx_disk in fbx_disks:
211  disk: dict[str, Any] = {**fbx_disk}
212  disk_part: dict[int, dict[str, Any]] = {}
213  for fbx_disk_part in fbx_disk["partitions"]:
214  disk_part[fbx_disk_part["id"]] = fbx_disk_part
215  disk["partitions"] = disk_part
216  self.disks[fbx_disk["id"]] = disk
217 
218  async def _update_raids_sensors(self) -> None:
219  """Update Freebox raids."""
220  # None at first request
221  if not self.supports_raidsupports_raid:
222  return
223 
224  try:
225  fbx_raids: list[dict[str, Any]] = await self._api.storage.get_raids() or []
226  except HttpRequestError:
227  self.supports_raidsupports_raid = False
228  _LOGGER.warning(
229  "Router %s API does not support RAID",
230  self.name,
231  )
232  return
233 
234  for fbx_raid in fbx_raids:
235  self.raids[fbx_raid["id"]] = fbx_raid
236 
237  async def update_home_devices(self) -> None:
238  """Update Home devices (alarm, light, sensor, switch, remote ...)."""
239  if not self.home_grantedhome_granted:
240  return
241 
242  try:
243  home_nodes: list[Any] = await self.homehome.get_home_nodes() or []
244  except HttpRequestError:
245  self.home_grantedhome_granted = False
246  _LOGGER.warning("Home access is not granted")
247  return
248 
249  new_device = False
250  for home_node in home_nodes:
251  if home_node["category"] in HOME_COMPATIBLE_CATEGORIES:
252  if self.home_devices.get(home_node["id"]) is None:
253  new_device = True
254  self.home_devices[home_node["id"]] = home_node
255 
256  async_dispatcher_send(self.hasshass, self.signal_home_device_updatesignal_home_device_update)
257 
258  if new_device:
259  async_dispatcher_send(self.hasshass, self.signal_home_device_newsignal_home_device_new)
260 
261  async def reboot(self) -> None:
262  """Reboot the Freebox."""
263  await self._api.system.reboot()
264 
265  async def close(self) -> None:
266  """Close the connection."""
267  with suppress(NotOpenError):
268  await self._api.close()
269 
270  @property
271  def device_info(self) -> DeviceInfo:
272  """Return the device information."""
273  return DeviceInfo(
274  configuration_url=f"https://{self._host}:{self._port}/",
275  connections={(CONNECTION_NETWORK_MAC, self.mac)},
276  identifiers={(DOMAIN, self.mac)},
277  manufacturer="Freebox SAS",
278  name=self.name,
279  sw_version=self._sw_v,
280  )
281 
282  @property
283  def signal_device_new(self) -> str:
284  """Event specific per Freebox entry to signal new device."""
285  return f"{DOMAIN}-{self._host}-device-new"
286 
287  @property
288  def signal_home_device_new(self) -> str:
289  """Event specific per Freebox entry to signal new home device."""
290  return f"{DOMAIN}-{self._host}-home-device-new"
291 
292  @property
293  def signal_device_update(self) -> str:
294  """Event specific per Freebox entry to signal updates in devices."""
295  return f"{DOMAIN}-{self._host}-device-update"
296 
297  @property
298  def signal_sensor_update(self) -> str:
299  """Event specific per Freebox entry to signal updates in sensors."""
300  return f"{DOMAIN}-{self._host}-sensor-update"
301 
302  @property
303  def signal_home_device_update(self) -> str:
304  """Event specific per Freebox entry to signal update in home devices."""
305  return f"{DOMAIN}-{self._host}-home-device-update"
306 
307  @property
308  def sensors(self) -> dict[str, Any]:
309  """Return sensors."""
310  return {**self.sensors_temperature, **self.sensors_connection}
311 
312  @property
313  def call(self) -> Call:
314  """Return the call."""
315  return self._api.call
316 
317  @property
318  def wifi(self) -> Wifi:
319  """Return the wifi."""
320  return self._api.wifi
321 
322  @property
323  def home(self) -> Home:
324  """Return the home."""
325  return self._api.home
None update_all(self, datetime|None now=None)
Definition: router.py:128
None __init__(self, HomeAssistant hass, ConfigEntry entry, Freepybox api, Mapping[str, Any] freebox_config)
Definition: router.py:104
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
Freepybox get_api(HomeAssistant hass, str host)
Definition: router.py:56
tuple[bool, list[dict[str, Any]]] get_hosts_list_if_supported(Freepybox fbx_api)
Definition: router.py:70
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193