Home Assistant Unofficial Reference 2024.12.1
common.py
Go to the documentation of this file.
1 """The Synology DSM component."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Callable
7 from contextlib import suppress
8 import logging
9 
10 from synology_dsm import SynologyDSM
11 from synology_dsm.api.core.security import SynoCoreSecurity
12 from synology_dsm.api.core.system import SynoCoreSystem
13 from synology_dsm.api.core.upgrade import SynoCoreUpgrade
14 from synology_dsm.api.core.utilization import SynoCoreUtilization
15 from synology_dsm.api.dsm.information import SynoDSMInformation
16 from synology_dsm.api.dsm.network import SynoDSMNetwork
17 from synology_dsm.api.photos import SynoPhotos
18 from synology_dsm.api.storage.storage import SynoStorage
19 from synology_dsm.api.surveillance_station import SynoSurveillanceStation
20 from synology_dsm.exceptions import (
21  SynologyDSMAPIErrorException,
22  SynologyDSMException,
23  SynologyDSMRequestException,
24 )
25 
26 from homeassistant.config_entries import ConfigEntry
27 from homeassistant.const import (
28  CONF_HOST,
29  CONF_PASSWORD,
30  CONF_PORT,
31  CONF_SSL,
32  CONF_USERNAME,
33  CONF_VERIFY_SSL,
34 )
35 from homeassistant.core import HomeAssistant, callback
36 from homeassistant.exceptions import ConfigEntryAuthFailed
37 from homeassistant.helpers.aiohttp_client import async_get_clientsession
38 
39 from .const import (
40  CONF_DEVICE_TOKEN,
41  DEFAULT_TIMEOUT,
42  EXCEPTION_DETAILS,
43  EXCEPTION_UNKNOWN,
44  SYNOLOGY_CONNECTION_EXCEPTIONS,
45 )
46 
47 LOGGER = logging.getLogger(__name__)
48 
49 
50 class SynoApi:
51  """Class to interface with Synology DSM API."""
52 
53  dsm: SynologyDSM
54 
55  def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
56  """Initialize the API wrapper class."""
57  self._hass_hass = hass
58  self._entry_entry = entry
59  if entry.data.get(CONF_SSL):
60  self.config_urlconfig_url = f"https://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}"
61  else:
62  self.config_urlconfig_url = f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}"
63 
64  # DSM APIs
65  self.informationinformation: SynoDSMInformation | None = None
66  self.networknetwork: SynoDSMNetwork | None = None
67  self.securitysecurity: SynoCoreSecurity | None = None
68  self.storagestorage: SynoStorage | None = None
69  self.photosphotos: SynoPhotos | None = None
70  self.surveillance_stationsurveillance_station: SynoSurveillanceStation | None = None
71  self.systemsystem: SynoCoreSystem | None = None
72  self.upgradeupgrade: SynoCoreUpgrade | None = None
73  self.utilisationutilisation: SynoCoreUtilization | None = None
74 
75  # Should we fetch them
76  self._fetching_entities: dict[str, set[str]] = {}
77  self._with_information_with_information = True
78  self._with_security_with_security = True
79  self._with_storage_with_storage = True
80  self._with_photos_with_photos = True
81  self._with_surveillance_station_with_surveillance_station = True
82  self._with_system_with_system = True
83  self._with_upgrade_with_upgrade = True
84  self._with_utilisation_with_utilisation = True
85 
86  self._login_future_login_future: asyncio.Future[None] | None = None
87 
88  async def async_login(self) -> None:
89  """Login to the Synology DSM API.
90 
91  This function will only login once if called multiple times
92  by multiple different callers.
93 
94  If a login is already in progress, the function will await the
95  login to complete before returning.
96  """
97  if self._login_future_login_future:
98  return await self._login_future_login_future
99 
100  self._login_future_login_future = self._hass_hass.loop.create_future()
101  try:
102  await self.dsmdsm.login()
103  self._login_future_login_future.set_result(None)
104  except BaseException as err:
105  if not self._login_future_login_future.done():
106  self._login_future_login_future.set_exception(err)
107  with suppress(BaseException):
108  # Clear the flag as its normal that nothing
109  # will wait for this future to be resolved
110  # if there are no concurrent login attempts
111  await self._login_future_login_future
112  raise
113  finally:
114  self._login_future_login_future = None
115 
116  async def async_setup(self) -> None:
117  """Start interacting with the NAS."""
118  session = async_get_clientsession(self._hass_hass, self._entry_entry.data[CONF_VERIFY_SSL])
119  self.dsmdsm = SynologyDSM(
120  session,
121  self._entry_entry.data[CONF_HOST],
122  self._entry_entry.data[CONF_PORT],
123  self._entry_entry.data[CONF_USERNAME],
124  self._entry_entry.data[CONF_PASSWORD],
125  self._entry_entry.data[CONF_SSL],
126  timeout=DEFAULT_TIMEOUT,
127  device_token=self._entry_entry.data.get(CONF_DEVICE_TOKEN),
128  )
129  await self.async_loginasync_login()
130 
131  # check if surveillance station is used
132  self._with_surveillance_station_with_surveillance_station = bool(
133  self.dsmdsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY)
134  )
135  if self._with_surveillance_station_with_surveillance_station:
136  try:
137  await self.dsmdsm.surveillance_station.update()
138  except SYNOLOGY_CONNECTION_EXCEPTIONS:
139  self._with_surveillance_station_with_surveillance_station = False
140  self.dsmdsm.reset(SynoSurveillanceStation.API_KEY)
141  LOGGER.warning(
142  "Surveillance Station found, but disabled due to missing user"
143  " permissions"
144  )
145 
146  LOGGER.debug(
147  "State of Surveillance_station during setup of '%s': %s",
148  self._entry_entry.unique_id,
149  self._with_surveillance_station_with_surveillance_station,
150  )
151 
152  # check if upgrade is available
153  try:
154  await self.dsmdsm.upgrade.update()
155  except SYNOLOGY_CONNECTION_EXCEPTIONS as ex:
156  self._with_upgrade_with_upgrade = False
157  self.dsmdsm.reset(SynoCoreUpgrade.API_KEY)
158  LOGGER.debug("Disabled fetching upgrade data during setup: %s", ex)
159 
160  await self._fetch_device_configuration_fetch_device_configuration()
161 
162  try:
163  await self._update_update()
164  except SYNOLOGY_CONNECTION_EXCEPTIONS as err:
165  LOGGER.debug(
166  "Connection error during setup of '%s' with exception: %s",
167  self._entry_entry.unique_id,
168  err,
169  )
170  raise
171 
172  @callback
173  def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]:
174  """Subscribe an entity to API fetches."""
175  LOGGER.debug("Subscribe new entity: %s", unique_id)
176  if api_key not in self._fetching_entities:
177  self._fetching_entities[api_key] = set()
178  self._fetching_entities[api_key].add(unique_id)
179 
180  @callback
181  def unsubscribe() -> None:
182  """Unsubscribe an entity from API fetches (when disable)."""
183  LOGGER.debug("Unsubscribe entity: %s", unique_id)
184  self._fetching_entities[api_key].remove(unique_id)
185  if len(self._fetching_entities[api_key]) == 0:
186  self._fetching_entities.pop(api_key)
187 
188  return unsubscribe
189 
190  def _setup_api_requests(self) -> None:
191  """Determine if we should fetch each API, if one entity needs it."""
192  # Entities not added yet, fetch all
193  if not self._fetching_entities:
194  LOGGER.debug(
195  "Entities not added yet, fetch all for '%s'", self._entry_entry.unique_id
196  )
197  return
198 
199  # surveillance_station is updated by own coordinator
200  if self.surveillance_stationsurveillance_station:
201  self.dsmdsm.reset(self.surveillance_stationsurveillance_station)
202 
203  # Determine if we should fetch an API
204  self._with_system_with_system = bool(self.dsmdsm.apis.get(SynoCoreSystem.API_KEY))
205  self._with_security_with_security = bool(
206  self._fetching_entities.get(SynoCoreSecurity.API_KEY)
207  )
208  self._with_storage_with_storage = bool(self._fetching_entities.get(SynoStorage.API_KEY))
209  self._with_photos_with_photos = bool(self._fetching_entities.get(SynoStorage.API_KEY))
210  self._with_upgrade_with_upgrade = bool(self._fetching_entities.get(SynoCoreUpgrade.API_KEY))
211  self._with_utilisation_with_utilisation = bool(
212  self._fetching_entities.get(SynoCoreUtilization.API_KEY)
213  )
214  self._with_information_with_information = bool(
215  self._fetching_entities.get(SynoDSMInformation.API_KEY)
216  )
217 
218  # Reset not used API, information is not reset since it's used in device_info
219  if not self._with_security_with_security:
220  LOGGER.debug(
221  "Disable security api from being updated for '%s'",
222  self._entry_entry.unique_id,
223  )
224  if self.securitysecurity:
225  self.dsmdsm.reset(self.securitysecurity)
226  self.securitysecurity = None
227 
228  if not self._with_photos_with_photos:
229  LOGGER.debug(
230  "Disable photos api from being updated or '%s'", self._entry_entry.unique_id
231  )
232  if self.photosphotos:
233  self.dsmdsm.reset(self.photosphotos)
234  self.photosphotos = None
235 
236  if not self._with_storage_with_storage:
237  LOGGER.debug(
238  "Disable storage api from being updatedf or '%s'", self._entry_entry.unique_id
239  )
240  if self.storagestorage:
241  self.dsmdsm.reset(self.storagestorage)
242  self.storagestorage = None
243 
244  if not self._with_system_with_system:
245  LOGGER.debug(
246  "Disable system api from being updated for '%s'", self._entry_entry.unique_id
247  )
248  if self.systemsystem:
249  self.dsmdsm.reset(self.systemsystem)
250  self.systemsystem = None
251 
252  if not self._with_upgrade_with_upgrade:
253  LOGGER.debug(
254  "Disable upgrade api from being updated for '%s'", self._entry_entry.unique_id
255  )
256  if self.upgradeupgrade:
257  self.dsmdsm.reset(self.upgradeupgrade)
258  self.upgradeupgrade = None
259 
260  if not self._with_utilisation_with_utilisation:
261  LOGGER.debug(
262  "Disable utilisation api from being updated for '%s'",
263  self._entry_entry.unique_id,
264  )
265  if self.utilisationutilisation:
266  self.dsmdsm.reset(self.utilisationutilisation)
267  self.utilisationutilisation = None
268 
269  async def _fetch_device_configuration(self) -> None:
270  """Fetch initial device config."""
271  self.informationinformation = self.dsmdsm.information
272  self.networknetwork = self.dsmdsm.network
273  await self.networknetwork.update()
274 
275  if self._with_security_with_security:
276  LOGGER.debug("Enable security api updates for '%s'", self._entry_entry.unique_id)
277  self.securitysecurity = self.dsmdsm.security
278 
279  if self._with_photos_with_photos:
280  LOGGER.debug("Enable photos api updates for '%s'", self._entry_entry.unique_id)
281  self.photosphotos = self.dsmdsm.photos
282 
283  if self._with_storage_with_storage:
284  LOGGER.debug("Enable storage api updates for '%s'", self._entry_entry.unique_id)
285  self.storagestorage = self.dsmdsm.storage
286 
287  if self._with_upgrade_with_upgrade:
288  LOGGER.debug("Enable upgrade api updates for '%s'", self._entry_entry.unique_id)
289  self.upgradeupgrade = self.dsmdsm.upgrade
290 
291  if self._with_system_with_system:
292  LOGGER.debug("Enable system api updates for '%s'", self._entry_entry.unique_id)
293  self.systemsystem = self.dsmdsm.system
294 
295  if self._with_utilisation_with_utilisation:
296  LOGGER.debug(
297  "Enable utilisation api updates for '%s'", self._entry_entry.unique_id
298  )
299  self.utilisationutilisation = self.dsmdsm.utilisation
300 
301  if self._with_surveillance_station_with_surveillance_station:
302  LOGGER.debug(
303  "Enable surveillance_station api updates for '%s'",
304  self._entry_entry.unique_id,
305  )
306  self.surveillance_stationsurveillance_station = self.dsmdsm.surveillance_station
307 
308  async def _syno_api_executer(self, api_call: Callable) -> None:
309  """Synology api call wrapper."""
310  try:
311  await api_call()
312  except (SynologyDSMAPIErrorException, SynologyDSMRequestException) as err:
313  LOGGER.debug(
314  "Error from '%s': %s", self._entry_entry.unique_id, err, exc_info=True
315  )
316  raise
317 
318  async def async_reboot(self) -> None:
319  """Reboot NAS."""
320  if self.systemsystem:
321  await self._syno_api_executer_syno_api_executer(self.systemsystem.reboot)
322 
323  async def async_shutdown(self) -> None:
324  """Shutdown NAS."""
325  if self.systemsystem:
326  await self._syno_api_executer_syno_api_executer(self.systemsystem.shutdown)
327 
328  async def async_unload(self) -> None:
329  """Stop interacting with the NAS and prepare for removal from hass."""
330  # ignore API errors during logout
331  with suppress(SynologyDSMException):
332  await self._syno_api_executer_syno_api_executer(self.dsmdsm.logout)
333 
334  async def async_update(self) -> None:
335  """Update function for updating API information."""
336  await self._update_update()
337 
338  async def _update(self) -> None:
339  """Update function for updating API information."""
340  LOGGER.debug("Start data update for '%s'", self._entry_entry.unique_id)
341  self._setup_api_requests_setup_api_requests()
342  await self.dsmdsm.update(self._with_information_with_information)
343 
344 
345 def raise_config_entry_auth_error(err: Exception) -> None:
346  """Raise ConfigEntryAuthFailed if error is related to authentication."""
347  if err.args[0] and isinstance(err.args[0], dict):
348  details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN)
349  else:
350  details = EXCEPTION_UNKNOWN
351  raise ConfigEntryAuthFailed(f"reason: {details}") from err
None __init__(self, HomeAssistant hass, ConfigEntry entry)
Definition: common.py:55
Callable[[], None] subscribe(self, str api_key, str unique_id)
Definition: common.py:173
None _syno_api_executer(self, Callable api_call)
Definition: common.py:308
bool add(self, _T matcher)
Definition: match.py:185
bool remove(self, _T matcher)
Definition: match.py:214
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
None raise_config_entry_auth_error(Exception err)
Definition: common.py:345
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)