Coverage for custom_components/ha_repl_server/objtree.py: 70%

263 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-10-05 13:50 +0000

1"""`obj[...]` - a live, read-only lookup over Home Assistant's entities. 

2 

3Entities only for now; the full integration/domain/area/label tree from the 

4README's Object Browser design is a later step. Lookup forms: 

5 

6 obj["light.kitchen_lights"] # plain entity_id 

7 obj["/demo/light/kitchen_lights"] # /integration/domain/object_id 

8 

9A path that doesn't reach all the way to an object_id instead returns an 

10ObjTree restricted to that part of the tree, so the object browser's 

11directories can be navigated a level at a time: 

12 

13 alexa = obj["/alexa_devices"] # restricted to that integration 

14 alexa["media_player"] # -> restricted to that domain too 

15 alexa["media_player/kitchen_show"] # -> the live entity 

16 

17`.keys()`/iteration mirror that: `alexa.keys()` lists just the domains directly 

18under `/alexa_devices` (one level, like `ls` on a directory), not every entity 

19underneath it recursively. `obj.find()` is the flat alternative - every 

20(path, entity) pair under a subtree, optionally filtered by 

21platform/domain/area/label; `obj.find_paths()` is the same search with just 

22the tree path strings (e.g. "/demo/light/kitchen_lights"), and 

23`obj.find_names()` with just the HA entity_id strings (e.g. 

24"light.kitchen_lights") instead. `obj.show(path)` renders one entity's 

25registry entry plus its live state. 

26 

27Both return the live Entity object - the actual LightEntity/SensorEntity/etc. 

28instance a component wrote, not the hass.states.get() State snapshot - since this 

29is strict mode: you use it exactly as you would from component code. 

30 

31`obj.mode("api")` switches that: entity lookups/find() return the same 

32read-only, dict-shaped ApiEntity data API client mode's `obj` would give you 

33(built locally from the live entity/registry, not a second round-trip 

34through the API) instead of the live Entity object - e.g. to preview how a 

35snippet will behave once ported to API client mode, or just to avoid live 

36side effects. It's a session-wide switch, not a one-off: it persists on 

37every view sharing this `obj`'s root until `obj.mode("live")` is called. 

38`obj.mode()` with no argument returns the current mode. 

39 

40Only entities in the entity registry are reachable by the /integration/... path, 

41since that's where the owning integration ("platform") is recorded; legacy YAML 

42entities without a unique_id aren't registered there. Both lookup forms require a 

43live Entity object backing the entity - true for everything set up the normal way 

44through an EntityPlatform (YAML or config-entry), but not for a bare state set 

45directly via hass.states.async_set() with no Entity subclass behind it (rare 

46outside of tests/templates). See the README's Object Browser enhancements for the 

47registry-less follow-up. 

48""" 

49 

50from __future__ import annotations 

51 

52import re 

53from collections.abc import ( 

54 Callable, 

55 ItemsView, 

56 Iterable, 

57 Iterator, 

58 KeysView, 

59 Mapping, 

60 Sequence, 

61 ValuesView, 

62) 

63from dataclasses import dataclass, field 

64from datetime import datetime 

65from typing import Any, NamedTuple 

66 

67from homeassistant.core import HomeAssistant 

68from homeassistant.helpers import area_registry as ar 

69from homeassistant.helpers import device_registry as dr 

70from homeassistant.helpers import entity_registry as er 

71from homeassistant.helpers import label_registry as lr 

72from homeassistant.helpers.entity import Entity 

73from homeassistant.helpers.entity_platform import DATA_DOMAIN_ENTITIES 

74from homeassistant.helpers.typing import UNDEFINED 

75from homeassistant.util import dt as dt_util 

76 

77from .paths import parse_path 

78 

79 

80def _get_entity(hass: HomeAssistant, entity_id: str) -> Entity | None: 

81 """The live Entity instance behind entity_id, or None. 

82 

83 hass.data[DATA_DOMAIN_ENTITIES] is maintained by EntityPlatform itself (for 

84 every platform, YAML or config-entry) as {domain: {entity_id: Entity}} - the 

85 one lookup that's both centralized and independent of which integration or 

86 setup style owns the entity. It's a private implementation detail of 

87 homeassistant.helpers.entity_platform, not public API, so this is written 

88 defensively (plain dict .get chains) rather than assuming its shape. 

89 """ 

90 domain = entity_id.partition(".")[0] 

91 return hass.data.get(DATA_DOMAIN_ENTITIES, {}).get(domain, {}).get(entity_id) 

92 

93 

94def _as_set(value: str | list[str] | None) -> set[str]: 

95 if value is None: 

96 return set() 

97 return {value} if isinstance(value, str) else set(value) 

98 

99 

100# HA's own slugs (integration/domain/object_id) are always [a-z0-9_] - never any 

101# of these - so a `find()` path containing one is unambiguously a regex, not a 

102# literal path, with no risk of misreading a real path as a pattern. 

103_REGEX_METACHARS = frozenset(".^$*+?{}[]|()\\") 

104 

105 

106def _require_str(value: Any, what: str = "path") -> str: 

107 """A clear TypeError at the boundary beats a confusing one from deep 

108 inside parse_path (e.g. passing an Entity instead of its path - an easy 

109 slip, since indexing returns one and it's natural to then pass it back 

110 in somewhere a path string was wanted).""" 

111 if not isinstance(value, str): 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true

112 raise TypeError(f"{what} must be a str, not {type(value).__name__}") 

113 return value 

114 

115 

116@dataclass(frozen=True) 

117class ApiEntity: 

118 """One entity's worth of `obj.mode("api")` data - the same read-only 

119 subset of fields API client mode's own ApiEntity exposes (see 

120 homeassistant_repl.api_objtree.ApiEntity), built here from the already- 

121 live Entity/registry entry directly rather than a second round-trip 

122 through the API. Not the live component instance: no calling methods on 

123 it, no fields an integration keeps off the registry/state. 

124 """ 

125 

126 entity_id: str 

127 platform: str 

128 domain: str 

129 object_id: str 

130 state: str | None 

131 name: str | None 

132 state_attributes: dict[str, Any] 

133 area_id: str | None 

134 labels: frozenset[str] 

135 registry: dict[str, Any] 

136 

137 

138@dataclass 

139class _Mode: 

140 """Mutable, shared by reference across every ObjTree view derived from 

141 the same root - a plain field on the (frozen, otherwise immutable) 

142 ObjTree dataclass would only flip that one view, not "the rest of the 

143 session" the way obj.mode(...) promises.""" 

144 

145 api: bool = False 

146 

147 

148class _Found(NamedTuple): 

149 """Internal only - never returned from find()/find_paths()/find_names(), 

150 just the shared (path, entity) pair each of them projects from 

151 (.entity, .path, and .entity_id respectively) so the filtering logic in 

152 _find() lives in exactly one place. Any attribute other than path/entity 

153 falls through to `entity`, so e.g. found.entity_id works without 

154 found.entity.entity_id.""" 

155 

156 path: str 

157 entity: Entity | ApiEntity 

158 

159 def __getattr__(self, name: str) -> Any: 

160 return getattr(self.entity, name) 

161 

162 

163def _resolve_ids( 

164 values: str | list[str] | None, 

165 kind: str, 

166 by_id: Callable[[str], Any], 

167 by_name: Callable[[str], Any], 

168 id_attr: str, 

169) -> set[str]: 

170 """Resolve each of `values` (an id or a display name) to its canonical id. 

171 

172 Raises KeyError on anything that matches neither - a typo'd area/label 

173 name should fail loudly, not just quietly match nothing in find(). 

174 """ 

175 ids: set[str] = set() 

176 for value in _as_set(values): 176 ↛ 177line 176 didn't jump to line 177 because the loop on line 176 never started

177 found = by_id(value) or by_name(value) 

178 if found is None: 

179 raise KeyError(f"no such {kind}: {value!r}") 

180 ids.add(getattr(found, id_attr)) 

181 return ids 

182 

183 

184def _entity_area_id(entry: er.RegistryEntry, devices: dr.DeviceRegistry) -> str | None: 

185 """The entry's own area, falling back to its device's - the same rule the 

186 frontend uses to decide which area an entity "is in".""" 

187 if entry.area_id is not None: 187 ↛ 188line 187 didn't jump to line 188 because the condition on line 187 was never true

188 return entry.area_id 

189 if entry.device_id is not None: 189 ↛ 190line 189 didn't jump to line 190 because the condition on line 189 was never true

190 device = devices.async_get(entry.device_id) 

191 if device is not None: 

192 return device.area_id 

193 return None 

194 

195 

196def _public_attrs(obj: Any) -> dict[str, Any]: 

197 """Every attrs-declared field on `obj`, minus _cache and anything else 

198 marked private by the leading-underscore convention attrs itself uses for 

199 fields like RegistryEntry._cache.""" 

200 return { 

201 a.name: getattr(obj, a.name) 

202 for a in getattr(obj, "__attrs_attrs__", ()) 

203 if not a.name.startswith("_") 

204 } 

205 

206 

207def _normalize(value: Any) -> Any: 

208 if isinstance(value, datetime): 

209 return dt_util.as_local(value).isoformat() 

210 if isinstance(value, Mapping): 

211 return _clean(dict(value)) 

212 return value 

213 

214 

215def _clean(data: dict[str, Any]) -> dict[str, Any]: 

216 """Recursively drop None/empty-set values and render datetimes as local 

217 ISO 8601 strings - applied throughout, not just at the top level, since 

218 things like state_attributes are themselves dicts that can hold either.""" 

219 cleaned = {} 

220 for key, value in data.items(): 

221 value = _normalize(value) 

222 if value is None or value == set(): 

223 continue 

224 cleaned[key] = value 

225 return cleaned 

226 

227 

228class _OrderedView(Iterable[Any]): 

229 """Adds positional access/slicing on top of a MappingView's existing order. 

230 

231 ObjTree.__iter__ already yields children in a meaningful (alphanumeric) 

232 order, so the views built on it can afford to be real Sequences too - not 

233 just the usual Collection (ValuesView) or Set (KeysView/ItemsView) - without 

234 giving up set arithmetic or `isinstance(x, KeysView)` duck-typing: this is a 

235 mixin, not a replacement, so a class using it keeps its MappingView base's 

236 behaviour and only adds `__getitem__`/`__reversed__` on top. 

237 

238 Inherits Iterable (abstract, no `__iter__` of its own) only so type checkers 

239 know `self` supports `tuple(self)` here - the concrete classes below supply 

240 the real `__iter__` via their MappingView base, same as at runtime. 

241 """ 

242 

243 def __getitem__(self, index: int | slice) -> Any: 

244 return tuple(self)[index] 

245 

246 def __reversed__(self) -> Iterator[Any]: 

247 return reversed(tuple(self)) 

248 

249 

250class OrderedKeysView(_OrderedView, KeysView, Sequence): 

251 pass 

252 

253 

254class OrderedValuesView(_OrderedView, ValuesView, Sequence): 

255 pass 

256 

257 

258class OrderedItemsView(_OrderedView, ItemsView, Sequence): # type: ignore[misc] # ty: ignore[invalid-method-override] 

259 # Sequence.__contains__(value) vs ItemsView.__contains__(item: tuple) is a 

260 # real static Liskov mismatch, but fine at runtime - both just delegate to 

261 # tuple(self).__contains__ via _OrderedView/Iterable, deliberately getting 

262 # both Set and Sequence behavior at once. 

263 pass 

264 

265 

266@dataclass(frozen=True) 

267class ObjTree(Mapping[str, "Entity | ApiEntity | ObjTree"]): 

268 """A view of the object tree, optionally restricted to a subtree. 

269 

270 `integration` and/or `domain` pin this view to that part of the tree - 

271 the way `obj["/alexa_devices"]` or `obj["/alexa_devices/media_player"]` 

272 does. Indexing a restricted view only needs the remaining path segments, 

273 given either as a single "a/b" string or one segment at a time. 

274 

275 Implementing collections.abc.Mapping (on top of __getitem__, __iter__ and 

276 __len__) gets `in`, .get(), and real KeysView/ItemsView/ValuesView from 

277 .keys()/.items()/.values() for free, consistent with any other dict-like 

278 object - overridden below to also be Sequences, since every level in this 

279 tree is naturally a list (see _OrderedView): `obj["/alexa_devices"].keys()[0]` 

280 and slicing work, alongside the usual Set/Collection behaviour. 

281 """ 

282 

283 hass: HomeAssistant 

284 integration: str | None = None 

285 domain: str | None = None 

286 # Shared by reference with every view derived from this one (see _Mode's 

287 # docstring) - not compared/hashed, since it's a toggle, not identity. 

288 _mode: _Mode = field(default_factory=_Mode, compare=False) 

289 

290 def mode(self, value: str | None = None) -> str | None: 

291 """Switch between "live" (the default - indexing/find() return the 

292 real, live Entity, exactly as component code sees it) and "api" 

293 (the same read-only, dict-shaped ApiEntity data API client mode's 

294 `obj` would give you) - see the module docstring. Persists on every 

295 view sharing this obj's root until mode() is called again; with no 

296 argument, returns the current mode instead of changing it. 

297 """ 

298 if value is None: 

299 return "api" if self._mode.api else "live" 

300 if value not in ("live", "api"): 

301 raise ValueError(f"mode must be 'live' or 'api', not {value!r}") 

302 self._mode.api = value == "api" 

303 return None 

304 

305 def __getitem__(self, key: str) -> Entity | ApiEntity | ObjTree: 

306 _require_str(key, "key") 

307 if ( 

308 "/" not in key 

309 and "." in key 

310 and self.integration is not None 

311 and self.domain is None 

312 ): 

313 # Courtesy: a full entity_id ("domain.object_id") also works 

314 # scoped to just an integration, not only at the root - translate 

315 # to the equivalent "/"-path so the logic below (which already 

316 # enforces the platform match via _entity_at/_api_entity_at) 

317 # handles it the same way. 

318 domain, _, object_id = key.partition(".") 

319 key = f"{domain}/{object_id}" 

320 if "/" not in key and self.integration is None: 

321 if self._mode.api: 

322 # No integration to check against here (that's the whole 

323 # point of a bare entity_id) - just need entry+entity, not 

324 # _api_entity_at's platform match. 

325 entry = er.async_get(self.hass).async_get(key) 

326 entity = _get_entity(self.hass, key) if entry is not None else None 

327 if entry is None or entity is None: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true

328 raise KeyError(key) 

329 domain, _, object_id = key.partition(".") 

330 return self._to_api_entity(entry, domain, object_id, entity) 

331 entity = _get_entity(self.hass, key) 

332 if entity is None: 332 ↛ 333line 332 didn't jump to line 333 because the condition on line 332 was never true

333 raise KeyError(key) 

334 return entity 

335 try: 

336 parts = parse_path(key) 

337 except ValueError as err: 

338 raise KeyError(str(err)) from None 

339 scope = tuple(p for p in (self.integration, self.domain) if p is not None) 

340 full = scope + parts 

341 if len(full) > 3: 341 ↛ 342line 341 didn't jump to line 342 because the condition on line 341 was never true

342 raise KeyError(key) 

343 if len(full) < 3: 

344 # Not `ObjTree(self.hass, *full, _mode=...)`: mypy can't tell a 

345 # variable-length tuple unpack won't collide with a later keyword. 

346 padded = full + (None,) * (2 - len(full)) 

347 subtree = ObjTree(self.hass, padded[0], padded[1], _mode=self._mode) 

348 # Without this, `in`/.get() (Mapping's default __contains__ tries 

349 # self[key]) would say yes to any made-up integration/domain name, 

350 # disagreeing with .keys() - which only ever lists ones with entities. 

351 if not subtree: 351 ↛ 352line 351 didn't jump to line 352 because the condition on line 351 was never true

352 raise KeyError(key) 

353 return subtree 

354 if self._mode.api: 

355 api_entity = self._api_entity_at(*full) 

356 if api_entity is None: 356 ↛ 357line 356 didn't jump to line 357 because the condition on line 356 was never true

357 raise KeyError(key) 

358 return api_entity 

359 entity = self._entity_at(*full) 

360 if entity is None: 

361 raise KeyError(key) 

362 return entity 

363 

364 def _entity_at( 

365 self, integration: str, domain: str, object_id: str 

366 ) -> Entity | None: 

367 entity_id = f"{domain}.{object_id}" 

368 entry = er.async_get(self.hass).async_get(entity_id) 

369 if entry is None or entry.platform != integration: 

370 return None 

371 return _get_entity(self.hass, entity_id) 

372 

373 def _api_entity_at( 

374 self, integration: str, domain: str, object_id: str 

375 ) -> ApiEntity | None: 

376 entity_id = f"{domain}.{object_id}" 

377 entry = er.async_get(self.hass).async_get(entity_id) 

378 if entry is None or entry.platform != integration: 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true

379 return None 

380 entity = _get_entity(self.hass, entity_id) 

381 if entity is None: 381 ↛ 382line 381 didn't jump to line 382 because the condition on line 381 was never true

382 return None 

383 return self._to_api_entity(entry, domain, object_id, entity) 

384 

385 def _to_api_entity( 

386 self, entry: er.RegistryEntry, domain: str, object_id: str, entity: Entity 

387 ) -> ApiEntity: 

388 devices = dr.async_get(self.hass) 

389 name = entity.name 

390 return ApiEntity( 

391 entity_id=entry.entity_id, 

392 platform=entry.platform, 

393 domain=domain, 

394 object_id=object_id, 

395 # Mirrors what /api/states actually publishes: both come back as 

396 # plain strings there, not the live property's broader type 

397 # (state: StateType, name: str | UndefinedType | None). 

398 state=None if entity.state is None else str(entity.state), 

399 name=None if name is UNDEFINED else name, 

400 state_attributes=dict(entity.state_attributes or {}), 

401 area_id=_entity_area_id(entry, devices), 

402 labels=frozenset(entry.labels), 

403 registry=_public_attrs(entry), 

404 ) 

405 

406 def _entries( 

407 self, prefix: tuple[str, ...] 

408 ) -> Iterator[tuple[er.RegistryEntry, str, str]]: 

409 """Every (registry entry, domain, object_id) whose (platform, domain, 

410 object_id) matches `prefix` position by position - `prefix` may have 

411 0-3 elements, a narrower prefix just matching fewer positions. The 

412 shared scan both __iter__ (one level, deduped) and find() (full depth, 

413 flat) are built on. 

414 """ 

415 registry = er.async_get(self.hass) 

416 domain_entities = self.hass.data.get(DATA_DOMAIN_ENTITIES, {}) 

417 for domain, entities in domain_entities.items(): 

418 if len(prefix) >= 2 and domain != prefix[1]: 418 ↛ 419line 418 didn't jump to line 419 because the condition on line 418 was never true

419 continue 

420 for entity_id in entities: 

421 entry = registry.async_get(entity_id) 

422 if entry is None: 422 ↛ 423line 422 didn't jump to line 423 because the condition on line 422 was never true

423 continue 

424 if len(prefix) >= 1 and entry.platform != prefix[0]: 424 ↛ 425line 424 didn't jump to line 425 because the condition on line 424 was never true

425 continue 

426 object_id = entity_id.split(".", 1)[1] 

427 if len(prefix) >= 3 and object_id != prefix[2]: 427 ↛ 428line 427 didn't jump to line 428 because the condition on line 427 was never true

428 continue 

429 yield entry, domain, object_id 

430 

431 def __iter__(self) -> Iterator[str]: 

432 """The immediate child names one level down, alphanumeric order - a 

433 directory listing, not a recursive flattening down to every entity 

434 (that's find(), a different, explicitly flat view, not this one).""" 

435 scope = tuple(p for p in (self.integration, self.domain) if p is not None) 

436 children = { 

437 (entry.platform, domain, object_id)[len(scope)] 

438 for entry, domain, object_id in self._entries(scope) 

439 } 

440 yield from sorted(children) 

441 

442 def _find( 

443 self, 

444 path: str, 

445 *, 

446 platform: str | list[str] | None, 

447 domain: str | list[str] | None, 

448 area: str | list[str] | None, 

449 label: str | list[str] | None, 

450 ) -> Iterator[_Found]: 

451 """The real search, shared by find()/find_paths()/find_names() - each 

452 just projects a different field from the (path, entity) pairs this 

453 yields. Skips anything with no live Entity object backing it - same 

454 requirement as indexing - so an entity id from here always works if 

455 passed back into this view. 

456 

457 `path` is normally an exact /integration/domain/object_id prefix, the 

458 same as indexing - but a path containing a regex metacharacter (e.g. 

459 "/mqtt/binary_sensor/barn.*") is matched as a regular expression 

460 against each candidate's full path instead, anchored at the start 

461 (so it behaves like a prefix match unless you anchor the end 

462 yourself with `$`). That trades the usual early narrowing for a scan 

463 of this view's whole subtree, filtered by the pattern. 

464 

465 `platform` matches the registry entry's platform (owning integration) 

466 directly; `domain` matches the HA domain (e.g. "light"), letting you 

467 search across integrations without fixing `path` to one; `area`/`label` 

468 each take an id or a display name, and an entity's area falls back to 

469 its device's when it has none of its own - the same rule the frontend 

470 uses. Each of the four ORs within itself when given a list, and they 

471 AND together. No order guarantee; wrap in sorted(...) if you want one. 

472 """ 

473 _require_str(path) 

474 scope = tuple(p for p in (self.integration, self.domain) if p is not None) 

475 pattern = None 

476 if path not in ("", "/") and _REGEX_METACHARS.intersection(path): 

477 pattern = re.compile(path) 

478 prefix = scope 

479 else: 

480 try: 

481 prefix = scope if path in ("", "/") else scope + parse_path(path) 

482 except ValueError as err: 

483 raise KeyError(str(err)) from None 

484 if len(prefix) > 3: 484 ↛ 485line 484 didn't jump to line 485 because the condition on line 484 was never true

485 raise KeyError(path) 

486 

487 platforms = _as_set(platform) 

488 domains = _as_set(domain) 

489 areas = ar.async_get(self.hass) 

490 labels = lr.async_get(self.hass) 

491 area_ids = _resolve_ids( 

492 area, "area", areas.async_get_area, areas.async_get_area_by_name, "id" 

493 ) 

494 label_ids = _resolve_ids( 

495 label, 

496 "label", 

497 labels.async_get_label, 

498 labels.async_get_label_by_name, 

499 "label_id", 

500 ) 

501 devices = dr.async_get(self.hass) 

502 

503 def _matches() -> Iterator[_Found]: 

504 for entry, dom, object_id in self._entries(prefix): 

505 if platforms and entry.platform not in platforms: 505 ↛ 506line 505 didn't jump to line 506 because the condition on line 505 was never true

506 continue 

507 if domains and dom not in domains: 

508 continue 

509 if area_ids and _entity_area_id(entry, devices) not in area_ids: 509 ↛ 510line 509 didn't jump to line 510 because the condition on line 509 was never true

510 continue 

511 if label_ids and not (entry.labels & label_ids): 511 ↛ 512line 511 didn't jump to line 512 because the condition on line 511 was never true

512 continue 

513 entity = _get_entity(self.hass, entry.entity_id) 

514 if entity is None: 514 ↛ 515line 514 didn't jump to line 515 because the condition on line 514 was never true

515 continue 

516 full_path = "/" + "/".join( 

517 (entry.platform, dom, object_id)[len(scope) :] 

518 ) 

519 if pattern is not None and not pattern.match(full_path): 

520 continue 

521 found_entity = ( 

522 self._to_api_entity(entry, dom, object_id, entity) 

523 if self._mode.api 

524 else entity 

525 ) 

526 yield _Found(full_path, found_entity) 

527 

528 return _matches() 

529 

530 def find( 

531 self, 

532 path: str = "/", 

533 *, 

534 platform: str | list[str] | None = None, 

535 domain: str | list[str] | None = None, 

536 area: str | list[str] | None = None, 

537 label: str | list[str] | None = None, 

538 raw: bool = False, 

539 ) -> Iterator[Entity | ApiEntity] | Iterator[dict[str, Any]]: 

540 """Every entity (or raw dict, if raw=True) matching the filters under 

541 `path` (relative to this view, "/" meaning this view's whole 

542 subtree) - flat, skipping the directory-style one-level-at-a-time 

543 grouping .keys()/indexing give you. find_paths()/find_names() are the 

544 same search with just the tree-path or entity_id strings, if that's 

545 all you want - see _find() for the filters. 

546 

547 `raw=True` yields the object as received instead of the live Entity - 

548 exactly what show(path) would return for that path (the registry 

549 entry's fields plus state/state_attributes, cleaned the same way). 

550 The dict's own "entity_id" key identifies which entity it came from. 

551 """ 

552 matches = self._find( 

553 path, platform=platform, domain=domain, area=area, label=label 

554 ) 

555 if raw: 555 ↛ 556line 555 didn't jump to line 556 because the condition on line 555 was never true

556 return (self.show(found.path) for found in matches) 

557 return (found.entity for found in matches) 

558 

559 def find_paths( 

560 self, 

561 path: str = "/", 

562 *, 

563 platform: str | list[str] | None = None, 

564 domain: str | list[str] | None = None, 

565 area: str | list[str] | None = None, 

566 label: str | list[str] | None = None, 

567 ) -> Iterator[str]: 

568 """Just the tree-path strings from find() (e.g. 

569 "/demo/light/kitchen_lights") - see _find() for the filters.""" 

570 return ( 

571 found.path 

572 for found in self._find( 

573 path, platform=platform, domain=domain, area=area, label=label 

574 ) 

575 ) 

576 

577 def find_names( 

578 self, 

579 path: str = "/", 

580 *, 

581 platform: str | list[str] | None = None, 

582 domain: str | list[str] | None = None, 

583 area: str | list[str] | None = None, 

584 label: str | list[str] | None = None, 

585 ) -> Iterator[str]: 

586 """Just the HA entity_id strings from find() (e.g. 

587 "light.kitchen_lights") - see _find() for the filters. Not the same 

588 as find_paths(): this is the flat entity_id, not this tree's 

589 /integration/domain/object_id path.""" 

590 return ( 

591 found.entity_id 

592 for found in self._find( 

593 path, platform=platform, domain=domain, area=area, label=label 

594 ) 

595 ) 

596 

597 def show(self, path: str) -> dict[str, Any]: 

598 """The registry entry's own fields (minus _cache and other data this 

599 hides by the leading-underscore convention) plus the live entity's 

600 `state` and `state_attributes` - meant to be the trailing expression 

601 at the REPL, so the usual Pretty-printed echo renders it; this doesn't 

602 print anything itself. Datetimes come back as local ISO 8601 strings, 

603 and None/empty-set values are dropped throughout, including inside 

604 nested dicts like state_attributes. 

605 """ 

606 _require_str(path) 

607 scope = tuple(p for p in (self.integration, self.domain) if p is not None) 

608 try: 

609 full = scope + parse_path(path) 

610 except ValueError as err: 

611 raise KeyError(str(err)) from None 

612 if len(full) != 3: 

613 raise KeyError(f"{path!r} - show() needs a full entity path") 

614 integration, domain, object_id = full 

615 entity = self._entity_at(integration, domain, object_id) 

616 if entity is None: 

617 raise KeyError(path) 

618 

619 entry = er.async_get(self.hass).async_get(f"{domain}.{object_id}") 

620 data: dict[str, Any] = _public_attrs(entry) if entry is not None else {} 

621 # Overrides the registry's own (often-None, user-override-only) "name" 

622 # field with the live, fully-resolved display name - the direct 

623 # equivalent per https://developers.home-assistant.io/docs/core/entity/. 

624 data["name"] = entity.name 

625 data["state"] = entity.state 

626 data["state_attributes"] = entity.state_attributes 

627 return _clean(data) 

628 

629 def __len__(self) -> int: 

630 return sum(1 for _ in self) 

631 

632 def keys(self) -> OrderedKeysView: 

633 return OrderedKeysView(self) 

634 

635 def values(self) -> OrderedValuesView: 

636 return OrderedValuesView(self) 

637 

638 def items(self) -> OrderedItemsView: 

639 return OrderedItemsView(self) 

640 

641 def __repr__(self) -> str: 

642 scope = "/".join(p for p in (self.integration, self.domain) if p is not None) 

643 label = f"obj:/{scope}" if scope else "obj" 

644 kind = ( 

645 "entities" 

646 if self.domain is not None 

647 else "domains" 

648 if self.integration 

649 else "integrations" 

650 ) 

651 return f"<{label}: {len(self)} {kind}>"