mirror of
https://github.com/jdejaegh/irm-kmi-ha.git
synced 2025-06-27 03:35:56 +02:00
Add tests for weather radar data
This commit is contained in:
parent
f392e8b004
commit
aae39d8ddc
11 changed files with 1512 additions and 170 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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
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
BIN
tests/fixtures/clouds_nl.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
154
tests/fixtures/forecast.json
vendored
154
tests/fixtures/forecast.json
vendored
|
@ -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
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
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
BIN
tests/fixtures/loc_layer_nl_d.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue