From 2e50c1be8e9594df2c88e1ecb34d7a46d040045c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 4 Jan 2021 23:14:45 +0100 Subject: [PATCH] Add nearest method to get data for Airly integration (#44288) * Add nearest method * Add tests * Move urls to consts * Simplify config flow * Fix tests * Update tests * Use in instead get * Fix AirlyError message in tests * Fix manual update entity tests * Clean up tests * Fix after rebase * Increase test coverage * Format the code * Fix after rebase --- homeassistant/components/airly/__init__.py | 23 +++++++-- homeassistant/components/airly/air_quality.py | 21 ++++++--- homeassistant/components/airly/config_flow.py | 47 +++++++++++++------ homeassistant/components/airly/const.py | 1 + homeassistant/components/airly/sensor.py | 4 +- tests/components/airly/__init__.py | 1 + tests/components/airly/test_config_flow.py | 31 +++++++++++- tests/components/airly/test_init.py | 1 + 8 files changed, 102 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index de09d767b1f..9d6b46f82e5 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -20,6 +20,7 @@ from .const import ( ATTR_API_CAQI, ATTR_API_CAQI_DESCRIPTION, ATTR_API_CAQI_LEVEL, + CONF_USE_NEAREST, DOMAIN, MAX_REQUESTS_PER_DAY, NO_AIRLY_SENSORS, @@ -53,6 +54,7 @@ async def async_setup_entry(hass, config_entry): api_key = config_entry.data[CONF_API_KEY] latitude = config_entry.data[CONF_LATITUDE] longitude = config_entry.data[CONF_LONGITUDE] + use_nearest = config_entry.data.get(CONF_USE_NEAREST, False) # For backwards compat, set unique ID if config_entry.unique_id is None: @@ -67,7 +69,7 @@ async def async_setup_entry(hass, config_entry): ) coordinator = AirlyDataUpdateCoordinator( - hass, websession, api_key, latitude, longitude, update_interval + hass, websession, api_key, latitude, longitude, update_interval, use_nearest ) await coordinator.async_refresh() @@ -107,21 +109,36 @@ async def async_unload_entry(hass, config_entry): class AirlyDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold Airly data.""" - def __init__(self, hass, session, api_key, latitude, longitude, update_interval): + def __init__( + self, + hass, + session, + api_key, + latitude, + longitude, + update_interval, + use_nearest, + ): """Initialize.""" self.latitude = latitude self.longitude = longitude self.airly = Airly(api_key, session) + self.use_nearest = use_nearest super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) async def _async_update_data(self): """Update data via library.""" data = {} - with async_timeout.timeout(20): + if self.use_nearest: + measurements = self.airly.create_measurements_session_nearest( + self.latitude, self.longitude, max_distance_km=5 + ) + else: measurements = self.airly.create_measurements_session_point( self.latitude, self.longitude ) + with async_timeout.timeout(20): try: await measurements.update() except (AirlyError, ClientConnectorError) as error: diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 4a3de1e6543..e43a76b3418 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -87,13 +87,13 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): @round_state def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" - return self.coordinator.data[ATTR_API_PM25] + return self.coordinator.data.get(ATTR_API_PM25) @property @round_state def particulate_matter_10(self): """Return the particulate matter 10 level.""" - return self.coordinator.data[ATTR_API_PM10] + return self.coordinator.data.get(ATTR_API_PM10) @property def attribution(self): @@ -120,12 +120,19 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): @property def device_state_attributes(self): """Return the state attributes.""" - return { + attrs = { LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION], LABEL_ADVICE: self.coordinator.data[ATTR_API_ADVICE], LABEL_AQI_LEVEL: self.coordinator.data[ATTR_API_CAQI_LEVEL], - LABEL_PM_2_5_LIMIT: self.coordinator.data[ATTR_API_PM25_LIMIT], - LABEL_PM_2_5_PERCENT: round(self.coordinator.data[ATTR_API_PM25_PERCENT]), - LABEL_PM_10_LIMIT: self.coordinator.data[ATTR_API_PM10_LIMIT], - LABEL_PM_10_PERCENT: round(self.coordinator.data[ATTR_API_PM10_PERCENT]), } + if ATTR_API_PM25 in self.coordinator.data: + attrs[LABEL_PM_2_5_LIMIT] = self.coordinator.data[ATTR_API_PM25_LIMIT] + attrs[LABEL_PM_2_5_PERCENT] = round( + self.coordinator.data[ATTR_API_PM25_PERCENT] + ) + if ATTR_API_PM10 in self.coordinator.data: + attrs[LABEL_PM_10_LIMIT] = self.coordinator.data[ATTR_API_PM10_LIMIT] + attrs[LABEL_PM_10_PERCENT] = round( + self.coordinator.data[ATTR_API_PM10_PERCENT] + ) + return attrs diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 58d6a4295e9..d7636d1db33 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -10,12 +10,17 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, NO_AIRLY_SENSORS # pylint:disable=unused-import +from .const import ( # pylint:disable=unused-import + CONF_USE_NEAREST, + DOMAIN, + NO_AIRLY_SENSORS, +) class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -27,6 +32,7 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} + use_nearest = False websession = async_get_clientsession(self.hass) @@ -36,23 +42,32 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured() try: - location_valid = await test_location( + location_point_valid = await test_location( websession, user_input["api_key"], user_input["latitude"], user_input["longitude"], ) + if not location_point_valid: + await test_location( + websession, + user_input["api_key"], + user_input["latitude"], + user_input["longitude"], + use_nearest=True, + ) except AirlyError as err: if err.status_code == HTTP_UNAUTHORIZED: errors["base"] = "invalid_api_key" - else: - if not location_valid: + if err.status_code == HTTP_NOT_FOUND: errors["base"] = "wrong_location" - - if not errors: - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) + else: + if not location_point_valid: + use_nearest = True + return self.async_create_entry( + title=user_input[CONF_NAME], + data={**user_input, CONF_USE_NEAREST: use_nearest}, + ) return self.async_show_form( step_id="user", @@ -74,13 +89,17 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -async def test_location(client, api_key, latitude, longitude): +async def test_location(client, api_key, latitude, longitude, use_nearest=False): """Return true if location is valid.""" airly = Airly(api_key, client) - measurements = airly.create_measurements_session_point( - latitude=latitude, longitude=longitude - ) - + if use_nearest: + measurements = airly.create_measurements_session_nearest( + latitude=latitude, longitude=longitude, max_distance_km=5 + ) + else: + measurements = airly.create_measurements_session_point( + latitude=latitude, longitude=longitude + ) with async_timeout.timeout(10): await measurements.update() diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index dc21d68a8d8..b4711b50dd2 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -13,6 +13,7 @@ ATTR_API_PM25_LIMIT = "PM25_LIMIT" ATTR_API_PM25_PERCENT = "PM25_PERCENT" ATTR_API_PRESSURE = "PRESSURE" ATTR_API_TEMPERATURE = "TEMPERATURE" +CONF_USE_NEAREST = "use_nearest" DEFAULT_NAME = "Airly" DOMAIN = "airly" MANUFACTURER = "Airly sp. z o.o." diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index d4f472dfca8..420d11a5963 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -67,7 +67,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] for sensor in SENSOR_TYPES: - sensors.append(AirlySensor(coordinator, name, sensor)) + # When we use the nearest method, we are not sure which sensors are available + if coordinator.data.get(sensor): + sensors.append(AirlySensor(coordinator, name, sensor)) async_add_entities(sensors, False) diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index 197864b807c..64f2059857a 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -3,6 +3,7 @@ from homeassistant.components.airly.const import DOMAIN from tests.common import MockConfigEntry, load_fixture +API_NEAREST_URL = "https://airapi.airly.eu/v2/measurements/nearest?lat=123.000000&lng=456.000000&maxDistanceKM=5.000000" API_POINT_URL = ( "https://airapi.airly.eu/v2/measurements/point?lat=123.000000&lng=456.000000" ) diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 46dc5510b18..5683a06bb28 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -2,17 +2,18 @@ from airly.exceptions import AirlyError from homeassistant import data_entry_flow -from homeassistant.components.airly.const import DOMAIN +from homeassistant.components.airly.const import CONF_USE_NEAREST, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, ) -from . import API_POINT_URL +from . import API_NEAREST_URL, API_POINT_URL from tests.common import MockConfigEntry, load_fixture, patch @@ -54,6 +55,11 @@ async def test_invalid_location(hass, aioclient_mock): """Test that errors are shown when location is invalid.""" aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + aioclient_mock.get( + API_NEAREST_URL, + exc=AirlyError(HTTP_NOT_FOUND, {"message": "Installation was not found"}), + ) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -88,3 +94,24 @@ async def test_create_entry(hass, aioclient_mock): assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] 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 False + + +async def test_create_entry_with_nearest_method(hass, aioclient_mock): + """Test that the user step works with nearest method.""" + + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + + aioclient_mock.get(API_NEAREST_URL, text=load_fixture("airly_valid_station.json")) + + with patch("homeassistant.components.airly.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_NAME] + assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] + 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 diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index cb0ccf268f7..2898bd5c6f6 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -36,6 +36,7 @@ async def test_config_not_ready(hass, aioclient_mock): "latitude": 123, "longitude": 456, "name": "Home", + "use_nearest": True, }, )