Cleanup coordinators in synology_dsm (#73257)

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
This commit is contained in:
J. Nick Koston 2022-06-09 10:22:16 -10:00 committed by GitHub
parent 1d6068fa09
commit 22daea27c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 287 additions and 233 deletions

View file

@ -1200,6 +1200,7 @@ omit =
homeassistant/components/synology_dsm/binary_sensor.py homeassistant/components/synology_dsm/binary_sensor.py
homeassistant/components/synology_dsm/button.py homeassistant/components/synology_dsm/button.py
homeassistant/components/synology_dsm/camera.py homeassistant/components/synology_dsm/camera.py
homeassistant/components/synology_dsm/coordinator.py
homeassistant/components/synology_dsm/diagnostics.py homeassistant/components/synology_dsm/diagnostics.py
homeassistant/components/synology_dsm/common.py homeassistant/components/synology_dsm/common.py
homeassistant/components/synology_dsm/entity.py homeassistant/components/synology_dsm/entity.py

View file

@ -1,47 +1,32 @@
"""The Synology DSM component.""" """The Synology DSM component."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
import logging import logging
from typing import Any
import async_timeout
from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.api.surveillance_station import SynoSurveillanceStation
from synology_dsm.api.surveillance_station.camera import SynoCamera
from synology_dsm.exceptions import (
SynologyDSMAPIErrorException,
SynologyDSMLogin2SARequiredException,
SynologyDSMLoginDisabledAccountException,
SynologyDSMLoginFailedException,
SynologyDSMLoginInvalidException,
SynologyDSMLoginPermissionDeniedException,
SynologyDSMRequestException,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC, CONF_SCAN_INTERVAL, CONF_VERIFY_SSL from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, 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 homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .common import SynoApi from .common import SynoApi
from .const import ( from .const import (
COORDINATOR_CAMERAS,
COORDINATOR_CENTRAL,
COORDINATOR_SWITCHES,
DEFAULT_SCAN_INTERVAL,
DEFAULT_VERIFY_SSL, DEFAULT_VERIFY_SSL,
DOMAIN, DOMAIN,
EXCEPTION_DETAILS, EXCEPTION_DETAILS,
EXCEPTION_UNKNOWN, EXCEPTION_UNKNOWN,
PLATFORMS, PLATFORMS,
SIGNAL_CAMERA_SOURCE_CHANGED, SYNOLOGY_AUTH_FAILED_EXCEPTIONS,
SYNO_API, SYNOLOGY_CONNECTION_EXCEPTIONS,
SYSTEM_LOADED,
UNDO_UPDATE_LISTENER,
) )
from .coordinator import (
SynologyDSMCameraUpdateCoordinator,
SynologyDSMCentralUpdateCoordinator,
SynologyDSMSwitchUpdateCoordinator,
)
from .models import SynologyDSMData
from .service import async_setup_services from .service import async_setup_services
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@ -79,31 +64,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api = SynoApi(hass, entry) api = SynoApi(hass, entry)
try: try:
await api.async_setup() await api.async_setup()
except ( except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err:
SynologyDSMLogin2SARequiredException,
SynologyDSMLoginDisabledAccountException,
SynologyDSMLoginInvalidException,
SynologyDSMLoginPermissionDeniedException,
) 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)
else: else:
details = EXCEPTION_UNKNOWN details = EXCEPTION_UNKNOWN
raise ConfigEntryAuthFailed(f"reason: {details}") from err raise ConfigEntryAuthFailed(f"reason: {details}") from err
except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: except SYNOLOGY_CONNECTION_EXCEPTIONS as err:
if err.args[0] and isinstance(err.args[0], dict): 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)
else: else:
details = EXCEPTION_UNKNOWN details = EXCEPTION_UNKNOWN
raise ConfigEntryNotReady(details) from err raise ConfigEntryNotReady(details) from err
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.unique_id] = {
UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener),
SYNO_API: api,
SYSTEM_LOADED: True,
}
# Services # Services
await async_setup_services(hass) await async_setup_services(hass)
@ -114,111 +87,50 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry, data={**entry.data, CONF_MAC: network.macs} entry, data={**entry.data, CONF_MAC: network.macs}
) )
async def async_coordinator_update_data_cameras() -> dict[ # These all create executor jobs so we do not gather here
str, dict[str, SynoCamera] coordinator_central = SynologyDSMCentralUpdateCoordinator(hass, entry, api)
] | None: await coordinator_central.async_config_entry_first_refresh()
"""Fetch all camera data from api."""
if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]:
raise UpdateFailed("System not fully loaded")
if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: available_apis = api.dsm.apis
return None
surveillance_station = api.surveillance_station # The central coordinator needs to be refreshed first since
current_data: dict[str, SynoCamera] = { # the next two rely on data from it
camera.id: camera for camera in surveillance_station.get_all_cameras() coordinator_cameras: SynologyDSMCameraUpdateCoordinator | None = None
} if SynoSurveillanceStation.CAMERA_API_KEY in available_apis:
coordinator_cameras = SynologyDSMCameraUpdateCoordinator(hass, entry, api)
await coordinator_cameras.async_config_entry_first_refresh()
try: coordinator_switches: SynologyDSMSwitchUpdateCoordinator | None = None
async with async_timeout.timeout(30):
await hass.async_add_executor_job(surveillance_station.update)
except SynologyDSMAPIErrorException as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
new_data: dict[str, SynoCamera] = {
camera.id: camera for camera in surveillance_station.get_all_cameras()
}
for cam_id, cam_data_new in new_data.items():
if ( if (
(cam_data_current := current_data.get(cam_id)) is not None SynoSurveillanceStation.INFO_API_KEY in available_apis
and cam_data_current.live_view.rtsp != cam_data_new.live_view.rtsp and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis
): ):
async_dispatcher_send( coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api)
hass, await coordinator_switches.async_config_entry_first_refresh()
f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{entry.entry_id}_{cam_id}",
cam_data_new.live_view.rtsp,
)
return {"cameras": new_data}
async def async_coordinator_update_data_central() -> None:
"""Fetch all device and sensor data from api."""
try: try:
await api.async_update() await coordinator_switches.async_setup()
except Exception as err: except SYNOLOGY_CONNECTION_EXCEPTIONS as ex:
raise UpdateFailed(f"Error communicating with API: {err}") from err raise ConfigEntryNotReady from ex
return None
async def async_coordinator_update_data_switches() -> dict[ synology_data = SynologyDSMData(
str, dict[str, Any] api=api,
] | None: coordinator_central=coordinator_central,
"""Fetch all switch data from api.""" coordinator_cameras=coordinator_cameras,
if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: coordinator_switches=coordinator_switches,
raise UpdateFailed("System not fully loaded")
if SynoSurveillanceStation.HOME_MODE_API_KEY not in api.dsm.apis:
return None
surveillance_station = api.surveillance_station
return {
"switches": {
"home_mode": await hass.async_add_executor_job(
surveillance_station.get_home_mode_status
) )
} hass.data.setdefault(DOMAIN, {})[entry.unique_id] = synology_data
}
hass.data[DOMAIN][entry.unique_id][COORDINATOR_CAMERAS] = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"{entry.unique_id}_cameras",
update_method=async_coordinator_update_data_cameras,
update_interval=timedelta(seconds=30),
)
hass.data[DOMAIN][entry.unique_id][COORDINATOR_CENTRAL] = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"{entry.unique_id}_central",
update_method=async_coordinator_update_data_central,
update_interval=timedelta(
minutes=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
),
)
hass.data[DOMAIN][entry.unique_id][COORDINATOR_SWITCHES] = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"{entry.unique_id}_switches",
update_method=async_coordinator_update_data_switches,
update_interval=timedelta(seconds=30),
)
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Synology DSM sensors.""" """Unload Synology DSM sensors."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
if unload_ok: entry_data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
entry_data = hass.data[DOMAIN][entry.unique_id] await entry_data.api.async_unload()
entry_data[UNDO_UPDATE_LISTENER]()
await entry_data[SYNO_API].async_unload()
hass.data[DOMAIN].pop(entry.unique_id) hass.data[DOMAIN].pop(entry.unique_id)
return unload_ok return unload_ok

View file

@ -22,12 +22,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import SynoApi from . import SynoApi
from .const import COORDINATOR_CENTRAL, DOMAIN, SYNO_API from .const import DOMAIN
from .entity import ( from .entity import (
SynologyDSMBaseEntity, SynologyDSMBaseEntity,
SynologyDSMDeviceEntity, SynologyDSMDeviceEntity,
SynologyDSMEntityDescription, SynologyDSMEntityDescription,
) )
from .models import SynologyDSMData
@dataclass @dataclass
@ -80,10 +81,9 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the Synology NAS binary sensor.""" """Set up the Synology NAS binary sensor."""
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
data = hass.data[DOMAIN][entry.unique_id] api = data.api
api: SynoApi = data[SYNO_API] coordinator = data.coordinator_central
coordinator = data[COORDINATOR_CENTRAL]
entities: list[ entities: list[
SynoDSMSecurityBinarySensor SynoDSMSecurityBinarySensor

View file

@ -17,7 +17,8 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SynoApi from . import SynoApi
from .const import DOMAIN, SYNO_API from .const import DOMAIN
from .models import SynologyDSMData
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -60,10 +61,8 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set buttons for device.""" """Set buttons for device."""
data = hass.data[DOMAIN][entry.unique_id] data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
syno_api: SynoApi = data[SYNO_API] async_add_entities(SynologyDSMButton(data.api, button) for button in BUTTONS)
async_add_entities(SynologyDSMButton(syno_api, button) for button in BUTTONS)
class SynologyDSMButton(ButtonEntity): class SynologyDSMButton(ButtonEntity):

View file

@ -25,13 +25,12 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import SynoApi from . import SynoApi
from .const import ( from .const import (
CONF_SNAPSHOT_QUALITY, CONF_SNAPSHOT_QUALITY,
COORDINATOR_CAMERAS,
DEFAULT_SNAPSHOT_QUALITY, DEFAULT_SNAPSHOT_QUALITY,
DOMAIN, DOMAIN,
SIGNAL_CAMERA_SOURCE_CHANGED, SIGNAL_CAMERA_SOURCE_CHANGED,
SYNO_API,
) )
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
from .models import SynologyDSMData
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -47,21 +46,10 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the Synology NAS cameras.""" """Set up the Synology NAS cameras."""
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
data = hass.data[DOMAIN][entry.unique_id] if coordinator := data.coordinator_cameras:
api: SynoApi = data[SYNO_API]
if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis:
return
# initial data fetch
coordinator: DataUpdateCoordinator[dict[str, dict[str, SynoCamera]]] = data[
COORDINATOR_CAMERAS
]
await coordinator.async_config_entry_first_refresh()
async_add_entities( async_add_entities(
SynoDSMCamera(api, coordinator, camera_id) SynoDSMCamera(data.api, coordinator, camera_id)
for camera_id in coordinator.data["cameras"] for camera_id in coordinator.data["cameras"]
) )

View file

@ -32,7 +32,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from .const import CONF_DEVICE_TOKEN, DOMAIN, SYSTEM_LOADED from .const import CONF_DEVICE_TOKEN
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -217,11 +217,6 @@ class SynoApi:
) )
self.surveillance_station = self.dsm.surveillance_station self.surveillance_station = self.dsm.surveillance_station
def _set_system_loaded(self, state: bool = False) -> None:
"""Set system loaded flag."""
dsm_device = self._hass.data[DOMAIN].get(self.information.serial)
dsm_device[SYSTEM_LOADED] = state
async def _syno_api_executer(self, api_call: Callable) -> None: async def _syno_api_executer(self, api_call: Callable) -> None:
"""Synology api call wrapper.""" """Synology api call wrapper."""
try: try:
@ -235,12 +230,10 @@ 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) await self._syno_api_executer(self.system.reboot)
self._set_system_loaded()
async def async_shutdown(self) -> None: async def async_shutdown(self) -> None:
"""Shutdown NAS.""" """Shutdown NAS."""
await self._syno_api_executer(self.system.shutdown) await self._syno_api_executer(self.system.shutdown)
self._set_system_loaded()
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."""

View file

@ -2,6 +2,15 @@
from __future__ import annotations from __future__ import annotations
from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED
from synology_dsm.exceptions import (
SynologyDSMAPIErrorException,
SynologyDSMLogin2SARequiredException,
SynologyDSMLoginDisabledAccountException,
SynologyDSMLoginFailedException,
SynologyDSMLoginInvalidException,
SynologyDSMLoginPermissionDeniedException,
SynologyDSMRequestException,
)
from homeassistant.const import Platform from homeassistant.const import Platform
@ -15,17 +24,9 @@ PLATFORMS = [
Platform.SWITCH, Platform.SWITCH,
Platform.UPDATE, Platform.UPDATE,
] ]
COORDINATOR_CAMERAS = "coordinator_cameras"
COORDINATOR_CENTRAL = "coordinator_central"
COORDINATOR_SWITCHES = "coordinator_switches"
SYSTEM_LOADED = "system_loaded"
EXCEPTION_DETAILS = "details" EXCEPTION_DETAILS = "details"
EXCEPTION_UNKNOWN = "unknown" EXCEPTION_UNKNOWN = "unknown"
# Entry keys
SYNO_API = "syno_api"
UNDO_UPDATE_LISTENER = "undo_update_listener"
# Configuration # Configuration
CONF_SERIAL = "serial" CONF_SERIAL = "serial"
CONF_VOLUMES = "volumes" CONF_VOLUMES = "volumes"
@ -53,3 +54,16 @@ SERVICES = [
SERVICE_REBOOT, SERVICE_REBOOT,
SERVICE_SHUTDOWN, SERVICE_SHUTDOWN,
] ]
SYNOLOGY_AUTH_FAILED_EXCEPTIONS = (
SynologyDSMLogin2SARequiredException,
SynologyDSMLoginDisabledAccountException,
SynologyDSMLoginInvalidException,
SynologyDSMLoginPermissionDeniedException,
)
SYNOLOGY_CONNECTION_EXCEPTIONS = (
SynologyDSMAPIErrorException,
SynologyDSMLoginFailedException,
SynologyDSMRequestException,
)

View file

@ -0,0 +1,148 @@
"""synology_dsm coordinators."""
from __future__ import annotations
from datetime import timedelta
import logging
import async_timeout
from synology_dsm.api.surveillance_station.camera import SynoCamera
from synology_dsm.exceptions import SynologyDSMAPIErrorException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .common import SynoApi
from .const import (
DEFAULT_SCAN_INTERVAL,
SIGNAL_CAMERA_SOURCE_CHANGED,
SYNOLOGY_CONNECTION_EXCEPTIONS,
)
_LOGGER = logging.getLogger(__name__)
class SynologyDSMUpdateCoordinator(DataUpdateCoordinator):
"""DataUpdateCoordinator base class for synology_dsm."""
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
api: SynoApi,
update_interval: timedelta,
) -> None:
"""Initialize synology_dsm DataUpdateCoordinator."""
self.api = api
self.entry = entry
super().__init__(
hass,
_LOGGER,
name=f"{entry.title} {self.__class__.__name__}",
update_interval=update_interval,
)
class SynologyDSMSwitchUpdateCoordinator(SynologyDSMUpdateCoordinator):
"""DataUpdateCoordinator to gather data for a synology_dsm switch devices."""
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
api: SynoApi,
) -> None:
"""Initialize DataUpdateCoordinator for switch devices."""
super().__init__(hass, entry, api, timedelta(seconds=30))
self.version: str | None = None
async def async_setup(self) -> None:
"""Set up the coordinator initial data."""
info = await self.hass.async_add_executor_job(
self.api.dsm.surveillance_station.get_info
)
self.version = info["data"]["CMSMinVersion"]
async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None:
"""Fetch all data from api."""
surveillance_station = self.api.surveillance_station
return {
"switches": {
"home_mode": await self.hass.async_add_executor_job(
surveillance_station.get_home_mode_status
)
}
}
class SynologyDSMCentralUpdateCoordinator(SynologyDSMUpdateCoordinator):
"""DataUpdateCoordinator to gather data for a synology_dsm central device."""
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
api: SynoApi,
) -> None:
"""Initialize DataUpdateCoordinator for central device."""
super().__init__(
hass,
entry,
api,
timedelta(
minutes=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
),
)
async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None:
"""Fetch all data from api."""
try:
await self.api.async_update()
except SYNOLOGY_CONNECTION_EXCEPTIONS as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
return None
class SynologyDSMCameraUpdateCoordinator(SynologyDSMUpdateCoordinator):
"""DataUpdateCoordinator to gather data for a synology_dsm cameras."""
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
api: SynoApi,
) -> None:
"""Initialize DataUpdateCoordinator for cameras."""
super().__init__(hass, entry, api, timedelta(seconds=30))
async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None:
"""Fetch all camera data from api."""
surveillance_station = self.api.surveillance_station
current_data: dict[str, SynoCamera] = {
camera.id: camera for camera in surveillance_station.get_all_cameras()
}
try:
async with async_timeout.timeout(30):
await self.hass.async_add_executor_job(surveillance_station.update)
except SynologyDSMAPIErrorException as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
new_data: dict[str, SynoCamera] = {
camera.id: camera for camera in surveillance_station.get_all_cameras()
}
for cam_id, cam_data_new in new_data.items():
if (
(cam_data_current := current_data.get(cam_id)) is not None
and cam_data_current.live_view.rtsp != cam_data_new.live_view.rtsp
):
async_dispatcher_send(
self.hass,
f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{self.entry.entry_id}_{cam_id}",
cam_data_new.live_view.rtsp,
)
return {"cameras": new_data}

View file

@ -8,8 +8,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import SynoApi from .const import CONF_DEVICE_TOKEN, DOMAIN
from .const import CONF_DEVICE_TOKEN, DOMAIN, SYNO_API, SYSTEM_LOADED from .models import SynologyDSMData
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_DEVICE_TOKEN} TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_DEVICE_TOKEN}
@ -18,8 +18,8 @@ async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: ConfigEntry
) -> dict: ) -> dict:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
data: dict = hass.data[DOMAIN][entry.unique_id] data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
syno_api: SynoApi = data[SYNO_API] syno_api = data.api
dsm_info = syno_api.dsm.information dsm_info = syno_api.dsm.information
diag_data = { diag_data = {
@ -36,7 +36,7 @@ async def async_get_config_entry_diagnostics(
"surveillance_station": {"cameras": {}}, "surveillance_station": {"cameras": {}},
"upgrade": {}, "upgrade": {},
"utilisation": {}, "utilisation": {},
"is_system_loaded": data[SYSTEM_LOADED], "is_system_loaded": True,
"api_details": { "api_details": {
"fetching_entities": syno_api._fetching_entities, # pylint: disable=protected-access "fetching_entities": syno_api._fetching_entities, # pylint: disable=protected-access
}, },
@ -45,7 +45,7 @@ async def async_get_config_entry_diagnostics(
if syno_api.network is not None: if syno_api.network is not None:
intf: dict 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: ignore[index]
"type": intf["type"], "type": intf["type"],
"ip": intf["ip"], "ip": intf["ip"],
} }
@ -53,7 +53,7 @@ async def async_get_config_entry_diagnostics(
if syno_api.storage is not None: if syno_api.storage is not None:
disk: dict 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"]] = { # type: ignore[index]
"name": disk["name"], "name": disk["name"],
"vendor": disk["vendor"], "vendor": disk["vendor"],
"model": disk["model"], "model": disk["model"],
@ -64,7 +64,7 @@ async def async_get_config_entry_diagnostics(
volume: dict 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"]] = { # type: ignore[index]
"name": volume["fs_type"], "name": volume["fs_type"],
"size": volume["size"], "size": volume["size"],
} }
@ -72,7 +72,7 @@ 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 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] = { # type: ignore[index]
"name": camera.name, "name": camera.name,
"is_enabled": camera.is_enabled, "is_enabled": camera.is_enabled,
"is_motion_detection_enabled": camera.is_motion_detection_enabled, "is_motion_detection_enabled": camera.is_motion_detection_enabled,

View file

@ -0,0 +1,21 @@
"""The synology_dsm integration models."""
from __future__ import annotations
from dataclasses import dataclass
from .common import SynoApi
from .coordinator import (
SynologyDSMCameraUpdateCoordinator,
SynologyDSMCentralUpdateCoordinator,
SynologyDSMSwitchUpdateCoordinator,
)
@dataclass
class SynologyDSMData:
"""Data for the synology_dsm integration."""
api: SynoApi
coordinator_central: SynologyDSMCentralUpdateCoordinator
coordinator_cameras: SynologyDSMCameraUpdateCoordinator | None
coordinator_switches: SynologyDSMSwitchUpdateCoordinator | None

View file

@ -31,12 +31,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from . import SynoApi from . import SynoApi
from .const import CONF_VOLUMES, COORDINATOR_CENTRAL, DOMAIN, ENTITY_UNIT_LOAD, SYNO_API from .const import CONF_VOLUMES, DOMAIN, ENTITY_UNIT_LOAD
from .entity import ( from .entity import (
SynologyDSMBaseEntity, SynologyDSMBaseEntity,
SynologyDSMDeviceEntity, SynologyDSMDeviceEntity,
SynologyDSMEntityDescription, SynologyDSMEntityDescription,
) )
from .models import SynologyDSMData
@dataclass @dataclass
@ -279,10 +280,9 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the Synology NAS Sensor.""" """Set up the Synology NAS Sensor."""
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
data = hass.data[DOMAIN][entry.unique_id] api = data.api
api: SynoApi = data[SYNO_API] coordinator = data.coordinator_central
coordinator = data[COORDINATOR_CENTRAL]
entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [ entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [
SynoDSMUtilSensor(api, coordinator, description) SynoDSMUtilSensor(api, coordinator, description)

View file

@ -7,15 +7,8 @@ from synology_dsm.exceptions import SynologyDSMException
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from .common import SynoApi from .const import CONF_SERIAL, DOMAIN, SERVICE_REBOOT, SERVICE_SHUTDOWN, SERVICES
from .const import ( from .models import SynologyDSMData
CONF_SERIAL,
DOMAIN,
SERVICE_REBOOT,
SERVICE_SHUTDOWN,
SERVICES,
SYNO_API,
)
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -29,7 +22,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
dsm_devices = hass.data[DOMAIN] dsm_devices = hass.data[DOMAIN]
if serial: if serial:
dsm_device = dsm_devices.get(serial) dsm_device: SynologyDSMData = hass.data[DOMAIN][serial]
elif len(dsm_devices) == 1: elif len(dsm_devices) == 1:
dsm_device = next(iter(dsm_devices.values())) dsm_device = next(iter(dsm_devices.values()))
serial = next(iter(dsm_devices)) serial = next(iter(dsm_devices))
@ -45,7 +38,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
return return
if call.service in [SERVICE_REBOOT, SERVICE_SHUTDOWN]: if call.service in [SERVICE_REBOOT, SERVICE_SHUTDOWN]:
if not (dsm_device := hass.data[DOMAIN].get(serial)): if serial not in hass.data[DOMAIN]:
LOGGER.error("DSM with specified serial %s not found", serial) LOGGER.error("DSM with specified serial %s not found", serial)
return return
LOGGER.debug("%s DSM with serial %s", call.service, serial) LOGGER.debug("%s DSM with serial %s", call.service, serial)
@ -53,7 +46,8 @@ async def async_setup_services(hass: HomeAssistant) -> None:
"The %s service is deprecated and will be removed in future release. Please use the corresponding button entity", "The %s service is deprecated and will be removed in future release. Please use the corresponding button entity",
call.service, call.service,
) )
dsm_api: SynoApi = dsm_device[SYNO_API] dsm_device = hass.data[DOMAIN][serial]
dsm_api = dsm_device.api
try: try:
await getattr(dsm_api, f"async_{call.service}")() await getattr(dsm_api, f"async_{call.service}")()
except SynologyDSMException as ex: except SynologyDSMException as ex:

View file

@ -15,8 +15,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import SynoApi from . import SynoApi
from .const import COORDINATOR_SWITCHES, DOMAIN, SYNO_API from .const import DOMAIN
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
from .models import SynologyDSMData
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -42,30 +43,16 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the Synology NAS switch.""" """Set up the Synology NAS switch."""
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
data = hass.data[DOMAIN][entry.unique_id] if coordinator := data.coordinator_switches:
api: SynoApi = data[SYNO_API] assert coordinator.version is not None
async_add_entities(
entities = []
if SynoSurveillanceStation.INFO_API_KEY in api.dsm.apis:
info = await hass.async_add_executor_job(api.dsm.surveillance_station.get_info)
version = info["data"]["CMSMinVersion"]
# initial data fetch
coordinator: DataUpdateCoordinator = data[COORDINATOR_SWITCHES]
await coordinator.async_refresh()
entities.extend(
[
SynoDSMSurveillanceHomeModeToggle( SynoDSMSurveillanceHomeModeToggle(
api, version, coordinator, description data.api, coordinator.version, coordinator, description
) )
for description in SURVEILLANCE_SWITCH for description in SURVEILLANCE_SWITCH
]
) )
async_add_entities(entities, True)
class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, SwitchEntity): class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, SwitchEntity):
"""Representation a Synology Surveillance Station Home Mode toggle.""" """Representation a Synology Surveillance Station Home Mode toggle."""

View file

@ -13,9 +13,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SynoApi from .const import DOMAIN
from .const import COORDINATOR_CENTRAL, DOMAIN, SYNO_API
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
from .models import SynologyDSMData
@dataclass @dataclass
@ -39,12 +39,9 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up Synology DSM update entities.""" """Set up Synology DSM update entities."""
data = hass.data[DOMAIN][entry.unique_id] data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
api: SynoApi = data[SYNO_API]
coordinator = data[COORDINATOR_CENTRAL]
async_add_entities( async_add_entities(
SynoDSMUpdateEntity(api, coordinator, description) SynoDSMUpdateEntity(data.api, data.coordinator_central, description)
for description in UPDATE_ENTITIES for description in UPDATE_ENTITIES
) )

View file

@ -24,9 +24,9 @@ from tests.common import MockConfigEntry
@pytest.mark.no_bypass_setup @pytest.mark.no_bypass_setup
async def test_services_registered(hass: HomeAssistant): async def test_services_registered(hass: HomeAssistant):
"""Test if all services are registered.""" """Test if all services are registered."""
with patch( with patch("homeassistant.components.synology_dsm.common.SynologyDSM"), patch(
"homeassistant.components.synology_dsm.SynoApi.async_setup", return_value=True "homeassistant.components.synology_dsm.PLATFORMS", return_value=[]
), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): ):
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={ data={