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"""
# File inspired by https://github.com/jodur/imagesdirectory-camera/blob/main/custom_components/imagedirectory/camera.py
import logging
@ -47,7 +46,7 @@ class IrmKmiRadar(CoordinatorEntity, Camera):
self._image_index = 0
@property # Baseclass Camera property override
@property
def frame_interval(self) -> float:
"""Return the interval between frames of the mjpeg stream"""
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):
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:
if frame.get('uri', None) is not None:
coroutines.append(self._api_client.get_image(frame.get('uri')))
@ -184,6 +187,7 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
for current in hourly_forecast_data[:2]:
if datetime.now().strftime('%H') == current['hour']:
now_hourly = current
break
# Get UV index
module_data = api_data.get('module', None)
uv_index = None
@ -192,13 +196,33 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
if module.get('type', None) == 'uv':
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(
condition=CDT_MAP.get((api_data.get('obs', {}).get('ww'), api_data.get('obs', {}).get('dayNight')), None),
temperature=api_data.get('obs', {}).get('temp'),
wind_speed=now_hourly.get('windSpeedKm', None) if now_hourly is not None else None,
wind_gust_speed=now_hourly.get('windPeakSpeedKm', None) if now_hourly is not None else None,
temperature=temperature,
wind_speed=wind_speed,
wind_gust_speed=wind_gust_speed,
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
)

View file

@ -45,7 +45,6 @@ def mock_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMoc
fixture: str = "forecast.json"
forecast = json.loads(load_fixture(fixture))
print(type(forecast))
with patch(
"custom_components.irm_kmi.coordinator.IrmKmiApiClient", autospec=True
) as irm_kmi_api_mock:
@ -81,8 +80,35 @@ def mock_exception_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None
@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."""
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(
"custom_components.irm_kmi.IrmKmiCoordinator", autospec=True
) 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": {
"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,
"localisationLayerRatioY": 0.523,
"speed": 0.3,
@ -1459,158 +1459,6 @@
"position": 0,
"positionLower": 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": [],

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
from datetime import datetime
from io import BytesIO
from unittest.mock import AsyncMock
import pytz
from freezegun import freeze_time
from homeassistant.components.weather import (ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_PARTLYCLOUDY,
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 custom_components.irm_kmi.coordinator import IrmKmiCoordinator
from custom_components.irm_kmi.data import CurrentWeatherData, IrmKmiForecast
def get_api_data() -> dict:
fixture: str = "forecast.json"
def get_api_data(fixture: str) -> dict:
return json.loads(load_fixture(fixture))
@freeze_time(datetime.fromisoformat('2023-12-26T18:30:00.028724'))
def test_current_weather() -> None:
api_data = get_api_data()
@freeze_time(datetime.fromisoformat('2023-12-26T18:30:00'))
def test_current_weather_be() -> None:
api_data = get_api_data("forecast.json")
result = IrmKmiCoordinator.current_weather_from_data(api_data)
expected = CurrentWeatherData(
@ -34,9 +39,27 @@ def test_current_weather() -> None:
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'))
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)
assert isinstance(result, list)
@ -62,7 +85,7 @@ def test_daily_forecast() -> None:
@freeze_time(datetime.fromisoformat('2023-12-26T18:30:00.028724'))
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)
assert isinstance(result, list)
@ -83,3 +106,70 @@ def test_hourly_forecast() -> None:
)
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."""
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock
import pytest
from homeassistant.config_entries import ConfigEntryState