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.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

View file

@ -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):

View file

@ -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,

View file

@ -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
)

View file

@ -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,6 +166,7 @@ class SynoApi:
return
# surveillance_station is updated by own coordinator
if self.surveillance_station:
self.dsm.reset(self.surveillance_station)
# Determine if we should fetch an API
@ -182,6 +190,7 @@ class SynoApi:
"Disable security api from being updated for '%s'",
self._entry.unique_id,
)
if self.security:
self.dsm.reset(self.security)
self.security = None
@ -189,6 +198,7 @@ class SynoApi:
LOGGER.debug(
"Disable photos api from being updated or '%s'", self._entry.unique_id
)
if self.photos:
self.dsm.reset(self.photos)
self.photos = None
@ -196,6 +206,7 @@ class SynoApi:
LOGGER.debug(
"Disable storage api from being updatedf or '%s'", self._entry.unique_id
)
if self.storage:
self.dsm.reset(self.storage)
self.storage = None
@ -203,6 +214,7 @@ class SynoApi:
LOGGER.debug(
"Disable system api from being updated for '%s'", self._entry.unique_id
)
if self.system:
self.dsm.reset(self.system)
self.system = None
@ -210,6 +222,7 @@ class SynoApi:
LOGGER.debug(
"Disable upgrade api from being updated for '%s'", self._entry.unique_id
)
if self.upgrade:
self.dsm.reset(self.upgrade)
self.upgrade = None
@ -218,6 +231,7 @@ class SynoApi:
"Disable utilisation api from being updated for '%s'",
self._entry.unique_id,
)
if self.utilisation:
self.dsm.reset(self.utilisation)
self.utilisation = None
@ -272,10 +286,12 @@ class SynoApi:
async def async_reboot(self) -> None:
"""Reboot NAS."""
if self.system:
await self._syno_api_executer(self.system.reboot)
async def async_shutdown(self) -> None:
"""Shutdown NAS."""
if self.system:
await self._syno_api_executer(self.system.shutdown)
async def async_unload(self) -> None:
@ -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

View file

@ -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):

View file

@ -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."""
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()
}

View file

@ -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,

View file

@ -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,
)

View file

@ -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",

View file

@ -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:

View file

@ -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
]
)

View file

@ -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={
(

View file

@ -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"]

View file

@ -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

View file

@ -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

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_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"