mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add GATT polling support to INKBird (#142307)
* Add GATT polling support to INKBird * reduce * fixes * coverage * dry * reduce * reduce
This commit is contained in:
parent
8d82ef8e36
commit
04dfa45db0
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from inkbird_ble import INKBIRDBluetoothDeviceData, SensorUpdate
|
from inkbird_ble import INKBIRDBluetoothDeviceData, SensorUpdate
|
||||||
@ -9,13 +10,16 @@ from inkbird_ble import INKBIRDBluetoothDeviceData, SensorUpdate
|
|||||||
from homeassistant.components.bluetooth import (
|
from homeassistant.components.bluetooth import (
|
||||||
BluetoothScanningMode,
|
BluetoothScanningMode,
|
||||||
BluetoothServiceInfo,
|
BluetoothServiceInfo,
|
||||||
|
BluetoothServiceInfoBleak,
|
||||||
|
async_ble_device_from_address,
|
||||||
)
|
)
|
||||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
from homeassistant.components.bluetooth.active_update_processor import (
|
||||||
PassiveBluetoothProcessorCoordinator,
|
ActiveBluetoothProcessorCoordinator,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
from .const import CONF_DEVICE_TYPE, DOMAIN
|
from .const import CONF_DEVICE_TYPE, DOMAIN
|
||||||
|
|
||||||
@ -23,34 +27,87 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
FALLBACK_POLL_INTERVAL = timedelta(seconds=180)
|
||||||
|
|
||||||
|
|
||||||
|
class INKBIRDActiveBluetoothProcessorCoordinator(ActiveBluetoothProcessorCoordinator):
|
||||||
|
"""Coordinator for INKBIRD Bluetooth devices."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
data: INKBIRDBluetoothDeviceData,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the INKBIRD Bluetooth processor coordinator."""
|
||||||
|
self._data = data
|
||||||
|
self._entry = entry
|
||||||
|
address = entry.unique_id
|
||||||
|
assert address is not None
|
||||||
|
entry.async_on_unload(
|
||||||
|
async_track_time_interval(
|
||||||
|
hass, self._async_schedule_poll, FALLBACK_POLL_INTERVAL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
hass=hass,
|
||||||
|
logger=_LOGGER,
|
||||||
|
address=address,
|
||||||
|
mode=BluetoothScanningMode.ACTIVE,
|
||||||
|
update_method=self._async_on_update,
|
||||||
|
needs_poll_method=self._async_needs_poll,
|
||||||
|
poll_method=self._async_poll_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_poll_data(
|
||||||
|
self, last_service_info: BluetoothServiceInfoBleak
|
||||||
|
) -> SensorUpdate:
|
||||||
|
"""Poll the device."""
|
||||||
|
return await self._data.async_poll(last_service_info.device)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_needs_poll(
|
||||||
|
self, service_info: BluetoothServiceInfoBleak, last_poll: float | None
|
||||||
|
) -> bool:
|
||||||
|
return (
|
||||||
|
not self.hass.is_stopping
|
||||||
|
and self._data.poll_needed(service_info, last_poll)
|
||||||
|
and bool(
|
||||||
|
async_ble_device_from_address(
|
||||||
|
self.hass, service_info.device.address, connectable=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_on_update(self, service_info: BluetoothServiceInfo) -> SensorUpdate:
|
||||||
|
"""Handle update callback from the passive BLE processor."""
|
||||||
|
update = self._data.update(service_info)
|
||||||
|
if (
|
||||||
|
self._entry.data.get(CONF_DEVICE_TYPE) is None
|
||||||
|
and self._data.device_type is not None
|
||||||
|
):
|
||||||
|
device_type_str = str(self._data.device_type)
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
self._entry,
|
||||||
|
data={**self._entry.data, CONF_DEVICE_TYPE: device_type_str},
|
||||||
|
)
|
||||||
|
return update
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_schedule_poll(self, _: datetime) -> None:
|
||||||
|
"""Schedule a poll of the device."""
|
||||||
|
if self._last_service_info and self._async_needs_poll(
|
||||||
|
self._last_service_info, self._last_poll
|
||||||
|
):
|
||||||
|
self._debounced_poll.async_schedule_call()
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up INKBIRD BLE device from a config entry."""
|
"""Set up INKBIRD BLE device from a config entry."""
|
||||||
address = entry.unique_id
|
|
||||||
assert address is not None
|
|
||||||
device_type: str | None = entry.data.get(CONF_DEVICE_TYPE)
|
device_type: str | None = entry.data.get(CONF_DEVICE_TYPE)
|
||||||
data = INKBIRDBluetoothDeviceData(device_type)
|
data = INKBIRDBluetoothDeviceData(device_type)
|
||||||
|
coordinator = INKBIRDActiveBluetoothProcessorCoordinator(hass, entry, data)
|
||||||
@callback
|
|
||||||
def _async_on_update(service_info: BluetoothServiceInfo) -> SensorUpdate:
|
|
||||||
"""Handle update callback from the passive BLE processor."""
|
|
||||||
nonlocal device_type
|
|
||||||
update = data.update(service_info)
|
|
||||||
if device_type is None and data.device_type is not None:
|
|
||||||
device_type_str = str(data.device_type)
|
|
||||||
hass.config_entries.async_update_entry(
|
|
||||||
entry, data={**entry.data, CONF_DEVICE_TYPE: device_type_str}
|
|
||||||
)
|
|
||||||
device_type = device_type_str
|
|
||||||
return update
|
|
||||||
|
|
||||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
address=address,
|
|
||||||
mode=BluetoothScanningMode.ACTIVE,
|
|
||||||
update_method=_async_on_update,
|
|
||||||
)
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
# only start after all platforms have had a chance to subscribe
|
# only start after all platforms have had a chance to subscribe
|
||||||
|
@ -22,6 +22,18 @@ SPS_SERVICE_INFO = BluetoothServiceInfo(
|
|||||||
source="local",
|
source="local",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SPS_PASSIVE_SERVICE_INFO = BluetoothServiceInfo(
|
||||||
|
name="sps",
|
||||||
|
address="AA:BB:CC:DD:EE:FF",
|
||||||
|
rssi=-63,
|
||||||
|
service_data={},
|
||||||
|
manufacturer_data={},
|
||||||
|
service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"],
|
||||||
|
source="local",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
SPS_WITH_CORRUPT_NAME_SERVICE_INFO = BluetoothServiceInfo(
|
SPS_WITH_CORRUPT_NAME_SERVICE_INFO = BluetoothServiceInfo(
|
||||||
name="XXXXcorruptXXXX",
|
name="XXXXcorruptXXXX",
|
||||||
address="AA:BB:CC:DD:EE:FF",
|
address="AA:BB:CC:DD:EE:FF",
|
||||||
|
@ -1,16 +1,63 @@
|
|||||||
"""Test the INKBIRD config flow."""
|
"""Test the INKBIRD config flow."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from inkbird_ble import (
|
||||||
|
DeviceKey,
|
||||||
|
SensorDescription,
|
||||||
|
SensorDeviceInfo,
|
||||||
|
SensorUpdate,
|
||||||
|
SensorValue,
|
||||||
|
Units,
|
||||||
|
)
|
||||||
|
from sensor_state_data import SensorDeviceClass
|
||||||
|
|
||||||
|
from homeassistant.components.inkbird import FALLBACK_POLL_INTERVAL
|
||||||
from homeassistant.components.inkbird.const import CONF_DEVICE_TYPE, DOMAIN
|
from homeassistant.components.inkbird.const import CONF_DEVICE_TYPE, DOMAIN
|
||||||
from homeassistant.components.sensor import ATTR_STATE_CLASS
|
from homeassistant.components.sensor import ATTR_STATE_CLASS
|
||||||
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT
|
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import SPS_SERVICE_INFO, SPS_WITH_CORRUPT_NAME_SERVICE_INFO
|
from . import (
|
||||||
|
SPS_PASSIVE_SERVICE_INFO,
|
||||||
|
SPS_SERVICE_INFO,
|
||||||
|
SPS_WITH_CORRUPT_NAME_SERVICE_INFO,
|
||||||
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
from tests.components.bluetooth import inject_bluetooth_service_info
|
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||||
|
|
||||||
|
|
||||||
|
def _make_sensor_update(humidity: float) -> SensorUpdate:
|
||||||
|
return SensorUpdate(
|
||||||
|
title=None,
|
||||||
|
devices={
|
||||||
|
None: SensorDeviceInfo(
|
||||||
|
name="IBS-TH EEFF",
|
||||||
|
model="IBS-TH",
|
||||||
|
manufacturer="INKBIRD",
|
||||||
|
sw_version=None,
|
||||||
|
hw_version=None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
entity_descriptions={
|
||||||
|
DeviceKey(key="humidity", device_id=None): SensorDescription(
|
||||||
|
device_key=DeviceKey(key="humidity", device_id=None),
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
native_unit_of_measurement=Units.PERCENTAGE,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
entity_values={
|
||||||
|
DeviceKey(key="humidity", device_id=None): SensorValue(
|
||||||
|
device_key=DeviceKey(key="humidity", device_id=None),
|
||||||
|
name="Humidity",
|
||||||
|
native_value=humidity,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_sensors(hass: HomeAssistant) -> None:
|
async def test_sensors(hass: HomeAssistant) -> None:
|
||||||
"""Test setting up creates the sensors."""
|
"""Test setting up creates the sensors."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
@ -68,3 +115,50 @@ async def test_device_with_corrupt_name(hass: HomeAssistant) -> None:
|
|||||||
assert entry.data[CONF_DEVICE_TYPE] == "IBS-TH"
|
assert entry.data[CONF_DEVICE_TYPE] == "IBS-TH"
|
||||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_polling_sensor(hass: HomeAssistant) -> None:
|
||||||
|
"""Test setting up a device that needs polling."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id="AA:BB:CC:DD:EE:FF",
|
||||||
|
data={CONF_DEVICE_TYPE: "IBS-TH"},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.inkbird.INKBIRDBluetoothDeviceData.async_poll",
|
||||||
|
return_value=_make_sensor_update(10.24),
|
||||||
|
):
|
||||||
|
inject_bluetooth_service_info(hass, SPS_PASSIVE_SERVICE_INFO)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(hass.states.async_all()) == 1
|
||||||
|
|
||||||
|
temp_sensor = hass.states.get("sensor.ibs_th_eeff_humidity")
|
||||||
|
temp_sensor_attribtes = temp_sensor.attributes
|
||||||
|
assert temp_sensor.state == "10.24"
|
||||||
|
assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-TH EEFF Humidity"
|
||||||
|
assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%"
|
||||||
|
assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
|
||||||
|
assert entry.data[CONF_DEVICE_TYPE] == "IBS-TH"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.inkbird.INKBIRDBluetoothDeviceData.async_poll",
|
||||||
|
return_value=_make_sensor_update(20.24),
|
||||||
|
):
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + FALLBACK_POLL_INTERVAL)
|
||||||
|
inject_bluetooth_service_info(hass, SPS_PASSIVE_SERVICE_INFO)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
temp_sensor = hass.states.get("sensor.ibs_th_eeff_humidity")
|
||||||
|
temp_sensor_attribtes = temp_sensor.attributes
|
||||||
|
assert temp_sensor.state == "20.24"
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user