diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 81a6544c7cea58..de317709f5b028 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_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 3ef0eda0c8fbd7..c1d720733ecc8d 100644 --- a/homeassistant/components/openweathermap/coordinator.py +++ b/homeassistant/components/openweathermap/coordinator.py @@ -7,6 +7,7 @@ CurrentWeather, DailyWeatherForecast, HourlyWeatherForecast, + MinutelyWeatherForecast, OWMClient, RequestError, WeatherReport, @@ -27,10 +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, @@ -94,6 +99,11 @@ def _convert_weather_response(self, weather_report: WeatherReport): return { ATTR_API_CURRENT: current_weather, + ATTR_API_MINUTE_FORECAST: ( + self._get_minute_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 +114,20 @@ def _convert_weather_response(self, weather_report: WeatherReport): ], } + def _get_minute_weather_data( + self, minute_forecast: list[MinutelyWeatherForecast] + ) -> dict: + """Get minute weather data from the forecast.""" + return { + ATTR_API_FORECAST: [ + { + ATTR_API_DATETIME: item.date_time, + ATTR_API_PRECIPITATION: round(item.precipitation, 2), + } + for item in minute_forecast + ] + } + def _get_current_weather_data(self, current_weather: CurrentWeather): return { ATTR_API_CONDITION: self._get_condition(current_weather.condition.id), diff --git a/homeassistant/components/openweathermap/icons.json b/homeassistant/components/openweathermap/icons.json new file mode 100644 index 00000000000000..d493b1538ba956 --- /dev/null +++ b/homeassistant/components/openweathermap/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "get_minute_forecast": { + "service": "mdi:weather-snowy-rainy" + } + } +} diff --git a/homeassistant/components/openweathermap/services.yaml b/homeassistant/components/openweathermap/services.yaml new file mode 100644 index 00000000000000..6bbcf1b23e4e01 --- /dev/null +++ b/homeassistant/components/openweathermap/services.yaml @@ -0,0 +1,5 @@ +get_minute_forecast: + target: + entity: + domain: weather + integration: openweathermap diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index 46b5feab75c949..0692087bc23f8e 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -47,5 +47,16 @@ } } } + }, + "services": { + "get_minute_forecast": { + "name": "Get minute forecast", + "description": "Get minute weather forecast." + } + }, + "exceptions": { + "service_minute_forecast_mode": { + "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..e097c9f0f971da 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,7 @@ def __init__( manufacturer=MANUFACTURER, name=DEFAULT_NAME, ) + self.mode = mode if mode in (OWM_MODE_V30, OWM_MODE_V25): self._attr_supported_features = ( @@ -100,6 +114,17 @@ def __init__( elif mode == OWM_MODE_FREE_FORECAST: self._attr_supported_features = WeatherEntityFeature.FORECAST_HOURLY + async def async_get_minute_forecast(self) -> dict[str, list[dict]] | dict: + """Return Minute forecast.""" + + if self.mode == OWM_MODE_V30: + return self.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/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/snapshots/test_weather.ambr b/tests/components/openweathermap/snapshots/test_weather.ambr new file mode 100644 index 00000000000000..13e380b47bfc7b --- /dev/null +++ b/tests/components/openweathermap/snapshots/test_weather.ambr @@ -0,0 +1,159 @@ +# serializer version: 1 +# name: test_get_minute_forecast + dict({ + 'weather.openweathermap': dict({ + 'current': dict({ + 'clouds': 75, + 'condition': 'cloudy', + 'dew_point': 3.99, + 'feels_like_temperature': 2.07, + 'humidity': 82, + 'precipitation_kind': 'None', + 'pressure': 1000, + 'rain': 0, + 'snow': 0, + 'temperature': 6.84, + 'uv_index': 0.13, + 'visibility_distance': 10000, + 'weather': 'broken clouds', + 'weather_code': 803, + 'wind_bearing': 199, + 'wind_gust': None, + 'wind_speed': 9.83, + }), + 'daily_forecast': list([ + dict({ + 'cloud_coverage': 84, + 'condition': 'cloudy', + 'datetime': '2024-04-25T16:45:36+00:00', + 'humidity': 62, + 'native_apparent_temperature': dict({ + 'day': 18.76, + 'evening': 20.51, + 'max': 21.26, + 'min': 8.11, + 'morning': 8.47, + 'night': 13.06, + }), + 'native_dew_point': 11.34, + 'native_wind_gust_speed': 11.81, + 'precipitation': 0, + 'precipitation_probability': 0, + 'pressure': 1015, + 'temperature': 21.26, + 'templow': 8.11, + 'uv_index': 4.06, + 'wind_bearing': 168, + 'wind_speed': 8.14, + }), + ]), + 'hourly_forecast': list([ + ]), + 'minute_forecast': dict({ + 'forecast': list([ + dict({ + 'datetime': 1728672360, + 'precipitation': 0, + }), + dict({ + 'datetime': 1728672420, + 'precipitation': 1.23, + }), + dict({ + 'datetime': 1728672480, + 'precipitation': 4.5, + }), + dict({ + 'datetime': 1728672540, + 'precipitation': 0, + }), + ]), + }), + }), + }) +# --- +# name: test_get_weather + dict({ + 'current': dict({ + 'cloud_coverage': 75, + 'condition': dict({ + 'description': 'broken clouds', + 'icon': '04d', + 'id': 803, + 'main': 'Clouds', + }), + 'date_time': datetime.datetime(2024, 4, 25, 16, 45, 36, tzinfo=datetime.timezone.utc), + 'dew_point': 3.99, + 'feels_like': 2.07, + 'humidity': 82, + 'pressure': 1000, + 'rain': None, + 'snow': None, + 'temperature': 6.84, + 'uv_index': 0.13, + 'visibility': 10000, + 'wind_bearing': 199, + 'wind_gust': None, + 'wind_speed': 9.83, + }), + 'daily_forecast': list([ + dict({ + 'cloud_coverage': 84, + 'condition': dict({ + 'description': 'broken clouds', + 'icon': '04d', + 'id': 803, + 'main': 'Clouds', + }), + 'date_time': datetime.datetime(2024, 4, 25, 16, 45, 36, tzinfo=datetime.timezone.utc), + 'dew_point': 11.34, + 'feels_like': dict({ + 'day': 18.76, + 'evening': 20.51, + 'max': 21.26, + 'min': 8.11, + 'morning': 8.47, + 'night': 13.06, + }), + 'humidity': 62, + 'precipitation_probability': 0, + 'pressure': 1015, + 'rain': 0, + 'snow': 0, + 'summary': 'There will be clear sky until morning, then partly cloudy', + 'temperature': dict({ + 'day': 18.76, + 'evening': 20.51, + 'max': 21.26, + 'min': 8.11, + 'morning': 8.47, + 'night': 13.06, + }), + 'uv_index': 4.06, + 'wind_bearing': 168, + 'wind_gust': 11.81, + 'wind_speed': 8.14, + }), + ]), + 'hourly_forecast': list([ + ]), + 'minutely_forecast': list([ + dict({ + 'date_time': 1728672360, + 'precipitation': 0, + }), + dict({ + 'date_time': 1728672420, + 'precipitation': 1.23, + }), + dict({ + 'date_time': 1728672480, + 'precipitation': 4.5, + }), + dict({ + 'date_time': 1728672540, + 'precipitation': 0, + }), + ]), + }) +# --- diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index aec343607544a8..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"} @@ -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() diff --git a/tests/components/openweathermap/test_weather.py b/tests/components/openweathermap/test_weather.py new file mode 100644 index 00000000000000..135daed09d6eb6 --- /dev/null +++ b/tests/components/openweathermap/test_weather.py @@ -0,0 +1,148 @@ +"""Test the OpenWeatherMap weather entity.""" + +from pyopenweathermap.client.onecall_client import OWMOneCallClient +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.openweathermap.const import ( + DEFAULT_LANGUAGE, + DOMAIN, + OWM_MODE_V25, + OWM_MODE_V30, +) +from homeassistant.components.openweathermap.coordinator import WeatherUpdateCoordinator +from homeassistant.components.openweathermap.weather import SERVICE_GET_MINUTE_FORECAST +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + CONF_API_KEY, + CONF_LANGUAGE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from .test_config_flow import _create_mocked_owm_factory + +from tests.common import AsyncMock, MockConfigEntry, patch + +ENTITY_ID = "weather.openweathermap" +API_KEY = "test_api_key" +LATITUDE = 12.34 +LONGITUDE = 56.78 +NAME = "openweathermap" + +# Define test data for mocked weather report +mocked_weather_report = _create_mocked_owm_factory(True) +coordinator = WeatherUpdateCoordinator(None, None, None, None) +converted_weather_report = coordinator._convert_weather_response( + mocked_weather_report.get_weather.return_value +) + + +def mock_config_entry(mode: str) -> MockConfigEntry: + """Create a mock OpenWeatherMap config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: API_KEY, + CONF_LATITUDE: LATITUDE, + CONF_LONGITUDE: LONGITUDE, + CONF_NAME: NAME, + }, + options={CONF_MODE: mode, CONF_LANGUAGE: DEFAULT_LANGUAGE}, + version=5, + ) + + +@pytest.fixture +def mock_config_entry_v25() -> MockConfigEntry: + """Create a mock OpenWeatherMap v2.5 config entry.""" + return mock_config_entry(OWM_MODE_V25) + + +@pytest.fixture +def mock_config_entry_v30() -> MockConfigEntry: + """Create a mock OpenWeatherMap v3.0 config entry.""" + return mock_config_entry(OWM_MODE_V30) + + +async def setup_mock_config_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +): + """Set up the MockConfigEntry and assert it is loaded correctly.""" + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data + assert hass.states.get(ENTITY_ID) + assert mock_config_entry.state is ConfigEntryState.LOADED + + +@patch( + "pyopenweathermap.client.onecall_client.OWMOneCallClient.get_weather", + AsyncMock(return_value=mocked_weather_report), +) +async def test_get_weather( + snapshot: SnapshotAssertion, +) -> None: + """Test the get_weather method of OWMOneCallClient(OWMClient) to mock calling the OWM API.""" + # Create an instance of OWMClient + owm_client = OWMOneCallClient(API_KEY, OWM_MODE_V30) + + # Call the get_weather method + weather_report = await owm_client.get_weather(lat=LATITUDE, lon=LONGITUDE) + + # Assert the output matches the snapshot + assert weather_report.get_weather.return_value == snapshot + + +@patch( + "pyopenweathermap.client.onecall_client.OWMOneCallClient.get_weather", + AsyncMock(return_value=mocked_weather_report), +) +@patch( + "homeassistant.components.openweathermap.weather.OpenWeatherMapWeather.async_get_minute_forecast", + AsyncMock(return_value=converted_weather_report), +) +async def test_get_minute_forecast( + hass: HomeAssistant, + mock_config_entry_v30: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the get_minute_forecast Service call.""" + await setup_mock_config_entry(hass, mock_config_entry_v30) + + # Assert that the service matches the snapshot + result = await hass.services.async_call( + DOMAIN, + SERVICE_GET_MINUTE_FORECAST, + {"entity_id": ENTITY_ID}, + blocking=True, + return_response=True, + ) + assert result == snapshot + + +@patch( + "pyopenweathermap.client.onecall_client.OWMOneCallClient.get_weather", + AsyncMock(return_value=mocked_weather_report), +) +async def test_mode_fail( + hass: HomeAssistant, + mock_config_entry_v25: MockConfigEntry, +) -> None: + """Test that Minute forecasting fails when mode is not v3.0.""" + await setup_mock_config_entry(hass, mock_config_entry_v25) + + # Expect a ServiceValidationError when mode is not OWM_MODE_V30 + with pytest.raises(ServiceValidationError, match=".."): + await hass.services.async_call( + DOMAIN, + SERVICE_GET_MINUTE_FORECAST, + {"entity_id": ENTITY_ID}, + blocking=True, + return_response=True, + )