diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index cf5ef00777b..17e22accd6d 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.1.0"], + "requirements": ["xiaomi-ble==0.2.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 c380490dc8c..f485ccb8eb6 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -22,6 +22,8 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + CONDUCTIVITY, + LIGHT_LUX, PERCENTAGE, PRESSURE_MBAR, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -46,6 +48,12 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + (DeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( + key=f"{DeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", device_class=SensorDeviceClass.PRESSURE, @@ -68,6 +76,20 @@ SENSOR_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + # Used for e.g. moisture sensor on HHCCJCY01 + (None, Units.PERCENTAGE): SensorEntityDescription( + key=str(Units.PERCENTAGE), + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for e.g. conductivity sensor on HHCCJCY01 + (None, Units.CONDUCTIVITY): SensorEntityDescription( + key=str(Units.CONDUCTIVITY), + device_class=None, + native_unit_of_measurement=CONDUCTIVITY, + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -106,7 +128,7 @@ def sensor_update_to_bluetooth_data_update( (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() - if description.device_class and description.native_unit_of_measurement + if description.native_unit_of_measurement }, entity_data={ _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value diff --git a/requirements_all.txt b/requirements_all.txt index 293eff63383..80ca6871b48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2471,7 +2471,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.1.0 +xiaomi-ble==0.2.0 # homeassistant.components.knx xknx==0.21.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4fb04b7cda..6d0e6f54715 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1656,7 +1656,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.1.0 +xiaomi-ble==0.2.0 # homeassistant.components.knx xknx==0.21.5 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index 6022b15bf51..80ec2f19989 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -36,3 +36,18 @@ MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", ) + + +def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: + """Make a dummy advertisement.""" + return BluetoothServiceInfo( + name="Test Device", + address=address, + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": payload, + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", + ) diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 044b2ea3e87..44b29ff1051 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT -from . import MMC_T201_1_SERVICE_INFO +from . import MMC_T201_1_SERVICE_INFO, make_advertisement from tests.common import MockConfigEntry @@ -48,3 +48,85 @@ async def test_sensors(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_xiaomi_HHCCJCY01(hass): + """This device has multiple advertisements before all sensors are visible. Test that this works.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_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( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00fz>j\x8d|\xc4\r\x07\x10\x03\x00\x00\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00hz>j\x8d|\xc4\r\t\x10\x02W\x02" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00Gz>j\x8d|\xc4\r\x08\x10\x01@" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x04\x10\x02\xf4\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + illum_sensor = hass.states.get("sensor.test_device_illuminance") + illum_sensor_attr = illum_sensor.attributes + assert illum_sensor.state == "0" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Illuminance" + assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + cond_sensor = hass.states.get("sensor.test_device_conductivity") + cond_sensor_attribtes = cond_sensor.attributes + assert cond_sensor.state == "599" + assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Conductivity" + assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + moist_sensor = hass.states.get("sensor.test_device_moisture") + moist_sensor_attribtes = moist_sensor.attributes + assert moist_sensor.state == "64" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Moisture" + assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "24.4" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done()