Add BTHome binary sensors (#78151)

This commit is contained in:
Ernst Klamer 2022-09-10 04:43:25 +02:00 committed by GitHub
parent a1ec9c6147
commit 39f40011cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 212 additions and 6 deletions

View File

@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant
from .const import DOMAIN from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,93 @@
"""Support for BTHome binary sensors."""
from __future__ import annotations
from typing import Optional
from bthome_ble import BTHOME_BINARY_SENSORS, SensorUpdate
from homeassistant import config_entries
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate,
PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass
BINARY_SENSOR_DESCRIPTIONS = {}
for key in BTHOME_BINARY_SENSORS:
# Not all BTHome device classes are available in Home Assistant
DEV_CLASS: str | None = key
try:
BinarySensorDeviceClass(key)
except ValueError:
DEV_CLASS = None
BINARY_SENSOR_DESCRIPTIONS[key] = BinarySensorEntityDescription(
key=key,
device_class=DEV_CLASS,
)
def sensor_update_to_bluetooth_data_update(
sensor_update: SensorUpdate,
) -> PassiveBluetoothDataUpdate:
"""Convert a binary sensor update to a bluetooth data update."""
return PassiveBluetoothDataUpdate(
devices={
device_id: sensor_device_info_to_hass(device_info)
for device_id, device_info in sensor_update.devices.items()
},
entity_descriptions={
device_key_to_bluetooth_entity_key(device_key): BINARY_SENSOR_DESCRIPTIONS[
description.device_key.key
]
for device_key, description in sensor_update.binary_entity_descriptions.items()
},
entity_data={
device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value
for device_key, sensor_values in sensor_update.binary_entity_values.items()
},
entity_names={
device_key_to_bluetooth_entity_key(device_key): sensor_values.name
for device_key, sensor_values in sensor_update.binary_entity_values.items()
},
)
async def async_setup_entry(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the BTHome BLE binary 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(
BTHomeBluetoothBinarySensorEntity, async_add_entities
)
)
entry.async_on_unload(coordinator.async_register_processor(processor))
class BTHomeBluetoothBinarySensorEntity(
PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]],
BinarySensorEntity,
):
"""Representation of a BTHome binary sensor."""
@property
def is_on(self) -> bool | None:
"""Return the native value."""
return self.processor.entity_data.get(self.entity_key)

View File

@ -13,7 +13,7 @@
"service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb"
} }
], ],
"requirements": ["bthome-ble==1.0.0"], "requirements": ["bthome-ble==1.2.0"],
"dependencies": ["bluetooth"], "dependencies": ["bluetooth"],
"codeowners": ["@Ernst79"], "codeowners": ["@Ernst79"],
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -148,9 +148,9 @@ SENSOR_DESCRIPTIONS = {
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
# Used for moisture sensor # Used for moisture sensor
(None, Units.PERCENTAGE,): SensorEntityDescription( (DeviceClass.MOISTURE, Units.PERCENTAGE): SensorEntityDescription(
key=f"{DeviceClass.MOISTURE}_{Units.PERCENTAGE}", key=f"{DeviceClass.MOISTURE}_{Units.PERCENTAGE}",
device_class=None, device_class=SensorDeviceClass.MOISTURE,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),

View File

@ -464,7 +464,7 @@ bsblan==0.5.0
bt_proximity==0.2.1 bt_proximity==0.2.1
# homeassistant.components.bthome # homeassistant.components.bthome
bthome-ble==1.0.0 bthome-ble==1.2.0
# homeassistant.components.bt_home_hub_5 # homeassistant.components.bt_home_hub_5
bthomehub5-devicelist==0.1.1 bthomehub5-devicelist==0.1.1

View File

@ -365,7 +365,7 @@ brunt==1.2.0
bsblan==0.5.0 bsblan==0.5.0
# homeassistant.components.bthome # homeassistant.components.bthome
bthome-ble==1.0.0 bthome-ble==1.2.0
# homeassistant.components.buienradar # homeassistant.components.buienradar
buienradar==1.0.5 buienradar==1.0.5

View File

@ -0,0 +1,113 @@
"""Test BTHome binary sensors."""
import logging
from unittest.mock import patch
import pytest
from homeassistant.components.bluetooth import BluetoothChange
from homeassistant.components.bthome.const import DOMAIN
from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON
from . import make_advertisement
from tests.common import MockConfigEntry
_LOGGER = logging.getLogger(__name__)
@pytest.mark.parametrize(
"mac_address, advertisement, bind_key, result",
[
(
"A4:C1:38:8D:18:B2",
make_advertisement(
"A4:C1:38:8D:18:B2",
b"\x02\x10\x01",
),
None,
[
{
"binary_sensor_entity": "binary_sensor.test_device_18b2_power",
"friendly_name": "Test Device 18B2 Power",
"expected_state": STATE_ON,
},
],
),
(
"A4:C1:38:8D:18:B2",
make_advertisement(
"A4:C1:38:8D:18:B2",
b"\x02\x11\x00",
),
None,
[
{
"binary_sensor_entity": "binary_sensor.test_device_18b2_opening",
"friendly_name": "Test Device 18B2 Opening",
"expected_state": STATE_OFF,
},
],
),
(
"A4:C1:38:8D:18:B2",
make_advertisement(
"A4:C1:38:8D:18:B2",
b"\x02\x0F\x01",
),
None,
[
{
"binary_sensor_entity": "binary_sensor.test_device_18b2_generic",
"friendly_name": "Test Device 18B2 Generic",
"expected_state": STATE_ON,
},
],
),
],
)
async def test_binary_sensors(
hass,
mac_address,
advertisement,
bind_key,
result,
):
"""Test the different binary sensors."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=mac_address,
data={"bindkey": bind_key},
)
entry.add_to_hass(hass)
saved_callback = None
def _async_register_callback(_hass, _callback, _matcher, _mode):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
with patch(
"homeassistant.components.bluetooth.update_coordinator.async_register_callback",
_async_register_callback,
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
saved_callback(
advertisement,
BluetoothChange.ADVERTISEMENT,
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == len(result)
for meas in result:
binary_sensor = hass.states.get(meas["binary_sensor_entity"])
binary_sensor_attr = binary_sensor.attributes
assert binary_sensor.state == meas["expected_state"]
assert binary_sensor_attr[ATTR_FRIENDLY_NAME] == meas["friendly_name"]
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()