From 44dd78c0771f7bebd35a31daebcefa2e5bcfe460 Mon Sep 17 00:00:00 2001 From: Jules Dejaeghere Date: Sat, 20 Jan 2024 14:29:20 +0100 Subject: [PATCH 1/3] Add attributes to warning binary sensor --- README.md | 4 ++++ custom_components/irm_kmi/binary_sensor.py | 8 ++++++++ custom_components/irm_kmi/coordinator.py | 6 +++--- custom_components/irm_kmi/data.py | 2 +- tests/test_binary_sensor.py | 5 +++++ 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 113612b..4800149 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Each element in the list has the following attributes: * `text: str`: language specific additional information about the warning * `starts_at: datetime`: time at which the warning starts 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. @@ -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 | | 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 This is a personal project and isn't in any way affiliated with, sponsored or endorsed by [The Royal Meteorological diff --git a/custom_components/irm_kmi/binary_sensor.py b/custom_components/irm_kmi/binary_sensor.py index 5442512..1e490d3 100644 --- a/custom_components/irm_kmi/binary_sensor.py +++ b/custom_components/irm_kmi/binary_sensor.py @@ -59,4 +59,12 @@ class IrmKmiWarning(CoordinatorEntity, BinarySensorEntity): def extra_state_attributes(self) -> dict: """Return the camera state attributes.""" 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 diff --git a/custom_components/irm_kmi/coordinator.py b/custom_components/irm_kmi/coordinator.py index f7cfd39..b344e41 100644 --- a/custom_components/irm_kmi/coordinator.py +++ b/custom_components/irm_kmi/coordinator.py @@ -347,10 +347,10 @@ class IrmKmiCoordinator(DataUpdateCoordinator): dark_mode=self._dark_mode, 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""" if warning_data is None or not isinstance(warning_data, list) or len(warning_data) == 0: - return None + return [] result = list() for data in warning_data: @@ -379,4 +379,4 @@ class IrmKmiCoordinator(DataUpdateCoordinator): ) ) - return result if len(result) > 0 else None + return result if len(result) > 0 else [] diff --git a/custom_components/irm_kmi/data.py b/custom_components/irm_kmi/data.py index 4a045b1..6267ce3 100644 --- a/custom_components/irm_kmi/data.py +++ b/custom_components/irm_kmi/data.py @@ -63,4 +63,4 @@ class ProcessedCoordinatorData(TypedDict, total=False): hourly_forecast: List[Forecast] | None daily_forecast: List[IrmKmiForecast] | None animation: RadarAnimationData - warnings: List[WarningData] | None + warnings: List[WarningData] diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 276c82a..7f320a6 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -25,3 +25,8 @@ async def test_warning_data( assert warning.is_on 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" From a401bc172ab155fe26ba6313afc4685259f19b7c Mon Sep 17 00:00:00 2001 From: Jules Dejaeghere Date: Sat, 20 Jan 2024 14:39:42 +0100 Subject: [PATCH 2/3] Remove text_fr and text_nl from forecast and add text instead (auto-detect instance language) --- custom_components/irm_kmi/coordinator.py | 8 +++----- custom_components/irm_kmi/data.py | 4 +--- tests/fixtures/forecast.json | 3 ++- tests/test_coordinator.py | 12 ++++++++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/custom_components/irm_kmi/coordinator.py b/custom_components/irm_kmi/coordinator.py index b344e41..bf2246d 100644 --- a/custom_components/irm_kmi/coordinator.py +++ b/custom_components/irm_kmi/coordinator.py @@ -130,7 +130,7 @@ class IrmKmiCoordinator(DataUpdateCoordinator): """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=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')), animation=await self._async_animation_data(api_data=api_data), warnings=self.warnings_from_data(api_data.get('for', {}).get('warning')) @@ -257,8 +257,7 @@ class IrmKmiCoordinator(DataUpdateCoordinator): return forecasts - @staticmethod - def daily_list_to_forecast(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""" if data is None or not isinstance(data, list) or len(data) == 0: return None @@ -295,8 +294,7 @@ class IrmKmiCoordinator(DataUpdateCoordinator): precipitation_probability=f.get('precipChance', None), wind_bearing=f.get('wind', {}).get('dirText', {}).get('en'), is_daytime=is_daytime, - text_fr=f.get('text', {}).get('fr'), - text_nl=f.get('text', {}).get('nl') + text=f.get('text', {}).get(self.hass.config.language, ""), ) forecasts.append(forecast) if is_daytime or idx == 0: diff --git a/custom_components/irm_kmi/data.py b/custom_components/irm_kmi/data.py index 6267ce3..02b3989 100644 --- a/custom_components/irm_kmi/data.py +++ b/custom_components/irm_kmi/data.py @@ -9,9 +9,7 @@ class IrmKmiForecast(Forecast): """Forecast class with additional attributes for IRM KMI""" # 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_fr: str | None - text_nl: str | None + text: str | None class CurrentWeatherData(TypedDict, total=False): diff --git a/tests/fixtures/forecast.json b/tests/fixtures/forecast.json index c9c118d..de81f9e 100644 --- a/tests/fixtures/forecast.json +++ b/tests/fixtures/forecast.json @@ -66,7 +66,8 @@ "dayNight": "d", "text": { "nl": "Foo", - "fr": "Bar" + "fr": "Bar", + "en": "Hey!" }, "dawnRiseSeconds": "31440", "dawnSetSeconds": "60180", diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index 76374da..5d786bc 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -82,9 +82,14 @@ def test_current_weather_nl() -> None: @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') - 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 len(result) == 8 @@ -100,8 +105,7 @@ def test_daily_forecast() -> None: precipitation_probability=0, wind_bearing='S', is_daytime=True, - text_fr='Bar', - text_nl='Foo' + text='Hey!', ) assert result[1] == expected From 6aef5ffa19a87556547ba564aeb3b427d9f6c79a Mon Sep 17 00:00:00 2001 From: Jules Dejaeghere Date: Sat, 20 Jan 2024 14:50:59 +0100 Subject: [PATCH 3/3] Remove outdated TODO --- custom_components/irm_kmi/coordinator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/irm_kmi/coordinator.py b/custom_components/irm_kmi/coordinator.py index bf2246d..4121a8c 100644 --- a/custom_components/irm_kmi/coordinator.py +++ b/custom_components/irm_kmi/coordinator.py @@ -71,7 +71,6 @@ class IrmKmiCoordinator(DataUpdateCoordinator): raise UpdateFailed(f"Error communicating with API: {err}") 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.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 "