Merge pull request #6 from jdejaegh/attributes

Update attributes
This commit is contained in:
Jules 2024-01-20 15:04:15 +01:00 committed by GitHub
commit 2aa41f0155
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 35 additions and 18 deletions

View file

@ -87,6 +87,7 @@ Each element in the list has the following attributes:
* `text: str`: language specific additional information about the warning * `text: str`: language specific additional information about the warning
* `starts_at: datetime`: time at which the warning starts being relevant * `starts_at: datetime`: time at which the warning starts being relevant
* `ends_at: datetime`: time at which the warning stops being relevant * `ends_at: datetime`: time at which the warning stops being relevant
* `is_active: bool`: `true` if `starts_at` < now < `ends_at`
The following table summarizes the different known warning types. Other warning types may be returned and will have `unknown` as slug. Feel free to open an issue with the id and the English friendly name to have it added to this integration. The following table summarizes the different known warning types. Other warning types may be returned and will have `unknown` as slug. Feel free to open an issue with the id and the English friendly name to have it added to this integration.
@ -104,6 +105,9 @@ The following table summarizes the different known warning types. Other warning
| storm_surge | 15 | Storm surge, Marée forte, Stormtij, Sturmflut | | storm_surge | 15 | Storm surge, Marée forte, Stormtij, Sturmflut |
| coldspell | 17 | Coldspell, Vague de froid, Koude, Koude | | coldspell | 17 | Coldspell, Vague de froid, Koude, Koude |
The sensor has an attribute called `active_warnings_friendly_names`, holding a comma separated list of the friendly names
of the currently active warnings (e.g. `Fog, Ice or snow`). There is no particular order for the list.
## Disclaimer ## Disclaimer
This is a personal project and isn't in any way affiliated with, sponsored or endorsed by [The Royal Meteorological This is a personal project and isn't in any way affiliated with, sponsored or endorsed by [The Royal Meteorological

View file

@ -59,4 +59,12 @@ class IrmKmiWarning(CoordinatorEntity, BinarySensorEntity):
def extra_state_attributes(self) -> dict: def extra_state_attributes(self) -> dict:
"""Return the camera state attributes.""" """Return the camera state attributes."""
attrs = {"warnings": self.coordinator.data.get('warnings', [])} attrs = {"warnings": self.coordinator.data.get('warnings', [])}
now = datetime.datetime.now(tz=pytz.timezone(self.hass.config.time_zone))
for warning in attrs['warnings']:
warning['is_active'] = warning.get('starts_at') < now < warning.get('ends_at')
attrs["active_warnings_friendly_names"] = ", ".join([warning['friendly_name'] for warning in attrs['warnings']
if warning['is_active']])
return attrs return attrs

View file

@ -71,7 +71,6 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
raise UpdateFailed(f"Error communicating with API: {err}") raise UpdateFailed(f"Error communicating with API: {err}")
if api_data.get('cityName', None) in OUT_OF_BENELUX: if api_data.get('cityName', None) in OUT_OF_BENELUX:
# TODO create a repair when this triggers
_LOGGER.info(f"Config state: {self._config_entry.state}") _LOGGER.info(f"Config state: {self._config_entry.state}")
_LOGGER.error(f"The zone {self._zone} is now out of Benelux and forecast is only available in Benelux." _LOGGER.error(f"The zone {self._zone} is now out of Benelux and forecast is only available in Benelux."
f"Associated device is now disabled. Move the zone back in Benelux and re-enable to fix " f"Associated device is now disabled. Move the zone back in Benelux and re-enable to fix "
@ -130,7 +129,7 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
"""From the API data, create the object that will be used in the entities""" """From the API data, create the object that will be used in the entities"""
return ProcessedCoordinatorData( return ProcessedCoordinatorData(
current_weather=IrmKmiCoordinator.current_weather_from_data(api_data), current_weather=IrmKmiCoordinator.current_weather_from_data(api_data),
daily_forecast=IrmKmiCoordinator.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')),
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'))
@ -257,8 +256,7 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
return forecasts return forecasts
@staticmethod def daily_list_to_forecast(self, data: List[dict] | None) -> List[Forecast] | None:
def daily_list_to_forecast(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:
return None return None
@ -295,8 +293,7 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
precipitation_probability=f.get('precipChance', None), precipitation_probability=f.get('precipChance', None),
wind_bearing=f.get('wind', {}).get('dirText', {}).get('en'), wind_bearing=f.get('wind', {}).get('dirText', {}).get('en'),
is_daytime=is_daytime, is_daytime=is_daytime,
text_fr=f.get('text', {}).get('fr'), text=f.get('text', {}).get(self.hass.config.language, ""),
text_nl=f.get('text', {}).get('nl')
) )
forecasts.append(forecast) forecasts.append(forecast)
if is_daytime or idx == 0: if is_daytime or idx == 0:
@ -347,10 +344,10 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
dark_mode=self._dark_mode, dark_mode=self._dark_mode,
tz=self.hass.config.time_zone) tz=self.hass.config.time_zone)
def warnings_from_data(self, warning_data: list | None) -> List[WarningData] | None: def warnings_from_data(self, warning_data: list | None) -> List[WarningData]:
"""Create a list of warning data instances based on the api data""" """Create a list of warning data instances based on the api data"""
if warning_data is None or not isinstance(warning_data, list) or len(warning_data) == 0: if warning_data is None or not isinstance(warning_data, list) or len(warning_data) == 0:
return None return []
result = list() result = list()
for data in warning_data: for data in warning_data:
@ -379,4 +376,4 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
) )
) )
return result if len(result) > 0 else None return result if len(result) > 0 else []

View file

@ -9,9 +9,7 @@ class IrmKmiForecast(Forecast):
"""Forecast class with additional attributes for IRM KMI""" """Forecast class with additional attributes for IRM KMI"""
# TODO: add condition_2 as well and evolution to match data from the API? # TODO: add condition_2 as well and evolution to match data from the API?
# TODO: remove the _fr and _nl to have only one 'text' attribute text: str | None
text_fr: str | None
text_nl: str | None
class CurrentWeatherData(TypedDict, total=False): class CurrentWeatherData(TypedDict, total=False):
@ -63,4 +61,4 @@ class ProcessedCoordinatorData(TypedDict, total=False):
hourly_forecast: List[Forecast] | None hourly_forecast: List[Forecast] | None
daily_forecast: List[IrmKmiForecast] | None daily_forecast: List[IrmKmiForecast] | None
animation: RadarAnimationData animation: RadarAnimationData
warnings: List[WarningData] | None warnings: List[WarningData]

View file

@ -66,7 +66,8 @@
"dayNight": "d", "dayNight": "d",
"text": { "text": {
"nl": "Foo", "nl": "Foo",
"fr": "Bar" "fr": "Bar",
"en": "Hey!"
}, },
"dawnRiseSeconds": "31440", "dawnRiseSeconds": "31440",
"dawnSetSeconds": "60180", "dawnSetSeconds": "60180",

View file

@ -25,3 +25,8 @@ async def test_warning_data(
assert warning.is_on assert warning.is_on
assert len(warning.extra_state_attributes['warnings']) == 2 assert len(warning.extra_state_attributes['warnings']) == 2
for w in warning.extra_state_attributes['warnings']:
assert w['is_active']
assert warning.extra_state_attributes['active_warnings_friendly_names'] == "Fog, Ice or snow"

View file

@ -82,9 +82,14 @@ def test_current_weather_nl() -> None:
@freeze_time(datetime.fromisoformat('2023-12-26T18:30:00.028724')) @freeze_time(datetime.fromisoformat('2023-12-26T18:30:00.028724'))
def test_daily_forecast() -> None: async def test_daily_forecast(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry
) -> None:
api_data = get_api_data("forecast.json").get('for', {}).get('daily') api_data = get_api_data("forecast.json").get('for', {}).get('daily')
result = IrmKmiCoordinator.daily_list_to_forecast(api_data)
coordinator = IrmKmiCoordinator(hass, mock_config_entry)
result = coordinator.daily_list_to_forecast(api_data)
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 8 assert len(result) == 8
@ -100,8 +105,7 @@ def test_daily_forecast() -> None:
precipitation_probability=0, precipitation_probability=0,
wind_bearing='S', wind_bearing='S',
is_daytime=True, is_daytime=True,
text_fr='Bar', text='Hey!',
text_nl='Foo'
) )
assert result[1] == expected assert result[1] == expected