diff --git a/.coveragerc b/.coveragerc index ca7251cc2c0..70eae1eff5a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1102,9 +1102,12 @@ omit = homeassistant/components/synology_chat/notify.py homeassistant/components/synology_dsm/__init__.py homeassistant/components/synology_dsm/binary_sensor.py + homeassistant/components/synology_dsm/button.py homeassistant/components/synology_dsm/camera.py homeassistant/components/synology_dsm/diagnostics.py + homeassistant/components/synology_dsm/common.py homeassistant/components/synology_dsm/sensor.py + homeassistant/components/synology_dsm/service.py homeassistant/components/synology_dsm/switch.py homeassistant/components/synology_srm/device_tracker.py homeassistant/components/syslog/notify.py diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 6e90233b375..e07df808198 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -1,20 +1,11 @@ """The Synology DSM component.""" from __future__ import annotations -from collections.abc import Callable from datetime import timedelta import logging from typing import Any import async_timeout -from synology_dsm import SynologyDSM -from synology_dsm.api.core.security import SynoCoreSecurity -from synology_dsm.api.core.system import SynoCoreSystem -from synology_dsm.api.core.upgrade import SynoCoreUpgrade -from synology_dsm.api.core.utilization import SynoCoreUtilization -from synology_dsm.api.dsm.information import SynoDSMInformation -from synology_dsm.api.dsm.network import SynoDSMNetwork -from synology_dsm.api.storage.storage import SynoStorage from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.api.surveillance_station.camera import SynoCamera from synology_dsm.exceptions import ( @@ -28,18 +19,8 @@ from synology_dsm.exceptions import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MAC, - CONF_PASSWORD, - CONF_PORT, - CONF_SCAN_INTERVAL, - CONF_SSL, - CONF_TIMEOUT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.const import CONF_MAC, CONF_SCAN_INTERVAL, CONF_VERIFY_SSL +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry import homeassistant.helpers.config_validation as cv @@ -54,9 +35,8 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) +from .common import SynoApi from .const import ( - CONF_DEVICE_TOKEN, - CONF_SERIAL, COORDINATOR_CAMERAS, COORDINATOR_CENTRAL, COORDINATOR_SWITCHES, @@ -66,14 +46,12 @@ from .const import ( EXCEPTION_DETAILS, EXCEPTION_UNKNOWN, PLATFORMS, - SERVICE_REBOOT, - SERVICE_SHUTDOWN, - SERVICES, SYNO_API, SYSTEM_LOADED, UNDO_UPDATE_LISTENER, SynologyDSMEntityDescription, ) +from .service import async_setup_services CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -141,7 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } # Services - await _async_setup_services(hass) + await async_setup_services(hass) # For SSDP compat if not entry.data.get(CONF_MAC): @@ -249,279 +227,6 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non await hass.config_entries.async_reload(entry.entry_id) -async def _async_setup_services(hass: HomeAssistant) -> None: - """Service handler setup.""" - - async def service_handler(call: ServiceCall) -> None: - """Handle service call.""" - serial = call.data.get(CONF_SERIAL) - dsm_devices = hass.data[DOMAIN] - - if serial: - dsm_device = dsm_devices.get(serial) - elif len(dsm_devices) == 1: - dsm_device = next(iter(dsm_devices.values())) - serial = next(iter(dsm_devices)) - else: - _LOGGER.error( - "More than one DSM configured, must specify one of serials %s", - sorted(dsm_devices), - ) - return - - if not dsm_device: - _LOGGER.error("DSM with specified serial %s not found", serial) - return - - _LOGGER.debug("%s DSM with serial %s", call.service, serial) - dsm_api = dsm_device[SYNO_API] - dsm_device[SYSTEM_LOADED] = False - if call.service == SERVICE_REBOOT: - await dsm_api.async_reboot() - elif call.service == SERVICE_SHUTDOWN: - await dsm_api.async_shutdown() - - for service in SERVICES: - hass.services.async_register(DOMAIN, service, service_handler) - - -class SynoApi: - """Class to interface with Synology DSM API.""" - - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: - """Initialize the API wrapper class.""" - self._hass = hass - self._entry = entry - if entry.data.get(CONF_SSL): - self.config_url = f"https://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" - else: - self.config_url = f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" - - self.initialized = False - # DSM APIs - self.dsm: SynologyDSM = None - self.information: SynoDSMInformation = None - self.network: SynoDSMNetwork = None - self.security: SynoCoreSecurity = None - self.storage: SynoStorage = None - self.surveillance_station: SynoSurveillanceStation = None - self.system: SynoCoreSystem = None - self.upgrade: SynoCoreUpgrade = None - self.utilisation: SynoCoreUtilization = None - - # Should we fetch them - self._fetching_entities: dict[str, set[str]] = {} - self._with_information = True - self._with_security = True - self._with_storage = True - self._with_surveillance_station = True - self._with_system = True - self._with_upgrade = True - self._with_utilisation = True - - async def async_setup(self) -> None: - """Start interacting with the NAS.""" - self.dsm = SynologyDSM( - self._entry.data[CONF_HOST], - self._entry.data[CONF_PORT], - self._entry.data[CONF_USERNAME], - self._entry.data[CONF_PASSWORD], - self._entry.data[CONF_SSL], - self._entry.data[CONF_VERIFY_SSL], - timeout=self._entry.options.get(CONF_TIMEOUT), - device_token=self._entry.data.get(CONF_DEVICE_TOKEN), - ) - await self._hass.async_add_executor_job(self.dsm.login) - - # check if surveillance station is used - self._with_surveillance_station = bool( - self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) - ) - _LOGGER.debug( - "State of Surveillance_station during setup of '%s': %s", - self._entry.unique_id, - self._with_surveillance_station, - ) - - self._async_setup_api_requests() - - await self._hass.async_add_executor_job(self._fetch_device_configuration) - await self.async_update() - self.initialized = True - - @callback - def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: - """Subscribe an entity to API fetches.""" - _LOGGER.debug("Subscribe new entity: %s", unique_id) - if api_key not in self._fetching_entities: - self._fetching_entities[api_key] = set() - self._fetching_entities[api_key].add(unique_id) - - @callback - def unsubscribe() -> None: - """Unsubscribe an entity from API fetches (when disable).""" - _LOGGER.debug("Unsubscribe entity: %s", unique_id) - self._fetching_entities[api_key].remove(unique_id) - if len(self._fetching_entities[api_key]) == 0: - self._fetching_entities.pop(api_key) - - return unsubscribe - - @callback - def _async_setup_api_requests(self) -> None: - """Determine if we should fetch each API, if one entity needs it.""" - # Entities not added yet, fetch all - if not self._fetching_entities: - _LOGGER.debug( - "Entities not added yet, fetch all for '%s'", self._entry.unique_id - ) - return - - # surveillance_station is updated by own coordinator - self.dsm.reset(self.surveillance_station) - - # Determine if we should fetch an API - self._with_system = bool(self.dsm.apis.get(SynoCoreSystem.API_KEY)) - self._with_security = bool( - self._fetching_entities.get(SynoCoreSecurity.API_KEY) - ) - self._with_storage = bool(self._fetching_entities.get(SynoStorage.API_KEY)) - self._with_upgrade = bool(self._fetching_entities.get(SynoCoreUpgrade.API_KEY)) - self._with_utilisation = bool( - self._fetching_entities.get(SynoCoreUtilization.API_KEY) - ) - self._with_information = bool( - self._fetching_entities.get(SynoDSMInformation.API_KEY) - ) - - # Reset not used API, information is not reset since it's used in device_info - if not self._with_security: - _LOGGER.debug( - "Disable security api from being updated for '%s'", - self._entry.unique_id, - ) - self.dsm.reset(self.security) - self.security = None - - if not self._with_storage: - _LOGGER.debug( - "Disable storage api from being updatedf or '%s'", self._entry.unique_id - ) - self.dsm.reset(self.storage) - self.storage = None - - if not self._with_system: - _LOGGER.debug( - "Disable system api from being updated for '%s'", self._entry.unique_id - ) - self.dsm.reset(self.system) - self.system = None - - if not self._with_upgrade: - _LOGGER.debug( - "Disable upgrade api from being updated for '%s'", self._entry.unique_id - ) - self.dsm.reset(self.upgrade) - self.upgrade = None - - if not self._with_utilisation: - _LOGGER.debug( - "Disable utilisation api from being updated for '%s'", - self._entry.unique_id, - ) - self.dsm.reset(self.utilisation) - self.utilisation = None - - def _fetch_device_configuration(self) -> None: - """Fetch initial device config.""" - self.information = self.dsm.information - self.network = self.dsm.network - self.network.update() - - if self._with_security: - _LOGGER.debug("Enable security api updates for '%s'", self._entry.unique_id) - self.security = self.dsm.security - - if self._with_storage: - _LOGGER.debug("Enable storage api updates for '%s'", self._entry.unique_id) - self.storage = self.dsm.storage - - if self._with_upgrade: - _LOGGER.debug("Enable upgrade api updates for '%s'", self._entry.unique_id) - self.upgrade = self.dsm.upgrade - - if self._with_system: - _LOGGER.debug("Enable system api updates for '%s'", self._entry.unique_id) - self.system = self.dsm.system - - if self._with_utilisation: - _LOGGER.debug( - "Enable utilisation api updates for '%s'", self._entry.unique_id - ) - self.utilisation = self.dsm.utilisation - - if self._with_surveillance_station: - _LOGGER.debug( - "Enable surveillance_station api updates for '%s'", - self._entry.unique_id, - ) - self.surveillance_station = self.dsm.surveillance_station - - async def async_reboot(self) -> None: - """Reboot NAS.""" - try: - await self._hass.async_add_executor_job(self.system.reboot) - except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - _LOGGER.error( - "Reboot of '%s' not possible, please try again later", - self._entry.unique_id, - ) - _LOGGER.debug("Exception:%s", err) - - async def async_shutdown(self) -> None: - """Shutdown NAS.""" - try: - await self._hass.async_add_executor_job(self.system.shutdown) - except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - _LOGGER.error( - "Shutdown of '%s' not possible, please try again later", - self._entry.unique_id, - ) - _LOGGER.debug("Exception:%s", err) - - async def async_unload(self) -> None: - """Stop interacting with the NAS and prepare for removal from hass.""" - try: - await self._hass.async_add_executor_job(self.dsm.logout) - except (SynologyDSMAPIErrorException, SynologyDSMRequestException) as err: - _LOGGER.debug( - "Logout from '%s' not possible:%s", self._entry.unique_id, err - ) - - async def async_update(self, now: timedelta | None = None) -> None: - """Update function for updating API information.""" - _LOGGER.debug("Start data update for '%s'", self._entry.unique_id) - self._async_setup_api_requests() - try: - await self._hass.async_add_executor_job( - self.dsm.update, self._with_information - ) - except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - if not self.initialized: - raise err - - _LOGGER.warning( - "Connection error during update, fallback by reloading the entry" - ) - _LOGGER.debug( - "Connection error during update of '%s' with exception: %s", - self._entry.unique_id, - err, - ) - await self._hass.config_entries.async_reload(self._entry.entry_id) - return - - class SynologyDSMBaseEntity(CoordinatorEntity): """Representation of a Synology NAS entry.""" diff --git a/homeassistant/components/synology_dsm/button.py b/homeassistant/components/synology_dsm/button.py new file mode 100644 index 00000000000..58f1a0dfdd7 --- /dev/null +++ b/homeassistant/components/synology_dsm/button.py @@ -0,0 +1,96 @@ +"""Support for Synology DSM buttons.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging +from typing import Any, Final + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import SynoApi +from .const import DOMAIN, SYNO_API + +LOGGER = logging.getLogger(__name__) + + +@dataclass +class SynologyDSMbuttonDescriptionMixin: + """Mixin to describe a Synology DSM button entity.""" + + press_action: Callable[[SynoApi], Any] + + +@dataclass +class SynologyDSMbuttonDescription( + ButtonEntityDescription, SynologyDSMbuttonDescriptionMixin +): + """Class to describe a Synology DSM button entity.""" + + +BUTTONS: Final = [ + SynologyDSMbuttonDescription( + key="reboot", + name="Reboot", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + press_action=lambda syno_api: syno_api.async_reboot(), + ), + SynologyDSMbuttonDescription( + key="shutdown", + name="Shutdown", + icon="mdi:power", + entity_category=EntityCategory.CONFIG, + press_action=lambda syno_api: syno_api.async_shutdown(), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + 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) + + +class SynologyDSMButton(ButtonEntity): + """Defines a Synology DSM button.""" + + entity_description: SynologyDSMbuttonDescription + + def __init__( + self, + api: SynoApi, + description: SynologyDSMbuttonDescription, + ) -> None: + """Initialize the Synology DSM binary_sensor entity.""" + self.entity_description = description + self.syno_api = api + + self._attr_name = f"{api.network.hostname} {description.name}" + self._attr_unique_id = f"{api.information.serial}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, api.information.serial)} + ) + + async def async_press(self) -> None: + """Triggers the Synology DSM button press service.""" + LOGGER.debug( + "Trigger %s for %s", + self.entity_description.key, + self.syno_api.network.hostname, + ) + await self.entity_description.press_action(self.syno_api) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py new file mode 100644 index 00000000000..273c9cc6a42 --- /dev/null +++ b/homeassistant/components/synology_dsm/common.py @@ -0,0 +1,260 @@ +"""The Synology DSM component.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import timedelta +import logging + +from synology_dsm import SynologyDSM +from synology_dsm.api.core.security import SynoCoreSecurity +from synology_dsm.api.core.system import SynoCoreSystem +from synology_dsm.api.core.upgrade import SynoCoreUpgrade +from synology_dsm.api.core.utilization import SynoCoreUtilization +from synology_dsm.api.dsm.information import SynoDSMInformation +from synology_dsm.api.dsm.network import SynoDSMNetwork +from synology_dsm.api.storage.storage import SynoStorage +from synology_dsm.api.surveillance_station import SynoSurveillanceStation +from synology_dsm.exceptions import ( + SynologyDSMAPIErrorException, + SynologyDSMLoginFailedException, + SynologyDSMRequestException, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_TIMEOUT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant, callback + +from .const import CONF_DEVICE_TOKEN + +LOGGER = logging.getLogger(__name__) + + +class SynoApi: + """Class to interface with Synology DSM API.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the API wrapper class.""" + self._hass = hass + self._entry = entry + if entry.data.get(CONF_SSL): + self.config_url = f"https://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" + else: + self.config_url = f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" + + # DSM APIs + self.dsm: SynologyDSM = None + self.information: SynoDSMInformation = None + self.network: SynoDSMNetwork = None + self.security: SynoCoreSecurity = None + self.storage: SynoStorage = None + self.surveillance_station: SynoSurveillanceStation = None + self.system: SynoCoreSystem = None + self.upgrade: SynoCoreUpgrade = None + self.utilisation: SynoCoreUtilization = None + + # Should we fetch them + self._fetching_entities: dict[str, set[str]] = {} + self._with_information = True + self._with_security = True + self._with_storage = True + self._with_surveillance_station = True + self._with_system = True + self._with_upgrade = True + self._with_utilisation = True + + async def async_setup(self) -> None: + """Start interacting with the NAS.""" + self.dsm = SynologyDSM( + self._entry.data[CONF_HOST], + self._entry.data[CONF_PORT], + self._entry.data[CONF_USERNAME], + self._entry.data[CONF_PASSWORD], + self._entry.data[CONF_SSL], + self._entry.data[CONF_VERIFY_SSL], + timeout=self._entry.options.get(CONF_TIMEOUT), + device_token=self._entry.data.get(CONF_DEVICE_TOKEN), + ) + await self._hass.async_add_executor_job(self.dsm.login) + + # check if surveillance station is used + self._with_surveillance_station = bool( + self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) + ) + LOGGER.debug( + "State of Surveillance_station during setup of '%s': %s", + self._entry.unique_id, + self._with_surveillance_station, + ) + + self._async_setup_api_requests() + + await self._hass.async_add_executor_job(self._fetch_device_configuration) + await self.async_update() + + @callback + def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: + """Subscribe an entity to API fetches.""" + LOGGER.debug("Subscribe new entity: %s", unique_id) + if api_key not in self._fetching_entities: + self._fetching_entities[api_key] = set() + self._fetching_entities[api_key].add(unique_id) + + @callback + def unsubscribe() -> None: + """Unsubscribe an entity from API fetches (when disable).""" + LOGGER.debug("Unsubscribe entity: %s", unique_id) + self._fetching_entities[api_key].remove(unique_id) + if len(self._fetching_entities[api_key]) == 0: + self._fetching_entities.pop(api_key) + + return unsubscribe + + @callback + def _async_setup_api_requests(self) -> None: + """Determine if we should fetch each API, if one entity needs it.""" + # Entities not added yet, fetch all + if not self._fetching_entities: + LOGGER.debug( + "Entities not added yet, fetch all for '%s'", self._entry.unique_id + ) + return + + # surveillance_station is updated by own coordinator + self.dsm.reset(self.surveillance_station) + + # Determine if we should fetch an API + self._with_system = bool(self.dsm.apis.get(SynoCoreSystem.API_KEY)) + self._with_security = bool( + self._fetching_entities.get(SynoCoreSecurity.API_KEY) + ) + self._with_storage = bool(self._fetching_entities.get(SynoStorage.API_KEY)) + self._with_upgrade = bool(self._fetching_entities.get(SynoCoreUpgrade.API_KEY)) + self._with_utilisation = bool( + self._fetching_entities.get(SynoCoreUtilization.API_KEY) + ) + self._with_information = bool( + self._fetching_entities.get(SynoDSMInformation.API_KEY) + ) + + # Reset not used API, information is not reset since it's used in device_info + if not self._with_security: + LOGGER.debug( + "Disable security api from being updated for '%s'", + self._entry.unique_id, + ) + self.dsm.reset(self.security) + self.security = None + + if not self._with_storage: + LOGGER.debug( + "Disable storage api from being updatedf or '%s'", self._entry.unique_id + ) + self.dsm.reset(self.storage) + self.storage = None + + if not self._with_system: + LOGGER.debug( + "Disable system api from being updated for '%s'", self._entry.unique_id + ) + self.dsm.reset(self.system) + self.system = None + + if not self._with_upgrade: + LOGGER.debug( + "Disable upgrade api from being updated for '%s'", self._entry.unique_id + ) + self.dsm.reset(self.upgrade) + self.upgrade = None + + if not self._with_utilisation: + LOGGER.debug( + "Disable utilisation api from being updated for '%s'", + self._entry.unique_id, + ) + self.dsm.reset(self.utilisation) + self.utilisation = None + + def _fetch_device_configuration(self) -> None: + """Fetch initial device config.""" + self.information = self.dsm.information + self.network = self.dsm.network + self.network.update() + + if self._with_security: + LOGGER.debug("Enable security api updates for '%s'", self._entry.unique_id) + self.security = self.dsm.security + + if self._with_storage: + LOGGER.debug("Enable storage api updates for '%s'", self._entry.unique_id) + self.storage = self.dsm.storage + + if self._with_upgrade: + LOGGER.debug("Enable upgrade api updates for '%s'", self._entry.unique_id) + self.upgrade = self.dsm.upgrade + + if self._with_system: + LOGGER.debug("Enable system api updates for '%s'", self._entry.unique_id) + self.system = self.dsm.system + + if self._with_utilisation: + LOGGER.debug( + "Enable utilisation api updates for '%s'", self._entry.unique_id + ) + self.utilisation = self.dsm.utilisation + + if self._with_surveillance_station: + LOGGER.debug( + "Enable surveillance_station api updates for '%s'", + self._entry.unique_id, + ) + self.surveillance_station = self.dsm.surveillance_station + + async def _syno_api_executer(self, api_call: Callable) -> None: + """Synology api call wrapper.""" + try: + await self._hass.async_add_executor_job(api_call) + except (SynologyDSMAPIErrorException, SynologyDSMRequestException) as err: + LOGGER.debug( + "Error from '%s': %s", self._entry.unique_id, err, exc_info=True + ) + raise err + + async def async_reboot(self) -> None: + """Reboot NAS.""" + await self._syno_api_executer(self.system.reboot) + + async def async_shutdown(self) -> None: + """Shutdown NAS.""" + await self._syno_api_executer(self.system.shutdown) + + async def async_unload(self) -> None: + """Stop interacting with the NAS and prepare for removal from hass.""" + await self._syno_api_executer(self.dsm.logout) + + async def async_update(self, now: timedelta | None = None) -> None: + """Update function for updating API information.""" + LOGGER.debug("Start data update for '%s'", self._entry.unique_id) + self._async_setup_api_requests() + try: + await self._hass.async_add_executor_job( + self.dsm.update, self._with_information + ) + except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: + LOGGER.warning( + "Connection error during update, fallback by reloading the entry" + ) + LOGGER.debug( + "Connection error during update of '%s' with exception: %s", + self._entry.unique_id, + err, + ) + await self._hass.config_entries.async_reload(self._entry.entry_id) + return diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 18707c24f95..55e83dc52bf 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -32,7 +32,13 @@ from homeassistant.const import ( from homeassistant.helpers.entity import EntityCategory, EntityDescription DOMAIN = "synology_dsm" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.SENSOR, + Platform.SWITCH, +] COORDINATOR_CAMERAS = "coordinator_cameras" COORDINATOR_CENTRAL = "coordinator_central" COORDINATOR_SWITCHES = "coordinator_switches" diff --git a/homeassistant/components/synology_dsm/service.py b/homeassistant/components/synology_dsm/service.py new file mode 100644 index 00000000000..f26a43b2ca0 --- /dev/null +++ b/homeassistant/components/synology_dsm/service.py @@ -0,0 +1,72 @@ +"""The Synology DSM component.""" +from __future__ import annotations + +import logging + +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, + SYSTEM_LOADED, +) + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_services(hass: HomeAssistant) -> None: + """Service handler setup.""" + + async def service_handler(call: ServiceCall) -> None: + """Handle service call.""" + serial = call.data.get(CONF_SERIAL) + dsm_devices = hass.data[DOMAIN] + + if serial: + dsm_device = dsm_devices.get(serial) + elif len(dsm_devices) == 1: + dsm_device = next(iter(dsm_devices.values())) + serial = next(iter(dsm_devices)) + else: + LOGGER.error( + "More than one DSM configured, must specify one of serials %s", + sorted(dsm_devices), + ) + return + + if not dsm_device: + LOGGER.error("DSM with specified serial %s not found", serial) + return + + if call.service in [SERVICE_REBOOT, SERVICE_SHUTDOWN]: + dsm_device = hass.data[DOMAIN].get(serial) + if not dsm_device: + LOGGER.error("DSM with specified serial %s not found", serial) + return + LOGGER.debug("%s DSM with serial %s", call.service, serial) + LOGGER.warning( + "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] + try: + dsm_device[SYSTEM_LOADED] = False + await getattr(dsm_api, f"async_{call.service}")() + except SynologyDSMException as ex: + LOGGER.error( + "%s of DSM with serial %s not possible, because of %s", + call.service, + serial, + ex, + ) + return + + for service in SERVICES: + hass.services.async_register(DOMAIN, service, service_handler) diff --git a/homeassistant/components/synology_dsm/services.yaml b/homeassistant/components/synology_dsm/services.yaml index 3e25d4bef9d..245d45fc800 100644 --- a/homeassistant/components/synology_dsm/services.yaml +++ b/homeassistant/components/synology_dsm/services.yaml @@ -2,7 +2,7 @@ reboot: name: Reboot - description: Reboot the NAS. + description: Reboot the NAS. This service is deprecated and will be removed in future release. Please use the corresponding button entity. fields: serial: name: Serial @@ -13,7 +13,7 @@ reboot: shutdown: name: Shutdown - description: Shutdown the NAS. + description: Shutdown the NAS. This service is deprecated and will be removed in future release. Please use the corresponding button entity. fields: serial: name: Serial