diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py index 1cab9c9099f..9316aad1b17 100644 --- a/homeassistant/components/met_eireann/const.py +++ b/homeassistant/components/met_eireann/const.py @@ -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", } diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index bf0d7214c6e..67c0a830c61 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -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.""" diff --git a/tests/components/met_eireann/snapshots/test_weather.ambr b/tests/components/met_eireann/snapshots/test_weather.ambr new file mode 100644 index 00000000000..81d7a52aa06 --- /dev/null +++ b/tests/components/met_eireann/snapshots/test_weather.ambr @@ -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, + }), + ]) +# --- diff --git a/tests/components/met_eireann/test_weather.py b/tests/components/met_eireann/test_weather.py index 6983a47ff4b..e14cd485cc6 100644 --- a/tests/components/met_eireann/test_weather.py +++ b/tests/components/met_eireann/test_weather.py @@ -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