mirror of
https://github.com/home-assistant/developers.home-assistant.git
synced 2025-07-18 23:06:31 +00:00
Add docs on fetching Bluetooth data (#1635)
* Add docs on fetching Bluetooth data * Add docs on fetching Bluetooth data * Add docs on fetching Bluetooth data * Add docs on fetching Bluetooth data * Add docs on fetching Bluetooth data * Add docs on fetching Bluetooth data * Add docs on fetching Bluetooth data * Add docs on fetching Bluetooth data * Add docs on fetching Bluetooth data * Add docs on fetching Bluetooth data * Add docs on fetching Bluetooth data * Update docs/bluetooth_fetching_data.md * Update docs/bluetooth_fetching_data.md * Update docs/bluetooth_fetching_data.md Co-authored-by: 930913 <3722064+930913@users.noreply.github.com> * Update docs/bluetooth_fetching_data.md Co-authored-by: 930913 <3722064+930913@users.noreply.github.com> * Update docs/bluetooth_fetching_data.md * Update docs/bluetooth_fetching_data.md Co-authored-by: 930913 <3722064+930913@users.noreply.github.com> * Update docs/bluetooth_fetching_data.md Co-authored-by: 930913 <3722064+930913@users.noreply.github.com> * Update docs/bluetooth_fetching_data.md * Adjust sidebar * Fix link Co-authored-by: 930913 <3722064+930913@users.noreply.github.com> Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
6f43b97d14
commit
4d368a1b0f
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Bluetooth"
|
||||
sidebar_label: "Bluetooth"
|
||||
sidebar_label: "Best practices"
|
||||
---
|
||||
|
||||
### Best practices for integration authors
|
||||
|
398
docs/core/bluetooth/bluetooth_fetching_data.md
Normal file
398
docs/core/bluetooth/bluetooth_fetching_data.md
Normal file
@ -0,0 +1,398 @@
|
||||
---
|
||||
title: "Fetching Bluetooth Data"
|
||||
---
|
||||
|
||||
## Choosing a method to fetch data
|
||||
|
||||
If the device's primary method to notify of updates is Bluetooth advertisements and its primary function is a sensor, binary sensor, or firing events:
|
||||
|
||||
- If all sensors are updated via Bluetooth advertisements: [`PassiveBluetoothProcessorCoordinator`](#passivebluetoothprocessorcoordinator)
|
||||
- If active connection are needed for some sensors: [`ActiveBluetoothProcessorCoordinator`](#activebluetoothprocessorcoordinator)
|
||||
|
||||
If the device's primary method to notify of updates is Bluetooth advertisements and its primary function is **not** a sensor, binary sensor, or firing events:
|
||||
|
||||
- If all entities are updated via Bluetooth advertisements: [`PassiveBluetoothCoordinator`](#passivebluetoothcoordinator)
|
||||
- If active connections are needed: [`ActiveBluetoothCoordinator`](#activebluetoothcoordinator)
|
||||
|
||||
If your device only communicates with an active Bluetooth connection and does not use Bluetooth advertisements:
|
||||
|
||||
- [`DataUpdateCoordinator`](../../integration_fetching_data)
|
||||
|
||||
## BluetoothProcessorCoordinator
|
||||
|
||||
The `ActiveBluetoothProcessorCoordinator` and `PassiveBluetoothProcessorCoordinator` significantly reduce the code needed for creating integrations that primary function as sensor, binary sensors, or fire events. By formatting the data fed into the processor coordinators into a `PassiveBluetoothDataUpdate` object, the
|
||||
frameworks can take care of creating the entities on demand and allow for minimal `sensor` and `binary_sensor` platform implementations.
|
||||
|
||||
These frameworks require the data coming from the library to be formatted into a `PassiveBluetoothDataUpdate` as shown below:
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class PassiveBluetoothEntityKey:
|
||||
"""Key for a passive bluetooth entity.
|
||||
|
||||
Example:
|
||||
key: temperature
|
||||
device_id: outdoor_sensor_1
|
||||
"""
|
||||
|
||||
key: str
|
||||
device_id: str | None
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class PassiveBluetoothDataUpdate(Generic[_T]):
|
||||
"""Generic bluetooth data."""
|
||||
|
||||
devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict)
|
||||
entity_descriptions: Mapping[
|
||||
PassiveBluetoothEntityKey, EntityDescription
|
||||
] = dataclasses.field(default_factory=dict)
|
||||
entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field(
|
||||
default_factory=dict
|
||||
)
|
||||
entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field(
|
||||
default_factory=dict
|
||||
)
|
||||
```
|
||||
|
||||
### PassiveBluetoothProcessorCoordinator
|
||||
|
||||
Example `async_setup_entry` for an integration `__init__.py` using a `PassiveBluetoothProcessorCoordinator`:
|
||||
|
||||
```python
|
||||
import logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from homeassistant.const import Platform
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
from your_library import DataParser
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up example BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
data = DataParser()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.ACTIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
# only start after all platforms have had a chance to subscribe
|
||||
coordinator.async_start()
|
||||
)
|
||||
return True
|
||||
```
|
||||
|
||||
Example `sensor.py`:
|
||||
|
||||
```python
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothEntityKey,
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def sensor_update_to_bluetooth_data_update(parsed_data):
|
||||
"""Convert a sensor update to a Bluetooth data update."""
|
||||
# This function must convert the parsed_data
|
||||
# from your library's update_method to a `PassiveBluetoothDataUpdate`
|
||||
# See the structure above
|
||||
return PassiveBluetoothDataUpdate(
|
||||
devices={},
|
||||
entity_descriptions={},
|
||||
entity_data={},
|
||||
entity_names={},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the example BLE sensors."""
|
||||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
ExampleBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
|
||||
|
||||
class ExampleBluetoothSensorEntity(PassiveBluetoothProcessorEntity, SensorEntity):
|
||||
"""Representation of an example BLE sensor."""
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | int | str | None:
|
||||
"""Return the native value."""
|
||||
return self.processor.entity_data.get(self.entity_key)
|
||||
|
||||
```
|
||||
|
||||
### ActiveBluetoothProcessorCoordinator
|
||||
|
||||
An `ActiveBluetoothProcessorCoordinator` functions nearly the same as a `PassiveBluetoothProcessorCoordinator`
|
||||
but will also make an active connection to poll for data based on `needs_poll_method` and a `poll_method`
|
||||
function which are called when the device's Bluetooth advertisement changes. The `sensor.py` implementation
|
||||
is the same as the `PassiveBluetoothProcessorCoordinator`.
|
||||
|
||||
Example `async_setup_entry` for an integration `__init__.py` using an `ActiveBluetoothProcessorCoordinator`:
|
||||
|
||||
```python
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import CoreState, HomeAssistant
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfoBleak,
|
||||
async_ble_device_from_address,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
|
||||
from homeassistant.components.bluetooth.active_update_processor import (
|
||||
ActiveBluetoothProcessorCoordinator,
|
||||
)
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
from your_library import DataParser
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up example BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
data = DataParser()
|
||||
|
||||
def _needs_poll(
|
||||
service_info: BluetoothServiceInfoBleak, last_poll: float | None
|
||||
) -> bool:
|
||||
return (
|
||||
hass.state == CoreState.running
|
||||
and data.poll_needed(service_info, last_poll)
|
||||
and bool(
|
||||
async_ble_device_from_address(
|
||||
hass, service_info.device.address, connectable=True
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async def _async_poll(service_info: BluetoothServiceInfoBleak):
|
||||
if service_info.connectable:
|
||||
connectable_device = service_info.device
|
||||
elif device := async_ble_device_from_address(
|
||||
hass, service_info.device.address, True
|
||||
):
|
||||
connectable_device = device
|
||||
else:
|
||||
# We have no Bluetooth controller that is in range of
|
||||
# the device to poll it
|
||||
raise RuntimeError(
|
||||
f"No connectable device found for {service_info.device.address}"
|
||||
)
|
||||
return await data.async_poll(connectable_device)
|
||||
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = ActiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
needs_poll_method=_needs_poll,
|
||||
poll_method=_async_poll,
|
||||
# We will take advertisements from non-connectable devices
|
||||
# since we will trade the BLEDevice for a connectable one
|
||||
# if we need to poll it
|
||||
connectable=False,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
# only start after all platforms have had a chance to subscribe
|
||||
coordinator.async_start()
|
||||
)
|
||||
return True
|
||||
```
|
||||
|
||||
## BluetoothCoordinator
|
||||
|
||||
The `ActiveBluetoothCoordinator` and `PassiveBluetoothCoordinator` coordinators function similar
|
||||
to `DataUpdateCoordinators` except they are driven by incoming advertisement data instead of polling.
|
||||
|
||||
### PassiveBluetoothCoordinator
|
||||
|
||||
Below is an example of a `PassiveBluetoothDataUpdateCoordinator`. Incoming
|
||||
data is received via `_async_handle_bluetooth_event` and processed by the integration's
|
||||
library.
|
||||
|
||||
```python
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.active_update_coordinator import (
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.core import CoreState, HomeAssistant, callback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bleak.backends.device import BLEDevice
|
||||
|
||||
|
||||
class ExamplePassiveBluetoothDataUpdateCoordinator(
|
||||
PassiveBluetoothDataUpdateCoordinator[None]
|
||||
):
|
||||
"""Class to manage fetching example data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: logging.Logger,
|
||||
ble_device: BLEDevice,
|
||||
device: YourLibDevice,
|
||||
) -> None:
|
||||
"""Initialize example data coordinator."""
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=logger,
|
||||
address=ble_device.address,
|
||||
mode=bluetooth.BluetoothScanningMode.ACTIVE,
|
||||
connectable=False,
|
||||
)
|
||||
self.device = device
|
||||
|
||||
@callback
|
||||
def _async_handle_unavailable(
|
||||
self, service_info: bluetooth.BluetoothServiceInfoBleak
|
||||
) -> None:
|
||||
"""Handle the device going unavailable."""
|
||||
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||
change: bluetooth.BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
# Your device should process incoming advertisement data
|
||||
|
||||
```
|
||||
|
||||
### ActiveBluetoothCoordinator
|
||||
|
||||
Below is an example of an `ActiveBluetoothDataUpdateCoordinator`. Incoming
|
||||
data is received via `_async_handle_bluetooth_event` and processed by the integration's
|
||||
library.
|
||||
|
||||
The `_needs_poll` function is called each time there is the Bluetooth advertisement changes to
|
||||
determine if an active connection to the device is needed to fetch additional data.
|
||||
|
||||
If the `_needs_poll` function returns `True`, `_async_update` will be called.
|
||||
|
||||
```python
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
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
|
||||
|
||||
|
||||
class ExampleActiveBluetoothDataUpdateCoordinator(
|
||||
ActiveBluetoothDataUpdateCoordinator[None]
|
||||
):
|
||||
"""Class to manage fetching example data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: logging.Logger,
|
||||
ble_device: BLEDevice,
|
||||
device: YourLibDevice,
|
||||
) -> None:
|
||||
"""Initialize example data coordinator."""
|
||||
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=True,
|
||||
)
|
||||
self.device = device
|
||||
|
||||
@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."""
|
||||
|
||||
@callback
|
||||
def _async_handle_unavailable(
|
||||
self, service_info: bluetooth.BluetoothServiceInfoBleak
|
||||
) -> None:
|
||||
"""Handle the device going unavailable."""
|
||||
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||
change: bluetooth.BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
# Your device should process incoming advertisement data
|
||||
|
||||
```
|
@ -111,7 +111,11 @@ module.exports = {
|
||||
"integration_events",
|
||||
"integration_listen_events",
|
||||
"network_discovery",
|
||||
"bluetooth",
|
||||
{
|
||||
type: "category",
|
||||
label: "Bluetooth",
|
||||
items: ["bluetooth", "core/bluetooth/bluetooth_fetching_data"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user