mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Add airgradient integration (#114113)
This commit is contained in:
committed by
GitHub
parent
32061d4eb1
commit
6485973d9b
@ -48,6 +48,7 @@ homeassistant.components.adax.*
|
||||
homeassistant.components.adguard.*
|
||||
homeassistant.components.aftership.*
|
||||
homeassistant.components.air_quality.*
|
||||
homeassistant.components.airgradient.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.airnow.*
|
||||
homeassistant.components.airq.*
|
||||
|
@ -56,6 +56,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/agent_dvr/ @ispysoftware
|
||||
/homeassistant/components/air_quality/ @home-assistant/core
|
||||
/tests/components/air_quality/ @home-assistant/core
|
||||
/homeassistant/components/airgradient/ @airgradienthq @joostlek
|
||||
/tests/components/airgradient/ @airgradienthq @joostlek
|
||||
/homeassistant/components/airly/ @bieniu
|
||||
/tests/components/airly/ @bieniu
|
||||
/homeassistant/components/airnow/ @asymworks
|
||||
|
34
homeassistant/components/airgradient/__init__.py
Normal file
34
homeassistant/components/airgradient/__init__.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""The Airgradient integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirGradientDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Airgradient from a config entry."""
|
||||
|
||||
coordinator = AirGradientDataUpdateCoordinator(hass, entry.data[CONF_HOST])
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
83
homeassistant/components/airgradient/config_flow.py
Normal file
83
homeassistant/components/airgradient/config_flow.py
Normal file
@ -0,0 +1,83 @@
|
||||
"""Config flow for Airgradient."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from airgradient import AirGradientClient, AirGradientError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""AirGradient config flow."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self.data: dict[str, Any] = {}
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
self.data[CONF_HOST] = host = discovery_info.host
|
||||
self.data[CONF_MODEL] = discovery_info.properties["model"]
|
||||
|
||||
await self.async_set_unique_id(discovery_info.properties["serialno"])
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
air_gradient = AirGradientClient(host, session=session)
|
||||
await air_gradient.get_current_measures()
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
"model": self.data[CONF_MODEL],
|
||||
}
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=self.data[CONF_MODEL],
|
||||
data={CONF_HOST: self.data[CONF_HOST]},
|
||||
)
|
||||
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
description_placeholders={
|
||||
"model": self.data[CONF_MODEL],
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
session = async_get_clientsession(self.hass)
|
||||
air_gradient = AirGradientClient(user_input[CONF_HOST], session=session)
|
||||
try:
|
||||
current_measures = await air_gradient.get_current_measures()
|
||||
except AirGradientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(current_measures.serial_number)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=current_measures.model,
|
||||
data={CONF_HOST: user_input[CONF_HOST]},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
errors=errors,
|
||||
)
|
7
homeassistant/components/airgradient/const.py
Normal file
7
homeassistant/components/airgradient/const.py
Normal file
@ -0,0 +1,7 @@
|
||||
"""Constants for the Airgradient integration."""
|
||||
|
||||
import logging
|
||||
|
||||
DOMAIN = "airgradient"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
32
homeassistant/components/airgradient/coordinator.py
Normal file
32
homeassistant/components/airgradient/coordinator.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Define an object to manage fetching AirGradient data."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from airgradient import AirGradientClient, AirGradientError, Measures
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
|
||||
|
||||
class AirGradientDataUpdateCoordinator(DataUpdateCoordinator[Measures]):
|
||||
"""Class to manage fetching AirGradient data."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, host: str) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=LOGGER,
|
||||
name=f"AirGradient {host}",
|
||||
update_interval=timedelta(minutes=1),
|
||||
)
|
||||
session = async_get_clientsession(hass)
|
||||
self.client = AirGradientClient(host, session=session)
|
||||
|
||||
async def _async_update_data(self) -> Measures:
|
||||
try:
|
||||
return await self.client.get_current_measures()
|
||||
except AirGradientError as error:
|
||||
raise UpdateFailed(error) from error
|
15
homeassistant/components/airgradient/icons.json
Normal file
15
homeassistant/components/airgradient/icons.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"total_volatile_organic_component_index": {
|
||||
"default": "mdi:molecule"
|
||||
},
|
||||
"nitrogen_index": {
|
||||
"default": "mdi:molecule"
|
||||
},
|
||||
"pm003_count": {
|
||||
"default": "mdi:blur"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
homeassistant/components/airgradient/manifest.json
Normal file
11
homeassistant/components/airgradient/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "airgradient",
|
||||
"name": "Airgradient",
|
||||
"codeowners": ["@airgradienthq", "@joostlek"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airgradient==0.4.0"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
192
homeassistant/components/airgradient/sensor.py
Normal file
192
homeassistant/components/airgradient/sensor.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""Support for AirGradient sensors."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from airgradient.models import Measures
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AirGradientDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AirGradientSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes AirGradient sensor entity."""
|
||||
|
||||
value_fn: Callable[[Measures], StateType]
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = (
|
||||
AirGradientSensorEntityDescription(
|
||||
key="pm01",
|
||||
device_class=SensorDeviceClass.PM1,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.pm01,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="pm02",
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.pm02,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="pm10",
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.pm10,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.ambient_temperature,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.relative_humidity,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="signal_strength",
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda status: status.signal_strength,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="tvoc",
|
||||
translation_key="total_volatile_organic_component_index",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.total_volatile_organic_component_index,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="nitrogen_index",
|
||||
translation_key="nitrogen_index",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.nitrogen_index,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="co2",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.rco2,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="pm003",
|
||||
translation_key="pm003_count",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.pm003_count,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="nox_raw",
|
||||
translation_key="raw_nitrogen",
|
||||
native_unit_of_measurement="ticks",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda status: status.raw_nitrogen,
|
||||
),
|
||||
AirGradientSensorEntityDescription(
|
||||
key="tvoc_raw",
|
||||
translation_key="raw_total_volatile_organic_component",
|
||||
native_unit_of_measurement="ticks",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda status: status.raw_total_volatile_organic_component,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up AirGradient sensor entities based on a config entry."""
|
||||
|
||||
coordinator: AirGradientDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
listener: Callable[[], None] | None = None
|
||||
not_setup: set[AirGradientSensorEntityDescription] = set(SENSOR_TYPES)
|
||||
|
||||
@callback
|
||||
def add_entities() -> None:
|
||||
"""Add new entities based on the latest data."""
|
||||
nonlocal not_setup, listener
|
||||
sensor_descriptions = not_setup
|
||||
not_setup = set()
|
||||
sensors = []
|
||||
for description in sensor_descriptions:
|
||||
if description.value_fn(coordinator.data) is None:
|
||||
not_setup.add(description)
|
||||
else:
|
||||
sensors.append(AirGradientSensor(coordinator, description))
|
||||
|
||||
if sensors:
|
||||
async_add_entities(sensors)
|
||||
if not_setup:
|
||||
if not listener:
|
||||
listener = coordinator.async_add_listener(add_entities)
|
||||
elif listener:
|
||||
listener()
|
||||
|
||||
add_entities()
|
||||
|
||||
|
||||
class AirGradientSensor(
|
||||
CoordinatorEntity[AirGradientDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Defines an AirGradient sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
entity_description: AirGradientSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirGradientDataUpdateCoordinator,
|
||||
description: AirGradientSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize airgradient sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.data.serial_number)},
|
||||
model=coordinator.data.model,
|
||||
manufacturer="AirGradient",
|
||||
serial_number=coordinator.data.serial_number,
|
||||
sw_version=coordinator.data.firmware_version,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
44
homeassistant/components/airgradient/strings.json
Normal file
44
homeassistant/components/airgradient/strings.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{model}",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Airgradient device."
|
||||
}
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to setup {model}?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"total_volatile_organic_component_index": {
|
||||
"name": "Total VOC index"
|
||||
},
|
||||
"nitrogen_index": {
|
||||
"name": "Nitrogen index"
|
||||
},
|
||||
"pm003_count": {
|
||||
"name": "PM0.3 count"
|
||||
},
|
||||
"raw_total_volatile_organic_component": {
|
||||
"name": "Raw total VOC"
|
||||
},
|
||||
"raw_nitrogen": {
|
||||
"name": "Raw nitrogen"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ FLOWS = {
|
||||
"aemet",
|
||||
"aftership",
|
||||
"agent_dvr",
|
||||
"airgradient",
|
||||
"airly",
|
||||
"airnow",
|
||||
"airq",
|
||||
|
@ -93,6 +93,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"airgradient": {
|
||||
"name": "Airgradient",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"airly": {
|
||||
"name": "Airly",
|
||||
"integration_type": "service",
|
||||
|
@ -277,6 +277,11 @@ ZEROCONF = {
|
||||
"domain": "romy",
|
||||
},
|
||||
],
|
||||
"_airgradient._tcp.local.": [
|
||||
{
|
||||
"domain": "airgradient",
|
||||
},
|
||||
],
|
||||
"_airplay._tcp.local.": [
|
||||
{
|
||||
"domain": "apple_tv",
|
||||
|
10
mypy.ini
10
mypy.ini
@ -241,6 +241,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.airgradient.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.airly.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
@ -406,6 +406,9 @@ aiowithings==2.1.0
|
||||
# homeassistant.components.yandex_transport
|
||||
aioymaps==1.2.2
|
||||
|
||||
# homeassistant.components.airgradient
|
||||
airgradient==0.4.0
|
||||
|
||||
# homeassistant.components.airly
|
||||
airly==1.1.0
|
||||
|
||||
|
@ -379,6 +379,9 @@ aiowithings==2.1.0
|
||||
# homeassistant.components.yandex_transport
|
||||
aioymaps==1.2.2
|
||||
|
||||
# homeassistant.components.airgradient
|
||||
airgradient==0.4.0
|
||||
|
||||
# homeassistant.components.airly
|
||||
airly==1.1.0
|
||||
|
||||
|
13
tests/components/airgradient/__init__.py
Normal file
13
tests/components/airgradient/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Tests for the Airgradient integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
54
tests/components/airgradient/conftest.py
Normal file
54
tests/components/airgradient/conftest.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""AirGradient tests configuration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
from airgradient import Measures
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.airgradient.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
from tests.components.smhi.common import AsyncMock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.airgradient.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_airgradient_client() -> Generator[AsyncMock, None, None]:
|
||||
"""Mock an AirGradient client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.airgradient.coordinator.AirGradientClient",
|
||||
autospec=True,
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.airgradient.config_flow.AirGradientClient",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
client.get_current_measures.return_value = Measures.from_json(
|
||||
load_fixture("current_measures.json", DOMAIN)
|
||||
)
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Airgradient",
|
||||
data={CONF_HOST: "10.0.0.131"},
|
||||
unique_id="84fce612f5b8",
|
||||
)
|
19
tests/components/airgradient/fixtures/current_measures.json
Normal file
19
tests/components/airgradient/fixtures/current_measures.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"wifi": -52,
|
||||
"serialno": "84fce612f5b8",
|
||||
"rco2": 778,
|
||||
"pm01": 22,
|
||||
"pm02": 34,
|
||||
"pm10": 41,
|
||||
"pm003Count": 270,
|
||||
"tvocIndex": 99,
|
||||
"tvoc_raw": 31792,
|
||||
"noxIndex": 1,
|
||||
"nox_raw": 16931,
|
||||
"atmp": 27.96,
|
||||
"rhum": 48,
|
||||
"boot": 28,
|
||||
"ledMode": "co2",
|
||||
"firmwareVersion": "3.0.8",
|
||||
"fwMode": "I-9PSL"
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"wifi": -59,
|
||||
"serialno": "84fce612f5b8",
|
||||
"boot": 0,
|
||||
"ledMode": "co2",
|
||||
"firmwareVersion": "3.0.8",
|
||||
"fwMode": "I-9PSL"
|
||||
}
|
31
tests/components/airgradient/snapshots/test_init.ambr
Normal file
31
tests/components/airgradient/snapshots/test_init.ambr
Normal file
@ -0,0 +1,31 @@
|
||||
# serializer version: 1
|
||||
# name: test_device_info
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'airgradient',
|
||||
'84fce612f5b8',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'AirGradient',
|
||||
'model': 'I-9PSL',
|
||||
'name': 'Airgradient',
|
||||
'name_by_user': None,
|
||||
'serial_number': '84fce612f5b8',
|
||||
'suggested_area': None,
|
||||
'sw_version': '3.0.8',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
605
tests/components/airgradient/snapshots/test_sensor.ambr
Normal file
605
tests/components/airgradient/snapshots/test_sensor.ambr
Normal file
@ -0,0 +1,605 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[sensor.airgradient_carbon_dioxide-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_carbon_dioxide',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.CO2: 'carbon_dioxide'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Carbon dioxide',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '84fce612f5b8-co2',
|
||||
'unit_of_measurement': 'ppm',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_carbon_dioxide-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'carbon_dioxide',
|
||||
'friendly_name': 'Airgradient Carbon dioxide',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'ppm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_carbon_dioxide',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '778',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_humidity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '84fce612f5b8-humidity',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_humidity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': 'Airgradient Humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_humidity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '48.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_nitrogen_index-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_nitrogen_index',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Nitrogen index',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'nitrogen_index',
|
||||
'unique_id': '84fce612f5b8-nitrogen_index',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_nitrogen_index-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Airgradient Nitrogen index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_nitrogen_index',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_pm0_3_count-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_pm0_3_count',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'PM0.3 count',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'pm003_count',
|
||||
'unique_id': '84fce612f5b8-pm003',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_pm0_3_count-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Airgradient PM0.3 count',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_pm0_3_count',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '270',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_pm1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_pm1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PM1: 'pm1'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'PM1',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '84fce612f5b8-pm01',
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_pm1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'pm1',
|
||||
'friendly_name': 'Airgradient PM1',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_pm1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '22',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_pm10-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_pm10',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PM10: 'pm10'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'PM10',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '84fce612f5b8-pm10',
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_pm10-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'pm10',
|
||||
'friendly_name': 'Airgradient PM10',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_pm10',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '41',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_pm2_5-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_pm2_5',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PM25: 'pm25'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'PM2.5',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '84fce612f5b8-pm02',
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_pm2_5-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'pm25',
|
||||
'friendly_name': 'Airgradient PM2.5',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_pm2_5',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '34',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_raw_nitrogen-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_raw_nitrogen',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Raw nitrogen',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'raw_nitrogen',
|
||||
'unique_id': '84fce612f5b8-nox_raw',
|
||||
'unit_of_measurement': 'ticks',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_raw_nitrogen-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Airgradient Raw nitrogen',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'ticks',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_raw_nitrogen',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '16931',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_raw_total_voc-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_raw_total_voc',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Raw total VOC',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'raw_total_volatile_organic_component',
|
||||
'unique_id': '84fce612f5b8-tvoc_raw',
|
||||
'unit_of_measurement': 'ticks',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_raw_total_voc-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Airgradient Raw total VOC',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'ticks',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_raw_total_voc',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '31792',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_signal_strength-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.airgradient_signal_strength',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Signal strength',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '84fce612f5b8-signal_strength',
|
||||
'unit_of_measurement': 'dBm',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_signal_strength-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'signal_strength',
|
||||
'friendly_name': 'Airgradient Signal strength',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'dBm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_signal_strength',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-52',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '84fce612f5b8-temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Airgradient Temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '27.96',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_total_voc_index-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.airgradient_total_voc_index',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Total VOC index',
|
||||
'platform': 'airgradient',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'total_volatile_organic_component_index',
|
||||
'unique_id': '84fce612f5b8-tvoc',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.airgradient_total_voc_index-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Airgradient Total VOC index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.airgradient_total_voc_index',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '99',
|
||||
})
|
||||
# ---
|
149
tests/components/airgradient/test_config_flow.py
Normal file
149
tests/components/airgradient/test_config_flow.py
Normal file
@ -0,0 +1,149 @@
|
||||
"""Tests for the AirGradient config flow."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from airgradient import AirGradientConnectionError
|
||||
|
||||
from homeassistant.components.airgradient import DOMAIN
|
||||
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ZEROCONF_DISCOVERY = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("10.0.0.131"),
|
||||
ip_addresses=[ip_address("10.0.0.131")],
|
||||
hostname="airgradient_84fce612f5b8.local.",
|
||||
name="airgradient_84fce612f5b8._airgradient._tcp.local.",
|
||||
port=80,
|
||||
type="_airgradient._tcp.local.",
|
||||
properties={
|
||||
"vendor": "AirGradient",
|
||||
"fw_ver": "3.0.8",
|
||||
"serialno": "84fce612f5b8",
|
||||
"model": "I-9PSL",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_full_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_airgradient_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test full flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "10.0.0.131"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "I-9PSL"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "10.0.0.131",
|
||||
}
|
||||
assert result["result"].unique_id == "84fce612f5b8"
|
||||
|
||||
|
||||
async def test_flow_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_airgradient_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test flow errors."""
|
||||
mock_airgradient_client.get_current_measures.side_effect = (
|
||||
AirGradientConnectionError()
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "10.0.0.131"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
mock_airgradient_client.get_current_measures.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "10.0.0.131"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_duplicate(
|
||||
hass: HomeAssistant,
|
||||
mock_airgradient_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test duplicate flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "10.0.0.131"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_airgradient_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test zeroconf flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "I-9PSL"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "10.0.0.131",
|
||||
}
|
||||
assert result["result"].unique_id == "84fce612f5b8"
|
28
tests/components/airgradient/test_init.py
Normal file
28
tests/components/airgradient/test_init.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""Tests for the AirGradient integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.airgradient import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.airgradient import setup_integration
|
||||
|
||||
|
||||
async def test_device_info(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_airgradient_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test device registry integration."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, mock_config_entry.unique_id)}
|
||||
)
|
||||
assert device_entry is not None
|
||||
assert device_entry == snapshot
|
76
tests/components/airgradient/test_sensor.py
Normal file
76
tests/components/airgradient/test_sensor.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""Tests for the AirGradient sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from airgradient import AirGradientError, Measures
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.airgradient import DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_fire_time_changed,
|
||||
load_fixture,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_airgradient_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_create_entities(
|
||||
hass: HomeAssistant,
|
||||
mock_airgradient_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test creating entities."""
|
||||
mock_airgradient_client.get_current_measures.return_value = Measures.from_json(
|
||||
load_fixture("measures_after_boot.json", DOMAIN)
|
||||
)
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert len(hass.states.async_all()) == 0
|
||||
mock_airgradient_client.get_current_measures.return_value = Measures.from_json(
|
||||
load_fixture("current_measures.json", DOMAIN)
|
||||
)
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all()) == 9
|
||||
|
||||
|
||||
async def test_connection_error(
|
||||
hass: HomeAssistant,
|
||||
mock_airgradient_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test connection error."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_airgradient_client.get_current_measures.side_effect = AirGradientError()
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.airgradient_humidity").state == STATE_UNAVAILABLE
|
Reference in New Issue
Block a user