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
pull/44835/head
Maciej Bieniek 2021-01-04 23:14:45 +01:00 committed by GitHub
parent 76537305e2
commit 2e50c1be8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 102 additions and 27 deletions

View File

@ -20,6 +20,7 @@ from .const import (
ATTR_API_CAQI, ATTR_API_CAQI,
ATTR_API_CAQI_DESCRIPTION, ATTR_API_CAQI_DESCRIPTION,
ATTR_API_CAQI_LEVEL, ATTR_API_CAQI_LEVEL,
CONF_USE_NEAREST,
DOMAIN, DOMAIN,
MAX_REQUESTS_PER_DAY, MAX_REQUESTS_PER_DAY,
NO_AIRLY_SENSORS, NO_AIRLY_SENSORS,
@ -53,6 +54,7 @@ async def async_setup_entry(hass, config_entry):
api_key = config_entry.data[CONF_API_KEY] api_key = config_entry.data[CONF_API_KEY]
latitude = config_entry.data[CONF_LATITUDE] latitude = config_entry.data[CONF_LATITUDE]
longitude = config_entry.data[CONF_LONGITUDE] longitude = config_entry.data[CONF_LONGITUDE]
use_nearest = config_entry.data.get(CONF_USE_NEAREST, False)
# For backwards compat, set unique ID # For backwards compat, set unique ID
if config_entry.unique_id is None: if config_entry.unique_id is None:
@ -67,7 +69,7 @@ async def async_setup_entry(hass, config_entry):
) )
coordinator = AirlyDataUpdateCoordinator( coordinator = AirlyDataUpdateCoordinator(
hass, websession, api_key, latitude, longitude, update_interval hass, websession, api_key, latitude, longitude, update_interval, use_nearest
) )
await coordinator.async_refresh() await coordinator.async_refresh()
@ -107,21 +109,36 @@ async def async_unload_entry(hass, config_entry):
class AirlyDataUpdateCoordinator(DataUpdateCoordinator): class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
"""Define an object to hold Airly data.""" """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.""" """Initialize."""
self.latitude = latitude self.latitude = latitude
self.longitude = longitude self.longitude = longitude
self.airly = Airly(api_key, session) self.airly = Airly(api_key, session)
self.use_nearest = use_nearest
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self): async def _async_update_data(self):
"""Update data via library.""" """Update data via library."""
data = {} 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( measurements = self.airly.create_measurements_session_point(
self.latitude, self.longitude self.latitude, self.longitude
) )
with async_timeout.timeout(20):
try: try:
await measurements.update() await measurements.update()
except (AirlyError, ClientConnectorError) as error: except (AirlyError, ClientConnectorError) as error:

View File

@ -87,13 +87,13 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity):
@round_state @round_state
def particulate_matter_2_5(self): def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level.""" """Return the particulate matter 2.5 level."""
return self.coordinator.data[ATTR_API_PM25] return self.coordinator.data.get(ATTR_API_PM25)
@property @property
@round_state @round_state
def particulate_matter_10(self): def particulate_matter_10(self):
"""Return the particulate matter 10 level.""" """Return the particulate matter 10 level."""
return self.coordinator.data[ATTR_API_PM10] return self.coordinator.data.get(ATTR_API_PM10)
@property @property
def attribution(self): def attribution(self):
@ -120,12 +120,19 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { attrs = {
LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION], LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION],
LABEL_ADVICE: self.coordinator.data[ATTR_API_ADVICE], LABEL_ADVICE: self.coordinator.data[ATTR_API_ADVICE],
LABEL_AQI_LEVEL: self.coordinator.data[ATTR_API_CAQI_LEVEL], 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

View File

@ -10,12 +10,17 @@ from homeassistant.const import (
CONF_LATITUDE, CONF_LATITUDE,
CONF_LONGITUDE, CONF_LONGITUDE,
CONF_NAME, CONF_NAME,
HTTP_NOT_FOUND,
HTTP_UNAUTHORIZED, HTTP_UNAUTHORIZED,
) )
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 .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): 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): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
errors = {} errors = {}
use_nearest = False
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
@ -36,23 +42,32 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
) )
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
try: try:
location_valid = await test_location( location_point_valid = await test_location(
websession, websession,
user_input["api_key"], user_input["api_key"],
user_input["latitude"], user_input["latitude"],
user_input["longitude"], 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: except AirlyError as err:
if err.status_code == HTTP_UNAUTHORIZED: if err.status_code == HTTP_UNAUTHORIZED:
errors["base"] = "invalid_api_key" errors["base"] = "invalid_api_key"
else: if err.status_code == HTTP_NOT_FOUND:
if not location_valid:
errors["base"] = "wrong_location" errors["base"] = "wrong_location"
else:
if not errors: if not location_point_valid:
return self.async_create_entry( use_nearest = True
title=user_input[CONF_NAME], data=user_input return self.async_create_entry(
) title=user_input[CONF_NAME],
data={**user_input, CONF_USE_NEAREST: use_nearest},
)
return self.async_show_form( return self.async_show_form(
step_id="user", 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.""" """Return true if location is valid."""
airly = Airly(api_key, client) airly = Airly(api_key, client)
measurements = airly.create_measurements_session_point( if use_nearest:
latitude=latitude, longitude=longitude 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): with async_timeout.timeout(10):
await measurements.update() await measurements.update()

View File

@ -13,6 +13,7 @@ ATTR_API_PM25_LIMIT = "PM25_LIMIT"
ATTR_API_PM25_PERCENT = "PM25_PERCENT" ATTR_API_PM25_PERCENT = "PM25_PERCENT"
ATTR_API_PRESSURE = "PRESSURE" ATTR_API_PRESSURE = "PRESSURE"
ATTR_API_TEMPERATURE = "TEMPERATURE" ATTR_API_TEMPERATURE = "TEMPERATURE"
CONF_USE_NEAREST = "use_nearest"
DEFAULT_NAME = "Airly" DEFAULT_NAME = "Airly"
DOMAIN = "airly" DOMAIN = "airly"
MANUFACTURER = "Airly sp. z o.o." MANUFACTURER = "Airly sp. z o.o."

View File

@ -67,7 +67,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
sensors = [] sensors = []
for sensor in SENSOR_TYPES: 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) async_add_entities(sensors, False)

View File

@ -3,6 +3,7 @@ from homeassistant.components.airly.const import DOMAIN
from tests.common import MockConfigEntry, load_fixture 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 = ( API_POINT_URL = (
"https://airapi.airly.eu/v2/measurements/point?lat=123.000000&lng=456.000000" "https://airapi.airly.eu/v2/measurements/point?lat=123.000000&lng=456.000000"
) )

View File

@ -2,17 +2,18 @@
from airly.exceptions import AirlyError from airly.exceptions import AirlyError
from homeassistant import data_entry_flow 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.config_entries import SOURCE_USER
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_LATITUDE, CONF_LATITUDE,
CONF_LONGITUDE, CONF_LONGITUDE,
CONF_NAME, CONF_NAME,
HTTP_NOT_FOUND,
HTTP_UNAUTHORIZED, HTTP_UNAUTHORIZED,
) )
from . import API_POINT_URL from . import API_NEAREST_URL, API_POINT_URL
from tests.common import MockConfigEntry, load_fixture, patch 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.""" """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_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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG 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_LATITUDE] == CONFIG[CONF_LATITUDE]
assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE]
assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] 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

View File

@ -36,6 +36,7 @@ async def test_config_not_ready(hass, aioclient_mock):
"latitude": 123, "latitude": 123,
"longitude": 456, "longitude": 456,
"name": "Home", "name": "Home",
"use_nearest": True,
}, },
) )