Cleanup coordinators in synology_dsm (#73257)
Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>pull/73293/head
parent
1d6068fa09
commit
22daea27c2
|
@ -1200,6 +1200,7 @@ omit =
|
|||
homeassistant/components/synology_dsm/binary_sensor.py
|
||||
homeassistant/components/synology_dsm/button.py
|
||||
homeassistant/components/synology_dsm/camera.py
|
||||
homeassistant/components/synology_dsm/coordinator.py
|
||||
homeassistant/components/synology_dsm/diagnostics.py
|
||||
homeassistant/components/synology_dsm/common.py
|
||||
homeassistant/components/synology_dsm/entity.py
|
||||
|
|
|
@ -1,47 +1,32 @@
|
|||
"""The Synology DSM component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import async_timeout
|
||||
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.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.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
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 .const import (
|
||||
COORDINATOR_CAMERAS,
|
||||
COORDINATOR_CENTRAL,
|
||||
COORDINATOR_SWITCHES,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DEFAULT_VERIFY_SSL,
|
||||
DOMAIN,
|
||||
EXCEPTION_DETAILS,
|
||||
EXCEPTION_UNKNOWN,
|
||||
PLATFORMS,
|
||||
SIGNAL_CAMERA_SOURCE_CHANGED,
|
||||
SYNO_API,
|
||||
SYSTEM_LOADED,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
SYNOLOGY_AUTH_FAILED_EXCEPTIONS,
|
||||
SYNOLOGY_CONNECTION_EXCEPTIONS,
|
||||
)
|
||||
from .coordinator import (
|
||||
SynologyDSMCameraUpdateCoordinator,
|
||||
SynologyDSMCentralUpdateCoordinator,
|
||||
SynologyDSMSwitchUpdateCoordinator,
|
||||
)
|
||||
from .models import SynologyDSMData
|
||||
from .service import async_setup_services
|
||||
|
||||
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)
|
||||
try:
|
||||
await api.async_setup()
|
||||
except (
|
||||
SynologyDSMLogin2SARequiredException,
|
||||
SynologyDSMLoginDisabledAccountException,
|
||||
SynologyDSMLoginInvalidException,
|
||||
SynologyDSMLoginPermissionDeniedException,
|
||||
) as err:
|
||||
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
|
||||
except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as 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)
|
||||
else:
|
||||
details = EXCEPTION_UNKNOWN
|
||||
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
|
||||
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}
|
||||
)
|
||||
|
||||
async def async_coordinator_update_data_cameras() -> dict[
|
||||
str, dict[str, SynoCamera]
|
||||
] | None:
|
||||
"""Fetch all camera data from api."""
|
||||
if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]:
|
||||
raise UpdateFailed("System not fully loaded")
|
||||
# These all create executor jobs so we do not gather here
|
||||
coordinator_central = SynologyDSMCentralUpdateCoordinator(hass, entry, api)
|
||||
await coordinator_central.async_config_entry_first_refresh()
|
||||
|
||||
if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis:
|
||||
return None
|
||||
available_apis = api.dsm.apis
|
||||
|
||||
surveillance_station = api.surveillance_station
|
||||
current_data: dict[str, SynoCamera] = {
|
||||
camera.id: camera for camera in surveillance_station.get_all_cameras()
|
||||
}
|
||||
# The central coordinator needs to be refreshed first since
|
||||
# the next two rely on data from it
|
||||
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()
|
||||
|
||||
coordinator_switches: SynologyDSMSwitchUpdateCoordinator | None = None
|
||||
if (
|
||||
SynoSurveillanceStation.INFO_API_KEY in available_apis
|
||||
and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis
|
||||
):
|
||||
coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api)
|
||||
await coordinator_switches.async_config_entry_first_refresh()
|
||||
try:
|
||||
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
|
||||
await coordinator_switches.async_setup()
|
||||
except SYNOLOGY_CONNECTION_EXCEPTIONS as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
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(
|
||||
hass,
|
||||
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:
|
||||
await api.async_update()
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
return None
|
||||
|
||||
async def async_coordinator_update_data_switches() -> dict[
|
||||
str, dict[str, Any]
|
||||
] | None:
|
||||
"""Fetch all switch data from api."""
|
||||
if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]:
|
||||
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[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),
|
||||
synology_data = SynologyDSMData(
|
||||
api=api,
|
||||
coordinator_central=coordinator_central,
|
||||
coordinator_cameras=coordinator_cameras,
|
||||
coordinator_switches=coordinator_switches,
|
||||
)
|
||||
|
||||
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.data.setdefault(DOMAIN, {})[entry.unique_id] = synology_data
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Synology DSM sensors."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
entry_data = hass.data[DOMAIN][entry.unique_id]
|
||||
entry_data[UNDO_UPDATE_LISTENER]()
|
||||
await entry_data[SYNO_API].async_unload()
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
entry_data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
||||
await entry_data.api.async_unload()
|
||||
hass.data[DOMAIN].pop(entry.unique_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
|
|
|
@ -22,12 +22,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import SynoApi
|
||||
from .const import COORDINATOR_CENTRAL, DOMAIN, SYNO_API
|
||||
from .const import DOMAIN
|
||||
from .entity import (
|
||||
SynologyDSMBaseEntity,
|
||||
SynologyDSMDeviceEntity,
|
||||
SynologyDSMEntityDescription,
|
||||
)
|
||||
from .models import SynologyDSMData
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -80,10 +81,9 @@ async def async_setup_entry(
|
|||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Synology NAS binary sensor."""
|
||||
|
||||
data = hass.data[DOMAIN][entry.unique_id]
|
||||
api: SynoApi = data[SYNO_API]
|
||||
coordinator = data[COORDINATOR_CENTRAL]
|
||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
||||
api = data.api
|
||||
coordinator = data.coordinator_central
|
||||
|
||||
entities: list[
|
||||
SynoDSMSecurityBinarySensor
|
||||
|
|
|
@ -17,7 +17,8 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import SynoApi
|
||||
from .const import DOMAIN, SYNO_API
|
||||
from .const import DOMAIN
|
||||
from .models import SynologyDSMData
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -60,10 +61,8 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set buttons for device."""
|
||||
data = hass.data[DOMAIN][entry.unique_id]
|
||||
syno_api: SynoApi = data[SYNO_API]
|
||||
|
||||
async_add_entities(SynologyDSMButton(syno_api, button) for button in BUTTONS)
|
||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
||||
async_add_entities(SynologyDSMButton(data.api, button) for button in BUTTONS)
|
||||
|
||||
|
||||
class SynologyDSMButton(ButtonEntity):
|
||||
|
|
|
@ -25,13 +25,12 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|||
from . import SynoApi
|
||||
from .const import (
|
||||
CONF_SNAPSHOT_QUALITY,
|
||||
COORDINATOR_CAMERAS,
|
||||
DEFAULT_SNAPSHOT_QUALITY,
|
||||
DOMAIN,
|
||||
SIGNAL_CAMERA_SOURCE_CHANGED,
|
||||
SYNO_API,
|
||||
)
|
||||
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
||||
from .models import SynologyDSMData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -47,23 +46,12 @@ async def async_setup_entry(
|
|||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Synology NAS cameras."""
|
||||
|
||||
data = hass.data[DOMAIN][entry.unique_id]
|
||||
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(
|
||||
SynoDSMCamera(api, coordinator, camera_id)
|
||||
for camera_id in coordinator.data["cameras"]
|
||||
)
|
||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
||||
if coordinator := data.coordinator_cameras:
|
||||
async_add_entities(
|
||||
SynoDSMCamera(data.api, coordinator, camera_id)
|
||||
for camera_id in coordinator.data["cameras"]
|
||||
)
|
||||
|
||||
|
||||
class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
|
||||
|
|
|
@ -32,7 +32,7 @@ from homeassistant.const import (
|
|||
)
|
||||
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__)
|
||||
|
||||
|
@ -217,11 +217,6 @@ class SynoApi:
|
|||
)
|
||||
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:
|
||||
"""Synology api call wrapper."""
|
||||
try:
|
||||
|
@ -235,12 +230,10 @@ class SynoApi:
|
|||
async def async_reboot(self) -> None:
|
||||
"""Reboot NAS."""
|
||||
await self._syno_api_executer(self.system.reboot)
|
||||
self._set_system_loaded()
|
||||
|
||||
async def async_shutdown(self) -> None:
|
||||
"""Shutdown NAS."""
|
||||
await self._syno_api_executer(self.system.shutdown)
|
||||
self._set_system_loaded()
|
||||
|
||||
async def async_unload(self) -> None:
|
||||
"""Stop interacting with the NAS and prepare for removal from hass."""
|
||||
|
|
|
@ -2,6 +2,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
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
|
||||
|
||||
|
@ -15,17 +24,9 @@ PLATFORMS = [
|
|||
Platform.SWITCH,
|
||||
Platform.UPDATE,
|
||||
]
|
||||
COORDINATOR_CAMERAS = "coordinator_cameras"
|
||||
COORDINATOR_CENTRAL = "coordinator_central"
|
||||
COORDINATOR_SWITCHES = "coordinator_switches"
|
||||
SYSTEM_LOADED = "system_loaded"
|
||||
EXCEPTION_DETAILS = "details"
|
||||
EXCEPTION_UNKNOWN = "unknown"
|
||||
|
||||
# Entry keys
|
||||
SYNO_API = "syno_api"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
|
||||
# Configuration
|
||||
CONF_SERIAL = "serial"
|
||||
CONF_VOLUMES = "volumes"
|
||||
|
@ -53,3 +54,16 @@ SERVICES = [
|
|||
SERVICE_REBOOT,
|
||||
SERVICE_SHUTDOWN,
|
||||
]
|
||||
|
||||
SYNOLOGY_AUTH_FAILED_EXCEPTIONS = (
|
||||
SynologyDSMLogin2SARequiredException,
|
||||
SynologyDSMLoginDisabledAccountException,
|
||||
SynologyDSMLoginInvalidException,
|
||||
SynologyDSMLoginPermissionDeniedException,
|
||||
)
|
||||
|
||||
SYNOLOGY_CONNECTION_EXCEPTIONS = (
|
||||
SynologyDSMAPIErrorException,
|
||||
SynologyDSMLoginFailedException,
|
||||
SynologyDSMRequestException,
|
||||
)
|
||||
|
|
|
@ -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}
|
|
@ -8,8 +8,8 @@ from homeassistant.config_entries import ConfigEntry
|
|||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import SynoApi
|
||||
from .const import CONF_DEVICE_TOKEN, DOMAIN, SYNO_API, SYSTEM_LOADED
|
||||
from .const import CONF_DEVICE_TOKEN, DOMAIN
|
||||
from .models import SynologyDSMData
|
||||
|
||||
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_DEVICE_TOKEN}
|
||||
|
||||
|
@ -18,8 +18,8 @@ async def async_get_config_entry_diagnostics(
|
|||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict:
|
||||
"""Return diagnostics for a config entry."""
|
||||
data: dict = hass.data[DOMAIN][entry.unique_id]
|
||||
syno_api: SynoApi = data[SYNO_API]
|
||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
||||
syno_api = data.api
|
||||
dsm_info = syno_api.dsm.information
|
||||
|
||||
diag_data = {
|
||||
|
@ -36,7 +36,7 @@ async def async_get_config_entry_diagnostics(
|
|||
"surveillance_station": {"cameras": {}},
|
||||
"upgrade": {},
|
||||
"utilisation": {},
|
||||
"is_system_loaded": data[SYSTEM_LOADED],
|
||||
"is_system_loaded": True,
|
||||
"api_details": {
|
||||
"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:
|
||||
intf: dict
|
||||
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"],
|
||||
"ip": intf["ip"],
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ 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"]] = {
|
||||
diag_data["storage"]["disks"][disk["id"]] = { # type: ignore[index]
|
||||
"name": disk["name"],
|
||||
"vendor": disk["vendor"],
|
||||
"model": disk["model"],
|
||||
|
@ -64,7 +64,7 @@ async def async_get_config_entry_diagnostics(
|
|||
|
||||
volume: dict
|
||||
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"],
|
||||
"size": volume["size"],
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ 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] = {
|
||||
diag_data["surveillance_station"]["cameras"][camera.id] = { # type: ignore[index]
|
||||
"name": camera.name,
|
||||
"is_enabled": camera.is_enabled,
|
||||
"is_motion_detection_enabled": camera.is_motion_detection_enabled,
|
||||
|
|
|
@ -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
|
|
@ -31,12 +31,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|||
from homeassistant.util.dt import utcnow
|
||||
|
||||
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 (
|
||||
SynologyDSMBaseEntity,
|
||||
SynologyDSMDeviceEntity,
|
||||
SynologyDSMEntityDescription,
|
||||
)
|
||||
from .models import SynologyDSMData
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -279,10 +280,9 @@ async def async_setup_entry(
|
|||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Synology NAS Sensor."""
|
||||
|
||||
data = hass.data[DOMAIN][entry.unique_id]
|
||||
api: SynoApi = data[SYNO_API]
|
||||
coordinator = data[COORDINATOR_CENTRAL]
|
||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
||||
api = data.api
|
||||
coordinator = data.coordinator_central
|
||||
|
||||
entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [
|
||||
SynoDSMUtilSensor(api, coordinator, description)
|
||||
|
|
|
@ -7,15 +7,8 @@ from synology_dsm.exceptions import SynologyDSMException
|
|||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from .common import SynoApi
|
||||
from .const import (
|
||||
CONF_SERIAL,
|
||||
DOMAIN,
|
||||
SERVICE_REBOOT,
|
||||
SERVICE_SHUTDOWN,
|
||||
SERVICES,
|
||||
SYNO_API,
|
||||
)
|
||||
from .const import CONF_SERIAL, DOMAIN, SERVICE_REBOOT, SERVICE_SHUTDOWN, SERVICES
|
||||
from .models import SynologyDSMData
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -29,7 +22,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
|||
dsm_devices = hass.data[DOMAIN]
|
||||
|
||||
if serial:
|
||||
dsm_device = dsm_devices.get(serial)
|
||||
dsm_device: SynologyDSMData = hass.data[DOMAIN][serial]
|
||||
elif len(dsm_devices) == 1:
|
||||
dsm_device = next(iter(dsm_devices.values()))
|
||||
serial = next(iter(dsm_devices))
|
||||
|
@ -45,7 +38,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
|||
return
|
||||
|
||||
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)
|
||||
return
|
||||
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",
|
||||
call.service,
|
||||
)
|
||||
dsm_api: SynoApi = dsm_device[SYNO_API]
|
||||
dsm_device = hass.data[DOMAIN][serial]
|
||||
dsm_api = dsm_device.api
|
||||
try:
|
||||
await getattr(dsm_api, f"async_{call.service}")()
|
||||
except SynologyDSMException as ex:
|
||||
|
|
|
@ -15,8 +15,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import SynoApi
|
||||
from .const import COORDINATOR_SWITCHES, DOMAIN, SYNO_API
|
||||
from .const import DOMAIN
|
||||
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
||||
from .models import SynologyDSMData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -42,30 +43,16 @@ async def async_setup_entry(
|
|||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Synology NAS switch."""
|
||||
|
||||
data = hass.data[DOMAIN][entry.unique_id]
|
||||
api: SynoApi = data[SYNO_API]
|
||||
|
||||
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(
|
||||
api, version, coordinator, description
|
||||
)
|
||||
for description in SURVEILLANCE_SWITCH
|
||||
]
|
||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
||||
if coordinator := data.coordinator_switches:
|
||||
assert coordinator.version is not None
|
||||
async_add_entities(
|
||||
SynoDSMSurveillanceHomeModeToggle(
|
||||
data.api, coordinator.version, coordinator, description
|
||||
)
|
||||
for description in SURVEILLANCE_SWITCH
|
||||
)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, SwitchEntity):
|
||||
"""Representation a Synology Surveillance Station Home Mode toggle."""
|
||||
|
|
|
@ -13,9 +13,9 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import SynoApi
|
||||
from .const import COORDINATOR_CENTRAL, DOMAIN, SYNO_API
|
||||
from .const import DOMAIN
|
||||
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
||||
from .models import SynologyDSMData
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -39,12 +39,9 @@ async def async_setup_entry(
|
|||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up Synology DSM update entities."""
|
||||
data = hass.data[DOMAIN][entry.unique_id]
|
||||
api: SynoApi = data[SYNO_API]
|
||||
coordinator = data[COORDINATOR_CENTRAL]
|
||||
|
||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
||||
async_add_entities(
|
||||
SynoDSMUpdateEntity(api, coordinator, description)
|
||||
SynoDSMUpdateEntity(data.api, data.coordinator_central, description)
|
||||
for description in UPDATE_ENTITIES
|
||||
)
|
||||
|
||||
|
|
|
@ -24,9 +24,9 @@ from tests.common import MockConfigEntry
|
|||
@pytest.mark.no_bypass_setup
|
||||
async def test_services_registered(hass: HomeAssistant):
|
||||
"""Test if all services are registered."""
|
||||
with patch(
|
||||
"homeassistant.components.synology_dsm.SynoApi.async_setup", return_value=True
|
||||
), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]):
|
||||
with patch("homeassistant.components.synology_dsm.common.SynologyDSM"), patch(
|
||||
"homeassistant.components.synology_dsm.PLATFORMS", return_value=[]
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
|
|
Loading…
Reference in New Issue