From 167fb37929122507d60bc128bd171de610d6889a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 6 Feb 2025 19:45:53 +0100 Subject: [PATCH] Update library for smhi (#136375) * Update library for smhi * Imports * Fixes --- homeassistant/components/smhi/config_flow.py | 6 +- homeassistant/components/smhi/manifest.json | 4 +- homeassistant/components/smhi/weather.py | 64 ++++---- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/smhi/test_config_flow.py | 16 +- tests/components/smhi/test_init.py | 10 +- tests/components/smhi/test_weather.py | 145 +++++++++++-------- 8 files changed, 138 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 2521df3a333..387edfc6e11 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from smhi.smhi_lib import Smhi, SmhiForecastException +from pysmhi import SmhiForecastException, SMHIPointForecast import voluptuous as vol from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN @@ -26,9 +26,9 @@ async def async_check_location( ) -> bool: """Return true if location is ok.""" session = aiohttp_client.async_get_clientsession(hass) - smhi_api = Smhi(longitude, latitude, session=session) + smhi_api = SMHIPointForecast(str(longitude), str(latitude), session=session) try: - await smhi_api.async_get_forecast() + await smhi_api.async_get_daily_forecast() except SmhiForecastException: return False diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index 645ace41cab..fc3af634764 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smhi", "iot_class": "cloud_polling", - "loggers": ["smhi"], - "requirements": ["smhi-pkg==1.0.19"] + "loggers": ["pysmhi"], + "requirements": ["pysmhi==1.0.0"] } diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index d43ca4465ae..1707afa2fca 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -9,8 +9,7 @@ import logging from typing import Any, Final import aiohttp -from smhi import Smhi -from smhi.smhi_lib import SmhiForecast, SmhiForecastException +from pysmhi import SMHIForecast, SmhiForecastException, SMHIPointForecast from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -59,7 +58,7 @@ from homeassistant.helpers import aiohttp_client, sun from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later -from homeassistant.util import Throttle, dt as dt_util +from homeassistant.util import Throttle from .const import ATTR_SMHI_THUNDER_PROBABILITY, DOMAIN, ENTITY_ID_SENSOR_FORMAT @@ -139,10 +138,10 @@ class SmhiWeather(WeatherEntity): ) -> None: """Initialize the SMHI weather entity.""" self._attr_unique_id = f"{latitude}, {longitude}" - self._forecast_daily: list[SmhiForecast] | None = None - self._forecast_hourly: list[SmhiForecast] | None = None + self._forecast_daily: list[SMHIForecast] | None = None + self._forecast_hourly: list[SMHIForecast] | None = None self._fail_count = 0 - self._smhi_api = Smhi(longitude, latitude, session=session) + self._smhi_api = SMHIPointForecast(longitude, latitude, session=session) self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{latitude}, {longitude}")}, @@ -156,7 +155,7 @@ class SmhiWeather(WeatherEntity): """Return additional attributes.""" if self._forecast_daily: return { - ATTR_SMHI_THUNDER_PROBABILITY: self._forecast_daily[0].thunder, + ATTR_SMHI_THUNDER_PROBABILITY: self._forecast_daily[0]["thunder"], } return None @@ -165,8 +164,8 @@ class SmhiWeather(WeatherEntity): """Refresh the forecast data from SMHI weather API.""" try: async with asyncio.timeout(TIMEOUT): - self._forecast_daily = await self._smhi_api.async_get_forecast() - self._forecast_hourly = await self._smhi_api.async_get_forecast_hour() + self._forecast_daily = await self._smhi_api.async_get_daily_forecast() + self._forecast_hourly = await self._smhi_api.async_get_hourly_forecast() self._fail_count = 0 except (TimeoutError, SmhiForecastException): _LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes") @@ -176,15 +175,15 @@ class SmhiWeather(WeatherEntity): return if self._forecast_daily: - self._attr_native_temperature = self._forecast_daily[0].temperature - self._attr_humidity = self._forecast_daily[0].humidity - self._attr_native_wind_speed = self._forecast_daily[0].wind_speed - self._attr_wind_bearing = self._forecast_daily[0].wind_direction - self._attr_native_visibility = self._forecast_daily[0].horizontal_visibility - self._attr_native_pressure = self._forecast_daily[0].pressure - self._attr_native_wind_gust_speed = self._forecast_daily[0].wind_gust - self._attr_cloud_coverage = self._forecast_daily[0].cloudiness - self._attr_condition = CONDITION_MAP.get(self._forecast_daily[0].symbol) + self._attr_native_temperature = self._forecast_daily[0]["temperature"] + self._attr_humidity = self._forecast_daily[0]["humidity"] + self._attr_native_wind_speed = self._forecast_daily[0]["wind_speed"] + self._attr_wind_bearing = self._forecast_daily[0]["wind_direction"] + self._attr_native_visibility = self._forecast_daily[0]["visibility"] + self._attr_native_pressure = self._forecast_daily[0]["pressure"] + self._attr_native_wind_gust_speed = self._forecast_daily[0]["wind_gust"] + self._attr_cloud_coverage = self._forecast_daily[0]["total_cloud"] + self._attr_condition = CONDITION_MAP.get(self._forecast_daily[0]["symbol"]) if self._attr_condition == ATTR_CONDITION_SUNNY and not sun.is_up( self.hass ): @@ -196,7 +195,7 @@ class SmhiWeather(WeatherEntity): await self.async_update(no_throttle=True) def _get_forecast_data( - self, forecast_data: list[SmhiForecast] | None + self, forecast_data: list[SMHIForecast] | None ) -> list[Forecast] | None: """Get forecast data.""" if forecast_data is None or len(forecast_data) < 3: @@ -205,25 +204,28 @@ class SmhiWeather(WeatherEntity): data: list[Forecast] = [] for forecast in forecast_data[1:]: - condition = CONDITION_MAP.get(forecast.symbol) + condition = CONDITION_MAP.get(forecast["symbol"]) if condition == ATTR_CONDITION_SUNNY and not sun.is_up( - self.hass, forecast.valid_time.replace(tzinfo=dt_util.UTC) + self.hass, forecast["valid_time"] ): condition = ATTR_CONDITION_CLEAR_NIGHT data.append( { - ATTR_FORECAST_TIME: forecast.valid_time.isoformat(), - ATTR_FORECAST_NATIVE_TEMP: forecast.temperature_max, - ATTR_FORECAST_NATIVE_TEMP_LOW: forecast.temperature_min, - ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.total_precipitation, + ATTR_FORECAST_TIME: forecast["valid_time"].isoformat(), + ATTR_FORECAST_NATIVE_TEMP: forecast["temperature_max"], + ATTR_FORECAST_NATIVE_TEMP_LOW: forecast["temperature_min"], + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.get( + "total_precipitation" + ) + or forecast["mean_precipitation"], ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_NATIVE_PRESSURE: forecast.pressure, - ATTR_FORECAST_WIND_BEARING: forecast.wind_direction, - ATTR_FORECAST_NATIVE_WIND_SPEED: forecast.wind_speed, - ATTR_FORECAST_HUMIDITY: forecast.humidity, - ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: forecast.wind_gust, - ATTR_FORECAST_CLOUD_COVERAGE: forecast.cloudiness, + ATTR_FORECAST_NATIVE_PRESSURE: forecast["pressure"], + ATTR_FORECAST_WIND_BEARING: forecast["wind_direction"], + ATTR_FORECAST_NATIVE_WIND_SPEED: forecast["wind_speed"], + ATTR_FORECAST_HUMIDITY: forecast["humidity"], + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: forecast["wind_gust"], + ATTR_FORECAST_CLOUD_COVERAGE: forecast["total_cloud"], } ) diff --git a/requirements_all.txt b/requirements_all.txt index e97d39e23bd..a279f001e62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2309,6 +2309,9 @@ pysmartthings==0.7.8 # homeassistant.components.smarty pysmarty2==0.10.1 +# homeassistant.components.smhi +pysmhi==1.0.0 + # homeassistant.components.edl21 pysml==0.0.12 @@ -2738,9 +2741,6 @@ slixmpp==1.8.5 # homeassistant.components.smart_meter_texas smart-meter-texas==0.5.5 -# homeassistant.components.smhi -smhi-pkg==1.0.19 - # homeassistant.components.snapcast snapcast==2.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e195c6436ab..8fde12b53d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1881,6 +1881,9 @@ pysmartthings==0.7.8 # homeassistant.components.smarty pysmarty2==0.10.1 +# homeassistant.components.smhi +pysmhi==1.0.0 + # homeassistant.components.edl21 pysml==0.0.12 @@ -2205,9 +2208,6 @@ slack_sdk==3.33.4 # homeassistant.components.smart_meter_texas smart-meter-texas==0.5.5 -# homeassistant.components.smhi -smhi-pkg==1.0.19 - # homeassistant.components.snapcast snapcast==2.3.6 diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index 362adebe416..524aad873f9 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import patch -from smhi.smhi_lib import SmhiForecastException +from pysmhi import SmhiForecastException from homeassistant import config_entries from homeassistant.components.smhi.const import DOMAIN @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( @@ -66,7 +66,7 @@ async def test_form(hass: HomeAssistant) -> None: ) with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( @@ -102,7 +102,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", side_effect=SmhiForecastException, ): result2 = await hass.config_entries.flow.async_configure( @@ -122,7 +122,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: # Continue flow with new coordinates with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( @@ -170,7 +170,7 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ): result2 = await hass.config_entries.flow.async_configure( @@ -218,7 +218,7 @@ async def test_reconfigure_flow( assert result["type"] is FlowResultType.FORM with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", side_effect=SmhiForecastException, ): result = await hass.config_entries.flow.async_configure( @@ -237,7 +237,7 @@ async def test_reconfigure_flow( with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index d00742d4900..f301e684e3e 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,6 +1,6 @@ """Test SMHI component setup process.""" -from smhi.smhi_lib import APIURL_TEMPLATE +from pysmhi.const import API_POINT_FORECAST from homeassistant.components.smhi.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -17,7 +17,7 @@ async def test_setup_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test setup entry.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -35,7 +35,7 @@ async def test_remove_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test remove entry.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -62,7 +62,7 @@ async def test_migrate_entry( api_response: str, ) -> None: """Test migrate entry data.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG_MIGRATE["longitude"], TEST_CONFIG_MIGRATE["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -97,7 +97,7 @@ async def test_migrate_from_future_version( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test migrate entry not possible from future version.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG_MIGRATE["longitude"], TEST_CONFIG_MIGRATE["latitude"] ) aioclient_mock.get(uri, text=api_response) diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index cc6902710bd..a39cb72d4b8 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -4,8 +4,9 @@ from datetime import datetime, timedelta from unittest.mock import patch from freezegun import freeze_time +from pysmhi import SMHIForecast, SmhiForecastException +from pysmhi.const import API_POINT_FORECAST import pytest -from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecast, SmhiForecastException from syrupy.assertion import SnapshotAssertion from homeassistant.components.smhi.const import ATTR_SMHI_THUNDER_PROBABILITY @@ -44,7 +45,7 @@ async def test_setup_hass( snapshot: SnapshotAssertion, ) -> None: """Test for successfully setting up the smhi integration.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -54,7 +55,7 @@ async def test_setup_hass( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 1 # Testing the actual entity state for # deeper testing than normal unity test @@ -75,7 +76,7 @@ async def test_clear_night( """Test for successfully setting up the smhi integration.""" hass.config.latitude = "59.32624" hass.config.longitude = "17.84197" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response_night) @@ -85,7 +86,7 @@ async def test_clear_night( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 1 state = hass.states.get(ENTITY_ID) @@ -109,7 +110,7 @@ async def test_properties_no_data(hass: HomeAssistant) -> None: entry.add_to_hass(hass) with patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_daily_forecast", side_effect=SmhiForecastException("boom"), ): await hass.config_entries.async_setup(entry.entry_id) @@ -134,61 +135,77 @@ async def test_properties_no_data(hass: HomeAssistant) -> None: async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: """Test behaviour when unknown symbol from API.""" - data = SmhiForecast( - temperature=5, - temperature_max=10, - temperature_min=0, - humidity=5, - pressure=1008, - thunder=0, - cloudiness=52, - precipitation=1, - wind_direction=180, - wind_speed=10, - horizontal_visibility=6, - wind_gust=1.5, - mean_precipitation=0.5, - total_precipitation=1, + data = SMHIForecast( + frozen_precipitation=0, + high_cloud=100, + humidity=96, + low_cloud=100, + max_precipitation=0.0, + mean_precipitation=0.0, + median_precipitation=0.0, + medium_cloud=75, + min_precipitation=0.0, + precipitation_category=0, + pressure=1018.9, symbol=100, # Faulty symbol - valid_time=datetime(2018, 1, 1, 0, 1, 2), + temperature=1.0, + temperature_max=1.0, + temperature_min=1.0, + thunder=0, + total_cloud=100, + valid_time=datetime(2018, 1, 1, 0, 0, 0), + visibility=8.8, + wind_direction=114, + wind_gust=5.8, + wind_speed=2.5, ) - - data2 = SmhiForecast( - temperature=5, - temperature_max=10, - temperature_min=0, - humidity=5, - pressure=1008, - thunder=0, - cloudiness=52, - precipitation=1, - wind_direction=180, - wind_speed=10, - horizontal_visibility=6, - wind_gust=1.5, - mean_precipitation=0.5, - total_precipitation=1, + data2 = SMHIForecast( + frozen_precipitation=0, + high_cloud=100, + humidity=96, + low_cloud=100, + max_precipitation=0.0, + mean_precipitation=0.0, + median_precipitation=0.0, + medium_cloud=75, + min_precipitation=0.0, + precipitation_category=0, + pressure=1018.9, symbol=100, # Faulty symbol - valid_time=datetime(2018, 1, 1, 12, 1, 2), + temperature=1.0, + temperature_max=1.0, + temperature_min=1.0, + thunder=0, + total_cloud=100, + valid_time=datetime(2018, 1, 1, 12, 0, 0), + visibility=8.8, + wind_direction=114, + wind_gust=5.8, + wind_speed=2.5, ) - - data3 = SmhiForecast( - temperature=5, - temperature_max=10, - temperature_min=0, - humidity=5, - pressure=1008, - thunder=0, - cloudiness=52, - precipitation=1, - wind_direction=180, - wind_speed=10, - horizontal_visibility=6, - wind_gust=1.5, - mean_precipitation=0.5, - total_precipitation=1, + data3 = SMHIForecast( + frozen_precipitation=0, + high_cloud=100, + humidity=96, + low_cloud=100, + max_precipitation=0.0, + mean_precipitation=0.0, + median_precipitation=0.0, + medium_cloud=75, + min_precipitation=0.0, + precipitation_category=0, + pressure=1018.9, symbol=100, # Faulty symbol - valid_time=datetime(2018, 1, 2, 12, 1, 2), + temperature=1.0, + temperature_max=1.0, + temperature_min=1.0, + thunder=0, + total_cloud=100, + valid_time=datetime(2018, 1, 2, 0, 0, 0), + visibility=8.8, + wind_direction=114, + wind_gust=5.8, + wind_speed=2.5, ) testdata = [data, data2, data3] @@ -198,11 +215,11 @@ async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: with ( patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_daily_forecast", return_value=testdata, ), patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast_hour", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_hourly_forecast", return_value=None, ), ): @@ -237,7 +254,7 @@ async def test_refresh_weather_forecast_retry( now = dt_util.utcnow() with patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_daily_forecast", side_effect=error, ) as mock_get_forecast: await hass.config_entries.async_setup(entry.entry_id) @@ -352,7 +369,7 @@ async def test_custom_speed_unit( api_response: str, ) -> None: """Test Wind Gust speed with custom unit.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -389,7 +406,7 @@ async def test_forecast_services( snapshot: SnapshotAssertion, ) -> None: """Test multiple forecast.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -440,7 +457,7 @@ async def test_forecast_services( assert msg["type"] == "event" forecast1 = msg["event"]["forecast"] - assert len(forecast1) == 72 + assert len(forecast1) == 52 assert forecast1[0] == snapshot assert forecast1[6] == snapshot @@ -453,7 +470,7 @@ async def test_forecast_services_lack_of_data( snapshot: SnapshotAssertion, ) -> None: """Test forecast lacking data.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response_lack_data) @@ -498,7 +515,7 @@ async def test_forecast_service( service: str, ) -> None: """Test forecast service.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response)