mirror of
https://github.com/home-assistant/core.git
synced 2026-09-28 02:18:10 -04:00
Improve error handling in Airly integration (#181718)
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
|
||||
from asyncio import timeout
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp import ClientConnectorError, ClientSession
|
||||
from airly import Airly
|
||||
from airly.exceptions import AirlyError
|
||||
import voluptuous as vol
|
||||
@@ -14,11 +15,18 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_USE_NEAREST, DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS
|
||||
from .const import (
|
||||
CONF_USE_NEAREST,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
NO_AIRLY_SENSORS,
|
||||
)
|
||||
|
||||
DESCRIPTION_PLACEHOLDERS = {
|
||||
"developer_registration_url": "https://developer.airly.eu/register",
|
||||
}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@@ -59,8 +67,15 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
except AirlyError as err:
|
||||
if err.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
errors["base"] = "invalid_api_key"
|
||||
if err.status_code == HTTPStatus.NOT_FOUND:
|
||||
elif err.status_code == HTTPStatus.NOT_FOUND:
|
||||
errors["base"] = "wrong_location"
|
||||
else:
|
||||
errors["base"] = "unknown"
|
||||
except ClientConnectorError, TimeoutError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
if not location_point_valid:
|
||||
if not location_nearest_valid:
|
||||
@@ -106,7 +121,7 @@ async def check_location(
|
||||
measurements = airly.create_measurements_session_point(
|
||||
latitude=latitude, longitude=longitude
|
||||
)
|
||||
async with timeout(10):
|
||||
async with timeout(DEFAULT_TIMEOUT):
|
||||
await measurements.update()
|
||||
|
||||
current = measurements.current
|
||||
|
||||
@@ -37,3 +37,4 @@ NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet."
|
||||
URL = "https://airly.org/map/#{latitude},{longitude}"
|
||||
|
||||
DEFAULT_NAME: Final = "Airly"
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
@@ -21,6 +21,7 @@ from .const import (
|
||||
ATTR_API_CAQI,
|
||||
ATTR_API_CAQI_DESCRIPTION,
|
||||
ATTR_API_CAQI_LEVEL,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
MAX_UPDATE_INTERVAL,
|
||||
MIN_UPDATE_INTERVAL,
|
||||
@@ -103,18 +104,18 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | i
|
||||
measurements = self.airly.create_measurements_session_point(
|
||||
self.latitude, self.longitude
|
||||
)
|
||||
async with timeout(20):
|
||||
try:
|
||||
try:
|
||||
async with timeout(DEFAULT_TIMEOUT):
|
||||
await measurements.update()
|
||||
except (AirlyError, ClientConnectorError) as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
"error": repr(error),
|
||||
},
|
||||
) from error
|
||||
except (AirlyError, ClientConnectorError, TimeoutError) as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
"error": repr(error),
|
||||
},
|
||||
) from error
|
||||
|
||||
_LOGGER.debug(
|
||||
"Requests remaining: %s/%s",
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
"wrong_location": "[%key:component::airly::config::error::wrong_location%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"wrong_location": "No Airly measuring stations in this area."
|
||||
},
|
||||
"step": {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
from airly.exceptions import AirlyError
|
||||
import pytest
|
||||
|
||||
@@ -15,7 +16,7 @@ from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import API_NEAREST_URL, API_POINT_URL
|
||||
|
||||
from tests.common import MockConfigEntry, async_load_fixture, patch
|
||||
from tests.common import MockConfigEntry, async_load_fixture
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
CONFIG = {
|
||||
@@ -228,3 +229,90 @@ async def test_create_entry_with_nearest_method(
|
||||
assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE]
|
||||
assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY]
|
||||
assert result["data"][CONF_USE_NEAREST] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(TimeoutError(), "cannot_connect"),
|
||||
(ClientConnectorError(Mock(), OSError("test")), "cannot_connect"),
|
||||
],
|
||||
)
|
||||
async def test_cannot_connect(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test that cannot_connect error is shown when connection fails."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch("airly.measurements.MeasurementsSession.update", side_effect=exception):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=CONFIG
|
||||
)
|
||||
|
||||
assert result["errors"] == {"base": error}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.get(
|
||||
API_POINT_URL, text=await async_load_fixture(hass, "valid_station.json", DOMAIN)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(Exception("unexpected"), "unknown"),
|
||||
(
|
||||
AirlyError(HTTPStatus.INTERNAL_SERVER_ERROR, {"message": "Server error"}),
|
||||
"unknown",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_unknown_error(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test that unknown error is shown for unexpected exceptions."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch("airly.measurements.MeasurementsSession.update", side_effect=exception):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=CONFIG
|
||||
)
|
||||
|
||||
assert result["errors"] == {"base": error}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.get(
|
||||
API_POINT_URL, text=await async_load_fixture(hass, "valid_station.json", DOMAIN)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
@@ -5,6 +5,7 @@ from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
|
||||
from airly.exceptions import AirlyError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.airly.const import DOMAIN
|
||||
@@ -43,8 +44,17 @@ async def test_sensor(
|
||||
assert state == snapshot(name=f"{entity_entry.entity_id}-state")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
AirlyError(HTTPStatus.NOT_FOUND, {"message": "Not found"}),
|
||||
TimeoutError(),
|
||||
],
|
||||
)
|
||||
async def test_availability(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Ensure that we mark the entities unavailable correctly.
|
||||
|
||||
@@ -58,9 +68,7 @@ async def test_availability(
|
||||
assert state.state == "68.35"
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.get(
|
||||
API_POINT_URL, exc=AirlyError(HTTPStatus.NOT_FOUND, {"message": "Not found"})
|
||||
)
|
||||
aioclient_mock.get(API_POINT_URL, exc=exception)
|
||||
future = utcnow() + timedelta(minutes=60)
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
Reference in New Issue
Block a user