diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 457eac60407..2e1a502ca69 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.5.1"], + "requirements": ["xiaomi-ble==0.6.1"], "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 f33c864fa4a..a96722620a9 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -30,7 +30,9 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONDUCTIVITY, + ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, PRESSURE_MBAR, @@ -74,6 +76,12 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( + key=str(Units.ELECTRIC_POTENTIAL_VOLT), + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), ( DeviceClass.SIGNAL_STRENGTH, Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -98,6 +106,12 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=CONDUCTIVITY, state_class=SensorStateClass.MEASUREMENT, ), + # Used for e.g. formaldehyde + (None, Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER): SensorEntityDescription( + key=str(Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER), + native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), } diff --git a/requirements_all.txt b/requirements_all.txt index f1e14dc0aeb..7240c52de1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2470,7 +2470,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.5.1 +xiaomi-ble==0.6.1 # homeassistant.components.knx xknx==0.22.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d52446c4a26..0bf0328b77e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.5.1 +xiaomi-ble==0.6.1 # homeassistant.components.knx xknx==0.22.0 diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index b95ea37311e..dca00e92254 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -50,6 +50,154 @@ async def test_sensors(hass): await hass.async_block_till_done() +async def test_xiaomi_formaldeyhde(hass): + """Make sure that formldehyde sensors are correctly mapped.""" + 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.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 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1010, payload len is 0x2 and payload is 0xf400 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x10\x10\x02\xf4\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + sensor = hass.states.get("sensor.test_device_formaldehyde") + sensor_attr = sensor.attributes + assert sensor.state == "2.44" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Formaldehyde" + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "mg/m³" + assert sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_consumable(hass): + """Make sure that consumable sensors are correctly mapped.""" + 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.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 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x13\x10\x02\x60\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + sensor = hass.states.get("sensor.test_device_consumable") + sensor_attr = sensor.attributes + assert sensor.state == "96" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Consumable" + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_battery_voltage(hass): + """Make sure that battery voltage sensors are correctly mapped.""" + 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.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 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x0a10, payload len is 0x2 and payload is 0x6400 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x0a\x10\x02\x64\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + volt_sensor = hass.states.get("sensor.test_device_voltage") + volt_sensor_attr = volt_sensor.attributes + assert volt_sensor.state == "3.1" + assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Voltage" + assert volt_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "V" + assert volt_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + bat_sensor = hass.states.get("sensor.test_device_battery") + bat_sensor_attr = bat_sensor.attributes + assert bat_sensor.state == "100" + assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Battery" + assert bat_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert bat_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + 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(