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