diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index ec93c92a698..ec13ec929a5 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -11,10 +11,10 @@ from synology_dsm.api.surveillance_station.camera import SynoCamera from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr -from .common import SynoApi +from .common import SynoApi, raise_config_entry_auth_error from .const import ( DEFAULT_VERIFY_SSL, DOMAIN, @@ -68,11 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await api.async_setup() 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 + raise_config_entry_auth_error(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) @@ -147,8 +143,10 @@ async def async_remove_config_entry_device( """Remove synology_dsm config entry from a device.""" data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] api = data.api + assert api.information is not None serial = api.information.serial storage = api.storage + assert storage is not None all_cameras: list[SynoCamera] = [] if api.surveillance_station is not None: # get_all_cameras does not do I/O diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 28dc750bc91..b9c7ff483ea 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -69,6 +69,7 @@ async def async_setup_entry( data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] api = data.api coordinator = data.coordinator_central + assert api.storage is not None entities: list[SynoDSMSecurityBinarySensor | SynoDSMStorageBinarySensor] = [ SynoDSMSecurityBinarySensor(api, coordinator, description) @@ -121,7 +122,8 @@ class SynoDSMSecurityBinarySensor(SynoDSMBinarySensor): @property def extra_state_attributes(self) -> dict[str, str]: """Return security checks details.""" - return self._api.security.status_by_check # type: ignore[no-any-return] + assert self._api.security is not None + return self._api.security.status_by_check class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, SynoDSMBinarySensor): diff --git a/homeassistant/components/synology_dsm/button.py b/homeassistant/components/synology_dsm/button.py index 529682b4c6e..fccd0860036 100644 --- a/homeassistant/components/synology_dsm/button.py +++ b/homeassistant/components/synology_dsm/button.py @@ -73,7 +73,8 @@ class SynologyDSMButton(ButtonEntity): """Initialize the Synology DSM binary_sensor entity.""" self.entity_description = description self.syno_api = api - + assert api.network is not None + assert api.information is not None self._attr_name = f"{api.network.hostname} {description.name}" self._attr_unique_id = f"{api.information.serial}_{description.key}" self._attr_device_info = DeviceInfo( @@ -82,6 +83,7 @@ class SynologyDSMButton(ButtonEntity): async def async_press(self) -> None: """Triggers the Synology DSM button press service.""" + assert self.syno_api.network is not None LOGGER.debug( "Trigger %s for %s", self.entity_description.key, diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 82d15138f05..901fcb1d565 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -42,6 +42,8 @@ class SynologyDSMCameraEntityDescription( ): """Describes Synology DSM camera entity.""" + camera_id: int + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -65,12 +67,13 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C self, api: SynoApi, coordinator: SynologyDSMCameraUpdateCoordinator, - camera_id: str, + camera_id: int, ) -> None: """Initialize a Synology camera.""" description = SynologyDSMCameraEntityDescription( api_key=SynoSurveillanceStation.CAMERA_API_KEY, - key=camera_id, + key=str(camera_id), + camera_id=camera_id, name=coordinator.data["cameras"][camera_id].name, entity_registry_enabled_default=coordinator.data["cameras"][ camera_id @@ -85,23 +88,20 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C @property def camera_data(self) -> SynoCamera: """Camera data.""" - return self.coordinator.data["cameras"][self.entity_description.key] + return self.coordinator.data["cameras"][self.entity_description.camera_id] @property def device_info(self) -> DeviceInfo: """Return the device information.""" + information = self._api.information + assert information is not None return DeviceInfo( - identifiers={ - ( - DOMAIN, - f"{self._api.information.serial}_{self.camera_data.id}", - ) - }, + identifiers={(DOMAIN, f"{information.serial}_{self.camera_data.id}")}, name=self.camera_data.name, model=self.camera_data.model, via_device=( DOMAIN, - f"{self._api.information.serial}_{SynoSurveillanceStation.INFO_API_KEY}", + f"{information.serial}_{SynoSurveillanceStation.INFO_API_KEY}", ), ) @@ -113,12 +113,12 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C @property def is_recording(self) -> bool: """Return true if the device is recording.""" - return self.camera_data.is_recording # type: ignore[no-any-return] + return self.camera_data.is_recording @property def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" - return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] + return bool(self.camera_data.is_motion_detection_enabled) def _listen_source_updates(self) -> None: """Listen for camera source changed events.""" @@ -153,9 +153,10 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C ) if not self.available: return None + assert self._api.surveillance_station is not None try: - return await self._api.surveillance_station.get_camera_image( # type: ignore[no-any-return] - self.entity_description.key, self.snapshot_quality + return await self._api.surveillance_station.get_camera_image( + self.entity_description.camera_id, self.snapshot_quality ) except ( SynologyDSMAPIErrorException, @@ -178,7 +179,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C if not self.available: return None - return self.camera_data.live_view.rtsp # type: ignore[no-any-return] + return self.camera_data.live_view.rtsp async def async_enable_motion_detection(self) -> None: """Enable motion detection in the camera.""" @@ -186,8 +187,9 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C "SynoDSMCamera.enable_motion_detection(%s)", self.camera_data.name, ) + assert self._api.surveillance_station is not None await self._api.surveillance_station.enable_motion_detection( - self.entity_description.key + self.entity_description.camera_id ) async def async_disable_motion_detection(self) -> None: @@ -196,6 +198,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C "SynoDSMCamera.disable_motion_detection(%s)", self.camera_data.name, ) + assert self._api.surveillance_station is not None await self._api.surveillance_station.disable_motion_detection( - self.entity_description.key + self.entity_description.camera_id ) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 42ec45e94a4..04e8ae29ceb 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -33,9 +33,15 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_DEVICE_TOKEN, SYNOLOGY_CONNECTION_EXCEPTIONS +from .const import ( + CONF_DEVICE_TOKEN, + EXCEPTION_DETAILS, + EXCEPTION_UNKNOWN, + SYNOLOGY_CONNECTION_EXCEPTIONS, +) LOGGER = logging.getLogger(__name__) @@ -43,6 +49,8 @@ LOGGER = logging.getLogger(__name__) class SynoApi: """Class to interface with Synology DSM API.""" + dsm: SynologyDSM + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the API wrapper class.""" self._hass = hass @@ -53,16 +61,15 @@ class SynoApi: 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.photos: SynoPhotos = None - self.surveillance_station: SynoSurveillanceStation = None - self.system: SynoCoreSystem = None - self.upgrade: SynoCoreUpgrade = None - self.utilisation: SynoCoreUtilization = None + self.information: SynoDSMInformation | None = None + self.network: SynoDSMNetwork | None = None + self.security: SynoCoreSecurity | None = None + self.storage: SynoStorage | None = None + self.photos: SynoPhotos | None = None + self.surveillance_station: SynoSurveillanceStation | None = None + self.system: SynoCoreSystem | None = None + self.upgrade: SynoCoreUpgrade | None = None + self.utilisation: SynoCoreUtilization | None = None # Should we fetch them self._fetching_entities: dict[str, set[str]] = {} @@ -85,7 +92,7 @@ class SynoApi: self._entry.data[CONF_USERNAME], self._entry.data[CONF_PASSWORD], self._entry.data[CONF_SSL], - timeout=self._entry.options.get(CONF_TIMEOUT), + timeout=self._entry.options.get(CONF_TIMEOUT) or 10, device_token=self._entry.data.get(CONF_DEVICE_TOKEN), ) await self.dsm.login() @@ -159,7 +166,8 @@ class SynoApi: return # surveillance_station is updated by own coordinator - self.dsm.reset(self.surveillance_station) + if self.surveillance_station: + self.dsm.reset(self.surveillance_station) # Determine if we should fetch an API self._with_system = bool(self.dsm.apis.get(SynoCoreSystem.API_KEY)) @@ -182,35 +190,40 @@ class SynoApi: "Disable security api from being updated for '%s'", self._entry.unique_id, ) - self.dsm.reset(self.security) + if self.security: + self.dsm.reset(self.security) self.security = None if not self._with_photos: LOGGER.debug( "Disable photos api from being updated or '%s'", self._entry.unique_id ) - self.dsm.reset(self.photos) + if self.photos: + self.dsm.reset(self.photos) self.photos = 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) + if self.storage: + 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) + if self.system: + 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) + if self.upgrade: + self.dsm.reset(self.upgrade) self.upgrade = None if not self._with_utilisation: @@ -218,7 +231,8 @@ class SynoApi: "Disable utilisation api from being updated for '%s'", self._entry.unique_id, ) - self.dsm.reset(self.utilisation) + if self.utilisation: + self.dsm.reset(self.utilisation) self.utilisation = None async def _fetch_device_configuration(self) -> None: @@ -272,11 +286,13 @@ class SynoApi: async def async_reboot(self) -> None: """Reboot NAS.""" - await self._syno_api_executer(self.system.reboot) + if self.system: + await self._syno_api_executer(self.system.reboot) async def async_shutdown(self) -> None: """Shutdown NAS.""" - await self._syno_api_executer(self.system.shutdown) + if self.system: + 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.""" @@ -293,3 +309,12 @@ class SynoApi: LOGGER.debug("Start data update for '%s'", self._entry.unique_id) self._setup_api_requests() await self.dsm.update(self._with_information) + + +def raise_config_entry_auth_error(err: Exception) -> None: + """Raise ConfigEntryAuthFailed if error is related to authentication.""" + 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 diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index c77b8196faf..785baa50b29 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -425,7 +425,7 @@ async def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str | None) -> ): raise InvalidData - return api.information.serial # type: ignore[no-any-return] + return api.information.serial class InvalidData(HomeAssistantError): diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py index bc896b1ad45..34886828a58 100644 --- a/homeassistant/components/synology_dsm/coordinator.py +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -7,7 +7,10 @@ import logging from typing import Any, TypeVar from synology_dsm.api.surveillance_station.camera import SynoCamera -from synology_dsm.exceptions import SynologyDSMAPIErrorException +from synology_dsm.exceptions import ( + SynologyDSMAPIErrorException, + SynologyDSMNotLoggedInException, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL @@ -15,10 +18,11 @@ 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 .common import SynoApi, raise_config_entry_auth_error from .const import ( DEFAULT_SCAN_INTERVAL, SIGNAL_CAMERA_SOURCE_CHANGED, + SYNOLOGY_AUTH_FAILED_EXCEPTIONS, SYNOLOGY_CONNECTION_EXCEPTIONS, ) @@ -65,13 +69,17 @@ class SynologyDSMSwitchUpdateCoordinator( async def async_setup(self) -> None: """Set up the coordinator initial data.""" info = await self.api.dsm.surveillance_station.get_info() + assert info is not None self.version = info["data"]["CMSMinVersion"] async def _async_update_data(self) -> dict[str, dict[str, Any]]: """Fetch all data from api.""" surveillance_station = self.api.surveillance_station + assert surveillance_station is not None return { - "switches": {"home_mode": await surveillance_station.get_home_mode_status()} + "switches": { + "home_mode": bool(await surveillance_station.get_home_mode_status()) + } } @@ -96,14 +104,23 @@ class SynologyDSMCentralUpdateCoordinator(SynologyDSMUpdateCoordinator[None]): async def _async_update_data(self) -> 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 + for attempts in range(2): + try: + await self.api.async_update() + except SynologyDSMNotLoggedInException: + # If login is expired, try to login again + try: + await self.api.dsm.login() + except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: + raise_config_entry_auth_error(err) + if attempts == 0: + continue + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err class SynologyDSMCameraUpdateCoordinator( - SynologyDSMUpdateCoordinator[dict[str, dict[str, SynoCamera]]] + SynologyDSMUpdateCoordinator[dict[str, dict[int, SynoCamera]]] ): """DataUpdateCoordinator to gather data for a synology_dsm cameras.""" @@ -116,10 +133,11 @@ class SynologyDSMCameraUpdateCoordinator( """Initialize DataUpdateCoordinator for cameras.""" super().__init__(hass, entry, api, timedelta(seconds=30)) - async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]]: + async def _async_update_data(self) -> dict[str, dict[int, SynoCamera]]: """Fetch all camera data from api.""" surveillance_station = self.api.surveillance_station - current_data: dict[str, SynoCamera] = { + assert surveillance_station is not None + current_data: dict[int, SynoCamera] = { camera.id: camera for camera in surveillance_station.get_all_cameras() } @@ -128,7 +146,7 @@ class SynologyDSMCameraUpdateCoordinator( except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - new_data: dict[str, SynoCamera] = { + new_data: dict[int, SynoCamera] = { camera.id: camera for camera in surveillance_station.get_all_cameras() } diff --git a/homeassistant/components/synology_dsm/diagnostics.py b/homeassistant/components/synology_dsm/diagnostics.py index d9b4131b078..42a8ab8d60f 100644 --- a/homeassistant/components/synology_dsm/diagnostics.py +++ b/homeassistant/components/synology_dsm/diagnostics.py @@ -4,8 +4,6 @@ from __future__ import annotations from typing import Any -from synology_dsm.api.surveillance_station.camera import SynoCamera - from homeassistant.components.camera import diagnostics as camera_diagnostics from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -47,7 +45,6 @@ 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"]] = { "type": intf["type"], @@ -55,7 +52,6 @@ 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"]] = { "name": disk["name"], @@ -66,7 +62,6 @@ async def async_get_config_entry_diagnostics( "size_total": disk["size_total"], } - volume: dict for volume in syno_api.storage.volumes: diag_data["storage"]["volumes"][volume["id"]] = { "name": volume["fs_type"], @@ -74,7 +69,6 @@ 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] = { "name": camera.name, diff --git a/homeassistant/components/synology_dsm/entity.py b/homeassistant/components/synology_dsm/entity.py index 4bd1e526194..1a2e07af9e1 100644 --- a/homeassistant/components/synology_dsm/entity.py +++ b/homeassistant/components/synology_dsm/entity.py @@ -45,16 +45,21 @@ class SynologyDSMBaseEntity(CoordinatorEntity[_CoordinatorT]): self.entity_description = description self._api = api + information = api.information + network = api.network + assert information is not None + assert network is not None + self._attr_unique_id: str = ( - f"{api.information.serial}_{description.api_key}:{description.key}" + f"{information.serial}_{description.api_key}:{description.key}" ) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._api.information.serial)}, - name=self._api.network.hostname, + identifiers={(DOMAIN, information.serial)}, + name=network.hostname, manufacturer="Synology", - model=self._api.information.model, - sw_version=self._api.information.version_string, - configuration_url=self._api.config_url, + model=information.model, + sw_version=information.version_string, + configuration_url=api.config_url, ) async def async_added_to_hass(self) -> None: @@ -85,14 +90,22 @@ class SynologyDSMDeviceEntity( self._device_model: str | None = None self._device_firmware: str | None = None self._device_type = None + storage = api.storage + information = api.information + network = api.network + assert information is not None + assert storage is not None + assert network is not None if "volume" in description.key: - volume = self._api.storage.get_volume(self._device_id) + assert self._device_id is not None + volume = storage.get_volume(self._device_id) + assert volume is not None # Volume does not have a name self._device_name = volume["id"].replace("_", " ").capitalize() self._device_manufacturer = "Synology" - self._device_model = self._api.information.model - self._device_firmware = self._api.information.version_string + self._device_model = information.model + self._device_firmware = information.version_string self._device_type = ( volume["device_type"] .replace("_", " ") @@ -100,7 +113,9 @@ class SynologyDSMDeviceEntity( .replace("shr", "SHR") ) elif "disk" in description.key: - disk = self._api.storage.get_disk(self._device_id) + assert self._device_id is not None + disk = storage.get_disk(self._device_id) + assert disk is not None self._device_name = disk["name"] self._device_manufacturer = disk["vendor"] self._device_model = disk["model"].strip() @@ -109,11 +124,11 @@ class SynologyDSMDeviceEntity( self._attr_unique_id += f"_{self._device_id}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"{self._api.information.serial}_{self._device_id}")}, - name=f"{self._api.network.hostname} ({self._device_name})", + identifiers={(DOMAIN, f"{information.serial}_{self._device_id}")}, + name=f"{network.hostname} ({self._device_name})", manufacturer=self._device_manufacturer, model=self._device_model, sw_version=self._device_firmware, - via_device=(DOMAIN, self._api.information.serial), + via_device=(DOMAIN, information.serial), configuration_url=self._api.config_url, ) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 8060bce5c9b..caecfcbd0c9 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/synology_dsm", "iot_class": "local_polling", "loggers": ["synology_dsm"], - "requirements": ["py-synologydsm-api==2.1.4"], + "requirements": ["py-synologydsm-api==2.4.2"], "ssdp": [ { "manufacturer": "Synology", diff --git a/homeassistant/components/synology_dsm/media_source.py b/homeassistant/components/synology_dsm/media_source.py index 9a393813c3e..4699a1a5c20 100644 --- a/homeassistant/components/synology_dsm/media_source.py +++ b/homeassistant/components/synology_dsm/media_source.py @@ -105,6 +105,7 @@ class SynologyPhotosMediaSource(MediaSource): ] identifier = SynologyPhotosMediaSourceIdentifier(item.identifier) diskstation: SynologyDSMData = self.hass.data[DOMAIN][identifier.unique_id] + assert diskstation.api.photos is not None if identifier.album_id is None: # Get Albums @@ -112,6 +113,7 @@ class SynologyPhotosMediaSource(MediaSource): albums = await diskstation.api.photos.get_albums() except SynologyDSMException: return [] + assert albums is not None ret = [ BrowseMediaSource( @@ -148,6 +150,7 @@ class SynologyPhotosMediaSource(MediaSource): ) except SynologyDSMException: return [] + assert album_items is not None ret = [] for album_item in album_items: @@ -190,6 +193,8 @@ class SynologyPhotosMediaSource(MediaSource): self, item: SynoPhotosItem, diskstation: SynologyDSMData ) -> str | None: """Get thumbnail.""" + assert diskstation.api.photos is not None + try: thumbnail = await diskstation.api.photos.get_item_thumbnail_url(item) except SynologyDSMException: @@ -215,13 +220,14 @@ class SynologyDsmMediaView(http.HomeAssistantView): raise web.HTTPNotFound # location: {cache_key}/{filename} cache_key, file_name = location.split("/") - image_id = cache_key.split("_")[0] + image_id = int(cache_key.split("_")[0]) mime_type, _ = mimetypes.guess_type(file_name) if not isinstance(mime_type, str): raise web.HTTPNotFound diskstation: SynologyDSMData = self.hass.data[DOMAIN][source_dir_id] - item = SynoPhotosItem(image_id, "", "", "", cache_key, "") + assert diskstation.api.photos is not None + item = SynoPhotosItem(image_id, "", "", "", cache_key, "", False) try: image = await diskstation.api.photos.download_item(item) except SynologyDSMException as exc: diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 6769c1e4901..b29a33f7253 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -292,6 +292,8 @@ async def async_setup_entry( data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] api = data.api coordinator = data.coordinator_central + storage = api.storage + assert storage is not None entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [ SynoDSMUtilSensor(api, coordinator, description) @@ -299,21 +301,21 @@ async def async_setup_entry( ] # Handle all volumes - if api.storage.volumes_ids: + if storage.volumes_ids: entities.extend( [ SynoDSMStorageSensor(api, coordinator, description, volume) - for volume in entry.data.get(CONF_VOLUMES, api.storage.volumes_ids) + for volume in entry.data.get(CONF_VOLUMES, storage.volumes_ids) for description in STORAGE_VOL_SENSORS ] ) # Handle all disks - if api.storage.disks_ids: + if storage.disks_ids: entities.extend( [ SynoDSMStorageSensor(api, coordinator, description, disk) - for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids) + for disk in entry.data.get(CONF_DISKS, storage.disks_ids) for description in STORAGE_DISK_SENSORS ] ) diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index c19cdb8c815..facce824bda 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -79,6 +79,8 @@ class SynoDSMSurveillanceHomeModeToggle( async def async_turn_on(self, **kwargs: Any) -> None: """Turn on Home mode.""" + assert self._api.surveillance_station is not None + assert self._api.information _LOGGER.debug( "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)", self._api.information.serial, @@ -88,6 +90,8 @@ class SynoDSMSurveillanceHomeModeToggle( async def async_turn_off(self, **kwargs: Any) -> None: """Turn off Home mode.""" + assert self._api.surveillance_station is not None + assert self._api.information _LOGGER.debug( "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)", self._api.information.serial, @@ -103,6 +107,9 @@ class SynoDSMSurveillanceHomeModeToggle( @property def device_info(self) -> DeviceInfo: """Return the device information.""" + assert self._api.surveillance_station is not None + assert self._api.information is not None + assert self._api.network is not None return DeviceInfo( identifiers={ ( diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index c7bcff48cea..ed60191f296 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -64,24 +64,29 @@ class SynoDSMUpdateEntity( @property def installed_version(self) -> str | None: """Version installed and in use.""" - return self._api.information.version_string # type: ignore[no-any-return] + assert self._api.information is not None + return self._api.information.version_string @property def latest_version(self) -> str | None: """Latest version available for install.""" + assert self._api.upgrade is not None if not self._api.upgrade.update_available: return self.installed_version - return self._api.upgrade.available_version # type: ignore[no-any-return] + return self._api.upgrade.available_version @property def release_url(self) -> str | None: """URL to the full release notes of the latest version available.""" + assert self._api.information is not None + assert self._api.upgrade is not None + if (details := self._api.upgrade.available_version_details) is None: return None url = URL("http://update.synology.com/autoupdate/whatsnew.php") query = {"model": self._api.information.model} - if details.get("nano") > 0: + if details["nano"] > 0: query["update_version"] = f"{details['buildnumber']}-{details['nano']}" else: query["update_version"] = details["buildnumber"] diff --git a/requirements_all.txt b/requirements_all.txt index 4d7edd2301e..54d136c0b83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1634,7 +1634,7 @@ py-schluter==0.1.7 py-sucks==0.9.9 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.4 +py-synologydsm-api==2.4.2 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddb7061aee9..32bfdcd0968 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1293,7 +1293,7 @@ py-nightscout==1.2.2 py-sucks==0.9.9 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.4 +py-synologydsm-api==2.4.2 # homeassistant.components.seventeentrack py17track==2021.12.2 diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index 24e9a378c02..2a792d174f8 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -49,7 +49,9 @@ def dsm_with_photos() -> MagicMock: dsm.photos.get_albums = AsyncMock(return_value=[SynoPhotosAlbum(1, "Album 1", 10)]) dsm.photos.get_items_from_album = AsyncMock( - return_value=[SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm")] + return_value=[ + SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", False) + ] ) dsm.photos.get_item_thumbnail_url = AsyncMock( return_value="http://my.thumbnail.url"