diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index cf1df6b8ac8..97d54fb0669 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -14,19 +14,28 @@ from nettigo_air_monitor import ( NettigoAirMonitor, ) +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_NAME, DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER +from .const import ( + ATTR_SDS011, + ATTR_SPS30, + DEFAULT_NAME, + DEFAULT_UPDATE_INTERVAL, + DOMAIN, + MANUFACTURER, +) _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["air_quality", "sensor"] +PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -43,6 +52,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # Remove air_quality entities from registry if they exist + ent_reg = entity_registry.async_get(hass) + for sensor_type in ["sds", ATTR_SDS011, ATTR_SPS30]: + unique_id = f"{coordinator.unique_id}-{sensor_type}" + if entity_id := ent_reg.async_get_entity_id( + AIR_QUALITY_PLATFORM, DOMAIN, unique_id + ): + _LOGGER.debug("Removing deprecated air_quality entity %s", entity_id) + ent_reg.async_remove(entity_id) + return True diff --git a/homeassistant/components/nam/air_quality.py b/homeassistant/components/nam/air_quality.py deleted file mode 100644 index 4c51003f3e6..00000000000 --- a/homeassistant/components/nam/air_quality.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Support for the Nettigo Air Monitor air_quality service.""" -from __future__ import annotations - -import logging -from typing import Union, cast - -from homeassistant.components.air_quality import DOMAIN as PLATFORM, AirQualityEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from . import NAMDataUpdateCoordinator -from .const import ( - AIR_QUALITY_SENSORS, - ATTR_SDS011, - DEFAULT_NAME, - DOMAIN, - SUFFIX_P1, - SUFFIX_P2, -) - -PARALLEL_UPDATES = 1 - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Add a Nettigo Air Monitor entities from a config_entry.""" - coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - # Due to the change of the attribute name of one sensor, it is necessary to migrate - # the unique_id to the new name. - ent_reg = entity_registry.async_get(hass) - old_unique_id = f"{coordinator.unique_id}-sds" - new_unique_id = f"{coordinator.unique_id}-{ATTR_SDS011}" - if entity_id := ent_reg.async_get_entity_id(PLATFORM, DOMAIN, old_unique_id): - _LOGGER.debug( - "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", - entity_id, - old_unique_id, - new_unique_id, - ) - ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) - - entities: list[NAMAirQuality] = [] - for sensor in AIR_QUALITY_SENSORS: - if getattr(coordinator.data, f"{sensor}{SUFFIX_P1}") is not None: - entities.append(NAMAirQuality(coordinator, sensor)) - - async_add_entities(entities, False) - - -class NAMAirQuality(CoordinatorEntity, AirQualityEntity): - """Define an Nettigo Air Monitor air quality.""" - - coordinator: NAMDataUpdateCoordinator - - def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None: - """Initialize.""" - super().__init__(coordinator) - self._attr_device_info = coordinator.device_info - self._attr_name = f"{DEFAULT_NAME} {AIR_QUALITY_SENSORS[sensor_type]}" - self._attr_unique_id = f"{coordinator.unique_id}-{sensor_type}" - self.sensor_type = sensor_type - - @property - def particulate_matter_2_5(self) -> int | None: - """Return the particulate matter 2.5 level.""" - return cast( - Union[int, None], - getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}"), - ) - - @property - def particulate_matter_10(self) -> int | None: - """Return the particulate matter 10 level.""" - return cast( - Union[int, None], - getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P1}"), - ) - - @property - def carbon_dioxide(self) -> int | None: - """Return the particulate matter 10 level.""" - return cast(Union[int, None], self.coordinator.data.mhz14a_carbon_dioxide) - - @property - def available(self) -> bool: - """Return if entity is available.""" - available = super().available - - # For a short time after booting, the device does not return values for all - # sensors. For this reason, we mark entities for which data is missing as - # unavailable. - return ( - available - and getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}") - is not None - ) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 318d1e15802..f60d03eea78 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -9,6 +9,8 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, @@ -36,12 +38,17 @@ ATTR_DHT22_HUMIDITY: Final = "dht22_humidity" ATTR_DHT22_TEMPERATURE: Final = "dht22_temperature" ATTR_HECA_HUMIDITY: Final = "heca_humidity" ATTR_HECA_TEMPERATURE: Final = "heca_temperature" +ATTR_MHZ14A_CARBON_DIOXIDE: Final = "mhz14a_carbon_dioxide" ATTR_SDS011: Final = "sds011" +ATTR_SDS011_P1: Final = f"{ATTR_SDS011}{SUFFIX_P1}" +ATTR_SDS011_P2: Final = f"{ATTR_SDS011}{SUFFIX_P2}" ATTR_SHT3X_HUMIDITY: Final = "sht3x_humidity" ATTR_SHT3X_TEMPERATURE: Final = "sht3x_temperature" ATTR_SIGNAL_STRENGTH: Final = "signal" ATTR_SPS30: Final = "sps30" ATTR_SPS30_P0: Final = f"{ATTR_SPS30}{SUFFIX_P0}" +ATTR_SPS30_P1: Final = f"{ATTR_SPS30}{SUFFIX_P1}" +ATTR_SPS30_P2: Final = f"{ATTR_SPS30}{SUFFIX_P2}" ATTR_SPS30_P4: Final = f"{ATTR_SPS30}{SUFFIX_P4}" ATTR_UPTIME: Final = "uptime" @@ -54,11 +61,6 @@ DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6) DOMAIN: Final = "nam" MANUFACTURER: Final = "Nettigo" -AIR_QUALITY_SENSORS: Final[dict[str, str]] = { - ATTR_SDS011: "SDS011", - ATTR_SPS30: "SPS30", -} - MIGRATION_SENSORS: Final = [ ("temperature", ATTR_DHT22_TEMPERATURE), ("humidity", ATTR_DHT22_HUMIDITY), @@ -121,6 +123,30 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_ENABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, + ATTR_MHZ14A_CARBON_DIOXIDE: { + ATTR_LABEL: f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide", + ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CO2, + ATTR_ICON: None, + ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + ATTR_SDS011_P1: { + ATTR_LABEL: f"{DEFAULT_NAME} SDS011 Particulate Matter 10", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + ATTR_SDS011_P2: { + ATTR_LABEL: f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, ATTR_SHT3X_HUMIDITY: { ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Humidity", ATTR_UNIT: PERCENTAGE, @@ -145,6 +171,22 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_ENABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, + ATTR_SPS30_P1: { + ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 10", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + ATTR_SPS30_P2: { + ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, ATTR_SPS30_P4: { ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, diff --git a/tests/components/nam/test_air_quality.py b/tests/components/nam/test_air_quality.py deleted file mode 100644 index 5f687b8a29a..00000000000 --- a/tests/components/nam/test_air_quality.py +++ /dev/null @@ -1,173 +0,0 @@ -"""Test air_quality of Nettigo Air Monitor integration.""" -from datetime import timedelta -from unittest.mock import patch - -from nettigo_air_monitor import ApiError - -from homeassistant.components.air_quality import ( - ATTR_CO2, - ATTR_PM_2_5, - ATTR_PM_10, - DOMAIN as AIR_QUALITY_DOMAIN, -) -from homeassistant.components.nam.const import DOMAIN -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - STATE_UNAVAILABLE, -) -from homeassistant.helpers import entity_registry as er -from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow - -from . import INCOMPLETE_NAM_DATA, nam_data - -from tests.common import async_fire_time_changed -from tests.components.nam import init_integration - - -async def test_air_quality(hass): - """Test states of the air_quality.""" - await init_integration(hass) - registry = er.async_get(hass) - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state == "11" - assert state.attributes.get(ATTR_PM_10) == 19 - assert state.attributes.get(ATTR_PM_2_5) == 11 - assert state.attributes.get(ATTR_CO2) == 865 - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = registry.async_get("air_quality.nettigo_air_monitor_sds011") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011" - - state = hass.states.get("air_quality.nettigo_air_monitor_sps30") - assert state - assert state.state == "34" - assert state.attributes.get(ATTR_PM_10) == 21 - assert state.attributes.get(ATTR_PM_2_5) == 34 - assert state.attributes.get(ATTR_CO2) == 865 - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = registry.async_get("air_quality.nettigo_air_monitor_sps30") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30" - - -async def test_air_quality_without_co2_value(hass): - """Test states of the air_quality.""" - await init_integration(hass, co2_sensor=False) - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.attributes.get(ATTR_CO2) is None - - -async def test_incompleta_data_after_device_restart(hass): - """Test states of the air_quality after device restart.""" - await init_integration(hass) - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state == "11" - assert state.attributes.get(ATTR_PM_10) == 19 - assert state.attributes.get(ATTR_PM_2_5) == 11 - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - future = utcnow() + timedelta(minutes=6) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - return_value=INCOMPLETE_NAM_DATA, - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state == STATE_UNAVAILABLE - - -async def test_availability(hass): - """Ensure that we mark the entities unavailable correctly when device causes an error.""" - await init_integration(hass) - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "11" - - future = utcnow() + timedelta(minutes=6) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - side_effect=ApiError("API Error"), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state == STATE_UNAVAILABLE - - future = utcnow() + timedelta(minutes=12) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - return_value=nam_data, - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "11" - - -async def test_manual_update_entity(hass): - """Test manual update entity via service homeasasistant/update_entity.""" - await init_integration(hass) - - await async_setup_component(hass, "homeassistant", {}) - - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - return_value=nam_data, - ) as mock_get_data: - await hass.services.async_call( - "homeassistant", - "update_entity", - {ATTR_ENTITY_ID: ["air_quality.nettigo_air_monitor_sds011"]}, - blocking=True, - ) - - assert mock_get_data.call_count == 1 - - -async def test_unique_id_migration(hass): - """Test states of the unique_id migration.""" - registry = er.async_get(hass) - - registry.async_get_or_create( - AIR_QUALITY_DOMAIN, - DOMAIN, - "aa:bb:cc:dd:ee:ff-sds", - suggested_object_id="nettigo_air_monitor_sds011", - disabled_by=None, - ) - - await init_integration(hass) - - entry = registry.async_get("air_quality.nettigo_air_monitor_sds011") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011" diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index 943ea53f360..97392cbaff8 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -3,9 +3,11 @@ from unittest.mock import patch from nettigo_air_monitor import ApiError +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.components.nam.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry from tests.components.nam import init_integration @@ -15,7 +17,7 @@ async def test_async_setup_entry(hass): """Test a successful setup entry.""" await init_integration(hass) - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_2_5") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.state == "11" @@ -51,3 +53,32 @@ async def test_unload_entry(hass): assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_remove_air_quality_entities(hass): + """Test remove air_quality entities from registry.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + AIR_QUALITY_PLATFORM, + DOMAIN, + "aa:bb:cc:dd:ee:ff-sds011", + suggested_object_id="nettigo_air_monitor_sds011", + disabled_by=None, + ) + + registry.async_get_or_create( + AIR_QUALITY_PLATFORM, + DOMAIN, + "aa:bb:cc:dd:ee:ff-sps30", + suggested_object_id="nettigo_air_monitor_sps30", + disabled_by=None, + ) + + await init_integration(hass) + + entry = registry.async_get("air_quality.nettigo_air_monitor_sds011") + assert entry is None + + entry = registry.async_get("air_quality.nettigo_air_monitor_sps30") + assert entry is None diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 12fa0f01e44..506a81f7619 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -13,7 +13,11 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, @@ -205,6 +209,113 @@ async def test_sensor(hass): assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-uptime" + state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_10") + assert state + assert state.state == "19" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get( + "sensor.nettigo_air_monitor_sds011_particulate_matter_10" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p1" + + state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_2_5") + assert state + assert state.state == "11" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get( + "sensor.nettigo_air_monitor_sds011_particulate_matter_2_5" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p2" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_1_0") + assert state + assert state.state == "31" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get( + "sensor.nettigo_air_monitor_sps30_particulate_matter_1_0" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p0" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_10") + assert state + assert state.state == "21" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.nettigo_air_monitor_sps30_particulate_matter_10") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p1" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_2_5") + assert state + assert state.state == "34" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get( + "sensor.nettigo_air_monitor_sps30_particulate_matter_2_5" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p2" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_4_0") + assert state + assert state.state == "25" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get( + "sensor.nettigo_air_monitor_sps30_particulate_matter_4_0" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p4" + + state = hass.states.get("sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide") + assert state + assert state.state == "865" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO2 + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_MILLION + ) + entry = registry.async_get("sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-mhz14a_carbon_dioxide" + async def test_sensor_disabled(hass): """Test sensor disabled by default."""