Fix nws forecast coordinators and remove legacy forecast handling (#115857)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
ee7f818fcd
commit
6413376ccb
8 changed files with 82 additions and 234 deletions
|
@ -2,21 +2,18 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from pynws import SimpleNWS
|
from pynws import SimpleNWS, call_with_retry
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import debounce
|
from homeassistant.helpers import debounce
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
|
||||||
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
|
@ -27,8 +24,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||||
|
|
||||||
DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10)
|
DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10)
|
||||||
FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1)
|
RETRY_INTERVAL = datetime.timedelta(minutes=1)
|
||||||
DEBOUNCE_TIME = 60 # in seconds
|
RETRY_STOP = datetime.timedelta(minutes=10)
|
||||||
|
|
||||||
|
DEBOUNCE_TIME = 10 * 60 # in seconds
|
||||||
|
|
||||||
|
|
||||||
def base_unique_id(latitude: float, longitude: float) -> str:
|
def base_unique_id(latitude: float, longitude: float) -> str:
|
||||||
|
@ -41,62 +40,9 @@ class NWSData:
|
||||||
"""Data for the National Weather Service integration."""
|
"""Data for the National Weather Service integration."""
|
||||||
|
|
||||||
api: SimpleNWS
|
api: SimpleNWS
|
||||||
coordinator_observation: NwsDataUpdateCoordinator
|
coordinator_observation: TimestampDataUpdateCoordinator[None]
|
||||||
coordinator_forecast: NwsDataUpdateCoordinator
|
coordinator_forecast: TimestampDataUpdateCoordinator[None]
|
||||||
coordinator_forecast_hourly: NwsDataUpdateCoordinator
|
coordinator_forecast_hourly: TimestampDataUpdateCoordinator[None]
|
||||||
|
|
||||||
|
|
||||||
class NwsDataUpdateCoordinator(TimestampDataUpdateCoordinator[None]): # pylint: disable=hass-enforce-coordinator-module
|
|
||||||
"""NWS data update coordinator.
|
|
||||||
|
|
||||||
Implements faster data update intervals for failed updates and exposes a last successful update time.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
logger: logging.Logger,
|
|
||||||
*,
|
|
||||||
name: str,
|
|
||||||
update_interval: datetime.timedelta,
|
|
||||||
failed_update_interval: datetime.timedelta,
|
|
||||||
update_method: Callable[[], Awaitable[None]] | None = None,
|
|
||||||
request_refresh_debouncer: debounce.Debouncer | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize NWS coordinator."""
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
logger,
|
|
||||||
name=name,
|
|
||||||
update_interval=update_interval,
|
|
||||||
update_method=update_method,
|
|
||||||
request_refresh_debouncer=request_refresh_debouncer,
|
|
||||||
)
|
|
||||||
self.failed_update_interval = failed_update_interval
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _schedule_refresh(self) -> None:
|
|
||||||
"""Schedule a refresh."""
|
|
||||||
if self._unsub_refresh:
|
|
||||||
self._unsub_refresh()
|
|
||||||
self._unsub_refresh = None
|
|
||||||
|
|
||||||
# We _floor_ utcnow to create a schedule on a rounded second,
|
|
||||||
# minimizing the time between the point and the real activation.
|
|
||||||
# That way we obtain a constant update frequency,
|
|
||||||
# as long as the update process takes less than a second
|
|
||||||
if self.last_update_success:
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
# the base class allows None, but this one doesn't
|
|
||||||
assert self.update_interval is not None
|
|
||||||
update_interval = self.update_interval
|
|
||||||
else:
|
|
||||||
update_interval = self.failed_update_interval
|
|
||||||
self._unsub_refresh = async_track_point_in_utc_time(
|
|
||||||
self.hass,
|
|
||||||
self._handle_refresh_interval,
|
|
||||||
utcnow().replace(microsecond=0) + update_interval,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
@ -114,39 +60,57 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
|
||||||
async def update_observation() -> None:
|
async def update_observation() -> None:
|
||||||
"""Retrieve recent observations."""
|
"""Retrieve recent observations."""
|
||||||
await nws_data.update_observation(start_time=utcnow() - UPDATE_TIME_PERIOD)
|
await call_with_retry(
|
||||||
|
nws_data.update_observation,
|
||||||
|
RETRY_INTERVAL,
|
||||||
|
RETRY_STOP,
|
||||||
|
start_time=utcnow() - UPDATE_TIME_PERIOD,
|
||||||
|
)
|
||||||
|
|
||||||
coordinator_observation = NwsDataUpdateCoordinator(
|
async def update_forecast() -> None:
|
||||||
|
"""Retrieve twice-daily forecsat."""
|
||||||
|
await call_with_retry(
|
||||||
|
nws_data.update_forecast,
|
||||||
|
RETRY_INTERVAL,
|
||||||
|
RETRY_STOP,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update_forecast_hourly() -> None:
|
||||||
|
"""Retrieve hourly forecast."""
|
||||||
|
await call_with_retry(
|
||||||
|
nws_data.update_forecast_hourly,
|
||||||
|
RETRY_INTERVAL,
|
||||||
|
RETRY_STOP,
|
||||||
|
)
|
||||||
|
|
||||||
|
coordinator_observation = TimestampDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
name=f"NWS observation station {station}",
|
name=f"NWS observation station {station}",
|
||||||
update_method=update_observation,
|
update_method=update_observation,
|
||||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||||
failed_update_interval=FAILED_SCAN_INTERVAL,
|
|
||||||
request_refresh_debouncer=debounce.Debouncer(
|
request_refresh_debouncer=debounce.Debouncer(
|
||||||
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator_forecast = NwsDataUpdateCoordinator(
|
coordinator_forecast = TimestampDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
name=f"NWS forecast station {station}",
|
name=f"NWS forecast station {station}",
|
||||||
update_method=nws_data.update_forecast,
|
update_method=update_forecast,
|
||||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||||
failed_update_interval=FAILED_SCAN_INTERVAL,
|
|
||||||
request_refresh_debouncer=debounce.Debouncer(
|
request_refresh_debouncer=debounce.Debouncer(
|
||||||
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator_forecast_hourly = NwsDataUpdateCoordinator(
|
coordinator_forecast_hourly = TimestampDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
name=f"NWS forecast hourly station {station}",
|
name=f"NWS forecast hourly station {station}",
|
||||||
update_method=nws_data.update_forecast_hourly,
|
update_method=update_forecast_hourly,
|
||||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||||
failed_update_interval=FAILED_SCAN_INTERVAL,
|
|
||||||
request_refresh_debouncer=debounce.Debouncer(
|
request_refresh_debouncer=debounce.Debouncer(
|
||||||
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["metar", "pynws"],
|
"loggers": ["metar", "pynws"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pynws==1.6.0"]
|
"requirements": ["pynws[retry]==1.7.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,10 @@ from homeassistant.const import (
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
CoordinatorEntity,
|
||||||
|
TimestampDataUpdateCoordinator,
|
||||||
|
)
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
from homeassistant.util.unit_conversion import (
|
from homeassistant.util.unit_conversion import (
|
||||||
DistanceConverter,
|
DistanceConverter,
|
||||||
|
@ -34,7 +37,7 @@ from homeassistant.util.unit_conversion import (
|
||||||
)
|
)
|
||||||
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
||||||
|
|
||||||
from . import NWSData, NwsDataUpdateCoordinator, base_unique_id, device_info
|
from . import NWSData, base_unique_id, device_info
|
||||||
from .const import ATTRIBUTION, CONF_STATION, DOMAIN, OBSERVATION_VALID_TIME
|
from .const import ATTRIBUTION, CONF_STATION, DOMAIN, OBSERVATION_VALID_TIME
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
@ -158,7 +161,7 @@ async def async_setup_entry(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity):
|
class NWSSensor(CoordinatorEntity[TimestampDataUpdateCoordinator[None]], SensorEntity):
|
||||||
"""An NWS Sensor Entity."""
|
"""An NWS Sensor Entity."""
|
||||||
|
|
||||||
entity_description: NWSSensorEntityDescription
|
entity_description: NWSSensorEntityDescription
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
|
@ -34,7 +35,6 @@ from homeassistant.const import (
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util.dt import utcnow
|
|
||||||
from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter
|
from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter
|
||||||
|
|
||||||
from . import NWSData, base_unique_id, device_info
|
from . import NWSData, base_unique_id, device_info
|
||||||
|
@ -46,7 +46,6 @@ from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FORECAST_VALID_TIME,
|
FORECAST_VALID_TIME,
|
||||||
HOURLY,
|
HOURLY,
|
||||||
OBSERVATION_VALID_TIME,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
@ -140,96 +139,69 @@ class NWSWeather(CoordinatorWeatherEntity):
|
||||||
self.nws = nws_data.api
|
self.nws = nws_data.api
|
||||||
latitude = entry_data[CONF_LATITUDE]
|
latitude = entry_data[CONF_LATITUDE]
|
||||||
longitude = entry_data[CONF_LONGITUDE]
|
longitude = entry_data[CONF_LONGITUDE]
|
||||||
self.coordinator_forecast_legacy = nws_data.coordinator_forecast
|
|
||||||
self.station = self.nws.station
|
|
||||||
|
|
||||||
self.observation: dict[str, Any] | None = None
|
self.station = self.nws.station
|
||||||
self._forecast_hourly: list[dict[str, Any]] | None = None
|
|
||||||
self._forecast_legacy: list[dict[str, Any]] | None = None
|
|
||||||
self._forecast_twice_daily: list[dict[str, Any]] | None = None
|
|
||||||
|
|
||||||
self._attr_unique_id = _calculate_unique_id(entry_data, DAYNIGHT)
|
self._attr_unique_id = _calculate_unique_id(entry_data, DAYNIGHT)
|
||||||
self._attr_device_info = device_info(latitude, longitude)
|
self._attr_device_info = device_info(latitude, longitude)
|
||||||
self._attr_name = self.station
|
self._attr_name = self.station
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Set up a listener and load data."""
|
"""When entity is added to hass."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
self.async_on_remove(
|
self.async_on_remove(partial(self._remove_forecast_listener, "daily"))
|
||||||
self.coordinator_forecast_legacy.async_add_listener(
|
self.async_on_remove(partial(self._remove_forecast_listener, "hourly"))
|
||||||
self._handle_legacy_forecast_coordinator_update
|
self.async_on_remove(partial(self._remove_forecast_listener, "twice_daily"))
|
||||||
|
|
||||||
|
for forecast_type in ("twice_daily", "hourly"):
|
||||||
|
if (coordinator := self.forecast_coordinators[forecast_type]) is None:
|
||||||
|
continue
|
||||||
|
self.unsub_forecast[forecast_type] = coordinator.async_add_listener(
|
||||||
|
partial(self._handle_forecast_update, forecast_type)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
# Load initial data from coordinators
|
|
||||||
self._handle_coordinator_update()
|
|
||||||
self._handle_hourly_forecast_coordinator_update()
|
|
||||||
self._handle_twice_daily_forecast_coordinator_update()
|
|
||||||
self._handle_legacy_forecast_coordinator_update()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _handle_coordinator_update(self) -> None:
|
|
||||||
"""Load data from integration."""
|
|
||||||
self.observation = self.nws.observation
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _handle_hourly_forecast_coordinator_update(self) -> None:
|
|
||||||
"""Handle updated data from the hourly forecast coordinator."""
|
|
||||||
self._forecast_hourly = self.nws.forecast_hourly
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _handle_twice_daily_forecast_coordinator_update(self) -> None:
|
|
||||||
"""Handle updated data from the twice daily forecast coordinator."""
|
|
||||||
self._forecast_twice_daily = self.nws.forecast
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _handle_legacy_forecast_coordinator_update(self) -> None:
|
|
||||||
"""Handle updated data from the legacy forecast coordinator."""
|
|
||||||
self._forecast_legacy = self.nws.forecast
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_temperature(self) -> float | None:
|
def native_temperature(self) -> float | None:
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
if self.observation:
|
if observation := self.nws.observation:
|
||||||
return self.observation.get("temperature")
|
return observation.get("temperature")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_pressure(self) -> int | None:
|
def native_pressure(self) -> int | None:
|
||||||
"""Return the current pressure."""
|
"""Return the current pressure."""
|
||||||
if self.observation:
|
if observation := self.nws.observation:
|
||||||
return self.observation.get("seaLevelPressure")
|
return observation.get("seaLevelPressure")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def humidity(self) -> float | None:
|
def humidity(self) -> float | None:
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
if self.observation:
|
if observation := self.nws.observation:
|
||||||
return self.observation.get("relativeHumidity")
|
return observation.get("relativeHumidity")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_wind_speed(self) -> float | None:
|
def native_wind_speed(self) -> float | None:
|
||||||
"""Return the current windspeed."""
|
"""Return the current windspeed."""
|
||||||
if self.observation:
|
if observation := self.nws.observation:
|
||||||
return self.observation.get("windSpeed")
|
return observation.get("windSpeed")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wind_bearing(self) -> int | None:
|
def wind_bearing(self) -> int | None:
|
||||||
"""Return the current wind bearing (degrees)."""
|
"""Return the current wind bearing (degrees)."""
|
||||||
if self.observation:
|
if observation := self.nws.observation:
|
||||||
return self.observation.get("windDirection")
|
return observation.get("windDirection")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def condition(self) -> str | None:
|
def condition(self) -> str | None:
|
||||||
"""Return current condition."""
|
"""Return current condition."""
|
||||||
weather = None
|
weather = None
|
||||||
if self.observation:
|
if observation := self.nws.observation:
|
||||||
weather = self.observation.get("iconWeather")
|
weather = observation.get("iconWeather")
|
||||||
time = cast(str, self.observation.get("iconTime"))
|
time = cast(str, observation.get("iconTime"))
|
||||||
|
|
||||||
if weather:
|
if weather:
|
||||||
return convert_condition(time, weather)
|
return convert_condition(time, weather)
|
||||||
|
@ -238,8 +210,8 @@ class NWSWeather(CoordinatorWeatherEntity):
|
||||||
@property
|
@property
|
||||||
def native_visibility(self) -> int | None:
|
def native_visibility(self) -> int | None:
|
||||||
"""Return visibility."""
|
"""Return visibility."""
|
||||||
if self.observation:
|
if observation := self.nws.observation:
|
||||||
return self.observation.get("visibility")
|
return observation.get("visibility")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _forecast(
|
def _forecast(
|
||||||
|
@ -302,33 +274,12 @@ class NWSWeather(CoordinatorWeatherEntity):
|
||||||
@callback
|
@callback
|
||||||
def _async_forecast_hourly(self) -> list[Forecast] | None:
|
def _async_forecast_hourly(self) -> list[Forecast] | None:
|
||||||
"""Return the hourly forecast in native units."""
|
"""Return the hourly forecast in native units."""
|
||||||
return self._forecast(self._forecast_hourly, HOURLY)
|
return self._forecast(self.nws.forecast_hourly, HOURLY)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_forecast_twice_daily(self) -> list[Forecast] | None:
|
def _async_forecast_twice_daily(self) -> list[Forecast] | None:
|
||||||
"""Return the twice daily forecast in native units."""
|
"""Return the twice daily forecast in native units."""
|
||||||
return self._forecast(self._forecast_twice_daily, DAYNIGHT)
|
return self._forecast(self.nws.forecast, DAYNIGHT)
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if state is available."""
|
|
||||||
last_success = (
|
|
||||||
self.coordinator.last_update_success
|
|
||||||
and self.coordinator_forecast_legacy.last_update_success
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
self.coordinator.last_update_success_time
|
|
||||||
and self.coordinator_forecast_legacy.last_update_success_time
|
|
||||||
):
|
|
||||||
last_success_time = (
|
|
||||||
utcnow() - self.coordinator.last_update_success_time
|
|
||||||
< OBSERVATION_VALID_TIME
|
|
||||||
and utcnow() - self.coordinator_forecast_legacy.last_update_success_time
|
|
||||||
< FORECAST_VALID_TIME
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
last_success_time = False
|
|
||||||
return last_success or last_success_time
|
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update the entity.
|
"""Update the entity.
|
||||||
|
@ -336,4 +287,7 @@ class NWSWeather(CoordinatorWeatherEntity):
|
||||||
Only used by the generic entity update service.
|
Only used by the generic entity update service.
|
||||||
"""
|
"""
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
await self.coordinator_forecast_legacy.async_request_refresh()
|
|
||||||
|
for forecast_type in ("twice_daily", "hourly"):
|
||||||
|
if (coordinator := self.forecast_coordinators[forecast_type]) is not None:
|
||||||
|
await coordinator.async_request_refresh()
|
||||||
|
|
|
@ -2004,7 +2004,7 @@ pynobo==1.8.1
|
||||||
pynuki==1.6.3
|
pynuki==1.6.3
|
||||||
|
|
||||||
# homeassistant.components.nws
|
# homeassistant.components.nws
|
||||||
pynws==1.6.0
|
pynws[retry]==1.7.0
|
||||||
|
|
||||||
# homeassistant.components.nx584
|
# homeassistant.components.nx584
|
||||||
pynx584==0.5
|
pynx584==0.5
|
||||||
|
|
|
@ -1567,7 +1567,7 @@ pynobo==1.8.1
|
||||||
pynuki==1.6.3
|
pynuki==1.6.3
|
||||||
|
|
||||||
# homeassistant.components.nws
|
# homeassistant.components.nws
|
||||||
pynws==1.6.0
|
pynws[retry]==1.7.0
|
||||||
|
|
||||||
# homeassistant.components.nx584
|
# homeassistant.components.nx584
|
||||||
pynx584==0.5
|
pynx584==0.5
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .const import DEFAULT_FORECAST, DEFAULT_OBSERVATION
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_simple_nws():
|
def mock_simple_nws():
|
||||||
"""Mock pynws SimpleNWS with default values."""
|
"""Mock pynws SimpleNWS with default values."""
|
||||||
|
|
||||||
with patch("homeassistant.components.nws.SimpleNWS") as mock_nws:
|
with patch("homeassistant.components.nws.SimpleNWS") as mock_nws:
|
||||||
instance = mock_nws.return_value
|
instance = mock_nws.return_value
|
||||||
instance.set_station = AsyncMock(return_value=None)
|
instance.set_station = AsyncMock(return_value=None)
|
||||||
|
|
|
@ -13,7 +13,6 @@ from homeassistant.components.weather import (
|
||||||
ATTR_CONDITION_CLEAR_NIGHT,
|
ATTR_CONDITION_CLEAR_NIGHT,
|
||||||
ATTR_CONDITION_SUNNY,
|
ATTR_CONDITION_SUNNY,
|
||||||
DOMAIN as WEATHER_DOMAIN,
|
DOMAIN as WEATHER_DOMAIN,
|
||||||
LEGACY_SERVICE_GET_FORECAST,
|
|
||||||
SERVICE_GET_FORECASTS,
|
SERVICE_GET_FORECASTS,
|
||||||
)
|
)
|
||||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||||
|
@ -181,7 +180,7 @@ async def test_entity_refresh(hass: HomeAssistant, mock_simple_nws, no_sensor) -
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert instance.update_observation.call_count == 2
|
assert instance.update_observation.call_count == 2
|
||||||
assert instance.update_forecast.call_count == 2
|
assert instance.update_forecast.call_count == 2
|
||||||
instance.update_forecast_hourly.assert_called_once()
|
assert instance.update_forecast_hourly.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
async def test_error_observation(
|
async def test_error_observation(
|
||||||
|
@ -189,18 +188,8 @@ async def test_error_observation(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test error during update observation."""
|
"""Test error during update observation."""
|
||||||
utc_time = dt_util.utcnow()
|
utc_time = dt_util.utcnow()
|
||||||
with (
|
with patch("homeassistant.components.nws.utcnow") as mock_utc:
|
||||||
patch("homeassistant.components.nws.utcnow") as mock_utc,
|
|
||||||
patch("homeassistant.components.nws.weather.utcnow") as mock_utc_weather,
|
|
||||||
):
|
|
||||||
|
|
||||||
def increment_time(time):
|
|
||||||
mock_utc.return_value += time
|
|
||||||
mock_utc_weather.return_value += time
|
|
||||||
async_fire_time_changed(hass, mock_utc.return_value)
|
|
||||||
|
|
||||||
mock_utc.return_value = utc_time
|
mock_utc.return_value = utc_time
|
||||||
mock_utc_weather.return_value = utc_time
|
|
||||||
instance = mock_simple_nws.return_value
|
instance = mock_simple_nws.return_value
|
||||||
# first update fails
|
# first update fails
|
||||||
instance.update_observation.side_effect = aiohttp.ClientError
|
instance.update_observation.side_effect = aiohttp.ClientError
|
||||||
|
@ -219,68 +208,6 @@ async def test_error_observation(
|
||||||
assert state
|
assert state
|
||||||
assert state.state == STATE_UNAVAILABLE
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
# second update happens faster and succeeds
|
|
||||||
instance.update_observation.side_effect = None
|
|
||||||
increment_time(timedelta(minutes=1))
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert instance.update_observation.call_count == 2
|
|
||||||
|
|
||||||
state = hass.states.get("weather.abc")
|
|
||||||
assert state
|
|
||||||
assert state.state == ATTR_CONDITION_SUNNY
|
|
||||||
|
|
||||||
# third udate fails, but data is cached
|
|
||||||
instance.update_observation.side_effect = aiohttp.ClientError
|
|
||||||
|
|
||||||
increment_time(timedelta(minutes=10))
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert instance.update_observation.call_count == 3
|
|
||||||
|
|
||||||
state = hass.states.get("weather.abc")
|
|
||||||
assert state
|
|
||||||
assert state.state == ATTR_CONDITION_SUNNY
|
|
||||||
|
|
||||||
# after 20 minutes data caching expires, data is no longer shown
|
|
||||||
increment_time(timedelta(minutes=10))
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
state = hass.states.get("weather.abc")
|
|
||||||
assert state
|
|
||||||
assert state.state == STATE_UNAVAILABLE
|
|
||||||
|
|
||||||
|
|
||||||
async def test_error_forecast(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None:
|
|
||||||
"""Test error during update forecast."""
|
|
||||||
instance = mock_simple_nws.return_value
|
|
||||||
instance.update_forecast.side_effect = aiohttp.ClientError
|
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
|
||||||
domain=nws.DOMAIN,
|
|
||||||
data=NWS_CONFIG,
|
|
||||||
)
|
|
||||||
entry.add_to_hass(hass)
|
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
instance.update_forecast.assert_called_once()
|
|
||||||
|
|
||||||
state = hass.states.get("weather.abc")
|
|
||||||
assert state
|
|
||||||
assert state.state == STATE_UNAVAILABLE
|
|
||||||
|
|
||||||
instance.update_forecast.side_effect = None
|
|
||||||
|
|
||||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1))
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert instance.update_forecast.call_count == 2
|
|
||||||
|
|
||||||
state = hass.states.get("weather.abc")
|
|
||||||
assert state
|
|
||||||
assert state.state == ATTR_CONDITION_SUNNY
|
|
||||||
|
|
||||||
|
|
||||||
async def test_new_config_entry(hass: HomeAssistant, no_sensor) -> None:
|
async def test_new_config_entry(hass: HomeAssistant, no_sensor) -> None:
|
||||||
"""Test the expected entities are created."""
|
"""Test the expected entities are created."""
|
||||||
|
@ -304,7 +231,6 @@ async def test_new_config_entry(hass: HomeAssistant, no_sensor) -> None:
|
||||||
("service"),
|
("service"),
|
||||||
[
|
[
|
||||||
SERVICE_GET_FORECASTS,
|
SERVICE_GET_FORECASTS,
|
||||||
LEGACY_SERVICE_GET_FORECAST,
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_forecast_service(
|
async def test_forecast_service(
|
||||||
|
@ -355,7 +281,7 @@ async def test_forecast_service(
|
||||||
|
|
||||||
assert instance.update_observation.call_count == 2
|
assert instance.update_observation.call_count == 2
|
||||||
assert instance.update_forecast.call_count == 2
|
assert instance.update_forecast.call_count == 2
|
||||||
assert instance.update_forecast_hourly.call_count == 1
|
assert instance.update_forecast_hourly.call_count == 2
|
||||||
|
|
||||||
for forecast_type in ("twice_daily", "hourly"):
|
for forecast_type in ("twice_daily", "hourly"):
|
||||||
response = await hass.services.async_call(
|
response = await hass.services.async_call(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue