diff --git a/homeassistant/components/aranet/sensor.py b/homeassistant/components/aranet/sensor.py index cbe3113439f0..c2d4ae2516be 100644 --- a/homeassistant/components/aranet/sensor.py +++ b/homeassistant/components/aranet/sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( ATTR_SW_VERSION, EntityCategory, UnitOfPressure, + UnitOfRadiationConcentration, UnitOfRatio, UnitOfTemperature, UnitOfTime, @@ -107,7 +108,8 @@ SENSOR_DESCRIPTIONS = { key="radon_concentration", translation_key="radon_concentration", name="Radon Concentration", - native_unit_of_measurement="Bq/m³", + device_class=SensorDeviceClass.RADON, + native_unit_of_measurement=UnitOfRadiationConcentration.BECQUEREL_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), "battery": AranetSensorEntityDescription( @@ -209,6 +211,17 @@ class Aranet4BluetoothSensorEntity( ): """Representation of an Aranet sensor.""" + def __init__( + self, + processor: PassiveBluetoothDataProcessor[ + float | int | None, Aranet4Advertisement + ], + entity_key: PassiveBluetoothEntityKey, + description: EntityDescription, + ) -> None: + """Initialize with current metadata instead of a cached description.""" + super().__init__(processor, entity_key, SENSOR_DESCRIPTIONS[entity_key.key]) + @property @override def available(self) -> bool: diff --git a/tests/components/aranet/__init__.py b/tests/components/aranet/__init__.py index 3b37c1d5b5ca..bbd5035ec7cb 100644 --- a/tests/components/aranet/__init__.py +++ b/tests/components/aranet/__init__.py @@ -105,3 +105,14 @@ VALID_ARANET_RADON_DATA_SERVICE_INFO = fake_service_info( ) }, ) + +VALID_ARANET_RADON1_DATA_SERVICE_INFO = fake_service_info( + "AranetRn1 12345", + "0000fce0-0000-1000-8000-00805f9b34fb", + { + 1794: ( + b"\x03!\x04\x06\x01\x00\x00\x00\x07\x00" + b"\x00\x00\xc9'\x00\x00\x00d\x01X\x02\xf6\x01\x08" + ) + }, +) diff --git a/tests/components/aranet/conftest.py b/tests/components/aranet/conftest.py index da5c3c81404d..53023a4d70ff 100644 --- a/tests/components/aranet/conftest.py +++ b/tests/components/aranet/conftest.py @@ -1,8 +1,18 @@ """Aranet session fixtures.""" +from typing import Any + import pytest +@pytest.fixture +def mock_bluetooth_storage( + hass_storage: dict[str, Any], request: pytest.FixtureRequest +) -> None: + """Load Bluetooth storage before setting up the integration.""" + hass_storage.update(getattr(request, "param", {})) + + @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth: None) -> None: +def mock_bluetooth(mock_bluetooth_storage: None, enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/aranet/test_sensor.py b/tests/components/aranet/test_sensor.py index 7a997fe6c7d3..6690a5d6c479 100644 --- a/tests/components/aranet/test_sensor.py +++ b/tests/components/aranet/test_sensor.py @@ -3,15 +3,33 @@ import pytest from homeassistant.components.aranet.const import DOMAIN -from homeassistant.components.sensor import ATTR_OPTIONS, ATTR_STATE_CLASS -from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.components.bluetooth.passive_update_processor import STORAGE_KEY +from homeassistant.components.sensor import ( + ATTR_OPTIONS, + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + UnitOfRadiationConcentration, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.unit_system import ( + METRIC_SYSTEM, + US_CUSTOMARY_SYSTEM, + UnitSystem, +) from . import ( DISABLED_INTEGRATIONS_SERVICE_INFO, VALID_ARANET2_DATA_SERVICE_INFO, VALID_ARANET_RADIATION_DATA_SERVICE_INFO, + VALID_ARANET_RADON1_DATA_SERVICE_INFO, VALID_ARANET_RADON_DATA_SERVICE_INFO, VALID_DATA_SERVICE_INFO, ) @@ -19,6 +37,9 @@ from . import ( from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info +RADON_ENTITY_ID = "sensor.aranetrn_12345_radon_concentration" +RADON_UNIQUE_ID = "aa:bb:cc:dd:ee:ff-radon_concentration-aa:bb:cc:dd:ee:ff" + @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors_aranet_radiation( @@ -263,7 +284,7 @@ async def test_sensors_aranetrn( assert batt_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" assert batt_sensor_attrs[ATTR_STATE_CLASS] == "measurement" - co2_sensor = hass.states.get("sensor.aranetrn_12345_radon_concentration") + co2_sensor = hass.states.get(RADON_ENTITY_ID) co2_sensor_attrs = co2_sensor.attributes assert co2_sensor.state == "7" assert co2_sensor_attrs[ATTR_FRIENDLY_NAME] == "AranetRn+ 12345 Radon Concentration" @@ -319,6 +340,170 @@ async def test_sensors_aranetrn( await hass.async_block_till_done() +@pytest.mark.parametrize( + ("service_info", "entity_id"), + [ + pytest.param( + VALID_ARANET_RADON_DATA_SERVICE_INFO, + RADON_ENTITY_ID, + id="aranetrn_plus", + ), + pytest.param( + VALID_ARANET_RADON1_DATA_SERVICE_INFO, + "sensor.aranetrn1_12345_radon_concentration", + id="aranetrn1", + ), + ], +) +@pytest.mark.parametrize( + ("unit_system", "expected_unit", "expected_value"), + [ + pytest.param( + METRIC_SYSTEM, + UnitOfRadiationConcentration.BECQUEREL_PER_CUBIC_METER, + 7, + id="metric", + ), + pytest.param( + US_CUSTOMARY_SYSTEM, + UnitOfRadiationConcentration.PICOCURIES_PER_LITER, + 7 / 37, + id="us_customary", + ), + ], +) +async def test_radon_device_class_and_units( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + service_info: BluetoothServiceInfoBleak, + entity_id: str, + unit_system: UnitSystem, + expected_unit: UnitOfRadiationConcentration, + expected_value: float, +) -> None: + """Test radon metadata and default units.""" + hass.config.units = unit_system + entry = MockConfigEntry(domain=DOMAIN, unique_id=service_info.address) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + inject_bluetooth_service_info(hass, service_info) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state is not None + assert float(state.state) == pytest.approx(expected_value) + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.RADON + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == expected_unit + assert ( + state.attributes[ATTR_FRIENDLY_NAME] + == f"{service_info.name} Radon Concentration" + ) + entity = entity_registry.async_get(entity_id) + assert entity is not None + assert entity.unique_id == RADON_UNIQUE_ID + assert entity.original_device_class == SensorDeviceClass.RADON + + +@pytest.mark.parametrize( + ("mock_bluetooth_storage", "restored_device_classes"), + [ + pytest.param({}, {}, id="without_cache"), + pytest.param( + { + STORAGE_KEY: { + "version": 1, + "minor_version": 1, + "key": STORAGE_KEY, + "data": { + "aranet_radon": { + "sensor": { + "devices": { + "aa:bb:cc:dd:ee:ff": { + "name": "AranetRn+ 12345", + "connections": [ + ["bluetooth", "aa:bb:cc:dd:ee:ff"] + ], + } + }, + "entity_descriptions": { + "radon_concentration___aa:bb:cc:dd:ee:ff": { + "key": "radon_concentration", + "translation_key": "radon_concentration", + "name": "Radon Concentration", + "native_unit_of_measurement": "Bq/m³", + "state_class": "measurement", + } + }, + "entity_names": { + "radon_concentration___aa:bb:cc:dd:ee:ff": ( + "Radon Concentration" + ) + }, + "entity_data": { + "radon_concentration___aa:bb:cc:dd:ee:ff": 7 + }, + } + } + }, + } + }, + {RADON_ENTITY_ID: SensorDeviceClass.RADON}, + id="cached_without_device_class", + ), + ], + indirect=["mock_bluetooth_storage"], +) +async def test_existing_radon_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + restored_device_classes: dict[str, SensorDeviceClass], +) -> None: + """Test existing radon sensors gain the class without changing identity or units.""" + hass.config.units = US_CUSTOMARY_SYSTEM + entry = MockConfigEntry( + domain=DOMAIN, + entry_id="aranet_radon", + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + entity = entity_registry.async_get_or_create( + "sensor", + DOMAIN, + RADON_UNIQUE_ID, + config_entry=entry, + suggested_object_id="aranetrn_12345_radon_concentration", + original_name="Radon Concentration", + unit_of_measurement=UnitOfRadiationConcentration.BECQUEREL_PER_CUBIC_METER, + ) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert { + state.entity_id: state.attributes[ATTR_DEVICE_CLASS] + for state in hass.states.async_all("sensor") + } == restored_device_classes + + inject_bluetooth_service_info(hass, VALID_ARANET_RADON_DATA_SERVICE_INFO) + await hass.async_block_till_done() + + state = hass.states.get(RADON_ENTITY_ID) + assert state is not None + assert state.state == "7" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.RADON + assert ( + state.attributes[ATTR_UNIT_OF_MEASUREMENT] + == UnitOfRadiationConcentration.BECQUEREL_PER_CUBIC_METER + ) + assert state.attributes[ATTR_FRIENDLY_NAME] == "AranetRn+ 12345 Radon Concentration" + updated_entity = entity_registry.async_get(RADON_ENTITY_ID) + assert updated_entity is not None + assert updated_entity.id == entity.id + assert updated_entity.unique_id == RADON_UNIQUE_ID + assert updated_entity.original_device_class == SensorDeviceClass.RADON + + @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_smart_home_integration_disabled(hass: HomeAssistant) -> None: """Test disabling smart home integration marks entities as unavailable."""