mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Rework Synology DSM to use config entry runtime_data (#141084)
rework to use config entry runtime_data
This commit is contained in:
parent
d8a5881eaa
commit
489c486278
@ -9,7 +9,6 @@ from synology_dsm.api.surveillance_station import SynoSurveillanceStation
|
|||||||
from synology_dsm.api.surveillance_station.camera import SynoCamera
|
from synology_dsm.api.surveillance_station.camera import SynoCamera
|
||||||
from synology_dsm.exceptions import SynologyDSMNotLoggedInException
|
from synology_dsm.exceptions import SynologyDSMNotLoggedInException
|
||||||
|
|
||||||
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_SCAN_INTERVAL, CONF_VERIFY_SSL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
@ -31,15 +30,16 @@ from .const import (
|
|||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
SynologyDSMCameraUpdateCoordinator,
|
SynologyDSMCameraUpdateCoordinator,
|
||||||
SynologyDSMCentralUpdateCoordinator,
|
SynologyDSMCentralUpdateCoordinator,
|
||||||
|
SynologyDSMConfigEntry,
|
||||||
|
SynologyDSMData,
|
||||||
SynologyDSMSwitchUpdateCoordinator,
|
SynologyDSMSwitchUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from .models import SynologyDSMData
|
|
||||||
from .service import async_setup_services
|
from .service import async_setup_services
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: SynologyDSMConfigEntry) -> bool:
|
||||||
"""Set up Synology DSM sensors."""
|
"""Set up Synology DSM sensors."""
|
||||||
|
|
||||||
# Migrate device identifiers
|
# Migrate device identifiers
|
||||||
@ -120,13 +120,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
except SYNOLOGY_CONNECTION_EXCEPTIONS as ex:
|
except SYNOLOGY_CONNECTION_EXCEPTIONS as ex:
|
||||||
raise ConfigEntryNotReady from ex
|
raise ConfigEntryNotReady from ex
|
||||||
|
|
||||||
synology_data = SynologyDSMData(
|
entry.runtime_data = SynologyDSMData(
|
||||||
api=api,
|
api=api,
|
||||||
coordinator_central=coordinator_central,
|
coordinator_central=coordinator_central,
|
||||||
coordinator_cameras=coordinator_cameras,
|
coordinator_cameras=coordinator_cameras,
|
||||||
coordinator_switches=coordinator_switches,
|
coordinator_switches=coordinator_switches,
|
||||||
)
|
)
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.unique_id] = synology_data
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||||
|
|
||||||
@ -143,25 +142,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: SynologyDSMConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload Synology DSM sensors."""
|
"""Unload Synology DSM sensors."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
entry_data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
entry_data = entry.runtime_data
|
||||||
await entry_data.api.async_unload()
|
await entry_data.api.async_unload()
|
||||||
hass.data[DOMAIN].pop(entry.unique_id)
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def _async_update_listener(
|
||||||
|
hass: HomeAssistant, entry: SynologyDSMConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Handle options update."""
|
"""Handle options update."""
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_remove_config_entry_device(
|
async def async_remove_config_entry_device(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, device_entry: dr.DeviceEntry
|
hass: HomeAssistant, entry: SynologyDSMConfigEntry, device_entry: dr.DeviceEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""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 = entry.runtime_data
|
||||||
api = data.api
|
api = data.api
|
||||||
assert api.information is not None
|
assert api.information is not None
|
||||||
serial = api.information.serial
|
serial = api.information.serial
|
||||||
|
@ -17,7 +17,6 @@ from homeassistant.components.backup import (
|
|||||||
BackupNotFound,
|
BackupNotFound,
|
||||||
suggested_filename,
|
suggested_filename,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.aiohttp_client import ChunkAsyncStreamIterator
|
from homeassistant.helpers.aiohttp_client import ChunkAsyncStreamIterator
|
||||||
from homeassistant.helpers.json import json_dumps
|
from homeassistant.helpers.json import json_dumps
|
||||||
@ -29,7 +28,7 @@ from .const import (
|
|||||||
DATA_BACKUP_AGENT_LISTENERS,
|
DATA_BACKUP_AGENT_LISTENERS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from .models import SynologyDSMData
|
from .coordinator import SynologyDSMConfigEntry
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -47,18 +46,17 @@ async def async_get_backup_agents(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
) -> list[BackupAgent]:
|
) -> list[BackupAgent]:
|
||||||
"""Return a list of backup agents."""
|
"""Return a list of backup agents."""
|
||||||
if not (
|
entries: list[SynologyDSMConfigEntry] = hass.config_entries.async_loaded_entries(
|
||||||
entries := hass.config_entries.async_loaded_entries(DOMAIN)
|
DOMAIN
|
||||||
) or not hass.data.get(DOMAIN):
|
)
|
||||||
|
if not entries:
|
||||||
LOGGER.debug("No proper config entry found")
|
LOGGER.debug("No proper config entry found")
|
||||||
return []
|
return []
|
||||||
syno_datas: dict[str, SynologyDSMData] = hass.data[DOMAIN]
|
|
||||||
return [
|
return [
|
||||||
SynologyDSMBackupAgent(hass, entry, entry.unique_id)
|
SynologyDSMBackupAgent(hass, entry, entry.unique_id)
|
||||||
for entry in entries
|
for entry in entries
|
||||||
if entry.unique_id is not None
|
if entry.unique_id is not None
|
||||||
and (syno_data := syno_datas.get(entry.unique_id))
|
and entry.runtime_data.api.file_station
|
||||||
and syno_data.api.file_station
|
|
||||||
and entry.options.get(CONF_BACKUP_PATH)
|
and entry.options.get(CONF_BACKUP_PATH)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -91,7 +89,9 @@ class SynologyDSMBackupAgent(BackupAgent):
|
|||||||
|
|
||||||
domain = DOMAIN
|
domain = DOMAIN
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, unique_id: str) -> None:
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, entry: SynologyDSMConfigEntry, unique_id: str
|
||||||
|
) -> None:
|
||||||
"""Initialize the Synology DSM backup agent."""
|
"""Initialize the Synology DSM backup agent."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
LOGGER.debug("Initializing Synology DSM backup agent for %s", entry.unique_id)
|
LOGGER.debug("Initializing Synology DSM backup agent for %s", entry.unique_id)
|
||||||
@ -100,7 +100,7 @@ class SynologyDSMBackupAgent(BackupAgent):
|
|||||||
self.path = (
|
self.path = (
|
||||||
f"{entry.options[CONF_BACKUP_SHARE]}/{entry.options[CONF_BACKUP_PATH]}"
|
f"{entry.options[CONF_BACKUP_SHARE]}/{entry.options[CONF_BACKUP_PATH]}"
|
||||||
)
|
)
|
||||||
syno_data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
syno_data = entry.runtime_data
|
||||||
self.api = syno_data.api
|
self.api = syno_data.api
|
||||||
self.backup_base_names: dict[str, str] = {}
|
self.backup_base_names: dict[str, str] = {}
|
||||||
|
|
||||||
|
@ -12,20 +12,17 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_DISKS, EntityCategory
|
from homeassistant.const import CONF_DISKS, EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import SynoApi
|
from . import SynoApi
|
||||||
from .const import DOMAIN
|
from .coordinator import SynologyDSMCentralUpdateCoordinator, SynologyDSMConfigEntry
|
||||||
from .coordinator import SynologyDSMCentralUpdateCoordinator
|
|
||||||
from .entity import (
|
from .entity import (
|
||||||
SynologyDSMBaseEntity,
|
SynologyDSMBaseEntity,
|
||||||
SynologyDSMDeviceEntity,
|
SynologyDSMDeviceEntity,
|
||||||
SynologyDSMEntityDescription,
|
SynologyDSMEntityDescription,
|
||||||
)
|
)
|
||||||
from .models import SynologyDSMData
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@ -64,11 +61,11 @@ STORAGE_DISK_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: SynologyDSMConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> 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 = entry.runtime_data
|
||||||
api = data.api
|
api = data.api
|
||||||
coordinator = data.coordinator_central
|
coordinator = data.coordinator_central
|
||||||
assert api.storage is not None
|
assert api.storage is not None
|
||||||
|
@ -12,7 +12,6 @@ from homeassistant.components.button import (
|
|||||||
ButtonEntity,
|
ButtonEntity,
|
||||||
ButtonEntityDescription,
|
ButtonEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
@ -20,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
|
|
||||||
from . import SynoApi
|
from . import SynoApi
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .models import SynologyDSMData
|
from .coordinator import SynologyDSMConfigEntry
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -52,11 +51,11 @@ BUTTONS: Final = [
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: SynologyDSMConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set buttons for device."""
|
"""Set buttons for device."""
|
||||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
data = entry.runtime_data
|
||||||
async_add_entities(SynologyDSMButton(data.api, button) for button in BUTTONS)
|
async_add_entities(SynologyDSMButton(data.api, button) for button in BUTTONS)
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ from homeassistant.components.camera import (
|
|||||||
CameraEntityDescription,
|
CameraEntityDescription,
|
||||||
CameraEntityFeature,
|
CameraEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
@ -29,9 +28,8 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
SIGNAL_CAMERA_SOURCE_CHANGED,
|
SIGNAL_CAMERA_SOURCE_CHANGED,
|
||||||
)
|
)
|
||||||
from .coordinator import SynologyDSMCameraUpdateCoordinator
|
from .coordinator import SynologyDSMCameraUpdateCoordinator, SynologyDSMConfigEntry
|
||||||
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
||||||
from .models import SynologyDSMData
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -47,11 +45,11 @@ class SynologyDSMCameraEntityDescription(
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: SynologyDSMConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Synology NAS cameras."""
|
"""Set up the Synology NAS cameras."""
|
||||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
data = entry.runtime_data
|
||||||
if coordinator := data.coordinator_cameras:
|
if coordinator := data.coordinator_cameras:
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
SynoDSMCamera(data.api, coordinator, camera_id)
|
SynoDSMCamera(data.api, coordinator, camera_id)
|
||||||
|
@ -72,7 +72,7 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
SYNOLOGY_CONNECTION_EXCEPTIONS,
|
SYNOLOGY_CONNECTION_EXCEPTIONS,
|
||||||
)
|
)
|
||||||
from .models import SynologyDSMData
|
from .coordinator import SynologyDSMConfigEntry
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: ConfigEntry,
|
config_entry: SynologyDSMConfigEntry,
|
||||||
) -> SynologyDSMOptionsFlowHandler:
|
) -> SynologyDSMOptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return SynologyDSMOptionsFlowHandler()
|
return SynologyDSMOptionsFlowHandler()
|
||||||
@ -444,6 +444,8 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
class SynologyDSMOptionsFlowHandler(OptionsFlow):
|
class SynologyDSMOptionsFlowHandler(OptionsFlow):
|
||||||
"""Handle a option flow."""
|
"""Handle a option flow."""
|
||||||
|
|
||||||
|
config_entry: SynologyDSMConfigEntry
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@ -451,7 +453,7 @@ class SynologyDSMOptionsFlowHandler(OptionsFlow):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return self.async_create_entry(title="", data=user_input)
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
syno_data: SynologyDSMData = self.hass.data[DOMAIN][self.config_entry.unique_id]
|
syno_data = self.config_entry.runtime_data
|
||||||
|
|
||||||
data_schema = vol.Schema(
|
data_schema = vol.Schema(
|
||||||
{
|
{
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable, Coroutine
|
from collections.abc import Awaitable, Callable, Coroutine
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Concatenate
|
from typing import Any, Concatenate
|
||||||
@ -28,6 +29,19 @@ from .const import (
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SynologyDSMData:
|
||||||
|
"""Data for the synology_dsm integration."""
|
||||||
|
|
||||||
|
api: SynoApi
|
||||||
|
coordinator_central: SynologyDSMCentralUpdateCoordinator
|
||||||
|
coordinator_cameras: SynologyDSMCameraUpdateCoordinator | None
|
||||||
|
coordinator_switches: SynologyDSMSwitchUpdateCoordinator | None
|
||||||
|
|
||||||
|
|
||||||
|
type SynologyDSMConfigEntry = ConfigEntry[SynologyDSMData]
|
||||||
|
|
||||||
|
|
||||||
def async_re_login_on_expired[_T: SynologyDSMUpdateCoordinator[Any], **_P, _R](
|
def async_re_login_on_expired[_T: SynologyDSMUpdateCoordinator[Any], **_P, _R](
|
||||||
func: Callable[Concatenate[_T, _P], Awaitable[_R]],
|
func: Callable[Concatenate[_T, _P], Awaitable[_R]],
|
||||||
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]]:
|
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]]:
|
||||||
@ -57,12 +71,12 @@ def async_re_login_on_expired[_T: SynologyDSMUpdateCoordinator[Any], **_P, _R](
|
|||||||
class SynologyDSMUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
class SynologyDSMUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||||
"""DataUpdateCoordinator base class for synology_dsm."""
|
"""DataUpdateCoordinator base class for synology_dsm."""
|
||||||
|
|
||||||
config_entry: ConfigEntry
|
config_entry: SynologyDSMConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: SynologyDSMConfigEntry,
|
||||||
api: SynoApi,
|
api: SynoApi,
|
||||||
update_interval: timedelta,
|
update_interval: timedelta,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -85,7 +99,7 @@ class SynologyDSMSwitchUpdateCoordinator(
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: SynologyDSMConfigEntry,
|
||||||
api: SynoApi,
|
api: SynoApi,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize DataUpdateCoordinator for switch devices."""
|
"""Initialize DataUpdateCoordinator for switch devices."""
|
||||||
@ -116,7 +130,7 @@ class SynologyDSMCentralUpdateCoordinator(SynologyDSMUpdateCoordinator[None]):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: SynologyDSMConfigEntry,
|
||||||
api: SynoApi,
|
api: SynoApi,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize DataUpdateCoordinator for central device."""
|
"""Initialize DataUpdateCoordinator for central device."""
|
||||||
@ -136,7 +150,7 @@ class SynologyDSMCameraUpdateCoordinator(
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: SynologyDSMConfigEntry,
|
||||||
api: SynoApi,
|
api: SynoApi,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize DataUpdateCoordinator for cameras."""
|
"""Initialize DataUpdateCoordinator for cameras."""
|
||||||
|
@ -6,21 +6,20 @@ from typing import Any
|
|||||||
|
|
||||||
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.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 .const import CONF_DEVICE_TOKEN, DOMAIN
|
from .const import CONF_DEVICE_TOKEN
|
||||||
from .models import SynologyDSMData
|
from .coordinator import SynologyDSMConfigEntry
|
||||||
|
|
||||||
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_DEVICE_TOKEN}
|
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_DEVICE_TOKEN}
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, entry: ConfigEntry
|
hass: HomeAssistant, entry: SynologyDSMConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
data = entry.runtime_data
|
||||||
syno_api = data.api
|
syno_api = data.api
|
||||||
dsm_info = syno_api.dsm.information
|
dsm_info = syno_api.dsm.information
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from logging import getLogger
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@ -22,7 +23,9 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import DOMAIN, SHARED_SUFFIX
|
from .const import DOMAIN, SHARED_SUFFIX
|
||||||
from .models import SynologyDSMData
|
from .coordinator import SynologyDSMConfigEntry, SynologyDSMData
|
||||||
|
|
||||||
|
LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||||
@ -41,15 +44,13 @@ class SynologyPhotosMediaSourceIdentifier:
|
|||||||
"""Split identifier into parts."""
|
"""Split identifier into parts."""
|
||||||
parts = identifier.split("/")
|
parts = identifier.split("/")
|
||||||
|
|
||||||
self.unique_id = None
|
self.unique_id = parts[0]
|
||||||
self.album_id = None
|
self.album_id = None
|
||||||
self.cache_key = None
|
self.cache_key = None
|
||||||
self.file_name = None
|
self.file_name = None
|
||||||
self.is_shared = False
|
self.is_shared = False
|
||||||
self.passphrase = ""
|
self.passphrase = ""
|
||||||
|
|
||||||
self.unique_id = parts[0]
|
|
||||||
|
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
album_parts = parts[1].split("_")
|
album_parts = parts[1].split("_")
|
||||||
self.album_id = album_parts[0]
|
self.album_id = album_parts[0]
|
||||||
@ -82,7 +83,7 @@ class SynologyPhotosMediaSource(MediaSource):
|
|||||||
item: MediaSourceItem,
|
item: MediaSourceItem,
|
||||||
) -> BrowseMediaSource:
|
) -> BrowseMediaSource:
|
||||||
"""Return media."""
|
"""Return media."""
|
||||||
if not self.hass.data.get(DOMAIN):
|
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||||
raise BrowseError("Diskstation not initialized")
|
raise BrowseError("Diskstation not initialized")
|
||||||
return BrowseMediaSource(
|
return BrowseMediaSource(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
@ -116,7 +117,13 @@ class SynologyPhotosMediaSource(MediaSource):
|
|||||||
for entry in self.entries
|
for entry in self.entries
|
||||||
]
|
]
|
||||||
identifier = SynologyPhotosMediaSourceIdentifier(item.identifier)
|
identifier = SynologyPhotosMediaSourceIdentifier(item.identifier)
|
||||||
diskstation: SynologyDSMData = self.hass.data[DOMAIN][identifier.unique_id]
|
entry: SynologyDSMConfigEntry | None = (
|
||||||
|
self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||||
|
DOMAIN, identifier.unique_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert entry
|
||||||
|
diskstation = entry.runtime_data
|
||||||
assert diskstation.api.photos is not None
|
assert diskstation.api.photos is not None
|
||||||
|
|
||||||
if identifier.album_id is None:
|
if identifier.album_id is None:
|
||||||
@ -244,7 +251,7 @@ class SynologyDsmMediaView(http.HomeAssistantView):
|
|||||||
self, request: web.Request, source_dir_id: str, location: str
|
self, request: web.Request, source_dir_id: str, location: str
|
||||||
) -> web.Response:
|
) -> web.Response:
|
||||||
"""Start a GET request."""
|
"""Start a GET request."""
|
||||||
if not self.hass.data.get(DOMAIN):
|
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||||
raise web.HTTPNotFound
|
raise web.HTTPNotFound
|
||||||
# location: {cache_key}/{filename}
|
# location: {cache_key}/{filename}
|
||||||
cache_key, file_name, passphrase = location.split("/")
|
cache_key, file_name, passphrase = location.split("/")
|
||||||
@ -257,7 +264,13 @@ class SynologyDsmMediaView(http.HomeAssistantView):
|
|||||||
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]
|
entry: SynologyDSMConfigEntry | None = (
|
||||||
|
self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||||
|
DOMAIN, source_dir_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert entry
|
||||||
|
diskstation = entry.runtime_data
|
||||||
assert diskstation.api.photos is not None
|
assert diskstation.api.photos is not None
|
||||||
item = SynoPhotosItem(image_id, "", "", "", cache_key, "xl", shared, passphrase)
|
item = SynoPhotosItem(image_id, "", "", "", cache_key, "xl", shared, passphrase)
|
||||||
try:
|
try:
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
"""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
|
|
@ -11,7 +11,6 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant import data_entry_flow
|
from homeassistant import data_entry_flow
|
||||||
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
|
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import issue_registry as ir
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers.selector import (
|
from homeassistant.helpers.selector import (
|
||||||
@ -28,7 +27,7 @@ from .const import (
|
|||||||
ISSUE_MISSING_BACKUP_SETUP,
|
ISSUE_MISSING_BACKUP_SETUP,
|
||||||
SYNOLOGY_CONNECTION_EXCEPTIONS,
|
SYNOLOGY_CONNECTION_EXCEPTIONS,
|
||||||
)
|
)
|
||||||
from .models import SynologyDSMData
|
from .coordinator import SynologyDSMConfigEntry
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -36,7 +35,7 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
class MissingBackupSetupRepairFlow(RepairsFlow):
|
class MissingBackupSetupRepairFlow(RepairsFlow):
|
||||||
"""Handler for an issue fixing flow."""
|
"""Handler for an issue fixing flow."""
|
||||||
|
|
||||||
def __init__(self, entry: ConfigEntry, issue_id: str) -> None:
|
def __init__(self, entry: SynologyDSMConfigEntry, issue_id: str) -> None:
|
||||||
"""Create flow."""
|
"""Create flow."""
|
||||||
self.entry = entry
|
self.entry = entry
|
||||||
self.issue_id = issue_id
|
self.issue_id = issue_id
|
||||||
@ -59,7 +58,7 @@ class MissingBackupSetupRepairFlow(RepairsFlow):
|
|||||||
) -> data_entry_flow.FlowResult:
|
) -> data_entry_flow.FlowResult:
|
||||||
"""Handle the confirm step of a fix flow."""
|
"""Handle the confirm step of a fix flow."""
|
||||||
|
|
||||||
syno_data: SynologyDSMData = self.hass.data[DOMAIN][self.entry.unique_id]
|
syno_data = self.entry.runtime_data
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self.hass.config_entries.async_update_entry(
|
self.hass.config_entries.async_update_entry(
|
||||||
|
@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_DISKS,
|
CONF_DISKS,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
@ -31,14 +30,13 @@ from homeassistant.helpers.typing import StateType
|
|||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from . import SynoApi
|
from . import SynoApi
|
||||||
from .const import CONF_VOLUMES, DOMAIN, ENTITY_UNIT_LOAD
|
from .const import CONF_VOLUMES, ENTITY_UNIT_LOAD
|
||||||
from .coordinator import SynologyDSMCentralUpdateCoordinator
|
from .coordinator import SynologyDSMCentralUpdateCoordinator, SynologyDSMConfigEntry
|
||||||
from .entity import (
|
from .entity import (
|
||||||
SynologyDSMBaseEntity,
|
SynologyDSMBaseEntity,
|
||||||
SynologyDSMDeviceEntity,
|
SynologyDSMDeviceEntity,
|
||||||
SynologyDSMEntityDescription,
|
SynologyDSMEntityDescription,
|
||||||
)
|
)
|
||||||
from .models import SynologyDSMData
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@ -287,11 +285,11 @@ INFORMATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: SynologyDSMConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Synology NAS Sensor."""
|
"""Set up the Synology NAS Sensor."""
|
||||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
data = entry.runtime_data
|
||||||
api = data.api
|
api = data.api
|
||||||
coordinator = data.coordinator_central
|
coordinator = data.coordinator_central
|
||||||
storage = api.storage
|
storage = api.storage
|
||||||
|
@ -3,13 +3,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
from synology_dsm.exceptions import SynologyDSMException
|
from synology_dsm.exceptions import SynologyDSMException
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
|
||||||
from .const import CONF_SERIAL, DOMAIN, SERVICE_REBOOT, SERVICE_SHUTDOWN, SERVICES
|
from .const import CONF_SERIAL, DOMAIN, SERVICE_REBOOT, SERVICE_SHUTDOWN, SERVICES
|
||||||
from .models import SynologyDSMData
|
from .coordinator import SynologyDSMConfigEntry
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -19,11 +20,20 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
async def service_handler(call: ServiceCall) -> None:
|
async def service_handler(call: ServiceCall) -> None:
|
||||||
"""Handle service call."""
|
"""Handle service call."""
|
||||||
serial = call.data.get(CONF_SERIAL)
|
serial: str | None = call.data.get(CONF_SERIAL)
|
||||||
dsm_devices = hass.data[DOMAIN]
|
entries: list[SynologyDSMConfigEntry] = (
|
||||||
|
hass.config_entries.async_loaded_entries(DOMAIN)
|
||||||
|
)
|
||||||
|
dsm_devices = {
|
||||||
|
cast(str, entry.unique_id): entry.runtime_data for entry in entries
|
||||||
|
}
|
||||||
|
|
||||||
if serial:
|
if serial:
|
||||||
dsm_device: SynologyDSMData = hass.data[DOMAIN][serial]
|
entry: SynologyDSMConfigEntry | None = (
|
||||||
|
hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, serial)
|
||||||
|
)
|
||||||
|
assert entry
|
||||||
|
dsm_device = entry.runtime_data
|
||||||
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))
|
||||||
@ -39,7 +49,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 serial not in hass.data[DOMAIN]:
|
if serial not in dsm_devices:
|
||||||
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)
|
||||||
@ -50,7 +60,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
|||||||
),
|
),
|
||||||
call.service,
|
call.service,
|
||||||
)
|
)
|
||||||
dsm_device = hass.data[DOMAIN][serial]
|
dsm_device = dsm_devices[serial]
|
||||||
dsm_api = dsm_device.api
|
dsm_api = dsm_device.api
|
||||||
try:
|
try:
|
||||||
await getattr(dsm_api, f"async_{call.service}")()
|
await getattr(dsm_api, f"async_{call.service}")()
|
||||||
|
@ -9,16 +9,14 @@ from typing import Any
|
|||||||
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
|
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import SynoApi
|
from . import SynoApi
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import SynologyDSMSwitchUpdateCoordinator
|
from .coordinator import SynologyDSMConfigEntry, SynologyDSMSwitchUpdateCoordinator
|
||||||
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
||||||
from .models import SynologyDSMData
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -41,11 +39,11 @@ SURVEILLANCE_SWITCH: tuple[SynologyDSMSwitchEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: SynologyDSMConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Synology NAS switch."""
|
"""Set up the Synology NAS switch."""
|
||||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
data = entry.runtime_data
|
||||||
if coordinator := data.coordinator_switches:
|
if coordinator := data.coordinator_switches:
|
||||||
assert coordinator.version is not None
|
assert coordinator.version is not None
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
|
@ -9,15 +9,12 @@ from synology_dsm.api.core.upgrade import SynoCoreUpgrade
|
|||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
from homeassistant.components.update import UpdateEntity, UpdateEntityDescription
|
from homeassistant.components.update import UpdateEntity, UpdateEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .coordinator import SynologyDSMCentralUpdateCoordinator, SynologyDSMConfigEntry
|
||||||
from .coordinator import SynologyDSMCentralUpdateCoordinator
|
|
||||||
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
|
||||||
from .models import SynologyDSMData
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@ -39,11 +36,11 @@ UPDATE_ENTITIES: Final = [
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: SynologyDSMConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Synology DSM update entities."""
|
"""Set up Synology DSM update entities."""
|
||||||
data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id]
|
data = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
SynoDSMUpdateEntity(data.api, data.coordinator_central, description)
|
SynoDSMUpdateEntity(data.api, data.coordinator_central, description)
|
||||||
for description in UPDATE_ENTITIES
|
for description in UPDATE_ENTITIES
|
||||||
|
Loading…
x
Reference in New Issue
Block a user