core/homeassistant/components/switchbot/coordinator.py

124 lines
3.9 KiB
Python

"""Provides the switchbot DataUpdateCoordinator."""
from __future__ import annotations
import asyncio
import contextlib
import logging
from typing import TYPE_CHECKING
import async_timeout
import switchbot
from switchbot import SwitchbotModel
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth.active_update_coordinator import (
ActiveBluetoothDataUpdateCoordinator,
)
from homeassistant.core import CoreState, HomeAssistant, callback
if TYPE_CHECKING:
from bleak.backends.device import BLEDevice
_LOGGER = logging.getLogger(__name__)
DEVICE_STARTUP_TIMEOUT = 30
class SwitchbotDataUpdateCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
"""Class to manage fetching switchbot data."""
def __init__(
self,
hass: HomeAssistant,
logger: logging.Logger,
ble_device: BLEDevice,
device: switchbot.SwitchbotDevice,
base_unique_id: str,
device_name: str,
connectable: bool,
model: SwitchbotModel,
) -> None:
"""Initialize global switchbot data updater."""
super().__init__(
hass=hass,
logger=logger,
address=ble_device.address,
needs_poll_method=self._needs_poll,
poll_method=self._async_update,
mode=bluetooth.BluetoothScanningMode.ACTIVE,
connectable=connectable,
)
self.ble_device = ble_device
self.device = device
self.device_name = device_name
self.base_unique_id = base_unique_id
self.model = model
self._ready_event = asyncio.Event()
self._was_unavailable = True
@callback
def _needs_poll(
self,
service_info: bluetooth.BluetoothServiceInfoBleak,
seconds_since_last_poll: float | None,
) -> bool:
# Only poll if hass is running, we need to poll,
# and we actually have a way to connect to the device
return (
self.hass.state == CoreState.running
and self.device.poll_needed(seconds_since_last_poll)
and bool(
bluetooth.async_ble_device_from_address(
self.hass, service_info.device.address, connectable=True
)
)
)
async def _async_update(
self, service_info: bluetooth.BluetoothServiceInfoBleak
) -> None:
"""Poll the device."""
await self.device.update()
@callback
def _async_handle_unavailable(
self, service_info: bluetooth.BluetoothServiceInfoBleak
) -> None:
"""Handle the device going unavailable."""
super()._async_handle_unavailable(service_info)
self._was_unavailable = True
@callback
def _async_handle_bluetooth_event(
self,
service_info: bluetooth.BluetoothServiceInfoBleak,
change: bluetooth.BluetoothChange,
) -> None:
"""Handle a Bluetooth event."""
self.ble_device = service_info.device
if not (
adv := switchbot.parse_advertisement_data(
service_info.device, service_info.advertisement, self.model
)
):
return
if "modelName" in adv.data:
self._ready_event.set()
_LOGGER.debug(
"%s: Switchbot data: %s", self.ble_device.address, self.device.data
)
if not self.device.advertisement_changed(adv) and not self._was_unavailable:
return
self._was_unavailable = False
self.device.update_from_advertisement(adv)
super()._async_handle_bluetooth_event(service_info, change)
async def async_wait_ready(self) -> bool:
"""Wait for the device to be ready."""
with contextlib.suppress(asyncio.TimeoutError):
async with async_timeout.timeout(DEVICE_STARTUP_TIMEOUT):
await self._ready_event.wait()
return True
return False