Add config flow to WAQI (#98220)
* Migrate WAQI to aiowaqi library * Migrate WAQI to aiowaqi library * Migrate WAQI to aiowaqi library * Add config flow to WAQI * Finish config flow * Add tests * Add tests * Fix ruff * Add issues on failing to import * Add issues on failing to import * Add issues on failing to import * Add importing issue * Finish coverage * Remove url from translation string * Fix feedback * Fix feedback
This commit is contained in:
parent
fdddbd7363
commit
9be16d9d42
17 changed files with 822 additions and 76 deletions
|
@ -1391,7 +1391,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/wake_word/ @home-assistant/core @synesthesiam
|
/tests/components/wake_word/ @home-assistant/core @synesthesiam
|
||||||
/homeassistant/components/wallbox/ @hesselonline
|
/homeassistant/components/wallbox/ @hesselonline
|
||||||
/tests/components/wallbox/ @hesselonline
|
/tests/components/wallbox/ @hesselonline
|
||||||
/homeassistant/components/waqi/ @andrey-git
|
/homeassistant/components/waqi/ @joostlek
|
||||||
|
/tests/components/waqi/ @joostlek
|
||||||
/homeassistant/components/water_heater/ @home-assistant/core
|
/homeassistant/components/water_heater/ @home-assistant/core
|
||||||
/tests/components/water_heater/ @home-assistant/core
|
/tests/components/water_heater/ @home-assistant/core
|
||||||
/homeassistant/components/watson_tts/ @rutkai
|
/homeassistant/components/watson_tts/ @rutkai
|
||||||
|
|
|
@ -1 +1,37 @@
|
||||||
"""The waqi component."""
|
"""The World Air Quality Index (WAQI) integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from aiowaqi import WAQIClient
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_API_KEY, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import WAQIDataUpdateCoordinator
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up World Air Quality Index (WAQI) from a config entry."""
|
||||||
|
|
||||||
|
client = WAQIClient(session=async_get_clientsession(hass))
|
||||||
|
client.authenticate(entry.data[CONF_API_KEY])
|
||||||
|
|
||||||
|
waqi_coordinator = WAQIDataUpdateCoordinator(hass, client)
|
||||||
|
await waqi_coordinator.async_config_entry_first_refresh()
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = waqi_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
|
||||||
|
|
135
homeassistant/components/waqi/config_flow.py
Normal file
135
homeassistant/components/waqi/config_flow.py
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
"""Config flow for World Air Quality Index (WAQI) integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiowaqi import (
|
||||||
|
WAQIAirQuality,
|
||||||
|
WAQIAuthenticationError,
|
||||||
|
WAQIClient,
|
||||||
|
WAQIConnectionError,
|
||||||
|
)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_LATITUDE,
|
||||||
|
CONF_LOCATION,
|
||||||
|
CONF_LONGITUDE,
|
||||||
|
CONF_NAME,
|
||||||
|
)
|
||||||
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||||
|
from homeassistant.data_entry_flow import AbortFlow, FlowResult
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
|
from homeassistant.helpers.selector import LocationSelector
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import CONF_STATION_NUMBER, DOMAIN, ISSUE_PLACEHOLDER
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WAQIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for World Air Quality Index (WAQI)."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
async with WAQIClient(
|
||||||
|
session=async_get_clientsession(self.hass)
|
||||||
|
) as waqi_client:
|
||||||
|
waqi_client.authenticate(user_input[CONF_API_KEY])
|
||||||
|
location = user_input[CONF_LOCATION]
|
||||||
|
try:
|
||||||
|
measuring_station: WAQIAirQuality = (
|
||||||
|
await waqi_client.get_by_coordinates(
|
||||||
|
location[CONF_LATITUDE], location[CONF_LONGITUDE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except WAQIAuthenticationError:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except WAQIConnectionError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception(exc)
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(str(measuring_station.station_id))
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=measuring_station.city.name,
|
||||||
|
data={
|
||||||
|
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||||
|
CONF_STATION_NUMBER: measuring_station.station_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
vol.Required(
|
||||||
|
CONF_LOCATION,
|
||||||
|
): LocationSelector(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
user_input
|
||||||
|
or {
|
||||||
|
CONF_LOCATION: {
|
||||||
|
CONF_LATITUDE: self.hass.config.latitude,
|
||||||
|
CONF_LONGITUDE: self.hass.config.longitude,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, import_config: ConfigType) -> FlowResult:
|
||||||
|
"""Handle importing from yaml."""
|
||||||
|
await self.async_set_unique_id(str(import_config[CONF_STATION_NUMBER]))
|
||||||
|
try:
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
except AbortFlow as exc:
|
||||||
|
async_create_issue(
|
||||||
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
"deprecated_yaml_import_issue_already_configured",
|
||||||
|
breaks_in_ha_version="2024.4.0",
|
||||||
|
is_fixable=False,
|
||||||
|
severity=IssueSeverity.ERROR,
|
||||||
|
translation_key="deprecated_yaml_import_issue_already_configured",
|
||||||
|
translation_placeholders=ISSUE_PLACEHOLDER,
|
||||||
|
)
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
async_create_issue(
|
||||||
|
self.hass,
|
||||||
|
HOMEASSISTANT_DOMAIN,
|
||||||
|
f"deprecated_yaml_{DOMAIN}",
|
||||||
|
breaks_in_ha_version="2024.4.0",
|
||||||
|
is_fixable=False,
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_yaml",
|
||||||
|
translation_placeholders={
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"integration_title": "World Air Quality Index",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=import_config[CONF_NAME],
|
||||||
|
data={
|
||||||
|
CONF_API_KEY: import_config[CONF_API_KEY],
|
||||||
|
CONF_STATION_NUMBER: import_config[CONF_STATION_NUMBER],
|
||||||
|
},
|
||||||
|
)
|
10
homeassistant/components/waqi/const.py
Normal file
10
homeassistant/components/waqi/const.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
"""Constants for the World Air Quality Index (WAQI) integration."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
DOMAIN = "waqi"
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
CONF_STATION_NUMBER = "station_number"
|
||||||
|
|
||||||
|
ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=waqi"}
|
36
homeassistant/components/waqi/coordinator.py
Normal file
36
homeassistant/components/waqi/coordinator.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""Coordinator for the World Air Quality Index (WAQI) integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from aiowaqi import WAQIAirQuality, WAQIClient, WAQIError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import CONF_STATION_NUMBER, DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
class WAQIDataUpdateCoordinator(DataUpdateCoordinator[WAQIAirQuality]):
|
||||||
|
"""The WAQI Data Update Coordinator."""
|
||||||
|
|
||||||
|
config_entry: ConfigEntry
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, client: WAQIClient) -> None:
|
||||||
|
"""Initialize the WAQI data coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(minutes=5),
|
||||||
|
)
|
||||||
|
self._client = client
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> WAQIAirQuality:
|
||||||
|
try:
|
||||||
|
return await self._client.get_by_station_number(
|
||||||
|
self.config_entry.data[CONF_STATION_NUMBER]
|
||||||
|
)
|
||||||
|
except WAQIError as exc:
|
||||||
|
raise UpdateFailed from exc
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"domain": "waqi",
|
"domain": "waqi",
|
||||||
"name": "World Air Quality Index (WAQI)",
|
"name": "World Air Quality Index (WAQI)",
|
||||||
"codeowners": ["@andrey-git"],
|
"codeowners": ["@joostlek"],
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/waqi",
|
"documentation": "https://www.home-assistant.io/integrations/waqi",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["waqiasync"],
|
"loggers": ["waqiasync"],
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
"""Support for the World Air Quality Index service."""
|
"""Support for the World Air Quality Index service."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiowaqi import WAQIAirQuality, WAQIClient, WAQIConnectionError, WAQISearchResult
|
from aiowaqi import WAQIAuthenticationError, WAQIClient, WAQIConnectionError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
|
@ -12,10 +11,13 @@ from homeassistant.components.sensor import (
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION,
|
ATTR_ATTRIBUTION,
|
||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
ATTR_TIME,
|
ATTR_TIME,
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_NAME,
|
||||||
CONF_TOKEN,
|
CONF_TOKEN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
@ -23,7 +25,12 @@ from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import CONF_STATION_NUMBER, DOMAIN, ISSUE_PLACEHOLDER
|
||||||
|
from .coordinator import WAQIDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -43,8 +50,6 @@ ATTR_ICON = "mdi:cloud"
|
||||||
CONF_LOCATIONS = "locations"
|
CONF_LOCATIONS = "locations"
|
||||||
CONF_STATIONS = "stations"
|
CONF_STATIONS = "stations"
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=5)
|
|
||||||
|
|
||||||
TIMEOUT = 10
|
TIMEOUT = 10
|
||||||
|
|
||||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
|
||||||
|
@ -70,102 +75,126 @@ async def async_setup_platform(
|
||||||
|
|
||||||
client = WAQIClient(session=async_get_clientsession(hass), request_timeout=TIMEOUT)
|
client = WAQIClient(session=async_get_clientsession(hass), request_timeout=TIMEOUT)
|
||||||
client.authenticate(token)
|
client.authenticate(token)
|
||||||
dev = []
|
station_count = 0
|
||||||
try:
|
try:
|
||||||
for location_name in locations:
|
for location_name in locations:
|
||||||
stations = await client.search(location_name)
|
stations = await client.search(location_name)
|
||||||
_LOGGER.debug("The following stations were returned: %s", stations)
|
_LOGGER.debug("The following stations were returned: %s", stations)
|
||||||
for station in stations:
|
for station in stations:
|
||||||
waqi_sensor = WaqiSensor(client, station)
|
station_count = station_count + 1
|
||||||
if not station_filter or {
|
if not station_filter or {
|
||||||
waqi_sensor.uid,
|
station.station_id,
|
||||||
waqi_sensor.url,
|
station.station.external_url,
|
||||||
waqi_sensor.station_name,
|
station.station.name,
|
||||||
} & set(station_filter):
|
} & set(station_filter):
|
||||||
dev.append(waqi_sensor)
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data={
|
||||||
|
CONF_STATION_NUMBER: station.station_id,
|
||||||
|
CONF_NAME: station.station.name,
|
||||||
|
CONF_API_KEY: config[CONF_TOKEN],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except WAQIAuthenticationError as err:
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"deprecated_yaml_import_issue_invalid_auth",
|
||||||
|
breaks_in_ha_version="2024.4.0",
|
||||||
|
is_fixable=False,
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_yaml_import_issue_invalid_auth",
|
||||||
|
translation_placeholders=ISSUE_PLACEHOLDER,
|
||||||
|
)
|
||||||
|
_LOGGER.exception("Could not authenticate with WAQI")
|
||||||
|
raise PlatformNotReady from err
|
||||||
except WAQIConnectionError as err:
|
except WAQIConnectionError as err:
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"deprecated_yaml_import_issue_cannot_connect",
|
||||||
|
breaks_in_ha_version="2024.4.0",
|
||||||
|
is_fixable=False,
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_yaml_import_issue_cannot_connect",
|
||||||
|
translation_placeholders=ISSUE_PLACEHOLDER,
|
||||||
|
)
|
||||||
_LOGGER.exception("Failed to connect to WAQI servers")
|
_LOGGER.exception("Failed to connect to WAQI servers")
|
||||||
raise PlatformNotReady from err
|
raise PlatformNotReady from err
|
||||||
async_add_entities(dev, True)
|
if station_count == 0:
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"deprecated_yaml_import_issue_none_found",
|
||||||
|
breaks_in_ha_version="2024.4.0",
|
||||||
|
is_fixable=False,
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_yaml_import_issue_none_found",
|
||||||
|
translation_placeholders=ISSUE_PLACEHOLDER,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WaqiSensor(SensorEntity):
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up the WAQI sensor."""
|
||||||
|
coordinator: WAQIDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
async_add_entities([WaqiSensor(coordinator)])
|
||||||
|
|
||||||
|
|
||||||
|
class WaqiSensor(CoordinatorEntity[WAQIDataUpdateCoordinator], SensorEntity):
|
||||||
"""Implementation of a WAQI sensor."""
|
"""Implementation of a WAQI sensor."""
|
||||||
|
|
||||||
_attr_icon = ATTR_ICON
|
_attr_icon = ATTR_ICON
|
||||||
_attr_device_class = SensorDeviceClass.AQI
|
_attr_device_class = SensorDeviceClass.AQI
|
||||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||||
|
|
||||||
_data: WAQIAirQuality | None = None
|
def __init__(self, coordinator: WAQIDataUpdateCoordinator) -> None:
|
||||||
|
|
||||||
def __init__(self, client: WAQIClient, search_result: WAQISearchResult) -> None:
|
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._client = client
|
super().__init__(coordinator)
|
||||||
self.uid = search_result.station_id
|
self._attr_name = f"WAQI {self.coordinator.data.city.name}"
|
||||||
self.url = search_result.station.external_url
|
self._attr_unique_id = str(coordinator.data.station_id)
|
||||||
self.station_name = search_result.station.name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
"""Return the name of the sensor."""
|
|
||||||
if self.station_name:
|
|
||||||
return f"WAQI {self.station_name}"
|
|
||||||
return f"WAQI {self.url if self.url else self.uid}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> int | None:
|
def native_value(self) -> int | None:
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
assert self._data
|
return self.coordinator.data.air_quality_index
|
||||||
return self._data.air_quality_index
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self):
|
|
||||||
"""Return sensor availability."""
|
|
||||||
return self._data is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
"""Return unique ID."""
|
|
||||||
return self.uid
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self):
|
def extra_state_attributes(self):
|
||||||
"""Return the state attributes of the last update."""
|
"""Return the state attributes of the last update."""
|
||||||
attrs = {}
|
attrs = {}
|
||||||
|
try:
|
||||||
|
attrs[ATTR_ATTRIBUTION] = " and ".join(
|
||||||
|
[ATTRIBUTION]
|
||||||
|
+ [
|
||||||
|
attribution.name
|
||||||
|
for attribution in self.coordinator.data.attributions
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
if self._data is not None:
|
attrs[ATTR_TIME] = self.coordinator.data.measured_at
|
||||||
try:
|
attrs[ATTR_DOMINENTPOL] = self.coordinator.data.dominant_pollutant
|
||||||
attrs[ATTR_ATTRIBUTION] = " and ".join(
|
|
||||||
[ATTRIBUTION]
|
|
||||||
+ [attribution.name for attribution in self._data.attributions]
|
|
||||||
)
|
|
||||||
|
|
||||||
attrs[ATTR_TIME] = self._data.measured_at
|
iaqi = self.coordinator.data.extended_air_quality
|
||||||
attrs[ATTR_DOMINENTPOL] = self._data.dominant_pollutant
|
|
||||||
|
|
||||||
iaqi = self._data.extended_air_quality
|
attribute = {
|
||||||
|
ATTR_PM2_5: iaqi.pm25,
|
||||||
attribute = {
|
ATTR_PM10: iaqi.pm10,
|
||||||
ATTR_PM2_5: iaqi.pm25,
|
ATTR_HUMIDITY: iaqi.humidity,
|
||||||
ATTR_PM10: iaqi.pm10,
|
ATTR_PRESSURE: iaqi.pressure,
|
||||||
ATTR_HUMIDITY: iaqi.humidity,
|
ATTR_TEMPERATURE: iaqi.temperature,
|
||||||
ATTR_PRESSURE: iaqi.pressure,
|
ATTR_OZONE: iaqi.ozone,
|
||||||
ATTR_TEMPERATURE: iaqi.temperature,
|
ATTR_NITROGEN_DIOXIDE: iaqi.nitrogen_dioxide,
|
||||||
ATTR_OZONE: iaqi.ozone,
|
ATTR_SULFUR_DIOXIDE: iaqi.sulfur_dioxide,
|
||||||
ATTR_NITROGEN_DIOXIDE: iaqi.nitrogen_dioxide,
|
}
|
||||||
ATTR_SULFUR_DIOXIDE: iaqi.sulfur_dioxide,
|
res_attributes = {k: v for k, v in attribute.items() if v is not None}
|
||||||
}
|
return {**attrs, **res_attributes}
|
||||||
res_attributes = {k: v for k, v in attribute.items() if v is not None}
|
except (IndexError, KeyError):
|
||||||
return {**attrs, **res_attributes}
|
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||||
except (IndexError, KeyError):
|
|
||||||
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
|
||||||
"""Get the latest data and updates the states."""
|
|
||||||
if self.uid:
|
|
||||||
result = await self._client.get_by_station_number(self.uid)
|
|
||||||
elif self.url:
|
|
||||||
result = await self._client.get_by_name(self.url)
|
|
||||||
else:
|
|
||||||
result = None
|
|
||||||
self._data = result
|
|
||||||
|
|
39
homeassistant/components/waqi/strings.json
Normal file
39
homeassistant/components/waqi/strings.json
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"description": "Select a location to get the closest measuring station.",
|
||||||
|
"data": {
|
||||||
|
"location": "[%key:common::config_flow::data::location%]",
|
||||||
|
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"issues": {
|
||||||
|
"deprecated_yaml_import_issue_invalid_auth": {
|
||||||
|
"title": "The World Air Quality Index YAML configuration import failed",
|
||||||
|
"description": "Configuring World Air Quality Index using YAML is being removed but there was an authentication error importing your YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the World Air Quality Index YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||||
|
},
|
||||||
|
"deprecated_yaml_import_issue_cannot_connect": {
|
||||||
|
"title": "The WAQI YAML configuration import failed",
|
||||||
|
"description": "Configuring World Air Quality Index using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to WAQI works and restart Home Assistant to try again or remove the World Air Quality Index YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||||
|
},
|
||||||
|
"deprecated_yaml_import_issue_already_configured": {
|
||||||
|
"title": "The WAQI YAML configuration import failed",
|
||||||
|
"description": "Configuring World Air Quality Index using YAML is being removed but the measuring station was already imported when trying to import the YAML configuration.\n\nEnsure the imported configuration is correct and remove the World Air Quality Index YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||||
|
},
|
||||||
|
"deprecated_yaml_import_issue_none_found": {
|
||||||
|
"title": "The WAQI YAML configuration import failed",
|
||||||
|
"description": "Configuring World Air Quality Index using YAML is being removed but there weren't any stations imported because they couldn't be found.\n\nEnsure the imported configuration is correct and remove the World Air Quality Index YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -516,6 +516,7 @@ FLOWS = {
|
||||||
"volvooncall",
|
"volvooncall",
|
||||||
"vulcan",
|
"vulcan",
|
||||||
"wallbox",
|
"wallbox",
|
||||||
|
"waqi",
|
||||||
"watttime",
|
"watttime",
|
||||||
"waze_travel_time",
|
"waze_travel_time",
|
||||||
"webostv",
|
"webostv",
|
||||||
|
|
|
@ -6288,7 +6288,7 @@
|
||||||
"waqi": {
|
"waqi": {
|
||||||
"name": "World Air Quality Index (WAQI)",
|
"name": "World Air Quality Index (WAQI)",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"config_flow": false,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
"waterfurnace": {
|
"waterfurnace": {
|
||||||
|
|
|
@ -347,6 +347,9 @@ aiovlc==0.1.0
|
||||||
# homeassistant.components.vodafone_station
|
# homeassistant.components.vodafone_station
|
||||||
aiovodafone==0.1.0
|
aiovodafone==0.1.0
|
||||||
|
|
||||||
|
# homeassistant.components.waqi
|
||||||
|
aiowaqi==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.watttime
|
# homeassistant.components.watttime
|
||||||
aiowatttime==0.1.1
|
aiowatttime==0.1.1
|
||||||
|
|
||||||
|
|
1
tests/components/waqi/__init__.py
Normal file
1
tests/components/waqi/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the World Air Quality Index (WAQI) integration."""
|
30
tests/components/waqi/conftest.py
Normal file
30
tests/components/waqi/conftest.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"""Common fixtures for the World Air Quality Index (WAQI) tests."""
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.waqi.const import CONF_STATION_NUMBER, DOMAIN
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.waqi.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Mock config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id="4584",
|
||||||
|
title="de Jongweg, Utrecht",
|
||||||
|
data={CONF_API_KEY: "asd", CONF_STATION_NUMBER: 4584},
|
||||||
|
)
|
160
tests/components/waqi/fixtures/air_quality_sensor.json
Normal file
160
tests/components/waqi/fixtures/air_quality_sensor.json
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
{
|
||||||
|
"aqi": 29,
|
||||||
|
"idx": 4584,
|
||||||
|
"attributions": [
|
||||||
|
{
|
||||||
|
"url": "http://www.luchtmeetnet.nl/",
|
||||||
|
"name": "RIVM - Rijksinstituut voor Volksgezondheid en Milieum, Landelijk Meetnet Luchtkwaliteit",
|
||||||
|
"logo": "Netherland-RIVM.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://waqi.info/",
|
||||||
|
"name": "World Air Quality Index Project"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"city": {
|
||||||
|
"geo": [52.105031, 5.124464],
|
||||||
|
"name": "de Jongweg, Utrecht",
|
||||||
|
"url": "https://aqicn.org/city/netherland/utrecht/de-jongweg",
|
||||||
|
"location": ""
|
||||||
|
},
|
||||||
|
"dominentpol": "o3",
|
||||||
|
"iaqi": {
|
||||||
|
"h": {
|
||||||
|
"v": 80
|
||||||
|
},
|
||||||
|
"no2": {
|
||||||
|
"v": 2.3
|
||||||
|
},
|
||||||
|
"o3": {
|
||||||
|
"v": 29.4
|
||||||
|
},
|
||||||
|
"p": {
|
||||||
|
"v": 1008.8
|
||||||
|
},
|
||||||
|
"pm10": {
|
||||||
|
"v": 12
|
||||||
|
},
|
||||||
|
"pm25": {
|
||||||
|
"v": 17
|
||||||
|
},
|
||||||
|
"t": {
|
||||||
|
"v": 16
|
||||||
|
},
|
||||||
|
"w": {
|
||||||
|
"v": 1.4
|
||||||
|
},
|
||||||
|
"wg": {
|
||||||
|
"v": 2.4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"s": "2023-08-07 17:00:00",
|
||||||
|
"tz": "+02:00",
|
||||||
|
"v": 1691427600,
|
||||||
|
"iso": "2023-08-07T17:00:00+02:00"
|
||||||
|
},
|
||||||
|
"forecast": {
|
||||||
|
"daily": {
|
||||||
|
"o3": [
|
||||||
|
{
|
||||||
|
"avg": 28,
|
||||||
|
"day": "2023-08-07",
|
||||||
|
"max": 34,
|
||||||
|
"min": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 22,
|
||||||
|
"day": "2023-08-08",
|
||||||
|
"max": 29,
|
||||||
|
"min": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 23,
|
||||||
|
"day": "2023-08-09",
|
||||||
|
"max": 35,
|
||||||
|
"min": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 18,
|
||||||
|
"day": "2023-08-10",
|
||||||
|
"max": 38,
|
||||||
|
"min": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 17,
|
||||||
|
"day": "2023-08-11",
|
||||||
|
"max": 17,
|
||||||
|
"min": 11
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pm10": [
|
||||||
|
{
|
||||||
|
"avg": 8,
|
||||||
|
"day": "2023-08-07",
|
||||||
|
"max": 10,
|
||||||
|
"min": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 9,
|
||||||
|
"day": "2023-08-08",
|
||||||
|
"max": 12,
|
||||||
|
"min": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 9,
|
||||||
|
"day": "2023-08-09",
|
||||||
|
"max": 13,
|
||||||
|
"min": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 23,
|
||||||
|
"day": "2023-08-10",
|
||||||
|
"max": 33,
|
||||||
|
"min": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 27,
|
||||||
|
"day": "2023-08-11",
|
||||||
|
"max": 34,
|
||||||
|
"min": 27
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pm25": [
|
||||||
|
{
|
||||||
|
"avg": 19,
|
||||||
|
"day": "2023-08-07",
|
||||||
|
"max": 29,
|
||||||
|
"min": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 25,
|
||||||
|
"day": "2023-08-08",
|
||||||
|
"max": 37,
|
||||||
|
"min": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 27,
|
||||||
|
"day": "2023-08-09",
|
||||||
|
"max": 45,
|
||||||
|
"min": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 64,
|
||||||
|
"day": "2023-08-10",
|
||||||
|
"max": 86,
|
||||||
|
"min": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avg": 72,
|
||||||
|
"day": "2023-08-11",
|
||||||
|
"max": 89,
|
||||||
|
"min": 72
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"sync": "2023-08-08T01:29:52+09:00"
|
||||||
|
}
|
||||||
|
}
|
32
tests/components/waqi/fixtures/search_result.json
Normal file
32
tests/components/waqi/fixtures/search_result.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"uid": 6332,
|
||||||
|
"aqi": "27",
|
||||||
|
"time": {
|
||||||
|
"tz": "+02:00",
|
||||||
|
"stime": "2023-08-08 15:00:00",
|
||||||
|
"vtime": 1691499600
|
||||||
|
},
|
||||||
|
"station": {
|
||||||
|
"name": "Griftpark, Utrecht",
|
||||||
|
"geo": [52.101308, 5.128183],
|
||||||
|
"url": "netherland/utrecht/griftpark",
|
||||||
|
"country": "NL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": 4584,
|
||||||
|
"aqi": "27",
|
||||||
|
"time": {
|
||||||
|
"tz": "+02:00",
|
||||||
|
"stime": "2023-08-08 15:00:00",
|
||||||
|
"vtime": 1691499600
|
||||||
|
},
|
||||||
|
"station": {
|
||||||
|
"name": "de Jongweg, Utrecht",
|
||||||
|
"geo": [52.105031, 5.124464],
|
||||||
|
"url": "netherland/utrecht/de-jongweg",
|
||||||
|
"country": "NL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
108
tests/components/waqi/test_config_flow.py
Normal file
108
tests/components/waqi/test_config_flow.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
"""Test the World Air Quality Index (WAQI) config flow."""
|
||||||
|
import json
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from aiowaqi import WAQIAirQuality, WAQIAuthenticationError, WAQIConnectionError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.waqi.const import CONF_STATION_NUMBER, DOMAIN
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_LATITUDE,
|
||||||
|
CONF_LOCATION,
|
||||||
|
CONF_LONGITUDE,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from tests.common import load_fixture
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||||
|
"""Test we get the form."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"aiowaqi.WAQIClient.authenticate",
|
||||||
|
), patch(
|
||||||
|
"aiowaqi.WAQIClient.get_by_coordinates",
|
||||||
|
return_value=WAQIAirQuality.parse_obj(
|
||||||
|
json.loads(load_fixture("waqi/air_quality_sensor.json"))
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_LOCATION: {CONF_LATITUDE: 50.0, CONF_LONGITUDE: 10.0},
|
||||||
|
CONF_API_KEY: "asd",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "de Jongweg, Utrecht"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_API_KEY: "asd",
|
||||||
|
CONF_STATION_NUMBER: 4584,
|
||||||
|
}
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception", "error"),
|
||||||
|
[
|
||||||
|
(WAQIAuthenticationError(), "invalid_auth"),
|
||||||
|
(WAQIConnectionError(), "cannot_connect"),
|
||||||
|
(Exception(), "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_flow_errors(
|
||||||
|
hass: HomeAssistant, exception: Exception, error: str
|
||||||
|
) -> None:
|
||||||
|
"""Test we handle errors during configuration."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"aiowaqi.WAQIClient.authenticate",
|
||||||
|
), patch(
|
||||||
|
"aiowaqi.WAQIClient.get_by_coordinates",
|
||||||
|
side_effect=exception,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_LOCATION: {CONF_LATITUDE: 50.0, CONF_LONGITUDE: 10.0},
|
||||||
|
CONF_API_KEY: "asd",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": error}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"aiowaqi.WAQIClient.authenticate",
|
||||||
|
), patch(
|
||||||
|
"aiowaqi.WAQIClient.get_by_coordinates",
|
||||||
|
return_value=WAQIAirQuality.parse_obj(
|
||||||
|
json.loads(load_fixture("waqi/air_quality_sensor.json"))
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_LOCATION: {CONF_LATITUDE: 50.0, CONF_LONGITUDE: 10.0},
|
||||||
|
CONF_API_KEY: "asd",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
124
tests/components/waqi/test_sensor.py
Normal file
124
tests/components/waqi/test_sensor.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
"""Test the World Air Quality Index (WAQI) sensor."""
|
||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from aiowaqi import WAQIAirQuality, WAQIError, WAQISearchResult
|
||||||
|
|
||||||
|
from homeassistant.components.waqi.const import CONF_STATION_NUMBER, DOMAIN
|
||||||
|
from homeassistant.components.waqi.sensor import CONF_LOCATIONS, CONF_STATIONS
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntryState
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
CONF_TOKEN,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import issue_registry as ir
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
LEGACY_CONFIG = {
|
||||||
|
Platform.SENSOR: [
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: DOMAIN,
|
||||||
|
CONF_TOKEN: "asd",
|
||||||
|
CONF_LOCATIONS: ["utrecht"],
|
||||||
|
CONF_STATIONS: [6332],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_legacy_migration(hass: HomeAssistant) -> None:
|
||||||
|
"""Test migration from yaml to config flow."""
|
||||||
|
search_result_json = json.loads(load_fixture("waqi/search_result.json"))
|
||||||
|
search_results = [
|
||||||
|
WAQISearchResult.parse_obj(search_result)
|
||||||
|
for search_result in search_result_json
|
||||||
|
]
|
||||||
|
with patch(
|
||||||
|
"aiowaqi.WAQIClient.search",
|
||||||
|
return_value=search_results,
|
||||||
|
), patch(
|
||||||
|
"aiowaqi.WAQIClient.get_by_station_number",
|
||||||
|
return_value=WAQIAirQuality.parse_obj(
|
||||||
|
json.loads(load_fixture("waqi/air_quality_sensor.json"))
|
||||||
|
),
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, Platform.SENSOR, LEGACY_CONFIG)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0].state is ConfigEntryState.LOADED
|
||||||
|
issue_registry = ir.async_get(hass)
|
||||||
|
assert len(issue_registry.issues) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_legacy_migration_already_imported(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test migration from yaml to config flow after already imported."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
with patch(
|
||||||
|
"aiowaqi.WAQIClient.get_by_station_number",
|
||||||
|
return_value=WAQIAirQuality.parse_obj(
|
||||||
|
json.loads(load_fixture("waqi/air_quality_sensor.json"))
|
||||||
|
),
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.waqi_de_jongweg_utrecht")
|
||||||
|
assert state.state == "29"
|
||||||
|
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data={
|
||||||
|
CONF_STATION_NUMBER: 4584,
|
||||||
|
CONF_NAME: "xyz",
|
||||||
|
CONF_API_KEY: "asd",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0].state is ConfigEntryState.LOADED
|
||||||
|
issue_registry = ir.async_get(hass)
|
||||||
|
assert len(issue_registry.issues) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensor(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None:
|
||||||
|
"""Test failed update."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
with patch(
|
||||||
|
"aiowaqi.WAQIClient.get_by_station_number",
|
||||||
|
return_value=WAQIAirQuality.parse_obj(
|
||||||
|
json.loads(load_fixture("waqi/air_quality_sensor.json"))
|
||||||
|
),
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.waqi_de_jongweg_utrecht")
|
||||||
|
assert state.state == "29"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_updating_failed(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test failed update."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
with patch(
|
||||||
|
"aiowaqi.WAQIClient.get_by_station_number",
|
||||||
|
side_effect=WAQIError(),
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY
|
Loading…
Add table
Add a link
Reference in a new issue