diff --git a/custom_components/irm_kmi/binary_sensor.py b/custom_components/irm_kmi/binary_sensor.py index 616a767..922a8b5 100644 --- a/custom_components/irm_kmi/binary_sensor.py +++ b/custom_components/irm_kmi/binary_sensor.py @@ -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') diff --git a/custom_components/irm_kmi/coordinator.py b/custom_components/irm_kmi/coordinator.py index 2988f9b..a4031b4 100644 --- a/custom_components/irm_kmi/coordinator.py +++ b/custom_components/irm_kmi/coordinator.py @@ -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""" diff --git a/custom_components/irm_kmi/manifest.json b/custom_components/irm_kmi/manifest.json index c8d2699..8bd74e7 100644 --- a/custom_components/irm_kmi/manifest.json +++ b/custom_components/irm_kmi/manifest.json @@ -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" } \ No newline at end of file diff --git a/custom_components/irm_kmi/rain_graph.py b/custom_components/irm_kmi/rain_graph.py index cde5855..2ead986 100644 --- a/custom_components/irm_kmi/rain_graph.py +++ b/custom_components/irm_kmi/rain_graph.py @@ -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) diff --git a/custom_components/irm_kmi/sensor.py b/custom_components/irm_kmi/sensor.py index b3ed85f..39c8ab9 100644 --- a/custom_components/irm_kmi/sensor.py +++ b/custom_components/irm_kmi/sensor.py @@ -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( diff --git a/custom_components/irm_kmi/weather.py b/custom_components/irm_kmi/weather.py index 2aba43c..29d3e12 100644 --- a/custom_components/irm_kmi/weather.py +++ b/custom_components/irm_kmi/weather.py @@ -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) diff --git a/requirements.txt b/requirements.txt index 3cedcb8..02ec6ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file +svgwrite==1.4.3 +aiofile==3.8.8 \ No newline at end of file diff --git a/requirements_tests.txt b/requirements_tests.txt index 3c3a46e..4e68489 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -1,5 +1,5 @@ homeassistant==2024.6.0b4 -pytest pytest_homeassistant_custom_component==0.13.128 +pytest freezegun isort \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index e9134a5..f619cba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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: diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index aafb277..a41948a 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -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) diff --git a/tests/test_init.py b/tests/test_init.py index c29b615..1947ae0 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -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( diff --git a/tests/test_rain_graph.py b/tests/test_rain_graph.py index 21c024a..4d72e30 100644 --- a/tests/test_rain_graph.py +++ b/tests/test_rain_graph.py @@ -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 ' None: