mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 00:07:10 +00:00
Modernize met_eireann weather (#98030)
This commit is contained in:
parent
50ef674902
commit
0d55a7600e
@ -9,13 +9,11 @@ from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_SNOWY,
|
||||
ATTR_CONDITION_SNOWY_RAINY,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_NATIVE_PRESSURE,
|
||||
ATTR_FORECAST_NATIVE_TEMP,
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW,
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
)
|
||||
@ -29,12 +27,10 @@ HOME_LOCATION_NAME = "Home"
|
||||
ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}"
|
||||
|
||||
FORECAST_MAP = {
|
||||
ATTR_FORECAST_CONDITION: "condition",
|
||||
ATTR_FORECAST_NATIVE_PRESSURE: "pressure",
|
||||
ATTR_FORECAST_PRECIPITATION: "precipitation",
|
||||
ATTR_FORECAST_NATIVE_TEMP: "temperature",
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW: "templow",
|
||||
ATTR_FORECAST_TIME: "datetime",
|
||||
ATTR_FORECAST_WIND_BEARING: "wind_bearing",
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed",
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
"""Support for Met Éireann weather service."""
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_TIME,
|
||||
Forecast,
|
||||
WeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@ -16,7 +19,7 @@ from homeassistant.const import (
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@ -28,7 +31,7 @@ from .const import CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_condition(condition: str):
|
||||
def format_condition(condition: str | None) -> str | None:
|
||||
"""Map the conditions provided by the weather API to those supported by the frontend."""
|
||||
if condition is not None:
|
||||
for key, value in CONDITION_MAP.items():
|
||||
@ -60,6 +63,9 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
|
||||
_attr_native_pressure_unit = UnitOfPressure.HPA
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||
_attr_supported_features = (
|
||||
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
||||
)
|
||||
|
||||
def __init__(self, coordinator, config, hourly):
|
||||
"""Initialise the platform with a data instance and site."""
|
||||
@ -67,6 +73,15 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
|
||||
self._config = config
|
||||
self._hourly = hourly
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
super()._handle_coordinator_update()
|
||||
assert self.platform.config_entry
|
||||
self.platform.config_entry.async_create_task(
|
||||
self.hass, self.async_update_listeners(("daily", "hourly"))
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID."""
|
||||
@ -126,35 +141,51 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
|
||||
"""Return the wind direction."""
|
||||
return self.coordinator.data.current_weather_data.get("wind_bearing")
|
||||
|
||||
@property
|
||||
def forecast(self):
|
||||
def _forecast(self, hourly: bool) -> list[Forecast]:
|
||||
"""Return the forecast array."""
|
||||
if self._hourly:
|
||||
if hourly:
|
||||
me_forecast = self.coordinator.data.hourly_forecast
|
||||
else:
|
||||
me_forecast = self.coordinator.data.daily_forecast
|
||||
required_keys = {"temperature", "datetime"}
|
||||
|
||||
ha_forecast = []
|
||||
ha_forecast: list[Forecast] = []
|
||||
|
||||
for item in me_forecast:
|
||||
if not set(item).issuperset(required_keys):
|
||||
continue
|
||||
ha_item = {
|
||||
k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None
|
||||
}
|
||||
if ha_item.get(ATTR_FORECAST_CONDITION):
|
||||
ha_item[ATTR_FORECAST_CONDITION] = format_condition(
|
||||
ha_item[ATTR_FORECAST_CONDITION]
|
||||
)
|
||||
# Convert timestamp to UTC
|
||||
if ha_item.get(ATTR_FORECAST_TIME):
|
||||
ha_item: Forecast = cast(
|
||||
Forecast,
|
||||
{
|
||||
k: item[v]
|
||||
for k, v in FORECAST_MAP.items()
|
||||
if item.get(v) is not None
|
||||
},
|
||||
)
|
||||
# Convert condition
|
||||
if item.get("condition"):
|
||||
ha_item[ATTR_FORECAST_CONDITION] = format_condition(item["condition"])
|
||||
# Convert timestamp to UTC string
|
||||
if item.get("datetime"):
|
||||
ha_item[ATTR_FORECAST_TIME] = dt_util.as_utc(
|
||||
ha_item.get(ATTR_FORECAST_TIME)
|
||||
item["datetime"]
|
||||
).isoformat()
|
||||
ha_forecast.append(ha_item)
|
||||
return ha_forecast
|
||||
|
||||
@property
|
||||
def forecast(self) -> list[Forecast]:
|
||||
"""Return the forecast array."""
|
||||
return self._forecast(self._hourly)
|
||||
|
||||
async def async_forecast_daily(self) -> list[Forecast]:
|
||||
"""Return the daily forecast in native units."""
|
||||
return self._forecast(False)
|
||||
|
||||
async def async_forecast_hourly(self) -> list[Forecast]:
|
||||
"""Return the hourly forecast in native units."""
|
||||
return self._forecast(True)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Device info."""
|
||||
|
89
tests/components/met_eireann/snapshots/test_weather.ambr
Normal file
89
tests/components/met_eireann/snapshots/test_weather.ambr
Normal file
@ -0,0 +1,89 @@
|
||||
# serializer version: 1
|
||||
# name: test_forecast_service
|
||||
dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-08T12:00:00+00:00',
|
||||
'temperature': 10.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-09T12:00:00+00:00',
|
||||
'temperature': 20.0,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service.1
|
||||
dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-08T12:00:00+00:00',
|
||||
'temperature': 10.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-09T12:00:00+00:00',
|
||||
'temperature': 20.0,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_subscription[daily]
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-08T12:00:00+00:00',
|
||||
'temperature': 10.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-09T12:00:00+00:00',
|
||||
'temperature': 20.0,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription[daily].1
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-08T12:00:00+00:00',
|
||||
'temperature': 15.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-09T12:00:00+00:00',
|
||||
'temperature': 25.0,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription[hourly]
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-08T12:00:00+00:00',
|
||||
'temperature': 10.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-09T12:00:00+00:00',
|
||||
'temperature': 20.0,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription[hourly].1
|
||||
list([
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-08T12:00:00+00:00',
|
||||
'temperature': 15.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'lightning-rainy',
|
||||
'datetime': '2023-08-09T12:00:00+00:00',
|
||||
'temperature': 25.0,
|
||||
}),
|
||||
])
|
||||
# ---
|
@ -1,13 +1,25 @@
|
||||
"""Test Met Éireann weather entity."""
|
||||
import datetime
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.met_eireann import UPDATE_INTERVAL
|
||||
from homeassistant.components.met_eireann.const import DOMAIN
|
||||
from homeassistant.components.weather import (
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
async def test_weather(hass: HomeAssistant, mock_weather) -> None:
|
||||
"""Test weather entity."""
|
||||
# Create a mock configuration for testing
|
||||
async def setup_config_entry(hass: HomeAssistant) -> ConfigEntry:
|
||||
"""Create a mock configuration for testing."""
|
||||
mock_data = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0},
|
||||
@ -16,6 +28,12 @@ async def test_weather(hass: HomeAssistant, mock_weather) -> None:
|
||||
|
||||
await hass.config_entries.async_setup(mock_data.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return mock_data
|
||||
|
||||
|
||||
async def test_weather(hass: HomeAssistant, mock_weather) -> None:
|
||||
"""Test weather entity."""
|
||||
await setup_config_entry(hass)
|
||||
assert len(hass.states.async_entity_ids("weather")) == 1
|
||||
assert len(mock_weather.mock_calls) == 4
|
||||
|
||||
@ -29,3 +47,123 @@ async def test_weather(hass: HomeAssistant, mock_weather) -> None:
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_entity_ids("weather")) == 0
|
||||
|
||||
|
||||
async def test_forecast_service(
|
||||
hass: HomeAssistant,
|
||||
mock_weather,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test multiple forecast."""
|
||||
mock_weather.get_forecast.return_value = [
|
||||
{
|
||||
"condition": "SleetSunThunder",
|
||||
"datetime": datetime.datetime(2023, 8, 8, 12, 0, tzinfo=datetime.UTC),
|
||||
"temperature": 10.0,
|
||||
},
|
||||
{
|
||||
"condition": "SleetSunThunder",
|
||||
"datetime": datetime.datetime(2023, 8, 9, 12, 0, tzinfo=datetime.UTC),
|
||||
"temperature": 20.0,
|
||||
},
|
||||
]
|
||||
|
||||
await setup_config_entry(hass)
|
||||
assert len(hass.states.async_entity_ids("weather")) == 1
|
||||
entity_id = hass.states.async_entity_ids("weather")[0]
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{
|
||||
"entity_id": entity_id,
|
||||
"type": "daily",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == snapshot
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{
|
||||
"entity_id": entity_id,
|
||||
"type": "hourly",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize("forecast_type", ["daily", "hourly"])
|
||||
async def test_forecast_subscription(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_weather,
|
||||
snapshot: SnapshotAssertion,
|
||||
forecast_type: str,
|
||||
) -> None:
|
||||
"""Test multiple forecast."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
mock_weather.get_forecast.return_value = [
|
||||
{
|
||||
"condition": "SleetSunThunder",
|
||||
"datetime": datetime.datetime(2023, 8, 8, 12, 0, tzinfo=datetime.UTC),
|
||||
"temperature": 10.0,
|
||||
},
|
||||
{
|
||||
"condition": "SleetSunThunder",
|
||||
"datetime": datetime.datetime(2023, 8, 9, 12, 0, tzinfo=datetime.UTC),
|
||||
"temperature": 20.0,
|
||||
},
|
||||
]
|
||||
|
||||
await setup_config_entry(hass)
|
||||
assert len(hass.states.async_entity_ids("weather")) == 1
|
||||
entity_id = hass.states.async_entity_ids("weather")[0]
|
||||
|
||||
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 == snapshot
|
||||
|
||||
mock_weather.get_forecast.return_value = [
|
||||
{
|
||||
"condition": "SleetSunThunder",
|
||||
"datetime": datetime.datetime(2023, 8, 8, 12, 0, tzinfo=datetime.UTC),
|
||||
"temperature": 15.0,
|
||||
},
|
||||
{
|
||||
"condition": "SleetSunThunder",
|
||||
"datetime": datetime.datetime(2023, 8, 9, 12, 0, tzinfo=datetime.UTC),
|
||||
"temperature": 25.0,
|
||||
},
|
||||
]
|
||||
|
||||
freezer.tick(UPDATE_INTERVAL + datetime.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 == snapshot
|
||||
|
Loading…
x
Reference in New Issue
Block a user