Create new service: irm_kmi.get_forecasts_radar

This commit is contained in:
Jules 2024-05-19 22:54:55 +02:00
parent 22b7305e14
commit 121b6e50c3
Signed by: jdejaegh
GPG key ID: 99D6D184CA66933A
12 changed files with 178 additions and 13 deletions

View file

@ -13,8 +13,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry from homeassistant.helpers import issue_registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import (TimestampDataUpdateCoordinator, from homeassistant.helpers.update_coordinator import (
UpdateFailed) TimestampDataUpdateCoordinator, UpdateFailed)
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .api import IrmKmiApiClient, IrmKmiApiError from .api import IrmKmiApiClient, IrmKmiApiError
@ -168,6 +168,7 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
current_weather=IrmKmiCoordinator.current_weather_from_data(api_data), current_weather=IrmKmiCoordinator.current_weather_from_data(api_data),
daily_forecast=self.daily_list_to_forecast(api_data.get('for', {}).get('daily')), 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')), hourly_forecast=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), animation=await self._async_animation_data(api_data=api_data),
warnings=self.warnings_from_data(api_data.get('for', {}).get('warning')), warnings=self.warnings_from_data(api_data.get('for', {}).get('warning')),
pollen=await self._async_pollen_data(api_data=api_data) pollen=await self._async_pollen_data(api_data=api_data)
@ -317,6 +318,21 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
return forecasts return forecasts
@staticmethod
def radar_list_to_forecast(data: dict | None) -> List[Forecast] | None:
if data is None:
return None
forecast = list()
for f in data.get("sequence", []):
forecast.append(
Forecast(
datetime=f.get("time"),
native_precipitation=f.get('value')
)
)
return forecast
def daily_list_to_forecast(self, data: List[dict] | None) -> List[Forecast] | None: 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""" """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: if data is None or not isinstance(data, list) or len(data) == 0:

View file

@ -60,6 +60,7 @@ class ProcessedCoordinatorData(TypedDict, total=False):
current_weather: CurrentWeatherData current_weather: CurrentWeatherData
hourly_forecast: List[Forecast] | None hourly_forecast: List[Forecast] | None
daily_forecast: List[IrmKmiForecast] | None daily_forecast: List[IrmKmiForecast] | None
radar_forecast: List[Forecast] | None
animation: RadarAnimationData animation: RadarAnimationData
warnings: List[WarningData] warnings: List[WarningData]
pollen: dict pollen: dict

View file

@ -0,0 +1,5 @@
{
"services": {
"get_forecasts_radar": "mdi:weather-cloudy-clock"
}
}

View file

@ -0,0 +1,11 @@
get_forecasts_radar:
target:
entity:
integration: irm_kmi
domain: weather
fields:
include_past_forecasts:
required: true
default: false
selector:
boolean:

View file

@ -166,5 +166,17 @@
} }
} }
} }
},
"services": {
"get_forecasts_radar": {
"name": "Get forecast from the radar",
"description": "Get weather forecast from the radar. Only precipitation is available.",
"fields": {
"include_past_forecasts": {
"name": "Include past forecasts",
"description": "Also return forecasts for that are in the past."
}
}
}
} }
} }

View file

@ -166,5 +166,17 @@
} }
} }
} }
},
"services": {
"get_forecasts_radar": {
"name": "Obtenir les prévisions du radar",
"description": "Obtenez les prévisions météorologiques depuis le radar. Seules les précipitations sont disponibles.",
"fields": {
"include_past_forecasts": {
"name": "Inclure les prévisions passées",
"description": "Retourne également les prévisions qui sont dans le passé."
}
}
}
} }
} }

View file

@ -166,5 +166,17 @@
} }
} }
} }
},
"services": {
"get_forecasts_radar": {
"name": "Get forecast from the radar",
"description": "Weersverwachting van radar ophalen. Alleen neerslag is beschikbaar.",
"fields": {
"include_past_forecasts": {
"name": "Verleden weersvoorspellingen opnemen",
"description": "Geeft ook weersvoorspellingen uit het verleden."
}
}
}
} }
} }

View file

@ -1,16 +1,20 @@
"""Support for IRM KMI weather.""" """Support for IRM KMI weather."""
import copy
import logging import logging
from datetime import datetime
from typing import List from typing import List
import voluptuous as vol
from homeassistant.components.weather import (Forecast, WeatherEntity, from homeassistant.components.weather import (Forecast, WeatherEntity,
WeatherEntityFeature) WeatherEntityFeature)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (UnitOfPrecipitationDepth, UnitOfPressure, from homeassistant.const import (UnitOfPrecipitationDepth, UnitOfPressure,
UnitOfSpeed, UnitOfTemperature) UnitOfSpeed, UnitOfTemperature)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, SupportsResponse
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt
from . import CONF_USE_DEPRECATED_FORECAST, DOMAIN from . import CONF_USE_DEPRECATED_FORECAST, DOMAIN
from .const import (OPTION_DEPRECATED_FORECAST_DAILY, from .const import (OPTION_DEPRECATED_FORECAST_DAILY,
@ -25,11 +29,25 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
"""Set up the weather entry.""" """Set up the weather entry."""
add_services()
coordinator = hass.data[DOMAIN][entry.entry_id] coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([IrmKmiWeather(coordinator, entry)]) async_add_entities([IrmKmiWeather(coordinator, entry)])
def add_services() -> None:
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
"get_forecasts_radar",
cv.make_entity_service_schema({
vol.Optional("include_past_forecasts"): vol.Boolean()
}),
IrmKmiWeather.get_forecasts_radar_service.__name__,
supports_response=SupportsResponse.ONLY
)
class IrmKmiWeather(CoordinatorEntity, WeatherEntity): class IrmKmiWeather(CoordinatorEntity, WeatherEntity):
def __init__(self, def __init__(self,
@ -41,7 +59,6 @@ class IrmKmiWeather(CoordinatorEntity, WeatherEntity):
self._name = entry.title self._name = entry.title
self._attr_unique_id = entry.entry_id self._attr_unique_id = entry.entry_id
self._attr_device_info = coordinator.shared_device_info self._attr_device_info = coordinator.shared_device_info
self._deprecated_forecast_as = get_config_value(entry, CONF_USE_DEPRECATED_FORECAST) self._deprecated_forecast_as = get_config_value(entry, CONF_USE_DEPRECATED_FORECAST)
if self._deprecated_forecast_as != OPTION_DEPRECATED_FORECAST_NOT_USED: if self._deprecated_forecast_as != OPTION_DEPRECATED_FORECAST_NOT_USED:
@ -134,6 +151,20 @@ class IrmKmiWeather(CoordinatorEntity, WeatherEntity):
return [f for f in data if f.get('is_daytime')] return [f for f in data if f.get('is_daytime')]
def get_forecasts_radar_service(self, include_past_forecasts: bool) -> List[Forecast] | None:
"""
Forecast service based on data from the radar. Only contains datetime and precipitation amount.
The result always include the current 10 minutes interval, even if include_past_forecast is false.
: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)
return [f for f in self.coordinator.data.get('radar_forecast')
if include_past_forecasts or datetime.fromisoformat(f.get('datetime')) >= now]
@property @property
def extra_state_attributes(self) -> dict: def extra_state_attributes(self) -> dict:
"""Here to keep the DEPRECATED forecast attribute. """Here to keep the DEPRECATED forecast attribute.

View file

@ -14,7 +14,8 @@ from custom_components.irm_kmi.api import (IrmKmiApiError,
IrmKmiApiParametersError) IrmKmiApiParametersError)
from custom_components.irm_kmi.const import ( from custom_components.irm_kmi.const import (
CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST, DOMAIN, CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST, DOMAIN,
OPTION_DEPRECATED_FORECAST_NOT_USED, OPTION_STYLE_STD, OPTION_DEPRECATED_FORECAST_TWICE_DAILY) OPTION_DEPRECATED_FORECAST_NOT_USED,
OPTION_DEPRECATED_FORECAST_TWICE_DAILY, OPTION_STYLE_STD)
def get_api_data(fixture: str) -> dict: def get_api_data(fixture: str) -> dict:

View file

@ -1408,7 +1408,7 @@
{ {
"time": "2023-12-26T17:40:00+01:00", "time": "2023-12-26T17:40:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261650&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720", "uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261650&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0, "value": 0.1,
"position": 0, "position": 0,
"positionLower": 0, "positionLower": 0,
"positionHigher": 0 "positionHigher": 0
@ -1416,7 +1416,7 @@
{ {
"time": "2023-12-26T17:50:00+01:00", "time": "2023-12-26T17:50:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261700&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720", "uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261700&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0, "value": 0.01,
"position": 0, "position": 0,
"positionLower": 0, "positionLower": 0,
"positionHigher": 0 "positionHigher": 0
@ -1424,7 +1424,7 @@
{ {
"time": "2023-12-26T18:00:00+01:00", "time": "2023-12-26T18:00:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261710&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720", "uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261710&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0, "value": 0.12,
"position": 0, "position": 0,
"positionLower": 0, "positionLower": 0,
"positionHigher": 0 "positionHigher": 0
@ -1432,7 +1432,7 @@
{ {
"time": "2023-12-26T18:10:00+01:00", "time": "2023-12-26T18:10:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261720&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720", "uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261720&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0, "value": 1.2,
"position": 0, "position": 0,
"positionLower": 0, "positionLower": 0,
"positionHigher": 0 "positionHigher": 0
@ -1440,7 +1440,7 @@
{ {
"time": "2023-12-26T18:20:00+01:00", "time": "2023-12-26T18:20:00+01:00",
"uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261730&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720", "uri": "https:\/\/app.meteo.be\/services\/appv4\/?s=getIncaImage&i=202312261730&f=2&k=4a71be18d6cb09f98c49c53f59902f8c&d=202312261720",
"value": 0, "value": 2,
"position": 0, "position": 0,
"positionLower": 0, "positionLower": 0,
"positionHigher": 0 "positionHigher": 0

View file

@ -8,8 +8,9 @@ from homeassistant.core import HomeAssistant
from pytest_homeassistant_custom_component.common import MockConfigEntry from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.irm_kmi.coordinator import IrmKmiCoordinator from custom_components.irm_kmi.coordinator import IrmKmiCoordinator
from custom_components.irm_kmi.data import CurrentWeatherData, IrmKmiForecast, ProcessedCoordinatorData, \ from custom_components.irm_kmi.data import (CurrentWeatherData, IrmKmiForecast,
RadarAnimationData ProcessedCoordinatorData,
RadarAnimationData)
from custom_components.irm_kmi.pollen import PollenParser from custom_components.irm_kmi.pollen import PollenParser
from tests.conftest import get_api_data from tests.conftest import get_api_data
@ -177,3 +178,24 @@ async def test_refresh_succeed_even_when_pollen_and_radar_fail(
assert result.get('animation').get('hint') == "This will remain unchanged" assert result.get('animation').get('hint') == "This will remain unchanged"
assert result.get('pollen') == {'foo': 'bar'} assert result.get('pollen') == {'foo': 'bar'}
def test_radar_forecast() -> None:
api_data = get_api_data("forecast.json")
result = IrmKmiCoordinator.radar_list_to_forecast(api_data.get('animation'))
expected = [
Forecast(datetime="2023-12-26T17:00:00+01:00", native_precipitation=0),
Forecast(datetime="2023-12-26T17:10:00+01:00", native_precipitation=0),
Forecast(datetime="2023-12-26T17:20:00+01:00", native_precipitation=0),
Forecast(datetime="2023-12-26T17:30:00+01:00", native_precipitation=0),
Forecast(datetime="2023-12-26T17:40:00+01:00", native_precipitation=0.1),
Forecast(datetime="2023-12-26T17:50:00+01:00", native_precipitation=0.01),
Forecast(datetime="2023-12-26T18:00:00+01:00", native_precipitation=0.12),
Forecast(datetime="2023-12-26T18:10:00+01:00", native_precipitation=1.2),
Forecast(datetime="2023-12-26T18:20:00+01:00", native_precipitation=2),
Forecast(datetime="2023-12-26T18:30:00+01:00", native_precipitation=0),
Forecast(datetime="2023-12-26T18:40:00+01:00", native_precipitation=0)
]
assert expected == result

View file

@ -9,6 +9,8 @@ from homeassistant.core import HomeAssistant
from pytest_homeassistant_custom_component.common import MockConfigEntry from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.irm_kmi import IrmKmiCoordinator, IrmKmiWeather from custom_components.irm_kmi import IrmKmiCoordinator, IrmKmiWeather
from custom_components.irm_kmi.data import ProcessedCoordinatorData
from tests.conftest import get_api_data
@freeze_time(datetime.fromisoformat("2023-12-28T15:30:00+01:00")) @freeze_time(datetime.fromisoformat("2023-12-28T15:30:00+01:00"))
@ -91,3 +93,43 @@ async def test_forecast_attribute_same_as_service_call(
result_forecast: List[Forecast] = weather.extra_state_attributes['forecast'] result_forecast: List[Forecast] = weather.extra_state_attributes['forecast']
assert result_service == result_forecast assert result_service == result_forecast
@freeze_time(datetime.fromisoformat("2023-12-26T17:58:03+01:00"))
async def test_radar_forecast_service(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry
):
hass.config.time_zone = 'Europe/Brussels'
coordinator = IrmKmiCoordinator(hass, mock_config_entry)
api_data = get_api_data("forecast.json")
data = IrmKmiCoordinator.radar_list_to_forecast(api_data.get('animation'))
coordinator.data = ProcessedCoordinatorData(
radar_forecast=data
)
weather = IrmKmiWeather(coordinator, mock_config_entry)
result_service: List[Forecast] = weather.get_forecasts_radar_service(False)
expected = [
Forecast(datetime="2023-12-26T17:00:00+01:00", native_precipitation=0),
Forecast(datetime="2023-12-26T17:10:00+01:00", native_precipitation=0),
Forecast(datetime="2023-12-26T17:20:00+01:00", native_precipitation=0),
Forecast(datetime="2023-12-26T17:30:00+01:00", native_precipitation=0),
Forecast(datetime="2023-12-26T17:40:00+01:00", native_precipitation=0.1),
Forecast(datetime="2023-12-26T17:50:00+01:00", native_precipitation=0.01),
Forecast(datetime="2023-12-26T18:00:00+01:00", native_precipitation=0.12),
Forecast(datetime="2023-12-26T18:10:00+01:00", native_precipitation=1.2),
Forecast(datetime="2023-12-26T18:20:00+01:00", native_precipitation=2),
Forecast(datetime="2023-12-26T18:30:00+01:00", native_precipitation=0),
Forecast(datetime="2023-12-26T18:40:00+01:00", native_precipitation=0)
]
assert result_service == expected[5:]
result_service: List[Forecast] = weather.get_forecasts_radar_service(True)
assert result_service == expected