diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index dfb556deeb5..4ac2518ffb6 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -16,6 +16,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( _LOGGER, + ALL_MATCH_REGEX, + CONF_AREA_FILTER, CONF_FILTER_CORONA, CONF_HEADLINE_FILTER, CONF_REGIONS, @@ -42,8 +44,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: new_data.pop(CONF_FILTER_CORONA, None) hass.config_entries.async_update_entry(entry, data=new_data) + if CONF_AREA_FILTER not in entry.data: + new_data = {**entry.data, CONF_AREA_FILTER: ALL_MATCH_REGEX} + hass.config_entries.async_update_entry(entry, data=new_data) + coordinator = NINADataUpdateCoordinator( - hass, regions, entry.data[CONF_HEADLINE_FILTER] + hass, + regions, + entry.data[CONF_HEADLINE_FILTER], + entry.data[CONF_AREA_FILTER], ) await coordinator.async_config_entry_first_refresh() @@ -77,6 +86,7 @@ class NinaWarningData: sender: str severity: str recommended_actions: str + affected_areas: str sent: str start: str expires: str @@ -89,12 +99,17 @@ class NINADataUpdateCoordinator( """Class to manage fetching NINA data API.""" def __init__( - self, hass: HomeAssistant, regions: dict[str, str], headline_filter: str + self, + hass: HomeAssistant, + regions: dict[str, str], + headline_filter: str, + area_filter: str, ) -> None: """Initialize.""" self._regions: dict[str, str] = regions self._nina: Nina = Nina(async_get_clientsession(hass)) self.headline_filter: str = headline_filter + self.area_filter: str = area_filter for region in regions: self._nina.addRegion(region) @@ -147,6 +162,21 @@ class NINADataUpdateCoordinator( if re.search( self.headline_filter, raw_warn.headline, flags=re.IGNORECASE ): + _LOGGER.debug( + f"Ignore warning ({raw_warn.id}) by headline filter ({self.headline_filter}) with headline: {raw_warn.headline}" + ) + continue + + affected_areas_string: str = ", ".join( + [str(area) for area in raw_warn.affected_areas] + ) + + if not re.search( + self.area_filter, affected_areas_string, flags=re.IGNORECASE + ): + _LOGGER.debug( + f"Ignore warning ({raw_warn.id}) by area filter ({self.area_filter}) with area: {affected_areas_string}" + ) continue warning_data: NinaWarningData = NinaWarningData( @@ -156,6 +186,7 @@ class NINADataUpdateCoordinator( raw_warn.sender, raw_warn.severity, " ".join([str(action) for action in raw_warn.recommended_actions]), + affected_areas_string, raw_warn.sent or "", raw_warn.start or "", raw_warn.expires or "", diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index 24d6d35d0e8..19f802f1cec 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -14,6 +14,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NINADataUpdateCoordinator from .const import ( + ATTR_AFFECTED_AREAS, ATTR_DESCRIPTION, ATTR_EXPIRES, ATTR_HEADLINE, @@ -73,7 +74,7 @@ class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEnti @property def is_on(self) -> bool: """Return the state of the sensor.""" - if not len(self.coordinator.data[self._region]) > self._warning_index: + if len(self.coordinator.data[self._region]) <= self._warning_index: return False data = self.coordinator.data[self._region][self._warning_index] @@ -94,6 +95,7 @@ class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEnti ATTR_SENDER: data.sender, ATTR_SEVERITY: data.severity, ATTR_RECOMMENDED_ACTIONS: data.recommended_actions, + ATTR_AFFECTED_AREAS: data.affected_areas, ATTR_ID: data.id, ATTR_SENT: data.sent, ATTR_START: data.start, diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index d41fa6dee3e..9c6de40ac6b 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity_registry import ( from .const import ( _LOGGER, + CONF_AREA_FILTER, CONF_HEADLINE_FILTER, CONF_MESSAGE_SLOTS, CONF_REGIONS, @@ -263,6 +264,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_HEADLINE_FILTER, default=self.data[CONF_HEADLINE_FILTER], ): cv.string, + vol.Optional( + CONF_AREA_FILTER, + default=self.data[CONF_AREA_FILTER], + ): cv.string, } ), errors=errors, diff --git a/homeassistant/components/nina/const.py b/homeassistant/components/nina/const.py index 36096d97dc1..198e21c2689 100644 --- a/homeassistant/components/nina/const.py +++ b/homeassistant/components/nina/const.py @@ -12,17 +12,20 @@ SCAN_INTERVAL: timedelta = timedelta(minutes=5) DOMAIN: str = "nina" NO_MATCH_REGEX: str = "/(?!)/" +ALL_MATCH_REGEX: str = ".*" CONF_REGIONS: str = "regions" CONF_MESSAGE_SLOTS: str = "slots" CONF_FILTER_CORONA: str = "corona_filter" # deprecated CONF_HEADLINE_FILTER: str = "headline_filter" +CONF_AREA_FILTER: str = "area_filter" ATTR_HEADLINE: str = "headline" ATTR_DESCRIPTION: str = "description" ATTR_SENDER: str = "sender" ATTR_SEVERITY: str = "severity" ATTR_RECOMMENDED_ACTIONS: str = "recommended_actions" +ATTR_AFFECTED_AREAS: str = "affected_areas" ATTR_ID: str = "id" ATTR_SENT: str = "sent" ATTR_START: str = "start" diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json index e145f5ea8ca..5e0393d024f 100644 --- a/homeassistant/components/nina/strings.json +++ b/homeassistant/components/nina/strings.json @@ -36,7 +36,8 @@ "_r_to_u": "[%key:component::nina::config::step::user::data::_r_to_u%]", "_v_to_z": "[%key:component::nina::config::step::user::data::_v_to_z%]", "slots": "[%key:component::nina::config::step::user::data::slots%]", - "headline_filter": "[%key:component::nina::config::step::user::data::headline_filter%]" + "headline_filter": "[%key:component::nina::config::step::user::data::headline_filter%]", + "area_filter": "Whitelist regex to filter warnings based on affected areas" } } }, diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index c6fd5bdd830..8532415c6b1 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -6,6 +6,7 @@ from unittest.mock import patch from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.nina.const import ( + ATTR_AFFECTED_AREAS, ATTR_DESCRIPTION, ATTR_EXPIRES, ATTR_HEADLINE, @@ -38,6 +39,13 @@ ENTRY_DATA_NO_CORONA: dict[str, Any] = { "regions": {"083350000000": "Aach, Stadt"}, } +ENTRY_DATA_NO_AREA: dict[str, Any] = { + "slots": 5, + "corona_filter": False, + "area_filter": ".*nagold.*", + "regions": {"083350000000": "Aach, Stadt"}, +} + async def test_sensors(hass: HomeAssistant) -> None: """Test the creation and values of the NINA sensors.""" @@ -70,6 +78,10 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w1.attributes.get(ATTR_SENDER) == "Deutscher Wetterdienst" assert state_w1.attributes.get(ATTR_SEVERITY) == "Minor" assert state_w1.attributes.get(ATTR_RECOMMENDED_ACTIONS) == "" + assert ( + state_w1.attributes.get(ATTR_AFFECTED_AREAS) + == "Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere." + ) assert state_w1.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" assert state_w1.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" @@ -87,6 +99,7 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w2.attributes.get(ATTR_SENDER) is None assert state_w2.attributes.get(ATTR_SEVERITY) is None assert state_w2.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w2.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w2.attributes.get(ATTR_ID) is None assert state_w2.attributes.get(ATTR_SENT) is None assert state_w2.attributes.get(ATTR_START) is None @@ -104,6 +117,7 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w3.attributes.get(ATTR_SENDER) is None assert state_w3.attributes.get(ATTR_SEVERITY) is None assert state_w3.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w3.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w3.attributes.get(ATTR_ID) is None assert state_w3.attributes.get(ATTR_SENT) is None assert state_w3.attributes.get(ATTR_START) is None @@ -121,6 +135,7 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w4.attributes.get(ATTR_SENDER) is None assert state_w4.attributes.get(ATTR_SEVERITY) is None assert state_w4.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w4.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w4.attributes.get(ATTR_ID) is None assert state_w4.attributes.get(ATTR_SENT) is None assert state_w4.attributes.get(ATTR_START) is None @@ -138,6 +153,7 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w5.attributes.get(ATTR_SENDER) is None assert state_w5.attributes.get(ATTR_SEVERITY) is None assert state_w5.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w5.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w5.attributes.get(ATTR_ID) is None assert state_w5.attributes.get(ATTR_SENT) is None assert state_w5.attributes.get(ATTR_START) is None @@ -184,6 +200,10 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: state_w1.attributes.get(ATTR_RECOMMENDED_ACTIONS) == "Waschen sich regelmäßig und gründlich die Hände." ) + assert ( + state_w1.attributes.get(ATTR_AFFECTED_AREAS) + == "Bundesland: Freie Hansestadt Bremen, Land Berlin, Land Hessen, Land Nordrhein-Westfalen, Land Brandenburg, Freistaat Bayern, Land Mecklenburg-Vorpommern, Land Rheinland-Pfalz, Freistaat Sachsen, Land Schleswig-Holstein, Freie und Hansestadt Hamburg, Freistaat Thüringen, Land Niedersachsen, Land Saarland, Land Sachsen-Anhalt, Land Baden-Württemberg" + ) assert state_w1.attributes.get(ATTR_ID) == "mow.DE-BW-S-SE018-20211102-18-001" assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T20:07:16+01:00" assert state_w1.attributes.get(ATTR_START) == "" @@ -201,6 +221,10 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: state_w2.attributes.get(ATTR_DESCRIPTION) == "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden." ) + assert ( + state_w2.attributes.get(ATTR_AFFECTED_AREAS) + == "Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere." + ) assert state_w2.attributes.get(ATTR_SENDER) == "Deutscher Wetterdienst" assert state_w2.attributes.get(ATTR_SEVERITY) == "Minor" assert state_w2.attributes.get(ATTR_RECOMMENDED_ACTIONS) == "" @@ -221,6 +245,7 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w3.attributes.get(ATTR_SENDER) is None assert state_w3.attributes.get(ATTR_SEVERITY) is None assert state_w3.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w3.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w3.attributes.get(ATTR_ID) is None assert state_w3.attributes.get(ATTR_SENT) is None assert state_w3.attributes.get(ATTR_START) is None @@ -238,6 +263,7 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w4.attributes.get(ATTR_SENDER) is None assert state_w4.attributes.get(ATTR_SEVERITY) is None assert state_w4.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w4.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w4.attributes.get(ATTR_ID) is None assert state_w4.attributes.get(ATTR_SENT) is None assert state_w4.attributes.get(ATTR_START) is None @@ -255,6 +281,7 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w5.attributes.get(ATTR_SENDER) is None assert state_w5.attributes.get(ATTR_SEVERITY) is None assert state_w5.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w5.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w5.attributes.get(ATTR_ID) is None assert state_w5.attributes.get(ATTR_SENT) is None assert state_w5.attributes.get(ATTR_START) is None @@ -262,3 +289,63 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert entry_w5.unique_id == "083350000000-5" assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + +async def test_sensors_with_area_filter(hass: HomeAssistant) -> None: + """Test the creation and values of the NINA sensors with an area filter.""" + + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + wraps=mocked_request_function, + ): + conf_entry: MockConfigEntry = MockConfigEntry( + domain=DOMAIN, title="NINA", data=ENTRY_DATA_NO_AREA + ) + + entity_registry: er = er.async_get(hass) + conf_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(conf_entry.entry_id) + await hass.async_block_till_done() + + assert conf_entry.state == ConfigEntryState.LOADED + + state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") + entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") + + assert state_w1.state == STATE_ON + + assert entry_w1.unique_id == "083350000000-1" + assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w2 = hass.states.get("binary_sensor.warning_aach_stadt_2") + entry_w2 = entity_registry.async_get("binary_sensor.warning_aach_stadt_2") + + assert state_w2.state == STATE_OFF + + assert entry_w2.unique_id == "083350000000-2" + assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w3 = hass.states.get("binary_sensor.warning_aach_stadt_3") + entry_w3 = entity_registry.async_get("binary_sensor.warning_aach_stadt_3") + + assert state_w3.state == STATE_OFF + + assert entry_w3.unique_id == "083350000000-3" + assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w4 = hass.states.get("binary_sensor.warning_aach_stadt_4") + entry_w4 = entity_registry.async_get("binary_sensor.warning_aach_stadt_4") + + assert state_w4.state == STATE_OFF + + assert entry_w4.unique_id == "083350000000-4" + assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w5 = hass.states.get("binary_sensor.warning_aach_stadt_5") + entry_w5 = entity_registry.async_get("binary_sensor.warning_aach_stadt_5") + + assert state_w5.state == STATE_OFF + + assert entry_w5.unique_id == "083350000000-5" + assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index 194f0298dd5..aad24691f42 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -10,6 +10,7 @@ from pynina import ApiError from homeassistant import data_entry_flow from homeassistant.components.nina.const import ( + CONF_AREA_FILTER, CONF_HEADLINE_FILTER, CONF_MESSAGE_SLOTS, CONF_REGIONS, @@ -38,6 +39,7 @@ DUMMY_DATA: dict[str, Any] = { CONST_REGION_R_TO_U: ["072320000000_0", "072320000000_1"], CONST_REGION_V_TO_Z: ["081270000000_0", "081270000000_1"], CONF_HEADLINE_FILTER: ".*corona.*", + CONF_AREA_FILTER: ".*", } DUMMY_RESPONSE_REGIONS: dict[str, Any] = json.loads( @@ -146,6 +148,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: title="NINA", data={ CONF_HEADLINE_FILTER: deepcopy(DUMMY_DATA[CONF_HEADLINE_FILTER]), + CONF_AREA_FILTER: deepcopy(DUMMY_DATA[CONF_AREA_FILTER]), CONF_MESSAGE_SLOTS: deepcopy(DUMMY_DATA[CONF_MESSAGE_SLOTS]), CONST_REGION_A_TO_D: deepcopy(DUMMY_DATA[CONST_REGION_A_TO_D]), CONF_REGIONS: {"095760000000": "Aach"}, @@ -184,6 +187,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: assert dict(config_entry.data) == { CONF_HEADLINE_FILTER: deepcopy(DUMMY_DATA[CONF_HEADLINE_FILTER]), + CONF_AREA_FILTER: deepcopy(DUMMY_DATA[CONF_AREA_FILTER]), CONF_MESSAGE_SLOTS: deepcopy(DUMMY_DATA[CONF_MESSAGE_SLOTS]), CONST_REGION_A_TO_D: ["072350000000_1"], CONST_REGION_E_TO_H: [], diff --git a/tests/components/nina/test_init.py b/tests/components/nina/test_init.py index 826b8e422ed..da73c8d8711 100644 --- a/tests/components/nina/test_init.py +++ b/tests/components/nina/test_init.py @@ -16,6 +16,7 @@ from tests.common import MockConfigEntry ENTRY_DATA: dict[str, Any] = { "slots": 5, "headline_filter": ".*corona.*", + "area_filter": ".*", "regions": {"083350000000": "Aach, Stadt"}, }