Improve bluetooth
generic typing (#84891)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
6b95fa5942
commit
972eb34ed9
10 changed files with 71 additions and 42 deletions
|
@ -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.
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue