mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
weatherkit: use stale data for up to an hour if updates fail (#130398)
This commit is contained in:
parent
bb2d027532
commit
53ef96c63e
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from apple_weatherkit import DataSetType
|
||||
from apple_weatherkit.client import WeatherKitApiClient, WeatherKitApiClientError
|
||||
@ -20,12 +20,15 @@ REQUESTED_DATA_SETS = [
|
||||
DataSetType.HOURLY_FORECAST,
|
||||
]
|
||||
|
||||
STALE_DATA_THRESHOLD = timedelta(hours=1)
|
||||
|
||||
|
||||
class WeatherKitDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching data from the API."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
supported_data_sets: list[DataSetType] | None = None
|
||||
last_updated_at: datetime | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -62,10 +65,20 @@ class WeatherKitDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
if not self.supported_data_sets:
|
||||
await self.update_supported_data_sets()
|
||||
|
||||
return await self.client.get_weather_data(
|
||||
updated_data = await self.client.get_weather_data(
|
||||
self.config_entry.data[CONF_LATITUDE],
|
||||
self.config_entry.data[CONF_LONGITUDE],
|
||||
self.supported_data_sets,
|
||||
)
|
||||
except WeatherKitApiClientError as exception:
|
||||
raise UpdateFailed(exception) from exception
|
||||
if self.data is None or (
|
||||
self.last_updated_at is not None
|
||||
and datetime.now() - self.last_updated_at > STALE_DATA_THRESHOLD
|
||||
):
|
||||
raise UpdateFailed(exception) from exception
|
||||
|
||||
LOGGER.debug("Using stale data because update failed: %s", exception)
|
||||
return self.data
|
||||
else:
|
||||
self.last_updated_at = datetime.now()
|
||||
return updated_data
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Tests for the Apple WeatherKit integration."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import patch
|
||||
|
||||
from apple_weatherkit import DataSetType
|
||||
@ -26,20 +27,13 @@ EXAMPLE_CONFIG_DATA = {
|
||||
}
|
||||
|
||||
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
@contextmanager
|
||||
def mock_weather_response(
|
||||
is_night_time: bool = False,
|
||||
has_hourly_forecast: bool = True,
|
||||
has_daily_forecast: bool = True,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the WeatherKit integration in Home Assistant."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Home",
|
||||
unique_id="0123456",
|
||||
data=EXAMPLE_CONFIG_DATA,
|
||||
)
|
||||
|
||||
):
|
||||
"""Mock a successful WeatherKit API response."""
|
||||
weather_response = load_json_object_fixture("weatherkit/weather_response.json")
|
||||
|
||||
available_data_sets = [DataSetType.CURRENT_WEATHER]
|
||||
@ -68,8 +62,22 @@ async def init_integration(
|
||||
return_value=available_data_sets,
|
||||
),
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
yield
|
||||
|
||||
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the WeatherKit integration in Home Assistant."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Home",
|
||||
unique_id="0123456",
|
||||
data=EXAMPLE_CONFIG_DATA,
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
|
@ -4,30 +4,93 @@ from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from apple_weatherkit.client import WeatherKitApiClientError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import init_integration
|
||||
from . import init_integration, mock_weather_response
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
async def test_failed_updates(hass: HomeAssistant) -> None:
|
||||
"""Test that we properly handle failed updates."""
|
||||
await init_integration(hass)
|
||||
async def test_update_uses_stale_data_before_threshold(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test that stale data from the last successful update is used if an update failure occurs before the threshold."""
|
||||
with mock_weather_response():
|
||||
await init_integration(hass)
|
||||
|
||||
state = hass.states.get("weather.home")
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
initial_state = state.state
|
||||
|
||||
# Expect stale data to be used before one hour
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data",
|
||||
side_effect=WeatherKitApiClientError,
|
||||
):
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
utcnow() + timedelta(minutes=5),
|
||||
)
|
||||
freezer.tick(timedelta(minutes=59))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.home")
|
||||
assert state
|
||||
assert state.state == initial_state
|
||||
|
||||
|
||||
async def test_update_becomes_unavailable_after_threshold(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test that the entity becomes unavailable if an update failure occurs after the threshold."""
|
||||
with mock_weather_response():
|
||||
await init_integration(hass)
|
||||
|
||||
# Expect state to be unavailable after one hour
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data",
|
||||
side_effect=WeatherKitApiClientError,
|
||||
):
|
||||
freezer.tick(timedelta(hours=1, minutes=5))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.home")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_update_recovers_after_failure(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test that a successful update after repeated failures recovers the entity's state."""
|
||||
with mock_weather_response():
|
||||
await init_integration(hass)
|
||||
|
||||
# Trigger a failure after threshold
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data",
|
||||
side_effect=WeatherKitApiClientError,
|
||||
):
|
||||
freezer.tick(timedelta(hours=1, minutes=5))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Expect that a successful update recovers the entity
|
||||
|
||||
with mock_weather_response():
|
||||
freezer.tick(timedelta(minutes=5))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.home")
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import init_integration
|
||||
from . import init_integration, mock_weather_response
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -20,7 +20,8 @@ async def test_sensor_values(
|
||||
hass: HomeAssistant, entity_name: str, expected_value: Any
|
||||
) -> None:
|
||||
"""Test that various sensor values match what we expect."""
|
||||
await init_integration(hass)
|
||||
with mock_weather_response():
|
||||
await init_integration(hass)
|
||||
|
||||
state = hass.states.get(entity_name)
|
||||
assert state
|
||||
|
@ -23,12 +23,13 @@ from homeassistant.components.weatherkit.const import ATTRIBUTION
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_SUPPORTED_FEATURES
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import init_integration
|
||||
from . import init_integration, mock_weather_response
|
||||
|
||||
|
||||
async def test_current_weather(hass: HomeAssistant) -> None:
|
||||
"""Test states of the current weather."""
|
||||
await init_integration(hass)
|
||||
with mock_weather_response():
|
||||
await init_integration(hass)
|
||||
|
||||
state = hass.states.get("weather.home")
|
||||
assert state
|
||||
@ -49,7 +50,8 @@ async def test_current_weather(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_current_weather_nighttime(hass: HomeAssistant) -> None:
|
||||
"""Test that the condition is clear-night when it's sunny and night time."""
|
||||
await init_integration(hass, is_night_time=True)
|
||||
with mock_weather_response(is_night_time=True):
|
||||
await init_integration(hass)
|
||||
|
||||
state = hass.states.get("weather.home")
|
||||
assert state
|
||||
@ -58,7 +60,8 @@ async def test_current_weather_nighttime(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_daily_forecast_missing(hass: HomeAssistant) -> None:
|
||||
"""Test that daily forecast is not supported when WeatherKit doesn't support it."""
|
||||
await init_integration(hass, has_daily_forecast=False)
|
||||
with mock_weather_response(has_daily_forecast=False):
|
||||
await init_integration(hass)
|
||||
|
||||
state = hass.states.get("weather.home")
|
||||
assert state
|
||||
@ -69,7 +72,8 @@ async def test_daily_forecast_missing(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_hourly_forecast_missing(hass: HomeAssistant) -> None:
|
||||
"""Test that hourly forecast is not supported when WeatherKit doesn't support it."""
|
||||
await init_integration(hass, has_hourly_forecast=False)
|
||||
with mock_weather_response(has_hourly_forecast=False):
|
||||
await init_integration(hass)
|
||||
|
||||
state = hass.states.get("weather.home")
|
||||
assert state
|
||||
@ -86,7 +90,8 @@ async def test_hourly_forecast(
|
||||
hass: HomeAssistant, snapshot: SnapshotAssertion, service: str
|
||||
) -> None:
|
||||
"""Test states of the hourly forecast."""
|
||||
await init_integration(hass)
|
||||
with mock_weather_response():
|
||||
await init_integration(hass)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
@ -109,7 +114,8 @@ async def test_daily_forecast(
|
||||
hass: HomeAssistant, snapshot: SnapshotAssertion, service: str
|
||||
) -> None:
|
||||
"""Test states of the daily forecast."""
|
||||
await init_integration(hass)
|
||||
with mock_weather_response():
|
||||
await init_integration(hass)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
|
Loading…
x
Reference in New Issue
Block a user