diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index de0929cf9f4..24d22704a89 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -63,42 +63,53 @@ def merge_include_exclude_filters( def sqlalchemy_filter_from_include_exclude_conf(conf: ConfigType) -> Filters | None: """Build a sql filter from config.""" - filters = Filters() - if exclude := conf.get(CONF_EXCLUDE): - filters.excluded_entities = exclude.get(CONF_ENTITIES, []) - filters.excluded_domains = exclude.get(CONF_DOMAINS, []) - filters.excluded_entity_globs = exclude.get(CONF_ENTITY_GLOBS, []) - if include := conf.get(CONF_INCLUDE): - filters.included_entities = include.get(CONF_ENTITIES, []) - filters.included_domains = include.get(CONF_DOMAINS, []) - filters.included_entity_globs = include.get(CONF_ENTITY_GLOBS, []) - + exclude = conf.get(CONF_EXCLUDE, {}) + include = conf.get(CONF_INCLUDE, {}) + filters = Filters( + excluded_entities=exclude.get(CONF_ENTITIES, []), + excluded_domains=exclude.get(CONF_DOMAINS, []), + excluded_entity_globs=exclude.get(CONF_ENTITY_GLOBS, []), + included_entities=include.get(CONF_ENTITIES, []), + included_domains=include.get(CONF_DOMAINS, []), + included_entity_globs=include.get(CONF_ENTITY_GLOBS, []), + ) return filters if filters.has_config else None class Filters: - """Container for the configured include and exclude filters.""" + """Container for the configured include and exclude filters. - def __init__(self) -> None: + A filter must never change after it is created since it is used in a + cache key. + """ + + def __init__( + self, + excluded_entities: Collection[str] | None = None, + excluded_domains: Collection[str] | None = None, + excluded_entity_globs: Collection[str] | None = None, + included_entities: Collection[str] | None = None, + included_domains: Collection[str] | None = None, + included_entity_globs: Collection[str] | None = None, + ) -> None: """Initialise the include and exclude filters.""" - self.excluded_entities: Collection[str] = [] - self.excluded_domains: Collection[str] = [] - self.excluded_entity_globs: Collection[str] = [] - - self.included_entities: Collection[str] = [] - self.included_domains: Collection[str] = [] - self.included_entity_globs: Collection[str] = [] + self._excluded_entities = excluded_entities or [] + self._excluded_domains = excluded_domains or [] + self._excluded_entity_globs = excluded_entity_globs or [] + self._included_entities = included_entities or [] + self._included_domains = included_domains or [] + self._included_entity_globs = included_entity_globs or [] def __repr__(self) -> str: """Return human readable excludes/includes.""" return ( "" ) @@ -110,17 +121,17 @@ class Filters: @property def _have_exclude(self) -> bool: return bool( - self.excluded_entities - or self.excluded_domains - or self.excluded_entity_globs + self._excluded_entities + or self._excluded_domains + or self._excluded_entity_globs ) @property def _have_include(self) -> bool: return bool( - self.included_entities - or self.included_domains - or self.included_entity_globs + self._included_entities + or self._included_domains + or self._included_entity_globs ) def _generate_filter_for_columns( @@ -130,14 +141,14 @@ class Filters: This must match exactly how homeassistant.helpers.entityfilter works. """ - i_domains = _domain_matcher(self.included_domains, columns, encoder) - i_entities = _entity_matcher(self.included_entities, columns, encoder) - i_entity_globs = _globs_to_like(self.included_entity_globs, columns, encoder) + i_domains = _domain_matcher(self._included_domains, columns, encoder) + i_entities = _entity_matcher(self._included_entities, columns, encoder) + i_entity_globs = _globs_to_like(self._included_entity_globs, columns, encoder) includes = [i_domains, i_entities, i_entity_globs] - e_domains = _domain_matcher(self.excluded_domains, columns, encoder) - e_entities = _entity_matcher(self.excluded_entities, columns, encoder) - e_entity_globs = _globs_to_like(self.excluded_entity_globs, columns, encoder) + e_domains = _domain_matcher(self._excluded_domains, columns, encoder) + e_entities = _entity_matcher(self._excluded_entities, columns, encoder) + e_entity_globs = _globs_to_like(self._excluded_entity_globs, columns, encoder) excludes = [e_domains, e_entities, e_entity_globs] have_exclude = self._have_exclude @@ -173,7 +184,7 @@ class Filters: # - Otherwise, entity matches glob exclude: exclude # - Otherwise, entity matches domain include: include # - Otherwise: exclude - if self.included_domains or self.included_entity_globs: + if self._included_domains or self._included_entity_globs: return or_( i_entities, # https://github.com/sqlalchemy/sqlalchemy/issues/9190 @@ -187,7 +198,7 @@ class Filters: # - Otherwise, entity matches glob exclude: exclude # - Otherwise, entity matches domain exclude: exclude # - Otherwise: include - if self.excluded_domains or self.excluded_entity_globs: + if self._excluded_domains or self._excluded_entity_globs: return (not_(or_(*excludes)) | i_entities).self_group() # type: ignore[no-any-return, no-untyped-call] # Case 6 - No Domain and/or glob includes or excludes diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index a5c3919505e..8b46bd97602 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -545,16 +545,15 @@ def test_get_significant_states_only(hass_history) -> None: def check_significant_states(hass, zero, four, states, config): """Check if significant states are retrieved.""" - filters = history.Filters() - exclude = config[history.DOMAIN].get(CONF_EXCLUDE) - if exclude: - filters.excluded_entities = exclude.get(CONF_ENTITIES, []) - filters.excluded_domains = exclude.get(CONF_DOMAINS, []) - include = config[history.DOMAIN].get(CONF_INCLUDE) - if include: - filters.included_entities = include.get(CONF_ENTITIES, []) - filters.included_domains = include.get(CONF_DOMAINS, []) - + domain_config = config[history.DOMAIN] + exclude = domain_config.get(CONF_EXCLUDE, {}) + include = domain_config.get(CONF_INCLUDE, {}) + filters = history.Filters( + excluded_entities=exclude.get(CONF_ENTITIES, []), + excluded_domains=exclude.get(CONF_DOMAINS, []), + included_entities=include.get(CONF_ENTITIES, []), + included_domains=include.get(CONF_DOMAINS, []), + ) hist = get_significant_states(hass, zero, four, filters=filters) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py index a300f58b96a..7668d6794d9 100644 --- a/tests/components/history/test_init_db_schema_30.py +++ b/tests/components/history/test_init_db_schema_30.py @@ -600,16 +600,15 @@ def test_get_significant_states_only(legacy_hass_history) -> None: def check_significant_states(hass, zero, four, states, config): """Check if significant states are retrieved.""" - filters = history.Filters() - exclude = config[history.DOMAIN].get(CONF_EXCLUDE) - if exclude: - filters.excluded_entities = exclude.get(CONF_ENTITIES, []) - filters.excluded_domains = exclude.get(CONF_DOMAINS, []) - include = config[history.DOMAIN].get(CONF_INCLUDE) - if include: - filters.included_entities = include.get(CONF_ENTITIES, []) - filters.included_domains = include.get(CONF_DOMAINS, []) - + domain_config = config[history.DOMAIN] + exclude = domain_config.get(CONF_EXCLUDE, {}) + include = domain_config.get(CONF_INCLUDE, {}) + filters = history.Filters( + excluded_entities=exclude.get(CONF_ENTITIES, []), + excluded_domains=exclude.get(CONF_DOMAINS, []), + included_entities=include.get(CONF_ENTITIES, []), + included_domains=include.get(CONF_DOMAINS, []), + ) hist = get_significant_states(hass, zero, four, filters=filters) assert_dict_of_states_equal_without_context_and_last_changed(states, hist)