From 4f8a6979d9916714cb2a47141cd1615d91c7dc26 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Sat, 10 Aug 2024 17:01:26 +0200 Subject: [PATCH] Bump OpenWeatherMap to 0.1.1 (#120178) * add owm modes * fix tests * fix modes * remove sensors * Update homeassistant/components/openweathermap/sensor.py Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- .../components/openweathermap/__init__.py | 7 +-- .../components/openweathermap/const.py | 13 +++-- .../components/openweathermap/coordinator.py | 16 +++++-- .../components/openweathermap/manifest.json | 2 +- .../components/openweathermap/sensor.py | 27 +++++++---- .../components/openweathermap/utils.py | 4 +- .../components/openweathermap/weather.py | 48 +++++++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../openweathermap/test_config_flow.py | 26 +++++----- 10 files changed, 97 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 7aea6aafe20..747b93179bc 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass import logging -from pyopenweathermap import OWMClient +from pyopenweathermap import create_owm_client from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -33,6 +33,7 @@ class OpenweathermapData: """Runtime data definition.""" name: str + mode: str coordinator: WeatherUpdateCoordinator @@ -52,7 +53,7 @@ async def async_setup_entry( else: async_delete_issue(hass, entry.entry_id) - owm_client = OWMClient(api_key, mode, lang=language) + owm_client = create_owm_client(api_key, mode, lang=language) weather_coordinator = WeatherUpdateCoordinator( owm_client, latitude, longitude, hass ) @@ -61,7 +62,7 @@ async def async_setup_entry( entry.async_on_unload(entry.add_update_listener(async_update_options)) - entry.runtime_data = OpenweathermapData(name, weather_coordinator) + entry.runtime_data = OpenweathermapData(name, mode, weather_coordinator) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 6c9997fc061..d34125a2405 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -58,10 +58,17 @@ FORECAST_MODE_DAILY = "daily" FORECAST_MODE_FREE_DAILY = "freedaily" FORECAST_MODE_ONECALL_HOURLY = "onecall_hourly" FORECAST_MODE_ONECALL_DAILY = "onecall_daily" -OWM_MODE_V25 = "v2.5" +OWM_MODE_FREE_CURRENT = "current" +OWM_MODE_FREE_FORECAST = "forecast" OWM_MODE_V30 = "v3.0" -OWM_MODES = [OWM_MODE_V30, OWM_MODE_V25] -DEFAULT_OWM_MODE = OWM_MODE_V30 +OWM_MODE_V25 = "v2.5" +OWM_MODES = [ + OWM_MODE_FREE_CURRENT, + OWM_MODE_FREE_FORECAST, + OWM_MODE_V30, + OWM_MODE_V25, +] +DEFAULT_OWM_MODE = OWM_MODE_FREE_CURRENT LANGUAGES = [ "af", diff --git a/homeassistant/components/openweathermap/coordinator.py b/homeassistant/components/openweathermap/coordinator.py index 0f99af5ad64..f7672a1290b 100644 --- a/homeassistant/components/openweathermap/coordinator.py +++ b/homeassistant/components/openweathermap/coordinator.py @@ -86,8 +86,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): """Format the weather response correctly.""" _LOGGER.debug("OWM weather response: %s", weather_report) + current_weather = ( + self._get_current_weather_data(weather_report.current) + if weather_report.current is not None + else {} + ) + return { - ATTR_API_CURRENT: self._get_current_weather_data(weather_report.current), + ATTR_API_CURRENT: current_weather, ATTR_API_HOURLY_FORECAST: [ self._get_hourly_forecast_weather_data(item) for item in weather_report.hourly_forecast @@ -122,6 +128,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): } def _get_hourly_forecast_weather_data(self, forecast: HourlyWeatherForecast): + uv_index = float(forecast.uv_index) if forecast.uv_index is not None else None + return Forecast( datetime=forecast.date_time.isoformat(), condition=self._get_condition(forecast.condition.id), @@ -134,12 +142,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): wind_speed=forecast.wind_speed, native_wind_gust_speed=forecast.wind_gust, wind_bearing=forecast.wind_bearing, - uv_index=float(forecast.uv_index), + uv_index=uv_index, precipitation_probability=round(forecast.precipitation_probability * 100), precipitation=self._calc_precipitation(forecast.rain, forecast.snow), ) def _get_daily_forecast_weather_data(self, forecast: DailyWeatherForecast): + uv_index = float(forecast.uv_index) if forecast.uv_index is not None else None + return Forecast( datetime=forecast.date_time.isoformat(), condition=self._get_condition(forecast.condition.id), @@ -153,7 +163,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): wind_speed=forecast.wind_speed, native_wind_gust_speed=forecast.wind_gust, wind_bearing=forecast.wind_bearing, - uv_index=float(forecast.uv_index), + uv_index=uv_index, precipitation_probability=round(forecast.precipitation_probability * 100), precipitation=round(forecast.rain + forecast.snow, 2), ) diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index e2c809cf385..199e750ad4f 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.0.9"] + "requirements": ["pyopenweathermap==0.1.1"] } diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 89905e99ed9..46789f4b3d2 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -47,6 +48,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, MANUFACTURER, + OWM_MODE_FREE_FORECAST, ) from .coordinator import WeatherUpdateCoordinator @@ -161,16 +163,23 @@ async def async_setup_entry( name = domain_data.name weather_coordinator = domain_data.coordinator - entities: list[AbstractOpenWeatherMapSensor] = [ - OpenWeatherMapSensor( - name, - f"{config_entry.unique_id}-{description.key}", - description, - weather_coordinator, + if domain_data.mode == OWM_MODE_FREE_FORECAST: + entity_registry = er.async_get(hass) + entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + for entry in entries: + entity_registry.async_remove(entry.entity_id) + else: + async_add_entities( + OpenWeatherMapSensor( + name, + f"{config_entry.unique_id}-{description.key}", + description, + weather_coordinator, + ) + for description in WEATHER_SENSOR_TYPES ) - for description in WEATHER_SENSOR_TYPES - ] - async_add_entities(entities) class AbstractOpenWeatherMapSensor(SensorEntity): diff --git a/homeassistant/components/openweathermap/utils.py b/homeassistant/components/openweathermap/utils.py index 7f2391b21a1..ba5378fb31c 100644 --- a/homeassistant/components/openweathermap/utils.py +++ b/homeassistant/components/openweathermap/utils.py @@ -2,7 +2,7 @@ from typing import Any -from pyopenweathermap import OWMClient, RequestError +from pyopenweathermap import RequestError, create_owm_client from homeassistant.const import CONF_LANGUAGE, CONF_MODE @@ -16,7 +16,7 @@ async def validate_api_key(api_key, mode): api_key_valid = None errors, description_placeholders = {}, {} try: - owm_client = OWMClient(api_key, mode) + owm_client = create_owm_client(api_key, mode) api_key_valid = await owm_client.validate_key() except RequestError as error: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 62b15218233..3a134a0ee26 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -8,6 +8,7 @@ from homeassistant.components.weather import ( WeatherEntityFeature, ) from homeassistant.const import ( + UnitOfLength, UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, @@ -29,6 +30,7 @@ from .const import ( ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, + ATTR_API_VISIBILITY_DISTANCE, ATTR_API_WIND_BEARING, ATTR_API_WIND_GUST, ATTR_API_WIND_SPEED, @@ -36,6 +38,9 @@ from .const import ( DEFAULT_NAME, DOMAIN, MANUFACTURER, + OWM_MODE_FREE_FORECAST, + OWM_MODE_V25, + OWM_MODE_V30, ) from .coordinator import WeatherUpdateCoordinator @@ -48,10 +53,11 @@ async def async_setup_entry( """Set up OpenWeatherMap weather entity based on a config entry.""" domain_data = config_entry.runtime_data name = domain_data.name + mode = domain_data.mode weather_coordinator = domain_data.coordinator unique_id = f"{config_entry.unique_id}" - owm_weather = OpenWeatherMapWeather(name, unique_id, weather_coordinator) + owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator) async_add_entities([owm_weather], False) @@ -66,11 +72,13 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND + _attr_native_visibility_unit = UnitOfLength.METERS def __init__( self, name: str, unique_id: str, + mode: str, weather_coordinator: WeatherUpdateCoordinator, ) -> None: """Initialize the sensor.""" @@ -83,59 +91,71 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina manufacturer=MANUFACTURER, name=DEFAULT_NAME, ) - self._attr_supported_features = ( - WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY - ) + + if mode in (OWM_MODE_V30, OWM_MODE_V25): + self._attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY + | WeatherEntityFeature.FORECAST_HOURLY + ) + elif mode == OWM_MODE_FREE_FORECAST: + self._attr_supported_features = WeatherEntityFeature.FORECAST_HOURLY @property def condition(self) -> str | None: """Return the current condition.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_CONDITION] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_CONDITION) @property def cloud_coverage(self) -> float | None: """Return the Cloud coverage in %.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_CLOUDS] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_CLOUDS) @property def native_apparent_temperature(self) -> float | None: """Return the apparent temperature.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_FEELS_LIKE_TEMPERATURE] + return self.coordinator.data[ATTR_API_CURRENT].get( + ATTR_API_FEELS_LIKE_TEMPERATURE + ) @property def native_temperature(self) -> float | None: """Return the temperature.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_TEMPERATURE] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_TEMPERATURE) @property def native_pressure(self) -> float | None: """Return the pressure.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_PRESSURE] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_PRESSURE) @property def humidity(self) -> float | None: """Return the humidity.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_HUMIDITY] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_HUMIDITY) @property def native_dew_point(self) -> float | None: """Return the dew point.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_DEW_POINT] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_DEW_POINT) @property def native_wind_gust_speed(self) -> float | None: """Return the wind gust speed.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_GUST] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_GUST) @property def native_wind_speed(self) -> float | None: """Return the wind speed.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_SPEED] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_SPEED) @property def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_BEARING] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_BEARING) + + @property + def visibility(self) -> float | str | None: + """Return visibility.""" + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_VISIBILITY_DISTANCE) @callback def _async_forecast_daily(self) -> list[Forecast] | None: diff --git a/requirements_all.txt b/requirements_all.txt index 3f0d1f2d99f..a7f66c5c161 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2074,7 +2074,7 @@ pyombi==0.1.10 pyopenuv==2023.02.0 # homeassistant.components.openweathermap -pyopenweathermap==0.0.9 +pyopenweathermap==0.1.1 # homeassistant.components.opnsense pyopnsense==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7d98d3e057..2bb4ecf5998 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1661,7 +1661,7 @@ pyoctoprintapi==0.1.12 pyopenuv==2023.02.0 # homeassistant.components.openweathermap -pyopenweathermap==0.0.9 +pyopenweathermap==0.1.1 # homeassistant.components.opnsense pyopnsense==0.4.0 diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index be02a6b01a9..f18aa432e2f 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -45,7 +45,7 @@ CONFIG = { VALID_YAML_CONFIG = {CONF_API_KEY: "foo"} -def _create_mocked_owm_client(is_valid: bool): +def _create_mocked_owm_factory(is_valid: bool): current_weather = CurrentWeather( date_time=datetime.fromtimestamp(1714063536, tz=UTC), temperature=6.84, @@ -118,18 +118,18 @@ def _create_mocked_owm_client(is_valid: bool): def mock_owm_client(): """Mock config_flow OWMClient.""" with patch( - "homeassistant.components.openweathermap.OWMClient", - ) as owm_client_mock: - yield owm_client_mock + "homeassistant.components.openweathermap.create_owm_client", + ) as mock: + yield mock @pytest.fixture(name="config_flow_owm_client_mock") def mock_config_flow_owm_client(): """Mock config_flow OWMClient.""" with patch( - "homeassistant.components.openweathermap.utils.OWMClient", - ) as config_flow_owm_client_mock: - yield config_flow_owm_client_mock + "homeassistant.components.openweathermap.utils.create_owm_client", + ) as mock: + yield mock async def test_successful_config_flow( @@ -138,7 +138,7 @@ async def test_successful_config_flow( config_flow_owm_client_mock, ) -> None: """Test that the form is served with valid input.""" - mock = _create_mocked_owm_client(True) + mock = _create_mocked_owm_factory(True) owm_client_mock.return_value = mock config_flow_owm_client_mock.return_value = mock @@ -177,7 +177,7 @@ async def test_abort_config_flow( config_flow_owm_client_mock, ) -> None: """Test that the form is served with same data.""" - mock = _create_mocked_owm_client(True) + mock = _create_mocked_owm_factory(True) owm_client_mock.return_value = mock config_flow_owm_client_mock.return_value = mock @@ -200,7 +200,7 @@ async def test_config_flow_options_change( config_flow_owm_client_mock, ) -> None: """Test that the options form.""" - mock = _create_mocked_owm_client(True) + mock = _create_mocked_owm_factory(True) owm_client_mock.return_value = mock config_flow_owm_client_mock.return_value = mock @@ -261,7 +261,7 @@ async def test_form_invalid_api_key( config_flow_owm_client_mock, ) -> None: """Test that the form is served with no input.""" - config_flow_owm_client_mock.return_value = _create_mocked_owm_client(False) + config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(False) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -269,7 +269,7 @@ async def test_form_invalid_api_key( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_api_key"} - config_flow_owm_client_mock.return_value = _create_mocked_owm_client(True) + config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(True) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG ) @@ -282,7 +282,7 @@ async def test_form_api_call_error( config_flow_owm_client_mock, ) -> None: """Test setting up with api call error.""" - config_flow_owm_client_mock.return_value = _create_mocked_owm_client(True) + config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(True) config_flow_owm_client_mock.side_effect = RequestError("oops") result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG