Add dynamic update interval to Airly integration (#47505)

* Add dynamic update interval

* Update tests

* Improve tests

* Improve comments

* Add MAX_UPDATE_INTERVAL

* Suggested change

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Use async_fire_time_changed to test update interval

* Fix test_update_interval

* Patch dt_util in airly integration

* Cleaning

* Use total_seconds instead of seconds

* Fix update interval test

* Refactor update interval test

* Don't create new context manager

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Maciej Bieniek 2021-04-27 23:34:53 +02:00 committed by GitHub
parent 9db6d0cee4
commit 513685bbea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 103 additions and 34 deletions

View file

@ -11,6 +11,7 @@ import async_timeout
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import (
ATTR_API_ADVICE,
@ -19,7 +20,8 @@ from .const import (
ATTR_API_CAQI_LEVEL,
CONF_USE_NEAREST,
DOMAIN,
MAX_REQUESTS_PER_DAY,
MAX_UPDATE_INTERVAL,
MIN_UPDATE_INTERVAL,
NO_AIRLY_SENSORS,
)
@ -28,15 +30,30 @@ PLATFORMS = ["air_quality", "sensor"]
_LOGGER = logging.getLogger(__name__)
def set_update_interval(hass, instances):
"""Set update_interval to another configured Airly instances."""
# We check how many Airly configured instances are and calculate interval to not
# exceed allowed numbers of requests.
interval = timedelta(minutes=ceil(24 * 60 / MAX_REQUESTS_PER_DAY) * instances)
def set_update_interval(instances, requests_remaining):
"""
Return data update interval.
if hass.data.get(DOMAIN):
for instance in hass.data[DOMAIN].values():
instance.update_interval = interval
The number of requests is reset at midnight UTC so we calculate the update
interval based on number of minutes until midnight, the number of Airly instances
and the number of remaining requests.
"""
now = dt_util.utcnow()
midnight = dt_util.find_next_time_expression_time(
now, seconds=[0], minutes=[0], hours=[0]
)
minutes_to_midnight = (midnight - now).total_seconds() / 60
interval = timedelta(
minutes=min(
max(
ceil(minutes_to_midnight / requests_remaining * instances),
MIN_UPDATE_INTERVAL,
),
MAX_UPDATE_INTERVAL,
)
)
_LOGGER.debug("Data will be update every %s", interval)
return interval
@ -55,10 +72,8 @@ async def async_setup_entry(hass, config_entry):
)
websession = async_get_clientsession(hass)
# Change update_interval for other Airly instances
update_interval = set_update_interval(
hass, len(hass.config_entries.async_entries(DOMAIN))
)
update_interval = timedelta(minutes=MIN_UPDATE_INTERVAL)
coordinator = AirlyDataUpdateCoordinator(
hass, websession, api_key, latitude, longitude, update_interval, use_nearest
@ -82,9 +97,6 @@ async def async_unload_entry(hass, config_entry):
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
# Change update_interval for other Airly instances
set_update_interval(hass, len(hass.data[DOMAIN]))
return unload_ok
@ -132,6 +144,14 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
self.airly.requests_per_day,
)
# Airly API sometimes returns None for requests remaining so we update
# update_interval only if we have valid value.
if self.airly.requests_remaining:
self.update_interval = set_update_interval(
len(self.hass.config_entries.async_entries(DOMAIN)),
self.airly.requests_remaining,
)
values = measurements.current["values"]
index = measurements.current["indexes"][0]
standards = measurements.current["standards"]

View file

@ -24,5 +24,6 @@ DEFAULT_NAME = "Airly"
DOMAIN = "airly"
LABEL_ADVICE = "advice"
MANUFACTURER = "Airly sp. z o.o."
MAX_REQUESTS_PER_DAY = 100
MAX_UPDATE_INTERVAL = 90
MIN_UPDATE_INTERVAL = 5
NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet."

View file

@ -1,6 +1,7 @@
"""Test init of Airly integration."""
from datetime import timedelta
from unittest.mock import patch
from homeassistant.components.airly import set_update_interval
from homeassistant.components.airly.const import DOMAIN
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
@ -8,10 +9,11 @@ from homeassistant.config_entries import (
ENTRY_STATE_SETUP_RETRY,
)
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.util.dt import utcnow
from . import API_POINT_URL
from tests.common import MockConfigEntry, load_fixture
from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture
from tests.components.airly import init_integration
@ -88,37 +90,83 @@ async def test_config_with_turned_off_station(hass, aioclient_mock):
async def test_update_interval(hass, aioclient_mock):
"""Test correct update interval when the number of configured instances changes."""
entry = await init_integration(hass, aioclient_mock)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
for instance in hass.data[DOMAIN].values():
assert instance.update_interval == timedelta(minutes=15)
REMAINING_RQUESTS = 15
HEADERS = {
"X-RateLimit-Limit-day": "100",
"X-RateLimit-Remaining-day": str(REMAINING_RQUESTS),
}
entry = MockConfigEntry(
domain=DOMAIN,
title="Work",
unique_id="66.66-111.11",
title="Home",
unique_id="123-456",
data={
"api_key": "foo",
"latitude": 66.66,
"longitude": 111.11,
"name": "Work",
"latitude": 123,
"longitude": 456,
"name": "Home",
},
)
aioclient_mock.get(
"https://airapi.airly.eu/v2/measurements/point?lat=66.660000&lng=111.110000",
API_POINT_URL,
text=load_fixture("airly_valid_station.json"),
headers=HEADERS,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
instances = 1
assert len(hass.config_entries.async_entries(DOMAIN)) == 2
assert aioclient_mock.call_count == 1
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
for instance in hass.data[DOMAIN].values():
assert instance.update_interval == timedelta(minutes=30)
update_interval = set_update_interval(instances, REMAINING_RQUESTS)
future = utcnow() + update_interval
with patch("homeassistant.util.dt.utcnow") as mock_utcnow:
mock_utcnow.return_value = future
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
# call_count should increase by one because we have one instance configured
assert aioclient_mock.call_count == 2
# Now we add the second Airly instance
entry = MockConfigEntry(
domain=DOMAIN,
title="Work",
unique_id="66.66-111.11",
data={
"api_key": "foo",
"latitude": 66.66,
"longitude": 111.11,
"name": "Work",
},
)
aioclient_mock.get(
"https://airapi.airly.eu/v2/measurements/point?lat=66.660000&lng=111.110000",
text=load_fixture("airly_valid_station.json"),
headers=HEADERS,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
instances = 2
assert aioclient_mock.call_count == 3
assert len(hass.config_entries.async_entries(DOMAIN)) == 2
assert entry.state == ENTRY_STATE_LOADED
update_interval = set_update_interval(instances, REMAINING_RQUESTS)
future = utcnow() + update_interval
mock_utcnow.return_value = future
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
# call_count should increase by two because we have two instances configured
assert aioclient_mock.call_count == 5
async def test_unload_entry(hass, aioclient_mock):