diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index ae8a1634cf6a16..29c1fa0181f1c5 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -3,8 +3,6 @@ from dataclasses import dataclass import logging -from pyopenweathermap import create_owm_client - from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE from homeassistant.core import HomeAssistant @@ -12,7 +10,7 @@ from .const import CONFIG_FLOW_VERSION, DEFAULT_OWM_MODE, OWM_MODES, PLATFORMS from .coordinator import OWMUpdateCoordinator, get_owm_update_coordinator from .repairs import async_create_issue, async_delete_issue -from .utils import build_data_and_options +from .utils import build_data_and_options, get_owm_client _LOGGER = logging.getLogger(__name__) @@ -40,7 +38,7 @@ async def async_setup_entry( else: async_delete_issue(hass, entry.entry_id) - owm_client = create_owm_client(api_key, mode, lang=language) + owm_client = get_owm_client(api_key, mode, language) owm_coordinator = get_owm_update_coordinator(mode)(hass, entry, owm_client) await owm_coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 08408f0198197a..de12ed5e1715c4 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -65,9 +65,11 @@ OWM_MODE_FREE_CURRENT = "current" OWM_MODE_FREE_FORECAST = "forecast" OWM_MODE_V30 = "v3.0" +OWM_MODE_V40 = "v4.0" OWM_MODE_AIRPOLLUTION = "air_pollution" OWM_MODES = [ OWM_MODE_V30, + OWM_MODE_V40, OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_FORECAST, OWM_MODE_AIRPOLLUTION, diff --git a/homeassistant/components/openweathermap/coordinator.py b/homeassistant/components/openweathermap/coordinator.py index 4750df1e31b797..8460cd07e30b2c 100644 --- a/homeassistant/components/openweathermap/coordinator.py +++ b/homeassistant/components/openweathermap/coordinator.py @@ -69,6 +69,7 @@ OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_FORECAST, OWM_MODE_V30, + OWM_MODE_V40, WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT, ) @@ -309,6 +310,7 @@ def get_owm_update_coordinator(mode: str) -> type[OWMUpdateCoordinator]: """Create coordinator with a factory.""" coordinators = { OWM_MODE_V30: WeatherUpdateCoordinator, + OWM_MODE_V40: WeatherUpdateCoordinator, OWM_MODE_FREE_CURRENT: WeatherUpdateCoordinator, OWM_MODE_FREE_FORECAST: WeatherUpdateCoordinator, OWM_MODE_AIRPOLLUTION: AirPollutionUpdateCoordinator, diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index 37419d82b8fae6..6b7fd082b8b9c8 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -69,7 +69,7 @@ }, "exceptions": { "service_minute_forecast_mode": { - "message": "Minute forecast is available only when {name} mode is set to v3.0" + "message": "Minute forecast is available only when {name} mode is set to v3.0 or v4.0" } }, "issues": { diff --git a/homeassistant/components/openweathermap/utils.py b/homeassistant/components/openweathermap/utils.py index ba5378fb31cea2..949811ac6aa1e7 100644 --- a/homeassistant/components/openweathermap/utils.py +++ b/homeassistant/components/openweathermap/utils.py @@ -3,20 +3,42 @@ from typing import Any from pyopenweathermap import RequestError, create_owm_client +from pyopenweathermap.client.onecall_client import OWMOneCallClient from homeassistant.const import CONF_LANGUAGE, CONF_MODE -from .const import DEFAULT_LANGUAGE, DEFAULT_OWM_MODE +from .const import DEFAULT_LANGUAGE, DEFAULT_OWM_MODE, OWM_MODE_V40 OPTION_DEFAULTS = {CONF_LANGUAGE: DEFAULT_LANGUAGE, CONF_MODE: DEFAULT_OWM_MODE} +class OWMOneCallClientV4(OWMOneCallClient): + """OWM client for One Call API 4.0.""" + + def _get_url(self, lat: float, lon: float) -> str: + return ( + "https://api.openweathermap.org/data/4.0/onecall?" + f"lat={lat}&" + f"lon={lon}&" + f"appid={self.api_key}&" + f"units={self.units}&" + f"lang={self.lang}" + ) + + +def get_owm_client(api_key: str, mode: str, language: str = DEFAULT_LANGUAGE) -> Any: + """Get the OWM client.""" + if mode == OWM_MODE_V40: + return OWMOneCallClientV4(api_key, mode, lang=language) + return create_owm_client(api_key, mode, lang=language) + + async def validate_api_key(api_key, mode): """Validate API key.""" api_key_valid = None errors, description_placeholders = {}, {} try: - owm_client = create_owm_client(api_key, mode) + owm_client = get_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 84c8c4f18dc637..d7491702cde13d 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -44,6 +44,7 @@ OWM_MODE_AIRPOLLUTION, OWM_MODE_FREE_FORECAST, OWM_MODE_V30, + OWM_MODE_V40, ) from .coordinator import OWMUpdateCoordinator @@ -106,7 +107,7 @@ def __init__( ) self.mode = mode - if mode == OWM_MODE_V30: + if mode in (OWM_MODE_V30, OWM_MODE_V40): self._attr_supported_features = ( WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY @@ -117,7 +118,7 @@ def __init__( async def async_get_minute_forecast(self) -> dict[str, list[dict]] | dict: """Return Minute forecast.""" - if self.mode == OWM_MODE_V30: + if self.mode in (OWM_MODE_V30, OWM_MODE_V40): return self.coordinator.data[ATTR_API_MINUTE_FORECAST] raise ServiceValidationError( translation_domain=DOMAIN, diff --git a/tests/components/openweathermap/conftest.py b/tests/components/openweathermap/conftest.py index b534d8fd98c788..f1166e41c25a2d 100644 --- a/tests/components/openweathermap/conftest.py +++ b/tests/components/openweathermap/conftest.py @@ -155,11 +155,11 @@ def owm_client_mock() -> Generator[AsyncMock]: client.validate_key.return_value = True with ( patch( - "homeassistant.components.openweathermap.create_owm_client", + "homeassistant.components.openweathermap.get_owm_client", return_value=client, ), patch( - "homeassistant.components.openweathermap.utils.create_owm_client", + "homeassistant.components.openweathermap.utils.get_owm_client", return_value=client, ), ): diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr index d4c07e43cf0bb9..a1f0034e2f5968 100644 --- a/tests/components/openweathermap/snapshots/test_sensor.ambr +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -2372,3 +2372,966 @@ 'state': '35.388', }) # --- +# name: test_sensor_states[v4.0][sensor.openweathermap_apparent_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_apparent_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Apparent temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Apparent temperature', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'feels_like_temperature', + 'unique_id': '12.34-56.78-feels_like_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_apparent_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'temperature', + : 'OpenWeatherMap Apparent temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_apparent_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.07', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_cloud_coverage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_cloud_coverage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Cloud coverage', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cloud coverage', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'clouds', + 'unique_id': '12.34-56.78-clouds', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_cloud_coverage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'OpenWeatherMap Cloud coverage', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_cloud_coverage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_condition-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_condition', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Condition', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Condition', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'condition', + 'unique_id': '12.34-56.78-condition', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_condition-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'OpenWeatherMap Condition', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_condition', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cloudy', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_dew_point_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_dew_point_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Dew point temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Dew point temperature', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dew_point', + 'unique_id': '12.34-56.78-dew_point', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_dew_point_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'temperature', + : 'OpenWeatherMap Dew point temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_dew_point_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.99', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Humidity', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-humidity', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'humidity', + : 'OpenWeatherMap Humidity', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '82', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_precipitation_kind-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_precipitation_kind', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Precipitation kind', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Precipitation kind', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precipitation_kind', + 'unique_id': '12.34-56.78-precipitation_kind', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_precipitation_kind-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'OpenWeatherMap Precipitation kind', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_precipitation_kind', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Rain', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Pressure', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pressure', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'pressure', + : 'OpenWeatherMap Pressure', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1000', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_rain_intensity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_rain_intensity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Rain intensity', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Rain intensity', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'rain', + 'unique_id': '12.34-56.78-rain', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_rain_intensity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'precipitation_intensity', + : 'OpenWeatherMap Rain intensity', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_rain_intensity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.21', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_snow_intensity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_snow_intensity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Snow intensity', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Snow intensity', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'snow', + 'unique_id': '12.34-56.78-snow', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_snow_intensity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'precipitation_intensity', + : 'OpenWeatherMap Snow intensity', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_snow_intensity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'temperature', + : 'OpenWeatherMap Temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.84', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_uv_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_uv_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'UV index', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'UV index', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index', + 'unique_id': '12.34-56.78-uv_index', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_uv_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'OpenWeatherMap UV index', + : , + : 'UV index', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.13', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_visibility-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_visibility', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Visibility', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Visibility', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'visibility_distance', + 'unique_id': '12.34-56.78-visibility_distance', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_visibility-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'distance', + : 'OpenWeatherMap Visibility', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_visibility', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10000', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_weather-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_weather', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Weather', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Weather', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'weather', + 'unique_id': '12.34-56.78-weather', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_weather-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'OpenWeatherMap Weather', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_weather', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'broken clouds', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_weather_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_weather_code', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Weather code', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Weather code', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'weather_code', + 'unique_id': '12.34-56.78-weather_code', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_weather_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'OpenWeatherMap Weather code', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_weather_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '803', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_wind_direction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_wind_direction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Wind direction', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind direction', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-wind_bearing', + 'unit_of_measurement': '°', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_wind_direction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'wind_direction', + : 'OpenWeatherMap Wind direction', + : , + : '°', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_wind_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '199', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_wind_gust_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_wind_gust_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Wind gust speed', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust speed', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust', + 'unique_id': '12.34-56.78-wind_gust', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_wind_gust_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'wind_speed', + : 'OpenWeatherMap Wind gust speed', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_wind_gust_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42.516', + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_wind_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_wind_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Wind speed', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-wind_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v4.0][sensor.openweathermap_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Data provided by OpenWeatherMap', + : 'wind_speed', + : 'OpenWeatherMap Wind speed', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35.388', + }) +# --- diff --git a/tests/components/openweathermap/snapshots/test_weather.ambr b/tests/components/openweathermap/snapshots/test_weather.ambr index 5fa23353805353..e5d22beef92771 100644 --- a/tests/components/openweathermap/snapshots/test_weather.ambr +++ b/tests/components/openweathermap/snapshots/test_weather.ambr @@ -23,6 +23,30 @@ }), }) # --- +# name: test_get_minute_forecast[v4.0][mock_service_response] + dict({ + 'weather.openweathermap': 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_weather_states[current][weather.openweathermap-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -223,3 +247,70 @@ 'state': 'cloudy', }) # --- +# name: test_weather_states[v4.0][weather.openweathermap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'weather', + 'entity_category': None, + 'entity_id': 'weather.openweathermap', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '12.34-56.78', + 'unit_of_measurement': None, + }) +# --- +# name: test_weather_states[v4.0][weather.openweathermap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 2.1, + : 'Data provided by OpenWeatherMap', + : 75, + : 4.0, + : 'OpenWeatherMap', + : 82, + : , + : 1000.0, + : , + : , + : 6.8, + : , + : 10.0, + : , + : 199, + : 42.52, + : 35.39, + : , + }), + 'context': , + 'entity_id': 'weather.openweathermap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cloudy', + }) +# --- diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index 039498e5ec3562..4f3545d555105e 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -11,6 +11,7 @@ DEFAULT_OWM_MODE, DOMAIN, OWM_MODE_V30, + OWM_MODE_V40, ) from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import ( @@ -46,11 +47,14 @@ VALID_YAML_CONFIG = {CONF_API_KEY: "foo"} +@pytest.mark.parametrize("mode", [OWM_MODE_V30, OWM_MODE_V40]) async def test_successful_config_flow( hass: HomeAssistant, owm_client_mock: AsyncMock, + mode: str, ) -> None: """Test that the form is served with valid input.""" + user_input = {**USER_INPUT, CONF_MODE: mode} result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -61,14 +65,14 @@ async def test_successful_config_flow( # create entry result = await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT, + user_input, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME - assert result["data"][CONF_LATITUDE] == USER_INPUT[CONF_LOCATION][CONF_LATITUDE] - assert result["data"][CONF_LONGITUDE] == USER_INPUT[CONF_LOCATION][CONF_LONGITUDE] - assert result["data"][CONF_API_KEY] == USER_INPUT[CONF_API_KEY] + assert result["data"][CONF_LATITUDE] == user_input[CONF_LOCATION][CONF_LATITUDE] + assert result["data"][CONF_LONGITUDE] == user_input[CONF_LOCATION][CONF_LONGITUDE] + assert result["data"][CONF_API_KEY] == user_input[CONF_API_KEY] # validate entry state conf_entries = hass.config_entries.async_entries(DOMAIN) @@ -81,7 +85,7 @@ async def test_successful_config_flow( assert entry.state is ConfigEntryState.NOT_LOADED -@pytest.mark.parametrize("mode", [OWM_MODE_V30], indirect=True) +@pytest.mark.parametrize("mode", [OWM_MODE_V30, OWM_MODE_V40], indirect=True) async def test_abort_config_flow( hass: HomeAssistant, owm_client_mock: AsyncMock, diff --git a/tests/components/openweathermap/test_sensor.py b/tests/components/openweathermap/test_sensor.py index 516633e9bc2ee5..9e26325523b576 100644 --- a/tests/components/openweathermap/test_sensor.py +++ b/tests/components/openweathermap/test_sensor.py @@ -10,6 +10,7 @@ OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_FORECAST, OWM_MODE_V30, + OWM_MODE_V40, ) from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -21,7 +22,9 @@ @pytest.mark.parametrize( - "mode", [OWM_MODE_V30, OWM_MODE_FREE_CURRENT, OWM_MODE_AIRPOLLUTION], indirect=True + "mode", + [OWM_MODE_V30, OWM_MODE_V40, OWM_MODE_FREE_CURRENT, OWM_MODE_AIRPOLLUTION], + indirect=True, ) async def test_sensor_states( hass: HomeAssistant, diff --git a/tests/components/openweathermap/test_weather.py b/tests/components/openweathermap/test_weather.py index f87201672efe81..279d9c1efa2afa 100644 --- a/tests/components/openweathermap/test_weather.py +++ b/tests/components/openweathermap/test_weather.py @@ -10,6 +10,7 @@ OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_FORECAST, OWM_MODE_V30, + OWM_MODE_V40, ) from homeassistant.components.openweathermap.weather import SERVICE_GET_MINUTE_FORECAST from homeassistant.const import Platform @@ -24,7 +25,7 @@ ENTITY_ID = "weather.openweathermap" -@pytest.mark.parametrize("mode", [OWM_MODE_V30], indirect=True) +@pytest.mark.parametrize("mode", [OWM_MODE_V30, OWM_MODE_V40], indirect=True) async def test_get_minute_forecast( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -61,7 +62,7 @@ async def test_get_minute_forecast_unavailable( with pytest.raises( ServiceValidationError, match="Minute forecast is available only when" - " OpenWeatherMap mode is set to v3.0", + " OpenWeatherMap mode is set to v3.0 or v4.0", ): await hass.services.async_call( DOMAIN, @@ -73,7 +74,9 @@ async def test_get_minute_forecast_unavailable( @pytest.mark.parametrize( - "mode", [OWM_MODE_V30, OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_FORECAST], indirect=True + "mode", + [OWM_MODE_V30, OWM_MODE_V40, OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_FORECAST], + indirect=True, ) async def test_weather_states( hass: HomeAssistant,