Bump Nettigo Air Monitor library (#52085)
This commit is contained in:
parent
456755c077
commit
d08129352f
10 changed files with 164 additions and 52 deletions
|
@ -9,8 +9,8 @@ from aiohttp.client_exceptions import ClientConnectorError
|
|||
import async_timeout
|
||||
from nettigo_air_monitor import (
|
||||
ApiError,
|
||||
DictToObj,
|
||||
InvalidSensorData,
|
||||
NAMSensors,
|
||||
NettigoAirMonitor,
|
||||
)
|
||||
|
||||
|
@ -75,7 +75,7 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator):
|
|||
hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> DictToObj:
|
||||
async def _async_update_data(self) -> NAMSensors:
|
||||
"""Update data via library."""
|
||||
try:
|
||||
# Device firmware uses synchronous code and doesn't respond to http queries
|
||||
|
@ -86,8 +86,6 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator):
|
|||
except (ApiError, ClientConnectorError, InvalidSensorData) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
_LOGGER.debug(data)
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
"""Support for the Nettigo Air Monitor air_quality service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.air_quality import AirQualityEntity
|
||||
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.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NAMDataUpdateCoordinator
|
||||
from .const import (
|
||||
AIR_QUALITY_SENSORS,
|
||||
ATTR_MHZ14A_CARBON_DIOXIDE,
|
||||
ATTR_SDS011,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
SUFFIX_P1,
|
||||
|
@ -20,6 +23,8 @@ from .const import (
|
|||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
|
@ -27,9 +32,23 @@ async def async_setup_entry(
|
|||
"""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 f"{sensor}{SUFFIX_P1}" in coordinator.data:
|
||||
if getattr(coordinator.data, f"{sensor}{SUFFIX_P1}") is not None:
|
||||
entities.append(NAMAirQuality(coordinator, sensor))
|
||||
|
||||
async_add_entities(entities, False)
|
||||
|
@ -49,25 +68,25 @@ class NAMAirQuality(CoordinatorEntity, AirQualityEntity):
|
|||
self.sensor_type = sensor_type
|
||||
|
||||
@property
|
||||
def particulate_matter_2_5(self) -> StateType:
|
||||
def particulate_matter_2_5(self) -> int | None:
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
return round_state(
|
||||
getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}")
|
||||
return cast(
|
||||
Union[int, None],
|
||||
getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}"),
|
||||
)
|
||||
|
||||
@property
|
||||
def particulate_matter_10(self) -> StateType:
|
||||
def particulate_matter_10(self) -> int | None:
|
||||
"""Return the particulate matter 10 level."""
|
||||
return round_state(
|
||||
getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P1}")
|
||||
return cast(
|
||||
Union[int, None],
|
||||
getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P1}"),
|
||||
)
|
||||
|
||||
@property
|
||||
def carbon_dioxide(self) -> StateType:
|
||||
def carbon_dioxide(self) -> int | None:
|
||||
"""Return the particulate matter 10 level."""
|
||||
return round_state(
|
||||
getattr(self.coordinator.data, ATTR_MHZ14A_CARBON_DIOXIDE, None)
|
||||
)
|
||||
return cast(Union[int, None], self.coordinator.data.mhz14a_carbon_dioxide)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
@ -77,14 +96,8 @@ class NAMAirQuality(CoordinatorEntity, AirQualityEntity):
|
|||
# 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 bool(
|
||||
getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}", None)
|
||||
return (
|
||||
available
|
||||
and getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}")
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def round_state(state: StateType) -> StateType:
|
||||
"""Round state."""
|
||||
if isinstance(state, float):
|
||||
return round(state)
|
||||
|
||||
return state
|
||||
|
|
|
@ -118,4 +118,4 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
# when reading data from sensors. The nettigo-air-monitor library tries to get
|
||||
# the data 4 times, so we use a longer than usual timeout here.
|
||||
with async_timeout.timeout(30):
|
||||
return cast(str, await nam.async_get_mac_address())
|
||||
return await nam.async_get_mac_address()
|
||||
|
|
|
@ -22,21 +22,27 @@ from homeassistant.const import (
|
|||
|
||||
from .model import SensorDescription
|
||||
|
||||
SUFFIX_P0: Final = "_p0"
|
||||
SUFFIX_P1: Final = "_p1"
|
||||
SUFFIX_P2: Final = "_p2"
|
||||
SUFFIX_P4: Final = "_p4"
|
||||
|
||||
ATTR_BME280_HUMIDITY: Final = "bme280_humidity"
|
||||
ATTR_BME280_PRESSURE: Final = "bme280_pressure"
|
||||
ATTR_BME280_TEMPERATURE: Final = "bme280_temperature"
|
||||
ATTR_BMP280_PRESSURE: Final = "bmp280_pressure"
|
||||
ATTR_BMP280_TEMPERATURE: Final = "bmp280_temperature"
|
||||
ATTR_DHT22_HUMIDITY: Final = "humidity"
|
||||
ATTR_DHT22_TEMPERATURE: Final = "temperature"
|
||||
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 = "conc_co2_ppm"
|
||||
ATTR_SDS011: Final = "sds011"
|
||||
ATTR_SHT3X_HUMIDITY: Final = "sht3x_humidity"
|
||||
ATTR_SHT3X_TEMPERATURE: Final = "sht3x_temperature"
|
||||
ATTR_SIGNAL_STRENGTH: Final = "signal"
|
||||
ATTR_SPS30_P0: Final = "sps30_p0"
|
||||
ATTR_SPS30_P4: Final = "sps30_p4"
|
||||
ATTR_SPS30: Final = "sps30"
|
||||
ATTR_SPS30_P0: Final = f"{ATTR_SPS30}{SUFFIX_P0}"
|
||||
ATTR_SPS30_P4: Final = f"{ATTR_SPS30}{SUFFIX_P4}"
|
||||
ATTR_UPTIME: Final = "uptime"
|
||||
|
||||
ATTR_ENABLED: Final = "enabled"
|
||||
|
@ -48,10 +54,15 @@ DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6)
|
|||
DOMAIN: Final = "nam"
|
||||
MANUFACTURER: Final = "Nettigo"
|
||||
|
||||
SUFFIX_P1: Final = "_p1"
|
||||
SUFFIX_P2: Final = "_p2"
|
||||
AIR_QUALITY_SENSORS: Final[dict[str, str]] = {
|
||||
ATTR_SDS011: "SDS011",
|
||||
ATTR_SPS30: "SPS30",
|
||||
}
|
||||
|
||||
AIR_QUALITY_SENSORS: Final[dict[str, str]] = {"sds": "SDS011", "sps30": "SPS30"}
|
||||
MIGRATION_SENSORS: Final = [
|
||||
("temperature", ATTR_DHT22_TEMPERATURE),
|
||||
("humidity", ATTR_DHT22_HUMIDITY),
|
||||
]
|
||||
|
||||
SENSORS: Final[dict[str, SensorDescription]] = {
|
||||
ATTR_BME280_HUMIDITY: {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Nettigo Air Monitor",
|
||||
"documentation": "https://www.home-assistant.io/integrations/nam",
|
||||
"codeowners": ["@bieniu"],
|
||||
"requirements": ["nettigo-air-monitor==0.2.6"],
|
||||
"requirements": ["nettigo-air-monitor==1.0.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
|
|
@ -2,21 +2,38 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_STATE_CLASS,
|
||||
DOMAIN as PLATFORM,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import NAMDataUpdateCoordinator
|
||||
from .const import ATTR_ENABLED, ATTR_LABEL, ATTR_UNIT, ATTR_UPTIME, DOMAIN, SENSORS
|
||||
from .const import (
|
||||
ATTR_ENABLED,
|
||||
ATTR_LABEL,
|
||||
ATTR_UNIT,
|
||||
ATTR_UPTIME,
|
||||
DOMAIN,
|
||||
MIGRATION_SENSORS,
|
||||
SENSORS,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
|
@ -24,9 +41,24 @@ async def async_setup_entry(
|
|||
"""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 two sensora, it is necessary to migrate
|
||||
# the unique_ids to the new names.
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
for old_sensor, new_sensor in MIGRATION_SENSORS:
|
||||
old_unique_id = f"{coordinator.unique_id}-{old_sensor}"
|
||||
new_unique_id = f"{coordinator.unique_id}-{new_sensor}"
|
||||
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)
|
||||
|
||||
sensors: list[NAMSensor | NAMSensorUptime] = []
|
||||
for sensor in SENSORS:
|
||||
if sensor in coordinator.data:
|
||||
if getattr(coordinator.data, sensor) is not None:
|
||||
if sensor == ATTR_UPTIME:
|
||||
sensors.append(NAMSensorUptime(coordinator, sensor))
|
||||
else:
|
||||
|
@ -55,9 +87,9 @@ class NAMSensor(CoordinatorEntity, SensorEntity):
|
|||
self.sensor_type = sensor_type
|
||||
|
||||
@property
|
||||
def state(self) -> Any:
|
||||
def state(self) -> StateType:
|
||||
"""Return the state."""
|
||||
return getattr(self.coordinator.data, self.sensor_type)
|
||||
return cast(StateType, getattr(self.coordinator.data, self.sensor_type))
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
@ -67,8 +99,8 @@ class NAMSensor(CoordinatorEntity, SensorEntity):
|
|||
# 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 bool(
|
||||
getattr(self.coordinator.data, self.sensor_type, None)
|
||||
return (
|
||||
available and getattr(self.coordinator.data, self.sensor_type) is not None
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1008,7 +1008,7 @@ netdata==0.2.0
|
|||
netdisco==2.8.3
|
||||
|
||||
# homeassistant.components.nam
|
||||
nettigo-air-monitor==0.2.6
|
||||
nettigo-air-monitor==1.0.0
|
||||
|
||||
# homeassistant.components.neurio_energy
|
||||
neurio==0.3.1
|
||||
|
|
|
@ -562,7 +562,7 @@ nessclient==0.9.15
|
|||
netdisco==2.8.3
|
||||
|
||||
# homeassistant.components.nam
|
||||
nettigo-air-monitor==0.2.6
|
||||
nettigo-air-monitor==1.0.0
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.9.7
|
||||
|
|
|
@ -4,7 +4,13 @@ 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
|
||||
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,
|
||||
|
@ -39,7 +45,7 @@ async def test_air_quality(hass):
|
|||
|
||||
entry = registry.async_get("air_quality.nettigo_air_monitor_sds011")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds"
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011"
|
||||
|
||||
state = hass.states.get("air_quality.nettigo_air_monitor_sps30")
|
||||
assert state
|
||||
|
@ -146,3 +152,22 @@ async def test_manual_update_entity(hass):
|
|||
)
|
||||
|
||||
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"
|
||||
|
|
|
@ -143,7 +143,7 @@ async def test_sensor(hass):
|
|||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-humidity"
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_humidity"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_dht22_temperature")
|
||||
assert state
|
||||
|
@ -154,7 +154,7 @@ async def test_sensor(hass):
|
|||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-temperature"
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_temperature"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_heca_humidity")
|
||||
assert state
|
||||
|
@ -302,3 +302,36 @@ async def test_manual_update_entity(hass):
|
|||
)
|
||||
|
||||
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(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
"aa:bb:cc:dd:ee:ff-temperature",
|
||||
suggested_object_id="nettigo_air_monitor_dht22_temperature",
|
||||
disabled_by=None,
|
||||
)
|
||||
|
||||
registry.async_get_or_create(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
"aa:bb:cc:dd:ee:ff-humidity",
|
||||
suggested_object_id="nettigo_air_monitor_dht22_humidity",
|
||||
disabled_by=None,
|
||||
)
|
||||
|
||||
await init_integration(hass)
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_temperature"
|
||||
|
||||
await init_integration(hass)
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_humidity"
|
||||
|
|
Loading…
Add table
Reference in a new issue