1 """Provide pre-made queries on top of the recorder component."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Collection, Iterable
8 from sqlalchemy
import Column, Text, cast, not_, or_
9 from sqlalchemy.sql.elements
import ColumnElement
16 from .db_schema
import ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT, States, StatesMeta
19 HISTORY_FILTERS =
"history_filters"
30 FILTER_TYPES = (CONF_EXCLUDE, CONF_INCLUDE)
31 FITLER_MATCHERS = (CONF_ENTITIES, CONF_DOMAINS, CONF_ENTITY_GLOBS)
35 """Extract an include exclude filter from configuration.
37 This makes a copy so we do not alter the original data.
41 matcher: set(conf.get(filter_type, {}).
get(matcher)
or [])
42 for matcher
in FITLER_MATCHERS
44 for filter_type
in FILTER_TYPES
49 base_filter: dict[str, Any], add_filter: dict[str, Any]
53 This makes a copy so we do not alter the original data.
57 matcher: base_filter[filter_type][matcher]
58 | add_filter[filter_type][matcher]
59 for matcher
in FITLER_MATCHERS
61 for filter_type
in FILTER_TYPES
66 """Build a sql filter from config."""
67 exclude = conf.get(CONF_EXCLUDE, {})
68 include = conf.get(CONF_INCLUDE, {})
70 excluded_entities=exclude.get(CONF_ENTITIES, []),
71 excluded_domains=exclude.get(CONF_DOMAINS, []),
72 excluded_entity_globs=exclude.get(CONF_ENTITY_GLOBS, []),
73 included_entities=include.get(CONF_ENTITIES, []),
74 included_domains=include.get(CONF_DOMAINS, []),
75 included_entity_globs=include.get(CONF_ENTITY_GLOBS, []),
77 return filters
if filters.has_config
else None
81 """Container for the configured include and exclude filters.
83 A filter must never change after it is created since it is used in a
89 excluded_entities: Collection[str] |
None =
None,
90 excluded_domains: Collection[str] |
None =
None,
91 excluded_entity_globs: Collection[str] |
None =
None,
92 included_entities: Collection[str] |
None =
None,
93 included_domains: Collection[str] |
None =
None,
94 included_entity_globs: Collection[str] |
None =
None,
96 """Initialise the include and exclude filters."""
105 """Return human readable excludes/includes."""
108 f
" excluded_entities={self._excluded_entities}"
109 f
" excluded_domains={self._excluded_domains}"
110 f
" excluded_entity_globs={self._excluded_entity_globs}"
111 f
" included_entities={self._included_entities}"
112 f
" included_domains={self._included_domains}"
113 f
" included_entity_globs={self._included_entity_globs}"
119 """Determine if there is any filter configuration."""
139 self, columns: Iterable[Column], encoder: Callable[[Any], Any]
141 """Generate a filter from pre-computed sets and pattern lists.
143 This must match exactly how homeassistant.helpers.entityfilter works.
148 includes = [i_domains, i_entities, i_entity_globs]
153 excludes = [e_domains, e_entities, e_entity_globs]
160 if not have_include
and not have_exclude:
162 "No filter configuration provided, check has_config before calling this method."
170 if have_include
and not have_exclude:
171 return or_(*includes).self_group()
178 if not have_include
and have_exclude:
179 return not_(or_(*excludes).self_group())
191 (~e_entities & (i_entity_globs | (~e_entity_globs & i_domains))),
201 return (not_(or_(*excludes)) | i_entities).self_group()
209 """Generate the States.entity_id filter query.
211 This is no longer used except by the legacy queries.
215 """Nothing to encode for states since there is no json."""
222 """Generate the StatesMeta.entity_id filter query."""
225 """Nothing to encode for states since there is no json."""
232 """Generate the entity filter query."""
233 _encoder = json_dumps
242 ((ENTITY_ID_IN_EVENT == JSON_NULL) | ENTITY_ID_IN_EVENT.is_(
None))
244 (OLD_ENTITY_ID_IN_EVENT == JSON_NULL) | OLD_ENTITY_ID_IN_EVENT.is_(
None)
248 (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT),
255 glob_strs: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any]
257 """Translate glob to sql."""
261 & cast(column, Text()).like(
262 encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape=
"\\"
265 for glob_str
in glob_strs
266 for column
in columns
268 return or_(*matchers)
if matchers
else or_(
False)
272 entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any]
277 & cast(column, Text()).in_([encoder(entity_id)
for entity_id
in entity_ids])
279 for column
in columns
281 return or_(*matchers)
if matchers
else or_(
False)
285 domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any]
288 (column.is_not(
None) & cast(column, Text()).like(encoder(domain_matcher)))
290 for column
in columns
292 return or_(*matchers)
if matchers
else or_(
False)
296 """Convert a list of domains to sql LIKE matchers."""
297 return [f
"{domain}.%" for domain
in domains]
ColumnElement states_metadata_entity_filter(self)
None __init__(self, Collection[str]|None excluded_entities=None, Collection[str]|None excluded_domains=None, Collection[str]|None excluded_entity_globs=None, Collection[str]|None included_entities=None, Collection[str]|None included_domains=None, Collection[str]|None included_entity_globs=None)
ColumnElement states_entity_filter(self)
ColumnElement _generate_filter_for_columns(self, Iterable[Column] columns, Callable[[Any], Any] encoder)
ColumnElement events_entity_filter(self)
web.Response get(self, web.Request request, str config_key)
ColumnElement _globs_to_like(Iterable[str] glob_strs, Iterable[Column] columns, Callable[[Any], Any] encoder)
ColumnElement _domain_matcher(Iterable[str] domains, Iterable[Column] columns, Callable[[Any], Any] encoder)
dict[str, Any] merge_include_exclude_filters(dict[str, Any] base_filter, dict[str, Any] add_filter)
dict[str, Any] extract_include_exclude_filter_conf(ConfigType conf)
list[str] like_domain_matchers(Iterable[str] domains)
Filters|None sqlalchemy_filter_from_include_exclude_conf(ConfigType conf)
ColumnElement _entity_matcher(Iterable[str] entity_ids, Iterable[Column] columns, Callable[[Any], Any] encoder)