Add support for OpenWeatherMap One Call API (#39839)
parent
79b10a8efd
commit
5ddf99e496
|
@ -632,7 +632,6 @@ omit =
|
||||||
homeassistant/components/openuv/sensor.py
|
homeassistant/components/openuv/sensor.py
|
||||||
homeassistant/components/openweathermap/sensor.py
|
homeassistant/components/openweathermap/sensor.py
|
||||||
homeassistant/components/openweathermap/weather.py
|
homeassistant/components/openweathermap/weather.py
|
||||||
homeassistant/components/openweathermap/forecast_update_coordinator.py
|
|
||||||
homeassistant/components/openweathermap/weather_update_coordinator.py
|
homeassistant/components/openweathermap/weather_update_coordinator.py
|
||||||
homeassistant/components/openweathermap/abstract_owm_sensor.py
|
homeassistant/components/openweathermap/abstract_owm_sensor.py
|
||||||
homeassistant/components/opnsense/*
|
homeassistant/components/opnsense/*
|
||||||
|
|
|
@ -318,7 +318,7 @@ homeassistant/components/openerz/* @misialq
|
||||||
homeassistant/components/opengarage/* @danielhiversen
|
homeassistant/components/opengarage/* @danielhiversen
|
||||||
homeassistant/components/opentherm_gw/* @mvn23
|
homeassistant/components/opentherm_gw/* @mvn23
|
||||||
homeassistant/components/openuv/* @bachya
|
homeassistant/components/openuv/* @bachya
|
||||||
homeassistant/components/openweathermap/* @fabaff @freekode
|
homeassistant/components/openweathermap/* @fabaff @freekode @nzapponi
|
||||||
homeassistant/components/opnsense/* @mtreinish
|
homeassistant/components/opnsense/* @mtreinish
|
||||||
homeassistant/components/orangepi_gpio/* @pascallj
|
homeassistant/components/orangepi_gpio/* @pascallj
|
||||||
homeassistant/components/oru/* @bvlaicu
|
homeassistant/components/oru/* @bvlaicu
|
||||||
|
|
|
@ -3,8 +3,9 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pyowm import OWM
|
from pyowm import OWM
|
||||||
|
from pyowm.utils.config import get_default_config
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
|
@ -18,13 +19,14 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from .const import (
|
from .const import (
|
||||||
COMPONENTS,
|
COMPONENTS,
|
||||||
CONF_LANGUAGE,
|
CONF_LANGUAGE,
|
||||||
|
CONFIG_FLOW_VERSION,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ENTRY_FORECAST_COORDINATOR,
|
|
||||||
ENTRY_NAME,
|
ENTRY_NAME,
|
||||||
ENTRY_WEATHER_COORDINATOR,
|
ENTRY_WEATHER_COORDINATOR,
|
||||||
|
FORECAST_MODE_FREE_DAILY,
|
||||||
|
FORECAST_MODE_ONECALL_DAILY,
|
||||||
UPDATE_LISTENER,
|
UPDATE_LISTENER,
|
||||||
)
|
)
|
||||||
from .forecast_update_coordinator import ForecastUpdateCoordinator
|
|
||||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -33,11 +35,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||||
"""Set up the OpenWeatherMap component."""
|
"""Set up the OpenWeatherMap component."""
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
weather_configs = _filter_domain_configs(config.get("weather", []), DOMAIN)
|
|
||||||
sensor_configs = _filter_domain_configs(config.get("sensor", []), DOMAIN)
|
|
||||||
|
|
||||||
_import_configs(hass, weather_configs + sensor_configs)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,26 +47,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||||
forecast_mode = _get_config_value(config_entry, CONF_MODE)
|
forecast_mode = _get_config_value(config_entry, CONF_MODE)
|
||||||
language = _get_config_value(config_entry, CONF_LANGUAGE)
|
language = _get_config_value(config_entry, CONF_LANGUAGE)
|
||||||
|
|
||||||
owm = OWM(API_key=api_key, language=language)
|
config_dict = _get_owm_config(language)
|
||||||
weather_coordinator = WeatherUpdateCoordinator(owm, latitude, longitude, hass)
|
|
||||||
forecast_coordinator = ForecastUpdateCoordinator(
|
owm = OWM(api_key, config_dict).weather_manager()
|
||||||
|
weather_coordinator = WeatherUpdateCoordinator(
|
||||||
owm, latitude, longitude, forecast_mode, hass
|
owm, latitude, longitude, forecast_mode, hass
|
||||||
)
|
)
|
||||||
|
|
||||||
await weather_coordinator.async_refresh()
|
await weather_coordinator.async_refresh()
|
||||||
await forecast_coordinator.async_refresh()
|
|
||||||
|
|
||||||
if (
|
if not weather_coordinator.last_update_success:
|
||||||
not weather_coordinator.last_update_success
|
|
||||||
and not forecast_coordinator.last_update_success
|
|
||||||
):
|
|
||||||
raise ConfigEntryNotReady
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
hass.data[DOMAIN][config_entry.entry_id] = {
|
hass.data[DOMAIN][config_entry.entry_id] = {
|
||||||
ENTRY_NAME: name,
|
ENTRY_NAME: name,
|
||||||
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
|
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
|
||||||
ENTRY_FORECAST_COORDINATOR: forecast_coordinator,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for component in COMPONENTS:
|
for component in COMPONENTS:
|
||||||
|
@ -83,6 +76,28 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass, entry):
|
||||||
|
"""Migrate old entry."""
|
||||||
|
config_entries = hass.config_entries
|
||||||
|
data = entry.data
|
||||||
|
version = entry.version
|
||||||
|
|
||||||
|
_LOGGER.debug("Migrating OpenWeatherMap entry from version %s", version)
|
||||||
|
|
||||||
|
if version == 1:
|
||||||
|
mode = data[CONF_MODE]
|
||||||
|
if mode == FORECAST_MODE_FREE_DAILY:
|
||||||
|
mode = FORECAST_MODE_ONECALL_DAILY
|
||||||
|
|
||||||
|
new_data = {**data, CONF_MODE: mode}
|
||||||
|
version = entry.version = CONFIG_FLOW_VERSION
|
||||||
|
config_entries.async_update_entry(entry, data=new_data)
|
||||||
|
|
||||||
|
_LOGGER.info("Migration to version %s successful", version)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry):
|
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||||
"""Update options."""
|
"""Update options."""
|
||||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
@ -106,18 +121,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
def _import_configs(hass, configs):
|
|
||||||
for config in configs:
|
|
||||||
_LOGGER.debug("Importing OpenWeatherMap %s", config)
|
|
||||||
hass.async_create_task(
|
|
||||||
hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_IMPORT},
|
|
||||||
data=config,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_domain_configs(elements, domain):
|
def _filter_domain_configs(elements, domain):
|
||||||
return list(filter(lambda elem: elem["platform"] == domain, elements))
|
return list(filter(lambda elem: elem["platform"] == domain, elements))
|
||||||
|
|
||||||
|
@ -126,3 +129,10 @@ def _get_config_value(config_entry, key):
|
||||||
if config_entry.options:
|
if config_entry.options:
|
||||||
return config_entry.options[key]
|
return config_entry.options[key]
|
||||||
return config_entry.data[key]
|
return config_entry.data[key]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_owm_config(language):
|
||||||
|
"""Get OpenWeatherMap configuration and add language to it."""
|
||||||
|
config_dict = get_default_config()
|
||||||
|
config_dict["language"] = language
|
||||||
|
return config_dict
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""Config flow for OpenWeatherMap."""
|
"""Config flow for OpenWeatherMap."""
|
||||||
from pyowm import OWM
|
from pyowm import OWM
|
||||||
from pyowm.exceptions.api_call_error import APICallError
|
from pyowm.commons.exceptions import APIRequestError, UnauthorizedError
|
||||||
from pyowm.exceptions.api_response_error import UnauthorizedError
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
@ -17,6 +16,7 @@ import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_LANGUAGE,
|
CONF_LANGUAGE,
|
||||||
|
CONFIG_FLOW_VERSION,
|
||||||
DEFAULT_FORECAST_MODE,
|
DEFAULT_FORECAST_MODE,
|
||||||
DEFAULT_LANGUAGE,
|
DEFAULT_LANGUAGE,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
|
@ -25,22 +25,11 @@ from .const import (
|
||||||
)
|
)
|
||||||
from .const import DOMAIN # pylint:disable=unused-import
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_API_KEY): str,
|
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
|
||||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
|
||||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
|
||||||
vol.Optional(CONF_MODE, default=DEFAULT_FORECAST_MODE): vol.In(FORECAST_MODES),
|
|
||||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGES),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Config flow for OpenWeatherMap."""
|
"""Config flow for OpenWeatherMap."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = CONFIG_FLOW_VERSION
|
||||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -62,35 +51,40 @@ class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api_online = await _is_owm_api_online(
|
api_online = await _is_owm_api_online(
|
||||||
self.hass, user_input[CONF_API_KEY]
|
self.hass, user_input[CONF_API_KEY], latitude, longitude
|
||||||
)
|
)
|
||||||
if not api_online:
|
if not api_online:
|
||||||
errors["base"] = "invalid_api_key"
|
errors["base"] = "invalid_api_key"
|
||||||
except UnauthorizedError:
|
except UnauthorizedError:
|
||||||
errors["base"] = "invalid_api_key"
|
errors["base"] = "invalid_api_key"
|
||||||
except APICallError:
|
except APIRequestError:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=user_input[CONF_NAME], data=user_input
|
title=user_input[CONF_NAME], data=user_input
|
||||||
)
|
)
|
||||||
return self.async_show_form(step_id="user", data_schema=SCHEMA, errors=errors)
|
|
||||||
|
|
||||||
async def async_step_import(self, import_input=None):
|
schema = vol.Schema(
|
||||||
"""Set the config entry up from yaml."""
|
{
|
||||||
config = import_input.copy()
|
vol.Required(CONF_API_KEY): str,
|
||||||
if CONF_NAME not in config:
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
||||||
config[CONF_NAME] = DEFAULT_NAME
|
vol.Optional(
|
||||||
if CONF_LATITUDE not in config:
|
CONF_LATITUDE, default=self.hass.config.latitude
|
||||||
config[CONF_LATITUDE] = self.hass.config.latitude
|
): cv.latitude,
|
||||||
if CONF_LONGITUDE not in config:
|
vol.Optional(
|
||||||
config[CONF_LONGITUDE] = self.hass.config.longitude
|
CONF_LONGITUDE, default=self.hass.config.longitude
|
||||||
if CONF_MODE not in config:
|
): cv.longitude,
|
||||||
config[CONF_MODE] = DEFAULT_FORECAST_MODE
|
vol.Optional(CONF_MODE, default=DEFAULT_FORECAST_MODE): vol.In(
|
||||||
if CONF_LANGUAGE not in config:
|
FORECAST_MODES
|
||||||
config[CONF_LANGUAGE] = DEFAULT_LANGUAGE
|
),
|
||||||
return await self.async_step_user(config)
|
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(
|
||||||
|
LANGUAGES
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||||
|
|
||||||
|
|
||||||
class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow):
|
class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow):
|
||||||
|
@ -129,6 +123,6 @@ class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _is_owm_api_online(hass, api_key):
|
async def _is_owm_api_online(hass, api_key, lat, lon):
|
||||||
owm = OWM(api_key)
|
owm = OWM(api_key).weather_manager()
|
||||||
return await hass.async_add_executor_job(owm.is_API_online)
|
return await hass.async_add_executor_job(owm.one_call, lat, lon)
|
||||||
|
|
|
@ -24,11 +24,10 @@ from homeassistant.const import (
|
||||||
DOMAIN = "openweathermap"
|
DOMAIN = "openweathermap"
|
||||||
DEFAULT_NAME = "OpenWeatherMap"
|
DEFAULT_NAME = "OpenWeatherMap"
|
||||||
DEFAULT_LANGUAGE = "en"
|
DEFAULT_LANGUAGE = "en"
|
||||||
DEFAULT_FORECAST_MODE = "freedaily"
|
|
||||||
ATTRIBUTION = "Data provided by OpenWeatherMap"
|
ATTRIBUTION = "Data provided by OpenWeatherMap"
|
||||||
CONF_LANGUAGE = "language"
|
CONF_LANGUAGE = "language"
|
||||||
|
CONFIG_FLOW_VERSION = 2
|
||||||
ENTRY_NAME = "name"
|
ENTRY_NAME = "name"
|
||||||
ENTRY_FORECAST_COORDINATOR = "forecast_coordinator"
|
|
||||||
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
||||||
ATTR_API_PRECIPITATION = "precipitation"
|
ATTR_API_PRECIPITATION = "precipitation"
|
||||||
ATTR_API_DATETIME = "datetime"
|
ATTR_API_DATETIME = "datetime"
|
||||||
|
@ -44,13 +43,25 @@ ATTR_API_RAIN = "rain"
|
||||||
ATTR_API_SNOW = "snow"
|
ATTR_API_SNOW = "snow"
|
||||||
ATTR_API_WEATHER_CODE = "weather_code"
|
ATTR_API_WEATHER_CODE = "weather_code"
|
||||||
ATTR_API_FORECAST = "forecast"
|
ATTR_API_FORECAST = "forecast"
|
||||||
ATTR_API_THIS_DAY_FORECAST = "this_day_forecast"
|
|
||||||
SENSOR_NAME = "sensor_name"
|
SENSOR_NAME = "sensor_name"
|
||||||
SENSOR_UNIT = "sensor_unit"
|
SENSOR_UNIT = "sensor_unit"
|
||||||
SENSOR_DEVICE_CLASS = "sensor_device_class"
|
SENSOR_DEVICE_CLASS = "sensor_device_class"
|
||||||
UPDATE_LISTENER = "update_listener"
|
UPDATE_LISTENER = "update_listener"
|
||||||
COMPONENTS = ["sensor", "weather"]
|
COMPONENTS = ["sensor", "weather"]
|
||||||
FORECAST_MODES = ["hourly", "daily", "freedaily"]
|
|
||||||
|
FORECAST_MODE_HOURLY = "hourly"
|
||||||
|
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_ONECALL_DAILY
|
||||||
|
|
||||||
MONITORED_CONDITIONS = [
|
MONITORED_CONDITIONS = [
|
||||||
ATTR_API_WEATHER,
|
ATTR_API_WEATHER,
|
||||||
ATTR_API_TEMPERATURE,
|
ATTR_API_TEMPERATURE,
|
||||||
|
|
|
@ -1,137 +0,0 @@
|
||||||
"""Forecast data coordinator for the OpenWeatherMap (OWM) service."""
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import async_timeout
|
|
||||||
from pyowm.exceptions.api_call_error import APICallError
|
|
||||||
from pyowm.exceptions.api_response_error import UnauthorizedError
|
|
||||||
|
|
||||||
from homeassistant.components.weather import (
|
|
||||||
ATTR_FORECAST_CONDITION,
|
|
||||||
ATTR_FORECAST_PRECIPITATION,
|
|
||||||
ATTR_FORECAST_TEMP,
|
|
||||||
ATTR_FORECAST_TEMP_LOW,
|
|
||||||
ATTR_FORECAST_TIME,
|
|
||||||
ATTR_FORECAST_WIND_BEARING,
|
|
||||||
ATTR_FORECAST_WIND_SPEED,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
ATTR_API_FORECAST,
|
|
||||||
ATTR_API_THIS_DAY_FORECAST,
|
|
||||||
CONDITION_CLASSES,
|
|
||||||
DOMAIN,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
FORECAST_UPDATE_INTERVAL = timedelta(minutes=30)
|
|
||||||
|
|
||||||
|
|
||||||
class ForecastUpdateCoordinator(DataUpdateCoordinator):
|
|
||||||
"""Forecast data update coordinator."""
|
|
||||||
|
|
||||||
def __init__(self, owm, latitude, longitude, forecast_mode, hass):
|
|
||||||
"""Initialize coordinator."""
|
|
||||||
self._owm_client = owm
|
|
||||||
self._forecast_mode = forecast_mode
|
|
||||||
self._latitude = latitude
|
|
||||||
self._longitude = longitude
|
|
||||||
self._forecast_limit = 15
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
hass, _LOGGER, name=DOMAIN, update_interval=FORECAST_UPDATE_INTERVAL
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_update_data(self):
|
|
||||||
data = {}
|
|
||||||
with async_timeout.timeout(20):
|
|
||||||
try:
|
|
||||||
forecast_response = await self._get_owm_forecast()
|
|
||||||
data = self._convert_forecast_response(forecast_response)
|
|
||||||
except (APICallError, UnauthorizedError) as error:
|
|
||||||
raise UpdateFailed(error) from error
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
async def _get_owm_forecast(self):
|
|
||||||
if self._forecast_mode == "daily":
|
|
||||||
forecast_response = await self.hass.async_add_executor_job(
|
|
||||||
self._owm_client.daily_forecast_at_coords,
|
|
||||||
self._latitude,
|
|
||||||
self._longitude,
|
|
||||||
self._forecast_limit,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
forecast_response = await self.hass.async_add_executor_job(
|
|
||||||
self._owm_client.three_hours_forecast_at_coords,
|
|
||||||
self._latitude,
|
|
||||||
self._longitude,
|
|
||||||
)
|
|
||||||
return forecast_response.get_forecast()
|
|
||||||
|
|
||||||
def _convert_forecast_response(self, forecast_response):
|
|
||||||
weathers = self._get_weathers(forecast_response)
|
|
||||||
|
|
||||||
forecast_entries = self._convert_forecast_entries(weathers)
|
|
||||||
|
|
||||||
return {
|
|
||||||
ATTR_API_FORECAST: forecast_entries,
|
|
||||||
ATTR_API_THIS_DAY_FORECAST: forecast_entries[0],
|
|
||||||
}
|
|
||||||
|
|
||||||
def _get_weathers(self, forecast_response):
|
|
||||||
if self._forecast_mode == "freedaily":
|
|
||||||
return forecast_response.get_weathers()[::8]
|
|
||||||
return forecast_response.get_weathers()
|
|
||||||
|
|
||||||
def _convert_forecast_entries(self, entries):
|
|
||||||
if self._forecast_mode == "daily":
|
|
||||||
return list(map(self._convert_daily_forecast, entries))
|
|
||||||
return list(map(self._convert_forecast, entries))
|
|
||||||
|
|
||||||
def _convert_daily_forecast(self, entry):
|
|
||||||
return {
|
|
||||||
ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000,
|
|
||||||
ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get("day"),
|
|
||||||
ATTR_FORECAST_TEMP_LOW: entry.get_temperature("celsius").get("night"),
|
|
||||||
ATTR_FORECAST_PRECIPITATION: self._calc_daily_precipitation(
|
|
||||||
entry.get_rain().get("all"), entry.get_snow().get("all")
|
|
||||||
),
|
|
||||||
ATTR_FORECAST_WIND_SPEED: entry.get_wind().get("speed"),
|
|
||||||
ATTR_FORECAST_WIND_BEARING: entry.get_wind().get("deg"),
|
|
||||||
ATTR_FORECAST_CONDITION: self._get_condition(entry.get_weather_code()),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _convert_forecast(self, entry):
|
|
||||||
return {
|
|
||||||
ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000,
|
|
||||||
ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get("temp"),
|
|
||||||
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(entry),
|
|
||||||
ATTR_FORECAST_WIND_SPEED: entry.get_wind().get("speed"),
|
|
||||||
ATTR_FORECAST_WIND_BEARING: entry.get_wind().get("deg"),
|
|
||||||
ATTR_FORECAST_CONDITION: self._get_condition(entry.get_weather_code()),
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _calc_daily_precipitation(rain, snow):
|
|
||||||
"""Calculate the precipitation."""
|
|
||||||
rain_value = 0 if rain is None else rain
|
|
||||||
snow_value = 0 if snow is None else snow
|
|
||||||
if round(rain_value + snow_value, 1) == 0:
|
|
||||||
return None
|
|
||||||
return round(rain_value + snow_value, 1)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _calc_precipitation(entry):
|
|
||||||
return (
|
|
||||||
round(entry.get_rain().get("1h"), 1)
|
|
||||||
if entry.get_rain().get("1h") is not None
|
|
||||||
and (round(entry.get_rain().get("1h"), 1) > 0)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_condition(weather_code):
|
|
||||||
return [k for k, v in CONDITION_CLASSES.items() if weather_code in v][0]
|
|
|
@ -3,6 +3,6 @@
|
||||||
"name": "OpenWeatherMap",
|
"name": "OpenWeatherMap",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/openweathermap",
|
"documentation": "https://www.home-assistant.io/integrations/openweathermap",
|
||||||
"requirements": ["pyowm==2.10.0"],
|
"requirements": ["pyowm==3.1.0"],
|
||||||
"codeowners": ["@fabaff", "@freekode"]
|
"codeowners": ["@fabaff", "@freekode", "@nzapponi"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
"""Support for the OpenWeatherMap (OWM) service."""
|
"""Support for the OpenWeatherMap (OWM) service."""
|
||||||
from .abstract_owm_sensor import AbstractOpenWeatherMapSensor
|
from .abstract_owm_sensor import AbstractOpenWeatherMapSensor
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_API_THIS_DAY_FORECAST,
|
ATTR_API_FORECAST,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ENTRY_FORECAST_COORDINATOR,
|
|
||||||
ENTRY_NAME,
|
ENTRY_NAME,
|
||||||
ENTRY_WEATHER_COORDINATOR,
|
ENTRY_WEATHER_COORDINATOR,
|
||||||
FORECAST_MONITORED_CONDITIONS,
|
FORECAST_MONITORED_CONDITIONS,
|
||||||
|
@ -11,7 +10,6 @@ from .const import (
|
||||||
MONITORED_CONDITIONS,
|
MONITORED_CONDITIONS,
|
||||||
WEATHER_SENSOR_TYPES,
|
WEATHER_SENSOR_TYPES,
|
||||||
)
|
)
|
||||||
from .forecast_update_coordinator import ForecastUpdateCoordinator
|
|
||||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,7 +18,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
name = domain_data[ENTRY_NAME]
|
name = domain_data[ENTRY_NAME]
|
||||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||||
forecast_coordinator = domain_data[ENTRY_FORECAST_COORDINATOR]
|
|
||||||
|
|
||||||
weather_sensor_types = WEATHER_SENSOR_TYPES
|
weather_sensor_types = WEATHER_SENSOR_TYPES
|
||||||
forecast_sensor_types = FORECAST_SENSOR_TYPES
|
forecast_sensor_types = FORECAST_SENSOR_TYPES
|
||||||
|
@ -46,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
unique_id,
|
unique_id,
|
||||||
sensor_type,
|
sensor_type,
|
||||||
forecast_sensor_types[sensor_type],
|
forecast_sensor_types[sensor_type],
|
||||||
forecast_coordinator,
|
weather_coordinator,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -85,17 +82,18 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor):
|
||||||
unique_id,
|
unique_id,
|
||||||
sensor_type,
|
sensor_type,
|
||||||
sensor_configuration,
|
sensor_configuration,
|
||||||
forecast_coordinator: ForecastUpdateCoordinator,
|
weather_coordinator: WeatherUpdateCoordinator,
|
||||||
):
|
):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name, unique_id, sensor_type, sensor_configuration, forecast_coordinator
|
name, unique_id, sensor_type, sensor_configuration, weather_coordinator
|
||||||
)
|
)
|
||||||
self._forecast_coordinator = forecast_coordinator
|
self._weather_coordinator = weather_coordinator
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
return self._forecast_coordinator.data[ATTR_API_THIS_DAY_FORECAST].get(
|
forecasts = self._weather_coordinator.data.get(ATTR_API_FORECAST)
|
||||||
self._sensor_type, None
|
if forecasts is not None and len(forecasts) > 0:
|
||||||
)
|
return forecasts[0].get(self._sensor_type, None)
|
||||||
|
return None
|
||||||
|
|
|
@ -12,11 +12,9 @@ from .const import (
|
||||||
ATTR_API_WIND_SPEED,
|
ATTR_API_WIND_SPEED,
|
||||||
ATTRIBUTION,
|
ATTRIBUTION,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ENTRY_FORECAST_COORDINATOR,
|
|
||||||
ENTRY_NAME,
|
ENTRY_NAME,
|
||||||
ENTRY_WEATHER_COORDINATOR,
|
ENTRY_WEATHER_COORDINATOR,
|
||||||
)
|
)
|
||||||
from .forecast_update_coordinator import ForecastUpdateCoordinator
|
|
||||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,12 +23,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
name = domain_data[ENTRY_NAME]
|
name = domain_data[ENTRY_NAME]
|
||||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||||
forecast_coordinator = domain_data[ENTRY_FORECAST_COORDINATOR]
|
|
||||||
|
|
||||||
unique_id = f"{config_entry.unique_id}"
|
unique_id = f"{config_entry.unique_id}"
|
||||||
owm_weather = OpenWeatherMapWeather(
|
owm_weather = OpenWeatherMapWeather(name, unique_id, weather_coordinator)
|
||||||
name, unique_id, weather_coordinator, forecast_coordinator
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities([owm_weather], False)
|
async_add_entities([owm_weather], False)
|
||||||
|
|
||||||
|
@ -43,13 +38,11 @@ class OpenWeatherMapWeather(WeatherEntity):
|
||||||
name,
|
name,
|
||||||
unique_id,
|
unique_id,
|
||||||
weather_coordinator: WeatherUpdateCoordinator,
|
weather_coordinator: WeatherUpdateCoordinator,
|
||||||
forecast_coordinator: ForecastUpdateCoordinator,
|
|
||||||
):
|
):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._unique_id = unique_id
|
self._unique_id = unique_id
|
||||||
self._weather_coordinator = weather_coordinator
|
self._weather_coordinator = weather_coordinator
|
||||||
self._forecast_coordinator = forecast_coordinator
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -112,26 +105,19 @@ class OpenWeatherMapWeather(WeatherEntity):
|
||||||
@property
|
@property
|
||||||
def forecast(self):
|
def forecast(self):
|
||||||
"""Return the forecast array."""
|
"""Return the forecast array."""
|
||||||
return self._forecast_coordinator.data[ATTR_API_FORECAST]
|
return self._weather_coordinator.data[ATTR_API_FORECAST]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return (
|
return self._weather_coordinator.last_update_success
|
||||||
self._weather_coordinator.last_update_success
|
|
||||||
and self._forecast_coordinator.last_update_success
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Connect to dispatcher listening for entity data notifications."""
|
"""Connect to dispatcher listening for entity data notifications."""
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
self._weather_coordinator.async_add_listener(self.async_write_ha_state)
|
self._weather_coordinator.async_add_listener(self.async_write_ha_state)
|
||||||
)
|
)
|
||||||
self.async_on_remove(
|
|
||||||
self._forecast_coordinator.async_add_listener(self.async_write_ha_state)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Get the latest data from OWM and updates the states."""
|
"""Get the latest data from OWM and updates the states."""
|
||||||
await self._weather_coordinator.async_request_refresh()
|
await self._weather_coordinator.async_request_refresh()
|
||||||
await self._forecast_coordinator.async_request_refresh()
|
|
||||||
|
|
|
@ -3,14 +3,23 @@ from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
from pyowm.exceptions.api_call_error import APICallError
|
from pyowm.commons.exceptions import APIRequestError, UnauthorizedError
|
||||||
from pyowm.exceptions.api_response_error import UnauthorizedError
|
|
||||||
|
|
||||||
|
from homeassistant.components.weather import (
|
||||||
|
ATTR_FORECAST_CONDITION,
|
||||||
|
ATTR_FORECAST_PRECIPITATION,
|
||||||
|
ATTR_FORECAST_TEMP,
|
||||||
|
ATTR_FORECAST_TEMP_LOW,
|
||||||
|
ATTR_FORECAST_TIME,
|
||||||
|
ATTR_FORECAST_WIND_BEARING,
|
||||||
|
ATTR_FORECAST_WIND_SPEED,
|
||||||
|
)
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_API_CLOUDS,
|
ATTR_API_CLOUDS,
|
||||||
ATTR_API_CONDITION,
|
ATTR_API_CONDITION,
|
||||||
|
ATTR_API_FORECAST,
|
||||||
ATTR_API_HUMIDITY,
|
ATTR_API_HUMIDITY,
|
||||||
ATTR_API_PRESSURE,
|
ATTR_API_PRESSURE,
|
||||||
ATTR_API_RAIN,
|
ATTR_API_RAIN,
|
||||||
|
@ -22,6 +31,10 @@ from .const import (
|
||||||
ATTR_API_WIND_SPEED,
|
ATTR_API_WIND_SPEED,
|
||||||
CONDITION_CLASSES,
|
CONDITION_CLASSES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
FORECAST_MODE_DAILY,
|
||||||
|
FORECAST_MODE_HOURLY,
|
||||||
|
FORECAST_MODE_ONECALL_DAILY,
|
||||||
|
FORECAST_MODE_ONECALL_HOURLY,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -32,11 +45,15 @@ WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
||||||
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Weather data update coordinator."""
|
"""Weather data update coordinator."""
|
||||||
|
|
||||||
def __init__(self, owm, latitude, longitude, hass):
|
def __init__(self, owm, latitude, longitude, forecast_mode, hass):
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
self._owm_client = owm
|
self._owm_client = owm
|
||||||
self._latitude = latitude
|
self._latitude = latitude
|
||||||
self._longitude = longitude
|
self._longitude = longitude
|
||||||
|
self._forecast_mode = forecast_mode
|
||||||
|
self._forecast_limit = None
|
||||||
|
if forecast_mode == FORECAST_MODE_DAILY:
|
||||||
|
self._forecast_limit = 15
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
|
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
|
||||||
|
@ -48,47 +65,137 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||||
try:
|
try:
|
||||||
weather_response = await self._get_owm_weather()
|
weather_response = await self._get_owm_weather()
|
||||||
data = self._convert_weather_response(weather_response)
|
data = self._convert_weather_response(weather_response)
|
||||||
except (APICallError, UnauthorizedError) as error:
|
except (APIRequestError, UnauthorizedError) as error:
|
||||||
raise UpdateFailed(error) from error
|
raise UpdateFailed(error) from error
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def _get_owm_weather(self):
|
async def _get_owm_weather(self):
|
||||||
|
"""Poll weather data from OWM."""
|
||||||
|
if (
|
||||||
|
self._forecast_mode == FORECAST_MODE_ONECALL_HOURLY
|
||||||
|
or self._forecast_mode == FORECAST_MODE_ONECALL_DAILY
|
||||||
|
):
|
||||||
weather = await self.hass.async_add_executor_job(
|
weather = await self.hass.async_add_executor_job(
|
||||||
self._owm_client.weather_at_coords, self._latitude, self._longitude
|
self._owm_client.one_call, self._latitude, self._longitude
|
||||||
)
|
)
|
||||||
return weather.get_weather()
|
else:
|
||||||
|
weather = await self.hass.async_add_executor_job(
|
||||||
|
self._get_legacy_weather_and_forecast
|
||||||
|
)
|
||||||
|
|
||||||
|
return weather
|
||||||
|
|
||||||
|
def _get_legacy_weather_and_forecast(self):
|
||||||
|
"""Get weather and forecast data from OWM."""
|
||||||
|
interval = self._get_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_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_response):
|
||||||
|
"""Format the weather response correctly."""
|
||||||
|
current_weather = weather_response.current
|
||||||
|
forecast_weather = self._get_forecast_from_weather_response(weather_response)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_API_TEMPERATURE: weather_response.get_temperature("celsius").get(
|
ATTR_API_TEMPERATURE: current_weather.temperature("celsius").get("temp"),
|
||||||
"temp"
|
ATTR_API_PRESSURE: current_weather.pressure.get("press"),
|
||||||
),
|
ATTR_API_HUMIDITY: current_weather.humidity,
|
||||||
ATTR_API_PRESSURE: weather_response.get_pressure().get("press"),
|
ATTR_API_WIND_BEARING: current_weather.wind().get("deg"),
|
||||||
ATTR_API_HUMIDITY: weather_response.get_humidity(),
|
ATTR_API_WIND_SPEED: current_weather.wind().get("speed"),
|
||||||
ATTR_API_WIND_BEARING: weather_response.get_wind().get("deg"),
|
ATTR_API_CLOUDS: current_weather.clouds,
|
||||||
ATTR_API_WIND_SPEED: weather_response.get_wind().get("speed"),
|
ATTR_API_RAIN: self._get_rain(current_weather.rain),
|
||||||
ATTR_API_CLOUDS: weather_response.get_clouds(),
|
ATTR_API_SNOW: self._get_snow(current_weather.snow),
|
||||||
ATTR_API_RAIN: self._get_rain(weather_response.get_rain()),
|
ATTR_API_WEATHER: current_weather.detailed_status,
|
||||||
ATTR_API_SNOW: self._get_snow(weather_response.get_snow()),
|
ATTR_API_CONDITION: self._get_condition(current_weather.weather_code),
|
||||||
ATTR_API_WEATHER: weather_response.get_detailed_status(),
|
ATTR_API_WEATHER_CODE: current_weather.weather_code,
|
||||||
ATTR_API_CONDITION: self._get_condition(
|
ATTR_API_FORECAST: forecast_weather,
|
||||||
weather_response.get_weather_code()
|
|
||||||
),
|
|
||||||
ATTR_API_WEATHER_CODE: weather_response.get_weather_code(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _get_forecast_from_weather_response(self, 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 _convert_forecast(self, entry):
|
||||||
|
forecast = {
|
||||||
|
ATTR_FORECAST_TIME: entry.reference_time("unix") * 1000,
|
||||||
|
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(
|
||||||
|
entry.rain, entry.snow
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_WIND_SPEED: entry.wind().get("speed"),
|
||||||
|
ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"),
|
||||||
|
ATTR_FORECAST_CONDITION: self._get_condition(entry.weather_code),
|
||||||
|
}
|
||||||
|
|
||||||
|
temperature_dict = entry.temperature("celsius")
|
||||||
|
if "max" in temperature_dict and "min" in temperature_dict:
|
||||||
|
forecast[ATTR_FORECAST_TEMP] = entry.temperature("celsius").get("max")
|
||||||
|
forecast[ATTR_FORECAST_TEMP_LOW] = entry.temperature("celsius").get("min")
|
||||||
|
else:
|
||||||
|
forecast[ATTR_FORECAST_TEMP] = entry.temperature("celsius").get("temp")
|
||||||
|
|
||||||
|
return forecast
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_rain(rain):
|
def _get_rain(rain):
|
||||||
|
"""Get rain data from weather data."""
|
||||||
|
if "all" in rain:
|
||||||
|
return round(rain["all"], 0)
|
||||||
if "1h" in rain:
|
if "1h" in rain:
|
||||||
return round(rain["1h"], 0)
|
return round(rain["1h"], 0)
|
||||||
return "not raining"
|
return "not raining"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_snow(snow):
|
def _get_snow(snow):
|
||||||
|
"""Get snow data from weather data."""
|
||||||
if snow:
|
if snow:
|
||||||
return round(snow, 0)
|
if "all" in snow:
|
||||||
|
return round(snow["all"], 0)
|
||||||
|
if "1h" in snow:
|
||||||
|
return round(snow["1h"], 0)
|
||||||
|
return "not snowing"
|
||||||
return "not snowing"
|
return "not snowing"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calc_precipitation(rain, snow):
|
||||||
|
"""Calculate the precipitation."""
|
||||||
|
rain_value = 0
|
||||||
|
if WeatherUpdateCoordinator._get_rain(rain) != "not raining":
|
||||||
|
rain_value = WeatherUpdateCoordinator._get_rain(rain)
|
||||||
|
|
||||||
|
snow_value = 0
|
||||||
|
if WeatherUpdateCoordinator._get_snow(snow) != "not snowing":
|
||||||
|
snow_value = WeatherUpdateCoordinator._get_snow(snow)
|
||||||
|
|
||||||
|
if round(rain_value + snow_value, 1) == 0:
|
||||||
|
return None
|
||||||
|
return round(rain_value + snow_value, 1)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_condition(weather_code):
|
def _get_condition(weather_code):
|
||||||
|
"""Get weather condition from weather data."""
|
||||||
return [k for k, v in CONDITION_CLASSES.items() if weather_code in v][0]
|
return [k for k, v in CONDITION_CLASSES.items() if weather_code in v][0]
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
@ -1589,7 +1589,7 @@ pyotgw==0.6b1
|
||||||
pyotp==2.3.0
|
pyotp==2.3.0
|
||||||
|
|
||||||
# homeassistant.components.openweathermap
|
# homeassistant.components.openweathermap
|
||||||
pyowm==2.10.0
|
pyowm==3.1.0
|
||||||
|
|
||||||
# homeassistant.components.onewire
|
# homeassistant.components.onewire
|
||||||
pyownet==0.10.0.post1
|
pyownet==0.10.0.post1
|
||||||
|
|
|
@ -790,7 +790,7 @@ pyotgw==0.6b1
|
||||||
pyotp==2.3.0
|
pyotp==2.3.0
|
||||||
|
|
||||||
# homeassistant.components.openweathermap
|
# homeassistant.components.openweathermap
|
||||||
pyowm==2.10.0
|
pyowm==3.1.0
|
||||||
|
|
||||||
# homeassistant.components.onewire
|
# homeassistant.components.onewire
|
||||||
pyownet==0.10.0.post1
|
pyownet==0.10.0.post1
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
"""Define tests for the OpenWeatherMap config flow."""
|
"""Define tests for the OpenWeatherMap config flow."""
|
||||||
from pyowm.exceptions.api_call_error import APICallError
|
from pyowm.commons.exceptions import APIRequestError, UnauthorizedError
|
||||||
from pyowm.exceptions.api_response_error import UnauthorizedError
|
|
||||||
|
|
||||||
from homeassistant import data_entry_flow
|
from homeassistant import data_entry_flow
|
||||||
from homeassistant.components.openweathermap.const import (
|
from homeassistant.components.openweathermap.const import (
|
||||||
|
@ -9,7 +8,7 @@ from homeassistant.components.openweathermap.const import (
|
||||||
DEFAULT_LANGUAGE,
|
DEFAULT_LANGUAGE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
|
@ -38,7 +37,7 @@ async def test_form(hass):
|
||||||
mocked_owm = _create_mocked_owm(True)
|
mocked_owm = _create_mocked_owm(True)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"pyowm.weatherapi25.owm25.OWM25",
|
"pyowm.weatherapi25.weather_manager.WeatherManager",
|
||||||
return_value=mocked_owm,
|
return_value=mocked_owm,
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
@ -70,38 +69,12 @@ async def test_form(hass):
|
||||||
assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY]
|
assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY]
|
||||||
|
|
||||||
|
|
||||||
async def test_form_import(hass):
|
|
||||||
"""Test we can import yaml config."""
|
|
||||||
mocked_owm = _create_mocked_owm(True)
|
|
||||||
|
|
||||||
with patch("pyowm.weatherapi25.owm25.OWM25", return_value=mocked_owm), patch(
|
|
||||||
"homeassistant.components.openweathermap.async_setup", return_value=True
|
|
||||||
) as mock_setup, patch(
|
|
||||||
"homeassistant.components.openweathermap.async_setup_entry",
|
|
||||||
return_value=True,
|
|
||||||
) as mock_setup_entry:
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_IMPORT},
|
|
||||||
data=VALID_YAML_CONFIG.copy(),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
|
||||||
assert result["data"][CONF_LATITUDE] == hass.config.latitude
|
|
||||||
assert result["data"][CONF_LONGITUDE] == hass.config.longitude
|
|
||||||
assert result["data"][CONF_API_KEY] == VALID_YAML_CONFIG[CONF_API_KEY]
|
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert len(mock_setup.mock_calls) == 1
|
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
|
||||||
|
|
||||||
|
|
||||||
async def test_form_options(hass):
|
async def test_form_options(hass):
|
||||||
"""Test that the options form."""
|
"""Test that the options form."""
|
||||||
mocked_owm = _create_mocked_owm(True)
|
mocked_owm = _create_mocked_owm(True)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"pyowm.weatherapi25.owm25.OWM25",
|
"pyowm.weatherapi25.weather_manager.WeatherManager",
|
||||||
return_value=mocked_owm,
|
return_value=mocked_owm,
|
||||||
):
|
):
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
|
@ -139,12 +112,12 @@ async def test_form_options(hass):
|
||||||
assert result["step_id"] == "init"
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
result = await hass.config_entries.options.async_configure(
|
result = await hass.config_entries.options.async_configure(
|
||||||
result["flow_id"], user_input={CONF_MODE: "freedaily"}
|
result["flow_id"], user_input={CONF_MODE: "onecall_daily"}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert config_entry.options == {
|
assert config_entry.options == {
|
||||||
CONF_MODE: "freedaily",
|
CONF_MODE: "onecall_daily",
|
||||||
CONF_LANGUAGE: DEFAULT_LANGUAGE,
|
CONF_LANGUAGE: DEFAULT_LANGUAGE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +131,7 @@ async def test_form_invalid_api_key(hass):
|
||||||
mocked_owm = _create_mocked_owm(True)
|
mocked_owm = _create_mocked_owm(True)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"pyowm.weatherapi25.owm25.OWM25",
|
"pyowm.weatherapi25.weather_manager.WeatherManager",
|
||||||
return_value=mocked_owm,
|
return_value=mocked_owm,
|
||||||
side_effect=UnauthorizedError(""),
|
side_effect=UnauthorizedError(""),
|
||||||
):
|
):
|
||||||
|
@ -174,9 +147,9 @@ async def test_form_api_call_error(hass):
|
||||||
mocked_owm = _create_mocked_owm(True)
|
mocked_owm = _create_mocked_owm(True)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"pyowm.weatherapi25.owm25.OWM25",
|
"pyowm.weatherapi25.weather_manager.WeatherManager",
|
||||||
return_value=mocked_owm,
|
return_value=mocked_owm,
|
||||||
side_effect=APICallError(""),
|
side_effect=APIRequestError(""),
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
||||||
|
@ -202,31 +175,37 @@ async def test_form_api_offline(hass):
|
||||||
|
|
||||||
def _create_mocked_owm(is_api_online: bool):
|
def _create_mocked_owm(is_api_online: bool):
|
||||||
mocked_owm = MagicMock()
|
mocked_owm = MagicMock()
|
||||||
mocked_owm.is_API_online.return_value = is_api_online
|
|
||||||
|
|
||||||
weather = MagicMock()
|
weather = MagicMock()
|
||||||
weather.get_temperature.return_value.get.return_value = 10
|
weather.temperature.return_value.get.return_value = 10
|
||||||
weather.get_pressure.return_value.get.return_value = 10
|
weather.pressure.get.return_value = 10
|
||||||
weather.get_humidity.return_value = 10
|
weather.humidity.return_value = 10
|
||||||
weather.get_wind.return_value.get.return_value = 0
|
weather.wind.return_value.get.return_value = 0
|
||||||
weather.get_clouds.return_value = "clouds"
|
weather.clouds.return_value = "clouds"
|
||||||
weather.get_rain.return_value = []
|
weather.rain.return_value = []
|
||||||
weather.get_snow.return_value = 3
|
weather.snow.return_value = []
|
||||||
weather.get_detailed_status.return_value = "status"
|
weather.detailed_status.return_value = "status"
|
||||||
weather.get_weather_code.return_value = 803
|
weather.weather_code = 803
|
||||||
|
|
||||||
mocked_owm.weather_at_coords.return_value.get_weather.return_value = weather
|
mocked_owm.weather_at_coords.return_value.weather = weather
|
||||||
|
|
||||||
one_day_forecast = MagicMock()
|
one_day_forecast = MagicMock()
|
||||||
one_day_forecast.get_reference_time.return_value = 10
|
one_day_forecast.reference_time.return_value = 10
|
||||||
one_day_forecast.get_temperature.return_value.get.return_value = 10
|
one_day_forecast.temperature.return_value.get.return_value = 10
|
||||||
one_day_forecast.get_rain.return_value.get.return_value = 0
|
one_day_forecast.rain.return_value.get.return_value = 0
|
||||||
one_day_forecast.get_snow.return_value.get.return_value = 0
|
one_day_forecast.snow.return_value.get.return_value = 0
|
||||||
one_day_forecast.get_wind.return_value.get.return_value = 0
|
one_day_forecast.wind.return_value.get.return_value = 0
|
||||||
one_day_forecast.get_weather_code.return_value = 803
|
one_day_forecast.weather_code = 803
|
||||||
|
|
||||||
mocked_owm.three_hours_forecast_at_coords.return_value.get_forecast.return_value.get_weathers.return_value = [
|
mocked_owm.forecast_at_coords.return_value.forecast.weathers = [one_day_forecast]
|
||||||
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.one_call.return_value = is_api_online
|
||||||
|
|
||||||
return mocked_owm
|
return mocked_owm
|
||||||
|
|
Loading…
Reference in New Issue