Update library for smhi (#136375)

* Update library for smhi

* Imports

* Fixes
This commit is contained in:
G Johansson
2025-02-06 19:45:53 +01:00
committed by GitHub
parent 603a1ed69c
commit 167fb37929
8 changed files with 138 additions and 119 deletions

View File

@ -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

View File

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

View File

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

6
requirements_all.txt generated
View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -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)