From 22daea27c205b9c931de8fb7cb42a630866b4960 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 10:22:16 -1000 Subject: [PATCH] Cleanup coordinators in synology_dsm (#73257) Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .coveragerc | 1 + .../components/synology_dsm/__init__.py | 170 +++++------------- .../components/synology_dsm/binary_sensor.py | 10 +- .../components/synology_dsm/button.py | 9 +- .../components/synology_dsm/camera.py | 26 +-- .../components/synology_dsm/common.py | 9 +- .../components/synology_dsm/const.py | 30 +++- .../components/synology_dsm/coordinator.py | 148 +++++++++++++++ .../components/synology_dsm/diagnostics.py | 18 +- .../components/synology_dsm/models.py | 21 +++ .../components/synology_dsm/sensor.py | 10 +- .../components/synology_dsm/service.py | 18 +- .../components/synology_dsm/switch.py | 33 ++-- .../components/synology_dsm/update.py | 11 +- tests/components/synology_dsm/test_init.py | 6 +- 15 files changed, 287 insertions(+), 233 deletions(-) create mode 100644 homeassistant/components/synology_dsm/coordinator.py create mode 100644 homeassistant/components/synology_dsm/models.py diff --git a/.coveragerc b/.coveragerc index 44b205746df..ea2c41f7e46 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1200,6 +1200,7 @@ omit = homeassistant/components/synology_dsm/binary_sensor.py homeassistant/components/synology_dsm/button.py homeassistant/components/synology_dsm/camera.py + homeassistant/components/synology_dsm/coordinator.py homeassistant/components/synology_dsm/diagnostics.py homeassistant/components/synology_dsm/common.py homeassistant/components/synology_dsm/entity.py diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index ece38bf7326..d8d768d36e4 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -1,47 +1,32 @@ """The Synology DSM component.""" from __future__ import annotations -from datetime import timedelta import logging -from typing import Any -import async_timeout from synology_dsm.api.surveillance_station import SynoSurveillanceStation -from synology_dsm.api.surveillance_station.camera import SynoCamera -from synology_dsm.exceptions import ( - SynologyDSMAPIErrorException, - SynologyDSMLogin2SARequiredException, - SynologyDSMLoginDisabledAccountException, - SynologyDSMLoginFailedException, - SynologyDSMLoginInvalidException, - SynologyDSMLoginPermissionDeniedException, - SynologyDSMRequestException, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_SCAN_INTERVAL, CONF_VERIFY_SSL +from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .common import SynoApi from .const import ( - COORDINATOR_CAMERAS, - COORDINATOR_CENTRAL, - COORDINATOR_SWITCHES, - DEFAULT_SCAN_INTERVAL, DEFAULT_VERIFY_SSL, DOMAIN, EXCEPTION_DETAILS, EXCEPTION_UNKNOWN, PLATFORMS, - SIGNAL_CAMERA_SOURCE_CHANGED, - SYNO_API, - SYSTEM_LOADED, - UNDO_UPDATE_LISTENER, + SYNOLOGY_AUTH_FAILED_EXCEPTIONS, + SYNOLOGY_CONNECTION_EXCEPTIONS, ) +from .coordinator import ( + SynologyDSMCameraUpdateCoordinator, + SynologyDSMCentralUpdateCoordinator, + SynologyDSMSwitchUpdateCoordinator, +) +from .models import SynologyDSMData from .service import async_setup_services CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -79,31 +64,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = SynoApi(hass, entry) try: await api.async_setup() - except ( - SynologyDSMLogin2SARequiredException, - SynologyDSMLoginDisabledAccountException, - SynologyDSMLoginInvalidException, - SynologyDSMLoginPermissionDeniedException, - ) as err: + except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: if err.args[0] and isinstance(err.args[0], dict): details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN raise ConfigEntryAuthFailed(f"reason: {details}") from err - except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: if err.args[0] and isinstance(err.args[0], dict): details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN raise ConfigEntryNotReady(details) from err - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.unique_id] = { - UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener), - SYNO_API: api, - SYSTEM_LOADED: True, - } - # Services await async_setup_services(hass) @@ -114,111 +87,50 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, data={**entry.data, CONF_MAC: network.macs} ) - async def async_coordinator_update_data_cameras() -> dict[ - str, dict[str, SynoCamera] - ] | None: - """Fetch all camera data from api.""" - if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: - raise UpdateFailed("System not fully loaded") + # These all create executor jobs so we do not gather here + coordinator_central = SynologyDSMCentralUpdateCoordinator(hass, entry, api) + await coordinator_central.async_config_entry_first_refresh() - if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: - return None + available_apis = api.dsm.apis - surveillance_station = api.surveillance_station - current_data: dict[str, SynoCamera] = { - camera.id: camera for camera in surveillance_station.get_all_cameras() - } + # The central coordinator needs to be refreshed first since + # the next two rely on data from it + coordinator_cameras: SynologyDSMCameraUpdateCoordinator | None = None + if SynoSurveillanceStation.CAMERA_API_KEY in available_apis: + coordinator_cameras = SynologyDSMCameraUpdateCoordinator(hass, entry, api) + await coordinator_cameras.async_config_entry_first_refresh() + coordinator_switches: SynologyDSMSwitchUpdateCoordinator | None = None + if ( + SynoSurveillanceStation.INFO_API_KEY in available_apis + and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis + ): + coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api) + await coordinator_switches.async_config_entry_first_refresh() try: - async with async_timeout.timeout(30): - await hass.async_add_executor_job(surveillance_station.update) - except SynologyDSMAPIErrorException as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err + await coordinator_switches.async_setup() + except SYNOLOGY_CONNECTION_EXCEPTIONS as ex: + raise ConfigEntryNotReady from ex - new_data: dict[str, SynoCamera] = { - camera.id: camera for camera in surveillance_station.get_all_cameras() - } - - for cam_id, cam_data_new in new_data.items(): - if ( - (cam_data_current := current_data.get(cam_id)) is not None - and cam_data_current.live_view.rtsp != cam_data_new.live_view.rtsp - ): - async_dispatcher_send( - hass, - f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{entry.entry_id}_{cam_id}", - cam_data_new.live_view.rtsp, - ) - - return {"cameras": new_data} - - async def async_coordinator_update_data_central() -> None: - """Fetch all device and sensor data from api.""" - try: - await api.async_update() - except Exception as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err - return None - - async def async_coordinator_update_data_switches() -> dict[ - str, dict[str, Any] - ] | None: - """Fetch all switch data from api.""" - if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: - raise UpdateFailed("System not fully loaded") - if SynoSurveillanceStation.HOME_MODE_API_KEY not in api.dsm.apis: - return None - - surveillance_station = api.surveillance_station - - return { - "switches": { - "home_mode": await hass.async_add_executor_job( - surveillance_station.get_home_mode_status - ) - } - } - - hass.data[DOMAIN][entry.unique_id][COORDINATOR_CAMERAS] = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"{entry.unique_id}_cameras", - update_method=async_coordinator_update_data_cameras, - update_interval=timedelta(seconds=30), + synology_data = SynologyDSMData( + api=api, + coordinator_central=coordinator_central, + coordinator_cameras=coordinator_cameras, + coordinator_switches=coordinator_switches, ) - - hass.data[DOMAIN][entry.unique_id][COORDINATOR_CENTRAL] = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"{entry.unique_id}_central", - update_method=async_coordinator_update_data_central, - update_interval=timedelta( - minutes=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - ), - ) - - hass.data[DOMAIN][entry.unique_id][COORDINATOR_SWITCHES] = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"{entry.unique_id}_switches", - update_method=async_coordinator_update_data_switches, - update_interval=timedelta(seconds=30), - ) - + hass.data.setdefault(DOMAIN, {})[entry.unique_id] = synology_data hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Synology DSM sensors.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - entry_data = hass.data[DOMAIN][entry.unique_id] - entry_data[UNDO_UPDATE_LISTENER]() - await entry_data[SYNO_API].async_unload() + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + entry_data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + await entry_data.api.async_unload() hass.data[DOMAIN].pop(entry.unique_id) - return unload_ok diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index a5c96575307..b5f5effbb8e 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -22,12 +22,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi -from .const import COORDINATOR_CENTRAL, DOMAIN, SYNO_API +from .const import DOMAIN from .entity import ( SynologyDSMBaseEntity, SynologyDSMDeviceEntity, SynologyDSMEntityDescription, ) +from .models import SynologyDSMData @dataclass @@ -80,10 +81,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS binary sensor.""" - - data = hass.data[DOMAIN][entry.unique_id] - api: SynoApi = data[SYNO_API] - coordinator = data[COORDINATOR_CENTRAL] + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + api = data.api + coordinator = data.coordinator_central entities: list[ SynoDSMSecurityBinarySensor diff --git a/homeassistant/components/synology_dsm/button.py b/homeassistant/components/synology_dsm/button.py index 58f1a0dfdd7..a1337e672f6 100644 --- a/homeassistant/components/synology_dsm/button.py +++ b/homeassistant/components/synology_dsm/button.py @@ -17,7 +17,8 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SynoApi -from .const import DOMAIN, SYNO_API +from .const import DOMAIN +from .models import SynologyDSMData LOGGER = logging.getLogger(__name__) @@ -60,10 +61,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set buttons for device.""" - data = hass.data[DOMAIN][entry.unique_id] - syno_api: SynoApi = data[SYNO_API] - - async_add_entities(SynologyDSMButton(syno_api, button) for button in BUTTONS) + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + async_add_entities(SynologyDSMButton(data.api, button) for button in BUTTONS) class SynologyDSMButton(ButtonEntity): diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 0a6934b45a7..6dac67cf72d 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -25,13 +25,12 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi from .const import ( CONF_SNAPSHOT_QUALITY, - COORDINATOR_CAMERAS, DEFAULT_SNAPSHOT_QUALITY, DOMAIN, SIGNAL_CAMERA_SOURCE_CHANGED, - SYNO_API, ) from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription +from .models import SynologyDSMData _LOGGER = logging.getLogger(__name__) @@ -47,23 +46,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS cameras.""" - - data = hass.data[DOMAIN][entry.unique_id] - api: SynoApi = data[SYNO_API] - - if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: - return - - # initial data fetch - coordinator: DataUpdateCoordinator[dict[str, dict[str, SynoCamera]]] = data[ - COORDINATOR_CAMERAS - ] - await coordinator.async_config_entry_first_refresh() - - async_add_entities( - SynoDSMCamera(api, coordinator, camera_id) - for camera_id in coordinator.data["cameras"] - ) + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + if coordinator := data.coordinator_cameras: + async_add_entities( + SynoDSMCamera(data.api, coordinator, camera_id) + for camera_id in coordinator.data["cameras"] + ) class SynoDSMCamera(SynologyDSMBaseEntity, Camera): diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 2ca9cbf3ccf..088686660e4 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -32,7 +32,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback -from .const import CONF_DEVICE_TOKEN, DOMAIN, SYSTEM_LOADED +from .const import CONF_DEVICE_TOKEN LOGGER = logging.getLogger(__name__) @@ -217,11 +217,6 @@ class SynoApi: ) self.surveillance_station = self.dsm.surveillance_station - def _set_system_loaded(self, state: bool = False) -> None: - """Set system loaded flag.""" - dsm_device = self._hass.data[DOMAIN].get(self.information.serial) - dsm_device[SYSTEM_LOADED] = state - async def _syno_api_executer(self, api_call: Callable) -> None: """Synology api call wrapper.""" try: @@ -235,12 +230,10 @@ class SynoApi: async def async_reboot(self) -> None: """Reboot NAS.""" await self._syno_api_executer(self.system.reboot) - self._set_system_loaded() async def async_shutdown(self) -> None: """Shutdown NAS.""" await self._syno_api_executer(self.system.shutdown) - self._set_system_loaded() async def async_unload(self) -> None: """Stop interacting with the NAS and prepare for removal from hass.""" diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index f716130a5e4..c5c9e590684 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -2,6 +2,15 @@ from __future__ import annotations from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED +from synology_dsm.exceptions import ( + SynologyDSMAPIErrorException, + SynologyDSMLogin2SARequiredException, + SynologyDSMLoginDisabledAccountException, + SynologyDSMLoginFailedException, + SynologyDSMLoginInvalidException, + SynologyDSMLoginPermissionDeniedException, + SynologyDSMRequestException, +) from homeassistant.const import Platform @@ -15,17 +24,9 @@ PLATFORMS = [ Platform.SWITCH, Platform.UPDATE, ] -COORDINATOR_CAMERAS = "coordinator_cameras" -COORDINATOR_CENTRAL = "coordinator_central" -COORDINATOR_SWITCHES = "coordinator_switches" -SYSTEM_LOADED = "system_loaded" EXCEPTION_DETAILS = "details" EXCEPTION_UNKNOWN = "unknown" -# Entry keys -SYNO_API = "syno_api" -UNDO_UPDATE_LISTENER = "undo_update_listener" - # Configuration CONF_SERIAL = "serial" CONF_VOLUMES = "volumes" @@ -53,3 +54,16 @@ SERVICES = [ SERVICE_REBOOT, SERVICE_SHUTDOWN, ] + +SYNOLOGY_AUTH_FAILED_EXCEPTIONS = ( + SynologyDSMLogin2SARequiredException, + SynologyDSMLoginDisabledAccountException, + SynologyDSMLoginInvalidException, + SynologyDSMLoginPermissionDeniedException, +) + +SYNOLOGY_CONNECTION_EXCEPTIONS = ( + SynologyDSMAPIErrorException, + SynologyDSMLoginFailedException, + SynologyDSMRequestException, +) diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py new file mode 100644 index 00000000000..332efb50bc8 --- /dev/null +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -0,0 +1,148 @@ +"""synology_dsm coordinators.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import async_timeout +from synology_dsm.api.surveillance_station.camera import SynoCamera +from synology_dsm.exceptions import SynologyDSMAPIErrorException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .common import SynoApi +from .const import ( + DEFAULT_SCAN_INTERVAL, + SIGNAL_CAMERA_SOURCE_CHANGED, + SYNOLOGY_CONNECTION_EXCEPTIONS, +) + +_LOGGER = logging.getLogger(__name__) + + +class SynologyDSMUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator base class for synology_dsm.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + api: SynoApi, + update_interval: timedelta, + ) -> None: + """Initialize synology_dsm DataUpdateCoordinator.""" + self.api = api + self.entry = entry + super().__init__( + hass, + _LOGGER, + name=f"{entry.title} {self.__class__.__name__}", + update_interval=update_interval, + ) + + +class SynologyDSMSwitchUpdateCoordinator(SynologyDSMUpdateCoordinator): + """DataUpdateCoordinator to gather data for a synology_dsm switch devices.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + api: SynoApi, + ) -> None: + """Initialize DataUpdateCoordinator for switch devices.""" + super().__init__(hass, entry, api, timedelta(seconds=30)) + self.version: str | None = None + + async def async_setup(self) -> None: + """Set up the coordinator initial data.""" + info = await self.hass.async_add_executor_job( + self.api.dsm.surveillance_station.get_info + ) + self.version = info["data"]["CMSMinVersion"] + + async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None: + """Fetch all data from api.""" + surveillance_station = self.api.surveillance_station + return { + "switches": { + "home_mode": await self.hass.async_add_executor_job( + surveillance_station.get_home_mode_status + ) + } + } + + +class SynologyDSMCentralUpdateCoordinator(SynologyDSMUpdateCoordinator): + """DataUpdateCoordinator to gather data for a synology_dsm central device.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + api: SynoApi, + ) -> None: + """Initialize DataUpdateCoordinator for central device.""" + super().__init__( + hass, + entry, + api, + timedelta( + minutes=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + ), + ) + + async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None: + """Fetch all data from api.""" + try: + await self.api.async_update() + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + return None + + +class SynologyDSMCameraUpdateCoordinator(SynologyDSMUpdateCoordinator): + """DataUpdateCoordinator to gather data for a synology_dsm cameras.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + api: SynoApi, + ) -> None: + """Initialize DataUpdateCoordinator for cameras.""" + super().__init__(hass, entry, api, timedelta(seconds=30)) + + async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None: + """Fetch all camera data from api.""" + surveillance_station = self.api.surveillance_station + current_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() + } + + try: + async with async_timeout.timeout(30): + await self.hass.async_add_executor_job(surveillance_station.update) + except SynologyDSMAPIErrorException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + new_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() + } + + for cam_id, cam_data_new in new_data.items(): + if ( + (cam_data_current := current_data.get(cam_id)) is not None + and cam_data_current.live_view.rtsp != cam_data_new.live_view.rtsp + ): + async_dispatcher_send( + self.hass, + f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{self.entry.entry_id}_{cam_id}", + cam_data_new.live_view.rtsp, + ) + + return {"cameras": new_data} diff --git a/homeassistant/components/synology_dsm/diagnostics.py b/homeassistant/components/synology_dsm/diagnostics.py index 8709170a6f8..485a44b290a 100644 --- a/homeassistant/components/synology_dsm/diagnostics.py +++ b/homeassistant/components/synology_dsm/diagnostics.py @@ -8,8 +8,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import SynoApi -from .const import CONF_DEVICE_TOKEN, DOMAIN, SYNO_API, SYSTEM_LOADED +from .const import CONF_DEVICE_TOKEN, DOMAIN +from .models import SynologyDSMData TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_DEVICE_TOKEN} @@ -18,8 +18,8 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict: """Return diagnostics for a config entry.""" - data: dict = hass.data[DOMAIN][entry.unique_id] - syno_api: SynoApi = data[SYNO_API] + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + syno_api = data.api dsm_info = syno_api.dsm.information diag_data = { @@ -36,7 +36,7 @@ async def async_get_config_entry_diagnostics( "surveillance_station": {"cameras": {}}, "upgrade": {}, "utilisation": {}, - "is_system_loaded": data[SYSTEM_LOADED], + "is_system_loaded": True, "api_details": { "fetching_entities": syno_api._fetching_entities, # pylint: disable=protected-access }, @@ -45,7 +45,7 @@ async def async_get_config_entry_diagnostics( if syno_api.network is not None: intf: dict for intf in syno_api.network.interfaces: - diag_data["network"]["interfaces"][intf["id"]] = { + diag_data["network"]["interfaces"][intf["id"]] = { # type: ignore[index] "type": intf["type"], "ip": intf["ip"], } @@ -53,7 +53,7 @@ async def async_get_config_entry_diagnostics( if syno_api.storage is not None: disk: dict for disk in syno_api.storage.disks: - diag_data["storage"]["disks"][disk["id"]] = { + diag_data["storage"]["disks"][disk["id"]] = { # type: ignore[index] "name": disk["name"], "vendor": disk["vendor"], "model": disk["model"], @@ -64,7 +64,7 @@ async def async_get_config_entry_diagnostics( volume: dict for volume in syno_api.storage.volumes: - diag_data["storage"]["volumes"][volume["id"]] = { + diag_data["storage"]["volumes"][volume["id"]] = { # type: ignore[index] "name": volume["fs_type"], "size": volume["size"], } @@ -72,7 +72,7 @@ async def async_get_config_entry_diagnostics( if syno_api.surveillance_station is not None: camera: SynoCamera for camera in syno_api.surveillance_station.get_all_cameras(): - diag_data["surveillance_station"]["cameras"][camera.id] = { + diag_data["surveillance_station"]["cameras"][camera.id] = { # type: ignore[index] "name": camera.name, "is_enabled": camera.is_enabled, "is_motion_detection_enabled": camera.is_motion_detection_enabled, diff --git a/homeassistant/components/synology_dsm/models.py b/homeassistant/components/synology_dsm/models.py new file mode 100644 index 00000000000..8c4341a2d37 --- /dev/null +++ b/homeassistant/components/synology_dsm/models.py @@ -0,0 +1,21 @@ +"""The synology_dsm integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from .common import SynoApi +from .coordinator import ( + SynologyDSMCameraUpdateCoordinator, + SynologyDSMCentralUpdateCoordinator, + SynologyDSMSwitchUpdateCoordinator, +) + + +@dataclass +class SynologyDSMData: + """Data for the synology_dsm integration.""" + + api: SynoApi + coordinator_central: SynologyDSMCentralUpdateCoordinator + coordinator_cameras: SynologyDSMCameraUpdateCoordinator | None + coordinator_switches: SynologyDSMSwitchUpdateCoordinator | None diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 6015dc689b7..6a2a92b9fd5 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -31,12 +31,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow from . import SynoApi -from .const import CONF_VOLUMES, COORDINATOR_CENTRAL, DOMAIN, ENTITY_UNIT_LOAD, SYNO_API +from .const import CONF_VOLUMES, DOMAIN, ENTITY_UNIT_LOAD from .entity import ( SynologyDSMBaseEntity, SynologyDSMDeviceEntity, SynologyDSMEntityDescription, ) +from .models import SynologyDSMData @dataclass @@ -279,10 +280,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS Sensor.""" - - data = hass.data[DOMAIN][entry.unique_id] - api: SynoApi = data[SYNO_API] - coordinator = data[COORDINATOR_CENTRAL] + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + api = data.api + coordinator = data.coordinator_central entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [ SynoDSMUtilSensor(api, coordinator, description) diff --git a/homeassistant/components/synology_dsm/service.py b/homeassistant/components/synology_dsm/service.py index 130ad110b46..0cb2bf7d822 100644 --- a/homeassistant/components/synology_dsm/service.py +++ b/homeassistant/components/synology_dsm/service.py @@ -7,15 +7,8 @@ from synology_dsm.exceptions import SynologyDSMException from homeassistant.core import HomeAssistant, ServiceCall -from .common import SynoApi -from .const import ( - CONF_SERIAL, - DOMAIN, - SERVICE_REBOOT, - SERVICE_SHUTDOWN, - SERVICES, - SYNO_API, -) +from .const import CONF_SERIAL, DOMAIN, SERVICE_REBOOT, SERVICE_SHUTDOWN, SERVICES +from .models import SynologyDSMData LOGGER = logging.getLogger(__name__) @@ -29,7 +22,7 @@ async def async_setup_services(hass: HomeAssistant) -> None: dsm_devices = hass.data[DOMAIN] if serial: - dsm_device = dsm_devices.get(serial) + dsm_device: SynologyDSMData = hass.data[DOMAIN][serial] elif len(dsm_devices) == 1: dsm_device = next(iter(dsm_devices.values())) serial = next(iter(dsm_devices)) @@ -45,7 +38,7 @@ async def async_setup_services(hass: HomeAssistant) -> None: return if call.service in [SERVICE_REBOOT, SERVICE_SHUTDOWN]: - if not (dsm_device := hass.data[DOMAIN].get(serial)): + if serial not in hass.data[DOMAIN]: LOGGER.error("DSM with specified serial %s not found", serial) return LOGGER.debug("%s DSM with serial %s", call.service, serial) @@ -53,7 +46,8 @@ async def async_setup_services(hass: HomeAssistant) -> None: "The %s service is deprecated and will be removed in future release. Please use the corresponding button entity", call.service, ) - dsm_api: SynoApi = dsm_device[SYNO_API] + dsm_device = hass.data[DOMAIN][serial] + dsm_api = dsm_device.api try: await getattr(dsm_api, f"async_{call.service}")() except SynologyDSMException as ex: diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index eb61b8334ca..26909ceddd9 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -15,8 +15,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi -from .const import COORDINATOR_SWITCHES, DOMAIN, SYNO_API +from .const import DOMAIN from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription +from .models import SynologyDSMData _LOGGER = logging.getLogger(__name__) @@ -42,30 +43,16 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS switch.""" - - data = hass.data[DOMAIN][entry.unique_id] - api: SynoApi = data[SYNO_API] - - entities = [] - - if SynoSurveillanceStation.INFO_API_KEY in api.dsm.apis: - info = await hass.async_add_executor_job(api.dsm.surveillance_station.get_info) - version = info["data"]["CMSMinVersion"] - - # initial data fetch - coordinator: DataUpdateCoordinator = data[COORDINATOR_SWITCHES] - await coordinator.async_refresh() - entities.extend( - [ - SynoDSMSurveillanceHomeModeToggle( - api, version, coordinator, description - ) - for description in SURVEILLANCE_SWITCH - ] + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + if coordinator := data.coordinator_switches: + assert coordinator.version is not None + async_add_entities( + SynoDSMSurveillanceHomeModeToggle( + data.api, coordinator.version, coordinator, description + ) + for description in SURVEILLANCE_SWITCH ) - async_add_entities(entities, True) - class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, SwitchEntity): """Representation a Synology Surveillance Station Home Mode toggle.""" diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index 48b3eeca2ed..d3f3cc56eac 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -13,9 +13,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SynoApi -from .const import COORDINATOR_CENTRAL, DOMAIN, SYNO_API +from .const import DOMAIN from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription +from .models import SynologyDSMData @dataclass @@ -39,12 +39,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Synology DSM update entities.""" - data = hass.data[DOMAIN][entry.unique_id] - api: SynoApi = data[SYNO_API] - coordinator = data[COORDINATOR_CENTRAL] - + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] async_add_entities( - SynoDSMUpdateEntity(api, coordinator, description) + SynoDSMUpdateEntity(data.api, data.coordinator_central, description) for description in UPDATE_ENTITIES ) diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index 4d6708a2e79..db373f41656 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -24,9 +24,9 @@ from tests.common import MockConfigEntry @pytest.mark.no_bypass_setup async def test_services_registered(hass: HomeAssistant): """Test if all services are registered.""" - with patch( - "homeassistant.components.synology_dsm.SynoApi.async_setup", return_value=True - ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + with patch("homeassistant.components.synology_dsm.common.SynologyDSM"), patch( + "homeassistant.components.synology_dsm.PLATFORMS", return_value=[] + ): entry = MockConfigEntry( domain=DOMAIN, data={