Improve typing for synology_dsm (#49656)

pull/50306/head
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.sun.*
homeassistant.components.switch.*
homeassistant.components.synology_dsm.*
homeassistant.components.systemmonitor.*
homeassistant.components.tts.*
homeassistant.components.vacuum.*

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from typing import Any, Callable
import async_timeout
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.storage.storage import SynoStorage
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
from synology_dsm.api.surveillance_station.camera import SynoCamera
from synology_dsm.exceptions import (
SynologyDSMAPIErrorException,
SynologyDSMLoginFailedException,
@ -38,9 +39,14 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
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
from homeassistant.helpers.device_registry import (
DeviceEntry,
async_get_registry as get_dev_reg,
)
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
@ -74,6 +80,7 @@ from .const import (
SYSTEM_LOADED,
UNDO_UPDATE_LISTENER,
UTILISATION_SENSORS,
EntityInfo,
)
CONFIG_SCHEMA = vol.Schema(
@ -103,7 +110,7 @@ ATTRIBUTION = "Data provided by Synology"
_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."""
conf = config.get(DOMAIN)
@ -122,12 +129,16 @@ async def async_setup(hass, config):
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."""
# Migrate old unique_id
@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."""
# Reject if new unique_id
if "SYNO." in entity_entry.unique_id:
@ -152,7 +163,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
):
return None
entity_type = None
entity_type: str | None = None
for entity_key, entity_attrs in entries.items():
if (
device_id
@ -170,6 +181,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if entity_attrs[ENTITY_NAME] == label:
entity_type = entity_key
if entity_type is None:
return None
new_unique_id = "_".join([serial, entity_type])
if 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)
# 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
if entry.data.get(CONF_VERIFY_SSL) is None:
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}
)
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."""
if not hass.data[DOMAIN][entry.unique_id][SYSTEM_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."""
try:
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
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."""
if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]:
raise UpdateFailed("System not fully loaded")
@ -294,7 +328,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
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_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
@ -306,15 +340,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
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."""
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."""
async def service_handler(call: ServiceCall):
async def service_handler(call: ServiceCall) -> None:
"""Handle service call."""
serial = call.data.get(CONF_SERIAL)
dsm_devices = hass.data[DOMAIN]
@ -350,7 +384,7 @@ async def _async_setup_services(hass: HomeAssistant):
class SynoApi:
"""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."""
self._hass = hass
self._entry = entry
@ -367,7 +401,7 @@ class SynoApi:
self.utilisation: SynoCoreUtilization = None
# Should we fetch them
self._fetching_entities = {}
self._fetching_entities: dict[str, set[str]] = {}
self._with_information = True
self._with_security = True
self._with_storage = True
@ -376,7 +410,7 @@ class SynoApi:
self._with_upgrade = True
self._with_utilisation = True
async def async_setup(self):
async def async_setup(self) -> None:
"""Start interacting with the NAS."""
self.dsm = SynologyDSM(
self._entry.data[CONF_HOST],
@ -406,7 +440,7 @@ class SynoApi:
await self.async_update()
@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."""
_LOGGER.debug("Subscribe new entity: %s", unique_id)
if api_key not in self._fetching_entities:
@ -424,7 +458,7 @@ class SynoApi:
return unsubscribe
@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."""
# Entities not added yet, fetch all
if not self._fetching_entities:
@ -488,7 +522,7 @@ class SynoApi:
self.dsm.reset(self.utilisation)
self.utilisation = None
def _fetch_device_configuration(self):
def _fetch_device_configuration(self) -> None:
"""Fetch initial device config."""
self.information = self.dsm.information
self.network = self.dsm.network
@ -523,7 +557,7 @@ class SynoApi:
)
self.surveillance_station = self.dsm.surveillance_station
async def async_reboot(self):
async def async_reboot(self) -> None:
"""Reboot NAS."""
try:
await self._hass.async_add_executor_job(self.system.reboot)
@ -534,7 +568,7 @@ class SynoApi:
)
_LOGGER.debug("Exception:%s", err)
async def async_shutdown(self):
async def async_shutdown(self) -> None:
"""Shutdown NAS."""
try:
await self._hass.async_add_executor_job(self.system.shutdown)
@ -545,7 +579,7 @@ class SynoApi:
)
_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."""
try:
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
)
async def async_update(self, now=None):
async def async_update(self, now: timedelta | None = None) -> None:
"""Update function for updating API information."""
_LOGGER.debug("Start data update for '%s'", self._entry.unique_id)
self._async_setup_api_requests()
@ -582,9 +616,9 @@ class SynologyDSMBaseEntity(CoordinatorEntity):
self,
api: SynoApi,
entity_type: str,
entity_info: dict[str, str],
coordinator: DataUpdateCoordinator,
):
entity_info: EntityInfo,
coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]],
) -> None:
"""Initialize the Synology DSM entity."""
super().__init__(coordinator)
@ -609,12 +643,12 @@ class SynologyDSMBaseEntity(CoordinatorEntity):
return self._name
@property
def icon(self) -> str:
def icon(self) -> str | None:
"""Return the icon."""
return self._icon
@property
def device_class(self) -> str:
def device_class(self) -> str | None:
"""Return the class of this device."""
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 self._enable_default
async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""Register entity for updates from API."""
self.async_on_remove(self._api.subscribe(self._api_key, self.unique_id))
await super().async_added_to_hass()
@ -652,10 +686,10 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity):
self,
api: SynoApi,
entity_type: str,
entity_info: dict[str, str],
coordinator: DataUpdateCoordinator,
device_id: str = None,
):
entity_info: EntityInfo,
coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]],
device_id: str | None = None,
) -> None:
"""Initialize the Synology DSM disk or volume entity."""
super().__init__(api, entity_type, entity_info, coordinator)
self._device_id = device_id
@ -691,16 +725,18 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return bool(self._api.storage)
return self._api.storage # type: ignore [no-any-return]
@property
def device_info(self) -> DeviceInfo:
"""Return the device information."""
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})",
"manufacturer": self._device_manufacturer,
"model": self._device_model,
"sw_version": self._device_firmware,
"manufacturer": self._device_manufacturer, # type: ignore[typeddict-item]
"model": self._device_model, # type: ignore[typeddict-item]
"sw_version": self._device_firmware, # type: ignore[typeddict-item]
"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.const import CONF_DISKS
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SynologyDSMBaseEntity, SynologyDSMDeviceEntity
from . import SynoApi, SynologyDSMBaseEntity, SynologyDSMDeviceEntity
from .const import (
COORDINATOR_CENTRAL,
DOMAIN,
@ -18,15 +19,19 @@ from .const import (
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Synology NAS binary sensor."""
data = hass.data[DOMAIN][entry.unique_id]
api = data[SYNO_API]
api: SynoApi = data[SYNO_API]
coordinator = data[COORDINATOR_CENTRAL]
entities = [
entities: list[
SynoDSMSecurityBinarySensor
| SynoDSMUpgradeBinarySensor
| SynoDSMStorageBinarySensor
] = [
SynoDSMSecurityBinarySensor(
api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type], coordinator
)
@ -63,7 +68,7 @@ class SynoDSMSecurityBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""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
def available(self) -> bool:
@ -73,7 +78,7 @@ class SynoDSMSecurityBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity):
@property
def extra_state_attributes(self) -> dict[str, str]:
"""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):
@ -82,7 +87,7 @@ class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""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):
@ -91,7 +96,7 @@ class SynoDSMUpgradeBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return the state."""
return getattr(self._api.upgrade, self.entity_type)
return bool(getattr(self._api.upgrade, self.entity_type))
@property
def available(self) -> bool:

View File

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

View File

@ -1,5 +1,8 @@
"""Config flow to configure the Synology DSM integration."""
from __future__ import annotations
import logging
from typing import Any
from urllib.parse import urlparse
from synology_dsm import SynologyDSM
@ -12,8 +15,14 @@ from synology_dsm.exceptions import (
)
import voluptuous as vol
from homeassistant import config_entries, exceptions
from homeassistant import exceptions
from homeassistant.components import ssdp
from homeassistant.config_entries import (
CONN_CLASS_CLOUD_POLL,
ConfigEntry,
ConfigFlow,
OptionsFlow,
)
from homeassistant.const import (
CONF_DISKS,
CONF_HOST,
@ -28,7 +37,9 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import (
CONF_DEVICE_TOKEN,
@ -47,11 +58,11 @@ _LOGGER = logging.getLogger(__name__)
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))
def _user_schema_with_defaults(user_input):
def _user_schema_with_defaults(user_input: dict[str, Any]) -> vol.Schema:
user_schema = {
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)
def _ordered_shared_schema(schema_input):
def _ordered_shared_schema(
schema_input: dict[str, Any]
) -> dict[vol.Required | vol.Optional, Any]:
return {
vol.Required(CONF_USERNAME, default=schema_input.get(CONF_USERNAME, "")): 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."""
VERSION = 1
CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL
@staticmethod
@callback
def async_get_options_flow(config_entry):
def async_get_options_flow(
config_entry: ConfigEntry,
) -> SynologyDSMOptionsFlowHandler:
"""Get the options flow for this handler."""
return SynologyDSMOptionsFlowHandler(config_entry)
def __init__(self):
def __init__(self) -> None:
"""Initialize the synology_dsm config flow."""
self.saved_user_input = {}
self.discovered_conf = {}
self.saved_user_input: dict[str, Any] = {}
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."""
if not user_input:
user_input = {}
@ -111,7 +131,9 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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."""
errors = {}
@ -188,7 +210,7 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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."""
parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION])
friendly_name = (
@ -211,15 +233,19 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.context["title_placeholders"] = self.discovered_conf
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."""
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."""
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."""
if not self.saved_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)
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."""
existing_macs = [
mac.replace("-", "")
@ -246,14 +272,16 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return mac in existing_macs
class SynologyDSMOptionsFlowHandler(config_entries.OptionsFlow):
class SynologyDSMOptionsFlowHandler(OptionsFlow):
"""Handle a option flow."""
def __init__(self, config_entry: config_entries.ConfigEntry):
def __init__(self, config_entry: ConfigEntry):
"""Initialize options flow."""
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."""
if user_input is not None:
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)
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."""
# These do i/o
api.login(otp_code)
@ -293,7 +321,7 @@ def _login_and_fetch_syno_info(api, otp_code):
):
raise InvalidData
return api.information.serial
return api.information.serial # type: ignore[no-any-return]
class InvalidData(exceptions.HomeAssistantError):

View File

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

View File

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

View File

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

View File

@ -529,6 +529,19 @@ warn_return_any = true
warn_unreachable = 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.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@ -1232,9 +1245,6 @@ ignore_errors = true
[mypy-homeassistant.components.switcher_kis.*]
ignore_errors = true
[mypy-homeassistant.components.synology_dsm.*]
ignore_errors = true
[mypy-homeassistant.components.synology_srm.*]
ignore_errors = true

View File

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