Add forecast service call for extra attributes for nws (#117254)
* add service call * fix snapshots in test * add tests * fix no data service;add test * remove unreachable code * use only extra attributes+context attributes * detailed descr. only in twice daily; add dewpoint * fix import from merge * Remove dewpoint from twice daily. nws recently removed it * cleanup unused snapshots * remove dewpoint; use short_forecast * return [] for forecasts instead of None * Use str for short_description Co-authored-by: G Johansson <goran.johansson@shiftit.se> --------- Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
parent
ec937781ca
commit
f479b64ff9
9 changed files with 221 additions and 103 deletions
|
@ -27,6 +27,7 @@ CONF_STATION = "station"
|
|||
|
||||
ATTRIBUTION = "Data from National Weather Service/NOAA"
|
||||
|
||||
ATTR_FORECAST_SHORT_DESCRIPTION: Final = "short_description"
|
||||
ATTR_FORECAST_DETAILED_DESCRIPTION: Final = "detailed_description"
|
||||
|
||||
CONDITION_CLASSES: dict[str, list[str]] = {
|
||||
|
|
5
homeassistant/components/nws/icons.json
Normal file
5
homeassistant/components/nws/icons.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"services": {
|
||||
"get_forecasts_extra": "mdi:weather-cloudy-clock"
|
||||
}
|
||||
}
|
13
homeassistant/components/nws/services.yaml
Normal file
13
homeassistant/components/nws/services.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
get_forecasts_extra:
|
||||
target:
|
||||
entity:
|
||||
domain: weather
|
||||
fields:
|
||||
type:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "hourly"
|
||||
- "twice_daily"
|
||||
translation_key: nws_forecast_type
|
|
@ -19,5 +19,25 @@
|
|||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"nws_forecast_type": {
|
||||
"options": {
|
||||
"hourly": "Hourly",
|
||||
"twice_daily": "Twice daily"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_forecasts_extra": {
|
||||
"name": "Get extra forecasts data.",
|
||||
"description": "Get extra data for weather forecasts.",
|
||||
"fields": {
|
||||
"type": {
|
||||
"name": "Forecast type",
|
||||
"description": "Forecast type: hourly or twice_daily."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ from __future__ import annotations
|
|||
|
||||
from functools import partial
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
from typing import TYPE_CHECKING, Any, Literal, Required, TypedDict, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
|
@ -31,15 +33,22 @@ from homeassistant.const import (
|
|||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import entity_platform, entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
|
||||
from homeassistant.util.json import JsonValueType
|
||||
from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter
|
||||
|
||||
from . import NWSConfigEntry, NWSData, base_unique_id, device_info
|
||||
from .const import (
|
||||
ATTR_FORECAST_DETAILED_DESCRIPTION,
|
||||
ATTR_FORECAST_SHORT_DESCRIPTION,
|
||||
ATTRIBUTION,
|
||||
CONDITION_CLASSES,
|
||||
DAYNIGHT,
|
||||
|
@ -92,15 +101,27 @@ async def async_setup_entry(
|
|||
):
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
platform.async_register_entity_service(
|
||||
"get_forecasts_extra",
|
||||
{vol.Required("type"): vol.In(("hourly", "twice_daily"))},
|
||||
"async_get_forecasts_extra_service",
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
async_add_entities([NWSWeather(entry.data, nws_data)], False)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
class ExtraForecast(TypedDict, total=False):
|
||||
"""Forecast extra fields from NWS."""
|
||||
|
||||
class NWSForecast(Forecast):
|
||||
"""Forecast with extra fields needed for NWS."""
|
||||
|
||||
detailed_description: str | None
|
||||
# common attributes
|
||||
datetime: Required[str]
|
||||
is_daytime: bool | None
|
||||
# extra attributes
|
||||
detailed_description: str | None
|
||||
short_description: str | None
|
||||
|
||||
|
||||
def _calculate_unique_id(entry_data: MappingProxyType[str, Any], mode: str) -> str:
|
||||
|
@ -217,17 +238,16 @@ class NWSWeather(CoordinatorWeatherEntity[TimestampDataUpdateCoordinator[None]])
|
|||
return None
|
||||
|
||||
def _forecast(
|
||||
self, nws_forecast: list[dict[str, Any]] | None, mode: str
|
||||
) -> list[Forecast] | None:
|
||||
self,
|
||||
nws_forecast: list[dict[str, Any]],
|
||||
mode: str,
|
||||
) -> list[Forecast]:
|
||||
"""Return forecast."""
|
||||
if nws_forecast is None:
|
||||
return None
|
||||
return []
|
||||
forecast: list[Forecast] = []
|
||||
for forecast_entry in nws_forecast:
|
||||
data: NWSForecast = {
|
||||
ATTR_FORECAST_DETAILED_DESCRIPTION: forecast_entry.get(
|
||||
"detailedForecast"
|
||||
),
|
||||
data: Forecast = {
|
||||
ATTR_FORECAST_TIME: cast(str, forecast_entry.get("startTime")),
|
||||
}
|
||||
|
||||
|
@ -273,6 +293,30 @@ class NWSWeather(CoordinatorWeatherEntity[TimestampDataUpdateCoordinator[None]])
|
|||
forecast.append(data)
|
||||
return forecast
|
||||
|
||||
def _forecast_extra(
|
||||
self,
|
||||
nws_forecast: list[dict[str, Any]] | None,
|
||||
mode: str,
|
||||
) -> list[ExtraForecast]:
|
||||
"""Return forecast."""
|
||||
if nws_forecast is None:
|
||||
return []
|
||||
forecast: list[ExtraForecast] = []
|
||||
for forecast_entry in nws_forecast:
|
||||
data: ExtraForecast = {
|
||||
ATTR_FORECAST_TIME: cast(str, forecast_entry.get("startTime")),
|
||||
}
|
||||
if mode == DAYNIGHT:
|
||||
data[ATTR_FORECAST_IS_DAYTIME] = forecast_entry.get("isDaytime")
|
||||
|
||||
data[ATTR_FORECAST_DETAILED_DESCRIPTION] = forecast_entry.get(
|
||||
"detailedForecast"
|
||||
)
|
||||
|
||||
data[ATTR_FORECAST_SHORT_DESCRIPTION] = forecast_entry.get("shortForecast")
|
||||
forecast.append(data)
|
||||
return forecast
|
||||
|
||||
@callback
|
||||
def _async_forecast_hourly(self) -> list[Forecast] | None:
|
||||
"""Return the hourly forecast in native units."""
|
||||
|
@ -293,3 +337,13 @@ class NWSWeather(CoordinatorWeatherEntity[TimestampDataUpdateCoordinator[None]])
|
|||
for forecast_type in ("twice_daily", "hourly"):
|
||||
if (coordinator := self.forecast_coordinators[forecast_type]) is not None:
|
||||
await coordinator.async_request_refresh()
|
||||
|
||||
async def async_get_forecasts_extra_service(self, type) -> ServiceResponse:
|
||||
"""Get extra weather forecast."""
|
||||
if type == "hourly":
|
||||
nws_forecast = self._forecast_extra(self.nws.forecast_hourly, HOURLY)
|
||||
else:
|
||||
nws_forecast = self._forecast_extra(self.nws.forecast, DAYNIGHT)
|
||||
return {
|
||||
"forecast": cast(JsonValueType, nws_forecast),
|
||||
}
|
||||
|
|
|
@ -185,6 +185,7 @@ DEFAULT_FORECAST = [
|
|||
"temperature": 10,
|
||||
"windSpeedAvg": 10,
|
||||
"windBearing": 180,
|
||||
"shortForecast": "A short forecast.",
|
||||
"detailedForecast": "A detailed forecast.",
|
||||
"timestamp": "2019-08-12T23:53:00+00:00",
|
||||
"iconTime": "night",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
'number': 1,
|
||||
'probabilityOfPrecipitation': 89,
|
||||
'relativeHumidity': 75,
|
||||
'shortForecast': 'A short forecast.',
|
||||
'startTime': '2019-08-12T20:00:00-04:00',
|
||||
'temperature': 10,
|
||||
'timestamp': '2019-08-12T23:53:00+00:00',
|
||||
|
@ -48,6 +49,7 @@
|
|||
'number': 1,
|
||||
'probabilityOfPrecipitation': 89,
|
||||
'relativeHumidity': 75,
|
||||
'shortForecast': 'A short forecast.',
|
||||
'startTime': '2019-08-12T20:00:00-04:00',
|
||||
'temperature': 10,
|
||||
'timestamp': '2019-08-12T23:53:00+00:00',
|
||||
|
|
|
@ -1,95 +1,44 @@
|
|||
# serializer version: 1
|
||||
# name: test_forecast_service[get_forecast]
|
||||
# name: test_detailed_forecast_service[hourly]
|
||||
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,
|
||||
}),
|
||||
]),
|
||||
'weather.abc': dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'short_description': 'A short forecast.',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service[get_forecast].1
|
||||
# name: test_detailed_forecast_service[twice_daily]
|
||||
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,
|
||||
}),
|
||||
]),
|
||||
'weather.abc': dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'datetime': '2019-08-12T20:00:00-04:00',
|
||||
'detailed_description': 'A detailed forecast.',
|
||||
'is_daytime': False,
|
||||
'short_description': 'A short forecast.',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service[get_forecast].2
|
||||
# name: test_detailed_forecast_service_no_data[hourly]
|
||||
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,
|
||||
}),
|
||||
]),
|
||||
'weather.abc': dict({
|
||||
'forecast': list([
|
||||
]),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service[get_forecast].3
|
||||
# name: test_detailed_forecast_service_no_data[twice_daily]
|
||||
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[get_forecast].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[get_forecast].5
|
||||
dict({
|
||||
'forecast': list([
|
||||
]),
|
||||
'weather.abc': dict({
|
||||
'forecast': list([
|
||||
]),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service[get_forecasts]
|
||||
|
@ -99,7 +48,6 @@
|
|||
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,
|
||||
|
@ -119,7 +67,6 @@
|
|||
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,
|
||||
|
@ -138,7 +85,6 @@
|
|||
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,
|
||||
|
@ -158,7 +104,6 @@
|
|||
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,
|
||||
|
@ -177,7 +122,6 @@
|
|||
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,
|
||||
|
@ -202,7 +146,6 @@
|
|||
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,
|
||||
|
@ -217,7 +160,6 @@
|
|||
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,
|
||||
|
|
|
@ -554,3 +554,83 @@ async def test_forecast_subscription_with_failing_coordinator(
|
|||
)
|
||||
msg = await client.receive_json()
|
||||
assert not msg["success"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("forecast_type"),
|
||||
[
|
||||
"hourly",
|
||||
"twice_daily",
|
||||
],
|
||||
)
|
||||
async def test_detailed_forecast_service(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_simple_nws,
|
||||
no_sensor,
|
||||
forecast_type: str,
|
||||
) -> None:
|
||||
"""Test detailed forecast."""
|
||||
|
||||
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()
|
||||
|
||||
response = await hass.services.async_call(
|
||||
nws.DOMAIN,
|
||||
"get_forecasts_extra",
|
||||
{
|
||||
"entity_id": "weather.abc",
|
||||
"type": forecast_type,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("forecast_type"),
|
||||
[
|
||||
"hourly",
|
||||
"twice_daily",
|
||||
],
|
||||
)
|
||||
async def test_detailed_forecast_service_no_data(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_simple_nws,
|
||||
no_sensor,
|
||||
forecast_type: str,
|
||||
) -> None:
|
||||
"""Test detailed forecast."""
|
||||
instance = mock_simple_nws.return_value
|
||||
instance.forecast = None
|
||||
instance.forecast_hourly = None
|
||||
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()
|
||||
|
||||
response = await hass.services.async_call(
|
||||
nws.DOMAIN,
|
||||
"get_forecasts_extra",
|
||||
{
|
||||
"entity_id": "weather.abc",
|
||||
"type": forecast_type,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == snapshot
|
||||
|
|
Loading…
Add table
Reference in a new issue