From 81c123931afb36a33c119c95569bf6f6c37aeeab Mon Sep 17 00:00:00 2001 From: Andrew <34544450+10100011@users.noreply.github.com> Date: Sat, 19 Oct 2024 19:42:47 +0100 Subject: [PATCH 1/9] Adding OpenWeatherMap Minutely forecast action --- .../components/openweathermap/__init__.py | 3 ++ .../components/openweathermap/const.py | 1 + .../components/openweathermap/coordinator.py | 28 +++++++++++---- .../components/openweathermap/icons.json | 7 ++++ .../components/openweathermap/manifest.json | 2 +- .../components/openweathermap/services.py | 35 +++++++++++++++++++ .../components/openweathermap/services.yaml | 1 + .../components/openweathermap/strings.json | 6 ++++ 8 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/openweathermap/icons.json create mode 100644 homeassistant/components/openweathermap/services.py create mode 100644 homeassistant/components/openweathermap/services.yaml diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 33cd23c4f6c229..52ca18ed10a7c7 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -21,6 +21,7 @@ from .const import CONFIG_FLOW_VERSION, OWM_MODE_V25, PLATFORMS from .coordinator import WeatherUpdateCoordinator from .repairs import async_create_issue, async_delete_issue +from .services import async_setup_services from .utils import build_data_and_options _LOGGER = logging.getLogger(__name__) @@ -66,6 +67,8 @@ async def async_setup_entry( await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await async_setup_services(hass, mode, weather_coordinator) + return True diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 81a6544c7cea58..7056c71ab9f66d 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -48,6 +48,7 @@ ATTR_API_CLOUD_COVERAGE = "cloud_coverage" ATTR_API_FORECAST = "forecast" ATTR_API_CURRENT = "current" +ATTR_API_MINUTELY_FORECAST = "minutely_forecast" ATTR_API_HOURLY_FORECAST = "hourly_forecast" ATTR_API_DAILY_FORECAST = "daily_forecast" UPDATE_LISTENER = "update_listener" diff --git a/homeassistant/components/openweathermap/coordinator.py b/homeassistant/components/openweathermap/coordinator.py index f7672a1290bc51..ccf4555b6184a9 100644 --- a/homeassistant/components/openweathermap/coordinator.py +++ b/homeassistant/components/openweathermap/coordinator.py @@ -17,6 +17,7 @@ ATTR_CONDITION_SUNNY, Forecast, ) +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -31,6 +32,7 @@ ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_API_HOURLY_FORECAST, ATTR_API_HUMIDITY, + ATTR_API_MINUTELY_FORECAST, ATTR_API_PRECIPITATION_KIND, ATTR_API_PRESSURE, ATTR_API_RAIN, @@ -94,6 +96,11 @@ def _convert_weather_response(self, weather_report: WeatherReport): return { ATTR_API_CURRENT: current_weather, + ATTR_API_MINUTELY_FORECAST: ( + self._get_minutely_weather_data(weather_report.minutely_forecast) + if weather_report.minutely_forecast is not None + else {} + ), ATTR_API_HOURLY_FORECAST: [ self._get_hourly_forecast_weather_data(item) for item in weather_report.hourly_forecast @@ -104,6 +111,14 @@ def _convert_weather_response(self, weather_report: WeatherReport): ], } + def _get_minutely_weather_data(self, minutely_forecast): + forecasts = [ + {"datetime": item.date_time, "precipitation": round(item.precipitation, 2)} + for item in minutely_forecast + ] + + return {f"{Platform.WEATHER}.{DOMAIN}": {"forecast": forecasts}} + def _get_current_weather_data(self, current_weather: CurrentWeather): return { ATTR_API_CONDITION: self._get_condition(current_weather.condition.id), @@ -192,12 +207,13 @@ def _calc_precipitation_kind(rain, snow): @staticmethod def _get_precipitation_value(precipitation): """Get precipitation value from weather data.""" - if "all" in precipitation: - return round(precipitation["all"], 2) - if "3h" in precipitation: - return round(precipitation["3h"], 2) - if "1h" in precipitation: - return round(precipitation["1h"], 2) + if precipitation is not None: + if "all" in precipitation: + return round(precipitation["all"], 2) + if "3h" in precipitation: + return round(precipitation["3h"], 2) + if "1h" in precipitation: + return round(precipitation["1h"], 2) return 0 def _get_condition(self, weather_code, timestamp=None): diff --git a/homeassistant/components/openweathermap/icons.json b/homeassistant/components/openweathermap/icons.json new file mode 100644 index 00000000000000..4b46fb18ef3b96 --- /dev/null +++ b/homeassistant/components/openweathermap/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "get_minutely_forecasts": { + "service": "mdi:weather-snowy-rainy" + } + } +} diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index 199e750ad4f3fa..14313a5a77e846 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/openweathermap", "iot_class": "cloud_polling", "loggers": ["pyopenweathermap"], - "requirements": ["pyopenweathermap==0.1.1"] + "requirements": ["pyopenweathermap==0.2.1"] } diff --git a/homeassistant/components/openweathermap/services.py b/homeassistant/components/openweathermap/services.py new file mode 100644 index 00000000000000..6c110514b594a1 --- /dev/null +++ b/homeassistant/components/openweathermap/services.py @@ -0,0 +1,35 @@ +"""Services for OpenWeatherMap.""" + +from __future__ import annotations + +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse + +from . import WeatherUpdateCoordinator +from .const import ATTR_API_MINUTELY_FORECAST, DOMAIN, OWM_MODE_V30 + +SERVICE_GET_MINUTELY_FORECAST = f"get_{ATTR_API_MINUTELY_FORECAST}" + + +async def async_setup_services( + hass: HomeAssistant, + mode: str, + weather_coordinator: WeatherUpdateCoordinator, +) -> None: + """Set up OpenWeatherMap services.""" + + def handle_get_minutely_forecasts(call: ServiceCall) -> None: + """Handle the service action call.""" + return weather_coordinator.data[ATTR_API_MINUTELY_FORECAST] + + if mode == OWM_MODE_V30: + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_GET_MINUTELY_FORECAST, + service_func=handle_get_minutely_forecasts, + supports_response=SupportsResponse.ONLY, + ) + else: + hass.services.async_remove( + domain=DOMAIN, + service=SERVICE_GET_MINUTELY_FORECAST, + ) diff --git a/homeassistant/components/openweathermap/services.yaml b/homeassistant/components/openweathermap/services.yaml new file mode 100644 index 00000000000000..2874e8e7ef6254 --- /dev/null +++ b/homeassistant/components/openweathermap/services.yaml @@ -0,0 +1 @@ +get_minutely_forecasts: diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index 46b5feab75c949..c81e225c819930 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -47,5 +47,11 @@ } } } + }, + "services": { + "get_minutely_forecasts": { + "name": "Get minutely forecasts", + "description": "Get minutely weather forecasts." + } } } From fdf39585f72624c7abb216a99ad79c1be1b2b79f Mon Sep 17 00:00:00 2001 From: Andrew <34544450+10100011@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:37:37 +0000 Subject: [PATCH 2/9] Actioning review comments --- homeassistant/components/openweathermap/const.py | 2 +- .../components/openweathermap/coordinator.py | 13 +++++++------ homeassistant/components/openweathermap/icons.json | 2 +- .../components/openweathermap/services.py | 14 +++++++------- .../components/openweathermap/services.yaml | 2 +- .../components/openweathermap/strings.json | 6 +++--- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 7056c71ab9f66d..de317709f5b028 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -48,7 +48,7 @@ ATTR_API_CLOUD_COVERAGE = "cloud_coverage" ATTR_API_FORECAST = "forecast" ATTR_API_CURRENT = "current" -ATTR_API_MINUTELY_FORECAST = "minutely_forecast" +ATTR_API_MINUTE_FORECAST = "minute_forecast" ATTR_API_HOURLY_FORECAST = "hourly_forecast" ATTR_API_DAILY_FORECAST = "daily_forecast" UPDATE_LISTENER = "update_listener" diff --git a/homeassistant/components/openweathermap/coordinator.py b/homeassistant/components/openweathermap/coordinator.py index ccf4555b6184a9..97c0ea667e8534 100644 --- a/homeassistant/components/openweathermap/coordinator.py +++ b/homeassistant/components/openweathermap/coordinator.py @@ -7,6 +7,7 @@ CurrentWeather, DailyWeatherForecast, HourlyWeatherForecast, + MinutelyWeatherForecast, OWMClient, RequestError, WeatherReport, @@ -32,7 +33,7 @@ ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_API_HOURLY_FORECAST, ATTR_API_HUMIDITY, - ATTR_API_MINUTELY_FORECAST, + ATTR_API_MINUTE_FORECAST, ATTR_API_PRECIPITATION_KIND, ATTR_API_PRESSURE, ATTR_API_RAIN, @@ -96,8 +97,8 @@ def _convert_weather_response(self, weather_report: WeatherReport): return { ATTR_API_CURRENT: current_weather, - ATTR_API_MINUTELY_FORECAST: ( - self._get_minutely_weather_data(weather_report.minutely_forecast) + ATTR_API_MINUTE_FORECAST: ( + self._get_minute_weather_data(weather_report.minutely_forecast) if weather_report.minutely_forecast is not None else {} ), @@ -111,13 +112,13 @@ def _convert_weather_response(self, weather_report: WeatherReport): ], } - def _get_minutely_weather_data(self, minutely_forecast): + def _get_minute_weather_data(self, minute_forecast: list[MinutelyWeatherForecast]): forecasts = [ {"datetime": item.date_time, "precipitation": round(item.precipitation, 2)} - for item in minutely_forecast + for item in minute_forecast ] - return {f"{Platform.WEATHER}.{DOMAIN}": {"forecast": forecasts}} + return {Platform.WEATHER + "." + DOMAIN: {"forecast": forecasts}} def _get_current_weather_data(self, current_weather: CurrentWeather): return { diff --git a/homeassistant/components/openweathermap/icons.json b/homeassistant/components/openweathermap/icons.json index 4b46fb18ef3b96..d28a958ce2165d 100644 --- a/homeassistant/components/openweathermap/icons.json +++ b/homeassistant/components/openweathermap/icons.json @@ -1,6 +1,6 @@ { "services": { - "get_minutely_forecasts": { + "get_minute_forecasts": { "service": "mdi:weather-snowy-rainy" } } diff --git a/homeassistant/components/openweathermap/services.py b/homeassistant/components/openweathermap/services.py index 6c110514b594a1..4e8f4defd70a97 100644 --- a/homeassistant/components/openweathermap/services.py +++ b/homeassistant/components/openweathermap/services.py @@ -5,9 +5,9 @@ from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse from . import WeatherUpdateCoordinator -from .const import ATTR_API_MINUTELY_FORECAST, DOMAIN, OWM_MODE_V30 +from .const import ATTR_API_MINUTE_FORECAST, DOMAIN, OWM_MODE_V30 -SERVICE_GET_MINUTELY_FORECAST = f"get_{ATTR_API_MINUTELY_FORECAST}" +SERVICE_GET_MINUTE_FORECAST = f"get_{ATTR_API_MINUTE_FORECAST}" async def async_setup_services( @@ -17,19 +17,19 @@ async def async_setup_services( ) -> None: """Set up OpenWeatherMap services.""" - def handle_get_minutely_forecasts(call: ServiceCall) -> None: + def handle_get_minute_forecasts(call: ServiceCall) -> None: """Handle the service action call.""" - return weather_coordinator.data[ATTR_API_MINUTELY_FORECAST] + return weather_coordinator.data[ATTR_API_MINUTE_FORECAST] if mode == OWM_MODE_V30: hass.services.async_register( domain=DOMAIN, - service=SERVICE_GET_MINUTELY_FORECAST, - service_func=handle_get_minutely_forecasts, + service=SERVICE_GET_MINUTE_FORECAST, + service_func=handle_get_minute_forecasts, supports_response=SupportsResponse.ONLY, ) else: hass.services.async_remove( domain=DOMAIN, - service=SERVICE_GET_MINUTELY_FORECAST, + service=SERVICE_GET_MINUTE_FORECAST, ) diff --git a/homeassistant/components/openweathermap/services.yaml b/homeassistant/components/openweathermap/services.yaml index 2874e8e7ef6254..f28f1a5f9b09c9 100644 --- a/homeassistant/components/openweathermap/services.yaml +++ b/homeassistant/components/openweathermap/services.yaml @@ -1 +1 @@ -get_minutely_forecasts: +get_minute_forecasts: diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index c81e225c819930..6c01a35031b4f0 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -49,9 +49,9 @@ } }, "services": { - "get_minutely_forecasts": { - "name": "Get minutely forecasts", - "description": "Get minutely weather forecasts." + "get_minute_forecasts": { + "name": "Get minute forecasts", + "description": "Get minute weather forecasts." } } } From ede911f8ca52d09dafc4bc734669a8c28f3259f6 Mon Sep 17 00:00:00 2001 From: Andrew <34544450+10100011@users.noreply.github.com> Date: Mon, 21 Oct 2024 20:27:22 +0000 Subject: [PATCH 3/9] Check for correct mode per action call --- .../components/openweathermap/services.py | 29 ++++++++++--------- .../components/openweathermap/strings.json | 5 ++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/openweathermap/services.py b/homeassistant/components/openweathermap/services.py index 4e8f4defd70a97..499b0c77e4a754 100644 --- a/homeassistant/components/openweathermap/services.py +++ b/homeassistant/components/openweathermap/services.py @@ -3,9 +3,10 @@ from __future__ import annotations from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse +from homeassistant.exceptions import ServiceValidationError from . import WeatherUpdateCoordinator -from .const import ATTR_API_MINUTE_FORECAST, DOMAIN, OWM_MODE_V30 +from .const import ATTR_API_MINUTE_FORECAST, DEFAULT_NAME, DOMAIN, OWM_MODE_V30 SERVICE_GET_MINUTE_FORECAST = f"get_{ATTR_API_MINUTE_FORECAST}" @@ -19,17 +20,17 @@ async def async_setup_services( def handle_get_minute_forecasts(call: ServiceCall) -> None: """Handle the service action call.""" - return weather_coordinator.data[ATTR_API_MINUTE_FORECAST] - - if mode == OWM_MODE_V30: - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_GET_MINUTE_FORECAST, - service_func=handle_get_minute_forecasts, - supports_response=SupportsResponse.ONLY, - ) - else: - hass.services.async_remove( - domain=DOMAIN, - service=SERVICE_GET_MINUTE_FORECAST, + if mode == OWM_MODE_V30: + return weather_coordinator.data[ATTR_API_MINUTE_FORECAST] + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_minute_forecast_mode", + translation_placeholders={"name": DEFAULT_NAME}, ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_GET_MINUTE_FORECAST, + service_func=handle_get_minute_forecasts, + supports_response=SupportsResponse.ONLY, + ) diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index 6c01a35031b4f0..68df673549eb0f 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -53,5 +53,10 @@ "name": "Get minute forecasts", "description": "Get minute weather forecasts." } + }, + "exceptions": { + "service_minute_forecast_mode": { + "message": "Minute forecasts are available only when {name} mode is set to v3.0" + } } } From 956472ea1ec0ec2a8cf849be4b5d3b10d9cc58d3 Mon Sep 17 00:00:00 2001 From: Andrew <34544450+10100011@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:28:06 +0000 Subject: [PATCH 4/9] Action may now be called per entity / location --- .../components/openweathermap/__init__.py | 3 -- .../components/openweathermap/coordinator.py | 16 +++++---- .../components/openweathermap/icons.json | 2 +- .../components/openweathermap/services.py | 36 ------------------- .../components/openweathermap/services.yaml | 6 +++- .../components/openweathermap/strings.json | 8 ++--- .../components/openweathermap/weather.py | 28 ++++++++++++++- .../openweathermap/test_config_flow.py | 15 ++++---- 8 files changed, 55 insertions(+), 59 deletions(-) delete mode 100644 homeassistant/components/openweathermap/services.py diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 52ca18ed10a7c7..33cd23c4f6c229 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -21,7 +21,6 @@ from .const import CONFIG_FLOW_VERSION, OWM_MODE_V25, PLATFORMS from .coordinator import WeatherUpdateCoordinator from .repairs import async_create_issue, async_delete_issue -from .services import async_setup_services from .utils import build_data_and_options _LOGGER = logging.getLogger(__name__) @@ -67,8 +66,6 @@ async def async_setup_entry( await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - await async_setup_services(hass, mode, weather_coordinator) - return True diff --git a/homeassistant/components/openweathermap/coordinator.py b/homeassistant/components/openweathermap/coordinator.py index 97c0ea667e8534..eebf8b68ee0459 100644 --- a/homeassistant/components/openweathermap/coordinator.py +++ b/homeassistant/components/openweathermap/coordinator.py @@ -18,7 +18,6 @@ ATTR_CONDITION_SUNNY, Forecast, ) -from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -113,12 +112,15 @@ def _convert_weather_response(self, weather_report: WeatherReport): } def _get_minute_weather_data(self, minute_forecast: list[MinutelyWeatherForecast]): - forecasts = [ - {"datetime": item.date_time, "precipitation": round(item.precipitation, 2)} - for item in minute_forecast - ] - - return {Platform.WEATHER + "." + DOMAIN: {"forecast": forecasts}} + return { + "forecast": [ + { + "datetime": item.date_time, + "precipitation": round(item.precipitation, 2), + } + for item in minute_forecast + ] + } def _get_current_weather_data(self, current_weather: CurrentWeather): return { diff --git a/homeassistant/components/openweathermap/icons.json b/homeassistant/components/openweathermap/icons.json index d28a958ce2165d..d493b1538ba956 100644 --- a/homeassistant/components/openweathermap/icons.json +++ b/homeassistant/components/openweathermap/icons.json @@ -1,6 +1,6 @@ { "services": { - "get_minute_forecasts": { + "get_minute_forecast": { "service": "mdi:weather-snowy-rainy" } } diff --git a/homeassistant/components/openweathermap/services.py b/homeassistant/components/openweathermap/services.py deleted file mode 100644 index 499b0c77e4a754..00000000000000 --- a/homeassistant/components/openweathermap/services.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Services for OpenWeatherMap.""" - -from __future__ import annotations - -from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse -from homeassistant.exceptions import ServiceValidationError - -from . import WeatherUpdateCoordinator -from .const import ATTR_API_MINUTE_FORECAST, DEFAULT_NAME, DOMAIN, OWM_MODE_V30 - -SERVICE_GET_MINUTE_FORECAST = f"get_{ATTR_API_MINUTE_FORECAST}" - - -async def async_setup_services( - hass: HomeAssistant, - mode: str, - weather_coordinator: WeatherUpdateCoordinator, -) -> None: - """Set up OpenWeatherMap services.""" - - def handle_get_minute_forecasts(call: ServiceCall) -> None: - """Handle the service action call.""" - if mode == OWM_MODE_V30: - return weather_coordinator.data[ATTR_API_MINUTE_FORECAST] - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="service_minute_forecast_mode", - translation_placeholders={"name": DEFAULT_NAME}, - ) - - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_GET_MINUTE_FORECAST, - service_func=handle_get_minute_forecasts, - supports_response=SupportsResponse.ONLY, - ) diff --git a/homeassistant/components/openweathermap/services.yaml b/homeassistant/components/openweathermap/services.yaml index f28f1a5f9b09c9..6bbcf1b23e4e01 100644 --- a/homeassistant/components/openweathermap/services.yaml +++ b/homeassistant/components/openweathermap/services.yaml @@ -1 +1,5 @@ -get_minute_forecasts: +get_minute_forecast: + target: + entity: + domain: weather + integration: openweathermap diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index 68df673549eb0f..0692087bc23f8e 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -49,14 +49,14 @@ } }, "services": { - "get_minute_forecasts": { - "name": "Get minute forecasts", - "description": "Get minute weather forecasts." + "get_minute_forecast": { + "name": "Get minute forecast", + "description": "Get minute weather forecast." } }, "exceptions": { "service_minute_forecast_mode": { - "message": "Minute forecasts are available only when {name} mode is set to v3.0" + "message": "Minute forecast is available only when {name} mode is set to v3.0" } } } diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 3a134a0ee26679..9476bd608f38c6 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -14,7 +14,9 @@ UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, SupportsResponse, callback +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_platform from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -28,6 +30,7 @@ ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_API_HOURLY_FORECAST, ATTR_API_HUMIDITY, + ATTR_API_MINUTE_FORECAST, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, ATTR_API_VISIBILITY_DISTANCE, @@ -44,6 +47,8 @@ ) from .coordinator import WeatherUpdateCoordinator +SERVICE_GET_MINUTE_FORECAST = "get_minute_forecast" + async def async_setup_entry( hass: HomeAssistant, @@ -61,6 +66,14 @@ async def async_setup_entry( async_add_entities([owm_weather], False) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + name=SERVICE_GET_MINUTE_FORECAST, + schema=None, + func="async_get_minute_forecast", + supports_response=SupportsResponse.ONLY, + ) + class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]): """Implementation of an OpenWeatherMap sensor.""" @@ -91,6 +104,8 @@ def __init__( manufacturer=MANUFACTURER, name=DEFAULT_NAME, ) + self.mode = mode + self.weather_coordinator = weather_coordinator if mode in (OWM_MODE_V30, OWM_MODE_V25): self._attr_supported_features = ( @@ -100,6 +115,17 @@ def __init__( elif mode == OWM_MODE_FREE_FORECAST: self._attr_supported_features = WeatherEntityFeature.FORECAST_HOURLY + async def async_get_minute_forecast(self) -> None: + """Return Minute forecast.""" + + if self.mode == OWM_MODE_V30: + return self.weather_coordinator.data[ATTR_API_MINUTE_FORECAST] + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_minute_forecast_mode", + translation_placeholders={"name": DEFAULT_NAME}, + ) + @property def condition(self) -> str | None: """Return the current condition.""" diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index aec343607544a8..f2d21c018cea22 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -60,8 +60,8 @@ def _create_mocked_owm_factory(is_valid: bool): wind_speed=9.83, wind_bearing=199, wind_gust=None, - rain={}, - snow={}, + rain=None, + snow=None, condition=WeatherCondition( id=803, main="Clouds", @@ -106,11 +106,14 @@ def _create_mocked_owm_factory(is_valid: bool): rain=0, snow=0, ) - minutely_weather_forecast = MinutelyWeatherForecast( - date_time=1728672360, precipitation=2.54 - ) + minutely_weather_forecast = [ + MinutelyWeatherForecast(date_time=1728672360, precipitation=0), + MinutelyWeatherForecast(date_time=1728672420, precipitation=1.23), + MinutelyWeatherForecast(date_time=1728672480, precipitation=4.5), + MinutelyWeatherForecast(date_time=1728672540, precipitation=0), + ] weather_report = WeatherReport( - current_weather, [minutely_weather_forecast], [], [daily_weather_forecast] + current_weather, minutely_weather_forecast, [], [daily_weather_forecast] ) mocked_owm_client = MagicMock() From 3686f668bb1e9c1883c074e721d4b3a54c5774d0 Mon Sep 17 00:00:00 2001 From: Andrew <34544450+10100011@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:45:43 +0000 Subject: [PATCH 5/9] v3.0 should provide greater and more relevant test coverage --- tests/components/openweathermap/test_config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index f2d21c018cea22..6d361901296744 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -18,7 +18,7 @@ DEFAULT_LANGUAGE, DEFAULT_OWM_MODE, DOMAIN, - OWM_MODE_V25, + OWM_MODE_V30, ) from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import ( @@ -40,7 +40,7 @@ CONF_LATITUDE: 50, CONF_LONGITUDE: 40, CONF_LANGUAGE: DEFAULT_LANGUAGE, - CONF_MODE: OWM_MODE_V25, + CONF_MODE: OWM_MODE_V30, } VALID_YAML_CONFIG = {CONF_API_KEY: "foo"} From edc084280ce5c301c53550b6fc6700b2efa701c8 Mon Sep 17 00:00:00 2001 From: Andrew <34544450+10100011@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:57:20 +0000 Subject: [PATCH 6/9] Prefer consistent strings --- homeassistant/components/openweathermap/coordinator.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openweathermap/coordinator.py b/homeassistant/components/openweathermap/coordinator.py index eebf8b68ee0459..cd2bc54c91f323 100644 --- a/homeassistant/components/openweathermap/coordinator.py +++ b/homeassistant/components/openweathermap/coordinator.py @@ -28,11 +28,14 @@ ATTR_API_CONDITION, ATTR_API_CURRENT, ATTR_API_DAILY_FORECAST, + ATTR_API_DATETIME, ATTR_API_DEW_POINT, ATTR_API_FEELS_LIKE_TEMPERATURE, + ATTR_API_FORECAST, ATTR_API_HOURLY_FORECAST, ATTR_API_HUMIDITY, ATTR_API_MINUTE_FORECAST, + ATTR_API_PRECIPITATION, ATTR_API_PRECIPITATION_KIND, ATTR_API_PRESSURE, ATTR_API_RAIN, @@ -113,10 +116,10 @@ def _convert_weather_response(self, weather_report: WeatherReport): def _get_minute_weather_data(self, minute_forecast: list[MinutelyWeatherForecast]): return { - "forecast": [ + ATTR_API_FORECAST: [ { - "datetime": item.date_time, - "precipitation": round(item.precipitation, 2), + ATTR_API_DATETIME: item.date_time, + ATTR_API_PRECIPITATION: round(item.precipitation, 2), } for item in minute_forecast ] From a547b7d45da752acf96390e2d2d7f56373aeba8f Mon Sep 17 00:00:00 2001 From: Andrew <34544450+10100011@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:45:45 +0000 Subject: [PATCH 7/9] Additional test coverage --- .../components/openweathermap/test_weather.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/components/openweathermap/test_weather.py diff --git a/tests/components/openweathermap/test_weather.py b/tests/components/openweathermap/test_weather.py new file mode 100644 index 00000000000000..6e5ffab0cf3bb1 --- /dev/null +++ b/tests/components/openweathermap/test_weather.py @@ -0,0 +1,79 @@ +"""Define tests for OpenWeatherMapWeather.""" + +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.openweathermap.const import ( + ATTR_API_DATETIME, + ATTR_API_MINUTE_FORECAST, + ATTR_API_PRECIPITATION, + OWM_MODE_V25, + OWM_MODE_V30, +) +from homeassistant.components.openweathermap.coordinator import WeatherUpdateCoordinator +from homeassistant.components.openweathermap.weather import OpenWeatherMapWeather +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +MOCK_UNIQUE_ID = "mock_unique_id" +MOCK_NAME = "Mock Name" +MOCK_WEATHER_DATA = { + ATTR_API_MINUTE_FORECAST: [ + {ATTR_API_DATETIME: "2023-10-01T12:00:00Z", ATTR_API_PRECIPITATION: 0}, + {ATTR_API_DATETIME: "2023-10-01T12:01:00Z", ATTR_API_PRECIPITATION: 0.1}, + ], +} + + +@pytest.fixture +def mock_hass(): + """Create a mock HomeAssistant instance.""" + return MagicMock(spec=HomeAssistant) + + +@pytest.fixture +def mock_coordinator(): + """Create a mock WeatherUpdateCoordinator.""" + coordinator = MagicMock(spec=WeatherUpdateCoordinator) + coordinator.data = MOCK_WEATHER_DATA + return coordinator + + +@pytest.fixture +def mock_config_entry(): + """Create a mock OpenweathermapConfigEntry.""" + config_entry = MagicMock() + config_entry.unique_id = MOCK_UNIQUE_ID + config_entry.runtime_data = MagicMock() + config_entry.runtime_data.name = MOCK_NAME + config_entry.runtime_data.mode = OWM_MODE_V30 + config_entry.runtime_data.coordinator = MagicMock(spec=WeatherUpdateCoordinator) + config_entry.runtime_data.coordinator.data = MOCK_WEATHER_DATA + return config_entry + + +@pytest.fixture +def mock_add_entities(): + """Create a mock AddEntitiesCallback.""" + return MagicMock(spec=AddEntitiesCallback) + + +async def test_async_get_minute_forecast(mock_coordinator) -> None: + """Test the async_get_minute_forecast method.""" + weather = OpenWeatherMapWeather( + name=MOCK_NAME, + unique_id=MOCK_UNIQUE_ID, + mode=OWM_MODE_V30, + weather_coordinator=mock_coordinator, + ) + + # Test successful minute forecast retrieval + result = await weather.async_get_minute_forecast() + assert result == mock_coordinator.data[ATTR_API_MINUTE_FORECAST] + + # Test exception when mode is not OWM_MODE_V30 + weather.mode = OWM_MODE_V25 + with pytest.raises(ServiceValidationError): + await weather.async_get_minute_forecast() From 31f436447a3e33ed09feb90b77ede72d12a338a5 Mon Sep 17 00:00:00 2001 From: Andrew <34544450+10100011@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:59:21 +0000 Subject: [PATCH 8/9] Corrected return typing | Better use of MockConfigEntry --- .../components/openweathermap/weather.py | 2 +- .../components/openweathermap/test_weather.py | 167 ++++++++++++------ 2 files changed, 111 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 9476bd608f38c6..2e4065d86fb45e 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -115,7 +115,7 @@ def __init__( elif mode == OWM_MODE_FREE_FORECAST: self._attr_supported_features = WeatherEntityFeature.FORECAST_HOURLY - async def async_get_minute_forecast(self) -> None: + async def async_get_minute_forecast(self) -> dict[str, list[dict]] | dict: """Return Minute forecast.""" if self.mode == OWM_MODE_V30: diff --git a/tests/components/openweathermap/test_weather.py b/tests/components/openweathermap/test_weather.py index 6e5ffab0cf3bb1..6a1afcfc3bc8f5 100644 --- a/tests/components/openweathermap/test_weather.py +++ b/tests/components/openweathermap/test_weather.py @@ -1,79 +1,132 @@ -"""Define tests for OpenWeatherMapWeather.""" +"""Test the OpenWeatherMap weather entity.""" -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, patch import pytest from homeassistant.components.openweathermap.const import ( - ATTR_API_DATETIME, ATTR_API_MINUTE_FORECAST, - ATTR_API_PRECIPITATION, + DOMAIN, OWM_MODE_V25, OWM_MODE_V30, ) -from homeassistant.components.openweathermap.coordinator import WeatherUpdateCoordinator -from homeassistant.components.openweathermap.weather import OpenWeatherMapWeather +from homeassistant.components.openweathermap.weather import SERVICE_GET_MINUTE_FORECAST +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -MOCK_UNIQUE_ID = "mock_unique_id" -MOCK_NAME = "Mock Name" -MOCK_WEATHER_DATA = { - ATTR_API_MINUTE_FORECAST: [ - {ATTR_API_DATETIME: "2023-10-01T12:00:00Z", ATTR_API_PRECIPITATION: 0}, - {ATTR_API_DATETIME: "2023-10-01T12:01:00Z", ATTR_API_PRECIPITATION: 0.1}, - ], -} - - -@pytest.fixture -def mock_hass(): - """Create a mock HomeAssistant instance.""" - return MagicMock(spec=HomeAssistant) - +from homeassistant.helpers import entity_registry as er -@pytest.fixture -def mock_coordinator(): - """Create a mock WeatherUpdateCoordinator.""" - coordinator = MagicMock(spec=WeatherUpdateCoordinator) - coordinator.data = MOCK_WEATHER_DATA - return coordinator +from tests.common import MockConfigEntry @pytest.fixture def mock_config_entry(): - """Create a mock OpenweathermapConfigEntry.""" - config_entry = MagicMock() - config_entry.unique_id = MOCK_UNIQUE_ID - config_entry.runtime_data = MagicMock() - config_entry.runtime_data.name = MOCK_NAME - config_entry.runtime_data.mode = OWM_MODE_V30 - config_entry.runtime_data.coordinator = MagicMock(spec=WeatherUpdateCoordinator) - config_entry.runtime_data.coordinator.data = MOCK_WEATHER_DATA - return config_entry + """Create a mock OpenWeatherMap config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test_api_key", + CONF_LATITUDE: 12.34, + CONF_LONGITUDE: 56.78, + CONF_MODE: OWM_MODE_V30, + CONF_NAME: "OpenWeatherMap", + }, + unique_id="test_unique_id", + ) @pytest.fixture -def mock_add_entities(): - """Create a mock AddEntitiesCallback.""" - return MagicMock(spec=AddEntitiesCallback) - - -async def test_async_get_minute_forecast(mock_coordinator) -> None: - """Test the async_get_minute_forecast method.""" - weather = OpenWeatherMapWeather( - name=MOCK_NAME, - unique_id=MOCK_UNIQUE_ID, - mode=OWM_MODE_V30, - weather_coordinator=mock_coordinator, - ) - - # Test successful minute forecast retrieval - result = await weather.async_get_minute_forecast() - assert result == mock_coordinator.data[ATTR_API_MINUTE_FORECAST] +def mock_weather_data(): + """Return mock weather data.""" + return { + ATTR_API_MINUTE_FORECAST: [ + {"time": "2024-10-01T12:00:00Z", "precipitation": 0}, + {"time": "2024-10-01T12:01:00Z", "precipitation": 0.1}, + {"time": "2024-10-01T12:02:00Z", "precipitation": 0.23}, + {"time": "2024-10-01T12:03:00Z", "precipitation": 0}, + ] + } + + +@pytest.fixture(autouse=True) +def mock_weather_update(): + """Mock the WeatherUpdateCoordinator to prevent API calls.""" + with patch( + "homeassistant.components.openweathermap.coordinator.WeatherUpdateCoordinator._async_update_data", + new=AsyncMock(), + ) as mock_update: + yield mock_update + + +@pytest.mark.asyncio +async def test_minute_forecast( + hass: HomeAssistant, + mock_config_entry, + mock_weather_data, + mock_weather_update, +) -> None: + """Test the OpenWeatherMapWeather Minute forecast.""" + # Set up the mock data to be returned by the coordinator + mock_weather_update.return_value = mock_weather_data + + # Add the MockConfigEntry to hass + mock_config_entry.add_to_hass(hass) + + # Set up the integration + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Verify the entry is loaded + assert mock_config_entry.state == ConfigEntryState.LOADED + + # Get the entity registry and verify the entity is registered + entity_registry = er.async_get(hass) + entity_id = "weather.openweathermap" + entry = entity_registry.async_get(entity_id) + assert entry is not None + + # Test the async_get_minute_forecast service + # We can call the service and assert the result + with patch( + "homeassistant.components.openweathermap.weather.OpenWeatherMapWeather.async_get_minute_forecast", + return_value=mock_weather_data[ATTR_API_MINUTE_FORECAST], + ): + result = await hass.services.async_call( + DOMAIN, + SERVICE_GET_MINUTE_FORECAST, + {"entity_id": entity_id}, + blocking=True, + return_response=True, + ) + assert result == { + "weather.openweathermap": mock_weather_data[ATTR_API_MINUTE_FORECAST] + } # Test exception when mode is not OWM_MODE_V30 - weather.mode = OWM_MODE_V25 + # Update the config entry data to change the mode + hass.config_entries.async_update_entry( + mock_config_entry, data={**mock_config_entry.data, CONF_MODE: OWM_MODE_V25} + ) + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Try calling the service again and expect a ServiceValidationError with pytest.raises(ServiceValidationError): - await weather.async_get_minute_forecast() + await hass.services.async_call( + DOMAIN, + SERVICE_GET_MINUTE_FORECAST, + {"entity_id": entity_id}, + blocking=True, + return_response=True, + ) + + # Cleanup + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() From 91bfaf7c1478be99c7efd0bfe9615255db01187b Mon Sep 17 00:00:00 2001 From: Andrew <34544450+10100011@users.noreply.github.com> Date: Fri, 1 Nov 2024 17:06:23 +0000 Subject: [PATCH 9/9] Mocking only the HTTP request --- tests/components/openweathermap/const.py | 350 ++++ .../fixtures/openweathermap.json | 1712 +++++++++++++++++ .../components/openweathermap/test_weather.py | 92 +- 3 files changed, 2093 insertions(+), 61 deletions(-) create mode 100644 tests/components/openweathermap/const.py create mode 100644 tests/components/openweathermap/fixtures/openweathermap.json diff --git a/tests/components/openweathermap/const.py b/tests/components/openweathermap/const.py new file mode 100644 index 00000000000000..d415e3da8d7625 --- /dev/null +++ b/tests/components/openweathermap/const.py @@ -0,0 +1,350 @@ +"""Constants for OpenWeatherMap testing.""" + +import datetime + +MINUTE_FORECAST = { + "weather.openweathermap": { + "forecast": [ + { + "datetime": datetime.datetime(2024, 10, 25, 16, 0, tzinfo=datetime.UTC), + "precipitation": 0.32, + }, + { + "datetime": datetime.datetime(2024, 10, 25, 16, 1, tzinfo=datetime.UTC), + "precipitation": 0.28, + }, + { + "datetime": datetime.datetime(2024, 10, 25, 16, 2, tzinfo=datetime.UTC), + "precipitation": 0.24, + }, + { + "datetime": datetime.datetime(2024, 10, 25, 16, 3, tzinfo=datetime.UTC), + "precipitation": 0.21, + }, + { + "datetime": datetime.datetime(2024, 10, 25, 16, 4, tzinfo=datetime.UTC), + "precipitation": 0.17, + }, + { + "datetime": datetime.datetime(2024, 10, 25, 16, 5, tzinfo=datetime.UTC), + "precipitation": 0.13, + }, + { + "datetime": datetime.datetime(2024, 10, 25, 16, 6, tzinfo=datetime.UTC), + "precipitation": 0.15, + }, + { + "datetime": datetime.datetime(2024, 10, 25, 16, 7, tzinfo=datetime.UTC), + "precipitation": 0.16, + }, + { + "datetime": datetime.datetime(2024, 10, 25, 16, 8, tzinfo=datetime.UTC), + "precipitation": 0.18, + }, + { + "datetime": datetime.datetime(2024, 10, 25, 16, 9, tzinfo=datetime.UTC), + "precipitation": 0.19, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 10, tzinfo=datetime.UTC + ), + "precipitation": 0.2, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 11, tzinfo=datetime.UTC + ), + "precipitation": 0.28, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 12, tzinfo=datetime.UTC + ), + "precipitation": 0.35, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 13, tzinfo=datetime.UTC + ), + "precipitation": 0.42, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 14, tzinfo=datetime.UTC + ), + "precipitation": 0.49, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 15, tzinfo=datetime.UTC + ), + "precipitation": 0.56, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 16, tzinfo=datetime.UTC + ), + "precipitation": 0.47, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 17, tzinfo=datetime.UTC + ), + "precipitation": 0.37, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 18, tzinfo=datetime.UTC + ), + "precipitation": 0.28, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 19, tzinfo=datetime.UTC + ), + "precipitation": 0.18, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 20, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 21, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 22, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 23, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 24, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 25, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 26, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 27, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 28, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 29, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 30, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 31, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 32, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 33, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 34, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 35, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 36, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 37, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 38, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 39, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 40, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 41, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 42, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 43, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 44, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 45, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 46, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 47, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 48, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 49, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 50, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 51, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 52, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 53, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 54, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 55, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 56, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 57, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 58, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + { + "datetime": datetime.datetime( + 2024, 10, 25, 16, 59, tzinfo=datetime.UTC + ), + "precipitation": 0, + }, + ] + } +} diff --git a/tests/components/openweathermap/fixtures/openweathermap.json b/tests/components/openweathermap/fixtures/openweathermap.json new file mode 100644 index 00000000000000..b3ada78de7fd15 --- /dev/null +++ b/tests/components/openweathermap/fixtures/openweathermap.json @@ -0,0 +1,1712 @@ +{ + "lat": 51.4769, + "lon": -0.0005, + "timezone": "Europe/London", + "timezone_offset": 3600, + "current": { + "dt": 1729871958, + "sunrise": 1729838492, + "sunset": 1729874777, + "temp": 287.89, + "feels_like": 287.77, + "pressure": 1014, + "humidity": 90, + "dew_point": 286.27, + "uvi": 0.02, + "clouds": 100, + "visibility": 7000, + "wind_speed": 4.12, + "wind_deg": 220, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "rain": { + "1h": 0.29 + } + }, + "minutely": [ + { + "dt": 1729872000, + "precipitation": 0.316 + }, + { + "dt": 1729872060, + "precipitation": 0.2794 + }, + { + "dt": 1729872120, + "precipitation": 0.2428 + }, + { + "dt": 1729872180, + "precipitation": 0.2062 + }, + { + "dt": 1729872240, + "precipitation": 0.1696 + }, + { + "dt": 1729872300, + "precipitation": 0.133 + }, + { + "dt": 1729872360, + "precipitation": 0.1474 + }, + { + "dt": 1729872420, + "precipitation": 0.1618 + }, + { + "dt": 1729872480, + "precipitation": 0.1762 + }, + { + "dt": 1729872540, + "precipitation": 0.1906 + }, + { + "dt": 1729872600, + "precipitation": 0.205 + }, + { + "dt": 1729872660, + "precipitation": 0.2764 + }, + { + "dt": 1729872720, + "precipitation": 0.3478 + }, + { + "dt": 1729872780, + "precipitation": 0.4192 + }, + { + "dt": 1729872840, + "precipitation": 0.4906 + }, + { + "dt": 1729872900, + "precipitation": 0.562 + }, + { + "dt": 1729872960, + "precipitation": 0.4668 + }, + { + "dt": 1729873020, + "precipitation": 0.3716 + }, + { + "dt": 1729873080, + "precipitation": 0.2764 + }, + { + "dt": 1729873140, + "precipitation": 0.1812 + }, + { + "dt": 1729873200, + "precipitation": 0 + }, + { + "dt": 1729873260, + "precipitation": 0 + }, + { + "dt": 1729873320, + "precipitation": 0 + }, + { + "dt": 1729873380, + "precipitation": 0 + }, + { + "dt": 1729873440, + "precipitation": 0 + }, + { + "dt": 1729873500, + "precipitation": 0 + }, + { + "dt": 1729873560, + "precipitation": 0 + }, + { + "dt": 1729873620, + "precipitation": 0 + }, + { + "dt": 1729873680, + "precipitation": 0 + }, + { + "dt": 1729873740, + "precipitation": 0 + }, + { + "dt": 1729873800, + "precipitation": 0 + }, + { + "dt": 1729873860, + "precipitation": 0 + }, + { + "dt": 1729873920, + "precipitation": 0 + }, + { + "dt": 1729873980, + "precipitation": 0 + }, + { + "dt": 1729874040, + "precipitation": 0 + }, + { + "dt": 1729874100, + "precipitation": 0 + }, + { + "dt": 1729874160, + "precipitation": 0 + }, + { + "dt": 1729874220, + "precipitation": 0 + }, + { + "dt": 1729874280, + "precipitation": 0 + }, + { + "dt": 1729874340, + "precipitation": 0 + }, + { + "dt": 1729874400, + "precipitation": 0 + }, + { + "dt": 1729874460, + "precipitation": 0 + }, + { + "dt": 1729874520, + "precipitation": 0 + }, + { + "dt": 1729874580, + "precipitation": 0 + }, + { + "dt": 1729874640, + "precipitation": 0 + }, + { + "dt": 1729874700, + "precipitation": 0 + }, + { + "dt": 1729874760, + "precipitation": 0 + }, + { + "dt": 1729874820, + "precipitation": 0 + }, + { + "dt": 1729874880, + "precipitation": 0 + }, + { + "dt": 1729874940, + "precipitation": 0 + }, + { + "dt": 1729875000, + "precipitation": 0 + }, + { + "dt": 1729875060, + "precipitation": 0 + }, + { + "dt": 1729875120, + "precipitation": 0 + }, + { + "dt": 1729875180, + "precipitation": 0 + }, + { + "dt": 1729875240, + "precipitation": 0 + }, + { + "dt": 1729875300, + "precipitation": 0 + }, + { + "dt": 1729875360, + "precipitation": 0 + }, + { + "dt": 1729875420, + "precipitation": 0 + }, + { + "dt": 1729875480, + "precipitation": 0 + }, + { + "dt": 1729875540, + "precipitation": 0 + } + ], + "hourly": [ + { + "dt": 1729868400, + "temp": 288.35, + "feels_like": 288.22, + "pressure": 1014, + "humidity": 88, + "dew_point": 286.38, + "uvi": 0.07, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.88, + "wind_deg": 174, + "wind_gust": 6.14, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1729872000, + "temp": 287.89, + "feels_like": 287.77, + "pressure": 1014, + "humidity": 90, + "dew_point": 286.27, + "uvi": 0.02, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.2, + "wind_deg": 197, + "wind_gust": 6.06, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.2, + "rain": { + "1h": 0.32 + } + }, + { + "dt": 1729875600, + "temp": 287.96, + "feels_like": 287.79, + "pressure": 1014, + "humidity": 88, + "dew_point": 285.99, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.65, + "wind_deg": 226, + "wind_gust": 6.53, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729879200, + "temp": 287.94, + "feels_like": 287.72, + "pressure": 1015, + "humidity": 86, + "dew_point": 285.62, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.61, + "wind_deg": 200, + "wind_gust": 5.78, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729882800, + "temp": 287.9, + "feels_like": 287.65, + "pressure": 1015, + "humidity": 85, + "dew_point": 285.41, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.19, + "wind_deg": 204, + "wind_gust": 3.85, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729886400, + "temp": 287.37, + "feels_like": 287.09, + "pressure": 1016, + "humidity": 86, + "dew_point": 285.06, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.23, + "wind_deg": 190, + "wind_gust": 1.34, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729890000, + "temp": 286.92, + "feels_like": 286.65, + "pressure": 1017, + "humidity": 88, + "dew_point": 284.86, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.35, + "wind_deg": 195, + "wind_gust": 1.9, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729893600, + "temp": 286.47, + "feels_like": 286.21, + "pressure": 1017, + "humidity": 90, + "dew_point": 284.75, + "uvi": 0, + "clouds": 93, + "visibility": 10000, + "wind_speed": 1.54, + "wind_deg": 193, + "wind_gust": 2.61, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729897200, + "temp": 286.22, + "feels_like": 285.93, + "pressure": 1017, + "humidity": 90, + "dew_point": 284.51, + "uvi": 0, + "clouds": 88, + "visibility": 10000, + "wind_speed": 0.88, + "wind_deg": 137, + "wind_gust": 0.86, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729900800, + "temp": 286.16, + "feels_like": 285.84, + "pressure": 1017, + "humidity": 89, + "dew_point": 284.29, + "uvi": 0, + "clouds": 89, + "visibility": 10000, + "wind_speed": 1.3, + "wind_deg": 144, + "wind_gust": 1.31, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729904400, + "temp": 285.71, + "feels_like": 285.37, + "pressure": 1017, + "humidity": 90, + "dew_point": 283.97, + "uvi": 0, + "clouds": 15, + "visibility": 10000, + "wind_speed": 1.54, + "wind_deg": 135, + "wind_gust": 2.16, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02n" + } + ], + "pop": 0 + }, + { + "dt": 1729908000, + "temp": 285.92, + "feels_like": 285.58, + "pressure": 1017, + "humidity": 89, + "dew_point": 283.94, + "uvi": 0, + "clouds": 42, + "visibility": 10000, + "wind_speed": 1.62, + "wind_deg": 140, + "wind_gust": 3.01, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0 + }, + { + "dt": 1729911600, + "temp": 286.07, + "feels_like": 285.74, + "pressure": 1017, + "humidity": 89, + "dew_point": 284.06, + "uvi": 0, + "clouds": 58, + "visibility": 10000, + "wind_speed": 1.93, + "wind_deg": 142, + "wind_gust": 5.86, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729915200, + "temp": 286.18, + "feels_like": 285.86, + "pressure": 1016, + "humidity": 89, + "dew_point": 284.3, + "uvi": 0, + "clouds": 69, + "visibility": 10000, + "wind_speed": 1.26, + "wind_deg": 122, + "wind_gust": 3.1, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729918800, + "temp": 285.74, + "feels_like": 285.48, + "pressure": 1017, + "humidity": 93, + "dew_point": 284.53, + "uvi": 0, + "clouds": 71, + "visibility": 10000, + "wind_speed": 1.69, + "wind_deg": 144, + "wind_gust": 4.11, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729922400, + "temp": 286.08, + "feels_like": 285.83, + "pressure": 1016, + "humidity": 92, + "dew_point": 284.7, + "uvi": 0, + "clouds": 76, + "visibility": 10000, + "wind_speed": 1.58, + "wind_deg": 158, + "wind_gust": 4.64, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729926000, + "temp": 286.12, + "feels_like": 285.85, + "pressure": 1017, + "humidity": 91, + "dew_point": 284.58, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.57, + "wind_deg": 167, + "wind_gust": 4.91, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1729929600, + "temp": 286.2, + "feels_like": 285.91, + "pressure": 1017, + "humidity": 90, + "dew_point": 284.48, + "uvi": 0.04, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.13, + "wind_deg": 156, + "wind_gust": 2.6, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1729933200, + "temp": 286.57, + "feels_like": 286.27, + "pressure": 1017, + "humidity": 88, + "dew_point": 284.49, + "uvi": 0.04, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.78, + "wind_deg": 200, + "wind_gust": 1.86, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1729936800, + "temp": 286.58, + "feels_like": 286.3, + "pressure": 1017, + "humidity": 89, + "dew_point": 284.61, + "uvi": 0.07, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.17, + "wind_deg": 175, + "wind_gust": 2.33, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1729940400, + "temp": 286.28, + "feels_like": 286.05, + "pressure": 1017, + "humidity": 92, + "dew_point": 284.83, + "uvi": 0.1, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.19, + "wind_deg": 112, + "wind_gust": 1.59, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.2, + "rain": { + "1h": 0.25 + } + }, + { + "dt": 1729944000, + "temp": 286.57, + "feels_like": 286.32, + "pressure": 1016, + "humidity": 90, + "dew_point": 284.78, + "uvi": 0.17, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.63, + "wind_deg": 119, + "wind_gust": 1.12, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1729947600, + "temp": 287.66, + "feels_like": 287.33, + "pressure": 1016, + "humidity": 83, + "dew_point": 284.59, + "uvi": 0.15, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.05, + "wind_deg": 226, + "wind_gust": 1.61, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1729951200, + "temp": 287.55, + "feels_like": 287.24, + "pressure": 1016, + "humidity": 84, + "dew_point": 284.71, + "uvi": 0.29, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.04, + "wind_deg": 292, + "wind_gust": 1.82, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1729954800, + "temp": 287.18, + "feels_like": 286.91, + "pressure": 1015, + "humidity": 87, + "dew_point": 284.9, + "uvi": 0.04, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.67, + "wind_deg": 271, + "wind_gust": 0.84, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1729958400, + "temp": 287.03, + "feels_like": 286.8, + "pressure": 1015, + "humidity": 89, + "dew_point": 284.93, + "uvi": 0.01, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.09, + "wind_deg": 201, + "wind_gust": 0.13, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1729962000, + "temp": 286.79, + "feels_like": 286.56, + "pressure": 1016, + "humidity": 90, + "dew_point": 284.98, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.75, + "wind_deg": 265, + "wind_gust": 0.77, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729965600, + "temp": 286.78, + "feels_like": 286.55, + "pressure": 1016, + "humidity": 90, + "dew_point": 284.96, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.4, + "wind_deg": 277, + "wind_gust": 2.05, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729969200, + "temp": 286.63, + "feels_like": 286.41, + "pressure": 1017, + "humidity": 91, + "dew_point": 284.95, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.4, + "wind_deg": 272, + "wind_gust": 2.49, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729972800, + "temp": 286.43, + "feels_like": 286.19, + "pressure": 1017, + "humidity": 91, + "dew_point": 284.92, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.54, + "wind_deg": 311, + "wind_gust": 4.4, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729976400, + "temp": 286.44, + "feels_like": 286.15, + "pressure": 1017, + "humidity": 89, + "dew_point": 284.66, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.77, + "wind_deg": 311, + "wind_gust": 4.7, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729980000, + "temp": 286.39, + "feels_like": 285.99, + "pressure": 1018, + "humidity": 85, + "dew_point": 283.79, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.68, + "wind_deg": 300, + "wind_gust": 4.67, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729983600, + "temp": 286.15, + "feels_like": 285.65, + "pressure": 1018, + "humidity": 82, + "dew_point": 282.98, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.06, + "wind_deg": 294, + "wind_gust": 5.49, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729987200, + "temp": 285.91, + "feels_like": 285.33, + "pressure": 1018, + "humidity": 80, + "dew_point": 282.43, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.95, + "wind_deg": 300, + "wind_gust": 4.94, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729990800, + "temp": 285.45, + "feels_like": 284.8, + "pressure": 1019, + "humidity": 79, + "dew_point": 281.73, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.16, + "wind_deg": 298, + "wind_gust": 5.91, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729994400, + "temp": 284.57, + "feels_like": 283.83, + "pressure": 1019, + "humidity": 79, + "dew_point": 280.99, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.14, + "wind_deg": 301, + "wind_gust": 6.6, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1729998000, + "temp": 284.09, + "feels_like": 283.33, + "pressure": 1020, + "humidity": 80, + "dew_point": 280.72, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.57, + "wind_deg": 301, + "wind_gust": 5.45, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1730001600, + "temp": 283.65, + "feels_like": 282.9, + "pressure": 1020, + "humidity": 82, + "dew_point": 280.56, + "uvi": 0, + "clouds": 79, + "visibility": 10000, + "wind_speed": 1.77, + "wind_deg": 308, + "wind_gust": 5.65, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1730005200, + "temp": 283.41, + "feels_like": 282.66, + "pressure": 1021, + "humidity": 83, + "dew_point": 280.42, + "uvi": 0, + "clouds": 64, + "visibility": 10000, + "wind_speed": 1.66, + "wind_deg": 310, + "wind_gust": 5.37, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1730008800, + "temp": 283.17, + "feels_like": 282.39, + "pressure": 1021, + "humidity": 83, + "dew_point": 280.31, + "uvi": 0, + "clouds": 53, + "visibility": 10000, + "wind_speed": 1.62, + "wind_deg": 319, + "wind_gust": 6.01, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1730012400, + "temp": 282.99, + "feels_like": 282.99, + "pressure": 1022, + "humidity": 84, + "dew_point": 280.3, + "uvi": 0, + "clouds": 0, + "visibility": 10000, + "wind_speed": 1.2, + "wind_deg": 318, + "wind_gust": 3.55, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "pop": 0 + }, + { + "dt": 1730016000, + "temp": 283.51, + "feels_like": 282.74, + "pressure": 1023, + "humidity": 82, + "dew_point": 280.5, + "uvi": 0.13, + "clouds": 0, + "visibility": 10000, + "wind_speed": 1.6, + "wind_deg": 330, + "wind_gust": 4.91, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "pop": 0 + }, + { + "dt": 1730019600, + "temp": 284.37, + "feels_like": 283.58, + "pressure": 1024, + "humidity": 78, + "dew_point": 280.6, + "uvi": 0.44, + "clouds": 0, + "visibility": 10000, + "wind_speed": 2.37, + "wind_deg": 347, + "wind_gust": 4.33, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "pop": 0 + }, + { + "dt": 1730023200, + "temp": 285.38, + "feels_like": 284.56, + "pressure": 1024, + "humidity": 73, + "dew_point": 280.49, + "uvi": 0.89, + "clouds": 2, + "visibility": 10000, + "wind_speed": 1.87, + "wind_deg": 0, + "wind_gust": 2.77, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "pop": 0 + }, + { + "dt": 1730026800, + "temp": 286.37, + "feels_like": 285.52, + "pressure": 1024, + "humidity": 68, + "dew_point": 280.42, + "uvi": 1.31, + "clouds": 3, + "visibility": 10000, + "wind_speed": 1.59, + "wind_deg": 3, + "wind_gust": 1.93, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "pop": 0 + }, + { + "dt": 1730030400, + "temp": 287.18, + "feels_like": 286.31, + "pressure": 1024, + "humidity": 64, + "dew_point": 280.36, + "uvi": 1.47, + "clouds": 8, + "visibility": 10000, + "wind_speed": 1.5, + "wind_deg": 353, + "wind_gust": 1.65, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "pop": 0 + }, + { + "dt": 1730034000, + "temp": 287.73, + "feels_like": 286.84, + "pressure": 1024, + "humidity": 61, + "dew_point": 280.22, + "uvi": 1.29, + "clouds": 20, + "visibility": 10000, + "wind_speed": 1.12, + "wind_deg": 348, + "wind_gust": 1.1, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02d" + } + ], + "pop": 0 + }, + { + "dt": 1730037600, + "temp": 287.99, + "feels_like": 287.1, + "pressure": 1024, + "humidity": 60, + "dew_point": 280.1, + "uvi": 0.89, + "clouds": 37, + "visibility": 10000, + "wind_speed": 0.76, + "wind_deg": 289, + "wind_gust": 1.11, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0 + } + ], + "daily": [ + { + "dt": 1729854000, + "sunrise": 1729838492, + "sunset": 1729874777, + "moonrise": 0, + "moonset": 1729867860, + "moon_phase": 0.78, + "summary": "Expect a day of partly cloudy with rain", + "temp": { + "day": 289.7, + "min": 285.72, + "max": 290.24, + "night": 286.47, + "eve": 287.96, + "morn": 285.72 + }, + "feels_like": { + "day": 289.6, + "night": 286.21, + "eve": 287.79, + "morn": 285.57 + }, + "pressure": 1013, + "humidity": 84, + "dew_point": 286.83, + "wind_speed": 3.6, + "wind_deg": 179, + "wind_gust": 9.35, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.2, + "rain": 0.54, + "uvi": 1.22 + }, + { + "dt": 1729940400, + "sunrise": 1729924997, + "sunset": 1729961060, + "moonrise": 1729900140, + "moonset": 1729955100, + "moon_phase": 0.82, + "summary": "Expect a day of partly cloudy with rain", + "temp": { + "day": 286.28, + "min": 285.71, + "max": 287.66, + "night": 286.39, + "eve": 286.79, + "morn": 285.74 + }, + "feels_like": { + "day": 286.05, + "night": 285.99, + "eve": 286.56, + "morn": 285.48 + }, + "pressure": 1017, + "humidity": 92, + "dew_point": 284.83, + "wind_speed": 1.93, + "wind_deg": 142, + "wind_gust": 5.86, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.2, + "rain": 0.25, + "uvi": 0.29 + }, + { + "dt": 1730026800, + "sunrise": 1730011503, + "sunset": 1730047344, + "moonrise": 1729990980, + "moonset": 1730042160, + "moon_phase": 0.85, + "summary": "Expect a day of partly cloudy with clear spells", + "temp": { + "day": 286.37, + "min": 282.99, + "max": 287.99, + "night": 284.24, + "eve": 286.36, + "morn": 283.41 + }, + "feels_like": { + "day": 285.52, + "night": 283.52, + "eve": 285.43, + "morn": 282.66 + }, + "pressure": 1024, + "humidity": 68, + "dew_point": 280.42, + "wind_speed": 2.41, + "wind_deg": 219, + "wind_gust": 7.19, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": 3, + "pop": 0, + "uvi": 1.47 + }, + { + "dt": 1730113200, + "sunrise": 1730098009, + "sunset": 1730133630, + "moonrise": 1730081640, + "moonset": 1730129160, + "moon_phase": 0.88, + "summary": "Expect a day of partly cloudy with rain", + "temp": { + "day": 286.76, + "min": 284.08, + "max": 289.01, + "night": 286.78, + "eve": 287.24, + "morn": 284.55 + }, + "feels_like": { + "day": 286, + "night": 286.55, + "eve": 286.92, + "morn": 284.04 + }, + "pressure": 1024, + "humidity": 70, + "dew_point": 281.23, + "wind_speed": 4.2, + "wind_deg": 249, + "wind_gust": 10.71, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.82, + "rain": 0.48, + "uvi": 0.57 + }, + { + "dt": 1730199600, + "sunrise": 1730184515, + "sunset": 1730219917, + "moonrise": 1730172240, + "moonset": 1730216100, + "moon_phase": 0.91, + "summary": "Expect a day of partly cloudy with clear spells", + "temp": { + "day": 289.24, + "min": 284.88, + "max": 289.91, + "night": 287.76, + "eve": 288.55, + "morn": 284.88 + }, + "feels_like": { + "day": 288.71, + "night": 287.39, + "eve": 288.16, + "morn": 284.59 + }, + "pressure": 1026, + "humidity": 69, + "dew_point": 283.42, + "wind_speed": 2.16, + "wind_deg": 225, + "wind_gust": 6.16, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02d" + } + ], + "clouds": 11, + "pop": 0, + "uvi": 0.33 + }, + { + "dt": 1730286000, + "sunrise": 1730271021, + "sunset": 1730306206, + "moonrise": 1730262780, + "moonset": 1730303100, + "moon_phase": 0.94, + "summary": "Expect a day of partly cloudy with rain", + "temp": { + "day": 289.88, + "min": 286.32, + "max": 290.13, + "night": 286.32, + "eve": 287.62, + "morn": 287.01 + }, + "feels_like": { + "day": 289.49, + "night": 285.96, + "eve": 287.21, + "morn": 286.67 + }, + "pressure": 1027, + "humidity": 72, + "dew_point": 284.59, + "wind_speed": 1.61, + "wind_deg": 56, + "wind_gust": 2.19, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 91, + "pop": 0.2, + "rain": 0.3, + "uvi": 1 + }, + { + "dt": 1730372400, + "sunrise": 1730357528, + "sunset": 1730392496, + "moonrise": 1730353440, + "moonset": 1730390100, + "moon_phase": 0.97, + "summary": "Expect a day of partly cloudy with clear spells", + "temp": { + "day": 288.77, + "min": 284.23, + "max": 289.35, + "night": 285.48, + "eve": 286.63, + "morn": 284.23 + }, + "feels_like": { + "day": 288.06, + "night": 284.99, + "eve": 286.1, + "morn": 283.74 + }, + "pressure": 1027, + "humidity": 64, + "dew_point": 281.94, + "wind_speed": 1.51, + "wind_deg": 90, + "wind_gust": 2.65, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": 6, + "pop": 0, + "uvi": 1 + }, + { + "dt": 1730458800, + "sunrise": 1730444034, + "sunset": 1730478788, + "moonrise": 1730444160, + "moonset": 1730477280, + "moon_phase": 0, + "summary": "Expect a day of partly cloudy with clear spells", + "temp": { + "day": 287.87, + "min": 283.42, + "max": 288.37, + "night": 285.07, + "eve": 286.02, + "morn": 283.42 + }, + "feels_like": { + "day": 287.12, + "night": 284.54, + "eve": 285.5, + "morn": 282.93 + }, + "pressure": 1021, + "humidity": 66, + "dew_point": 281.41, + "wind_speed": 3.06, + "wind_deg": 316, + "wind_gust": 6.95, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "clouds": 25, + "pop": 0, + "uvi": 1 + } + ] +} diff --git a/tests/components/openweathermap/test_weather.py b/tests/components/openweathermap/test_weather.py index 6a1afcfc3bc8f5..319144ddc68d89 100644 --- a/tests/components/openweathermap/test_weather.py +++ b/tests/components/openweathermap/test_weather.py @@ -1,11 +1,12 @@ """Test the OpenWeatherMap weather entity.""" +import json from unittest.mock import AsyncMock, patch import pytest from homeassistant.components.openweathermap.const import ( - ATTR_API_MINUTE_FORECAST, + DEFAULT_LANGUAGE, DOMAIN, OWM_MODE_V25, OWM_MODE_V30, @@ -14,6 +15,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_API_KEY, + CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, @@ -23,10 +25,14 @@ from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry +from .const import MINUTE_FORECAST +from tests.common import MockConfigEntry, load_fixture -@pytest.fixture +ENTITY_ID = "weather.openweathermap" + + +@pytest.fixture(autouse=True) def mock_config_entry(): """Create a mock OpenWeatherMap config entry.""" return MockConfigEntry( @@ -35,94 +41,58 @@ def mock_config_entry(): CONF_API_KEY: "test_api_key", CONF_LATITUDE: 12.34, CONF_LONGITUDE: 56.78, - CONF_MODE: OWM_MODE_V30, CONF_NAME: "OpenWeatherMap", }, - unique_id="test_unique_id", + options={CONF_MODE: OWM_MODE_V30, CONF_LANGUAGE: DEFAULT_LANGUAGE}, + version=5, ) -@pytest.fixture -def mock_weather_data(): - """Return mock weather data.""" - return { - ATTR_API_MINUTE_FORECAST: [ - {"time": "2024-10-01T12:00:00Z", "precipitation": 0}, - {"time": "2024-10-01T12:01:00Z", "precipitation": 0.1}, - {"time": "2024-10-01T12:02:00Z", "precipitation": 0.23}, - {"time": "2024-10-01T12:03:00Z", "precipitation": 0}, - ] - } - - -@pytest.fixture(autouse=True) -def mock_weather_update(): - """Mock the WeatherUpdateCoordinator to prevent API calls.""" - with patch( - "homeassistant.components.openweathermap.coordinator.WeatherUpdateCoordinator._async_update_data", - new=AsyncMock(), - ) as mock_update: - yield mock_update - - @pytest.mark.asyncio +@patch( + "pyopenweathermap.http_client.HttpClient.request", + new_callable=AsyncMock, + return_value=json.loads(load_fixture("openweathermap.json", "openweathermap")), +) async def test_minute_forecast( - hass: HomeAssistant, - mock_config_entry, - mock_weather_data, - mock_weather_update, + mock_api_response, hass: HomeAssistant, mock_config_entry ) -> None: """Test the OpenWeatherMapWeather Minute forecast.""" - # Set up the mock data to be returned by the coordinator - mock_weather_update.return_value = mock_weather_data - # Add the MockConfigEntry to hass mock_config_entry.add_to_hass(hass) - - # Set up the integration await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() + assert hass.data - # Verify the entry is loaded assert mock_config_entry.state == ConfigEntryState.LOADED - # Get the entity registry and verify the entity is registered entity_registry = er.async_get(hass) - entity_id = "weather.openweathermap" - entry = entity_registry.async_get(entity_id) + entry = entity_registry.async_get(ENTITY_ID) assert entry is not None - # Test the async_get_minute_forecast service - # We can call the service and assert the result - with patch( - "homeassistant.components.openweathermap.weather.OpenWeatherMapWeather.async_get_minute_forecast", - return_value=mock_weather_data[ATTR_API_MINUTE_FORECAST], - ): - result = await hass.services.async_call( - DOMAIN, - SERVICE_GET_MINUTE_FORECAST, - {"entity_id": entity_id}, - blocking=True, - return_response=True, - ) - assert result == { - "weather.openweathermap": mock_weather_data[ATTR_API_MINUTE_FORECAST] - } + result = await hass.services.async_call( + DOMAIN, + SERVICE_GET_MINUTE_FORECAST, + {"entity_id": ENTITY_ID}, + blocking=True, + return_response=True, + ) + assert result == MINUTE_FORECAST # Test exception when mode is not OWM_MODE_V30 - # Update the config entry data to change the mode hass.config_entries.async_update_entry( - mock_config_entry, data={**mock_config_entry.data, CONF_MODE: OWM_MODE_V25} + mock_config_entry, + options={**mock_config_entry.options, CONF_MODE: OWM_MODE_V25}, ) await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() - # Try calling the service again and expect a ServiceValidationError + # Expect a ServiceValidationError when mode is not OWM_MODE_V30 with pytest.raises(ServiceValidationError): await hass.services.async_call( DOMAIN, SERVICE_GET_MINUTE_FORECAST, - {"entity_id": entity_id}, + {"entity_id": ENTITY_ID}, blocking=True, return_response=True, )