Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate OpenWeaterMap to new library (support API 3.0) #116870

Merged
merged 24 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 12 additions & 24 deletions homeassistant/components/openweathermap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
import logging
from typing import Any

from pyowm import OWM
from pyowm.utils.config import get_default_config
from pyopenweathermap import OWMClient

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
Expand All @@ -20,12 +19,8 @@
)
from homeassistant.core import HomeAssistant

from .const import (
CONFIG_FLOW_VERSION,
FORECAST_MODE_FREE_DAILY,
FORECAST_MODE_ONECALL_DAILY,
PLATFORMS,
)
from .const import CONFIG_FLOW_VERSION, OWM_MODE_V25, PLATFORMS
from .repairs import async_create_issue, async_delete_issue
from .weather_update_coordinator import WeatherUpdateCoordinator

_LOGGER = logging.getLogger(__name__)
Expand All @@ -49,14 +44,17 @@ async def async_setup_entry(
api_key = entry.data[CONF_API_KEY]
latitude = entry.data.get(CONF_LATITUDE, hass.config.latitude)
longitude = entry.data.get(CONF_LONGITUDE, hass.config.longitude)
forecast_mode = _get_config_value(entry, CONF_MODE)
language = _get_config_value(entry, CONF_LANGUAGE)
mode = _get_config_value(entry, CONF_MODE)

config_dict = _get_owm_config(language)
if mode == OWM_MODE_V25:
async_create_issue(hass, entry.entry_id)
else:
async_delete_issue(hass, entry.entry_id)

owm = OWM(api_key, config_dict).weather_manager()
owm_client = OWMClient(api_key, mode, lang=language)
weather_coordinator = WeatherUpdateCoordinator(
owm, latitude, longitude, forecast_mode, hass
owm_client, latitude, longitude, hass
)

await weather_coordinator.async_config_entry_first_refresh()
Expand All @@ -78,11 +76,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

_LOGGER.debug("Migrating OpenWeatherMap entry from version %s", version)

if version == 1:
if (mode := data[CONF_MODE]) == FORECAST_MODE_FREE_DAILY:
mode = FORECAST_MODE_ONECALL_DAILY

new_data = {**data, CONF_MODE: mode}
if version < 3:
new_data = {**data, CONF_MODE: OWM_MODE_V25}
config_entries.async_update_entry(
entry, data=new_data, version=CONFIG_FLOW_VERSION
)
Expand All @@ -108,10 +103,3 @@ def _get_config_value(config_entry: ConfigEntry, key: str) -> Any:
if config_entry.options:
return config_entry.options[key]
return config_entry.data[key]


def _get_owm_config(language: str) -> dict[str, Any]:
"""Get OpenWeatherMap configuration and add language to it."""
config_dict = get_default_config()
config_dict["language"] = language
return config_dict
56 changes: 30 additions & 26 deletions homeassistant/components/openweathermap/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

from __future__ import annotations

from pyowm import OWM
from pyowm.commons.exceptions import APIRequestError, UnauthorizedError
from pyopenweathermap import OWMClient, RequestError
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import (
CONF_API_KEY,
CONF_LANGUAGE,
Expand All @@ -20,12 +24,12 @@

from .const import (
CONFIG_FLOW_VERSION,
DEFAULT_FORECAST_MODE,
DEFAULT_LANGUAGE,
DEFAULT_NAME,
DEFAULT_OWM_MODE,
DOMAIN,
FORECAST_MODES,
LANGUAGES,
OWM_MODES,
)


Expand All @@ -42,27 +46,29 @@ def async_get_options_flow(
"""Get the options flow for this handler."""
return OpenWeatherMapOptionsFlow(config_entry)

async def async_step_user(self, user_input=None):
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors = {}
description_placeholders = {}

if user_input is not None:
latitude = user_input[CONF_LATITUDE]
longitude = user_input[CONF_LONGITUDE]
mode = user_input[CONF_MODE]

await self.async_set_unique_id(f"{latitude}-{longitude}")
self._abort_if_unique_id_configured()

api_key_valid = None
try:
api_online = await _is_owm_api_online(
self.hass, user_input[CONF_API_KEY], latitude, longitude
)
if not api_online:
errors["base"] = "invalid_api_key"
except UnauthorizedError:
errors["base"] = "invalid_api_key"
except APIRequestError:
owm_client = OWMClient(user_input[CONF_API_KEY], mode)
api_key_valid = await owm_client.validate_key()
except RequestError as error:
errors["base"] = "cannot_connect"
description_placeholders["error"] = str(error)

if api_key_valid is False:
errors["base"] = "invalid_api_key"

if not errors:
return self.async_create_entry(
Expand All @@ -79,16 +85,19 @@ async def async_step_user(self, user_input=None):
vol.Optional(
CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude,
vol.Optional(CONF_MODE, default=DEFAULT_FORECAST_MODE): vol.In(
FORECAST_MODES
),
vol.Optional(CONF_MODE, default=DEFAULT_OWM_MODE): vol.In(OWM_MODES),
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(
LANGUAGES
),
}
)

return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
return self.async_show_form(
step_id="user",
data_schema=schema,
errors=errors,
description_placeholders=description_placeholders,
)


class OpenWeatherMapOptionsFlow(OptionsFlow):
Expand All @@ -98,7 +107,7 @@ def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry

async def async_step_init(self, user_input=None):
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
freekode marked this conversation as resolved.
Show resolved Hide resolved
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
Expand All @@ -115,9 +124,9 @@ def _get_options_schema(self):
CONF_MODE,
default=self.config_entry.options.get(
CONF_MODE,
self.config_entry.data.get(CONF_MODE, DEFAULT_FORECAST_MODE),
self.config_entry.data.get(CONF_MODE, DEFAULT_OWM_MODE),
),
): vol.In(FORECAST_MODES),
): vol.In(OWM_MODES),
vol.Optional(
CONF_LANGUAGE,
default=self.config_entry.options.get(
Expand All @@ -127,8 +136,3 @@ def _get_options_schema(self):
): vol.In(LANGUAGES),
}
)


async def _is_owm_api_online(hass, api_key, lat, lon):
owm = OWM(api_key).weather_manager()
return await hass.async_add_executor_job(owm.weather_at_coords, lat, lon)
17 changes: 9 additions & 8 deletions homeassistant/components/openweathermap/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
DEFAULT_LANGUAGE = "en"
ATTRIBUTION = "Data provided by OpenWeatherMap"
MANUFACTURER = "OpenWeather"
CONFIG_FLOW_VERSION = 2
CONFIG_FLOW_VERSION = 3
ATTR_API_PRECIPITATION = "precipitation"
ATTR_API_PRECIPITATION_KIND = "precipitation_kind"
ATTR_API_DATETIME = "datetime"
Expand All @@ -45,7 +45,11 @@
ATTR_API_UV_INDEX = "uv_index"
ATTR_API_VISIBILITY_DISTANCE = "visibility_distance"
ATTR_API_WEATHER_CODE = "weather_code"
ATTR_API_CLOUD_COVERAGE = "cloud_coverage"
ATTR_API_FORECAST = "forecast"
ATTR_API_CURRENT = "current"
ATTR_API_HOURLY_FORECAST = "hourly_forecast"
ATTR_API_DAILY_FORECAST = "daily_forecast"
UPDATE_LISTENER = "update_listener"
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]

Expand All @@ -67,13 +71,10 @@
FORECAST_MODE_FREE_DAILY = "freedaily"
FORECAST_MODE_ONECALL_HOURLY = "onecall_hourly"
FORECAST_MODE_ONECALL_DAILY = "onecall_daily"
FORECAST_MODES = [
FORECAST_MODE_HOURLY,
FORECAST_MODE_DAILY,
FORECAST_MODE_ONECALL_HOURLY,
FORECAST_MODE_ONECALL_DAILY,
]
DEFAULT_FORECAST_MODE = FORECAST_MODE_HOURLY
OWM_MODE_V25 = "v2.5"
OWM_MODE_V30 = "v3.0"
OWM_MODES = [OWM_MODE_V30, OWM_MODE_V25]
DEFAULT_OWM_MODE = OWM_MODE_V30

LANGUAGES = [
"af",
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/openweathermap/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/openweathermap",
"iot_class": "cloud_polling",
"loggers": ["geojson", "pyowm", "pysocks"],
"requirements": ["pyowm==3.2.0"]
"loggers": ["pyopenweathermap"],
"requirements": ["pyopenweathermap==0.0.9"]
}
31 changes: 31 additions & 0 deletions homeassistant/components/openweathermap/repairs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Issues for OpenWeatherMap."""

from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir

from .const import DOMAIN


def _get_issue_id(entry_id: str) -> str:
return "deprecated_v25_" + entry_id
freekode marked this conversation as resolved.
Show resolved Hide resolved


@callback
def async_create_issue(hass: HomeAssistant, entry_id: str) -> None:
"""Create issue for V2.5 deprecation."""
ir.async_create_issue(
hass=hass,
domain=DOMAIN,
issue_id=_get_issue_id(entry_id),
is_fixable=False,
frenck marked this conversation as resolved.
Show resolved Hide resolved
is_persistent=True,
severity=ir.IssueSeverity.WARNING,
learn_more_url="https://openweathermap.org/one-call-transfer",
translation_key="deprecated_v25",
)
bdraco marked this conversation as resolved.
Show resolved Hide resolved


@callback
def async_delete_issue(hass: HomeAssistant, entry_id: str) -> None:
"""Remove issue for V2.5 deprecation."""
ir.async_delete_issue(hass=hass, domain=DOMAIN, issue_id=_get_issue_id(entry_id))
20 changes: 10 additions & 10 deletions homeassistant/components/openweathermap/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@

from . import OpenweathermapConfigEntry
from .const import (
ATTR_API_CLOUD_COVERAGE,
ATTR_API_CLOUDS,
ATTR_API_CONDITION,
ATTR_API_CURRENT,
ATTR_API_DAILY_FORECAST,
ATTR_API_DEW_POINT,
ATTR_API_FEELS_LIKE_TEMPERATURE,
ATTR_API_FORECAST,
ATTR_API_FORECAST_CONDITION,
ATTR_API_FORECAST_PRECIPITATION,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_API_FORECAST_PRESSURE,
Expand Down Expand Up @@ -162,7 +163,7 @@
)
FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=ATTR_API_FORECAST_CONDITION,
key=ATTR_API_CONDITION,
name="Condition",
),
SensorEntityDescription(
Expand Down Expand Up @@ -211,7 +212,7 @@
device_class=SensorDeviceClass.WIND_SPEED,
),
SensorEntityDescription(
key=ATTR_API_CLOUDS,
key=ATTR_API_CLOUD_COVERAGE,
name="Cloud coverage",
native_unit_of_measurement=PERCENTAGE,
),
Expand Down Expand Up @@ -313,7 +314,9 @@ def __init__(
@property
def native_value(self) -> StateType:
"""Return the state of the device."""
return self._weather_coordinator.data.get(self.entity_description.key, None)
return self._weather_coordinator.data[ATTR_API_CURRENT].get(
self.entity_description.key
)


class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor):
Expand All @@ -333,11 +336,8 @@ def __init__(
@property
def native_value(self) -> StateType | datetime:
"""Return the state of the device."""
forecasts = self._weather_coordinator.data.get(ATTR_API_FORECAST)
if not forecasts:
return None

value = forecasts[0].get(self.entity_description.key, None)
forecasts = self._weather_coordinator.data[ATTR_API_DAILY_FORECAST]
value = forecasts[0].get(self.entity_description.key)
if (
value
and self.entity_description.device_class is SensorDeviceClass.TIMESTAMP
Expand Down
8 changes: 7 additions & 1 deletion homeassistant/components/openweathermap/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
},
"error": {
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"cannot_connect": "Failed to connect: {error}"
},
"step": {
"user": {
Expand All @@ -30,5 +30,11 @@
}
}
}
},
"issues": {
"deprecated_v25": {
"title": "OpenWeatherMap API V2.5 deprecated",
"description": "OWM API v2.5 will be closed in June 2024, migrate all your OpenWeatherMap integration to mode `v3.0."
frenck marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Loading