custom_components/govee_bluetooth/coordinator.py
"""Polling coordinator and availability tracking for a Govee BLE light."""
from __future__ import annotations
import asyncio
import logging
from datetime import timedelta
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .device import GoveeBleDevice, GoveeConnectionError, GoveeState
_LOGGER = logging.getLogger(__name__)
class GoveeCoordinator(DataUpdateCoordinator[GoveeState]):
"""Poll the light periodically and relay pushed state changes."""
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
device: GoveeBleDevice,
poll_interval: int,
) -> None:
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=f"{DOMAIN} {device.name}",
update_interval=timedelta(seconds=poll_interval),
)
self.device = device
self.initial_refresh: asyncio.Task[None] | None = None
# Snapshot used by the entry update listener to tell an options change
# from a data update the device made itself.
self.options_at_setup: dict = {}
self.ble_seen = bluetooth.async_address_present(hass, device.address, connectable=True)
self._unsubscribers = [
device.async_add_listener(self._handle_device_state),
bluetooth.async_register_callback(
hass,
self._handle_advertisement,
{"address": device.address, "connectable": True},
bluetooth.BluetoothScanningMode.PASSIVE,
),
bluetooth.async_track_unavailable(
hass, self._handle_unavailable, device.address, connectable=True
),
]
@property
def available(self) -> bool:
"""True when some adapter or proxy can currently reach the light."""
return self.ble_seen
@callback
def _handle_device_state(self, state: GoveeState) -> None:
self.async_set_updated_data(state)
@callback
def _handle_advertisement(
self, service_info: bluetooth.BluetoothServiceInfoBleak, change: bluetooth.BluetoothChange
) -> None:
if not self.ble_seen:
_LOGGER.debug("%s is back in Bluetooth range", self.device.name)
self.ble_seen = True
self.async_update_listeners()
@callback
def _handle_unavailable(self, service_info: bluetooth.BluetoothServiceInfoBleak) -> None:
_LOGGER.debug("%s is no longer seen by any connectable adapter", self.device.name)
self.ble_seen = False
self.async_update_listeners()
async def _async_update_data(self) -> GoveeState:
try:
return await self.device.async_update()
except GoveeConnectionError as err:
raise UpdateFailed(str(err)) from err
async def async_shutdown(self) -> None:
for unsubscribe in self._unsubscribers:
unsubscribe()
self._unsubscribers.clear()
await self.device.async_disconnect()
await super().async_shutdown()