Add filter for affected areas in NINA warnings (#97053)

* Add affected areas to warnings

* Update config flow

* Remove option from config_flow

* Add regex check

* Remove regex check
pull/98847/head
Maximilian 2023-08-22 20:23:34 +00:00 committed by GitHub
parent b65e3ddc99
commit d179f8b47d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 138 additions and 4 deletions

View File

@ -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 "",

View File

@ -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,

View File

@ -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,

View File

@ -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"

View File

@ -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"
}
}
},

View File

@ -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

View File

@ -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: [],

View File

@ -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"},
}