mirror of https://github.com/home-assistant/core
Add Nettigo Air Monitor integration (#49099)
This commit is contained in:
parent
dc29087416
commit
0587f834df
|
@ -25,6 +25,7 @@ homeassistant.components.light.*
|
|||
homeassistant.components.lock.*
|
||||
homeassistant.components.mailbox.*
|
||||
homeassistant.components.media_player.*
|
||||
homeassistant.components.nam.*
|
||||
homeassistant.components.notify.*
|
||||
homeassistant.components.number.*
|
||||
homeassistant.components.persistent_notification.*
|
||||
|
|
|
@ -308,6 +308,7 @@ homeassistant/components/my/* @home-assistant/core
|
|||
homeassistant/components/myq/* @bdraco
|
||||
homeassistant/components/mysensors/* @MartinHjelmare @functionpointer
|
||||
homeassistant/components/mystrom/* @fabaff
|
||||
homeassistant/components/nam/* @bieniu
|
||||
homeassistant/components/neato/* @dshokouhi @Santobert
|
||||
homeassistant/components/nederlandse_spoorwegen/* @YarmoM
|
||||
homeassistant/components/nello/* @pschmitt
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
"""The Nettigo Air Monitor component."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
import async_timeout
|
||||
from nettigo_air_monitor import (
|
||||
ApiError,
|
||||
DictToObj,
|
||||
InvalidSensorData,
|
||||
NettigoAirMonitor,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["air_quality", "sensor"]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Nettigo as config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
coordinator = NAMDataUpdateCoordinator(hass, websession, host, entry.unique_id)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class NAMDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching Nettigo Air Monitor data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
session: ClientSession,
|
||||
host: str,
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.host = host
|
||||
self.nam = NettigoAirMonitor(session, host)
|
||||
self._unique_id = unique_id
|
||||
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> DictToObj:
|
||||
"""Update data via library."""
|
||||
try:
|
||||
# Device firmware uses synchronous code and doesn't respond to http queries
|
||||
# when reading data from sensors. The nettigo-air-quality library tries to
|
||||
# get the data 4 times, so we use a longer than usual timeout here.
|
||||
with async_timeout.timeout(30):
|
||||
data = await self.nam.async_update()
|
||||
except (ApiError, ClientConnectorError, InvalidSensorData) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
_LOGGER.debug(data)
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str | None:
|
||||
"""Return a unique_id."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return {
|
||||
"connections": {(CONNECTION_NETWORK_MAC, cast(str, self._unique_id))},
|
||||
"name": DEFAULT_NAME,
|
||||
"sw_version": self.nam.software_version,
|
||||
"manufacturer": MANUFACTURER,
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
"""Support for the Nettigo Air Monitor air_quality service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.air_quality import AirQualityEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
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, DEFAULT_NAME, DOMAIN, SUFFIX_P1, SUFFIX_P2
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
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 = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
entities = []
|
||||
for sensor in AIR_QUALITY_SENSORS:
|
||||
if f"{sensor}{SUFFIX_P1}" in coordinator.data:
|
||||
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.sensor_type = sensor_type
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name."""
|
||||
return f"{DEFAULT_NAME} {AIR_QUALITY_SENSORS[self.sensor_type]}"
|
||||
|
||||
@property
|
||||
def particulate_matter_2_5(self) -> StateType:
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
return round_state(
|
||||
getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}")
|
||||
)
|
||||
|
||||
@property
|
||||
def particulate_matter_10(self) -> StateType:
|
||||
"""Return the particulate matter 10 level."""
|
||||
return round_state(
|
||||
getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P1}")
|
||||
)
|
||||
|
||||
@property
|
||||
def carbon_dioxide(self) -> StateType:
|
||||
"""Return the particulate matter 10 level."""
|
||||
return round_state(getattr(self.coordinator.data, "conc_co2_ppm", None))
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique_id for this entity."""
|
||||
return f"{self.coordinator.unique_id}-{self.sensor_type}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return self.coordinator.device_info
|
||||
|
||||
@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 bool(
|
||||
getattr(self.coordinator.data, f"{self.sensor_type}_p2", None)
|
||||
)
|
||||
|
||||
|
||||
def round_state(state: StateType) -> StateType:
|
||||
"""Round state."""
|
||||
if isinstance(state, float):
|
||||
return round(state)
|
||||
|
||||
return state
|
|
@ -0,0 +1,121 @@
|
|||
"""Adds config flow for Nettigo Air Monitor."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
import async_timeout
|
||||
from nettigo_air_monitor import ApiError, CannotGetMac, NettigoAirMonitor
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import ATTR_NAME, CONF_HOST
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for Nettigo Air Monitor."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize flow."""
|
||||
self.host: str | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
self.host = user_input[CONF_HOST]
|
||||
try:
|
||||
mac = await self._async_get_mac(cast(str, self.host))
|
||||
except (ApiError, ClientConnectorError, asyncio.TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotGetMac:
|
||||
return self.async_abort(reason="device_unsupported")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
|
||||
await self.async_set_unique_id(format_mac(mac))
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self.host})
|
||||
|
||||
return self.async_create_entry(
|
||||
title=cast(str, self.host),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST, default=""): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: DiscoveryInfoType
|
||||
) -> FlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
self.host = discovery_info[CONF_HOST]
|
||||
|
||||
try:
|
||||
mac = await self._async_get_mac(cast(str, self.host))
|
||||
except (ApiError, ClientConnectorError, asyncio.TimeoutError):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except CannotGetMac:
|
||||
return self.async_abort(reason="device_unsupported")
|
||||
|
||||
await self.async_set_unique_id(format_mac(mac))
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self.host})
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
ATTR_NAME: discovery_info[ATTR_NAME].split(".")[0]
|
||||
}
|
||||
|
||||
return await self.async_step_confirm_discovery()
|
||||
|
||||
async def async_step_confirm_discovery(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle discovery confirm."""
|
||||
errors: dict = {}
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=cast(str, self.host),
|
||||
data={CONF_HOST: self.host},
|
||||
)
|
||||
|
||||
self._set_confirm_only()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm_discovery",
|
||||
description_placeholders={CONF_HOST: self.host},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _async_get_mac(self, host: str) -> str:
|
||||
"""Get device MAC address."""
|
||||
websession = async_get_clientsession(self.hass)
|
||||
nam = NettigoAirMonitor(websession, host)
|
||||
# Device firmware uses synchronous code and doesn't respond to http queries
|
||||
# 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())
|
|
@ -0,0 +1,130 @@
|
|||
"""Constants for Nettigo Air Monitor integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
PERCENTAGE,
|
||||
PRESSURE_HPA,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
|
||||
from .model import SensorDescription
|
||||
|
||||
DEFAULT_NAME: Final = "Nettigo Air Monitor"
|
||||
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]] = {"sds": "SDS011", "sps30": "SPS30"}
|
||||
|
||||
SENSORS: Final[dict[str, SensorDescription]] = {
|
||||
"bme280_humidity": {
|
||||
"label": f"{DEFAULT_NAME} BME280 Humidity",
|
||||
"unit": PERCENTAGE,
|
||||
"device_class": DEVICE_CLASS_HUMIDITY,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
"bme280_pressure": {
|
||||
"label": f"{DEFAULT_NAME} BME280 Pressure",
|
||||
"unit": PRESSURE_HPA,
|
||||
"device_class": DEVICE_CLASS_PRESSURE,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
"bme280_temperature": {
|
||||
"label": f"{DEFAULT_NAME} BME280 Temperature",
|
||||
"unit": TEMP_CELSIUS,
|
||||
"device_class": DEVICE_CLASS_TEMPERATURE,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
"bmp280_pressure": {
|
||||
"label": f"{DEFAULT_NAME} BMP280 Pressure",
|
||||
"unit": PRESSURE_HPA,
|
||||
"device_class": DEVICE_CLASS_PRESSURE,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
"bmp280_temperature": {
|
||||
"label": f"{DEFAULT_NAME} BMP280 Temperature",
|
||||
"unit": TEMP_CELSIUS,
|
||||
"device_class": DEVICE_CLASS_TEMPERATURE,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
"heca_humidity": {
|
||||
"label": f"{DEFAULT_NAME} HECA Humidity",
|
||||
"unit": PERCENTAGE,
|
||||
"device_class": DEVICE_CLASS_HUMIDITY,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
"heca_temperature": {
|
||||
"label": f"{DEFAULT_NAME} HECA Temperature",
|
||||
"unit": TEMP_CELSIUS,
|
||||
"device_class": DEVICE_CLASS_TEMPERATURE,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
"sht3x_humidity": {
|
||||
"label": f"{DEFAULT_NAME} SHT3X Humidity",
|
||||
"unit": PERCENTAGE,
|
||||
"device_class": DEVICE_CLASS_HUMIDITY,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
"sht3x_temperature": {
|
||||
"label": f"{DEFAULT_NAME} SHT3X Temperature",
|
||||
"unit": TEMP_CELSIUS,
|
||||
"device_class": DEVICE_CLASS_TEMPERATURE,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
"sps30_p0": {
|
||||
"label": f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0",
|
||||
"unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
"device_class": None,
|
||||
"icon": "mdi:blur",
|
||||
"enabled": True,
|
||||
},
|
||||
"sps30_p4": {
|
||||
"label": f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0",
|
||||
"unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
"device_class": None,
|
||||
"icon": "mdi:blur",
|
||||
"enabled": True,
|
||||
},
|
||||
"humidity": {
|
||||
"label": f"{DEFAULT_NAME} DHT22 Humidity",
|
||||
"unit": PERCENTAGE,
|
||||
"device_class": DEVICE_CLASS_HUMIDITY,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
"signal": {
|
||||
"label": f"{DEFAULT_NAME} Signal Strength",
|
||||
"unit": SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
"device_class": DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
"icon": None,
|
||||
"enabled": False,
|
||||
},
|
||||
"temperature": {
|
||||
"label": f"{DEFAULT_NAME} DHT22 Temperature",
|
||||
"unit": TEMP_CELSIUS,
|
||||
"device_class": DEVICE_CLASS_TEMPERATURE,
|
||||
"icon": None,
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"domain": "nam",
|
||||
"name": "Nettigo Air Monitor",
|
||||
"documentation": "https://www.home-assistant.io/integrations/nam",
|
||||
"codeowners": ["@bieniu"],
|
||||
"requirements": ["nettigo-air-monitor==0.2.5"],
|
||||
"zeroconf": [{"type": "_http._tcp.local.", "name": "nam-*"}],
|
||||
"config_flow": true,
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "local_polling"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
"""Type definitions for Nettig Air Monitor integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class SensorDescription(TypedDict):
|
||||
"""Sensor description class."""
|
||||
|
||||
label: str
|
||||
unit: str | None
|
||||
device_class: str | None
|
||||
icon: str | None
|
||||
enabled: bool
|
|
@ -0,0 +1,94 @@
|
|||
"""Support for the Nettigo Air Monitor service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NAMDataUpdateCoordinator
|
||||
from .const import DOMAIN, SENSORS
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
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 = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
sensors = []
|
||||
for sensor in SENSORS:
|
||||
if sensor in coordinator.data:
|
||||
sensors.append(NAMSensor(coordinator, sensor))
|
||||
|
||||
async_add_entities(sensors, False)
|
||||
|
||||
|
||||
class NAMSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Define an Nettigo Air Monitor sensor."""
|
||||
|
||||
coordinator: NAMDataUpdateCoordinator
|
||||
|
||||
def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self.sensor_type = sensor_type
|
||||
self._description = SENSORS[self.sensor_type]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name."""
|
||||
return self._description["label"]
|
||||
|
||||
@property
|
||||
def state(self) -> Any:
|
||||
"""Return the state."""
|
||||
return getattr(self.coordinator.data, self.sensor_type)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._description["unit"]
|
||||
|
||||
@property
|
||||
def device_class(self) -> str | None:
|
||||
"""Return the class of this sensor."""
|
||||
return self._description["device_class"]
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon."""
|
||||
return self._description["icon"]
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self._description["enabled"]
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique_id for this entity."""
|
||||
return f"{self.coordinator.unique_id}-{self.sensor_type}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return self.coordinator.device_info
|
||||
|
||||
@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 bool(
|
||||
getattr(self.coordinator.data, self.sensor_type, None)
|
||||
)
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Set up Nettigo Air Monitor integration.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
},
|
||||
"confirm_discovery": {
|
||||
"description": "Do you want to set up Nettigo Air Monitor at {host}?"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"device_unsupported": "The device is unsupported."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -159,6 +159,7 @@ FLOWS = [
|
|||
"mutesync",
|
||||
"myq",
|
||||
"mysensors",
|
||||
"nam",
|
||||
"neato",
|
||||
"nest",
|
||||
"netatmo",
|
||||
|
|
|
@ -94,6 +94,10 @@ ZEROCONF = {
|
|||
}
|
||||
],
|
||||
"_http._tcp.local.": [
|
||||
{
|
||||
"domain": "nam",
|
||||
"name": "nam-*"
|
||||
},
|
||||
{
|
||||
"domain": "rachio",
|
||||
"name": "rachio*"
|
||||
|
|
13
mypy.ini
13
mypy.ini
|
@ -334,6 +334,19 @@ warn_return_any = true
|
|||
warn_unreachable = true
|
||||
warn_unused_ignores = true
|
||||
|
||||
[mypy-homeassistant.components.nam.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
strict_equality = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
warn_unused_ignores = true
|
||||
|
||||
[mypy-homeassistant.components.notify.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -987,6 +987,9 @@ netdata==0.2.0
|
|||
# homeassistant.components.ssdp
|
||||
netdisco==2.8.3
|
||||
|
||||
# homeassistant.components.nam
|
||||
nettigo-air-monitor==0.2.5
|
||||
|
||||
# homeassistant.components.neurio_energy
|
||||
neurio==0.3.1
|
||||
|
||||
|
|
|
@ -535,6 +535,9 @@ nessclient==0.9.15
|
|||
# homeassistant.components.ssdp
|
||||
netdisco==2.8.3
|
||||
|
||||
# homeassistant.components.nam
|
||||
nettigo-air-monitor==0.2.5
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.9.6
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
"""Tests for the Nettigo Air Monitor integration."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.nam.const import DOMAIN
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
INCOMPLETE_NAM_DATA = {
|
||||
"software_version": "NAMF-2020-36",
|
||||
"sensordatavalues": [],
|
||||
}
|
||||
|
||||
nam_data = {
|
||||
"software_version": "NAMF-2020-36",
|
||||
"sensordatavalues": [
|
||||
{"value_type": "SDS_P1", "value": "18.65"},
|
||||
{"value_type": "SDS_P2", "value": "11.03"},
|
||||
{"value_type": "SPS30_P0", "value": "31.23"},
|
||||
{"value_type": "SPS30_P1", "value": "21.23"},
|
||||
{"value_type": "SPS30_P2", "value": "34.32"},
|
||||
{"value_type": "SPS30_P4", "value": "24.72"},
|
||||
{"value_type": "conc_co2_ppm", "value": "865"},
|
||||
{"value_type": "BME280_temperature", "value": "7.56"},
|
||||
{"value_type": "BME280_humidity", "value": "45.69"},
|
||||
{"value_type": "BME280_pressure", "value": "101101.17"},
|
||||
{"value_type": "BMP280_temperature", "value": "5.56"},
|
||||
{"value_type": "BMP280_pressure", "value": "102201.18"},
|
||||
{"value_type": "SHT3X_temperature", "value": "6.28"},
|
||||
{"value_type": "SHT3X_humidity", "value": "34.69"},
|
||||
{"value_type": "humidity", "value": "46.23"},
|
||||
{"value_type": "temperature", "value": "6.26"},
|
||||
{"value_type": "HECA_temperature", "value": "7.95"},
|
||||
{"value_type": "HECA_humidity", "value": "49.97"},
|
||||
{"value_type": "signal", "value": "-72"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def init_integration(hass, co2_sensor=True) -> MockConfigEntry:
|
||||
"""Set up the Nettigo Air Monitor integration in Home Assistant."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="10.10.2.3",
|
||||
unique_id="aa:bb:cc:dd:ee:ff",
|
||||
data={"host": "10.10.2.3"},
|
||||
)
|
||||
|
||||
if not co2_sensor:
|
||||
# Remove conc_co2_ppm value
|
||||
nam_data["sensordatavalues"].pop(6)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.nam.NettigoAirMonitor._async_get_data",
|
||||
return_value=nam_data,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
|
@ -0,0 +1,148 @@
|
|||
"""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
|
||||
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-sds"
|
||||
|
||||
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
|
|
@ -0,0 +1,175 @@
|
|||
"""Define tests for the Nettigo Air Monitor config flow."""
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
from nettigo_air_monitor import ApiError, CannotGetMac
|
||||
import pytest
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.nam.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
DISCOVERY_INFO = {"host": "10.10.2.3", "name": "NAM-12345"}
|
||||
VALID_CONFIG = {"host": "10.10.2.3"}
|
||||
|
||||
|
||||
async def test_form_create_entry(hass):
|
||||
"""Test that the user step works."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
|
||||
return_value="aa:bb:cc:dd:ee:ff",
|
||||
), patch(
|
||||
"homeassistant.components.nam.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
VALID_CONFIG,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "10.10.2.3"
|
||||
assert result["data"]["host"] == "10.10.2.3"
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error",
|
||||
[
|
||||
(ApiError("Invalid response from device 10.10.2.3: 404"), "cannot_connect"),
|
||||
(asyncio.TimeoutError, "cannot_connect"),
|
||||
(ValueError, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_form_errors(hass, error):
|
||||
"""Test we handle errors."""
|
||||
exc, base_error = error
|
||||
with patch(
|
||||
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
|
||||
side_effect=exc,
|
||||
):
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=VALID_CONFIG,
|
||||
)
|
||||
|
||||
assert result["errors"] == {"base": base_error}
|
||||
|
||||
|
||||
async def test_form_abort(hass):
|
||||
"""Test we handle abort after error."""
|
||||
with patch(
|
||||
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
|
||||
side_effect=CannotGetMac("Cannot get MAC address from device"),
|
||||
):
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=VALID_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "device_unsupported"
|
||||
|
||||
|
||||
async def test_form_already_configured(hass):
|
||||
"""Test that errors are shown when duplicates are added."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=VALID_CONFIG
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
|
||||
return_value="aa:bb:cc:dd:ee:ff",
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"host": "1.1.1.1"},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
# Test config entry got updated with latest IP
|
||||
assert entry.data["host"] == "1.1.1.1"
|
||||
|
||||
|
||||
async def test_zeroconf(hass):
|
||||
"""Test we get the form."""
|
||||
with patch(
|
||||
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
|
||||
return_value="aa:bb:cc:dd:ee:ff",
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
data=DISCOVERY_INFO,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
)
|
||||
context = next(
|
||||
flow["context"]
|
||||
for flow in hass.config_entries.flow.async_progress()
|
||||
if flow["flow_id"] == result["flow_id"]
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {}
|
||||
assert context["title_placeholders"]["name"] == "NAM-12345"
|
||||
assert context["confirm_only"] is True
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.nam.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "10.10.2.3"
|
||||
assert result["data"] == {"host": "10.10.2.3"}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error",
|
||||
[
|
||||
(ApiError("Invalid response from device 10.10.2.3: 404"), "cannot_connect"),
|
||||
(CannotGetMac("Cannot get MAC address from device"), "device_unsupported"),
|
||||
],
|
||||
)
|
||||
async def test_zeroconf_errors(hass, error):
|
||||
"""Test we handle errors."""
|
||||
exc, reason = error
|
||||
with patch(
|
||||
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
|
||||
side_effect=exc,
|
||||
):
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
data=DISCOVERY_INFO,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == reason
|
|
@ -0,0 +1,57 @@
|
|||
"""Test init of Nettigo Air Monitor integration."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from nettigo_air_monitor import ApiError
|
||||
|
||||
from homeassistant.components.nam.const import DOMAIN
|
||||
from homeassistant.config_entries import (
|
||||
ENTRY_STATE_LOADED,
|
||||
ENTRY_STATE_NOT_LOADED,
|
||||
ENTRY_STATE_SETUP_RETRY,
|
||||
)
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.nam import init_integration
|
||||
|
||||
|
||||
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")
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.state == "11"
|
||||
|
||||
|
||||
async def test_config_not_ready(hass):
|
||||
"""Test for setup failure if the connection to the device fails."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="10.10.2.3",
|
||||
unique_id="aa:bb:cc:dd:ee:ff",
|
||||
data={"host": "10.10.2.3"},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.nam.NettigoAirMonitor._async_get_data",
|
||||
side_effect=ApiError("API Error"),
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state == ENTRY_STATE_SETUP_RETRY
|
||||
|
||||
|
||||
async def test_unload_entry(hass):
|
||||
"""Test successful unload of entry."""
|
||||
entry = await init_integration(hass)
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ENTRY_STATE_NOT_LOADED
|
||||
assert not hass.data.get(DOMAIN)
|
|
@ -0,0 +1,266 @@
|
|||
"""Test sensor of Nettigo Air Monitor integration."""
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from nettigo_air_monitor import ApiError
|
||||
|
||||
from homeassistant.components.nam.const import DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
PERCENTAGE,
|
||||
PRESSURE_HPA,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
STATE_UNAVAILABLE,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
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_sensor(hass):
|
||||
"""Test states of the air_quality."""
|
||||
registry = er.async_get(hass)
|
||||
|
||||
registry.async_get_or_create(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
"aa:bb:cc:dd:ee:ff-signal",
|
||||
suggested_object_id="nettigo_air_monitor_signal_strength",
|
||||
disabled_by=None,
|
||||
)
|
||||
|
||||
await init_integration(hass)
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_bme280_humidity")
|
||||
assert state
|
||||
assert state.state == "45.7"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_bme280_humidity")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_humidity"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature")
|
||||
assert state
|
||||
assert state.state == "7.6"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_bme280_temperature")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_temperature"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_bme280_pressure")
|
||||
assert state
|
||||
assert state.state == "1011"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_bme280_pressure")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_pressure"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_bmp280_temperature")
|
||||
assert state
|
||||
assert state.state == "5.6"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_temperature")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp280_temperature"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_bmp280_pressure")
|
||||
assert state
|
||||
assert state.state == "1022"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_pressure")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp280_pressure"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_sht3x_humidity")
|
||||
assert state
|
||||
assert state.state == "34.7"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_humidity")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sht3x_humidity"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_sht3x_temperature")
|
||||
assert state
|
||||
assert state.state == "6.3"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_temperature")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sht3x_temperature"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_dht22_humidity")
|
||||
assert state
|
||||
assert state.state == "46.2"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-humidity"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_dht22_temperature")
|
||||
assert state
|
||||
assert state.state == "6.3"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-temperature"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_heca_humidity")
|
||||
assert state
|
||||
assert state.state == "50.0"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_heca_humidity")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-heca_humidity"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature")
|
||||
assert state
|
||||
assert state.state == "8.0"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_heca_temperature")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-heca_temperature"
|
||||
|
||||
state = hass.states.get("sensor.nettigo_air_monitor_signal_strength")
|
||||
assert state
|
||||
assert state.state == "-72"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH
|
||||
assert (
|
||||
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
== SIGNAL_STRENGTH_DECIBELS_MILLIWATT
|
||||
)
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_signal_strength")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-signal"
|
||||
|
||||
|
||||
async def test_sensor_disabled(hass):
|
||||
"""Test sensor disabled by default."""
|
||||
await init_integration(hass)
|
||||
registry = er.async_get(hass)
|
||||
|
||||
entry = registry.async_get("sensor.nettigo_air_monitor_signal_strength")
|
||||
assert entry
|
||||
assert entry.unique_id == "aa:bb:cc:dd:ee:ff-signal"
|
||||
assert entry.disabled
|
||||
assert entry.disabled_by == er.DISABLED_INTEGRATION
|
||||
|
||||
# Test enabling entity
|
||||
updated_entry = registry.async_update_entity(
|
||||
entry.entity_id, **{"disabled_by": None}
|
||||
)
|
||||
|
||||
assert updated_entry != entry
|
||||
assert updated_entry.disabled is False
|
||||
|
||||
|
||||
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("sensor.nettigo_air_monitor_heca_temperature")
|
||||
assert state
|
||||
assert state.state == "8.0"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
|
||||
|
||||
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("sensor.nettigo_air_monitor_heca_temperature")
|
||||
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("sensor.nettigo_air_monitor_bme280_temperature")
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.state == "7.6"
|
||||
|
||||
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("sensor.nettigo_air_monitor_bme280_temperature")
|
||||
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("sensor.nettigo_air_monitor_bme280_temperature")
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.state == "7.6"
|
||||
|
||||
|
||||
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: ["sensor.nettigo_air_monitor_bme280_temperature"]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_get_data.call_count == 1
|
Loading…
Reference in New Issue