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.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import (TimestampDataUpdateCoordinator,
UpdateFailed)
from homeassistant.helpers.update_coordinator import (
TimestampDataUpdateCoordinator, UpdateFailed)
from homeassistant.util.dt import utcnow
from .api import IrmKmiApiClient, IrmKmiApiError
@ -168,6 +168,7 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
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')),
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')),
pollen=await self._async_pollen_data(api_data=api_data)
@ -317,6 +318,21 @@ class IrmKmiCoordinator(TimestampDataUpdateCoordinator):
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:
"""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:

View file

@ -60,6 +60,7 @@ class ProcessedCoordinatorData(TypedDict, total=False):
current_weather: CurrentWeatherData
hourly_forecast: List[Forecast] | None
daily_forecast: List[IrmKmiForecast] | None
radar_forecast: List[Forecast] | None
animation: RadarAnimationData
warnings: List[WarningData]
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."""
import copy
import logging
from datetime import datetime
from typing import List
import voluptuous as vol
from homeassistant.components.weather import (Forecast, WeatherEntity,
WeatherEntityFeature)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (UnitOfPrecipitationDepth, UnitOfPressure,
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.update_coordinator import CoordinatorEntity
from homeassistant.util import dt
from . import CONF_USE_DEPRECATED_FORECAST, DOMAIN
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):
"""Set up the weather entry."""
add_services()
coordinator = hass.data[DOMAIN][entry.entry_id]
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):
def __init__(self,
@ -41,7 +59,6 @@ class IrmKmiWeather(CoordinatorEntity, WeatherEntity):
self._name = entry.title
self._attr_unique_id = entry.entry_id
self._attr_device_info = coordinator.shared_device_info
self._deprecated_forecast_as = get_config_value(entry, CONF_USE_DEPRECATED_FORECAST)
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')]
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
def extra_state_attributes(self) -> dict:
"""Here to keep the DEPRECATED forecast attribute.

View file

@ -14,7 +14,8 @@ 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_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:

View file

@ -1408,7 +1408,7 @@
{
"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",
"value": 0,
"value": 0.1,
"position": 0,
"positionLower": 0,
"positionHigher": 0
@ -1416,7 +1416,7 @@
{
"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",
"value": 0,
"value": 0.01,
"position": 0,
"positionLower": 0,
"positionHigher": 0
@ -1424,7 +1424,7 @@
{
"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",
"value": 0,
"value": 0.12,
"position": 0,
"positionLower": 0,
"positionHigher": 0
@ -1432,7 +1432,7 @@
{
"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",
"value": 0,
"value": 1.2,
"position": 0,
"positionLower": 0,
"positionHigher": 0
@ -1440,7 +1440,7 @@
{
"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",
"value": 0,
"value": 2,
"position": 0,
"positionLower": 0,
"positionHigher": 0

View file

@ -8,8 +8,9 @@ from homeassistant.core import HomeAssistant
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.irm_kmi.coordinator import IrmKmiCoordinator
from custom_components.irm_kmi.data import CurrentWeatherData, IrmKmiForecast, ProcessedCoordinatorData, \
RadarAnimationData
from custom_components.irm_kmi.data import (CurrentWeatherData, IrmKmiForecast,
ProcessedCoordinatorData,
RadarAnimationData)
from custom_components.irm_kmi.pollen import PollenParser
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('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 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"))
@ -91,3 +93,43 @@ async def test_forecast_attribute_same_as_service_call(
result_forecast: List[Forecast] = weather.extra_state_attributes['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