Merge pull request #41 from jdejaegh/blocking_calls

Use non-blocking calls in the main loop
This commit is contained in:
Jules 2024-06-01 18:51:29 +02:00 committed by GitHub
commit 0812ef5eef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 107 additions and 105 deletions

View file

@ -1,8 +1,6 @@
"""Sensor to signal weather warning from the IRM KMI"""
import datetime
import logging
import pytz
from homeassistant.components import binary_sensor
from homeassistant.components.binary_sensor import (BinarySensorDeviceClass,
BinarySensorEntity)
@ -10,6 +8,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt
from custom_components.irm_kmi import DOMAIN, IrmKmiCoordinator
@ -44,7 +43,7 @@ class IrmKmiWarning(CoordinatorEntity, BinarySensorEntity):
if self.coordinator.data.get('warnings') is None:
return False
now = datetime.datetime.now(tz=pytz.timezone(self.hass.config.time_zone))
now = dt.now()
for item in self.coordinator.data.get('warnings'):
if item.get('starts_at') < now < item.get('ends_at'):
return True
@ -56,7 +55,7 @@ class IrmKmiWarning(CoordinatorEntity, BinarySensorEntity):
"""Return the warning sensor attributes."""
attrs = {"warnings": self.coordinator.data.get('warnings', [])}
now = datetime.datetime.now(tz=pytz.timezone(self.hass.config.time_zone))
now = dt.now()
for warning in attrs['warnings']:
warning['is_active'] = warning.get('starts_at') < now < warning.get('ends_at')

View file

@ -5,7 +5,6 @@ from datetime import datetime, timedelta
from typing import Any, List, Tuple
import async_timeout
import pytz
from homeassistant.components.weather import Forecast
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE
@ -15,6 +14,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import (
TimestampDataUpdateCoordinator, UpdateFailed)
from homeassistant.util import dt
from homeassistant.util.dt import utcnow
from .api import IrmKmiApiClient, IrmKmiApiError
@ -133,7 +133,8 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
unit=api_data.get('animation', {}).get('unit', {}).get(lang),
location=localisation
)
rain_graph = self.create_rain_graph(radar_animation, animation_data, country, images_from_api)
rain_graph = await self.create_rain_graph(radar_animation, animation_data, country, images_from_api)
print(rain_graph)
radar_animation['svg_animated'] = rain_graph.get_svg_string()
radar_animation['svg_still'] = rain_graph.get_svg_string(still_image=True)
return radar_animation
@ -164,9 +165,9 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
async def process_api_data(self, api_data: dict) -> ProcessedCoordinatorData:
"""From the API data, create the object that will be used in the entities"""
return ProcessedCoordinatorData(
current_weather=IrmKmiCoordinator.current_weather_from_data(api_data),
daily_forecast=self.daily_list_to_forecast(api_data.get('for', {}).get('daily')),
hourly_forecast=IrmKmiCoordinator.hourly_list_to_forecast(api_data.get('for', {}).get('hourly')),
current_weather=await IrmKmiCoordinator.current_weather_from_data(api_data),
daily_forecast=await self.daily_list_to_forecast(api_data.get('for', {}).get('daily')),
hourly_forecast=await IrmKmiCoordinator.hourly_list_to_forecast(api_data.get('for', {}).get('hourly')),
radar_forecast=IrmKmiCoordinator.radar_list_to_forecast(api_data.get('animation', {})),
animation=await self._async_animation_data(api_data=api_data),
warnings=self.warnings_from_data(api_data.get('for', {}).get('warning')),
@ -194,17 +195,19 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
return images_from_api
@staticmethod
def current_weather_from_data(api_data: dict) -> CurrentWeatherData:
async def current_weather_from_data(api_data: dict) -> CurrentWeatherData:
"""Parse the API data to build a CurrentWeatherData."""
# Process data to get current hour forecast
now_hourly = None
hourly_forecast_data = api_data.get('for', {}).get('hourly')
tz = await dt.async_get_time_zone('Europe/Brussels')
now = dt.now(time_zone=tz)
if not (hourly_forecast_data is None
or not isinstance(hourly_forecast_data, list)
or len(hourly_forecast_data) == 0):
for current in hourly_forecast_data[:2]:
if datetime.now().strftime('%H') == current['hour']:
if now.strftime('%H') == current['hour']:
now_hourly = current
break
# Get UV index
@ -267,13 +270,14 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
return current_weather
@staticmethod
def hourly_list_to_forecast(data: List[dict] | None) -> List[Forecast] | None:
async def hourly_list_to_forecast(data: List[dict] | None) -> List[Forecast] | None:
"""Parse data from the API to create a list of hourly forecasts"""
if data is None or not isinstance(data, list) or len(data) == 0:
return None
forecasts = list()
day = datetime.now(tz=pytz.timezone('Europe/Brussels')).replace(hour=0, minute=0, second=0, microsecond=0)
tz = await dt.async_get_time_zone('Europe/Brussels')
day = dt.now(time_zone=tz).replace(hour=0, minute=0, second=0, microsecond=0)
for idx, f in enumerate(data):
if 'dateShow' in f and idx > 0:
@ -332,7 +336,7 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
)
return forecast
def daily_list_to_forecast(self, data: List[dict] | None) -> List[Forecast] | None:
async def daily_list_to_forecast(self, data: List[dict] | None) -> List[Forecast] | None:
"""Parse data from the API to create a list of daily forecasts"""
if data is None or not isinstance(data, list) or len(data) == 0:
return None
@ -340,6 +344,8 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
forecasts = list()
n_days = 0
lang = preferred_language(self.hass, self._config_entry)
tz = await dt.async_get_time_zone('Europe/Brussels')
now = dt.now(tz)
for (idx, f) in enumerate(data):
precipitation = None
@ -364,10 +370,9 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
pass
is_daytime = f.get('dayNight', None) == 'd'
now = datetime.now(pytz.timezone('Europe/Brussels'))
forecast = IrmKmiForecast(
datetime=(now + timedelta(days=n_days)).strftime('%Y-%m-%d')
if is_daytime else now.strftime('%Y-%m-%d'),
datetime=(now + timedelta(days=n_days)).strftime('%Y-%m-%d') if is_daytime else now.strftime(
'%Y-%m-%d'),
condition=CDT_MAP.get((f.get('ww1', None), f.get('dayNight', None)), None),
native_precipitation=precipitation,
native_temperature=f.get('tempMax', None),
@ -392,16 +397,17 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
return forecasts
def create_rain_graph(self,
radar_animation: RadarAnimationData,
api_animation_data: List[dict],
country: str,
images_from_api: Tuple[bytes],
) -> RainGraph:
async def create_rain_graph(self,
radar_animation: RadarAnimationData,
api_animation_data: List[dict],
country: str,
images_from_api: Tuple[bytes],
) -> RainGraph:
"""Create a RainGraph object that is ready to output animated and still SVG images"""
sequence: List[AnimationFrameData] = list()
tz = pytz.timezone(self.hass.config.time_zone)
current_time = datetime.now(tz=tz)
tz = await dt.async_get_time_zone(self.hass.config.time_zone)
current_time = dt.now(time_zone=tz)
most_recent_frame = None
for idx, item in enumerate(api_animation_data):
@ -431,10 +437,8 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
f"{'satellite' if satellite_mode else 'black' if self._dark_mode else 'white'}.png")
bg_size = (640, 490)
return RainGraph(radar_animation, image_path, bg_size,
config_dir=self.hass.config.config_dir,
dark_mode=self._dark_mode,
tz=self.hass.config.time_zone)
return await RainGraph(radar_animation, image_path, bg_size, tz=tz, config_dir=self.hass.config.config_dir,
dark_mode=self._dark_mode).build()
def warnings_from_data(self, warning_data: list | None) -> List[WarningData]:
"""Create a list of warning data instances based on the api data"""

View file

@ -9,8 +9,8 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/jdejaegh/irm-kmi-ha/issues",
"requirements": [
"pytz==2024.1",
"svgwrite==1.4.3"
"svgwrite==1.4.3",
"aiofile==3.8.8"
],
"version": "0.2.14"
}

View file

@ -2,13 +2,16 @@
import base64
import copy
import datetime
import logging
import os
from typing import List
from typing import List, Self
import pytz
from aiofile import async_open
from homeassistant.util import dt
from svgwrite import Drawing
from svgwrite.animate import Animate
from svgwrite.utils import font_mimetype
from custom_components.irm_kmi.data import (AnimationFrameData,
RadarAnimationData)
@ -23,7 +26,7 @@ class RainGraph:
background_size: (int, int),
config_dir: str = '.',
dark_mode: bool = False,
tz: str = 'UTC',
tz: datetime.tzinfo = dt.get_default_time_zone(),
svg_width: float = 640,
inset: float = 20,
graph_height: float = 150,
@ -31,7 +34,6 @@ class RainGraph:
top_text_y_pos: float = 20,
bottom_text_space: float = 50,
bottom_text_y_pos: float = 218,
auto=True
):
self._animation_data: RadarAnimationData = animation_data
@ -39,7 +41,7 @@ class RainGraph:
self._background_size: (int, int) = background_size
self._config_dir: str = config_dir
self._dark_mode: bool = dark_mode
self._tz = pytz.timezone(tz)
self._tz = tz
self._svg_width: float = svg_width
self._inset: float = inset
self._graph_height: float = graph_height
@ -62,38 +64,45 @@ class RainGraph:
raise ValueError("bottom_text_y_pos must be below the graph")
self._dwg: Drawing = Drawing(size=(self._svg_width, self._svg_height), profile='full')
self._dwg_save: Drawing
self._dwg_animated: Drawing
self._dwg_still: Drawing
self._dwg_save: Drawing = Drawing()
self._dwg_animated: Drawing = Drawing()
self._dwg_still: Drawing = Drawing()
if auto:
self.draw_svg_frame()
self.draw_hour_bars()
self.draw_chances_path()
self.draw_data_line()
self.write_hint()
self.insert_background()
self._dwg_save = copy.deepcopy(self._dwg)
async def build(self) -> Self:
await self.draw_svg_frame()
self.draw_hour_bars()
self.draw_chances_path()
self.draw_data_line()
self.write_hint()
await self.insert_background()
self._dwg_save = copy.deepcopy(self._dwg)
self.draw_current_fame_line()
self.draw_description_text()
self.insert_cloud_layer()
self.draw_location()
self._dwg_animated = self._dwg
self.draw_current_fame_line()
self.draw_description_text()
self.insert_cloud_layer()
self.draw_location()
self._dwg_animated = self._dwg
self._dwg = self._dwg_save
idx = self._animation_data['most_recent_image_idx']
self.draw_current_fame_line(idx)
self.draw_description_text(idx)
self.insert_cloud_layer(idx)
self.draw_location()
self._dwg_still = self._dwg
self._dwg = self._dwg_save
idx = self._animation_data['most_recent_image_idx']
self.draw_current_fame_line(idx)
self.draw_description_text(idx)
self.insert_cloud_layer(idx)
self.draw_location()
self._dwg_still = self._dwg
return self
def draw_svg_frame(self):
async def draw_svg_frame(self):
"""Create the global area to draw the other items"""
font_file = os.path.join(self._config_dir, 'custom_components/irm_kmi/resources/roboto_medium.ttf')
_LOGGER.debug(f"Opening font file at {font_file}")
self._dwg.embed_font(name="Roboto Medium", filename=font_file)
async with async_open(font_file, 'rb') as font:
data = await font.read()
# Need to use the private class method as the public one does not offer an async call
# As this is run in the main loop, we cannot afford a blocking open() call
self._dwg._embed_font_data("Roboto Medium", data, font_mimetype(font_file))
self._dwg.embed_stylesheet("""
.roboto {
font-family: "Roboto Medium";
@ -299,10 +308,10 @@ class RainGraph:
def get_svg_string(self, still_image: bool = False) -> bytes:
return self._dwg_still.tostring().encode() if still_image else self._dwg_animated.tostring().encode()
def insert_background(self):
async def insert_background(self):
bg_image_path = os.path.join(self._config_dir, self._background_image_path)
with open(bg_image_path, 'rb') as f:
png_data = base64.b64encode(f.read()).decode('utf-8')
async with async_open(bg_image_path, 'rb') as f:
png_data = base64.b64encode(await f.read()).decode('utf-8')
image = self._dwg.image("data:image/png;base64," + png_data, insert=(0, 0), size=self._background_size)
self._dwg.add(image)

View file

@ -2,13 +2,13 @@
import datetime
import logging
import pytz
from homeassistant.components import sensor
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt
from custom_components.irm_kmi import DOMAIN, IrmKmiCoordinator
from custom_components.irm_kmi.const import POLLEN_NAMES, POLLEN_TO_ICON_MAP
@ -75,7 +75,7 @@ class IrmKmiNextWarning(CoordinatorEntity, SensorEntity):
if self.coordinator.data.get('warnings') is None:
return None
now = datetime.datetime.now(tz=pytz.timezone(self.hass.config.time_zone))
now = dt.now()
earliest_next = None
for item in self.coordinator.data.get('warnings'):
if now < item.get('starts_at'):
@ -89,7 +89,7 @@ class IrmKmiNextWarning(CoordinatorEntity, SensorEntity):
@property
def extra_state_attributes(self) -> dict:
"""Return the attributes related to all the future warnings."""
now = datetime.datetime.now(tz=pytz.timezone(self.hass.config.time_zone))
now = dt.now()
attrs = {"next_warnings": [w for w in self.coordinator.data.get('warnings', []) if now < w.get('starts_at')]}
attrs["next_warnings_friendly_names"] = ", ".join(

View file

@ -159,7 +159,6 @@ class IrmKmiWeather(CoordinatorEntity, WeatherEntity):
:param include_past_forecasts: whether to include data points that are in the past
:return: ordered list of forecasts
"""
# now = datetime.now(tz=pytz.timezone(self.hass.config.time_zone))
now = dt.now()
now = now.replace(minute=(now.minute // 10) * 10, second=0, microsecond=0)

View file

@ -2,5 +2,5 @@ aiohttp==3.9.5
async-timeout==4.0.3
homeassistant==2024.6.0b4
voluptuous==0.13.1
pytz==2024.1
svgwrite==1.4.3
aiofile==3.8.8

View file

@ -1,5 +1,5 @@
homeassistant==2024.6.0b4
pytest
pytest_homeassistant_custom_component==0.13.128
pytest
freezegun
isort

View file

@ -13,9 +13,9 @@ from pytest_homeassistant_custom_component.common import (MockConfigEntry,
from custom_components.irm_kmi.api import (IrmKmiApiError,
IrmKmiApiParametersError)
from custom_components.irm_kmi.const import (
CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST, DOMAIN,
OPTION_DEPRECATED_FORECAST_NOT_USED,
OPTION_DEPRECATED_FORECAST_TWICE_DAILY, OPTION_STYLE_STD, CONF_LANGUAGE_OVERRIDE)
CONF_DARK_MODE, CONF_LANGUAGE_OVERRIDE, CONF_STYLE,
CONF_USE_DEPRECATED_FORECAST, DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED,
OPTION_DEPRECATED_FORECAST_TWICE_DAILY, OPTION_STYLE_STD)
def get_api_data(fixture: str) -> dict:

View file

@ -25,7 +25,7 @@ async def test_jules_forgot_to_revert_update_interval_before_pushing(
assert timedelta(minutes=5) <= coordinator.update_interval
@freeze_time(datetime.fromisoformat('2024-01-12T07:10:00'))
@freeze_time(datetime.fromisoformat('2024-01-12T07:10:00+00:00'))
async def test_warning_data(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry
@ -49,10 +49,10 @@ async def test_warning_data(
assert first.get('level') == 1
@freeze_time(datetime.fromisoformat('2023-12-26T18:30:00'))
def test_current_weather_be() -> None:
@freeze_time(datetime.fromisoformat('2023-12-26T17:30:00+00:00'))
async def test_current_weather_be() -> None:
api_data = get_api_data("forecast.json")
result = IrmKmiCoordinator.current_weather_from_data(api_data)
result = await IrmKmiCoordinator.current_weather_from_data(api_data)
expected = CurrentWeatherData(
condition=ATTR_CONDITION_CLOUDY,
@ -68,9 +68,9 @@ def test_current_weather_be() -> None:
@freeze_time(datetime.fromisoformat("2023-12-28T15:30:00"))
def test_current_weather_nl() -> None:
async def test_current_weather_nl() -> None:
api_data = get_api_data("forecast_nl.json")
result = IrmKmiCoordinator.current_weather_from_data(api_data)
result = await IrmKmiCoordinator.current_weather_from_data(api_data)
expected = CurrentWeatherData(
condition=ATTR_CONDITION_CLOUDY,
@ -94,7 +94,7 @@ async def test_daily_forecast(
mock_config_entry.data = mock_config_entry.data | {CONF_LANGUAGE_OVERRIDE: 'fr'}
coordinator = IrmKmiCoordinator(hass, mock_config_entry)
result = coordinator.daily_list_to_forecast(api_data)
result = await coordinator.daily_list_to_forecast(api_data)
assert isinstance(result, list)
assert len(result) == 8
@ -117,9 +117,9 @@ async def test_daily_forecast(
@freeze_time(datetime.fromisoformat('2023-12-26T18:30:00+01:00'))
def test_hourly_forecast() -> None:
async def test_hourly_forecast() -> None:
api_data = get_api_data("forecast.json").get('for', {}).get('hourly')
result = IrmKmiCoordinator.hourly_list_to_forecast(api_data)
result = await IrmKmiCoordinator.hourly_list_to_forecast(api_data)
assert isinstance(result, list)
assert len(result) == 49
@ -142,9 +142,9 @@ def test_hourly_forecast() -> None:
@freeze_time(datetime.fromisoformat('2024-05-31T01:50:00+02:00'))
def test_hourly_forecast_bis() -> None:
async def test_hourly_forecast_bis() -> None:
api_data = get_api_data("no-midnight-bug-31-05-2024T01-55.json").get('for', {}).get('hourly')
result = IrmKmiCoordinator.hourly_list_to_forecast(api_data)
result = await IrmKmiCoordinator.hourly_list_to_forecast(api_data)
assert isinstance(result, list)
@ -158,10 +158,10 @@ def test_hourly_forecast_bis() -> None:
@freeze_time(datetime.fromisoformat('2024-05-31T00:10:00+02:00'))
def test_hourly_forecast_midnight_bug() -> None:
async def test_hourly_forecast_midnight_bug() -> None:
# Related to https://github.com/jdejaegh/irm-kmi-ha/issues/38
api_data = get_api_data("midnight-bug-31-05-2024T00-13.json").get('for', {}).get('hourly')
result = IrmKmiCoordinator.hourly_list_to_forecast(api_data)
result = await IrmKmiCoordinator.hourly_list_to_forecast(api_data)
assert isinstance(result, list)

View file

@ -9,8 +9,10 @@ from homeassistant.core import HomeAssistant
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.irm_kmi import async_migrate_entry
from custom_components.irm_kmi.const import DOMAIN, CONFIG_FLOW_VERSION, CONF_LANGUAGE_OVERRIDE, \
CONF_USE_DEPRECATED_FORECAST, OPTION_DEPRECATED_FORECAST_NOT_USED, CONF_DARK_MODE, CONF_STYLE, OPTION_STYLE_STD
from custom_components.irm_kmi.const import (
CONF_DARK_MODE, CONF_LANGUAGE_OVERRIDE, CONF_STYLE,
CONF_USE_DEPRECATED_FORECAST, CONFIG_FLOW_VERSION, DOMAIN,
OPTION_DEPRECATED_FORECAST_NOT_USED, OPTION_STYLE_STD)
async def test_load_unload_config_entry(

View file

@ -33,16 +33,15 @@ def get_radar_animation_data() -> RadarAnimationData:
)
def test_svg_frame_setup():
async 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()
await rain_graph.draw_svg_frame()
svg_str = rain_graph.get_dwg().tostring()
@ -60,7 +59,6 @@ def test_svg_hint():
animation_data=data,
background_image_path="custom_components/irm_kmi/resources/be_white.png",
background_size=(640, 490),
auto=False
)
rain_graph.write_hint()
@ -76,7 +74,6 @@ def test_svg_time_bars():
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()
@ -96,7 +93,6 @@ def test_draw_chances_path():
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()
@ -115,7 +111,6 @@ def test_draw_data_line():
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()
@ -128,16 +123,15 @@ def test_draw_data_line():
assert '<path ' in svg_str
def test_insert_background():
async 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()
await 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')
@ -158,7 +152,6 @@ def test_draw_current_frame_line_moving():
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()
@ -187,7 +180,6 @@ def test_draw_current_frame_line_index():
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)
@ -216,7 +208,6 @@ def test_draw_description_text():
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()
@ -244,7 +235,6 @@ def test_draw_cloud_layer():
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()
@ -265,7 +255,6 @@ def test_draw_location_layer():
animation_data=data,
background_image_path="custom_components/irm_kmi/resources/be_white.png",
background_size=(640, 490),
auto=False
)
rain_graph.draw_location()

View file

@ -35,7 +35,7 @@ async def test_warning_data(
@freeze_time(datetime.fromisoformat('2024-01-12T07:55:00+01:00'))
async def test_warning_data(
async def test_warning_data_unknown_lang(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry
) -> None: