mirror of
https://github.com/jdejaegh/irm-kmi-ha.git
synced 2025-06-27 03:35:56 +02:00
Add tests for new SVG camera and remove old ones
This commit is contained in:
parent
c9ec30b8b2
commit
b7d39bf753
5 changed files with 279 additions and 76 deletions
|
@ -54,7 +54,6 @@ class IrmKmiRadar(CoordinatorEntity, Camera):
|
||||||
width: int | None = None,
|
width: int | None = None,
|
||||||
height: int | None = None) -> bytes | None:
|
height: int | None = None) -> bytes | None:
|
||||||
"""Return still image to be used as thumbnail."""
|
"""Return still image to be used as thumbnail."""
|
||||||
# TODO make it a still image to avoid cuts in playback on the dashboard
|
|
||||||
return self.coordinator.data.get('animation', {}).get('svg_still')
|
return self.coordinator.data.get('animation', {}).get('svg_still')
|
||||||
|
|
||||||
async def async_camera_image(
|
async def async_camera_image(
|
||||||
|
|
|
@ -24,7 +24,6 @@ class CurrentWeatherData(TypedDict, total=False):
|
||||||
pressure: float | None
|
pressure: float | None
|
||||||
|
|
||||||
|
|
||||||
# TODO cleanup useless fields
|
|
||||||
class AnimationFrameData(TypedDict, total=False):
|
class AnimationFrameData(TypedDict, total=False):
|
||||||
"""Holds one single frame of the radar camera, along with the timestamp of the frame"""
|
"""Holds one single frame of the radar camera, along with the timestamp of the frame"""
|
||||||
time: datetime | None
|
time: datetime | None
|
||||||
|
|
|
@ -83,8 +83,6 @@ class RainGraph:
|
||||||
self.draw_location()
|
self.draw_location()
|
||||||
self._dwg_still = self._dwg
|
self._dwg_still = self._dwg
|
||||||
|
|
||||||
self._dwg_animated.saveas("animated_rain.svg")
|
|
||||||
|
|
||||||
def draw_svg_frame(self):
|
def draw_svg_frame(self):
|
||||||
"""Create the global area to draw the other items"""
|
"""Create the global area to draw the other items"""
|
||||||
self._dwg.embed_font(name="Roboto Medium", filename='custom_components/irm_kmi/resources/roboto_medium.ttf')
|
self._dwg.embed_font(name="Roboto Medium", filename='custom_components/irm_kmi/resources/roboto_medium.ttf')
|
||||||
|
@ -330,4 +328,4 @@ class RainGraph:
|
||||||
self._dwg.add(image)
|
self._dwg.add(image)
|
||||||
|
|
||||||
def get_dwg(self):
|
def get_dwg(self):
|
||||||
return copy.deepcopy(self._dwg)
|
return copy.deepcopy(self._dwg)
|
||||||
|
|
|
@ -107,74 +107,3 @@ def test_hourly_forecast() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result[8] == expected
|
assert result[8] == expected
|
||||||
|
|
||||||
|
|
||||||
@freeze_time(datetime.fromisoformat("2023-12-28T15:30:00+01:00"))
|
|
||||||
@pytest.mark.skip(reason="Outdated test, cannot be tested this way with the new camera features")
|
|
||||||
async def test_get_image_nl(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_image_irm_kmi_api: AsyncMock,
|
|
||||||
mock_config_entry: MockConfigEntry) -> None:
|
|
||||||
api_data = get_api_data("forecast_nl.json")
|
|
||||||
coordinator = IrmKmiCoordinator(hass, mock_config_entry)
|
|
||||||
|
|
||||||
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.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())
|
|
||||||
most_recent_image = result['sequence'][result['most_recent_image_idx']]['image']
|
|
||||||
thumb_image = Image.open(BytesIO(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"))
|
|
||||||
@pytest.mark.skip(reason="Outdated test, cannot be tested this way with the new camera features")
|
|
||||||
async def test_get_image_be(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_image_irm_kmi_api: AsyncMock,
|
|
||||||
mock_config_entry: MockConfigEntry
|
|
||||||
) -> None:
|
|
||||||
api_data = get_api_data("forecast.json")
|
|
||||||
coordinator = IrmKmiCoordinator(hass, mock_config_entry)
|
|
||||||
|
|
||||||
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_black.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())
|
|
||||||
most_recent_image = result['sequence'][result['most_recent_image_idx']]['image']
|
|
||||||
thumb_image = Image.open(BytesIO(most_recent_image)).convert('RGBA')
|
|
||||||
assert list(thumb_image.getdata()) == list(expected.getdata())
|
|
||||||
|
|
||||||
assert result['hint'] == "No rain forecasted shortly"
|
|
||||||
|
|
278
tests/test_rain_graph.py
Normal file
278
tests/test_rain_graph.py
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
import base64
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from custom_components.irm_kmi.data import (AnimationFrameData,
|
||||||
|
RadarAnimationData)
|
||||||
|
from custom_components.irm_kmi.rain_graph import RainGraph
|
||||||
|
|
||||||
|
|
||||||
|
def get_radar_animation_data() -> RadarAnimationData:
|
||||||
|
with open("tests/fixtures/clouds_be.png", "rb") as file:
|
||||||
|
image_data = file.read()
|
||||||
|
with open("tests/fixtures/loc_layer_be_n.png", "rb") as file:
|
||||||
|
location = file.read()
|
||||||
|
|
||||||
|
sequence = [
|
||||||
|
AnimationFrameData(
|
||||||
|
time=datetime.fromisoformat("2023-12-26T18:30:00+00:00") + timedelta(minutes=10 * i),
|
||||||
|
image=image_data,
|
||||||
|
value=2,
|
||||||
|
position=.5,
|
||||||
|
position_lower=.4,
|
||||||
|
position_higher=.6
|
||||||
|
)
|
||||||
|
for i in range(10)
|
||||||
|
]
|
||||||
|
|
||||||
|
return RadarAnimationData(
|
||||||
|
sequence=sequence,
|
||||||
|
most_recent_image_idx=2,
|
||||||
|
hint="Testing SVG camera",
|
||||||
|
unit="mm/10min",
|
||||||
|
location=location
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_svg_frame_setup():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.draw_svg_frame()
|
||||||
|
|
||||||
|
svg_str = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
with open("custom_components/irm_kmi/resources/roboto_medium.ttf", "rb") as file:
|
||||||
|
font_b64 = base64.b64encode(file.read()).decode('utf-8')
|
||||||
|
|
||||||
|
assert '#385E95' in svg_str
|
||||||
|
assert 'font-family: "Roboto Medium";' in svg_str
|
||||||
|
assert font_b64 in svg_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_svg_hint():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.write_hint()
|
||||||
|
|
||||||
|
svg_str = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
assert "Testing SVG camera" in svg_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_svg_time_bars():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.draw_hour_bars()
|
||||||
|
|
||||||
|
svg_str = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
assert "19h" in svg_str
|
||||||
|
assert "20h" in svg_str
|
||||||
|
|
||||||
|
assert "<line" in svg_str
|
||||||
|
assert 'stroke="white"' in svg_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_chances_path():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.draw_chances_path()
|
||||||
|
|
||||||
|
svg_str = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
assert 'fill="#63c8fa"' in svg_str
|
||||||
|
assert 'opacity="0.3"' in svg_str
|
||||||
|
assert 'stroke="none"' in svg_str
|
||||||
|
assert '<path ' in svg_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_data_line():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.draw_data_line()
|
||||||
|
|
||||||
|
svg_str = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
assert 'fill="none"' in svg_str
|
||||||
|
assert 'stroke-width="2"' in svg_str
|
||||||
|
assert 'stroke="#63c8fa"' in svg_str
|
||||||
|
assert '<path ' in svg_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_background():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.insert_background()
|
||||||
|
|
||||||
|
with open("custom_components/irm_kmi/resources/be_white.png", "rb") as file:
|
||||||
|
png_b64 = base64.b64encode(file.read()).decode('utf-8')
|
||||||
|
|
||||||
|
svg_str = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
assert png_b64 in svg_str
|
||||||
|
assert "<image " in svg_str
|
||||||
|
assert 'height="490"' in svg_str
|
||||||
|
assert 'width="640"' in svg_str
|
||||||
|
assert 'x="0"' in svg_str
|
||||||
|
assert 'y="0"' in svg_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_current_frame_line_moving():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.draw_current_fame_line()
|
||||||
|
|
||||||
|
str_svg = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
assert '<line' in str_svg
|
||||||
|
assert 'id="now"' in str_svg
|
||||||
|
assert 'opacity="1"' in str_svg
|
||||||
|
assert 'stroke="white"' in str_svg
|
||||||
|
assert 'stroke-width="2"' in str_svg
|
||||||
|
assert 'x1="50' in str_svg
|
||||||
|
assert 'x2="50' in str_svg
|
||||||
|
assert 'y1="520' in str_svg
|
||||||
|
assert 'y2="670' in str_svg
|
||||||
|
|
||||||
|
assert 'animateTransform' in str_svg
|
||||||
|
assert 'attributeName="transform"' in str_svg
|
||||||
|
assert 'repeatCount="indefinite"' in str_svg
|
||||||
|
assert 'type="translate"' in str_svg
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_current_frame_line_index():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.draw_current_fame_line(0)
|
||||||
|
|
||||||
|
str_svg = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
assert '<line' in str_svg
|
||||||
|
assert 'id="now"' in str_svg
|
||||||
|
assert 'opacity="1"' in str_svg
|
||||||
|
assert 'stroke="white"' in str_svg
|
||||||
|
assert 'stroke-width="2"' in str_svg
|
||||||
|
assert 'x1="50' in str_svg
|
||||||
|
assert 'x2="50' in str_svg
|
||||||
|
assert 'y1="520' in str_svg
|
||||||
|
assert 'y2="670' in str_svg
|
||||||
|
|
||||||
|
assert 'animateTransform' not in str_svg
|
||||||
|
assert 'attributeName="transform"' not in str_svg
|
||||||
|
assert 'repeatCount="indefinite"' not in str_svg
|
||||||
|
assert 'type="translate"' not in str_svg
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_description_text():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.draw_description_text()
|
||||||
|
|
||||||
|
str_svg = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
assert "18:30" in str_svg
|
||||||
|
assert "18:40" in str_svg
|
||||||
|
assert "18:50" in str_svg
|
||||||
|
assert "19:00" in str_svg
|
||||||
|
assert "19:10" in str_svg
|
||||||
|
assert "19:20" in str_svg
|
||||||
|
assert "19:30" in str_svg
|
||||||
|
assert "19:40" in str_svg
|
||||||
|
assert "19:50" in str_svg
|
||||||
|
assert "20:00" in str_svg
|
||||||
|
|
||||||
|
assert str_svg.count("2mm/10") == 10
|
||||||
|
assert 'class="roboto"' in str_svg
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_cloud_layer():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.insert_cloud_layer()
|
||||||
|
|
||||||
|
str_svg = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
with open("tests/fixtures/clouds_be.png", "rb") as file:
|
||||||
|
png_b64 = base64.b64encode(file.read()).decode('utf-8')
|
||||||
|
|
||||||
|
assert str_svg.count(png_b64) == 10
|
||||||
|
assert str_svg.count('height="490"') == 10
|
||||||
|
assert str_svg.count('width="640"') == 11 # Is also the width of the SVG itself
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_location_layer():
|
||||||
|
data = get_radar_animation_data()
|
||||||
|
rain_graph = RainGraph(
|
||||||
|
animation_data=data,
|
||||||
|
background_image_path="custom_components/irm_kmi/resources/be_white.png",
|
||||||
|
background_size=(640, 490),
|
||||||
|
auto=False
|
||||||
|
)
|
||||||
|
|
||||||
|
rain_graph.draw_location()
|
||||||
|
|
||||||
|
str_svg = rain_graph.get_dwg().tostring()
|
||||||
|
|
||||||
|
with open("tests/fixtures/loc_layer_be_n.png", "rb") as file:
|
||||||
|
png_b64 = base64.b64encode(file.read()).decode('utf-8')
|
||||||
|
|
||||||
|
assert png_b64 in str_svg
|
Loading…
Add table
Reference in a new issue