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

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Evgeny 2024-05-24 09:51:10 +02:00 committed by GitHub
parent 5bca9d142c
commit 24d31924a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 580 additions and 523 deletions

View File

@ -972,6 +972,7 @@ omit =
homeassistant/components/openuv/sensor.py
homeassistant/components/openweathermap/__init__.py
homeassistant/components/openweathermap/coordinator.py
homeassistant/components/openweathermap/repairs.py
homeassistant/components/openweathermap/sensor.py
homeassistant/components/openweathermap/weather.py
homeassistant/components/opnsense/__init__.py

View File

@ -6,8 +6,7 @@ from dataclasses import dataclass
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 (
@ -20,13 +19,9 @@ from homeassistant.const import (
)
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 .coordinator import WeatherUpdateCoordinator
from .repairs import async_create_issue, async_delete_issue
_LOGGER = logging.getLogger(__name__)
@ -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()
@ -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
)
@ -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

View File

@ -2,11 +2,14 @@
from __future__ import annotations
from pyowm import OWM
from pyowm.commons.exceptions import APIRequestError, UnauthorizedError
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,
@ -20,13 +23,14 @@ import homeassistant.helpers.config_validation as cv
from .const import (
CONFIG_FLOW_VERSION,
DEFAULT_FORECAST_MODE,
DEFAULT_LANGUAGE,
DEFAULT_NAME,
DEFAULT_OWM_MODE,
DOMAIN,
FORECAST_MODES,
LANGUAGES,
OWM_MODES,
)
from .utils import validate_api_key
class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
@ -42,27 +46,22 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
"""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()
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:
errors["base"] = "cannot_connect"
errors, description_placeholders = await validate_api_key(
user_input[CONF_API_KEY], mode
)
if not errors:
return self.async_create_entry(
@ -79,16 +78,19 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
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):
@ -98,7 +100,7 @@ class OpenWeatherMapOptionsFlow(OptionsFlow):
"""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: dict | None = None) -> ConfigFlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
@ -115,9 +117,9 @@ class OpenWeatherMapOptionsFlow(OptionsFlow):
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(
@ -127,8 +129,3 @@ class OpenWeatherMapOptionsFlow(OptionsFlow):
): 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)

View File

@ -25,7 +25,7 @@ DEFAULT_NAME = "OpenWeatherMap"
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"
@ -45,7 +45,11 @@ ATTR_API_SNOW = "snow"
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]
@ -67,13 +71,10 @@ FORECAST_MODE_DAILY = "daily"
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",

View File

@ -1,39 +1,35 @@
"""Weather data coordinator for the OpenWeatherMap (OWM) service."""
import asyncio
from datetime import timedelta
import logging
from pyowm.commons.exceptions import APIRequestError, UnauthorizedError
from pyopenweathermap import (
CurrentWeather,
DailyWeatherForecast,
HourlyWeatherForecast,
OWMClient,
RequestError,
WeatherReport,
)
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY,
Forecast,
)
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers import sun
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import TemperatureConverter
from .const import (
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_CLOUDS,
ATTR_API_FORECAST_CONDITION,
ATTR_API_FORECAST_FEELS_LIKE_TEMPERATURE,
ATTR_API_FORECAST_HUMIDITY,
ATTR_API_FORECAST_PRECIPITATION,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_API_FORECAST_PRESSURE,
ATTR_API_FORECAST_TEMP,
ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_SPEED,
ATTR_API_HOURLY_FORECAST,
ATTR_API_HUMIDITY,
ATTR_API_PRECIPITATION_KIND,
ATTR_API_PRESSURE,
@ -49,10 +45,6 @@ from .const import (
ATTR_API_WIND_SPEED,
CONDITION_MAP,
DOMAIN,
FORECAST_MODE_DAILY,
FORECAST_MODE_HOURLY,
FORECAST_MODE_ONECALL_DAILY,
FORECAST_MODE_ONECALL_HOURLY,
WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT,
)
@ -64,15 +56,17 @@ WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
class WeatherUpdateCoordinator(DataUpdateCoordinator):
"""Weather data update coordinator."""
def __init__(self, owm, latitude, longitude, forecast_mode, hass):
def __init__(
self,
owm_client: OWMClient,
latitude,
longitude,
hass: HomeAssistant,
) -> None:
"""Initialize coordinator."""
self._owm_client = owm
self._owm_client = owm_client
self._latitude = latitude
self._longitude = longitude
self.forecast_mode = forecast_mode
self._forecast_limit = None
if forecast_mode == FORECAST_MODE_DAILY:
self._forecast_limit = 15
super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
@ -80,184 +74,122 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
async def _async_update_data(self):
"""Update the data."""
data = {}
async with asyncio.timeout(20):
try:
weather_response = await self._get_owm_weather()
data = self._convert_weather_response(weather_response)
except (APIRequestError, UnauthorizedError) as error:
raise UpdateFailed(error) from error
return data
async def _get_owm_weather(self):
"""Poll weather data from OWM."""
if self.forecast_mode in (
FORECAST_MODE_ONECALL_HOURLY,
FORECAST_MODE_ONECALL_DAILY,
):
weather = await self.hass.async_add_executor_job(
self._owm_client.one_call, self._latitude, self._longitude
)
else:
weather = await self.hass.async_add_executor_job(
self._get_legacy_weather_and_forecast
try:
weather_report = await self._owm_client.get_weather(
self._latitude, self._longitude
)
except RequestError as error:
raise UpdateFailed(error) from error
return self._convert_weather_response(weather_report)
return weather
def _get_legacy_weather_and_forecast(self):
"""Get weather and forecast data from OWM."""
interval = self._get_legacy_forecast_interval()
weather = self._owm_client.weather_at_coords(self._latitude, self._longitude)
forecast = self._owm_client.forecast_at_coords(
self._latitude, self._longitude, interval, self._forecast_limit
)
return LegacyWeather(weather.weather, forecast.forecast.weathers)
def _get_legacy_forecast_interval(self):
"""Get the correct forecast interval depending on the forecast mode."""
interval = "daily"
if self.forecast_mode == FORECAST_MODE_HOURLY:
interval = "3h"
return interval
def _convert_weather_response(self, weather_response):
def _convert_weather_response(self, weather_report: WeatherReport):
"""Format the weather response correctly."""
current_weather = weather_response.current
forecast_weather = self._get_forecast_from_weather_response(weather_response)
_LOGGER.debug("OWM weather response: %s", weather_report)
return {
ATTR_API_TEMPERATURE: current_weather.temperature("celsius").get("temp"),
ATTR_API_FEELS_LIKE_TEMPERATURE: current_weather.temperature("celsius").get(
"feels_like"
),
ATTR_API_DEW_POINT: self._fmt_dewpoint(current_weather.dewpoint),
ATTR_API_PRESSURE: current_weather.pressure.get("press"),
ATTR_API_CURRENT: self._get_current_weather_data(weather_report.current),
ATTR_API_HOURLY_FORECAST: [
self._get_hourly_forecast_weather_data(item)
for item in weather_report.hourly_forecast
],
ATTR_API_DAILY_FORECAST: [
self._get_daily_forecast_weather_data(item)
for item in weather_report.daily_forecast
],
}
def _get_current_weather_data(self, current_weather: CurrentWeather):
return {
ATTR_API_CONDITION: self._get_condition(current_weather.condition.id),
ATTR_API_TEMPERATURE: current_weather.temperature,
ATTR_API_FEELS_LIKE_TEMPERATURE: current_weather.feels_like,
ATTR_API_PRESSURE: current_weather.pressure,
ATTR_API_HUMIDITY: current_weather.humidity,
ATTR_API_WIND_BEARING: current_weather.wind().get("deg"),
ATTR_API_WIND_GUST: current_weather.wind().get("gust"),
ATTR_API_WIND_SPEED: current_weather.wind().get("speed"),
ATTR_API_CLOUDS: current_weather.clouds,
ATTR_API_RAIN: self._get_rain(current_weather.rain),
ATTR_API_SNOW: self._get_snow(current_weather.snow),
ATTR_API_DEW_POINT: current_weather.dew_point,
ATTR_API_CLOUDS: current_weather.cloud_coverage,
ATTR_API_WIND_SPEED: current_weather.wind_speed,
ATTR_API_WIND_GUST: current_weather.wind_gust,
ATTR_API_WIND_BEARING: current_weather.wind_bearing,
ATTR_API_WEATHER: current_weather.condition.description,
ATTR_API_WEATHER_CODE: current_weather.condition.id,
ATTR_API_UV_INDEX: current_weather.uv_index,
ATTR_API_VISIBILITY_DISTANCE: current_weather.visibility,
ATTR_API_RAIN: self._get_precipitation_value(current_weather.rain),
ATTR_API_SNOW: self._get_precipitation_value(current_weather.snow),
ATTR_API_PRECIPITATION_KIND: self._calc_precipitation_kind(
current_weather.rain, current_weather.snow
),
ATTR_API_WEATHER: current_weather.detailed_status,
ATTR_API_CONDITION: self._get_condition(current_weather.weather_code),
ATTR_API_UV_INDEX: current_weather.uvi,
ATTR_API_VISIBILITY_DISTANCE: current_weather.visibility_distance,
ATTR_API_WEATHER_CODE: current_weather.weather_code,
ATTR_API_FORECAST: forecast_weather,
}
def _get_forecast_from_weather_response(self, weather_response):
"""Extract the forecast data from the weather response."""
forecast_arg = "forecast"
if self.forecast_mode == FORECAST_MODE_ONECALL_HOURLY:
forecast_arg = "forecast_hourly"
elif self.forecast_mode == FORECAST_MODE_ONECALL_DAILY:
forecast_arg = "forecast_daily"
return [
self._convert_forecast(x) for x in getattr(weather_response, forecast_arg)
]
def _get_hourly_forecast_weather_data(self, forecast: HourlyWeatherForecast):
return Forecast(
datetime=forecast.date_time.isoformat(),
condition=self._get_condition(forecast.condition.id),
temperature=forecast.temperature,
native_apparent_temperature=forecast.feels_like,
pressure=forecast.pressure,
humidity=forecast.humidity,
native_dew_point=forecast.dew_point,
cloud_coverage=forecast.cloud_coverage,
wind_speed=forecast.wind_speed,
native_wind_gust_speed=forecast.wind_gust,
wind_bearing=forecast.wind_bearing,
uv_index=float(forecast.uv_index),
precipitation_probability=round(forecast.precipitation_probability * 100),
precipitation=self._calc_precipitation(forecast.rain, forecast.snow),
)
def _convert_forecast(self, entry):
"""Convert the forecast data."""
forecast = {
ATTR_API_FORECAST_TIME: dt_util.utc_from_timestamp(
entry.reference_time("unix")
).isoformat(),
ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation(
entry.rain, entry.snow
),
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: (
round(entry.precipitation_probability * 100)
),
ATTR_API_FORECAST_PRESSURE: entry.pressure.get("press"),
ATTR_API_FORECAST_WIND_SPEED: entry.wind().get("speed"),
ATTR_API_FORECAST_WIND_BEARING: entry.wind().get("deg"),
ATTR_API_FORECAST_CONDITION: self._get_condition(
entry.weather_code, entry.reference_time("unix")
),
ATTR_API_FORECAST_CLOUDS: entry.clouds,
ATTR_API_FORECAST_FEELS_LIKE_TEMPERATURE: entry.temperature("celsius").get(
"feels_like_day"
),
ATTR_API_FORECAST_HUMIDITY: entry.humidity,
}
temperature_dict = entry.temperature("celsius")
if "max" in temperature_dict and "min" in temperature_dict:
forecast[ATTR_API_FORECAST_TEMP] = entry.temperature("celsius").get("max")
forecast[ATTR_API_FORECAST_TEMP_LOW] = entry.temperature("celsius").get(
"min"
)
else:
forecast[ATTR_API_FORECAST_TEMP] = entry.temperature("celsius").get("temp")
return forecast
@staticmethod
def _fmt_dewpoint(dewpoint):
"""Format the dewpoint data."""
if dewpoint is not None:
return round(
TemperatureConverter.convert(
dewpoint, UnitOfTemperature.KELVIN, UnitOfTemperature.CELSIUS
),
1,
)
return None
@staticmethod
def _get_rain(rain):
"""Get rain data from weather data."""
if "all" in rain:
return round(rain["all"], 2)
if "3h" in rain:
return round(rain["3h"], 2)
if "1h" in rain:
return round(rain["1h"], 2)
return 0
@staticmethod
def _get_snow(snow):
"""Get snow data from weather data."""
if snow:
if "all" in snow:
return round(snow["all"], 2)
if "3h" in snow:
return round(snow["3h"], 2)
if "1h" in snow:
return round(snow["1h"], 2)
return 0
def _get_daily_forecast_weather_data(self, forecast: DailyWeatherForecast):
return Forecast(
datetime=forecast.date_time.isoformat(),
condition=self._get_condition(forecast.condition.id),
temperature=forecast.temperature.max,
templow=forecast.temperature.min,
native_apparent_temperature=forecast.feels_like,
pressure=forecast.pressure,
humidity=forecast.humidity,
native_dew_point=forecast.dew_point,
cloud_coverage=forecast.cloud_coverage,
wind_speed=forecast.wind_speed,
native_wind_gust_speed=forecast.wind_gust,
wind_bearing=forecast.wind_bearing,
uv_index=float(forecast.uv_index),
precipitation_probability=round(forecast.precipitation_probability * 100),
precipitation=round(forecast.rain + forecast.snow, 2),
)
@staticmethod
def _calc_precipitation(rain, snow):
"""Calculate the precipitation."""
rain_value = 0
if WeatherUpdateCoordinator._get_rain(rain) != 0:
rain_value = WeatherUpdateCoordinator._get_rain(rain)
snow_value = 0
if WeatherUpdateCoordinator._get_snow(snow) != 0:
snow_value = WeatherUpdateCoordinator._get_snow(snow)
rain_value = WeatherUpdateCoordinator._get_precipitation_value(rain)
snow_value = WeatherUpdateCoordinator._get_precipitation_value(snow)
return round(rain_value + snow_value, 2)
@staticmethod
def _calc_precipitation_kind(rain, snow):
"""Determine the precipitation kind."""
if WeatherUpdateCoordinator._get_rain(rain) != 0:
if WeatherUpdateCoordinator._get_snow(snow) != 0:
rain_value = WeatherUpdateCoordinator._get_precipitation_value(rain)
snow_value = WeatherUpdateCoordinator._get_precipitation_value(snow)
if rain_value != 0:
if snow_value != 0:
return "Snow and Rain"
return "Rain"
if WeatherUpdateCoordinator._get_snow(snow) != 0:
if snow_value != 0:
return "Snow"
return "None"
@staticmethod
def _get_precipitation_value(precipitation):
"""Get precipitation value from weather data."""
if "all" in precipitation:
return round(precipitation["all"], 2)
if "3h" in precipitation:
return round(precipitation["3h"], 2)
if "1h" in precipitation:
return round(precipitation["1h"], 2)
return 0
def _get_condition(self, weather_code, timestamp=None):
"""Get weather condition from weather data."""
if weather_code == WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT:
@ -269,12 +201,3 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
return ATTR_CONDITION_CLEAR_NIGHT
return CONDITION_MAP.get(weather_code)
class LegacyWeather:
"""Class to harmonize weather data model for hourly, daily and One Call APIs."""
def __init__(self, current_weather, forecast):
"""Initialize weather object."""
self.current = current_weather
self.forecast = forecast

View File

@ -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"]
}

View File

@ -0,0 +1,87 @@
"""Issues for OpenWeatherMap."""
from typing import cast
from homeassistant import data_entry_flow
from homeassistant.components.repairs import RepairsFlow
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_MODE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir
from .const import DOMAIN, OWM_MODE_V30
from .utils import validate_api_key
class DeprecatedV25RepairFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
def __init__(self, entry: ConfigEntry) -> None:
"""Create flow."""
super().__init__()
self.entry = entry
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""
return self.async_show_form(step_id="migrate")
async def async_step_migrate(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the migrate step of a fix flow."""
errors, description_placeholders = {}, {}
new_options = {**self.entry.options, CONF_MODE: OWM_MODE_V30}
errors, description_placeholders = await validate_api_key(
self.entry.data[CONF_API_KEY], OWM_MODE_V30
)
if not errors:
self.hass.config_entries.async_update_entry(self.entry, options=new_options)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_create_entry(data={})
return self.async_show_form(
step_id="migrate",
errors=errors,
description_placeholders=description_placeholders,
)
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
data: dict[str, str | int | float | None],
) -> RepairsFlow:
"""Create single repair flow."""
entry_id = cast(str, data.get("entry_id"))
entry = hass.config_entries.async_get_entry(entry_id)
assert entry
return DeprecatedV25RepairFlow(entry)
def _get_issue_id(entry_id: str) -> str:
return f"deprecated_v25_{entry_id}"
@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=True,
is_persistent=True,
severity=ir.IssueSeverity.WARNING,
learn_more_url="https://www.home-assistant.io/integrations/openweathermap/",
translation_key="deprecated_v25",
data={"entry_id": entry_id},
)
@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))

View File

@ -30,12 +30,13 @@ from homeassistant.util import dt as dt_util
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,
@ -162,7 +163,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
)
FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=ATTR_API_FORECAST_CONDITION,
key=ATTR_API_CONDITION,
name="Condition",
),
SensorEntityDescription(
@ -211,7 +212,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
device_class=SensorDeviceClass.WIND_SPEED,
),
SensorEntityDescription(
key=ATTR_API_CLOUDS,
key=ATTR_API_CLOUD_COVERAGE,
name="Cloud coverage",
native_unit_of_measurement=PERCENTAGE,
),
@ -313,7 +314,9 @@ class OpenWeatherMapSensor(AbstractOpenWeatherMapSensor):
@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):
@ -333,11 +336,8 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor):
@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

View File

@ -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": {
@ -30,5 +30,22 @@
}
}
}
},
"issues": {
"deprecated_v25": {
"title": "OpenWeatherMap API V2.5 deprecated",
"fix_flow": {
"step": {
"migrate": {
"title": "OpenWeatherMap API V2.5 deprecated",
"description": "OWM API v2.5 will be closed in June 2024.\nYou need to migrate all your OpenWeatherMap integration to mode v3.0.\n\nBefore the migration, you must have active subscription (be aware subscripiton activation take up to 2h). After your subscription is activated click **Submit** to migrate the integration to API V3.0. Read documentation for more information."
}
},
"error": {
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"cannot_connect": "Failed to connect: {error}"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
"""Util functions for OpenWeatherMap."""
from pyopenweathermap import OWMClient, RequestError
async def validate_api_key(api_key, mode):
"""Validate API key."""
api_key_valid = None
errors, description_placeholders = {}, {}
try:
owm_client = OWMClient(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"
return errors, description_placeholders

View File

@ -2,21 +2,7 @@
from __future__ import annotations
from typing import cast
from homeassistant.components.weather import (
ATTR_FORECAST_CLOUD_COVERAGE,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_HUMIDITY,
ATTR_FORECAST_NATIVE_APPARENT_TEMP,
ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_FORECAST_NATIVE_PRESSURE,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
Forecast,
SingleCoordinatorWeatherEntity,
WeatherEntityFeature,
@ -35,21 +21,11 @@ from . import OpenweathermapConfigEntry
from .const import (
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_CLOUDS,
ATTR_API_FORECAST_CONDITION,
ATTR_API_FORECAST_FEELS_LIKE_TEMPERATURE,
ATTR_API_FORECAST_HUMIDITY,
ATTR_API_FORECAST_PRECIPITATION,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_API_FORECAST_PRESSURE,
ATTR_API_FORECAST_TEMP,
ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_SPEED,
ATTR_API_HOURLY_FORECAST,
ATTR_API_HUMIDITY,
ATTR_API_PRESSURE,
ATTR_API_TEMPERATURE,
@ -59,27 +35,10 @@ from .const import (
ATTRIBUTION,
DEFAULT_NAME,
DOMAIN,
FORECAST_MODE_DAILY,
FORECAST_MODE_ONECALL_DAILY,
MANUFACTURER,
)
from .coordinator import WeatherUpdateCoordinator
FORECAST_MAP = {
ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION,
ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_API_FORECAST_PRESSURE: ATTR_FORECAST_NATIVE_PRESSURE,
ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP,
ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_API_FORECAST_CLOUDS: ATTR_FORECAST_CLOUD_COVERAGE,
ATTR_API_FORECAST_HUMIDITY: ATTR_FORECAST_HUMIDITY,
ATTR_API_FORECAST_FEELS_LIKE_TEMPERATURE: ATTR_FORECAST_NATIVE_APPARENT_TEMP,
}
async def async_setup_entry(
hass: HomeAssistant,
@ -124,84 +83,66 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina
manufacturer=MANUFACTURER,
name=DEFAULT_NAME,
)
if weather_coordinator.forecast_mode in (
FORECAST_MODE_DAILY,
FORECAST_MODE_ONECALL_DAILY,
):
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
else: # FORECAST_MODE_DAILY or FORECAST_MODE_ONECALL_HOURLY
self._attr_supported_features = WeatherEntityFeature.FORECAST_HOURLY
self._attr_supported_features = (
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
)
@property
def condition(self) -> str | None:
"""Return the current condition."""
return self.coordinator.data[ATTR_API_CONDITION]
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_CONDITION]
@property
def cloud_coverage(self) -> float | None:
"""Return the Cloud coverage in %."""
return self.coordinator.data[ATTR_API_CLOUDS]
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_CLOUDS]
@property
def native_apparent_temperature(self) -> float | None:
"""Return the apparent temperature."""
return self.coordinator.data[ATTR_API_FEELS_LIKE_TEMPERATURE]
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_FEELS_LIKE_TEMPERATURE]
@property
def native_temperature(self) -> float | None:
"""Return the temperature."""
return self.coordinator.data[ATTR_API_TEMPERATURE]
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_TEMPERATURE]
@property
def native_pressure(self) -> float | None:
"""Return the pressure."""
return self.coordinator.data[ATTR_API_PRESSURE]
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_PRESSURE]
@property
def humidity(self) -> float | None:
"""Return the humidity."""
return self.coordinator.data[ATTR_API_HUMIDITY]
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_HUMIDITY]
@property
def native_dew_point(self) -> float | None:
"""Return the dew point."""
return self.coordinator.data[ATTR_API_DEW_POINT]
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_DEW_POINT]
@property
def native_wind_gust_speed(self) -> float | None:
"""Return the wind gust speed."""
return self.coordinator.data[ATTR_API_WIND_GUST]
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_GUST]
@property
def native_wind_speed(self) -> float | None:
"""Return the wind speed."""
return self.coordinator.data[ATTR_API_WIND_SPEED]
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_SPEED]
@property
def wind_bearing(self) -> float | str | None:
"""Return the wind bearing."""
return self.coordinator.data[ATTR_API_WIND_BEARING]
@property
def _forecast(self) -> list[Forecast] | None:
"""Return the forecast array."""
api_forecasts = self.coordinator.data[ATTR_API_FORECAST]
forecasts = [
{
ha_key: forecast[api_key]
for api_key, ha_key in FORECAST_MAP.items()
if api_key in forecast
}
for forecast in api_forecasts
]
return cast(list[Forecast], forecasts)
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_BEARING]
@callback
def _async_forecast_daily(self) -> list[Forecast] | None:
"""Return the daily forecast in native units."""
return self._forecast
return self.coordinator.data[ATTR_API_DAILY_FORECAST]
@callback
def _async_forecast_hourly(self) -> list[Forecast] | None:
"""Return the hourly forecast in native units."""
return self._forecast
return self.coordinator.data[ATTR_API_HOURLY_FORECAST]

View File

@ -2039,6 +2039,9 @@ pyombi==0.1.10
# homeassistant.components.openuv
pyopenuv==2023.02.0
# homeassistant.components.openweathermap
pyopenweathermap==0.0.9
# homeassistant.components.opnsense
pyopnsense==0.4.0
@ -2059,9 +2062,6 @@ pyotp==2.8.0
# homeassistant.components.overkiz
pyoverkiz==1.13.10
# homeassistant.components.openweathermap
pyowm==3.2.0
# homeassistant.components.onewire
pyownet==0.10.0.post1

View File

@ -1599,6 +1599,9 @@ pyoctoprintapi==0.1.12
# homeassistant.components.openuv
pyopenuv==2023.02.0
# homeassistant.components.openweathermap
pyopenweathermap==0.0.9
# homeassistant.components.opnsense
pyopnsense==0.4.0
@ -1616,9 +1619,6 @@ pyotp==2.8.0
# homeassistant.components.overkiz
pyoverkiz==1.13.10
# homeassistant.components.openweathermap
pyowm==3.2.0
# homeassistant.components.onewire
pyownet==0.10.0.post1

View File

@ -1,13 +1,23 @@
"""Define tests for the OpenWeatherMap config flow."""
from unittest.mock import MagicMock, patch
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
from pyowm.commons.exceptions import APIRequestError, UnauthorizedError
from pyopenweathermap import (
CurrentWeather,
DailyTemperature,
DailyWeatherForecast,
RequestError,
WeatherCondition,
WeatherReport,
)
import pytest
from homeassistant.components.openweathermap.const import (
DEFAULT_FORECAST_MODE,
DEFAULT_LANGUAGE,
DEFAULT_OWM_MODE,
DOMAIN,
OWM_MODE_V25,
)
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import (
@ -28,190 +38,262 @@ CONFIG = {
CONF_API_KEY: "foo",
CONF_LATITUDE: 50,
CONF_LONGITUDE: 40,
CONF_MODE: DEFAULT_FORECAST_MODE,
CONF_LANGUAGE: DEFAULT_LANGUAGE,
CONF_MODE: OWM_MODE_V25,
}
VALID_YAML_CONFIG = {CONF_API_KEY: "foo"}
async def test_form(hass: HomeAssistant) -> None:
def _create_mocked_owm_client(is_valid: bool):
current_weather = CurrentWeather(
date_time=datetime.fromtimestamp(1714063536, tz=UTC),
temperature=6.84,
feels_like=2.07,
pressure=1000,
humidity=82,
dew_point=3.99,
uv_index=0.13,
cloud_coverage=75,
visibility=10000,
wind_speed=9.83,
wind_bearing=199,
wind_gust=None,
rain={},
snow={},
condition=WeatherCondition(
id=803,
main="Clouds",
description="broken clouds",
icon="04d",
),
)
daily_weather_forecast = DailyWeatherForecast(
date_time=datetime.fromtimestamp(1714063536, tz=UTC),
summary="There will be clear sky until morning, then partly cloudy",
temperature=DailyTemperature(
day=18.76,
min=8.11,
max=21.26,
night=13.06,
evening=20.51,
morning=8.47,
),
feels_like=DailyTemperature(
day=18.76,
min=8.11,
max=21.26,
night=13.06,
evening=20.51,
morning=8.47,
),
pressure=1015,
humidity=62,
dew_point=11.34,
wind_speed=8.14,
wind_bearing=168,
wind_gust=11.81,
condition=WeatherCondition(
id=803,
main="Clouds",
description="broken clouds",
icon="04d",
),
cloud_coverage=84,
precipitation_probability=0,
uv_index=4.06,
rain=0,
snow=0,
)
weather_report = WeatherReport(current_weather, [], [daily_weather_forecast])
mocked_owm_client = MagicMock()
mocked_owm_client.validate_key = AsyncMock(return_value=is_valid)
mocked_owm_client.get_weather = AsyncMock(return_value=weather_report)
return mocked_owm_client
@pytest.fixture(name="owm_client_mock")
def mock_owm_client():
"""Mock config_flow OWMClient."""
with patch(
"homeassistant.components.openweathermap.OWMClient",
) as owm_client_mock:
yield owm_client_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
async def test_successful_config_flow(
hass: HomeAssistant,
owm_client_mock,
config_flow_owm_client_mock,
) -> None:
"""Test that the form is served with valid input."""
mocked_owm = _create_mocked_owm(True)
mock = _create_mocked_owm_client(True)
owm_client_mock.return_value = mock
config_flow_owm_client_mock.return_value = mock
with patch(
"pyowm.weatherapi25.weather_manager.WeatherManager",
return_value=mocked_owm,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
await hass.async_block_till_done()
conf_entries = hass.config_entries.async_entries(DOMAIN)
entry = conf_entries[0]
assert entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(conf_entries[0].entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == CONFIG[CONF_NAME]
assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE]
assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE]
assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY]
async def test_form_options(hass: HomeAssistant) -> None:
"""Test that the options form."""
mocked_owm = _create_mocked_owm(True)
with patch(
"pyowm.weatherapi25.weather_manager.WeatherManager",
return_value=mocked_owm,
):
config_entry = MockConfigEntry(
domain=DOMAIN, unique_id="openweathermap_unique_id", data=CONFIG
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_MODE: "daily"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert config_entry.options == {
CONF_MODE: "daily",
CONF_LANGUAGE: DEFAULT_LANGUAGE,
}
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_MODE: "onecall_daily"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert config_entry.options == {
CONF_MODE: "onecall_daily",
CONF_LANGUAGE: DEFAULT_LANGUAGE,
}
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
async def test_form_invalid_api_key(hass: HomeAssistant) -> None:
"""Test that the form is served with no input."""
mocked_owm = _create_mocked_owm(True)
with patch(
"pyowm.weatherapi25.weather_manager.WeatherManager",
return_value=mocked_owm,
side_effect=UnauthorizedError(""),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
assert result["errors"] == {"base": "invalid_api_key"}
async def test_form_api_call_error(hass: HomeAssistant) -> None:
"""Test setting up with api call error."""
mocked_owm = _create_mocked_owm(True)
with patch(
"pyowm.weatherapi25.weather_manager.WeatherManager",
return_value=mocked_owm,
side_effect=APIRequestError(""),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
assert result["errors"] == {"base": "cannot_connect"}
async def test_form_api_offline(hass: HomeAssistant) -> None:
"""Test setting up with api call error."""
mocked_owm = _create_mocked_owm(False)
with patch(
"homeassistant.components.openweathermap.config_flow.OWM",
return_value=mocked_owm,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
assert result["errors"] == {"base": "invalid_api_key"}
def _create_mocked_owm(is_api_online: bool):
mocked_owm = MagicMock()
weather = MagicMock()
weather.temperature.return_value.get.return_value = 10
weather.pressure.get.return_value = 10
weather.humidity.return_value = 10
weather.wind.return_value.get.return_value = 0
weather.clouds.return_value = "clouds"
weather.rain.return_value = []
weather.snow.return_value = []
weather.detailed_status.return_value = "status"
weather.weather_code = 803
weather.dewpoint = 10
mocked_owm.weather_at_coords.return_value.weather = weather
one_day_forecast = MagicMock()
one_day_forecast.reference_time.return_value = 10
one_day_forecast.temperature.return_value.get.return_value = 10
one_day_forecast.rain.return_value.get.return_value = 0
one_day_forecast.snow.return_value.get.return_value = 0
one_day_forecast.wind.return_value.get.return_value = 0
one_day_forecast.weather_code = 803
mocked_owm.forecast_at_coords.return_value.forecast.weathers = [one_day_forecast]
one_call = MagicMock()
one_call.current = weather
one_call.forecast_hourly = [one_day_forecast]
one_call.forecast_daily = [one_day_forecast]
mocked_owm.one_call.return_value = one_call
mocked_owm.weather_manager.return_value.weather_at_coords.return_value = (
is_api_online
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
return mocked_owm
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
await hass.async_block_till_done()
conf_entries = hass.config_entries.async_entries(DOMAIN)
entry = conf_entries[0]
assert entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(conf_entries[0].entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == CONFIG[CONF_NAME]
assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE]
assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE]
assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY]
async def test_abort_config_flow(
hass: HomeAssistant,
owm_client_mock,
config_flow_owm_client_mock,
) -> None:
"""Test that the form is served with same data."""
mock = _create_mocked_owm_client(True)
owm_client_mock.return_value = mock
config_flow_owm_client_mock.return_value = mock
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT
async def test_config_flow_options_change(
hass: HomeAssistant,
owm_client_mock,
config_flow_owm_client_mock,
) -> None:
"""Test that the options form."""
mock = _create_mocked_owm_client(True)
owm_client_mock.return_value = mock
config_flow_owm_client_mock.return_value = mock
config_entry = MockConfigEntry(
domain=DOMAIN, unique_id="openweathermap_unique_id", data=CONFIG
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
new_language = "es"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_MODE: DEFAULT_OWM_MODE, CONF_LANGUAGE: new_language},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert config_entry.options == {
CONF_LANGUAGE: new_language,
CONF_MODE: DEFAULT_OWM_MODE,
}
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
updated_language = "es"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_LANGUAGE: updated_language}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert config_entry.options == {
CONF_LANGUAGE: updated_language,
CONF_MODE: DEFAULT_OWM_MODE,
}
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
async def test_form_invalid_api_key(
hass: HomeAssistant,
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)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
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)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=CONFIG
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_form_api_call_error(
hass: HomeAssistant,
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.side_effect = RequestError("oops")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
config_flow_owm_client_mock.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=CONFIG
)
assert result["type"] is FlowResultType.CREATE_ENTRY