diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index d8b827bd24f..057e8f0955e 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -33,26 +33,20 @@ class EntityFilter: self._exclude_e = set(config[CONF_EXCLUDE_ENTITIES]) self._include_d = set(config[CONF_INCLUDE_DOMAINS]) self._exclude_d = set(config[CONF_EXCLUDE_DOMAINS]) - self._include_eg = _convert_globs_to_pattern_list( - config[CONF_INCLUDE_ENTITY_GLOBS] - ) - self._exclude_eg = _convert_globs_to_pattern_list( - config[CONF_EXCLUDE_ENTITY_GLOBS] - ) + self._include_eg = _convert_globs_to_pattern(config[CONF_INCLUDE_ENTITY_GLOBS]) + self._exclude_eg = _convert_globs_to_pattern(config[CONF_EXCLUDE_ENTITY_GLOBS]) self._filter: Callable[[str], bool] | None = None def explicitly_included(self, entity_id: str) -> bool: """Check if an entity is explicitly included.""" return entity_id in self._include_e or ( - bool(self._include_eg) - and _test_against_patterns(self._include_eg, entity_id) + bool(self._include_eg and self._include_eg.match(entity_id)) ) def explicitly_excluded(self, entity_id: str) -> bool: """Check if an entity is explicitly excluded.""" return entity_id in self._exclude_e or ( - bool(self._exclude_eg) - and _test_against_patterns(self._exclude_eg, entity_id) + bool(self._exclude_eg and self._exclude_eg.match(entity_id)) ) def __call__(self, entity_id: str) -> bool: @@ -140,19 +134,22 @@ INCLUDE_EXCLUDE_FILTER_SCHEMA = vol.All( ) -def _glob_to_re(glob: str) -> re.Pattern[str]: - """Translate and compile glob string into pattern.""" - return re.compile(fnmatch.translate(glob)) - - -def _test_against_patterns(patterns: list[re.Pattern[str]], entity_id: str) -> bool: - """Test entity against list of patterns, true if any match.""" - return any(pattern.match(entity_id) for pattern in patterns) - - -def _convert_globs_to_pattern_list(globs: list[str] | None) -> list[re.Pattern[str]]: +def _convert_globs_to_pattern(globs: list[str] | None) -> re.Pattern[str] | None: """Convert a list of globs to a re pattern list.""" - return list(map(_glob_to_re, set(globs or []))) + if globs is None: + return None + + translated_patterns: list[str] = [] + for glob in set(globs): + if pattern := fnmatch.translate(glob): + translated_patterns.append(pattern) + + if not translated_patterns: + return None + + inner = "|".join(translated_patterns) + combined = f"(?:{inner})" + return re.compile(combined) def generate_filter( @@ -169,8 +166,8 @@ def generate_filter( set(include_entities), set(exclude_domains), set(exclude_entities), - _convert_globs_to_pattern_list(include_entity_globs), - _convert_globs_to_pattern_list(exclude_entity_globs), + _convert_globs_to_pattern(include_entity_globs), + _convert_globs_to_pattern(exclude_entity_globs), ) @@ -179,8 +176,8 @@ def _generate_filter_from_sets_and_pattern_lists( include_e: set[str], exclude_d: set[str], exclude_e: set[str], - include_eg: list[re.Pattern[str]], - exclude_eg: list[re.Pattern[str]], + include_eg: re.Pattern[str] | None, + exclude_eg: re.Pattern[str] | None, ) -> Callable[[str], bool]: """Generate a filter from pre-comuted sets and pattern lists.""" have_exclude = bool(exclude_e or exclude_d or exclude_eg) @@ -191,7 +188,7 @@ def _generate_filter_from_sets_and_pattern_lists( return ( entity_id in include_e or domain in include_d - or (bool(include_eg) and _test_against_patterns(include_eg, entity_id)) + or (bool(include_eg and include_eg.match(entity_id))) ) def entity_excluded(domain: str, entity_id: str) -> bool: @@ -199,7 +196,7 @@ def _generate_filter_from_sets_and_pattern_lists( return ( entity_id in exclude_e or domain in exclude_d - or (bool(exclude_eg) and _test_against_patterns(exclude_eg, entity_id)) + or (bool(exclude_eg and exclude_eg.match(entity_id))) ) # Case 1 - No filter @@ -249,12 +246,10 @@ def _generate_filter_from_sets_and_pattern_lists( return entity_id in include_e or ( entity_id not in exclude_e and ( - (include_eg and _test_against_patterns(include_eg, entity_id)) + bool(include_eg and include_eg.match(entity_id)) or ( split_entity_id(entity_id)[0] in include_d - and not ( - exclude_eg and _test_against_patterns(exclude_eg, entity_id) - ) + and not (exclude_eg and exclude_eg.match(entity_id)) ) ) ) @@ -272,9 +267,7 @@ def _generate_filter_from_sets_and_pattern_lists( def entity_filter_4b(entity_id: str) -> bool: """Return filter function for case 4b.""" domain = split_entity_id(entity_id)[0] - if domain in exclude_d or ( - exclude_eg and _test_against_patterns(exclude_eg, entity_id) - ): + if domain in exclude_d or bool(exclude_eg and exclude_eg.match(entity_id)): return entity_id in include_e return entity_id not in exclude_e diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py index 9888704702c..2141c286914 100644 --- a/tests/helpers/test_entityfilter.py +++ b/tests/helpers/test_entityfilter.py @@ -369,7 +369,7 @@ def test_filter_schema_include_exclude() -> None: assert not filt.empty_filter -def test_exlictly_included() -> None: +def test_explicitly_included() -> None: """Test if an entity is explicitly included.""" conf = { "include": {