Add tests for weather radar data

This commit is contained in:
Jules 2023-12-28 21:09:09 +01:00
parent f392e8b004
commit aae39d8ddc
Signed by: jdejaegh
GPG key ID: 99D6D184CA66933A
11 changed files with 1512 additions and 170 deletions

View file

@ -1,5 +1,4 @@
"""Create a radar view for IRM KMI weather""" """Create a radar view for IRM KMI weather"""
# File inspired by https://github.com/jodur/imagesdirectory-camera/blob/main/custom_components/imagedirectory/camera.py
import logging import logging
@ -47,7 +46,7 @@ class IrmKmiRadar(CoordinatorEntity, Camera):
self._image_index = 0 self._image_index = 0
@property # Baseclass Camera property override @property
def frame_interval(self) -> float: def frame_interval(self) -> float:
"""Return the interval between frames of the mjpeg stream""" """Return the interval between frames of the mjpeg stream"""
return 0.3 return 0.3

View file

@ -102,7 +102,10 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
async def download_images_from_api(self, animation_data, country, localisation_layer_url): async def download_images_from_api(self, animation_data, country, localisation_layer_url):
coroutines = list() coroutines = list()
coroutines.append(self._api_client.get_image(f"{localisation_layer_url}&th={'d' if country == 'NL' else 'n'}")) coroutines.append(
self._api_client.get_image(localisation_layer_url,
params={'th': 'd' if country == 'NL' else 'n'}))
for frame in animation_data: for frame in animation_data:
if frame.get('uri', None) is not None: if frame.get('uri', None) is not None:
coroutines.append(self._api_client.get_image(frame.get('uri'))) coroutines.append(self._api_client.get_image(frame.get('uri')))
@ -184,6 +187,7 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
for current in hourly_forecast_data[:2]: for current in hourly_forecast_data[:2]:
if datetime.now().strftime('%H') == current['hour']: if datetime.now().strftime('%H') == current['hour']:
now_hourly = current now_hourly = current
break
# Get UV index # Get UV index
module_data = api_data.get('module', None) module_data = api_data.get('module', None)
uv_index = None uv_index = None
@ -192,13 +196,33 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
if module.get('type', None) == 'uv': if module.get('type', None) == 'uv':
uv_index = module.get('data', {}).get('levelValue') uv_index = module.get('data', {}).get('levelValue')
try:
pressure = float(now_hourly.get('pressure', None)) if now_hourly is not None else None
except TypeError:
pressure = None
try:
wind_speed = float(now_hourly.get('windSpeedKm', None)) if now_hourly is not None else None
except TypeError:
wind_speed = None
try:
wind_gust_speed = float(now_hourly.get('windPeakSpeedKm', None)) if now_hourly is not None else None
except TypeError:
wind_gust_speed = None
try:
temperature = float(api_data.get('obs', {}).get('temp'))
except TypeError:
temperature = None
current_weather = CurrentWeatherData( current_weather = CurrentWeatherData(
condition=CDT_MAP.get((api_data.get('obs', {}).get('ww'), api_data.get('obs', {}).get('dayNight')), None), condition=CDT_MAP.get((api_data.get('obs', {}).get('ww'), api_data.get('obs', {}).get('dayNight')), None),
temperature=api_data.get('obs', {}).get('temp'), temperature=temperature,
wind_speed=now_hourly.get('windSpeedKm', None) if now_hourly is not None else None, wind_speed=wind_speed,
wind_gust_speed=now_hourly.get('windPeakSpeedKm', None) if now_hourly is not None else None, wind_gust_speed=wind_gust_speed,
wind_bearing=now_hourly.get('windDirectionText', {}).get('en') if now_hourly is not None else None, wind_bearing=now_hourly.get('windDirectionText', {}).get('en') if now_hourly is not None else None,
pressure=now_hourly.get('pressure', None) if now_hourly is not None else None, pressure=pressure,
uv_index=uv_index uv_index=uv_index
) )

View file

@ -45,7 +45,6 @@ def mock_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMoc
fixture: str = "forecast.json" fixture: str = "forecast.json"
forecast = json.loads(load_fixture(fixture)) forecast = json.loads(load_fixture(fixture))
print(type(forecast))
with patch( with patch(
"custom_components.irm_kmi.coordinator.IrmKmiApiClient", autospec=True "custom_components.irm_kmi.coordinator.IrmKmiApiClient", autospec=True
) as irm_kmi_api_mock: ) as irm_kmi_api_mock:
@ -81,8 +80,35 @@ def mock_exception_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None
@pytest.fixture() @pytest.fixture()
def mock_coordinator(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: def mock_image_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
"""Return a mocked IrmKmi api client.""" """Return a mocked IrmKmi api client."""
async def patched(url: str, params: dict | None = None) -> bytes:
if "cdn.knmi.nl" in url:
file_name = "tests/fixtures/clouds_nl.png"
elif "app.meteo.be/services/appv4/?s=getIncaImage" in url:
file_name = "tests/fixtures/clouds_be.png"
elif "getLocalizationLayerBE" in url:
file_name = "tests/fixtures/loc_layer_be_n.png"
elif "getLocalizationLayerNL" in url:
file_name = "tests/fixtures/loc_layer_nl_d.png"
else:
raise ValueError("Not a valid parameter for the mock")
with open(file_name, "rb") as file:
return file.read()
with patch(
"custom_components.irm_kmi.coordinator.IrmKmiApiClient", autospec=True
) as irm_kmi_api_mock:
irm_kmi = irm_kmi_api_mock.return_value
irm_kmi.get_image.side_effect = patched
yield irm_kmi
@pytest.fixture()
def mock_coordinator(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
"""Return a mocked coordinator."""
with patch( with patch(
"custom_components.irm_kmi.IrmKmiCoordinator", autospec=True "custom_components.irm_kmi.IrmKmiCoordinator", autospec=True
) as coordinator_mock: ) as coordinator_mock:

BIN
tests/fixtures/clouds_be.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
tests/fixtures/clouds_nl.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -1359,7 +1359,7 @@
} }
], ],
"animation": { "animation": {
"localisationLayer": "https:\/\/app.meteo.be\/services\/appv4\/?s=getLocalizationLayer&ins=92094&f=2&k=2c886c51e74b671c8fc3865f4a0e9318", "localisationLayer": "https:\/\/app.meteo.be\/services\/appv4\/?s=getLocalizationLayerBE&ins=92094&f=2&k=2c886c51e74b671c8fc3865f4a0e9318",
"localisationLayerRatioX": 0.6667, "localisationLayerRatioX": 0.6667,
"localisationLayerRatioY": 0.523, "localisationLayerRatioY": 0.523,
"speed": 0.3, "speed": 0.3,
@ -1459,158 +1459,6 @@
"position": 0, "position": 0,
"positionLower": 0, "positionLower": 0,
"positionHigher": 0 "positionHigher": 0
},
{
"time": "2023-12-26T18:50:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261800&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T19:00:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261810&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T19:10:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261820&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T19:20:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261830&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T19:30:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261840&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T19:40:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261850&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T19:50:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261900&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T20:00:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261910&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T20:10:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261920&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T20:20:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261930&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T20:30:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261940&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T20:40:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261950&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T20:50:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312262000&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T21:00:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312262010&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T21:10:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312262020&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T21:20:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312262030&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T21:30:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312262040&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T21:40:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312262050&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
},
{
"time": "2023-12-26T21:50:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312262100&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0,
"position": 0,
"positionLower": 0,
"positionHigher": 0
} }
], ],
"threshold": [], "threshold": [],

1355
tests/fixtures/forecast_nl.json vendored Normal file

File diff suppressed because it is too large Load diff

BIN
tests/fixtures/loc_layer_be_n.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
tests/fixtures/loc_layer_nl_d.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,24 +1,29 @@
import json import json
from datetime import datetime from datetime import datetime
from io import BytesIO
from unittest.mock import AsyncMock
import pytz
from freezegun import freeze_time from freezegun import freeze_time
from homeassistant.components.weather import (ATTR_CONDITION_CLOUDY, from homeassistant.components.weather import (ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_PARTLYCLOUDY, ATTR_CONDITION_PARTLYCLOUDY,
ATTR_CONDITION_RAINY, Forecast) ATTR_CONDITION_RAINY, Forecast)
from homeassistant.components.zone import Zone
from homeassistant.core import HomeAssistant
from PIL import Image, ImageDraw, ImageFont
from pytest_homeassistant_custom_component.common import load_fixture from pytest_homeassistant_custom_component.common import load_fixture
from custom_components.irm_kmi.coordinator import IrmKmiCoordinator from custom_components.irm_kmi.coordinator import IrmKmiCoordinator
from custom_components.irm_kmi.data import CurrentWeatherData, IrmKmiForecast from custom_components.irm_kmi.data import CurrentWeatherData, IrmKmiForecast
def get_api_data() -> dict: def get_api_data(fixture: str) -> dict:
fixture: str = "forecast.json"
return json.loads(load_fixture(fixture)) return json.loads(load_fixture(fixture))
@freeze_time(datetime.fromisoformat('2023-12-26T18:30:00.028724')) @freeze_time(datetime.fromisoformat('2023-12-26T18:30:00'))
def test_current_weather() -> None: def test_current_weather_be() -> None:
api_data = get_api_data() api_data = get_api_data("forecast.json")
result = IrmKmiCoordinator.current_weather_from_data(api_data) result = IrmKmiCoordinator.current_weather_from_data(api_data)
expected = CurrentWeatherData( expected = CurrentWeatherData(
@ -34,9 +39,27 @@ def test_current_weather() -> None:
assert result == expected assert result == expected
@freeze_time(datetime.fromisoformat("2023-12-28T15:30:00"))
def test_current_weather_nl() -> None:
api_data = get_api_data("forecast_nl.json")
result = IrmKmiCoordinator.current_weather_from_data(api_data)
expected = CurrentWeatherData(
condition=ATTR_CONDITION_CLOUDY,
temperature=11,
wind_speed=40,
wind_gust_speed=None,
wind_bearing='SW',
pressure=1008,
uv_index=1
)
assert expected == result
@freeze_time(datetime.fromisoformat('2023-12-26T18:30:00.028724')) @freeze_time(datetime.fromisoformat('2023-12-26T18:30:00.028724'))
def test_daily_forecast() -> None: def test_daily_forecast() -> None:
api_data = get_api_data().get('for', {}).get('daily') api_data = get_api_data("forecast.json").get('for', {}).get('daily')
result = IrmKmiCoordinator.daily_list_to_forecast(api_data) result = IrmKmiCoordinator.daily_list_to_forecast(api_data)
assert isinstance(result, list) assert isinstance(result, list)
@ -62,7 +85,7 @@ def test_daily_forecast() -> None:
@freeze_time(datetime.fromisoformat('2023-12-26T18:30:00.028724')) @freeze_time(datetime.fromisoformat('2023-12-26T18:30:00.028724'))
def test_hourly_forecast() -> None: def test_hourly_forecast() -> None:
api_data = get_api_data().get('for', {}).get('hourly') api_data = get_api_data("forecast.json").get('for', {}).get('hourly')
result = IrmKmiCoordinator.hourly_list_to_forecast(api_data) result = IrmKmiCoordinator.hourly_list_to_forecast(api_data)
assert isinstance(result, list) assert isinstance(result, list)
@ -83,3 +106,70 @@ def test_hourly_forecast() -> None:
) )
assert result[8] == expected assert result[8] == expected
@freeze_time(datetime.fromisoformat("2023-12-28T15:30:00+01:00"))
async def test_get_image_nl(
hass: HomeAssistant,
mock_image_irm_kmi_api: AsyncMock) -> None:
api_data = get_api_data("forecast_nl.json")
coordinator = IrmKmiCoordinator(hass, Zone({}))
result = await coordinator._async_animation_data(api_data)
# Construct the expected image for the most recent one
tz = pytz.timezone(hass.config.time_zone)
background = Image.open("custom_components/irm_kmi/resources/nl.png").convert('RGBA')
layer = Image.open("tests/fixtures/clouds_nl.png").convert('RGBA')
localisation = Image.open("tests/fixtures/loc_layer_nl_d.png").convert('RGBA')
temp = Image.alpha_composite(background, layer)
expected = Image.alpha_composite(temp, localisation)
draw = ImageDraw.Draw(expected)
font = ImageFont.truetype("custom_components/irm_kmi/resources/roboto_medium.ttf", 16)
time_image = (datetime.fromisoformat("2023-12-28T14:25:00+00:00")
.astimezone(tz=tz))
time_str = time_image.isoformat(sep=' ', timespec='minutes')
draw.text((4, 4), time_str, (0, 0, 0), font=font)
result_image = Image.open(BytesIO(result['sequence'][-1]['image'])).convert('RGBA')
assert list(result_image.getdata()) == list(expected.getdata())
thumb_image = Image.open(BytesIO(result['most_recent_image'])).convert('RGBA')
assert list(thumb_image.getdata()) == list(expected.getdata())
assert result['hint'] == "No rain forecasted shortly"
@freeze_time(datetime.fromisoformat("2023-12-26T18:31:00+01:00"))
async def test_get_image_be(
hass: HomeAssistant,
mock_image_irm_kmi_api: AsyncMock,
) -> None:
api_data = get_api_data("forecast.json")
coordinator = IrmKmiCoordinator(hass, Zone({}))
result = await coordinator._async_animation_data(api_data)
# Construct the expected image for the most recent one
tz = pytz.timezone(hass.config.time_zone)
background = Image.open("custom_components/irm_kmi/resources/be_bw.png").convert('RGBA')
layer = Image.open("tests/fixtures/clouds_be.png").convert('RGBA')
localisation = Image.open("tests/fixtures/loc_layer_be_n.png").convert('RGBA')
temp = Image.alpha_composite(background, layer)
expected = Image.alpha_composite(temp, localisation)
draw = ImageDraw.Draw(expected)
font = ImageFont.truetype("custom_components/irm_kmi/resources/roboto_medium.ttf", 16)
time_image = (datetime.fromisoformat("2023-12-26T18:30:00+01:00")
.astimezone(tz=tz))
time_str = time_image.isoformat(sep=' ', timespec='minutes')
draw.text((4, 4), time_str, (255, 255, 255), font=font)
result_image = Image.open(BytesIO(result['sequence'][9]['image'])).convert('RGBA')
assert list(result_image.getdata()) == list(expected.getdata())
thumb_image = Image.open(BytesIO(result['most_recent_image'])).convert('RGBA')
assert list(thumb_image.getdata()) == list(expected.getdata())
assert result['hint'] == "No rain forecasted shortly"

View file

@ -1,6 +1,6 @@
"""Tests for the IRM KMI integration.""" """Tests for the IRM KMI integration."""
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock
import pytest import pytest
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState