diff --git a/homeassistant/core.py b/homeassistant/core.py index bfb88ab6bfd..eb584b22b49 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -759,6 +759,7 @@ class State: last_updated: last time this object was updated. context: Context in which it was created domain: Domain of this state. + object_id: Object id of this state. """ __slots__ = [ @@ -769,6 +770,7 @@ class State: "last_updated", "context", "domain", + "object_id", ] def __init__( @@ -802,12 +804,7 @@ class State: self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated self.context = context or Context() - self.domain = split_entity_id(self.entity_id)[0] - - @property - def object_id(self) -> str: - """Object id of this state.""" - return split_entity_id(self.entity_id)[1] + self.domain, self.object_id = split_entity_id(self.entity_id) @property def name(self) -> str: diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 721c1407f37..6261f7b2257 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -61,6 +61,17 @@ _RESERVED_NAMES = {"contextfunction", "evalcontextfunction", "environmentfunctio _GROUP_DOMAIN_PREFIX = "group." +_COLLECTABLE_STATE_ATTRIBUTES = { + "state", + "attributes", + "last_changed", + "last_updated", + "context", + "domain", + "object_id", + "name", +} + @bind_hass def attach(hass: HomeAssistantType, obj: Any) -> None: @@ -477,9 +488,7 @@ class AllStates: def __getattr__(self, name): """Return the domain state.""" if "." in name: - if not valid_entity_id(name): - raise TemplateError(f"Invalid entity ID '{name}'") - return _get_state(self._hass, name) + return _get_state_if_valid(self._hass, name) if name in _RESERVED_NAMES: return None @@ -489,6 +498,10 @@ class AllStates: return DomainStates(self._hass, name) + # Jinja will try __getitem__ first and it avoids the need + # to call is_safe_attribute + __getitem__ = __getattr__ + def _collect_all(self) -> None: render_info = self._hass.data.get(_RENDER_INFO) if render_info is not None: @@ -529,10 +542,11 @@ class DomainStates: def __getattr__(self, name): """Return the states.""" - entity_id = f"{self._domain}.{name}" - if not valid_entity_id(entity_id): - raise TemplateError(f"Invalid entity ID '{entity_id}'") - return _get_state(self._hass, entity_id) + return _get_state_if_valid(self._hass, f"{self._domain}.{name}") + + # Jinja will try __getitem__ first and it avoids the need + # to call is_safe_attribute + __getitem__ = __getattr__ def _collect_domain(self) -> None: entity_collect = self._hass.data.get(_RENDER_INFO) @@ -571,46 +585,96 @@ class TemplateState(State): self._hass = hass self._state = state - def _access_state(self): - state = object.__getattribute__(self, "_state") - hass = object.__getattribute__(self, "_hass") - _collect_state(hass, state.entity_id) - return state + def _collect_state(self): + if _RENDER_INFO in self._hass.data: + self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id) + + # Jinja will try __getitem__ first and it avoids the need + # to call is_safe_attribute + def __getitem__(self, item): + """Return a property as an attribute for jinja.""" + if item in _COLLECTABLE_STATE_ATTRIBUTES: + # _collect_state inlined here for performance + if _RENDER_INFO in self._hass.data: + self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id) + return getattr(self._state, item) + if item == "entity_id": + return self._state.entity_id + if item == "state_with_unit": + return self.state_with_unit + raise KeyError + + @property + def entity_id(self): + """Wrap State.entity_id. + + Intentionally does not collect state + """ + return self._state.entity_id + + @property + def state(self): + """Wrap State.state.""" + self._collect_state() + return self._state.state + + @property + def attributes(self): + """Wrap State.attributes.""" + self._collect_state() + return self._state.attributes + + @property + def last_changed(self): + """Wrap State.last_changed.""" + self._collect_state() + return self._state.last_changed + + @property + def last_updated(self): + """Wrap State.last_updated.""" + self._collect_state() + return self._state.last_updated + + @property + def context(self): + """Wrap State.context.""" + self._collect_state() + return self._state.context + + @property + def domain(self): + """Wrap State.domain.""" + self._collect_state() + return self._state.domain + + @property + def object_id(self): + """Wrap State.object_id.""" + self._collect_state() + return self._state.object_id + + @property + def name(self): + """Wrap State.name.""" + self._collect_state() + return self._state.name @property def state_with_unit(self) -> str: """Return the state concatenated with the unit if available.""" - state = object.__getattribute__(self, "_access_state")() - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if unit is None: - return state.state - return f"{state.state} {unit}" + self._collect_state() + unit = self._state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + return f"{self._state.state} {unit}" if unit else self._state.state def __eq__(self, other: Any) -> bool: """Ensure we collect on equality check.""" - state = object.__getattribute__(self, "_state") - hass = object.__getattribute__(self, "_hass") - _collect_state(hass, state.entity_id) - return super().__eq__(other) - - def __getattribute__(self, name): - """Return an attribute of the state.""" - # This one doesn't count as an access of the state - # since we either found it by looking direct for the ID - # or got it off an iterator. - if name == "entity_id" or name in object.__dict__: - state = object.__getattribute__(self, "_state") - return getattr(state, name) - if name in TemplateState.__dict__: - return object.__getattribute__(self, name) - state = object.__getattribute__(self, "_access_state")() - return getattr(state, name) + self._collect_state() + return self._state.__eq__(other) def __repr__(self) -> str: """Representation of Template State.""" - state = object.__getattribute__(self, "_access_state")() - rep = state.__repr__() - return f"