Bump meteo-lt-pkg to 0.7.3 (#174391)

Co-authored-by: Erwin Douna <e.douna@gmail.com>
This commit is contained in:
Ramūnas Bunokas
2026-07-26 22:32:22 +03:00
committed by GitHub
parent fa3125fcac
commit 377054bb0b
9 changed files with 280 additions and 28 deletions
@@ -8,6 +8,7 @@ from meteo_lt import MeteoLtAPI, Place
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_PLACE_CODE, DOMAIN
@@ -19,7 +20,6 @@ class MeteoLtConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
self._api = MeteoLtAPI()
self._places: list[Place] = []
self._selected_place: Place | None = None
@@ -49,9 +49,10 @@ class MeteoLtConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "invalid_location"
if not self._places:
api = MeteoLtAPI(session=async_get_clientsession(self.hass))
try:
await self._api.fetch_places()
self._places = self._api.places
await api.fetch_places()
self._places = api.places
except (aiohttp.ClientError, TimeoutError) as err:
_LOGGER.error("Error fetching places: %s", err)
return self.async_abort(reason="cannot_connect")
@@ -8,6 +8,7 @@ from meteo_lt import Forecast as MeteoLtForecast, MeteoLtAPI
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN
@@ -27,7 +28,7 @@ class MeteoLtUpdateCoordinator(DataUpdateCoordinator[MeteoLtForecast]):
config_entry: MeteoLtConfigEntry,
) -> None:
"""Initialize the coordinator."""
self.client = MeteoLtAPI()
self.client = MeteoLtAPI(session=async_get_clientsession(hass))
self.place_code = place_code
super().__init__(
@@ -42,7 +43,9 @@ class MeteoLtUpdateCoordinator(DataUpdateCoordinator[MeteoLtForecast]):
async def _async_update_data(self) -> MeteoLtForecast:
"""Fetch data from Meteo.lt API."""
try:
forecast = await self.client.get_forecast(self.place_code)
forecast = await self.client.get_forecast(
self.place_code, include_warnings=False
)
except aiohttp.ClientResponseError as err:
raise UpdateFailed(
f"API returned error status {err.status}: {err.message}"
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["meteo-lt-pkg==0.2.4"]
"requirements": ["meteo-lt-pkg==0.7.3"]
}
+57 -3
View File
@@ -5,6 +5,19 @@ from datetime import datetime
from typing import Any, override
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_EXCEPTIONAL,
ATTR_CONDITION_FOG,
ATTR_CONDITION_HAIL,
ATTR_CONDITION_LIGHTNING,
ATTR_CONDITION_LIGHTNING_RAINY,
ATTR_CONDITION_PARTLYCLOUDY,
ATTR_CONDITION_POURING,
ATTR_CONDITION_RAINY,
ATTR_CONDITION_SNOWY,
ATTR_CONDITION_SNOWY_RAINY,
ATTR_CONDITION_SUNNY,
Forecast,
WeatherEntity,
WeatherEntityFeature,
@@ -16,13 +29,36 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import sun
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .const import ATTRIBUTION, DOMAIN, MANUFACTURER, MODEL
from .coordinator import MeteoLtConfigEntry, MeteoLtUpdateCoordinator
_CONDITION_MAP: dict[str, str] = {
"partly-cloudy": ATTR_CONDITION_PARTLYCLOUDY,
"cloudy-with-sunny-intervals": ATTR_CONDITION_PARTLYCLOUDY,
"cloudy": ATTR_CONDITION_CLOUDY,
"thunder": ATTR_CONDITION_LIGHTNING,
"isolated-thunderstorms": ATTR_CONDITION_LIGHTNING_RAINY,
"thunderstorms": ATTR_CONDITION_LIGHTNING_RAINY,
"heavy-rain-with-thunderstorms": ATTR_CONDITION_LIGHTNING_RAINY,
"light-rain": ATTR_CONDITION_RAINY,
"rain": ATTR_CONDITION_RAINY,
"heavy-rain": ATTR_CONDITION_POURING,
"light-sleet": ATTR_CONDITION_SNOWY_RAINY,
"sleet": ATTR_CONDITION_SNOWY_RAINY,
"freezing-rain": ATTR_CONDITION_SNOWY_RAINY,
"hail": ATTR_CONDITION_HAIL,
"light-snow": ATTR_CONDITION_SNOWY,
"snow": ATTR_CONDITION_SNOWY,
"heavy-snow": ATTR_CONDITION_SNOWY,
"fog": ATTR_CONDITION_FOG,
}
async def async_setup_entry(
hass: HomeAssistant,
@@ -63,6 +99,19 @@ class MeteoLtWeatherEntity(CoordinatorEntity[MeteoLtUpdateCoordinator], WeatherE
model=MODEL,
)
def _map_condition(self, condition_code: str | None, datetime_str: str) -> str:
"""Map a meteo.lt condition code to a Home Assistant condition string."""
if condition_code is None:
return ATTR_CONDITION_EXCEPTIONAL
if condition_code == "clear":
dt = dt_util.parse_datetime(datetime_str)
return (
ATTR_CONDITION_SUNNY
if sun.is_up(self.hass, dt)
else ATTR_CONDITION_CLEAR_NIGHT
)
return _CONDITION_MAP.get(condition_code, ATTR_CONDITION_EXCEPTIONAL)
@property
@override
def native_temperature(self) -> float | None:
@@ -115,7 +164,8 @@ class MeteoLtWeatherEntity(CoordinatorEntity[MeteoLtUpdateCoordinator], WeatherE
@override
def condition(self) -> str | None:
"""Return the current condition."""
return self.coordinator.data.current_conditions.condition
cc = self.coordinator.data.current_conditions
return self._map_condition(cc.condition_code, cc.datetime)
def _convert_forecast_data(
self, forecast_data: Any, include_templow: bool = False
@@ -126,7 +176,9 @@ class MeteoLtWeatherEntity(CoordinatorEntity[MeteoLtUpdateCoordinator], WeatherE
native_temperature=forecast_data.temperature,
native_templow=forecast_data.temperature_low if include_templow else None,
native_apparent_temperature=forecast_data.apparent_temperature,
condition=forecast_data.condition,
condition=self._map_condition(
forecast_data.condition_code, forecast_data.datetime
),
native_precipitation=forecast_data.precipitation,
precipitation_probability=None, # Not provided by API
native_wind_speed=forecast_data.wind_speed,
@@ -169,7 +221,9 @@ class MeteoLtWeatherEntity(CoordinatorEntity[MeteoLtUpdateCoordinator], WeatherE
native_temperature=max_temp,
native_templow=min_temp,
native_apparent_temperature=midday_forecast.apparent_temperature,
condition=midday_forecast.condition,
condition=self._map_condition(
midday_forecast.condition_code, midday_forecast.datetime
),
# Calculate precipitation: sum if any values, else None
native_precipitation=(
sum(
+1 -1
View File
@@ -1571,7 +1571,7 @@ melnor-bluetooth==0.0.25
messagebird==1.2.1
# homeassistant.components.meteo_lt
meteo-lt-pkg==0.2.4
meteo-lt-pkg==0.7.3
# homeassistant.components.meteoalarm
meteoalertapi==0.3.1
+3 -3
View File
@@ -38,9 +38,9 @@ def mock_meteo_lt_api() -> Generator[AsyncMock]:
mock_api.places = mock_places
mock_api.fetch_places.return_value = None
mock_forecast = Forecast.from_dict(forecast_data)
mock_api.get_forecast.return_value = mock_forecast
mock_api.get_forecast.side_effect = lambda *args, **kwargs: Forecast.from_dict(
forecast_data
)
# Mock get_nearest_place to return Vilnius
mock_api.get_nearest_place.return_value = mock_places[0]
@@ -73,7 +73,7 @@
"seaLevelPressure": 1029,
"relativeHumidity": 55,
"totalPrecipitation": 0.3,
"conditionCode": "rainy"
"conditionCode": "rain"
},
{
"forecastTimeUtc": "2025-09-28 10:00:00",
@@ -86,7 +86,7 @@
"seaLevelPressure": 1028,
"relativeHumidity": 50,
"totalPrecipitation": 0.4,
"conditionCode": "rainy"
"conditionCode": "rain"
},
{
"forecastTimeUtc": "2025-09-29 10:00:00",
@@ -99,7 +99,7 @@
"seaLevelPressure": 1027,
"relativeHumidity": 45,
"totalPrecipitation": 0.5,
"conditionCode": "rainy"
"conditionCode": "rain"
},
{
"forecastTimeUtc": "2025-09-30 10:00:00",
@@ -112,7 +112,7 @@
"seaLevelPressure": 1026,
"relativeHumidity": 40,
"totalPrecipitation": 0.6,
"conditionCode": "rainy"
"conditionCode": "rain"
},
{
"forecastTimeUtc": "2025-10-01 10:00:00",
@@ -125,7 +125,7 @@
"seaLevelPressure": 1025,
"relativeHumidity": 35,
"totalPrecipitation": 0.7,
"conditionCode": "rainy"
"conditionCode": "rain"
}
]
}
@@ -61,6 +61,6 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'sunny',
'state': 'clear-night',
})
# ---
+203 -9
View File
@@ -1,16 +1,39 @@
"""Test Meteo.lt weather entity."""
from collections.abc import Generator
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from meteo_lt import Forecast
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.meteo_lt.const import DOMAIN
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_EXCEPTIONAL,
ATTR_CONDITION_FOG,
ATTR_CONDITION_HAIL,
ATTR_CONDITION_LIGHTNING,
ATTR_CONDITION_LIGHTNING_RAINY,
ATTR_CONDITION_PARTLYCLOUDY,
ATTR_CONDITION_POURING,
ATTR_CONDITION_RAINY,
ATTR_CONDITION_SNOWY,
ATTR_CONDITION_SNOWY_RAINY,
ATTR_CONDITION_SUNNY,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, snapshot_platform
from tests.common import (
MockConfigEntry,
async_load_json_object_fixture,
snapshot_platform,
)
@pytest.fixture(autouse=True)
@@ -36,12 +59,158 @@ async def test_weather_entity(
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.freeze_time("2025-09-25 9:00:00")
async def test_forecast_no_limits(
@pytest.mark.freeze_time("2025-09-25 10:00:00")
async def test_coordinator_requests_forecast_without_warnings(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_meteo_lt_api: AsyncMock,
) -> None:
"""Test that the coordinator fetches forecasts with warnings disabled."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_meteo_lt_api.get_forecast.assert_called_with("vilnius", include_warnings=False)
@pytest.mark.parametrize(
"expected_condition",
[
pytest.param(
ATTR_CONDITION_SUNNY,
marks=pytest.mark.freeze_time("2025-09-25 10:00:00"), # 13:00 in Vilnius
id="day",
),
pytest.param(
ATTR_CONDITION_CLEAR_NIGHT,
marks=pytest.mark.freeze_time("2025-09-25 22:00:00"), # 01:00 in Vilnius
id="night",
),
],
)
async def test_condition_clear_maps_day_night(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_meteo_lt_api: AsyncMock,
expected_condition: str,
) -> None:
"""Test that a clear condition maps to sunny or clear-night by sun position at the forecast time."""
hass.config.latitude = 54.68705 # Vilnius, matching the forecast place
hass.config.longitude = 25.28291
forecast_data = await async_load_json_object_fixture(hass, "forecast.json", DOMAIN)
current_hour = dt_util.utcnow().strftime("%Y-%m-%d %H:%M:%S")
forecast_data["forecastTimestamps"][0]["forecastTimeUtc"] = current_hour
forecast_data["forecastTimestamps"][0]["conditionCode"] = "clear"
mock_meteo_lt_api.get_forecast.side_effect = lambda *args, **kwargs: (
Forecast.from_dict(forecast_data)
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("weather.vilnius")
assert state is not None
assert state.state == expected_condition
@pytest.mark.freeze_time("2025-09-25 10:00:00")
@pytest.mark.parametrize(
("condition_code", "expected_condition"),
[
pytest.param("partly-cloudy", ATTR_CONDITION_PARTLYCLOUDY, id="partly-cloudy"),
pytest.param(
"cloudy-with-sunny-intervals",
ATTR_CONDITION_PARTLYCLOUDY,
id="cloudy-with-sunny-intervals",
),
pytest.param("cloudy", ATTR_CONDITION_CLOUDY, id="cloudy"),
pytest.param("thunder", ATTR_CONDITION_LIGHTNING, id="thunder"),
pytest.param(
"isolated-thunderstorms",
ATTR_CONDITION_LIGHTNING_RAINY,
id="isolated-thunderstorms",
),
pytest.param(
"thunderstorms", ATTR_CONDITION_LIGHTNING_RAINY, id="thunderstorms"
),
pytest.param(
"heavy-rain-with-thunderstorms",
ATTR_CONDITION_LIGHTNING_RAINY,
id="heavy-rain-with-thunderstorms",
),
pytest.param("light-rain", ATTR_CONDITION_RAINY, id="light-rain"),
pytest.param("rain", ATTR_CONDITION_RAINY, id="rain"),
pytest.param("heavy-rain", ATTR_CONDITION_POURING, id="heavy-rain"),
pytest.param("light-sleet", ATTR_CONDITION_SNOWY_RAINY, id="light-sleet"),
pytest.param("sleet", ATTR_CONDITION_SNOWY_RAINY, id="sleet"),
pytest.param("freezing-rain", ATTR_CONDITION_SNOWY_RAINY, id="freezing-rain"),
pytest.param("hail", ATTR_CONDITION_HAIL, id="hail"),
pytest.param("light-snow", ATTR_CONDITION_SNOWY, id="light-snow"),
pytest.param("snow", ATTR_CONDITION_SNOWY, id="snow"),
pytest.param("heavy-snow", ATTR_CONDITION_SNOWY, id="heavy-snow"),
pytest.param("fog", ATTR_CONDITION_FOG, id="fog"),
],
)
async def test_condition_code_mapping(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_meteo_lt_api: AsyncMock,
condition_code: str,
expected_condition: str,
) -> None:
"""Test that each meteo.lt condition code maps to the expected HA condition."""
forecast_data = await async_load_json_object_fixture(hass, "forecast.json", DOMAIN)
forecast_data["forecastTimestamps"][0]["conditionCode"] = condition_code
mock_meteo_lt_api.get_forecast.side_effect = lambda *args, **kwargs: (
Forecast.from_dict(forecast_data)
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("weather.vilnius")
assert state is not None
assert state.state == expected_condition
@pytest.mark.freeze_time("2025-09-25 10:00:00")
@pytest.mark.parametrize(
"condition_code",
[None, "not-a-real-condition"],
ids=["missing", "unknown"],
)
async def test_condition_unknown_maps_to_exceptional(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_meteo_lt_api: AsyncMock,
condition_code: str | None,
) -> None:
"""Test that a missing or unmapped condition code maps to exceptional."""
forecast_data = await async_load_json_object_fixture(hass, "forecast.json", DOMAIN)
forecast_data["forecastTimestamps"][0]["conditionCode"] = condition_code
mock_meteo_lt_api.get_forecast.side_effect = lambda *args, **kwargs: (
Forecast.from_dict(forecast_data)
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("weather.vilnius")
assert state is not None
assert state.state == ATTR_CONDITION_EXCEPTIONAL
@pytest.mark.freeze_time("2025-09-25 10:00:00")
async def test_forecast_hourly(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that forecast returns all available data from API without limits."""
"""Test hourly forecast returns all entries with correct condition mapping."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
@@ -53,8 +222,23 @@ async def test_forecast_no_limits(
blocking=True,
return_response=True,
)
hourly_forecasts = result["weather.vilnius"]["forecast"]
assert len(hourly_forecasts) == 9
forecasts = result["weather.vilnius"]["forecast"]
assert len(forecasts) == 8
assert forecasts[0]["condition"] == ATTR_CONDITION_PARTLYCLOUDY # 11:00
assert forecasts[1]["condition"] == ATTR_CONDITION_CLOUDY # 12:00
assert forecasts[3]["condition"] == ATTR_CONDITION_RAINY # 2025-09-27
@pytest.mark.freeze_time("2025-09-25 10:00:00")
async def test_forecast_daily(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test daily forecast aggregates hourly entries into per-day summaries."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.services.async_call(
"weather",
@@ -63,5 +247,15 @@ async def test_forecast_no_limits(
blocking=True,
return_response=True,
)
daily_forecasts = result["weather.vilnius"]["forecast"]
assert len(daily_forecasts) == 7
forecasts = result["weather.vilnius"]["forecast"]
assert len(forecasts) == 7
first_day = forecasts[0]
assert first_day["temperature"] == 13.5 # max(12.2, 13.5)
assert first_day["templow"] == 12.2 # min(12.2, 13.5)
assert first_day["precipitation"] == pytest.approx(0.1) # sum: 0 + 0.1
assert first_day["condition"] == ATTR_CONDITION_CLOUDY # midday 12:00 → "cloudy"
rainy_day = forecasts[2]
assert rainy_day["condition"] == ATTR_CONDITION_RAINY # 2025-09-27 → "rain"