From 6954fcc8ad35424e21787b1217b8110a7c880fa0 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:10:56 +0100 Subject: [PATCH] Add strict typing to ring integration (#115276) --- .strict-typing | 1 + homeassistant/components/ring/__init__.py | 47 +-- .../components/ring/binary_sensor.py | 62 ++-- homeassistant/components/ring/button.py | 21 +- homeassistant/components/ring/camera.py | 76 ++--- homeassistant/components/ring/config_flow.py | 14 +- homeassistant/components/ring/const.py | 6 - homeassistant/components/ring/coordinator.py | 74 ++--- homeassistant/components/ring/diagnostics.py | 12 +- homeassistant/components/ring/entity.py | 56 ++-- homeassistant/components/ring/light.py | 46 +-- homeassistant/components/ring/sensor.py | 269 +++++++++--------- homeassistant/components/ring/siren.py | 25 +- homeassistant/components/ring/switch.py | 36 +-- mypy.ini | 10 + tests/components/ring/common.py | 2 +- 16 files changed, 391 insertions(+), 366 deletions(-) diff --git a/.strict-typing b/.strict-typing index b1d6df7c9b8aa2..63a867e9c5059c 100644 --- a/.strict-typing +++ b/.strict-typing @@ -363,6 +363,7 @@ homeassistant.components.rest_command.* homeassistant.components.rfxtrx.* homeassistant.components.rhasspy.* homeassistant.components.ridwell.* +homeassistant.components.ring.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* homeassistant.components.romy.* diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index ffa9970452675c..36c66550ddcf5e 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -2,10 +2,12 @@ from __future__ import annotations +from dataclasses import dataclass from functools import partial import logging +from typing import Any, cast -from ring_doorbell import Auth, Ring +from ring_doorbell import Auth, Ring, RingDevices from homeassistant.config_entries import ConfigEntry from homeassistant.const import APPLICATION_NAME, CONF_TOKEN, __version__ @@ -13,23 +15,26 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from .const import ( - DOMAIN, - PLATFORMS, - RING_API, - RING_DEVICES, - RING_DEVICES_COORDINATOR, - RING_NOTIFICATIONS_COORDINATOR, -) +from .const import DOMAIN, PLATFORMS from .coordinator import RingDataCoordinator, RingNotificationsCoordinator _LOGGER = logging.getLogger(__name__) +@dataclass +class RingData: + """Class to support type hinting of ring data collection.""" + + api: Ring + devices: RingDevices + devices_coordinator: RingDataCoordinator + notifications_coordinator: RingNotificationsCoordinator + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - def token_updater(token): + def token_updater(token: dict[str, Any]) -> None: """Handle from sync context when token is updated.""" hass.loop.call_soon_threadsafe( partial( @@ -51,12 +56,12 @@ def token_updater(token): await devices_coordinator.async_config_entry_first_refresh() await notifications_coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - RING_API: ring, - RING_DEVICES: ring.devices(), - RING_DEVICES_COORDINATOR: devices_coordinator, - RING_NOTIFICATIONS_COORDINATOR: notifications_coordinator, - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = RingData( + api=ring, + devices=ring.devices(), + devices_coordinator=devices_coordinator, + notifications_coordinator=notifications_coordinator, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -83,8 +88,9 @@ async def async_refresh_all(_: ServiceCall) -> None: ) for info in hass.data[DOMAIN].values(): - await info[RING_DEVICES_COORDINATOR].async_refresh() - await info[RING_NOTIFICATIONS_COORDINATOR].async_refresh() + ring_data = cast(RingData, info) + await ring_data.devices_coordinator.async_refresh() + await ring_data.notifications_coordinator.async_refresh() # register service hass.services.async_register(DOMAIN, "update", async_refresh_all) @@ -121,8 +127,9 @@ async def _migrate_old_unique_ids(hass: HomeAssistant, entry_id: str) -> None: @callback def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, str] | None: # Old format for camera and light was int - if isinstance(entity_entry.unique_id, int): - new_unique_id = str(entity_entry.unique_id) + unique_id = cast(str | int, entity_entry.unique_id) + if isinstance(unique_id, int): + new_unique_id = str(unique_id) if existing_entity_id := entity_registry.async_get_entity_id( entity_entry.domain, entity_entry.platform, new_unique_id ): diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 19daebf9ce1b78..2db04cfd46168f 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -2,10 +2,13 @@ from __future__ import annotations +from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import datetime from typing import Any +from ring_doorbell import Ring, RingEvent, RingGeneric + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -15,29 +18,32 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, RING_API, RING_DEVICES, RING_NOTIFICATIONS_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingNotificationsCoordinator -from .entity import RingEntity +from .entity import RingBaseEntity @dataclass(frozen=True, kw_only=True) class RingBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes Ring binary sensor entity.""" - category: list[str] + exists_fn: Callable[[RingGeneric], bool] BINARY_SENSOR_TYPES: tuple[RingBinarySensorEntityDescription, ...] = ( RingBinarySensorEntityDescription( key="ding", translation_key="ding", - category=["doorbots", "authorized_doorbots", "other"], device_class=BinarySensorDeviceClass.OCCUPANCY, + exists_fn=lambda device: device.family + in {"doorbots", "authorized_doorbots", "other"}, ), RingBinarySensorEntityDescription( key="motion", - category=["doorbots", "authorized_doorbots", "stickup_cams"], device_class=BinarySensorDeviceClass.MOTION, + exists_fn=lambda device: device.family + in {"doorbots", "authorized_doorbots", "stickup_cams"}, ), ) @@ -48,34 +54,36 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Ring binary sensors from a config entry.""" - ring = hass.data[DOMAIN][config_entry.entry_id][RING_API] - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - notifications_coordinator: RingNotificationsCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ][RING_NOTIFICATIONS_COORDINATOR] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] entities = [ - RingBinarySensor(ring, device, notifications_coordinator, description) - for device_type in ("doorbots", "authorized_doorbots", "stickup_cams", "other") + RingBinarySensor( + ring_data.api, + device, + ring_data.notifications_coordinator, + description, + ) for description in BINARY_SENSOR_TYPES - if device_type in description.category - for device in devices[device_type] + for device in ring_data.devices.all_devices + if description.exists_fn(device) ] async_add_entities(entities) -class RingBinarySensor(RingEntity, BinarySensorEntity): +class RingBinarySensor( + RingBaseEntity[RingNotificationsCoordinator], BinarySensorEntity +): """A binary sensor implementation for Ring device.""" - _active_alert: dict[str, Any] | None = None + _active_alert: RingEvent | None = None entity_description: RingBinarySensorEntityDescription def __init__( self, - ring, - device, - coordinator, + ring: Ring, + device: RingGeneric, + coordinator: RingNotificationsCoordinator, description: RingBinarySensorEntityDescription, ) -> None: """Initialize a sensor for Ring device.""" @@ -89,13 +97,13 @@ def __init__( self._update_alert() @callback - def _handle_coordinator_update(self, _=None): + def _handle_coordinator_update(self, _: Any = None) -> None: """Call update method.""" self._update_alert() super()._handle_coordinator_update() @callback - def _update_alert(self): + def _update_alert(self) -> None: """Update active alert.""" self._active_alert = next( ( @@ -108,21 +116,23 @@ def _update_alert(self): ) @property - def is_on(self): + def is_on(self) -> bool: """Return True if the binary sensor is on.""" return self._active_alert is not None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes.""" attrs = super().extra_state_attributes if self._active_alert is None: return attrs + assert isinstance(attrs, dict) attrs["state"] = self._active_alert["state"] - attrs["expires_at"] = datetime.fromtimestamp( - self._active_alert.get("now") + self._active_alert.get("expires_in") - ).isoformat() + now = self._active_alert.get("now") + expires_in = self._active_alert.get("expires_in") + assert now and expires_in + attrs["expires_at"] = datetime.fromtimestamp(now + expires_in).isoformat() return attrs diff --git a/homeassistant/components/ring/button.py b/homeassistant/components/ring/button.py index d739dc29841152..a14853a08813e5 100644 --- a/homeassistant/components/ring/button.py +++ b/homeassistant/components/ring/button.py @@ -2,12 +2,15 @@ from __future__ import annotations +from ring_doorbell import RingOther + from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -22,14 +25,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the buttons for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( RingDoorButton(device, devices_coordinator, BUTTON_DESCRIPTION) - for device in devices["other"] + for device in ring_data.devices.other if device.has_capability("open") ) @@ -37,10 +38,12 @@ async def async_setup_entry( class RingDoorButton(RingEntity, ButtonEntity): """Creates a button to open the ring intercom door.""" + _device: RingOther + def __init__( self, - device, - coordinator, + device: RingOther, + coordinator: RingDataCoordinator, description: ButtonEntityDescription, ) -> None: """Initialize the button.""" @@ -52,6 +55,6 @@ def __init__( self._attr_unique_id = f"{device.id}-{description.key}" @exception_wrap - def press(self): + def press(self) -> None: """Open the door.""" self._device.open_door() diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index b9d73afe6de195..297e5f4762703e 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -3,11 +3,12 @@ from __future__ import annotations from datetime import timedelta -from itertools import chain import logging -from typing import Optional +from typing import Any +from aiohttp import web from haffmpeg.camera import CameraMjpeg +from ring_doorbell import RingDoorBell from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera @@ -17,7 +18,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -33,20 +35,15 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Ring Door Bell and StickUp Camera.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass) - cams = [] - for camera in chain( - devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"] - ): - if not camera.has_subscription: - continue - - cams.append(RingCam(camera, devices_coordinator, ffmpeg_manager)) + cams = [ + RingCam(camera, devices_coordinator, ffmpeg_manager) + for camera in ring_data.devices.video_devices + if camera.has_subscription + ] async_add_entities(cams) @@ -55,28 +52,34 @@ class RingCam(RingEntity, Camera): """An implementation of a Ring Door Bell camera.""" _attr_name = None - - def __init__(self, device, coordinator, ffmpeg_manager): + _device: RingDoorBell + + def __init__( + self, + device: RingDoorBell, + coordinator: RingDataCoordinator, + ffmpeg_manager: ffmpeg.FFmpegManager, + ) -> None: """Initialize a Ring Door Bell camera.""" super().__init__(device, coordinator) Camera.__init__(self) - self._ffmpeg_manager = ffmpeg_manager - self._last_event = None - self._last_video_id = None - self._video_url = None - self._image = None + self._last_event: dict[str, Any] | None = None + self._last_video_id: int | None = None + self._video_url: str | None = None + self._image: bytes | None = None self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL self._attr_unique_id = str(device.id) if device.has_capability(MOTION_DETECTION_CAPABILITY): self._attr_motion_detection_enabled = device.motion_detection @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" - history_data: Optional[list] - if not (history_data := self._get_coordinator_history()): - return + self._device = self._get_coordinator_data().get_video_device( + self._device.device_api_id + ) + history_data = self._device.last_history if history_data: self._last_event = history_data[0] self.async_schedule_update_ha_state(True) @@ -89,7 +92,7 @@ def _handle_coordinator_update(self): self.async_write_ha_state() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { "video_url": self._video_url, @@ -100,7 +103,7 @@ async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - if self._image is None and self._video_url: + if self._image is None and self._video_url is not None: image = await ffmpeg.async_get_image( self.hass, self._video_url, @@ -113,10 +116,12 @@ async def async_camera_image( return self._image - async def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream( + self, request: web.Request + ) -> web.StreamResponse | None: """Generate an HTTP MJPEG stream from the camera.""" if self._video_url is None: - return + return None stream = CameraMjpeg(self._ffmpeg_manager.binary) await stream.open_camera(self._video_url) @@ -132,7 +137,7 @@ async def handle_async_mjpeg_stream(self, request): finally: await stream.close() - async def async_update(self): + async def async_update(self) -> None: """Update camera entity and refresh attributes.""" if ( self._device.has_capability(MOTION_DETECTION_CAPABILITY) @@ -160,11 +165,14 @@ async def async_update(self): self._expires_at = FORCE_REFRESH_INTERVAL + utcnow @exception_wrap - def _get_video(self): - return self._device.recording_url(self._last_event["id"]) + def _get_video(self) -> str | None: + if self._last_event is None: + return None + assert (event_id := self._last_event.get("id")) and isinstance(event_id, int) + return self._device.recording_url(event_id) @exception_wrap - def _set_motion_detection_enabled(self, new_state): + def _set_motion_detection_enabled(self, new_state: bool) -> None: if not self._device.has_capability(MOTION_DETECTION_CAPABILITY): _LOGGER.error( "Entity %s does not have motion detection capability", self.entity_id diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 6d4f28eb3113ac..4762017c5bc183 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -28,7 +28,7 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) -async def validate_input(hass: HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, Any]: """Validate the user input allows us to connect.""" auth = Auth(f"{APPLICATION_NAME}/{ha_version}") @@ -56,9 +56,11 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN): user_pass: dict[str, Any] = {} reauth_entry: ConfigEntry | None = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: try: token = await validate_input(self.hass, user_input) @@ -82,7 +84,9 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_2fa(self, user_input=None): + async def async_step_2fa( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle 2fa step.""" if user_input: if self.reauth_entry: @@ -110,7 +114,7 @@ async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" - errors = {} + errors: dict[str, str] = {} assert self.reauth_entry is not None if user_input: diff --git a/homeassistant/components/ring/const.py b/homeassistant/components/ring/const.py index 23f378a38be427..70813a78c76b9e 100644 --- a/homeassistant/components/ring/const.py +++ b/homeassistant/components/ring/const.py @@ -28,10 +28,4 @@ SCAN_INTERVAL = timedelta(minutes=1) NOTIFICATIONS_SCAN_INTERVAL = timedelta(seconds=5) -RING_API = "api" -RING_DEVICES = "devices" - -RING_DEVICES_COORDINATOR = "device_data" -RING_NOTIFICATIONS_COORDINATOR = "dings_data" - CONF_2FA = "2fa" diff --git a/homeassistant/components/ring/coordinator.py b/homeassistant/components/ring/coordinator.py index fdb6fc1f296f04..a10f9317babfa2 100644 --- a/homeassistant/components/ring/coordinator.py +++ b/homeassistant/components/ring/coordinator.py @@ -2,11 +2,10 @@ from asyncio import TaskGroup from collections.abc import Callable -from dataclasses import dataclass import logging -from typing import Any, Optional +from typing import TypeVar, TypeVarTuple -from ring_doorbell import AuthenticationError, Ring, RingError, RingGeneric, RingTimeout +from ring_doorbell import AuthenticationError, Ring, RingDevices, RingError, RingTimeout from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -16,10 +15,13 @@ _LOGGER = logging.getLogger(__name__) +_R = TypeVar("_R") +_Ts = TypeVarTuple("_Ts") + async def _call_api( - hass: HomeAssistant, target: Callable[..., Any], *args, msg_suffix: str = "" -): + hass: HomeAssistant, target: Callable[[*_Ts], _R], *args: *_Ts, msg_suffix: str = "" +) -> _R: try: return await hass.async_add_executor_job(target, *args) except AuthenticationError as err: @@ -34,15 +36,7 @@ async def _call_api( raise UpdateFailed(f"Error communicating with API{msg_suffix}: {err}") from err -@dataclass -class RingDeviceData: - """RingDeviceData.""" - - device: RingGeneric - history: Optional[list] = None - - -class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]): +class RingDataCoordinator(DataUpdateCoordinator[RingDevices]): """Base class for device coordinators.""" def __init__( @@ -60,45 +54,39 @@ def __init__( self.ring_api: Ring = ring_api self.first_call: bool = True - async def _async_update_data(self): + async def _async_update_data(self) -> RingDevices: """Fetch data from API endpoint.""" update_method: str = "update_data" if self.first_call else "update_devices" await _call_api(self.hass, getattr(self.ring_api, update_method)) self.first_call = False - data: dict[str, RingDeviceData] = {} - devices: dict[str : list[RingGeneric]] = self.ring_api.devices() + devices: RingDevices = self.ring_api.devices() subscribed_device_ids = set(self.async_contexts()) - for device_type in devices: - for device in devices[device_type]: - # Don't update all devices in the ring api, only those that set - # their device id as context when they subscribed. - if device.id in subscribed_device_ids: - data[device.id] = RingDeviceData(device=device) - try: - history_task = None - async with TaskGroup() as tg: - if device.has_capability("history"): - history_task = tg.create_task( - _call_api( - self.hass, - lambda device: device.history(limit=10), - device, - msg_suffix=f" for device {device.name}", # device_id is the mac - ) - ) + for device in devices.all_devices: + # Don't update all devices in the ring api, only those that set + # their device id as context when they subscribed. + if device.id in subscribed_device_ids: + try: + async with TaskGroup() as tg: + if device.has_capability("history"): tg.create_task( _call_api( self.hass, - device.update_health_data, - msg_suffix=f" for device {device.name}", + lambda device: device.history(limit=10), + device, + msg_suffix=f" for device {device.name}", # device_id is the mac ) ) - if history_task: - data[device.id].history = history_task.result() - except ExceptionGroup as eg: - raise eg.exceptions[0] # noqa: B904 + tg.create_task( + _call_api( + self.hass, + device.update_health_data, + msg_suffix=f" for device {device.name}", + ) + ) + except ExceptionGroup as eg: + raise eg.exceptions[0] # noqa: B904 - return data + return devices class RingNotificationsCoordinator(DataUpdateCoordinator[None]): @@ -114,6 +102,6 @@ def __init__(self, hass: HomeAssistant, ring_api: Ring) -> None: ) self.ring_api: Ring = ring_api - async def _async_update_data(self): + async def _async_update_data(self) -> None: """Fetch data from API endpoint.""" await _call_api(self.hass, self.ring_api.update_dings) diff --git a/homeassistant/components/ring/diagnostics.py b/homeassistant/components/ring/diagnostics.py index 5295629979a6c4..2e7604d9f502e6 100644 --- a/homeassistant/components/ring/diagnostics.py +++ b/homeassistant/components/ring/diagnostics.py @@ -4,12 +4,11 @@ from typing import Any -from ring_doorbell import Ring - from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from . import RingData from .const import DOMAIN TO_REDACT = { @@ -33,11 +32,12 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - ring: Ring = hass.data[DOMAIN][entry.entry_id]["api"] + ring_data: RingData = hass.data[DOMAIN][entry.entry_id] + devices_data = ring_data.api.devices_data devices_raw = [ - ring.devices_data[device_type][device_id] - for device_type in ring.devices_data - for device_id in ring.devices_data[device_type] + devices_data[device_type][device_id] + for device_type in devices_data + for device_id in devices_data[device_type] ] return async_redact_data( {"device_data": devices_raw}, diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index fb617ecd7d13c1..54f76a19c5de49 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -3,7 +3,13 @@ from collections.abc import Callable from typing import Any, Concatenate, ParamSpec, TypeVar -from ring_doorbell import AuthenticationError, RingError, RingGeneric, RingTimeout +from ring_doorbell import ( + AuthenticationError, + RingDevices, + RingError, + RingGeneric, + RingTimeout, +) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -11,26 +17,23 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, DOMAIN -from .coordinator import ( - RingDataCoordinator, - RingDeviceData, - RingNotificationsCoordinator, -) +from .coordinator import RingDataCoordinator, RingNotificationsCoordinator _RingCoordinatorT = TypeVar( "_RingCoordinatorT", bound=(RingDataCoordinator | RingNotificationsCoordinator), ) -_T = TypeVar("_T", bound="RingEntity") +_RingBaseEntityT = TypeVar("_RingBaseEntityT", bound="RingBaseEntity[Any]") +_R = TypeVar("_R") _P = ParamSpec("_P") def exception_wrap( - func: Callable[Concatenate[_T, _P], Any], -) -> Callable[Concatenate[_T, _P], Any]: + func: Callable[Concatenate[_RingBaseEntityT, _P], _R], +) -> Callable[Concatenate[_RingBaseEntityT, _P], _R]: """Define a wrapper to catch exceptions and raise HomeAssistant errors.""" - def _wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + def _wrap(self: _RingBaseEntityT, *args: _P.args, **kwargs: _P.kwargs) -> _R: try: return func(self, *args, **kwargs) except AuthenticationError as err: @@ -50,7 +53,7 @@ def _wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: return _wrap -class RingEntity(CoordinatorEntity[_RingCoordinatorT]): +class RingBaseEntity(CoordinatorEntity[_RingCoordinatorT]): """Base implementation for Ring device.""" _attr_attribution = ATTRIBUTION @@ -73,29 +76,16 @@ def __init__( name=device.name, ) - def _get_coordinator_device_data(self) -> RingDeviceData | None: - if (data := self.coordinator.data) and ( - device_data := data.get(self._device.id) - ): - return device_data - return None - - def _get_coordinator_device(self) -> RingGeneric | None: - if (device_data := self._get_coordinator_device_data()) and ( - device := device_data.device - ): - return device - return None - - def _get_coordinator_history(self) -> list | None: - if (device_data := self._get_coordinator_device_data()) and ( - history := device_data.history - ): - return history - return None + +class RingEntity(RingBaseEntity[RingDataCoordinator]): + """Implementation for Ring devices.""" + + def _get_coordinator_data(self) -> RingDevices: + return self.coordinator.data @callback def _handle_coordinator_update(self) -> None: - if device := self._get_coordinator_device(): - self._device = device + self._device = self._get_coordinator_data().get_device( + self._device.device_api_id + ) super()._handle_coordinator_update() diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index b9e1c8c38b4362..a4eb8df5b46f6c 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,6 +1,7 @@ """Component providing HA switch support for Ring Door Bell/Chimes.""" from datetime import timedelta +from enum import StrEnum, auto import logging from typing import Any @@ -12,7 +13,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -26,8 +28,12 @@ SKIP_UPDATES_DELAY = timedelta(seconds=5) -ON_STATE = "on" -OFF_STATE = "off" + +class OnOffState(StrEnum): + """Enum for allowed on off states.""" + + ON = auto() + OFF = auto() async def async_setup_entry( @@ -36,14 +42,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the lights for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( RingLight(device, devices_coordinator) - for device in devices["stickup_cams"] + for device in ring_data.devices.stickup_cams if device.has_capability("light") ) @@ -55,37 +59,41 @@ class RingLight(RingEntity, LightEntity): _attr_supported_color_modes = {ColorMode.ONOFF} _attr_translation_key = "light" - def __init__(self, device, coordinator): + _device: RingStickUpCam + + def __init__( + self, device: RingStickUpCam, coordinator: RingDataCoordinator + ) -> None: """Initialize the light.""" super().__init__(device, coordinator) self._attr_unique_id = str(device.id) - self._attr_is_on = device.lights == ON_STATE + self._attr_is_on = device.lights == OnOffState.ON self._no_updates_until = dt_util.utcnow() @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" if self._no_updates_until > dt_util.utcnow(): return - if (device := self._get_coordinator_device()) and isinstance( - device, RingStickUpCam - ): - self._attr_is_on = device.lights == ON_STATE + device = self._get_coordinator_data().get_stickup_cam( + self._device.device_api_id + ) + self._attr_is_on = device.lights == OnOffState.ON super()._handle_coordinator_update() @exception_wrap - def _set_light(self, new_state): + def _set_light(self, new_state: OnOffState) -> None: """Update light state, and causes Home Assistant to correctly update.""" self._device.lights = new_state - self._attr_is_on = new_state == ON_STATE + self._attr_is_on = new_state == OnOffState.ON self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.schedule_update_ha_state() def turn_on(self, **kwargs: Any) -> None: """Turn the light on for 30 seconds.""" - self._set_light(ON_STATE) + self._set_light(OnOffState.ON) def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - self._set_light(OFF_STATE) + self._set_light(OnOffState.OFF) diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 9ba677e7e5b33e..0c4d1f4fdf5567 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -2,10 +2,19 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import Any - -from ring_doorbell import RingGeneric +from typing import Any, Generic, cast + +from ring_doorbell import ( + RingCapability, + RingChime, + RingDoorBell, + RingEventKind, + RingGeneric, + RingOther, +) +from typing_extensions import TypeVar from homeassistant.components.sensor import ( SensorDeviceClass, @@ -21,11 +30,15 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity +_RingDeviceT = TypeVar("_RingDeviceT", bound=RingGeneric, default=RingGeneric) + async def async_setup_entry( hass: HomeAssistant, @@ -33,209 +46,193 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a sensor for a Ring device.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator entities = [ - description.cls(device, devices_coordinator, description) - for device_type in ( - "chimes", - "doorbots", - "authorized_doorbots", - "stickup_cams", - "other", - ) + RingSensor(device, devices_coordinator, description) for description in SENSOR_TYPES - if device_type in description.category - for device in devices[device_type] - if not (device_type == "battery" and device.battery_life is None) + for device in ring_data.devices.all_devices + if description.exists_fn(device) ] async_add_entities(entities) -class RingSensor(RingEntity, SensorEntity): +class RingSensor(RingEntity, SensorEntity, Generic[_RingDeviceT]): """A sensor implementation for Ring device.""" - entity_description: RingSensorEntityDescription + entity_description: RingSensorEntityDescription[_RingDeviceT] + _device: _RingDeviceT def __init__( self, device: RingGeneric, coordinator: RingDataCoordinator, - description: RingSensorEntityDescription, + description: RingSensorEntityDescription[_RingDeviceT], ) -> None: """Initialize a sensor for Ring device.""" super().__init__(device, coordinator) self.entity_description = description self._attr_unique_id = f"{device.id}-{description.key}" - - @property - def native_value(self): - """Return the state of the sensor.""" - sensor_type = self.entity_description.key - if sensor_type == "volume": - return self._device.volume - if sensor_type == "doorbell_volume": - return self._device.doorbell_volume - if sensor_type == "mic_volume": - return self._device.mic_volume - if sensor_type == "voice_volume": - return self._device.voice_volume - - if sensor_type == "battery": - return self._device.battery_life - - -class HealthDataRingSensor(RingSensor): - """Ring sensor that relies on health data.""" - - # These sensors are data hungry and not useful. Disable by default. - _attr_entity_registry_enabled_default = False - - @property - def native_value(self): - """Return the state of the sensor.""" - sensor_type = self.entity_description.key - if sensor_type == "wifi_signal_category": - return self._device.wifi_signal_category - - if sensor_type == "wifi_signal_strength": - return self._device.wifi_signal_strength - - -class HistoryRingSensor(RingSensor): - """Ring sensor that relies on history data.""" - - _latest_event: dict[str, Any] | None = None + self._attr_entity_registry_enabled_default = ( + description.entity_registry_enabled_default + ) + self._attr_native_value = self.entity_description.value_fn(self._device) @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" - if not (history_data := self._get_coordinator_history()): - return - - kind = self.entity_description.kind - found = None - if kind is None: - found = history_data[0] - else: - for entry in history_data: - if entry["kind"] == kind: - found = entry - break - - if not found: - return - - self._latest_event = found - super()._handle_coordinator_update() - - @property - def native_value(self): - """Return the state of the sensor.""" - if self._latest_event is None: - return None - return self._latest_event["created_at"] - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - attrs = super().extra_state_attributes + self._device = cast( + _RingDeviceT, + self._get_coordinator_data().get_device(self._device.device_api_id), + ) + # History values can drop off the last 10 events so only update + # the value if it's not None + if native_value := self.entity_description.value_fn(self._device): + self._attr_native_value = native_value + if extra_attrs := self.entity_description.extra_state_attributes_fn( + self._device + ): + self._attr_extra_state_attributes = extra_attrs + super()._handle_coordinator_update() - if self._latest_event: - attrs["created_at"] = self._latest_event["created_at"] - attrs["answered"] = self._latest_event["answered"] - attrs["recording_status"] = self._latest_event["recording"]["status"] - attrs["category"] = self._latest_event["kind"] - return attrs +def _get_last_event( + history_data: list[dict[str, Any]], kind: RingEventKind | None +) -> dict[str, Any] | None: + if not history_data: + return None + if kind is None: + return history_data[0] + for entry in history_data: + if entry["kind"] == kind.value: + return entry + return None + + +def _get_last_event_attrs( + history_data: list[dict[str, Any]], kind: RingEventKind | None +) -> dict[str, Any] | None: + if last_event := _get_last_event(history_data, kind): + return { + "created_at": last_event.get("created_at"), + "answered": last_event.get("answered"), + "recording_status": last_event.get("recording", {}).get("status"), + "category": last_event.get("kind"), + } + return None @dataclass(frozen=True, kw_only=True) -class RingSensorEntityDescription(SensorEntityDescription): +class RingSensorEntityDescription(SensorEntityDescription, Generic[_RingDeviceT]): """Describes Ring sensor entity.""" - category: list[str] - cls: type[RingSensor] + value_fn: Callable[[_RingDeviceT], StateType] = lambda _: True + exists_fn: Callable[[RingGeneric], bool] = lambda _: True + extra_state_attributes_fn: Callable[[_RingDeviceT], dict[str, Any] | None] = ( + lambda _: None + ) - kind: str | None = None - -SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( - RingSensorEntityDescription( +# For some reason mypy doesn't properly type check the default TypeVar value here +# so for now the [RingGeneric] subscript needs to be specified. +# Once https://github.com/python/mypy/issues/14851 is closed this should hopefully +# be fixed and the [RingGeneric] subscript can be removed. +# https://github.com/home-assistant/core/pull/115276#discussion_r1560106576 +SENSOR_TYPES: tuple[RingSensorEntityDescription[Any], ...] = ( + RingSensorEntityDescription[RingGeneric]( key="battery", - category=["doorbots", "authorized_doorbots", "stickup_cams", "other"], native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - cls=RingSensor, + value_fn=lambda device: device.battery_life, + exists_fn=lambda device: device.family != "chimes", ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="last_activity", translation_key="last_activity", - category=["doorbots", "authorized_doorbots", "stickup_cams", "other"], device_class=SensorDeviceClass.TIMESTAMP, - cls=HistoryRingSensor, + value_fn=lambda device: last_event.get("created_at") + if (last_event := _get_last_event(device.last_history, None)) + else None, + extra_state_attributes_fn=lambda device: last_event_attrs + if (last_event_attrs := _get_last_event_attrs(device.last_history, None)) + else None, + exists_fn=lambda device: device.has_capability(RingCapability.HISTORY), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="last_ding", translation_key="last_ding", - category=["doorbots", "authorized_doorbots", "other"], - kind="ding", device_class=SensorDeviceClass.TIMESTAMP, - cls=HistoryRingSensor, + value_fn=lambda device: last_event.get("created_at") + if (last_event := _get_last_event(device.last_history, RingEventKind.DING)) + else None, + extra_state_attributes_fn=lambda device: last_event_attrs + if ( + last_event_attrs := _get_last_event_attrs( + device.last_history, RingEventKind.DING + ) + ) + else None, + exists_fn=lambda device: device.has_capability(RingCapability.HISTORY), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="last_motion", translation_key="last_motion", - category=["doorbots", "authorized_doorbots", "stickup_cams"], - kind="motion", device_class=SensorDeviceClass.TIMESTAMP, - cls=HistoryRingSensor, + value_fn=lambda device: last_event.get("created_at") + if (last_event := _get_last_event(device.last_history, RingEventKind.MOTION)) + else None, + extra_state_attributes_fn=lambda device: last_event_attrs + if ( + last_event_attrs := _get_last_event_attrs( + device.last_history, RingEventKind.MOTION + ) + ) + else None, + exists_fn=lambda device: device.has_capability(RingCapability.HISTORY), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingDoorBell | RingChime]( key="volume", translation_key="volume", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - cls=RingSensor, + value_fn=lambda device: device.volume, + exists_fn=lambda device: isinstance(device, (RingDoorBell, RingChime)), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingOther]( key="doorbell_volume", translation_key="doorbell_volume", - category=["other"], - cls=RingSensor, + value_fn=lambda device: device.doorbell_volume, + exists_fn=lambda device: isinstance(device, RingOther), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingOther]( key="mic_volume", translation_key="mic_volume", - category=["other"], - cls=RingSensor, + value_fn=lambda device: device.mic_volume, + exists_fn=lambda device: isinstance(device, RingOther), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingOther]( key="voice_volume", translation_key="voice_volume", - category=["other"], - cls=RingSensor, + value_fn=lambda device: device.voice_volume, + exists_fn=lambda device: isinstance(device, RingOther), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="wifi_signal_category", translation_key="wifi_signal_category", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"], entity_category=EntityCategory.DIAGNOSTIC, - cls=HealthDataRingSensor, + entity_registry_enabled_default=False, + value_fn=lambda device: device.wifi_signal_category, ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="wifi_signal_strength", translation_key="wifi_signal_strength", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"], native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, - cls=HealthDataRingSensor, + entity_registry_enabled_default=False, + value_fn=lambda device: device.wifi_signal_strength, ), ) diff --git a/homeassistant/components/ring/siren.py b/homeassistant/components/ring/siren.py index 4b7d9243dbfe98..27f68258badb35 100644 --- a/homeassistant/components/ring/siren.py +++ b/homeassistant/components/ring/siren.py @@ -1,15 +1,17 @@ """Component providing HA Siren support for Ring Chimes.""" import logging +from typing import Any -from ring_doorbell.const import CHIME_TEST_SOUND_KINDS, KIND_DING +from ring_doorbell import RingChime, RingEventKind from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -22,32 +24,33 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the sirens for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( - RingChimeSiren(device, coordinator) for device in devices["chimes"] + RingChimeSiren(device, devices_coordinator) + for device in ring_data.devices.chimes ) class RingChimeSiren(RingEntity, SirenEntity): """Creates a siren to play the test chimes of a Chime device.""" - _attr_available_tones = list(CHIME_TEST_SOUND_KINDS) + _attr_available_tones = [RingEventKind.DING.value, RingEventKind.MOTION.value] _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES _attr_translation_key = "siren" - def __init__(self, device, coordinator: RingDataCoordinator) -> None: + _device: RingChime + + def __init__(self, device: RingChime, coordinator: RingDataCoordinator) -> None: """Initialize a Ring Chime siren.""" super().__init__(device, coordinator) # Entity class attributes self._attr_unique_id = f"{self._device.id}-siren" @exception_wrap - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Play the test sound on a Ring Chime device.""" - tone = kwargs.get(ATTR_TONE) or KIND_DING + tone = kwargs.get(ATTR_TONE) or RingEventKind.DING.value self._device.test_sound(kind=tone) diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 2221eeb7705db6..b5cd59ac2fb43f 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -4,7 +4,7 @@ import logging from typing import Any -from ring_doorbell import RingGeneric, RingStickUpCam +from ring_doorbell import RingStickUpCam from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -12,7 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -33,14 +34,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the switches for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( - SirenSwitch(device, coordinator) - for device in devices["stickup_cams"] + SirenSwitch(device, devices_coordinator) + for device in ring_data.devices.stickup_cams if device.has_capability("siren") ) @@ -48,8 +47,10 @@ async def async_setup_entry( class BaseRingSwitch(RingEntity, SwitchEntity): """Represents a switch for controlling an aspect of a ring device.""" + _device: RingStickUpCam + def __init__( - self, device: RingGeneric, coordinator: RingDataCoordinator, device_type: str + self, device: RingStickUpCam, coordinator: RingDataCoordinator, device_type: str ) -> None: """Initialize the switch.""" super().__init__(device, coordinator) @@ -62,26 +63,27 @@ class SirenSwitch(BaseRingSwitch): _attr_translation_key = "siren" - def __init__(self, device, coordinator: RingDataCoordinator) -> None: + def __init__( + self, device: RingStickUpCam, coordinator: RingDataCoordinator + ) -> None: """Initialize the switch for a device with a siren.""" super().__init__(device, coordinator, "siren") self._no_updates_until = dt_util.utcnow() self._attr_is_on = device.siren > 0 @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" if self._no_updates_until > dt_util.utcnow(): return - - if (device := self._get_coordinator_device()) and isinstance( - device, RingStickUpCam - ): - self._attr_is_on = device.siren > 0 + device = self._get_coordinator_data().get_stickup_cam( + self._device.device_api_id + ) + self._attr_is_on = device.siren > 0 super()._handle_coordinator_update() @exception_wrap - def _set_switch(self, new_state): + def _set_switch(self, new_state: int) -> None: """Update switch state, and causes Home Assistant to correctly update.""" self._device.siren = new_state diff --git a/mypy.ini b/mypy.ini index 159101a21b3bd8..3e0419be2694bb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3391,6 +3391,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ring.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.rituals_perfume_genie.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index c6852bf87d65d2..b129623aa9571d 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -15,4 +15,4 @@ async def setup_platform(hass, platform): ) with patch("homeassistant.components.ring.PLATFORMS", [platform]): assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True)