Home Assistant Unofficial Reference 2024.12.1
entityfilter.py
Go to the documentation of this file.
1 """Helper class to implement include/exclude of entities and domains."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import fnmatch
7 from functools import lru_cache, partial
8 import operator
9 import re
10 
11 import voluptuous as vol
12 
13 from homeassistant.const import (
14  CONF_DOMAINS,
15  CONF_ENTITIES,
16  CONF_EXCLUDE,
17  CONF_INCLUDE,
18  MAX_EXPECTED_ENTITY_IDS,
19 )
20 from homeassistant.core import split_entity_id
21 
22 from . import config_validation as cv
23 
24 CONF_INCLUDE_DOMAINS = "include_domains"
25 CONF_INCLUDE_ENTITY_GLOBS = "include_entity_globs"
26 CONF_INCLUDE_ENTITIES = "include_entities"
27 CONF_EXCLUDE_DOMAINS = "exclude_domains"
28 CONF_EXCLUDE_ENTITY_GLOBS = "exclude_entity_globs"
29 CONF_EXCLUDE_ENTITIES = "exclude_entities"
30 
31 CONF_ENTITY_GLOBS = "entity_globs"
32 
33 
35  """A entity filter."""
36 
37  def __init__(self, config: dict[str, list[str]]) -> None:
38  """Init the filter."""
39  self.empty_filter: bool = sum(len(val) for val in config.values()) == 0
40  self.configconfig = config
41  self._include_e_include_e = set(config[CONF_INCLUDE_ENTITIES])
42  self._exclude_e_exclude_e = set(config[CONF_EXCLUDE_ENTITIES])
43  self._include_d_include_d = set(config[CONF_INCLUDE_DOMAINS])
44  self._exclude_d_exclude_d = set(config[CONF_EXCLUDE_DOMAINS])
45  self._include_eg_include_eg = _convert_globs_to_pattern(config[CONF_INCLUDE_ENTITY_GLOBS])
46  self._exclude_eg_exclude_eg = _convert_globs_to_pattern(config[CONF_EXCLUDE_ENTITY_GLOBS])
48  self._include_d_include_d,
49  self._include_e_include_e,
50  self._exclude_d_exclude_d,
51  self._exclude_e_exclude_e,
52  self._include_eg_include_eg,
53  self._exclude_eg_exclude_eg,
54  )
55 
56  def explicitly_included(self, entity_id: str) -> bool:
57  """Check if an entity is explicitly included."""
58  return entity_id in self._include_e_include_e or (
59  bool(self._include_eg_include_eg and self._include_eg_include_eg.match(entity_id))
60  )
61 
62  def explicitly_excluded(self, entity_id: str) -> bool:
63  """Check if an entity is explicitly excluded."""
64  return entity_id in self._exclude_e_exclude_e or (
65  bool(self._exclude_eg_exclude_eg and self._exclude_eg_exclude_eg.match(entity_id))
66  )
67 
68  def get_filter(self) -> Callable[[str], bool]:
69  """Return the filter function."""
70  return self._filter_filter
71 
72  def __call__(self, entity_id: str) -> bool:
73  """Run the filter."""
74  return self._filter_filter(entity_id)
75 
76 
77 def convert_filter(config: dict[str, list[str]]) -> EntityFilter:
78  """Convert the filter schema into a filter."""
79  return EntityFilter(config)
80 
81 
82 BASE_FILTER_SCHEMA = vol.Schema(
83  {
84  vol.Optional(CONF_EXCLUDE_DOMAINS, default=[]): vol.All(
85  cv.ensure_list, [cv.string]
86  ),
87  vol.Optional(CONF_EXCLUDE_ENTITY_GLOBS, default=[]): vol.All(
88  cv.ensure_list, [cv.string]
89  ),
90  vol.Optional(CONF_EXCLUDE_ENTITIES, default=[]): cv.entity_ids,
91  vol.Optional(CONF_INCLUDE_DOMAINS, default=[]): vol.All(
92  cv.ensure_list, [cv.string]
93  ),
94  vol.Optional(CONF_INCLUDE_ENTITY_GLOBS, default=[]): vol.All(
95  cv.ensure_list, [cv.string]
96  ),
97  vol.Optional(CONF_INCLUDE_ENTITIES, default=[]): cv.entity_ids,
98  }
99 )
100 
101 FILTER_SCHEMA = vol.All(BASE_FILTER_SCHEMA, convert_filter)
102 
103 
105  config: dict[str, dict[str, list[str]]],
106 ) -> EntityFilter:
107  """Convert the include exclude filter schema into a filter."""
108  include = config[CONF_INCLUDE]
109  exclude = config[CONF_EXCLUDE]
110  return convert_filter(
111  {
112  CONF_INCLUDE_DOMAINS: include[CONF_DOMAINS],
113  CONF_INCLUDE_ENTITY_GLOBS: include[CONF_ENTITY_GLOBS],
114  CONF_INCLUDE_ENTITIES: include[CONF_ENTITIES],
115  CONF_EXCLUDE_DOMAINS: exclude[CONF_DOMAINS],
116  CONF_EXCLUDE_ENTITY_GLOBS: exclude[CONF_ENTITY_GLOBS],
117  CONF_EXCLUDE_ENTITIES: exclude[CONF_ENTITIES],
118  }
119  )
120 
121 
122 INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER = vol.Schema(
123  {
124  vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]),
125  vol.Optional(CONF_ENTITY_GLOBS, default=[]): vol.All(
126  cv.ensure_list, [cv.string]
127  ),
128  vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
129  }
130 )
131 
132 INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA = vol.Schema(
133  {
134  vol.Optional(
135  CONF_INCLUDE, default=INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER({})
136  ): INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER,
137  vol.Optional(
138  CONF_EXCLUDE, default=INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER({})
139  ): INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER,
140  }
141 )
142 
143 INCLUDE_EXCLUDE_FILTER_SCHEMA = vol.All(
144  INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, convert_include_exclude_filter
145 )
146 
147 
148 def _convert_globs_to_pattern(globs: list[str] | None) -> re.Pattern[str] | None:
149  """Convert a list of globs to a re pattern list."""
150  if globs is None:
151  return None
152 
153  translated_patterns: list[str] = [
154  pattern for glob in set(globs) if (pattern := fnmatch.translate(glob))
155  ]
156 
157  if not translated_patterns:
158  return None
159 
160  inner = "|".join(translated_patterns)
161  combined = f"(?:{inner})"
162  return re.compile(combined)
163 
164 
166  include_domains: list[str],
167  include_entities: list[str],
168  exclude_domains: list[str],
169  exclude_entities: list[str],
170  include_entity_globs: list[str] | None = None,
171  exclude_entity_globs: list[str] | None = None,
172 ) -> Callable[[str], bool]:
173  """Return a function that will filter entities based on the args."""
175  set(include_domains),
176  set(include_entities),
177  set(exclude_domains),
178  set(exclude_entities),
179  _convert_globs_to_pattern(include_entity_globs),
180  _convert_globs_to_pattern(exclude_entity_globs),
181  )
182 
183 
185  include_d: set[str],
186  include_e: set[str],
187  exclude_d: set[str],
188  exclude_e: set[str],
189  include_eg: re.Pattern[str] | None,
190  exclude_eg: re.Pattern[str] | None,
191 ) -> Callable[[str], bool]:
192  """Generate a filter from pre-comuted sets and pattern lists."""
193  have_exclude = bool(exclude_e or exclude_d or exclude_eg)
194  have_include = bool(include_e or include_d or include_eg)
195 
196  # Case 1 - No filter
197  # - All entities included
198  if not have_include and not have_exclude:
199  return bool
200 
201  # Case 2 - Only includes
202  # - Entity listed in entities include: include
203  # - Otherwise, entity matches domain include: include
204  # - Otherwise, entity matches glob include: include
205  # - Otherwise: exclude
206  if have_include and not have_exclude:
207 
208  @lru_cache(maxsize=MAX_EXPECTED_ENTITY_IDS)
209  def entity_included(entity_id: str) -> bool:
210  """Return true if entity matches inclusion filters."""
211  return (
212  entity_id in include_e
213  or split_entity_id(entity_id)[0] in include_d
214  or (bool(include_eg and include_eg.match(entity_id)))
215  )
216 
217  # Return filter function for case 2
218  return entity_included
219 
220  # Case 3 - Only excludes
221  # - Entity listed in exclude: exclude
222  # - Otherwise, entity matches domain exclude: exclude
223  # - Otherwise, entity matches glob exclude: exclude
224  # - Otherwise: include
225  if not have_include and have_exclude:
226 
227  @lru_cache(maxsize=MAX_EXPECTED_ENTITY_IDS)
228  def entity_not_excluded(entity_id: str) -> bool:
229  """Return true if entity matches exclusion filters."""
230  return not (
231  entity_id in exclude_e
232  or split_entity_id(entity_id)[0] in exclude_d
233  or (exclude_eg and exclude_eg.match(entity_id))
234  )
235 
236  return entity_not_excluded
237 
238  # Case 4 - Domain and/or glob includes (may also have excludes)
239  # - Entity listed in entities include: include
240  # - Otherwise, entity listed in entities exclude: exclude
241  # - Otherwise, entity matches glob include: include
242  # - Otherwise, entity matches glob exclude: exclude
243  # - Otherwise, entity matches domain include: include
244  # - Otherwise: exclude
245  if include_d or include_eg:
246 
247  @lru_cache(maxsize=MAX_EXPECTED_ENTITY_IDS)
248  def entity_filter_4a(entity_id: str) -> bool:
249  """Return filter function for case 4a."""
250  return entity_id in include_e or (
251  entity_id not in exclude_e
252  and (
253  bool(include_eg and include_eg.match(entity_id))
254  or (
255  split_entity_id(entity_id)[0] in include_d
256  and not (exclude_eg and exclude_eg.match(entity_id))
257  )
258  )
259  )
260 
261  return entity_filter_4a
262 
263  # Case 5 - Domain and/or glob excludes (no domain and/or glob includes)
264  # - Entity listed in entities include: include
265  # - Otherwise, entity listed in exclude: exclude
266  # - Otherwise, entity matches glob exclude: exclude
267  # - Otherwise, entity matches domain exclude: exclude
268  # - Otherwise: include
269  if exclude_d or exclude_eg:
270 
271  @lru_cache(maxsize=MAX_EXPECTED_ENTITY_IDS)
272  def entity_filter_4b(entity_id: str) -> bool:
273  """Return filter function for case 4b."""
274  domain = split_entity_id(entity_id)[0]
275  if domain in exclude_d or bool(exclude_eg and exclude_eg.match(entity_id)):
276  return entity_id in include_e
277  return entity_id not in exclude_e
278 
279  return entity_filter_4b
280 
281  # Case 6 - No Domain and/or glob includes or excludes
282  # - Entity listed in entities include: include
283  # - Otherwise: exclude
284  return partial(operator.contains, include_e)
bool explicitly_included(self, str entity_id)
Definition: entityfilter.py:56
None __init__(self, dict[str, list[str]] config)
Definition: entityfilter.py:37
bool explicitly_excluded(self, str entity_id)
Definition: entityfilter.py:62
list[_T] match(self, BluetoothServiceInfoBleak service_info)
Definition: match.py:246
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
re.Pattern[str]|None _convert_globs_to_pattern(list[str]|None globs)
Callable[[str], bool] generate_filter(list[str] include_domains, list[str] include_entities, list[str] exclude_domains, list[str] exclude_entities, list[str]|None include_entity_globs=None, list[str]|None exclude_entity_globs=None)
Callable[[str], bool] _generate_filter_from_sets_and_pattern_lists(set[str] include_d, set[str] include_e, set[str] exclude_d, set[str] exclude_e, re.Pattern[str]|None include_eg, re.Pattern[str]|None exclude_eg)
EntityFilter convert_filter(dict[str, list[str]] config)
Definition: entityfilter.py:77
EntityFilter convert_include_exclude_filter(dict[str, dict[str, list[str]]] config)