Modernize nws weather (#98748)
This commit is contained in:
parent
79811984f0
commit
68e2809c36
6 changed files with 619 additions and 47 deletions
|
@ -90,7 +90,6 @@ class NwsDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||
# the base class allows None, but this one doesn't
|
||||
assert self.update_interval is not None
|
||||
update_interval = self.update_interval
|
||||
self.last_update_success_time = utcnow()
|
||||
else:
|
||||
update_interval = self.failed_update_interval
|
||||
self._unsub_refresh = async_track_point_in_utc_time(
|
||||
|
@ -99,6 +98,23 @@ class NwsDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||
utcnow().replace(microsecond=0) + update_interval,
|
||||
)
|
||||
|
||||
async def _async_refresh(
|
||||
self,
|
||||
log_failures: bool = True,
|
||||
raise_on_auth_failed: bool = False,
|
||||
scheduled: bool = False,
|
||||
raise_on_entry_error: bool = False,
|
||||
) -> None:
|
||||
"""Refresh data."""
|
||||
await super()._async_refresh(
|
||||
log_failures,
|
||||
raise_on_auth_failed,
|
||||
scheduled,
|
||||
raise_on_entry_error,
|
||||
)
|
||||
if self.last_update_success:
|
||||
self.last_update_success_time = utcnow()
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a National Weather Service entry."""
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
|
@ -25,7 +26,7 @@ CONF_STATION = "station"
|
|||
|
||||
ATTRIBUTION = "Data from National Weather Service/NOAA"
|
||||
|
||||
ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description"
|
||||
ATTR_FORECAST_DETAILED_DESCRIPTION: Final = "detailed_description"
|
||||
|
||||
CONDITION_CLASSES: dict[str, list[str]] = {
|
||||
ATTR_CONDITION_EXCEPTIONAL: [
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
"""Support for NWS weather service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
|
@ -16,8 +17,10 @@ from homeassistant.components.weather import (
|
|||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
Forecast,
|
||||
WeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
|
@ -29,13 +32,19 @@ from homeassistant.const import (
|
|||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
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_system import UnitSystem
|
||||
|
||||
from . import NWSData, base_unique_id, device_info
|
||||
from . import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
NWSData,
|
||||
NwsDataUpdateCoordinator,
|
||||
base_unique_id,
|
||||
device_info,
|
||||
)
|
||||
from .const import (
|
||||
ATTR_FORECAST_DETAILED_DESCRIPTION,
|
||||
ATTRIBUTION,
|
||||
|
@ -80,15 +89,20 @@ async def async_setup_entry(
|
|||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the NWS weather platform."""
|
||||
entity_registry = er.async_get(hass)
|
||||
nws_data: NWSData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
NWSWeather(entry.data, nws_data, DAYNIGHT, hass.config.units),
|
||||
NWSWeather(entry.data, nws_data, HOURLY, hass.config.units),
|
||||
],
|
||||
False,
|
||||
)
|
||||
entities = [NWSWeather(entry.data, nws_data, DAYNIGHT)]
|
||||
|
||||
# Add hourly entity to legacy config entries
|
||||
if entity_registry.async_get_entity_id(
|
||||
WEATHER_DOMAIN,
|
||||
DOMAIN,
|
||||
_calculate_unique_id(entry.data, HOURLY),
|
||||
):
|
||||
entities.append(NWSWeather(entry.data, nws_data, HOURLY))
|
||||
|
||||
async_add_entities(entities, False)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -99,34 +113,51 @@ if TYPE_CHECKING:
|
|||
detailed_description: str | None
|
||||
|
||||
|
||||
def _calculate_unique_id(entry_data: MappingProxyType[str, Any], mode: str) -> str:
|
||||
"""Calculate unique ID."""
|
||||
latitude = entry_data[CONF_LATITUDE]
|
||||
longitude = entry_data[CONF_LONGITUDE]
|
||||
return f"{base_unique_id(latitude, longitude)}_{mode}"
|
||||
|
||||
|
||||
class NWSWeather(WeatherEntity):
|
||||
"""Representation of a weather condition."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_should_poll = False
|
||||
_attr_supported_features = (
|
||||
WeatherEntityFeature.FORECAST_HOURLY | WeatherEntityFeature.FORECAST_TWICE_DAILY
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry_data: MappingProxyType[str, Any],
|
||||
nws_data: NWSData,
|
||||
mode: str,
|
||||
units: UnitSystem,
|
||||
) -> None:
|
||||
"""Initialise the platform with a data instance and station name."""
|
||||
self.nws = nws_data.api
|
||||
self.latitude = entry_data[CONF_LATITUDE]
|
||||
self.longitude = entry_data[CONF_LONGITUDE]
|
||||
self.coordinator_forecast_hourly = nws_data.coordinator_forecast_hourly
|
||||
self.coordinator_forecast_twice_daily = nws_data.coordinator_forecast
|
||||
self.coordinator_observation = nws_data.coordinator_observation
|
||||
if mode == DAYNIGHT:
|
||||
self.coordinator_forecast = nws_data.coordinator_forecast
|
||||
self.coordinator_forecast_legacy = nws_data.coordinator_forecast
|
||||
else:
|
||||
self.coordinator_forecast = nws_data.coordinator_forecast_hourly
|
||||
self.coordinator_forecast_legacy = nws_data.coordinator_forecast_hourly
|
||||
self.station = self.nws.station
|
||||
self._unsub_hourly_forecast: Callable[[], None] | None = None
|
||||
self._unsub_twice_daily_forecast: Callable[[], None] | None = None
|
||||
|
||||
self.mode = mode
|
||||
|
||||
self.observation = None
|
||||
self._forecast = None
|
||||
self.observation: dict[str, Any] | None = None
|
||||
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, mode)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Set up a listener and load data."""
|
||||
|
@ -134,20 +165,72 @@ class NWSWeather(WeatherEntity):
|
|||
self.coordinator_observation.async_add_listener(self._update_callback)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.coordinator_forecast.async_add_listener(self._update_callback)
|
||||
self.coordinator_forecast_legacy.async_add_listener(self._update_callback)
|
||||
)
|
||||
self.async_on_remove(self._remove_hourly_forecast_listener)
|
||||
self.async_on_remove(self._remove_twice_daily_forecast_listener)
|
||||
self._update_callback()
|
||||
|
||||
def _remove_hourly_forecast_listener(self) -> None:
|
||||
"""Remove hourly forecast listener."""
|
||||
if self._unsub_hourly_forecast:
|
||||
self._unsub_hourly_forecast()
|
||||
self._unsub_hourly_forecast = None
|
||||
|
||||
def _remove_twice_daily_forecast_listener(self) -> None:
|
||||
"""Remove hourly forecast listener."""
|
||||
if self._unsub_twice_daily_forecast:
|
||||
self._unsub_twice_daily_forecast()
|
||||
self._unsub_twice_daily_forecast = None
|
||||
|
||||
@callback
|
||||
def _async_subscription_started(
|
||||
self,
|
||||
forecast_type: Literal["daily", "hourly", "twice_daily"],
|
||||
) -> None:
|
||||
"""Start subscription to forecast_type."""
|
||||
if forecast_type == "hourly" and self.mode == DAYNIGHT:
|
||||
self._unsub_hourly_forecast = (
|
||||
self.coordinator_forecast_hourly.async_add_listener(
|
||||
self._update_callback
|
||||
)
|
||||
)
|
||||
return
|
||||
if forecast_type == "twice_daily" and self.mode == HOURLY:
|
||||
self._unsub_twice_daily_forecast = (
|
||||
self.coordinator_forecast_twice_daily.async_add_listener(
|
||||
self._update_callback
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
@callback
|
||||
def _async_subscription_ended(
|
||||
self,
|
||||
forecast_type: Literal["daily", "hourly", "twice_daily"],
|
||||
) -> None:
|
||||
"""End subscription to forecast_type."""
|
||||
if forecast_type == "hourly" and self.mode == DAYNIGHT:
|
||||
self._remove_hourly_forecast_listener()
|
||||
if forecast_type == "twice_daily" and self.mode == HOURLY:
|
||||
self._remove_twice_daily_forecast_listener()
|
||||
|
||||
@callback
|
||||
def _update_callback(self) -> None:
|
||||
"""Load data from integration."""
|
||||
self.observation = self.nws.observation
|
||||
self._forecast_hourly = self.nws.forecast_hourly
|
||||
self._forecast_twice_daily = self.nws.forecast
|
||||
if self.mode == DAYNIGHT:
|
||||
self._forecast = self.nws.forecast
|
||||
self._forecast_legacy = self.nws.forecast
|
||||
else:
|
||||
self._forecast = self.nws.forecast_hourly
|
||||
self._forecast_legacy = self.nws.forecast_hourly
|
||||
|
||||
self.async_write_ha_state()
|
||||
assert self.platform.config_entry
|
||||
self.platform.config_entry.async_create_task(
|
||||
self.hass, self.async_update_listeners(("hourly", "twice_daily"))
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -210,7 +293,7 @@ class NWSWeather(WeatherEntity):
|
|||
weather = None
|
||||
if self.observation:
|
||||
weather = self.observation.get("iconWeather")
|
||||
time = self.observation.get("iconTime")
|
||||
time = cast(str, self.observation.get("iconTime"))
|
||||
|
||||
if weather:
|
||||
return convert_condition(time, weather)
|
||||
|
@ -228,18 +311,19 @@ class NWSWeather(WeatherEntity):
|
|||
"""Return visibility unit."""
|
||||
return UnitOfLength.METERS
|
||||
|
||||
@property
|
||||
def forecast(self) -> list[Forecast] | None:
|
||||
def _forecast(
|
||||
self, nws_forecast: list[dict[str, Any]] | None, mode: str
|
||||
) -> list[Forecast] | None:
|
||||
"""Return forecast."""
|
||||
if self._forecast is None:
|
||||
if nws_forecast is None:
|
||||
return None
|
||||
forecast: list[NWSForecast] = []
|
||||
for forecast_entry in self._forecast:
|
||||
data = {
|
||||
forecast: list[Forecast] = []
|
||||
for forecast_entry in nws_forecast:
|
||||
data: NWSForecast = {
|
||||
ATTR_FORECAST_DETAILED_DESCRIPTION: forecast_entry.get(
|
||||
"detailedForecast"
|
||||
),
|
||||
ATTR_FORECAST_TIME: forecast_entry.get("startTime"),
|
||||
ATTR_FORECAST_TIME: cast(str, forecast_entry.get("startTime")),
|
||||
}
|
||||
|
||||
if (temp := forecast_entry.get("temperature")) is not None:
|
||||
|
@ -262,7 +346,7 @@ class NWSWeather(WeatherEntity):
|
|||
|
||||
data[ATTR_FORECAST_HUMIDITY] = forecast_entry.get("relativeHumidity")
|
||||
|
||||
if self.mode == DAYNIGHT:
|
||||
if mode == DAYNIGHT:
|
||||
data[ATTR_FORECAST_IS_DAYTIME] = forecast_entry.get("isDaytime")
|
||||
|
||||
time = forecast_entry.get("iconTime")
|
||||
|
@ -285,25 +369,56 @@ class NWSWeather(WeatherEntity):
|
|||
return forecast
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique_id for this entity."""
|
||||
return f"{base_unique_id(self.latitude, self.longitude)}_{self.mode}"
|
||||
def forecast(self) -> list[Forecast] | None:
|
||||
"""Return forecast."""
|
||||
return self._forecast(self._forecast_legacy, self.mode)
|
||||
|
||||
async def _async_forecast(
|
||||
self,
|
||||
coordinator: NwsDataUpdateCoordinator,
|
||||
nws_forecast: list[dict[str, Any]] | None,
|
||||
mode: str,
|
||||
) -> list[Forecast] | None:
|
||||
"""Refresh stale forecast and return it in native units."""
|
||||
if (
|
||||
not (last_success_time := coordinator.last_update_success_time)
|
||||
or utcnow() - last_success_time >= DEFAULT_SCAN_INTERVAL
|
||||
):
|
||||
await coordinator.async_refresh()
|
||||
if (
|
||||
not (last_success_time := coordinator.last_update_success_time)
|
||||
or utcnow() - last_success_time >= FORECAST_VALID_TIME
|
||||
):
|
||||
return None
|
||||
return self._forecast(nws_forecast, mode)
|
||||
|
||||
async def async_forecast_hourly(self) -> list[Forecast] | None:
|
||||
"""Return the hourly forecast in native units."""
|
||||
coordinator = self.coordinator_forecast_hourly
|
||||
return await self._async_forecast(coordinator, self._forecast_hourly, HOURLY)
|
||||
|
||||
async def async_forecast_twice_daily(self) -> list[Forecast] | None:
|
||||
"""Return the twice daily forecast in native units."""
|
||||
coordinator = self.coordinator_forecast_twice_daily
|
||||
return await self._async_forecast(
|
||||
coordinator, self._forecast_twice_daily, DAYNIGHT
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if state is available."""
|
||||
last_success = (
|
||||
self.coordinator_observation.last_update_success
|
||||
and self.coordinator_forecast.last_update_success
|
||||
and self.coordinator_forecast_legacy.last_update_success
|
||||
)
|
||||
if (
|
||||
self.coordinator_observation.last_update_success_time
|
||||
and self.coordinator_forecast.last_update_success_time
|
||||
and self.coordinator_forecast_legacy.last_update_success_time
|
||||
):
|
||||
last_success_time = (
|
||||
utcnow() - self.coordinator_observation.last_update_success_time
|
||||
< OBSERVATION_VALID_TIME
|
||||
and utcnow() - self.coordinator_forecast.last_update_success_time
|
||||
and utcnow() - self.coordinator_forecast_legacy.last_update_success_time
|
||||
< FORECAST_VALID_TIME
|
||||
)
|
||||
else:
|
||||
|
@ -316,7 +431,7 @@ class NWSWeather(WeatherEntity):
|
|||
Only used by the generic entity update service.
|
||||
"""
|
||||
await self.coordinator_observation.async_request_refresh()
|
||||
await self.coordinator_forecast.async_request_refresh()
|
||||
await self.coordinator_forecast_legacy.async_request_refresh()
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
|
|
|
@ -1079,6 +1079,22 @@ class WeatherEntity(Entity, PostInit):
|
|||
) and custom_unit_visibility in VALID_UNITS[ATTR_WEATHER_VISIBILITY_UNIT]:
|
||||
self._weather_option_visibility_unit = custom_unit_visibility
|
||||
|
||||
@callback
|
||||
def _async_subscription_started(
|
||||
self,
|
||||
forecast_type: Literal["daily", "hourly", "twice_daily"],
|
||||
) -> None:
|
||||
"""Start subscription to forecast_type."""
|
||||
return None
|
||||
|
||||
@callback
|
||||
def _async_subscription_ended(
|
||||
self,
|
||||
forecast_type: Literal["daily", "hourly", "twice_daily"],
|
||||
) -> None:
|
||||
"""End subscription to forecast_type."""
|
||||
return None
|
||||
|
||||
@final
|
||||
@callback
|
||||
def async_subscribe_forecast(
|
||||
|
@ -1090,11 +1106,16 @@ class WeatherEntity(Entity, PostInit):
|
|||
|
||||
Called by websocket API.
|
||||
"""
|
||||
subscription_started = not self._forecast_listeners[forecast_type]
|
||||
self._forecast_listeners[forecast_type].append(forecast_listener)
|
||||
if subscription_started:
|
||||
self._async_subscription_started(forecast_type)
|
||||
|
||||
@callback
|
||||
def unsubscribe() -> None:
|
||||
self._forecast_listeners[forecast_type].remove(forecast_listener)
|
||||
if not self._forecast_listeners[forecast_type]:
|
||||
self._async_subscription_ended(forecast_type)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
|
|
229
tests/components/nws/snapshots/test_weather.ambr
Normal file
229
tests/components/nws/snapshots/test_weather.ambr
Normal file
|
@ -0,0 +1,229 @@
|
|||
# serializer version: 1
|
||||
# name: test_forecast_service
|
||||
dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'is_daytime': False,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service.1
|
||||
dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service.2
|
||||
dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'is_daytime': False,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service.3
|
||||
dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service.4
|
||||
dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service.5
|
||||
dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_subscription[hourly-weather.abc_daynight]
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription[hourly-weather.abc_daynight].1
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription[hourly]
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription[hourly].1
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription[twice_daily-weather.abc_hourly]
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'is_daytime': False,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription[twice_daily-weather.abc_hourly].1
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'is_daytime': False,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription[twice_daily]
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'is_daytime': False,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription[twice_daily].1
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'dew_point': -15.6,
|
||||
'humidity': 75,
|
||||
'is_daytime': False,
|
||||
'precipitation_probability': 89,
|
||||
'temperature': -12.2,
|
||||
'wind_bearing': 180,
|
||||
'wind_speed': 16.09,
|
||||
}),
|
||||
])
|
||||
# ---
|
|
@ -3,7 +3,9 @@ from datetime import timedelta
|
|||
from unittest.mock import patch
|
||||
|
||||
import aiohttp
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import nws
|
||||
from homeassistant.components.weather import (
|
||||
|
@ -11,6 +13,7 @@ from homeassistant.components.weather import (
|
|||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_FORECAST,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
)
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -31,6 +34,7 @@ from .const import (
|
|||
)
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -354,10 +358,10 @@ async def test_error_forecast_hourly(
|
|||
assert state.state == ATTR_CONDITION_SUNNY
|
||||
|
||||
|
||||
async def test_forecast_hourly_disable_enable(
|
||||
hass: HomeAssistant, mock_simple_nws, no_sensor
|
||||
) -> None:
|
||||
"""Test error during update forecast hourly."""
|
||||
async def test_new_config_entry(hass: HomeAssistant, no_sensor) -> None:
|
||||
"""Test the expected entities are created."""
|
||||
registry = er.async_get(hass)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=nws.DOMAIN,
|
||||
data=NWS_CONFIG,
|
||||
|
@ -367,17 +371,203 @@ async def test_forecast_hourly_disable_enable(
|
|||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_entity_ids("weather")) == 1
|
||||
entry = hass.config_entries.async_entries()[0]
|
||||
assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 1
|
||||
|
||||
|
||||
async def test_legacy_config_entry(hass: HomeAssistant, no_sensor) -> None:
|
||||
"""Test the expected entities are created."""
|
||||
registry = er.async_get(hass)
|
||||
entry = registry.async_get_or_create(
|
||||
# Pre-create the hourly entity
|
||||
registry.async_get_or_create(
|
||||
WEATHER_DOMAIN,
|
||||
nws.DOMAIN,
|
||||
"35_-75_hourly",
|
||||
)
|
||||
assert entry.disabled is True
|
||||
|
||||
# Test enabling entity
|
||||
updated_entry = registry.async_update_entity(
|
||||
entry.entity_id, **{"disabled_by": None}
|
||||
entry = MockConfigEntry(
|
||||
domain=nws.DOMAIN,
|
||||
data=NWS_CONFIG,
|
||||
)
|
||||
assert updated_entry != entry
|
||||
assert updated_entry.disabled is False
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_entity_ids("weather")) == 2
|
||||
entry = hass.config_entries.async_entries()[0]
|
||||
assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 2
|
||||
|
||||
|
||||
async def test_forecast_service(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_simple_nws,
|
||||
no_sensor,
|
||||
) -> None:
|
||||
"""Test multiple forecast."""
|
||||
instance = mock_simple_nws.return_value
|
||||
|
||||
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_observation.assert_called_once()
|
||||
instance.update_forecast.assert_called_once()
|
||||
instance.update_forecast_hourly.assert_called_once()
|
||||
|
||||
for forecast_type in ("twice_daily", "hourly"):
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{
|
||||
"entity_id": "weather.abc_daynight",
|
||||
"type": forecast_type,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response["forecast"] != []
|
||||
assert response == snapshot
|
||||
|
||||
# Calling the services should use cached data
|
||||
instance.update_observation.assert_called_once()
|
||||
instance.update_forecast.assert_called_once()
|
||||
instance.update_forecast_hourly.assert_called_once()
|
||||
|
||||
# Trigger data refetch
|
||||
freezer.tick(nws.DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert instance.update_observation.call_count == 2
|
||||
assert instance.update_forecast.call_count == 2
|
||||
assert instance.update_forecast_hourly.call_count == 1
|
||||
|
||||
for forecast_type in ("twice_daily", "hourly"):
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{
|
||||
"entity_id": "weather.abc_daynight",
|
||||
"type": forecast_type,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response["forecast"] != []
|
||||
assert response == snapshot
|
||||
|
||||
# Calling the services should update the hourly forecast
|
||||
assert instance.update_observation.call_count == 2
|
||||
assert instance.update_forecast.call_count == 2
|
||||
assert instance.update_forecast_hourly.call_count == 2
|
||||
|
||||
# third update fails, but data is cached
|
||||
instance.update_forecast_hourly.side_effect = aiohttp.ClientError
|
||||
freezer.tick(nws.DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{
|
||||
"entity_id": "weather.abc_daynight",
|
||||
"type": "hourly",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response["forecast"] != []
|
||||
assert response == snapshot
|
||||
|
||||
# after additional 35 minutes data caching expires, data is no longer shown
|
||||
freezer.tick(timedelta(minutes=35))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{
|
||||
"entity_id": "weather.abc_daynight",
|
||||
"type": "hourly",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response["forecast"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("forecast_type", "entity_id"),
|
||||
[("hourly", "weather.abc_daynight"), ("twice_daily", "weather.abc_hourly")],
|
||||
)
|
||||
async def test_forecast_subscription(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_simple_nws,
|
||||
no_sensor,
|
||||
forecast_type: str,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Test multiple forecast."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
registry = er.async_get(hass)
|
||||
# Pre-create the hourly entity
|
||||
registry.async_get_or_create(
|
||||
WEATHER_DOMAIN,
|
||||
nws.DOMAIN,
|
||||
"35_-75_hourly",
|
||||
suggested_object_id="abc_hourly",
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "weather/subscribe_forecast",
|
||||
"forecast_type": forecast_type,
|
||||
"entity_id": entity_id,
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] is None
|
||||
subscription_id = msg["id"]
|
||||
|
||||
msg = await client.receive_json()
|
||||
assert msg["id"] == subscription_id
|
||||
assert msg["type"] == "event"
|
||||
forecast1 = msg["event"]["forecast"]
|
||||
|
||||
assert forecast1 != []
|
||||
assert forecast1 == snapshot
|
||||
|
||||
freezer.tick(nws.DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
|
||||
await hass.async_block_till_done()
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["id"] == subscription_id
|
||||
assert msg["type"] == "event"
|
||||
forecast2 = msg["event"]["forecast"]
|
||||
|
||||
assert forecast2 != []
|
||||
assert forecast2 == snapshot
|
||||
|
|
Loading…
Add table
Reference in a new issue