diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 031490d6d68..626b7325014 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py new file mode 100644 index 00000000000..448b1f176e5 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -0,0 +1,99 @@ +"""Support for Xiaomi binary sensors.""" +from __future__ import annotations + +from typing import Optional + +from xiaomi_ble.parser import ( + BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass, + 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 = { + XiaomiBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.MOTION, + device_class=BinarySensorDeviceClass.MOTION, + ), + XiaomiBinarySensorDeviceClass.LIGHT: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.LIGHT, + device_class=BinarySensorDeviceClass.LIGHT, + ), + XiaomiBinarySensorDeviceClass.SMOKE: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a 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_class + ] + for device_key, description in sensor_update.binary_entity_descriptions.items() + if description.device_class + }, + 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 Xiaomi 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( + XiaomiBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class XiaomiBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + BinarySensorEntity, +): + """Representation of a Xiaomi binary sensor.""" + + @property + def is_on(self) -> bool | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/xiaomi_ble/device.py b/homeassistant/components/xiaomi_ble/device.py new file mode 100644 index 00000000000..4ddfc31ae51 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/device.py @@ -0,0 +1,31 @@ +"""Support for Xioami BLE devices.""" +from __future__ import annotations + +from xiaomi_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 8c1d47ee423..e93dace95c6 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.8.2"], + "requirements": ["xiaomi-ble==0.9.0"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index d22ed46dd83..1aa1b7f8f72 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -3,13 +3,12 @@ from __future__ import annotations from typing import Optional, Union -from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from xiaomi_ble import DeviceClass, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothEntityKey, PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) @@ -20,9 +19,6 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONDUCTIVITY, ELECTRIC_POTENTIAL_VOLT, @@ -33,10 +29,10 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo 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 SENSOR_DESCRIPTIONS = { (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( @@ -108,49 +104,28 @@ SENSOR_DESCRIPTIONS = { } -def _device_key_to_bluetooth_entity_key( - device_key: DeviceKey, -) -> PassiveBluetoothEntityKey: - """Convert a device key to an entity key.""" - return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def _sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: _sensor_device_info_to_hass(device_info) + 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): SENSOR_DESCRIPTIONS[ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() if description.native_unit_of_measurement }, entity_data={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value for device_key, sensor_values in sensor_update.entity_values.items() }, entity_names={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + device_key_to_bluetooth_entity_key(device_key): sensor_values.name for device_key, sensor_values in sensor_update.entity_values.items() }, ) diff --git a/requirements_all.txt b/requirements_all.txt index b4d5fe6374a..a356cec0af6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.8.2 +xiaomi-ble==0.9.0 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8eb0066b32c..7cbef52b7ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.8.2 +xiaomi-ble==0.9.0 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py new file mode 100644 index 00000000000..03e3d52d783 --- /dev/null +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -0,0 +1,54 @@ +"""Test Xiaomi binary sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME + +from . import make_advertisement + +from tests.common import MockConfigEntry + + +async def test_smoke_sensor(hass): + """Test setting up a smoke sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + data={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + 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( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + smoke_sensor = hass.states.get("binary_sensor.thermometer_e39cbc_smoke") + smoke_sensor_attribtes = smoke_sensor.attributes + assert smoke_sensor.state == "on" + assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer E39CBC Smoke" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done()