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 rebasepull/44835/head
parent
76537305e2
commit
2e50c1be8e
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue