Bump py-synologydsm-api to 2.4.2 (#115499)

Co-authored-by: mib1185 <mail@mib85.de>
This commit is contained in:
J. Nick Koston 2024-04-13 09:38:37 -10:00 committed by GitHub
parent 64a4d52b3c
commit 008c42e282
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 170 additions and 91 deletions

View file

@ -11,10 +11,10 @@ from synology_dsm.api.surveillance_station.camera import SynoCamera
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant 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 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 ( from .const import (
DEFAULT_VERIFY_SSL, DEFAULT_VERIFY_SSL,
DOMAIN, DOMAIN,
@ -68,11 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
await api.async_setup() await api.async_setup()
except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err:
if err.args[0] and isinstance(err.args[0], dict): raise_config_entry_auth_error(err)
details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN)
else:
details = EXCEPTION_UNKNOWN
raise ConfigEntryAuthFailed(f"reason: {details}") from err
except SYNOLOGY_CONNECTION_EXCEPTIONS as err: except SYNOLOGY_CONNECTION_EXCEPTIONS as err:
if err.args[0] and isinstance(err.args[0], dict): if err.args[0] and isinstance(err.args[0], dict):
details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) 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.""" """Remove synology_dsm config entry from a device."""
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
api = data.api api = data.api
assert api.information is not None
serial = api.information.serial serial = api.information.serial
storage = api.storage storage = api.storage
assert storage is not None
all_cameras: list[SynoCamera] = [] all_cameras: list[SynoCamera] = []
if api.surveillance_station is not None: if api.surveillance_station is not None:
# get_all_cameras does not do I/O # get_all_cameras does not do I/O

View file

@ -69,6 +69,7 @@ async def async_setup_entry(
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
api = data.api api = data.api
coordinator = data.coordinator_central coordinator = data.coordinator_central
assert api.storage is not None
entities: list[SynoDSMSecurityBinarySensor | SynoDSMStorageBinarySensor] = [ entities: list[SynoDSMSecurityBinarySensor | SynoDSMStorageBinarySensor] = [
SynoDSMSecurityBinarySensor(api, coordinator, description) SynoDSMSecurityBinarySensor(api, coordinator, description)
@ -121,7 +122,8 @@ class SynoDSMSecurityBinarySensor(SynoDSMBinarySensor):
@property @property
def extra_state_attributes(self) -> dict[str, str]: def extra_state_attributes(self) -> dict[str, str]:
"""Return security checks details.""" """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): class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, SynoDSMBinarySensor):

View file

@ -73,7 +73,8 @@ class SynologyDSMButton(ButtonEntity):
"""Initialize the Synology DSM binary_sensor entity.""" """Initialize the Synology DSM binary_sensor entity."""
self.entity_description = description self.entity_description = description
self.syno_api = api 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_name = f"{api.network.hostname} {description.name}"
self._attr_unique_id = f"{api.information.serial}_{description.key}" self._attr_unique_id = f"{api.information.serial}_{description.key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
@ -82,6 +83,7 @@ class SynologyDSMButton(ButtonEntity):
async def async_press(self) -> None: async def async_press(self) -> None:
"""Triggers the Synology DSM button press service.""" """Triggers the Synology DSM button press service."""
assert self.syno_api.network is not None
LOGGER.debug( LOGGER.debug(
"Trigger %s for %s", "Trigger %s for %s",
self.entity_description.key, self.entity_description.key,

View file

@ -42,6 +42,8 @@ class SynologyDSMCameraEntityDescription(
): ):
"""Describes Synology DSM camera entity.""" """Describes Synology DSM camera entity."""
camera_id: int
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@ -65,12 +67,13 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C
self, self,
api: SynoApi, api: SynoApi,
coordinator: SynologyDSMCameraUpdateCoordinator, coordinator: SynologyDSMCameraUpdateCoordinator,
camera_id: str, camera_id: int,
) -> None: ) -> None:
"""Initialize a Synology camera.""" """Initialize a Synology camera."""
description = SynologyDSMCameraEntityDescription( description = SynologyDSMCameraEntityDescription(
api_key=SynoSurveillanceStation.CAMERA_API_KEY, api_key=SynoSurveillanceStation.CAMERA_API_KEY,
key=camera_id, key=str(camera_id),
camera_id=camera_id,
name=coordinator.data["cameras"][camera_id].name, name=coordinator.data["cameras"][camera_id].name,
entity_registry_enabled_default=coordinator.data["cameras"][ entity_registry_enabled_default=coordinator.data["cameras"][
camera_id camera_id
@ -85,23 +88,20 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C
@property @property
def camera_data(self) -> SynoCamera: def camera_data(self) -> SynoCamera:
"""Camera data.""" """Camera data."""
return self.coordinator.data["cameras"][self.entity_description.key] return self.coordinator.data["cameras"][self.entity_description.camera_id]
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device information.""" """Return the device information."""
information = self._api.information
assert information is not None
return DeviceInfo( return DeviceInfo(
identifiers={ identifiers={(DOMAIN, f"{information.serial}_{self.camera_data.id}")},
(
DOMAIN,
f"{self._api.information.serial}_{self.camera_data.id}",
)
},
name=self.camera_data.name, name=self.camera_data.name,
model=self.camera_data.model, model=self.camera_data.model,
via_device=( via_device=(
DOMAIN, 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 @property
def is_recording(self) -> bool: def is_recording(self) -> bool:
"""Return true if the device is recording.""" """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 @property
def motion_detection_enabled(self) -> bool: def motion_detection_enabled(self) -> bool:
"""Return the camera motion detection status.""" """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: def _listen_source_updates(self) -> None:
"""Listen for camera source changed events.""" """Listen for camera source changed events."""
@ -153,9 +153,10 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C
) )
if not self.available: if not self.available:
return None return None
assert self._api.surveillance_station is not None
try: try:
return await self._api.surveillance_station.get_camera_image( # type: ignore[no-any-return] return await self._api.surveillance_station.get_camera_image(
self.entity_description.key, self.snapshot_quality self.entity_description.camera_id, self.snapshot_quality
) )
except ( except (
SynologyDSMAPIErrorException, SynologyDSMAPIErrorException,
@ -178,7 +179,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C
if not self.available: if not self.available:
return None 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: async def async_enable_motion_detection(self) -> None:
"""Enable motion detection in the camera.""" """Enable motion detection in the camera."""
@ -186,8 +187,9 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C
"SynoDSMCamera.enable_motion_detection(%s)", "SynoDSMCamera.enable_motion_detection(%s)",
self.camera_data.name, self.camera_data.name,
) )
assert self._api.surveillance_station is not None
await self._api.surveillance_station.enable_motion_detection( 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: async def async_disable_motion_detection(self) -> None:
@ -196,6 +198,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C
"SynoDSMCamera.disable_motion_detection(%s)", "SynoDSMCamera.disable_motion_detection(%s)",
self.camera_data.name, self.camera_data.name,
) )
assert self._api.surveillance_station is not None
await self._api.surveillance_station.disable_motion_detection( await self._api.surveillance_station.disable_motion_detection(
self.entity_description.key self.entity_description.camera_id
) )

View file

@ -33,9 +33,15 @@ from homeassistant.const import (
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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__) LOGGER = logging.getLogger(__name__)
@ -43,6 +49,8 @@ LOGGER = logging.getLogger(__name__)
class SynoApi: class SynoApi:
"""Class to interface with Synology DSM API.""" """Class to interface with Synology DSM API."""
dsm: SynologyDSM
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the API wrapper class.""" """Initialize the API wrapper class."""
self._hass = hass self._hass = hass
@ -53,16 +61,15 @@ class SynoApi:
self.config_url = f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" self.config_url = f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}"
# DSM APIs # DSM APIs
self.dsm: SynologyDSM = None self.information: SynoDSMInformation | None = None
self.information: SynoDSMInformation = None self.network: SynoDSMNetwork | None = None
self.network: SynoDSMNetwork = None self.security: SynoCoreSecurity | None = None
self.security: SynoCoreSecurity = None self.storage: SynoStorage | None = None
self.storage: SynoStorage = None self.photos: SynoPhotos | None = None
self.photos: SynoPhotos = None self.surveillance_station: SynoSurveillanceStation | None = None
self.surveillance_station: SynoSurveillanceStation = None self.system: SynoCoreSystem | None = None
self.system: SynoCoreSystem = None self.upgrade: SynoCoreUpgrade | None = None
self.upgrade: SynoCoreUpgrade = None self.utilisation: SynoCoreUtilization | None = None
self.utilisation: SynoCoreUtilization = None
# Should we fetch them # Should we fetch them
self._fetching_entities: dict[str, set[str]] = {} self._fetching_entities: dict[str, set[str]] = {}
@ -85,7 +92,7 @@ class SynoApi:
self._entry.data[CONF_USERNAME], self._entry.data[CONF_USERNAME],
self._entry.data[CONF_PASSWORD], self._entry.data[CONF_PASSWORD],
self._entry.data[CONF_SSL], 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), device_token=self._entry.data.get(CONF_DEVICE_TOKEN),
) )
await self.dsm.login() await self.dsm.login()
@ -159,7 +166,8 @@ class SynoApi:
return return
# surveillance_station is updated by own coordinator # 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 # Determine if we should fetch an API
self._with_system = bool(self.dsm.apis.get(SynoCoreSystem.API_KEY)) 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'", "Disable security api from being updated for '%s'",
self._entry.unique_id, self._entry.unique_id,
) )
self.dsm.reset(self.security) if self.security:
self.dsm.reset(self.security)
self.security = None self.security = None
if not self._with_photos: if not self._with_photos:
LOGGER.debug( LOGGER.debug(
"Disable photos api from being updated or '%s'", self._entry.unique_id "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 self.photos = None
if not self._with_storage: if not self._with_storage:
LOGGER.debug( LOGGER.debug(
"Disable storage api from being updatedf or '%s'", self._entry.unique_id "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 self.storage = None
if not self._with_system: if not self._with_system:
LOGGER.debug( LOGGER.debug(
"Disable system api from being updated for '%s'", self._entry.unique_id "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 self.system = None
if not self._with_upgrade: if not self._with_upgrade:
LOGGER.debug( LOGGER.debug(
"Disable upgrade api from being updated for '%s'", self._entry.unique_id "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 self.upgrade = None
if not self._with_utilisation: if not self._with_utilisation:
@ -218,7 +231,8 @@ class SynoApi:
"Disable utilisation api from being updated for '%s'", "Disable utilisation api from being updated for '%s'",
self._entry.unique_id, self._entry.unique_id,
) )
self.dsm.reset(self.utilisation) if self.utilisation:
self.dsm.reset(self.utilisation)
self.utilisation = None self.utilisation = None
async def _fetch_device_configuration(self) -> None: async def _fetch_device_configuration(self) -> None:
@ -272,11 +286,13 @@ class SynoApi:
async def async_reboot(self) -> None: async def async_reboot(self) -> None:
"""Reboot NAS.""" """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: async def async_shutdown(self) -> None:
"""Shutdown NAS.""" """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: async def async_unload(self) -> None:
"""Stop interacting with the NAS and prepare for removal from hass.""" """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) LOGGER.debug("Start data update for '%s'", self._entry.unique_id)
self._setup_api_requests() self._setup_api_requests()
await self.dsm.update(self._with_information) 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

View file

@ -425,7 +425,7 @@ async def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str | None) ->
): ):
raise InvalidData raise InvalidData
return api.information.serial # type: ignore[no-any-return] return api.information.serial
class InvalidData(HomeAssistantError): class InvalidData(HomeAssistantError):

View file

@ -7,7 +7,10 @@ import logging
from typing import Any, TypeVar from typing import Any, TypeVar
from synology_dsm.api.surveillance_station.camera import SynoCamera 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.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL 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.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .common import SynoApi from .common import SynoApi, raise_config_entry_auth_error
from .const import ( from .const import (
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
SIGNAL_CAMERA_SOURCE_CHANGED, SIGNAL_CAMERA_SOURCE_CHANGED,
SYNOLOGY_AUTH_FAILED_EXCEPTIONS,
SYNOLOGY_CONNECTION_EXCEPTIONS, SYNOLOGY_CONNECTION_EXCEPTIONS,
) )
@ -65,13 +69,17 @@ class SynologyDSMSwitchUpdateCoordinator(
async def async_setup(self) -> None: async def async_setup(self) -> None:
"""Set up the coordinator initial data.""" """Set up the coordinator initial data."""
info = await self.api.dsm.surveillance_station.get_info() info = await self.api.dsm.surveillance_station.get_info()
assert info is not None
self.version = info["data"]["CMSMinVersion"] self.version = info["data"]["CMSMinVersion"]
async def _async_update_data(self) -> dict[str, dict[str, Any]]: async def _async_update_data(self) -> dict[str, dict[str, Any]]:
"""Fetch all data from api.""" """Fetch all data from api."""
surveillance_station = self.api.surveillance_station surveillance_station = self.api.surveillance_station
assert surveillance_station is not None
return { 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: async def _async_update_data(self) -> None:
"""Fetch all data from api.""" """Fetch all data from api."""
try: for attempts in range(2):
await self.api.async_update() try:
except SYNOLOGY_CONNECTION_EXCEPTIONS as err: await self.api.async_update()
raise UpdateFailed(f"Error communicating with API: {err}") from err 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( class SynologyDSMCameraUpdateCoordinator(
SynologyDSMUpdateCoordinator[dict[str, dict[str, SynoCamera]]] SynologyDSMUpdateCoordinator[dict[str, dict[int, SynoCamera]]]
): ):
"""DataUpdateCoordinator to gather data for a synology_dsm cameras.""" """DataUpdateCoordinator to gather data for a synology_dsm cameras."""
@ -116,10 +133,11 @@ class SynologyDSMCameraUpdateCoordinator(
"""Initialize DataUpdateCoordinator for cameras.""" """Initialize DataUpdateCoordinator for cameras."""
super().__init__(hass, entry, api, timedelta(seconds=30)) 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.""" """Fetch all camera data from api."""
surveillance_station = self.api.surveillance_station 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() camera.id: camera for camera in surveillance_station.get_all_cameras()
} }
@ -128,7 +146,7 @@ class SynologyDSMCameraUpdateCoordinator(
except SynologyDSMAPIErrorException as err: except SynologyDSMAPIErrorException as err:
raise UpdateFailed(f"Error communicating with API: {err}") from 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() camera.id: camera for camera in surveillance_station.get_all_cameras()
} }

View file

@ -4,8 +4,6 @@ from __future__ import annotations
from typing import Any 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.camera import diagnostics as camera_diagnostics
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -47,7 +45,6 @@ async def async_get_config_entry_diagnostics(
} }
if syno_api.network is not None: if syno_api.network is not None:
intf: dict
for intf in syno_api.network.interfaces: for intf in syno_api.network.interfaces:
diag_data["network"]["interfaces"][intf["id"]] = { diag_data["network"]["interfaces"][intf["id"]] = {
"type": intf["type"], "type": intf["type"],
@ -55,7 +52,6 @@ async def async_get_config_entry_diagnostics(
} }
if syno_api.storage is not None: if syno_api.storage is not None:
disk: dict
for disk in syno_api.storage.disks: for disk in syno_api.storage.disks:
diag_data["storage"]["disks"][disk["id"]] = { diag_data["storage"]["disks"][disk["id"]] = {
"name": disk["name"], "name": disk["name"],
@ -66,7 +62,6 @@ async def async_get_config_entry_diagnostics(
"size_total": disk["size_total"], "size_total": disk["size_total"],
} }
volume: dict
for volume in syno_api.storage.volumes: for volume in syno_api.storage.volumes:
diag_data["storage"]["volumes"][volume["id"]] = { diag_data["storage"]["volumes"][volume["id"]] = {
"name": volume["fs_type"], "name": volume["fs_type"],
@ -74,7 +69,6 @@ async def async_get_config_entry_diagnostics(
} }
if syno_api.surveillance_station is not None: if syno_api.surveillance_station is not None:
camera: SynoCamera
for camera in syno_api.surveillance_station.get_all_cameras(): for camera in syno_api.surveillance_station.get_all_cameras():
diag_data["surveillance_station"]["cameras"][camera.id] = { diag_data["surveillance_station"]["cameras"][camera.id] = {
"name": camera.name, "name": camera.name,

View file

@ -45,16 +45,21 @@ class SynologyDSMBaseEntity(CoordinatorEntity[_CoordinatorT]):
self.entity_description = description self.entity_description = description
self._api = api self._api = api
information = api.information
network = api.network
assert information is not None
assert network is not None
self._attr_unique_id: str = ( 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( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._api.information.serial)}, identifiers={(DOMAIN, information.serial)},
name=self._api.network.hostname, name=network.hostname,
manufacturer="Synology", manufacturer="Synology",
model=self._api.information.model, model=information.model,
sw_version=self._api.information.version_string, sw_version=information.version_string,
configuration_url=self._api.config_url, configuration_url=api.config_url,
) )
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
@ -85,14 +90,22 @@ class SynologyDSMDeviceEntity(
self._device_model: str | None = None self._device_model: str | None = None
self._device_firmware: str | None = None self._device_firmware: str | None = None
self._device_type = 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: 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 # Volume does not have a name
self._device_name = volume["id"].replace("_", " ").capitalize() self._device_name = volume["id"].replace("_", " ").capitalize()
self._device_manufacturer = "Synology" self._device_manufacturer = "Synology"
self._device_model = self._api.information.model self._device_model = information.model
self._device_firmware = self._api.information.version_string self._device_firmware = information.version_string
self._device_type = ( self._device_type = (
volume["device_type"] volume["device_type"]
.replace("_", " ") .replace("_", " ")
@ -100,7 +113,9 @@ class SynologyDSMDeviceEntity(
.replace("shr", "SHR") .replace("shr", "SHR")
) )
elif "disk" in description.key: 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_name = disk["name"]
self._device_manufacturer = disk["vendor"] self._device_manufacturer = disk["vendor"]
self._device_model = disk["model"].strip() self._device_model = disk["model"].strip()
@ -109,11 +124,11 @@ class SynologyDSMDeviceEntity(
self._attr_unique_id += f"_{self._device_id}" self._attr_unique_id += f"_{self._device_id}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._api.information.serial}_{self._device_id}")}, identifiers={(DOMAIN, f"{information.serial}_{self._device_id}")},
name=f"{self._api.network.hostname} ({self._device_name})", name=f"{network.hostname} ({self._device_name})",
manufacturer=self._device_manufacturer, manufacturer=self._device_manufacturer,
model=self._device_model, model=self._device_model,
sw_version=self._device_firmware, sw_version=self._device_firmware,
via_device=(DOMAIN, self._api.information.serial), via_device=(DOMAIN, information.serial),
configuration_url=self._api.config_url, configuration_url=self._api.config_url,
) )

View file

@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/synology_dsm", "documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["synology_dsm"], "loggers": ["synology_dsm"],
"requirements": ["py-synologydsm-api==2.1.4"], "requirements": ["py-synologydsm-api==2.4.2"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Synology", "manufacturer": "Synology",

View file

@ -105,6 +105,7 @@ class SynologyPhotosMediaSource(MediaSource):
] ]
identifier = SynologyPhotosMediaSourceIdentifier(item.identifier) identifier = SynologyPhotosMediaSourceIdentifier(item.identifier)
diskstation: SynologyDSMData = self.hass.data[DOMAIN][identifier.unique_id] diskstation: SynologyDSMData = self.hass.data[DOMAIN][identifier.unique_id]
assert diskstation.api.photos is not None
if identifier.album_id is None: if identifier.album_id is None:
# Get Albums # Get Albums
@ -112,6 +113,7 @@ class SynologyPhotosMediaSource(MediaSource):
albums = await diskstation.api.photos.get_albums() albums = await diskstation.api.photos.get_albums()
except SynologyDSMException: except SynologyDSMException:
return [] return []
assert albums is not None
ret = [ ret = [
BrowseMediaSource( BrowseMediaSource(
@ -148,6 +150,7 @@ class SynologyPhotosMediaSource(MediaSource):
) )
except SynologyDSMException: except SynologyDSMException:
return [] return []
assert album_items is not None
ret = [] ret = []
for album_item in album_items: for album_item in album_items:
@ -190,6 +193,8 @@ class SynologyPhotosMediaSource(MediaSource):
self, item: SynoPhotosItem, diskstation: SynologyDSMData self, item: SynoPhotosItem, diskstation: SynologyDSMData
) -> str | None: ) -> str | None:
"""Get thumbnail.""" """Get thumbnail."""
assert diskstation.api.photos is not None
try: try:
thumbnail = await diskstation.api.photos.get_item_thumbnail_url(item) thumbnail = await diskstation.api.photos.get_item_thumbnail_url(item)
except SynologyDSMException: except SynologyDSMException:
@ -215,13 +220,14 @@ class SynologyDsmMediaView(http.HomeAssistantView):
raise web.HTTPNotFound raise web.HTTPNotFound
# location: {cache_key}/{filename} # location: {cache_key}/{filename}
cache_key, file_name = location.split("/") 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) mime_type, _ = mimetypes.guess_type(file_name)
if not isinstance(mime_type, str): if not isinstance(mime_type, str):
raise web.HTTPNotFound raise web.HTTPNotFound
diskstation: SynologyDSMData = self.hass.data[DOMAIN][source_dir_id] 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: try:
image = await diskstation.api.photos.download_item(item) image = await diskstation.api.photos.download_item(item)
except SynologyDSMException as exc: except SynologyDSMException as exc:

View file

@ -292,6 +292,8 @@ async def async_setup_entry(
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
api = data.api api = data.api
coordinator = data.coordinator_central coordinator = data.coordinator_central
storage = api.storage
assert storage is not None
entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [ entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [
SynoDSMUtilSensor(api, coordinator, description) SynoDSMUtilSensor(api, coordinator, description)
@ -299,21 +301,21 @@ async def async_setup_entry(
] ]
# Handle all volumes # Handle all volumes
if api.storage.volumes_ids: if storage.volumes_ids:
entities.extend( entities.extend(
[ [
SynoDSMStorageSensor(api, coordinator, description, volume) 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 for description in STORAGE_VOL_SENSORS
] ]
) )
# Handle all disks # Handle all disks
if api.storage.disks_ids: if storage.disks_ids:
entities.extend( entities.extend(
[ [
SynoDSMStorageSensor(api, coordinator, description, disk) 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 for description in STORAGE_DISK_SENSORS
] ]
) )

View file

@ -79,6 +79,8 @@ class SynoDSMSurveillanceHomeModeToggle(
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on Home mode.""" """Turn on Home mode."""
assert self._api.surveillance_station is not None
assert self._api.information
_LOGGER.debug( _LOGGER.debug(
"SynoDSMSurveillanceHomeModeToggle.turn_on(%s)", "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)",
self._api.information.serial, self._api.information.serial,
@ -88,6 +90,8 @@ class SynoDSMSurveillanceHomeModeToggle(
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off Home mode.""" """Turn off Home mode."""
assert self._api.surveillance_station is not None
assert self._api.information
_LOGGER.debug( _LOGGER.debug(
"SynoDSMSurveillanceHomeModeToggle.turn_off(%s)", "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)",
self._api.information.serial, self._api.information.serial,
@ -103,6 +107,9 @@ class SynoDSMSurveillanceHomeModeToggle(
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device information.""" """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( return DeviceInfo(
identifiers={ identifiers={
( (

View file

@ -64,24 +64,29 @@ class SynoDSMUpdateEntity(
@property @property
def installed_version(self) -> str | None: def installed_version(self) -> str | None:
"""Version installed and in use.""" """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 @property
def latest_version(self) -> str | None: def latest_version(self) -> str | None:
"""Latest version available for install.""" """Latest version available for install."""
assert self._api.upgrade is not None
if not self._api.upgrade.update_available: if not self._api.upgrade.update_available:
return self.installed_version return self.installed_version
return self._api.upgrade.available_version # type: ignore[no-any-return] return self._api.upgrade.available_version
@property @property
def release_url(self) -> str | None: def release_url(self) -> str | None:
"""URL to the full release notes of the latest version available.""" """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: if (details := self._api.upgrade.available_version_details) is None:
return None return None
url = URL("http://update.synology.com/autoupdate/whatsnew.php") url = URL("http://update.synology.com/autoupdate/whatsnew.php")
query = {"model": self._api.information.model} query = {"model": self._api.information.model}
if details.get("nano") > 0: if details["nano"] > 0:
query["update_version"] = f"{details['buildnumber']}-{details['nano']}" query["update_version"] = f"{details['buildnumber']}-{details['nano']}"
else: else:
query["update_version"] = details["buildnumber"] query["update_version"] = details["buildnumber"]

View file

@ -1634,7 +1634,7 @@ py-schluter==0.1.7
py-sucks==0.9.9 py-sucks==0.9.9
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==2.1.4 py-synologydsm-api==2.4.2
# homeassistant.components.zabbix # homeassistant.components.zabbix
py-zabbix==1.1.7 py-zabbix==1.1.7

View file

@ -1293,7 +1293,7 @@ py-nightscout==1.2.2
py-sucks==0.9.9 py-sucks==0.9.9
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==2.1.4 py-synologydsm-api==2.4.2
# homeassistant.components.seventeentrack # homeassistant.components.seventeentrack
py17track==2021.12.2 py17track==2021.12.2

View file

@ -49,7 +49,9 @@ def dsm_with_photos() -> MagicMock:
dsm.photos.get_albums = AsyncMock(return_value=[SynoPhotosAlbum(1, "Album 1", 10)]) dsm.photos.get_albums = AsyncMock(return_value=[SynoPhotosAlbum(1, "Album 1", 10)])
dsm.photos.get_items_from_album = AsyncMock( 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( dsm.photos.get_item_thumbnail_url = AsyncMock(
return_value="http://my.thumbnail.url" return_value="http://my.thumbnail.url"