Improve bluetooth generic typing (#84891)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Marc Mueller 2023-01-03 08:19:53 +01:00 committed by GitHub
parent 6b95fa5942
commit 972eb34ed9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 71 additions and 42 deletions

View file

@ -21,7 +21,7 @@ _T = TypeVar("_T")
class ActiveBluetoothDataUpdateCoordinator( class ActiveBluetoothDataUpdateCoordinator(
Generic[_T], PassiveBluetoothDataUpdateCoordinator PassiveBluetoothDataUpdateCoordinator, Generic[_T]
): ):
""" """
A coordinator that receives passive data from advertisements but can also poll. A coordinator that receives passive data from advertisements but can also poll.

View file

@ -1,10 +1,13 @@
"""Passive update coordinator for the Bluetooth integration.""" """Passive update coordinator for the Bluetooth integration."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, TypeVar
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import (
BaseCoordinatorEntity,
BaseDataUpdateCoordinatorProtocol,
)
from .update_coordinator import BasePassiveBluetoothCoordinator from .update_coordinator import BasePassiveBluetoothCoordinator
@ -14,8 +17,15 @@ if TYPE_CHECKING:
from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
_PassiveBluetoothDataUpdateCoordinatorT = TypeVar(
"_PassiveBluetoothDataUpdateCoordinatorT",
bound="PassiveBluetoothDataUpdateCoordinator",
)
class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator):
class PassiveBluetoothDataUpdateCoordinator(
BasePassiveBluetoothCoordinator, BaseDataUpdateCoordinatorProtocol
):
"""Class to manage passive bluetooth advertisements. """Class to manage passive bluetooth advertisements.
This coordinator is responsible for dispatching the bluetooth data This coordinator is responsible for dispatching the bluetooth data
@ -78,11 +88,11 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator):
self.async_update_listeners() self.async_update_listeners()
class PassiveBluetoothCoordinatorEntity(CoordinatorEntity): class PassiveBluetoothCoordinatorEntity(
BaseCoordinatorEntity[_PassiveBluetoothDataUpdateCoordinatorT]
):
"""A class for entities using DataUpdateCoordinator.""" """A class for entities using DataUpdateCoordinator."""
coordinator: PassiveBluetoothDataUpdateCoordinator
async def async_update(self) -> None: async def async_update(self) -> None:
"""All updates are passive.""" """All updates are passive."""

View file

@ -64,10 +64,11 @@ async def async_setup_entry(
) )
class BSBLANClimate(BSBLANEntity, CoordinatorEntity, ClimateEntity): class BSBLANClimate(
BSBLANEntity, CoordinatorEntity[DataUpdateCoordinator[State]], ClimateEntity
):
"""Defines a BSBLAN climate device.""" """Defines a BSBLAN climate device."""
coordinator: DataUpdateCoordinator[State]
_attr_has_entity_name = True _attr_has_entity_name = True
# Determine preset modes # Determine preset modes
_attr_supported_features = ( _attr_supported_features = (
@ -80,7 +81,7 @@ class BSBLANClimate(BSBLANEntity, CoordinatorEntity, ClimateEntity):
def __init__( def __init__(
self, self,
coordinator: DataUpdateCoordinator, coordinator: DataUpdateCoordinator[State],
client: BSBLAN, client: BSBLAN,
device: Device, device: Device,
info: Info, info: Info,
@ -89,7 +90,7 @@ class BSBLANClimate(BSBLANEntity, CoordinatorEntity, ClimateEntity):
) -> None: ) -> None:
"""Initialize BSBLAN climate device.""" """Initialize BSBLAN climate device."""
super().__init__(client, device, info, static, entry) super().__init__(client, device, info, static, entry)
CoordinatorEntity.__init__(self, coordinator) super(CoordinatorEntity, self).__init__(coordinator)
self._attr_unique_id = f"{format_mac(device.MAC)}-climate" self._attr_unique_id = f"{format_mac(device.MAC)}-climate"
self._attr_min_temp = float(static.min_temp.value) self._attr_min_temp = float(static.min_temp.value)

View file

@ -76,7 +76,7 @@ class ElgatoLight(
) -> None: ) -> None:
"""Initialize Elgato Light.""" """Initialize Elgato Light."""
super().__init__(client, info, mac) super().__init__(client, info, mac)
CoordinatorEntity.__init__(self, coordinator) super(CoordinatorEntity, self).__init__(coordinator)
self._attr_min_mireds = 143 self._attr_min_mireds = 143
self._attr_max_mireds = 344 self._attr_max_mireds = 344

View file

@ -1,7 +1,7 @@
"""MicroBot class.""" """MicroBot class."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import Any
from homeassistant.components.bluetooth.passive_update_coordinator import ( from homeassistant.components.bluetooth.passive_update_coordinator import (
PassiveBluetoothCoordinatorEntity, PassiveBluetoothCoordinatorEntity,
@ -10,16 +10,12 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from .const import MANUFACTURER from .const import MANUFACTURER
from .coordinator import MicroBotDataUpdateCoordinator
if TYPE_CHECKING:
from . import MicroBotDataUpdateCoordinator
class MicroBotEntity(PassiveBluetoothCoordinatorEntity): class MicroBotEntity(PassiveBluetoothCoordinatorEntity[MicroBotDataUpdateCoordinator]):
"""Generic entity for all MicroBots.""" """Generic entity for all MicroBots."""
coordinator: MicroBotDataUpdateCoordinator
def __init__(self, coordinator, config_entry): def __init__(self, coordinator, config_entry):
"""Initialise the entity.""" """Initialise the entity."""
super().__init__(coordinator) super().__init__(coordinator)

View file

@ -1,7 +1,7 @@
"""Switch platform for MicroBot.""" """Switch platform for MicroBot."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import Any
import voluptuous as vol import voluptuous as vol
@ -11,11 +11,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from .const import DOMAIN from .const import DOMAIN
from .coordinator import MicroBotDataUpdateCoordinator
from .entity import MicroBotEntity from .entity import MicroBotEntity
if TYPE_CHECKING:
from . import MicroBotDataUpdateCoordinator
CALIBRATE = "calibrate" CALIBRATE = "calibrate"
CALIBRATE_SCHEMA = { CALIBRATE_SCHEMA = {
vol.Required("depth"): cv.positive_int, vol.Required("depth"): cv.positive_int,

View file

@ -203,7 +203,7 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor):
value_template: Template | None, value_template: Template | None,
) -> None: ) -> None:
"""Initialize a web scrape sensor.""" """Initialize a web scrape sensor."""
CoordinatorEntity.__init__(self, coordinator) super(CoordinatorEntity, self).__init__(coordinator)
TemplateSensor.__init__( TemplateSensor.__init__(
self, self,
hass, hass,

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
import contextlib import contextlib
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING
import async_timeout import async_timeout
import switchbot import switchbot
@ -25,9 +25,7 @@ _LOGGER = logging.getLogger(__name__)
DEVICE_STARTUP_TIMEOUT = 30 DEVICE_STARTUP_TIMEOUT = 30
class SwitchbotDataUpdateCoordinator( class SwitchbotDataUpdateCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
ActiveBluetoothDataUpdateCoordinator[dict[str, Any]]
):
"""Class to manage fetching switchbot data.""" """Class to manage fetching switchbot data."""
def __init__( def __init__(
@ -79,9 +77,9 @@ class SwitchbotDataUpdateCoordinator(
async def _async_update( async def _async_update(
self, service_info: bluetooth.BluetoothServiceInfoBleak self, service_info: bluetooth.BluetoothServiceInfoBleak
) -> dict[str, Any]: ) -> None:
"""Poll the device.""" """Poll the device."""
return await self.device.update() await self.device.update()
@callback @callback
def _async_handle_unavailable( def _async_handle_unavailable(

View file

@ -21,10 +21,11 @@ from .coordinator import SwitchbotDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): class SwitchbotEntity(
PassiveBluetoothCoordinatorEntity[SwitchbotDataUpdateCoordinator]
):
"""Generic entity encapsulating common features of Switchbot device.""" """Generic entity encapsulating common features of Switchbot device."""
coordinator: SwitchbotDataUpdateCoordinator
_device: SwitchbotDevice _device: SwitchbotDevice
_attr_has_entity_name = True _attr_has_entity_name = True

View file

@ -1,13 +1,14 @@
"""Helpers to help coordinate updates.""" """Helpers to help coordinate updates."""
from __future__ import annotations from __future__ import annotations
from abc import abstractmethod
import asyncio import asyncio
from collections.abc import Awaitable, Callable, Coroutine, Generator from collections.abc import Awaitable, Callable, Coroutine, Generator
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from random import randint from random import randint
from time import monotonic from time import monotonic
from typing import Any, Generic, TypeVar from typing import Any, Generic, Protocol, TypeVar
import urllib.error import urllib.error
import aiohttp import aiohttp
@ -29,6 +30,9 @@ REQUEST_REFRESH_DEFAULT_COOLDOWN = 10
REQUEST_REFRESH_DEFAULT_IMMEDIATE = True REQUEST_REFRESH_DEFAULT_IMMEDIATE = True
_T = TypeVar("_T") _T = TypeVar("_T")
_BaseDataUpdateCoordinatorT = TypeVar(
"_BaseDataUpdateCoordinatorT", bound="BaseDataUpdateCoordinatorProtocol"
)
_DataUpdateCoordinatorT = TypeVar( _DataUpdateCoordinatorT = TypeVar(
"_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]"
) )
@ -38,7 +42,17 @@ class UpdateFailed(Exception):
"""Raised when an update has failed.""" """Raised when an update has failed."""
class DataUpdateCoordinator(Generic[_T]): class BaseDataUpdateCoordinatorProtocol(Protocol):
"""Base protocol type for DataUpdateCoordinator."""
@callback
def async_add_listener(
self, update_callback: CALLBACK_TYPE, context: Any = None
) -> Callable[[], None]:
"""Listen for data updates."""
class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_T]):
"""Class to manage fetching data from single endpoint.""" """Class to manage fetching data from single endpoint."""
def __init__( def __init__(
@ -346,11 +360,11 @@ class DataUpdateCoordinator(Generic[_T]):
self.async_update_listeners() self.async_update_listeners()
class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): class BaseCoordinatorEntity(entity.Entity, Generic[_BaseDataUpdateCoordinatorT]):
"""A class for entities using DataUpdateCoordinator.""" """Base class for all Coordinator entities."""
def __init__( def __init__(
self, coordinator: _DataUpdateCoordinatorT, context: Any = None self, coordinator: _BaseDataUpdateCoordinatorT, context: Any = None
) -> None: ) -> None:
"""Create the entity with a DataUpdateCoordinator.""" """Create the entity with a DataUpdateCoordinator."""
self.coordinator = coordinator self.coordinator = coordinator
@ -361,11 +375,6 @@ class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]):
"""No need to poll. Coordinator notifies entity of updates.""" """No need to poll. Coordinator notifies entity of updates."""
return False return False
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""When entity is added to hass.""" """When entity is added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
@ -380,6 +389,22 @@ class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]):
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
self.async_write_ha_state() self.async_write_ha_state()
@abstractmethod
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
class CoordinatorEntity(BaseCoordinatorEntity[_DataUpdateCoordinatorT]):
"""A class for entities using DataUpdateCoordinator."""
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update the entity. """Update the entity.