Improve typing for synology_dsm (#49656)

This commit is contained in:
Michael 2021-05-09 22:44:55 +02:00 committed by GitHub
parent 717f4e69d5
commit 042822e35e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 241 additions and 132 deletions

View file

@ -40,6 +40,7 @@ homeassistant.components.slack.*
homeassistant.components.sonos.media_player homeassistant.components.sonos.media_player
homeassistant.components.sun.* homeassistant.components.sun.*
homeassistant.components.switch.* homeassistant.components.switch.*
homeassistant.components.synology_dsm.*
homeassistant.components.systemmonitor.* homeassistant.components.systemmonitor.*
homeassistant.components.tts.* homeassistant.components.tts.*
homeassistant.components.vacuum.* homeassistant.components.vacuum.*

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any, Callable
import async_timeout import async_timeout
from synology_dsm import SynologyDSM from synology_dsm import SynologyDSM
@ -15,6 +15,7 @@ from synology_dsm.api.dsm.information import SynoDSMInformation
from synology_dsm.api.dsm.network import SynoDSMNetwork from synology_dsm.api.dsm.network import SynoDSMNetwork
from synology_dsm.api.storage.storage import SynoStorage from synology_dsm.api.storage.storage import SynoStorage
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 ( from synology_dsm.exceptions import (
SynologyDSMAPIErrorException, SynologyDSMAPIErrorException,
SynologyDSMLoginFailedException, SynologyDSMLoginFailedException,
@ -38,9 +39,14 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry from homeassistant.helpers import device_registry, entity_registry
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import (
DeviceEntry,
async_get_registry as get_dev_reg,
)
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
@ -74,6 +80,7 @@ from .const import (
SYSTEM_LOADED, SYSTEM_LOADED,
UNDO_UPDATE_LISTENER, UNDO_UPDATE_LISTENER,
UTILISATION_SENSORS, UTILISATION_SENSORS,
EntityInfo,
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
@ -103,7 +110,7 @@ ATTRIBUTION = "Data provided by Synology"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Synology DSM sensors from legacy config file.""" """Set up Synology DSM sensors from legacy config file."""
conf = config.get(DOMAIN) conf = config.get(DOMAIN)
@ -122,12 +129,16 @@ async def async_setup(hass, config):
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_setup_entry( # noqa: C901
hass: HomeAssistant, entry: ConfigEntry
) -> bool:
"""Set up Synology DSM sensors.""" """Set up Synology DSM sensors."""
# Migrate old unique_id # Migrate old unique_id
@callback @callback
def _async_migrator(entity_entry: entity_registry.RegistryEntry): def _async_migrator(
entity_entry: entity_registry.RegistryEntry,
) -> dict[str, str] | None:
"""Migrate away from ID using label.""" """Migrate away from ID using label."""
# Reject if new unique_id # Reject if new unique_id
if "SYNO." in entity_entry.unique_id: if "SYNO." in entity_entry.unique_id:
@ -152,7 +163,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
): ):
return None return None
entity_type = None entity_type: str | None = None
for entity_key, entity_attrs in entries.items(): for entity_key, entity_attrs in entries.items():
if ( if (
device_id device_id
@ -170,6 +181,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if entity_attrs[ENTITY_NAME] == label: if entity_attrs[ENTITY_NAME] == label:
entity_type = entity_key entity_type = entity_key
if entity_type is None:
return None
new_unique_id = "_".join([serial, entity_type]) new_unique_id = "_".join([serial, entity_type])
if device_id: if device_id:
new_unique_id += f"_{device_id}" new_unique_id += f"_{device_id}"
@ -183,6 +197,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
await entity_registry.async_migrate_entries(hass, entry.entry_id, _async_migrator) await entity_registry.async_migrate_entries(hass, entry.entry_id, _async_migrator)
# migrate device indetifiers
dev_reg = await get_dev_reg(hass)
devices: list[DeviceEntry] = device_registry.async_entries_for_config_entry(
dev_reg, entry.entry_id
)
for device in devices:
old_identifier = list(next(iter(device.identifiers)))
if len(old_identifier) > 2:
new_identifier: set[tuple[str, ...]] = {
(old_identifier.pop(0), "_".join([str(x) for x in old_identifier]))
}
_LOGGER.debug(
"migrate identifier '%s' to '%s'", device.identifiers, new_identifier
)
dev_reg.async_update_device(device.id, new_identifiers=new_identifier)
# Migrate existing entry configuration # Migrate existing entry configuration
if entry.data.get(CONF_VERIFY_SSL) is None: if entry.data.get(CONF_VERIFY_SSL) is None:
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(
@ -216,7 +246,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
entry, data={**entry.data, CONF_MAC: network.macs} entry, data={**entry.data, CONF_MAC: network.macs}
) )
async def async_coordinator_update_data_cameras(): async def async_coordinator_update_data_cameras() -> dict[
str, dict[str, SynoCamera]
] | None:
"""Fetch all camera data from api.""" """Fetch all camera data from api."""
if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]:
raise UpdateFailed("System not fully loaded") raise UpdateFailed("System not fully loaded")
@ -238,7 +270,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
} }
} }
async def async_coordinator_update_data_central(): async def async_coordinator_update_data_central() -> None:
"""Fetch all device and sensor data from api.""" """Fetch all device and sensor data from api."""
try: try:
await api.async_update() await api.async_update()
@ -246,7 +278,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
raise UpdateFailed(f"Error communicating with API: {err}") from err raise UpdateFailed(f"Error communicating with API: {err}") from err
return None return None
async def async_coordinator_update_data_switches(): async def async_coordinator_update_data_switches() -> dict[
str, dict[str, Any]
] | None:
"""Fetch all switch data from api.""" """Fetch all switch data from api."""
if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]:
raise UpdateFailed("System not fully loaded") raise UpdateFailed("System not fully loaded")
@ -294,7 +328,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): 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) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: if unload_ok:
@ -306,15 +340,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
return unload_ok return unload_ok
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> 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_setup_services(hass: HomeAssistant): async def _async_setup_services(hass: HomeAssistant) -> None:
"""Service handler setup.""" """Service handler setup."""
async def service_handler(call: ServiceCall): async def service_handler(call: ServiceCall) -> None:
"""Handle service call.""" """Handle service call."""
serial = call.data.get(CONF_SERIAL) serial = call.data.get(CONF_SERIAL)
dsm_devices = hass.data[DOMAIN] dsm_devices = hass.data[DOMAIN]
@ -350,7 +384,7 @@ async def _async_setup_services(hass: HomeAssistant):
class SynoApi: class SynoApi:
"""Class to interface with Synology DSM API.""" """Class to interface with Synology DSM API."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry): def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the API wrapper class.""" """Initialize the API wrapper class."""
self._hass = hass self._hass = hass
self._entry = entry self._entry = entry
@ -367,7 +401,7 @@ class SynoApi:
self.utilisation: SynoCoreUtilization = None self.utilisation: SynoCoreUtilization = None
# Should we fetch them # Should we fetch them
self._fetching_entities = {} self._fetching_entities: dict[str, set[str]] = {}
self._with_information = True self._with_information = True
self._with_security = True self._with_security = True
self._with_storage = True self._with_storage = True
@ -376,7 +410,7 @@ class SynoApi:
self._with_upgrade = True self._with_upgrade = True
self._with_utilisation = True self._with_utilisation = True
async def async_setup(self): async def async_setup(self) -> None:
"""Start interacting with the NAS.""" """Start interacting with the NAS."""
self.dsm = SynologyDSM( self.dsm = SynologyDSM(
self._entry.data[CONF_HOST], self._entry.data[CONF_HOST],
@ -406,7 +440,7 @@ class SynoApi:
await self.async_update() await self.async_update()
@callback @callback
def subscribe(self, api_key, unique_id): def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]:
"""Subscribe an entity to API fetches.""" """Subscribe an entity to API fetches."""
_LOGGER.debug("Subscribe new entity: %s", unique_id) _LOGGER.debug("Subscribe new entity: %s", unique_id)
if api_key not in self._fetching_entities: if api_key not in self._fetching_entities:
@ -424,7 +458,7 @@ class SynoApi:
return unsubscribe return unsubscribe
@callback @callback
def _async_setup_api_requests(self): def _async_setup_api_requests(self) -> None:
"""Determine if we should fetch each API, if one entity needs it.""" """Determine if we should fetch each API, if one entity needs it."""
# Entities not added yet, fetch all # Entities not added yet, fetch all
if not self._fetching_entities: if not self._fetching_entities:
@ -488,7 +522,7 @@ class SynoApi:
self.dsm.reset(self.utilisation) self.dsm.reset(self.utilisation)
self.utilisation = None self.utilisation = None
def _fetch_device_configuration(self): def _fetch_device_configuration(self) -> None:
"""Fetch initial device config.""" """Fetch initial device config."""
self.information = self.dsm.information self.information = self.dsm.information
self.network = self.dsm.network self.network = self.dsm.network
@ -523,7 +557,7 @@ class SynoApi:
) )
self.surveillance_station = self.dsm.surveillance_station self.surveillance_station = self.dsm.surveillance_station
async def async_reboot(self): async def async_reboot(self) -> None:
"""Reboot NAS.""" """Reboot NAS."""
try: try:
await self._hass.async_add_executor_job(self.system.reboot) await self._hass.async_add_executor_job(self.system.reboot)
@ -534,7 +568,7 @@ class SynoApi:
) )
_LOGGER.debug("Exception:%s", err) _LOGGER.debug("Exception:%s", err)
async def async_shutdown(self): async def async_shutdown(self) -> None:
"""Shutdown NAS.""" """Shutdown NAS."""
try: try:
await self._hass.async_add_executor_job(self.system.shutdown) await self._hass.async_add_executor_job(self.system.shutdown)
@ -545,7 +579,7 @@ class SynoApi:
) )
_LOGGER.debug("Exception:%s", err) _LOGGER.debug("Exception:%s", err)
async def async_unload(self): 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."""
try: try:
await self._hass.async_add_executor_job(self.dsm.logout) await self._hass.async_add_executor_job(self.dsm.logout)
@ -554,7 +588,7 @@ class SynoApi:
"Logout from '%s' not possible:%s", self._entry.unique_id, err "Logout from '%s' not possible:%s", self._entry.unique_id, err
) )
async def async_update(self, now=None): async def async_update(self, now: timedelta | None = None) -> None:
"""Update function for updating API information.""" """Update function for updating API information."""
_LOGGER.debug("Start data update for '%s'", self._entry.unique_id) _LOGGER.debug("Start data update for '%s'", self._entry.unique_id)
self._async_setup_api_requests() self._async_setup_api_requests()
@ -582,9 +616,9 @@ class SynologyDSMBaseEntity(CoordinatorEntity):
self, self,
api: SynoApi, api: SynoApi,
entity_type: str, entity_type: str,
entity_info: dict[str, str], entity_info: EntityInfo,
coordinator: DataUpdateCoordinator, coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]],
): ) -> None:
"""Initialize the Synology DSM entity.""" """Initialize the Synology DSM entity."""
super().__init__(coordinator) super().__init__(coordinator)
@ -609,12 +643,12 @@ class SynologyDSMBaseEntity(CoordinatorEntity):
return self._name return self._name
@property @property
def icon(self) -> str: def icon(self) -> str | None:
"""Return the icon.""" """Return the icon."""
return self._icon return self._icon
@property @property
def device_class(self) -> str: def device_class(self) -> str | None:
"""Return the class of this device.""" """Return the class of this device."""
return self._class return self._class
@ -639,7 +673,7 @@ class SynologyDSMBaseEntity(CoordinatorEntity):
"""Return if the entity should be enabled when first added to the entity registry.""" """Return if the entity should be enabled when first added to the entity registry."""
return self._enable_default return self._enable_default
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Register entity for updates from API.""" """Register entity for updates from API."""
self.async_on_remove(self._api.subscribe(self._api_key, self.unique_id)) self.async_on_remove(self._api.subscribe(self._api_key, self.unique_id))
await super().async_added_to_hass() await super().async_added_to_hass()
@ -652,10 +686,10 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity):
self, self,
api: SynoApi, api: SynoApi,
entity_type: str, entity_type: str,
entity_info: dict[str, str], entity_info: EntityInfo,
coordinator: DataUpdateCoordinator, coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]],
device_id: str = None, device_id: str | None = None,
): ) -> None:
"""Initialize the Synology DSM disk or volume entity.""" """Initialize the Synology DSM disk or volume entity."""
super().__init__(api, entity_type, entity_info, coordinator) super().__init__(api, entity_type, entity_info, coordinator)
self._device_id = device_id self._device_id = device_id
@ -691,16 +725,18 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity):
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return bool(self._api.storage) return self._api.storage # type: ignore [no-any-return]
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device information.""" """Return the device information."""
return { return {
"identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, "identifiers": {
(DOMAIN, f"{self._api.information.serial}_{self._device_id}")
},
"name": f"Synology NAS ({self._device_name} - {self._device_type})", "name": f"Synology NAS ({self._device_name} - {self._device_type})",
"manufacturer": self._device_manufacturer, "manufacturer": self._device_manufacturer, # type: ignore[typeddict-item]
"model": self._device_model, "model": self._device_model, # type: ignore[typeddict-item]
"sw_version": self._device_firmware, "sw_version": self._device_firmware, # type: ignore[typeddict-item]
"via_device": (DOMAIN, self._api.information.serial), "via_device": (DOMAIN, self._api.information.serial),
} }

View file

@ -5,8 +5,9 @@ from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DISKS from homeassistant.const import CONF_DISKS
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SynologyDSMBaseEntity, SynologyDSMDeviceEntity from . import SynoApi, SynologyDSMBaseEntity, SynologyDSMDeviceEntity
from .const import ( from .const import (
COORDINATOR_CENTRAL, COORDINATOR_CENTRAL,
DOMAIN, DOMAIN,
@ -18,15 +19,19 @@ from .const import (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities 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 = hass.data[DOMAIN][entry.unique_id] data = hass.data[DOMAIN][entry.unique_id]
api = data[SYNO_API] api: SynoApi = data[SYNO_API]
coordinator = data[COORDINATOR_CENTRAL] coordinator = data[COORDINATOR_CENTRAL]
entities = [ entities: list[
SynoDSMSecurityBinarySensor
| SynoDSMUpgradeBinarySensor
| SynoDSMStorageBinarySensor
] = [
SynoDSMSecurityBinarySensor( SynoDSMSecurityBinarySensor(
api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type], coordinator api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type], coordinator
) )
@ -63,7 +68,7 @@ class SynoDSMSecurityBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state.""" """Return the state."""
return getattr(self._api.security, self.entity_type) != "safe" return getattr(self._api.security, self.entity_type) != "safe" # type: ignore[no-any-return]
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -73,7 +78,7 @@ class SynoDSMSecurityBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity):
@property @property
def extra_state_attributes(self) -> dict[str, str]: def extra_state_attributes(self) -> dict[str, str]:
"""Return security checks details.""" """Return security checks details."""
return self._api.security.status_by_check return self._api.security.status_by_check # type: ignore[no-any-return]
class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity): class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity):
@ -82,7 +87,7 @@ class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state.""" """Return the state."""
return getattr(self._api.storage, self.entity_type)(self._device_id) return bool(getattr(self._api.storage, self.entity_type)(self._device_id))
class SynoDSMUpgradeBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity): class SynoDSMUpgradeBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity):
@ -91,7 +96,7 @@ class SynoDSMUpgradeBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state.""" """Return the state."""
return getattr(self._api.upgrade, self.entity_type) return bool(getattr(self._api.upgrade, self.entity_type))
@property @property
def available(self) -> bool: def available(self) -> bool:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.api.surveillance_station import SynoCamera, SynoSurveillanceStation
from synology_dsm.exceptions import ( from synology_dsm.exceptions import (
SynologyDSMAPIErrorException, SynologyDSMAPIErrorException,
SynologyDSMRequestException, SynologyDSMRequestException,
@ -13,6 +13,7 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import SynoApi, SynologyDSMBaseEntity from . import SynoApi, SynologyDSMBaseEntity
@ -31,19 +32,21 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the Synology NAS cameras.""" """Set up the Synology NAS cameras."""
data = hass.data[DOMAIN][entry.unique_id] data = hass.data[DOMAIN][entry.unique_id]
api = data[SYNO_API] api: SynoApi = data[SYNO_API]
if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis:
return return
# initial data fetch # initial data fetch
coordinator = data[COORDINATOR_CAMERAS] coordinator: DataUpdateCoordinator[dict[str, dict[str, SynoCamera]]] = data[
await coordinator.async_refresh() COORDINATOR_CAMERAS
]
await coordinator.async_config_entry_first_refresh()
async_add_entities( async_add_entities(
SynoDSMCamera(api, coordinator, camera_id) SynoDSMCamera(api, coordinator, camera_id)
@ -54,9 +57,14 @@ async def async_setup_entry(
class SynoDSMCamera(SynologyDSMBaseEntity, Camera): class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
"""Representation a Synology camera.""" """Representation a Synology camera."""
coordinator: DataUpdateCoordinator[dict[str, dict[str, SynoCamera]]]
def __init__( def __init__(
self, api: SynoApi, coordinator: DataUpdateCoordinator, camera_id: int self,
): api: SynoApi,
coordinator: DataUpdateCoordinator[dict[str, dict[str, SynoCamera]]],
camera_id: str,
) -> None:
"""Initialize a Synology camera.""" """Initialize a Synology camera."""
super().__init__( super().__init__(
api, api,
@ -70,13 +78,11 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
}, },
coordinator, coordinator,
) )
Camera.__init__(self) Camera.__init__(self) # type: ignore[no-untyped-call]
self._camera_id = camera_id self._camera_id = camera_id
self._api = api
@property @property
def camera_data(self): def camera_data(self) -> SynoCamera:
"""Camera data.""" """Camera data."""
return self.coordinator.data["cameras"][self._camera_id] return self.coordinator.data["cameras"][self._camera_id]
@ -87,16 +93,14 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
"identifiers": { "identifiers": {
( (
DOMAIN, DOMAIN,
self._api.information.serial, f"{self._api.information.serial}_{self.camera_data.id}",
self.camera_data.id,
) )
}, },
"name": self.camera_data.name, "name": self.camera_data.name,
"model": self.camera_data.model, "model": self.camera_data.model,
"via_device": ( "via_device": (
DOMAIN, DOMAIN,
self._api.information.serial, f"{self._api.information.serial}_{SynoSurveillanceStation.INFO_API_KEY}",
SynoSurveillanceStation.INFO_API_KEY,
), ),
} }
@ -111,16 +115,16 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
return SUPPORT_STREAM return SUPPORT_STREAM
@property @property
def is_recording(self): def is_recording(self) -> bool:
"""Return true if the device is recording.""" """Return true if the device is recording."""
return self.camera_data.is_recording return self.camera_data.is_recording # type: ignore[no-any-return]
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self) -> bool:
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
return self.camera_data.is_motion_detection_enabled return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return]
def camera_image(self) -> bytes: def camera_image(self) -> bytes | None:
"""Return bytes of camera image.""" """Return bytes of camera image."""
_LOGGER.debug( _LOGGER.debug(
"SynoDSMCamera.camera_image(%s)", "SynoDSMCamera.camera_image(%s)",
@ -129,7 +133,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
if not self.available: if not self.available:
return None return None
try: try:
return self._api.surveillance_station.get_camera_image(self._camera_id) return self._api.surveillance_station.get_camera_image(self._camera_id) # type: ignore[no-any-return]
except ( except (
SynologyDSMAPIErrorException, SynologyDSMAPIErrorException,
SynologyDSMRequestException, SynologyDSMRequestException,
@ -142,7 +146,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
) )
return None return None
async def stream_source(self) -> str: async def stream_source(self) -> str | None:
"""Return the source of the stream.""" """Return the source of the stream."""
_LOGGER.debug( _LOGGER.debug(
"SynoDSMCamera.stream_source(%s)", "SynoDSMCamera.stream_source(%s)",
@ -150,9 +154,9 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
) )
if not self.available: if not self.available:
return None return None
return self.camera_data.live_view.rtsp return self.camera_data.live_view.rtsp # type: ignore[no-any-return]
def enable_motion_detection(self): def enable_motion_detection(self) -> None:
"""Enable motion detection in the camera.""" """Enable motion detection in the camera."""
_LOGGER.debug( _LOGGER.debug(
"SynoDSMCamera.enable_motion_detection(%s)", "SynoDSMCamera.enable_motion_detection(%s)",
@ -160,7 +164,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
) )
self._api.surveillance_station.enable_motion_detection(self._camera_id) self._api.surveillance_station.enable_motion_detection(self._camera_id)
def disable_motion_detection(self): def disable_motion_detection(self) -> None:
"""Disable motion detection in camera.""" """Disable motion detection in camera."""
_LOGGER.debug( _LOGGER.debug(
"SynoDSMCamera.disable_motion_detection(%s)", "SynoDSMCamera.disable_motion_detection(%s)",

View file

@ -1,5 +1,8 @@
"""Config flow to configure the Synology DSM integration.""" """Config flow to configure the Synology DSM integration."""
from __future__ import annotations
import logging import logging
from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from synology_dsm import SynologyDSM from synology_dsm import SynologyDSM
@ -12,8 +15,14 @@ from synology_dsm.exceptions import (
) )
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, exceptions from homeassistant import exceptions
from homeassistant.components import ssdp from homeassistant.components import ssdp
from homeassistant.config_entries import (
CONN_CLASS_CLOUD_POLL,
ConfigEntry,
ConfigFlow,
OptionsFlow,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_DISKS, CONF_DISKS,
CONF_HOST, CONF_HOST,
@ -28,7 +37,9 @@ from homeassistant.const import (
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import ( from .const import (
CONF_DEVICE_TOKEN, CONF_DEVICE_TOKEN,
@ -47,11 +58,11 @@ _LOGGER = logging.getLogger(__name__)
CONF_OTP_CODE = "otp_code" CONF_OTP_CODE = "otp_code"
def _discovery_schema_with_defaults(discovery_info): def _discovery_schema_with_defaults(discovery_info: DiscoveryInfoType) -> vol.Schema:
return vol.Schema(_ordered_shared_schema(discovery_info)) return vol.Schema(_ordered_shared_schema(discovery_info))
def _user_schema_with_defaults(user_input): def _user_schema_with_defaults(user_input: dict[str, Any]) -> vol.Schema:
user_schema = { user_schema = {
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str,
} }
@ -60,7 +71,9 @@ def _user_schema_with_defaults(user_input):
return vol.Schema(user_schema) return vol.Schema(user_schema)
def _ordered_shared_schema(schema_input): def _ordered_shared_schema(
schema_input: dict[str, Any]
) -> dict[vol.Required | vol.Optional, Any]:
return { return {
vol.Required(CONF_USERNAME, default=schema_input.get(CONF_USERNAME, "")): str, vol.Required(CONF_USERNAME, default=schema_input.get(CONF_USERNAME, "")): str,
vol.Required(CONF_PASSWORD, default=schema_input.get(CONF_PASSWORD, "")): str, vol.Required(CONF_PASSWORD, default=schema_input.get(CONF_PASSWORD, "")): str,
@ -75,23 +88,30 @@ def _ordered_shared_schema(schema_input):
} }
class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow.""" """Handle a config flow."""
VERSION = 1 VERSION = 1
CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: ConfigEntry,
) -> SynologyDSMOptionsFlowHandler:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return SynologyDSMOptionsFlowHandler(config_entry) return SynologyDSMOptionsFlowHandler(config_entry)
def __init__(self): def __init__(self) -> None:
"""Initialize the synology_dsm config flow.""" """Initialize the synology_dsm config flow."""
self.saved_user_input = {} self.saved_user_input: dict[str, Any] = {}
self.discovered_conf = {} self.discovered_conf: dict[str, Any] = {}
async def _show_setup_form(self, user_input=None, errors=None): async def _show_setup_form(
self,
user_input: dict[str, Any] | None = None,
errors: dict[str, str] | None = None,
) -> FlowResult:
"""Show the setup form to the user.""" """Show the setup form to the user."""
if not user_input: if not user_input:
user_input = {} user_input = {}
@ -111,7 +131,9 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders=self.discovered_conf or {}, description_placeholders=self.discovered_conf or {},
) )
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initiated by the user.""" """Handle a flow initiated by the user."""
errors = {} errors = {}
@ -188,7 +210,7 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=host, data=config_data) return self.async_create_entry(title=host, data=config_data)
async def async_step_ssdp(self, discovery_info): async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult:
"""Handle a discovered synology_dsm.""" """Handle a discovered synology_dsm."""
parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION])
friendly_name = ( friendly_name = (
@ -211,15 +233,19 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.context["title_placeholders"] = self.discovered_conf self.context["title_placeholders"] = self.discovered_conf
return await self.async_step_user() return await self.async_step_user()
async def async_step_import(self, user_input=None): async def async_step_import(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Import a config entry.""" """Import a config entry."""
return await self.async_step_user(user_input) return await self.async_step_user(user_input)
async def async_step_link(self, user_input): async def async_step_link(self, user_input: dict[str, Any]) -> FlowResult:
"""Link a config entry from discovery.""" """Link a config entry from discovery."""
return await self.async_step_user(user_input) return await self.async_step_user(user_input)
async def async_step_2sa(self, user_input, errors=None): async def async_step_2sa(
self, user_input: dict[str, Any], errors: dict[str, str] | None = None
) -> FlowResult:
"""Enter 2SA code to anthenticate.""" """Enter 2SA code to anthenticate."""
if not self.saved_user_input: if not self.saved_user_input:
self.saved_user_input = user_input self.saved_user_input = user_input
@ -236,7 +262,7 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_user(user_input) return await self.async_step_user(user_input)
def _mac_already_configured(self, mac): def _mac_already_configured(self, mac: str) -> bool:
"""See if we already have configured a NAS with this MAC address.""" """See if we already have configured a NAS with this MAC address."""
existing_macs = [ existing_macs = [
mac.replace("-", "") mac.replace("-", "")
@ -246,14 +272,16 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return mac in existing_macs return mac in existing_macs
class SynologyDSMOptionsFlowHandler(config_entries.OptionsFlow): class SynologyDSMOptionsFlowHandler(OptionsFlow):
"""Handle a option flow.""" """Handle a option flow."""
def __init__(self, config_entry: config_entries.ConfigEntry): def __init__(self, config_entry: ConfigEntry):
"""Initialize options flow.""" """Initialize options flow."""
self.config_entry = config_entry self.config_entry = config_entry
async def async_step_init(self, user_input=None): async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle options flow.""" """Handle options flow."""
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)
@ -277,7 +305,7 @@ class SynologyDSMOptionsFlowHandler(config_entries.OptionsFlow):
return self.async_show_form(step_id="init", data_schema=data_schema) return self.async_show_form(step_id="init", data_schema=data_schema)
def _login_and_fetch_syno_info(api, otp_code): def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str) -> str:
"""Login to the NAS and fetch basic data.""" """Login to the NAS and fetch basic data."""
# These do i/o # These do i/o
api.login(otp_code) api.login(otp_code)
@ -293,7 +321,7 @@ def _login_and_fetch_syno_info(api, otp_code):
): ):
raise InvalidData raise InvalidData
return api.information.serial return api.information.serial # type: ignore[no-any-return]
class InvalidData(exceptions.HomeAssistantError): class InvalidData(exceptions.HomeAssistantError):

View file

@ -1,4 +1,7 @@
"""Constants for Synology DSM.""" """Constants for Synology DSM."""
from __future__ import annotations
from typing import Final, TypedDict
from synology_dsm.api.core.security import SynoCoreSecurity from synology_dsm.api.core.security import SynoCoreSecurity
from synology_dsm.api.core.upgrade import SynoCoreUpgrade from synology_dsm.api.core.upgrade import SynoCoreUpgrade
@ -17,6 +20,17 @@ from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
) )
class EntityInfo(TypedDict):
"""TypedDict for EntityInfo."""
name: str
unit: str | None
icon: str | None
device_class: str | None
enable: bool
DOMAIN = "synology_dsm" DOMAIN = "synology_dsm"
PLATFORMS = ["binary_sensor", "camera", "sensor", "switch"] PLATFORMS = ["binary_sensor", "camera", "sensor", "switch"]
COORDINATOR_CAMERAS = "coordinator_cameras" COORDINATOR_CAMERAS = "coordinator_cameras"
@ -43,11 +57,11 @@ DEFAULT_TIMEOUT = 10 # sec
ENTITY_UNIT_LOAD = "load" ENTITY_UNIT_LOAD = "load"
ENTITY_NAME = "name" ENTITY_NAME: Final = "name"
ENTITY_UNIT = "unit" ENTITY_UNIT: Final = "unit"
ENTITY_ICON = "icon" ENTITY_ICON: Final = "icon"
ENTITY_CLASS = "device_class" ENTITY_CLASS: Final = "device_class"
ENTITY_ENABLE = "enable" ENTITY_ENABLE: Final = "enable"
# Services # Services
SERVICE_REBOOT = "reboot" SERVICE_REBOOT = "reboot"
@ -60,7 +74,7 @@ SERVICES = [
# Entity keys should start with the API_KEY to fetch # Entity keys should start with the API_KEY to fetch
# Binary sensors # Binary sensors
UPGRADE_BINARY_SENSORS = { UPGRADE_BINARY_SENSORS: dict[str, EntityInfo] = {
f"{SynoCoreUpgrade.API_KEY}:update_available": { f"{SynoCoreUpgrade.API_KEY}:update_available": {
ENTITY_NAME: "Update available", ENTITY_NAME: "Update available",
ENTITY_UNIT: None, ENTITY_UNIT: None,
@ -70,7 +84,7 @@ UPGRADE_BINARY_SENSORS = {
}, },
} }
SECURITY_BINARY_SENSORS = { SECURITY_BINARY_SENSORS: dict[str, EntityInfo] = {
f"{SynoCoreSecurity.API_KEY}:status": { f"{SynoCoreSecurity.API_KEY}:status": {
ENTITY_NAME: "Security status", ENTITY_NAME: "Security status",
ENTITY_UNIT: None, ENTITY_UNIT: None,
@ -80,7 +94,7 @@ SECURITY_BINARY_SENSORS = {
}, },
} }
STORAGE_DISK_BINARY_SENSORS = { STORAGE_DISK_BINARY_SENSORS: dict[str, EntityInfo] = {
f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": { f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": {
ENTITY_NAME: "Exceeded Max Bad Sectors", ENTITY_NAME: "Exceeded Max Bad Sectors",
ENTITY_UNIT: None, ENTITY_UNIT: None,
@ -98,7 +112,7 @@ STORAGE_DISK_BINARY_SENSORS = {
} }
# Sensors # Sensors
UTILISATION_SENSORS = { UTILISATION_SENSORS: dict[str, EntityInfo] = {
f"{SynoCoreUtilization.API_KEY}:cpu_other_load": { f"{SynoCoreUtilization.API_KEY}:cpu_other_load": {
ENTITY_NAME: "CPU Utilization (Other)", ENTITY_NAME: "CPU Utilization (Other)",
ENTITY_UNIT: PERCENTAGE, ENTITY_UNIT: PERCENTAGE,
@ -212,7 +226,7 @@ UTILISATION_SENSORS = {
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
}, },
} }
STORAGE_VOL_SENSORS = { STORAGE_VOL_SENSORS: dict[str, EntityInfo] = {
f"{SynoStorage.API_KEY}:volume_status": { f"{SynoStorage.API_KEY}:volume_status": {
ENTITY_NAME: "Status", ENTITY_NAME: "Status",
ENTITY_UNIT: None, ENTITY_UNIT: None,
@ -256,7 +270,7 @@ STORAGE_VOL_SENSORS = {
ENTITY_ENABLE: False, ENTITY_ENABLE: False,
}, },
} }
STORAGE_DISK_SENSORS = { STORAGE_DISK_SENSORS: dict[str, EntityInfo] = {
f"{SynoStorage.API_KEY}:disk_smart_status": { f"{SynoStorage.API_KEY}:disk_smart_status": {
ENTITY_NAME: "Status (Smart)", ENTITY_NAME: "Status (Smart)",
ENTITY_UNIT: None, ENTITY_UNIT: None,
@ -280,7 +294,7 @@ STORAGE_DISK_SENSORS = {
}, },
} }
INFORMATION_SENSORS = { INFORMATION_SENSORS: dict[str, EntityInfo] = {
f"{SynoDSMInformation.API_KEY}:temperature": { f"{SynoDSMInformation.API_KEY}:temperature": {
ENTITY_NAME: "temperature", ENTITY_NAME: "temperature",
ENTITY_UNIT: None, ENTITY_UNIT: None,
@ -298,7 +312,7 @@ INFORMATION_SENSORS = {
} }
# Switch # Switch
SURVEILLANCE_SWITCH = { SURVEILLANCE_SWITCH: dict[str, EntityInfo] = {
f"{SynoSurveillanceStation.HOME_MODE_API_KEY}:home_mode": { f"{SynoSurveillanceStation.HOME_MODE_API_KEY}:home_mode": {
ENTITY_NAME: "home mode", ENTITY_NAME: "home mode",
ENTITY_UNIT: None, ENTITY_UNIT: None,

View file

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from typing import Any
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -14,6 +15,7 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.temperature import display_temp
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
@ -30,19 +32,20 @@ from .const import (
SYNO_API, SYNO_API,
TEMP_SENSORS_KEYS, TEMP_SENSORS_KEYS,
UTILISATION_SENSORS, UTILISATION_SENSORS,
EntityInfo,
) )
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the Synology NAS Sensor.""" """Set up the Synology NAS Sensor."""
data = hass.data[DOMAIN][entry.unique_id] data = hass.data[DOMAIN][entry.unique_id]
api = data[SYNO_API] api: SynoApi = data[SYNO_API]
coordinator = data[COORDINATOR_CENTRAL] coordinator = data[COORDINATOR_CENTRAL]
entities = [ entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [
SynoDSMUtilSensor( SynoDSMUtilSensor(
api, sensor_type, UTILISATION_SENSORS[sensor_type], coordinator api, sensor_type, UTILISATION_SENSORS[sensor_type], coordinator
) )
@ -91,7 +94,7 @@ class SynoDSMSensor(SynologyDSMBaseEntity):
"""Mixin for sensor specific attributes.""" """Mixin for sensor specific attributes."""
@property @property
def unit_of_measurement(self) -> str: def unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
if self.entity_type in TEMP_SENSORS_KEYS: if self.entity_type in TEMP_SENSORS_KEYS:
return self.hass.config.units.temperature_unit return self.hass.config.units.temperature_unit
@ -102,7 +105,7 @@ class SynoDSMUtilSensor(SynoDSMSensor, SensorEntity):
"""Representation a Synology Utilisation sensor.""" """Representation a Synology Utilisation sensor."""
@property @property
def state(self): def state(self) -> Any | None:
"""Return the state.""" """Return the state."""
attr = getattr(self._api.utilisation, self.entity_type) attr = getattr(self._api.utilisation, self.entity_type)
if callable(attr): if callable(attr):
@ -134,7 +137,7 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor, SensorEntity)
"""Representation a Synology Storage sensor.""" """Representation a Synology Storage sensor."""
@property @property
def state(self): def state(self) -> Any | None:
"""Return the state.""" """Return the state."""
attr = getattr(self._api.storage, self.entity_type)(self._device_id) attr = getattr(self._api.storage, self.entity_type)(self._device_id)
if attr is None: if attr is None:
@ -158,16 +161,16 @@ class SynoDSMInfoSensor(SynoDSMSensor, SensorEntity):
self, self,
api: SynoApi, api: SynoApi,
entity_type: str, entity_type: str,
entity_info: dict[str, str], entity_info: EntityInfo,
coordinator: DataUpdateCoordinator, coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]],
): ) -> None:
"""Initialize the Synology SynoDSMInfoSensor entity.""" """Initialize the Synology SynoDSMInfoSensor entity."""
super().__init__(api, entity_type, entity_info, coordinator) super().__init__(api, entity_type, entity_info, coordinator)
self._previous_uptime = None self._previous_uptime: str | None = None
self._last_boot = None self._last_boot: str | None = None
@property @property
def state(self): def state(self) -> Any | None:
"""Return the state.""" """Return the state."""
attr = getattr(self._api.information, self.entity_type) attr = getattr(self._api.information, self.entity_type)
if attr is None: if attr is None:

View file

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any
from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.api.surveillance_station import SynoSurveillanceStation
@ -9,21 +10,28 @@ from homeassistant.components.switch import ToggleEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import SynoApi, SynologyDSMBaseEntity from . import SynoApi, SynologyDSMBaseEntity
from .const import COORDINATOR_SWITCHES, DOMAIN, SURVEILLANCE_SWITCH, SYNO_API from .const import (
COORDINATOR_SWITCHES,
DOMAIN,
SURVEILLANCE_SWITCH,
SYNO_API,
EntityInfo,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the Synology NAS switch.""" """Set up the Synology NAS switch."""
data = hass.data[DOMAIN][entry.unique_id] data = hass.data[DOMAIN][entry.unique_id]
api = data[SYNO_API] api: SynoApi = data[SYNO_API]
entities = [] entities = []
@ -32,7 +40,7 @@ async def async_setup_entry(
version = info["data"]["CMSMinVersion"] version = info["data"]["CMSMinVersion"]
# initial data fetch # initial data fetch
coordinator = data[COORDINATOR_SWITCHES] coordinator: DataUpdateCoordinator = data[COORDINATOR_SWITCHES]
await coordinator.async_refresh() await coordinator.async_refresh()
entities += [ entities += [
SynoDSMSurveillanceHomeModeToggle( SynoDSMSurveillanceHomeModeToggle(
@ -47,14 +55,16 @@ async def async_setup_entry(
class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity): class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity):
"""Representation a Synology Surveillance Station Home Mode toggle.""" """Representation a Synology Surveillance Station Home Mode toggle."""
coordinator: DataUpdateCoordinator[dict[str, dict[str, bool]]]
def __init__( def __init__(
self, self,
api: SynoApi, api: SynoApi,
entity_type: str, entity_type: str,
entity_info: dict[str, str], entity_info: EntityInfo,
version: str, version: str,
coordinator: DataUpdateCoordinator, coordinator: DataUpdateCoordinator[dict[str, dict[str, bool]]],
): ) -> None:
"""Initialize a Synology Surveillance Station Home Mode.""" """Initialize a Synology Surveillance Station Home Mode."""
super().__init__( super().__init__(
api, api,
@ -69,7 +79,7 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity):
"""Return the state.""" """Return the state."""
return self.coordinator.data["switches"][self.entity_type] return self.coordinator.data["switches"][self.entity_type]
async def async_turn_on(self, **kwargs) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on Home mode.""" """Turn on Home mode."""
_LOGGER.debug( _LOGGER.debug(
"SynoDSMSurveillanceHomeModeToggle.turn_on(%s)", "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)",
@ -80,7 +90,7 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity):
) )
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off Home mode.""" """Turn off Home mode."""
_LOGGER.debug( _LOGGER.debug(
"SynoDSMSurveillanceHomeModeToggle.turn_off(%s)", "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)",
@ -103,8 +113,7 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity):
"identifiers": { "identifiers": {
( (
DOMAIN, DOMAIN,
self._api.information.serial, f"{self._api.information.serial}_{SynoSurveillanceStation.INFO_API_KEY}",
SynoSurveillanceStation.INFO_API_KEY,
) )
}, },
"name": "Surveillance Station", "name": "Surveillance Station",

View file

@ -529,6 +529,19 @@ warn_return_any = true
warn_unreachable = true warn_unreachable = true
warn_unused_ignores = true warn_unused_ignores = true
[mypy-homeassistant.components.synology_dsm.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
strict_equality = true
warn_return_any = true
warn_unreachable = true
warn_unused_ignores = true
[mypy-homeassistant.components.systemmonitor.*] [mypy-homeassistant.components.systemmonitor.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
@ -1232,9 +1245,6 @@ ignore_errors = true
[mypy-homeassistant.components.switcher_kis.*] [mypy-homeassistant.components.switcher_kis.*]
ignore_errors = true ignore_errors = true
[mypy-homeassistant.components.synology_dsm.*]
ignore_errors = true
[mypy-homeassistant.components.synology_srm.*] [mypy-homeassistant.components.synology_srm.*]
ignore_errors = true ignore_errors = true

View file

@ -205,7 +205,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.surepetcare.*", "homeassistant.components.surepetcare.*",
"homeassistant.components.switchbot.*", "homeassistant.components.switchbot.*",
"homeassistant.components.switcher_kis.*", "homeassistant.components.switcher_kis.*",
"homeassistant.components.synology_dsm.*",
"homeassistant.components.synology_srm.*", "homeassistant.components.synology_srm.*",
"homeassistant.components.system_health.*", "homeassistant.components.system_health.*",
"homeassistant.components.system_log.*", "homeassistant.components.system_log.*",