mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 08:47: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,
|
||||||
ATTR_CONDITION_SNOWY_RAINY,
|
ATTR_CONDITION_SNOWY_RAINY,
|
||||||
ATTR_CONDITION_SUNNY,
|
ATTR_CONDITION_SUNNY,
|
||||||
ATTR_FORECAST_CONDITION,
|
|
||||||
ATTR_FORECAST_NATIVE_PRESSURE,
|
ATTR_FORECAST_NATIVE_PRESSURE,
|
||||||
ATTR_FORECAST_NATIVE_TEMP,
|
ATTR_FORECAST_NATIVE_TEMP,
|
||||||
ATTR_FORECAST_NATIVE_TEMP_LOW,
|
ATTR_FORECAST_NATIVE_TEMP_LOW,
|
||||||
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
||||||
ATTR_FORECAST_PRECIPITATION,
|
ATTR_FORECAST_PRECIPITATION,
|
||||||
ATTR_FORECAST_TIME,
|
|
||||||
ATTR_FORECAST_WIND_BEARING,
|
ATTR_FORECAST_WIND_BEARING,
|
||||||
DOMAIN as WEATHER_DOMAIN,
|
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}"
|
ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}"
|
||||||
|
|
||||||
FORECAST_MAP = {
|
FORECAST_MAP = {
|
||||||
ATTR_FORECAST_CONDITION: "condition",
|
|
||||||
ATTR_FORECAST_NATIVE_PRESSURE: "pressure",
|
ATTR_FORECAST_NATIVE_PRESSURE: "pressure",
|
||||||
ATTR_FORECAST_PRECIPITATION: "precipitation",
|
ATTR_FORECAST_PRECIPITATION: "precipitation",
|
||||||
ATTR_FORECAST_NATIVE_TEMP: "temperature",
|
ATTR_FORECAST_NATIVE_TEMP: "temperature",
|
||||||
ATTR_FORECAST_NATIVE_TEMP_LOW: "templow",
|
ATTR_FORECAST_NATIVE_TEMP_LOW: "templow",
|
||||||
ATTR_FORECAST_TIME: "datetime",
|
|
||||||
ATTR_FORECAST_WIND_BEARING: "wind_bearing",
|
ATTR_FORECAST_WIND_BEARING: "wind_bearing",
|
||||||
ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed",
|
ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed",
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
"""Support for Met Éireann weather service."""
|
"""Support for Met Éireann weather service."""
|
||||||
import logging
|
import logging
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
from homeassistant.components.weather import (
|
from homeassistant.components.weather import (
|
||||||
ATTR_FORECAST_CONDITION,
|
ATTR_FORECAST_CONDITION,
|
||||||
ATTR_FORECAST_TIME,
|
ATTR_FORECAST_TIME,
|
||||||
|
Forecast,
|
||||||
WeatherEntity,
|
WeatherEntity,
|
||||||
|
WeatherEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -16,7 +19,7 @@ from homeassistant.const import (
|
|||||||
UnitOfSpeed,
|
UnitOfSpeed,
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
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__)
|
_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."""
|
"""Map the conditions provided by the weather API to those supported by the frontend."""
|
||||||
if condition is not None:
|
if condition is not None:
|
||||||
for key, value in CONDITION_MAP.items():
|
for key, value in CONDITION_MAP.items():
|
||||||
@ -60,6 +63,9 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
|
|||||||
_attr_native_pressure_unit = UnitOfPressure.HPA
|
_attr_native_pressure_unit = UnitOfPressure.HPA
|
||||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||||
|
_attr_supported_features = (
|
||||||
|
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, coordinator, config, hourly):
|
def __init__(self, coordinator, config, hourly):
|
||||||
"""Initialise the platform with a data instance and site."""
|
"""Initialise the platform with a data instance and site."""
|
||||||
@ -67,6 +73,15 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
|
|||||||
self._config = config
|
self._config = config
|
||||||
self._hourly = hourly
|
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
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return unique ID."""
|
"""Return unique ID."""
|
||||||
@ -126,35 +141,51 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity):
|
|||||||
"""Return the wind direction."""
|
"""Return the wind direction."""
|
||||||
return self.coordinator.data.current_weather_data.get("wind_bearing")
|
return self.coordinator.data.current_weather_data.get("wind_bearing")
|
||||||
|
|
||||||
@property
|
def _forecast(self, hourly: bool) -> list[Forecast]:
|
||||||
def forecast(self):
|
|
||||||
"""Return the forecast array."""
|
"""Return the forecast array."""
|
||||||
if self._hourly:
|
if hourly:
|
||||||
me_forecast = self.coordinator.data.hourly_forecast
|
me_forecast = self.coordinator.data.hourly_forecast
|
||||||
else:
|
else:
|
||||||
me_forecast = self.coordinator.data.daily_forecast
|
me_forecast = self.coordinator.data.daily_forecast
|
||||||
required_keys = {"temperature", "datetime"}
|
required_keys = {"temperature", "datetime"}
|
||||||
|
|
||||||
ha_forecast = []
|
ha_forecast: list[Forecast] = []
|
||||||
|
|
||||||
for item in me_forecast:
|
for item in me_forecast:
|
||||||
if not set(item).issuperset(required_keys):
|
if not set(item).issuperset(required_keys):
|
||||||
continue
|
continue
|
||||||
ha_item = {
|
ha_item: Forecast = cast(
|
||||||
k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None
|
Forecast,
|
||||||
}
|
{
|
||||||
if ha_item.get(ATTR_FORECAST_CONDITION):
|
k: item[v]
|
||||||
ha_item[ATTR_FORECAST_CONDITION] = format_condition(
|
for k, v in FORECAST_MAP.items()
|
||||||
ha_item[ATTR_FORECAST_CONDITION]
|
if item.get(v) is not None
|
||||||
|
},
|
||||||
)
|
)
|
||||||
# Convert timestamp to UTC
|
# Convert condition
|
||||||
if ha_item.get(ATTR_FORECAST_TIME):
|
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[ATTR_FORECAST_TIME] = dt_util.as_utc(
|
||||||
ha_item.get(ATTR_FORECAST_TIME)
|
item["datetime"]
|
||||||
).isoformat()
|
).isoformat()
|
||||||
ha_forecast.append(ha_item)
|
ha_forecast.append(ha_item)
|
||||||
return ha_forecast
|
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
|
@property
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
"""Device info."""
|
"""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."""
|
"""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.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 homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
async def test_weather(hass: HomeAssistant, mock_weather) -> None:
|
async def setup_config_entry(hass: HomeAssistant) -> ConfigEntry:
|
||||||
"""Test weather entity."""
|
"""Create a mock configuration for testing."""
|
||||||
# Create a mock configuration for testing
|
|
||||||
mock_data = MockConfigEntry(
|
mock_data = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0},
|
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.config_entries.async_setup(mock_data.entry_id)
|
||||||
await hass.async_block_till_done()
|
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(hass.states.async_entity_ids("weather")) == 1
|
||||||
assert len(mock_weather.mock_calls) == 4
|
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.config_entries.async_remove(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(hass.states.async_entity_ids("weather")) == 0
|
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