mirror of
https://github.com/home-assistant/core.git
synced 2025-11-22 17:27:02 +00:00
Compare commits
28 Commits
context-tr
...
add-includ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb97822db9 | ||
|
|
33ffccabd1 | ||
|
|
56de03ce33 | ||
|
|
0cbf7002a8 | ||
|
|
cffceffe04 | ||
|
|
253189805e | ||
|
|
2e91725ac0 | ||
|
|
3b54dddc08 | ||
|
|
9bc3d83a55 | ||
|
|
d62a554cbf | ||
|
|
f071b7cd46 | ||
|
|
37f34f6189 | ||
|
|
27dc5b6d18 | ||
|
|
0bbc2f49a6 | ||
|
|
c121fa25e8 | ||
|
|
660cea8b65 | ||
|
|
c7749ebae1 | ||
|
|
a2acb744b3 | ||
|
|
0d9158689d | ||
|
|
f85e8d6c1f | ||
|
|
9be4cc5af1 | ||
|
|
a141eedf2c | ||
|
|
03040c131c | ||
|
|
3eef50632c | ||
|
|
eff150cd54 | ||
|
|
6dcc94b0a1 | ||
|
|
7201903877 | ||
|
|
5b776307ea |
@@ -25,6 +25,7 @@ from homeassistant.const import (
|
|||||||
ATTR_ASSUMED_STATE,
|
ATTR_ASSUMED_STATE,
|
||||||
ATTR_ATTRIBUTION,
|
ATTR_ATTRIBUTION,
|
||||||
ATTR_DEVICE_CLASS,
|
ATTR_DEVICE_CLASS,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
ATTR_ENTITY_PICTURE,
|
ATTR_ENTITY_PICTURE,
|
||||||
ATTR_FRIENDLY_NAME,
|
ATTR_FRIENDLY_NAME,
|
||||||
ATTR_ICON,
|
ATTR_ICON,
|
||||||
@@ -417,6 +418,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
|||||||
"extra_state_attributes",
|
"extra_state_attributes",
|
||||||
"force_update",
|
"force_update",
|
||||||
"icon",
|
"icon",
|
||||||
|
"included_unique_ids",
|
||||||
"name",
|
"name",
|
||||||
"should_poll",
|
"should_poll",
|
||||||
"state",
|
"state",
|
||||||
@@ -524,6 +526,9 @@ class Entity(
|
|||||||
__capabilities_updated_at_reported: bool = False
|
__capabilities_updated_at_reported: bool = False
|
||||||
__remove_future: asyncio.Future[None] | None = None
|
__remove_future: asyncio.Future[None] | None = None
|
||||||
|
|
||||||
|
# A list of included entity IDs in case the entity represents a group
|
||||||
|
_included_entities: list[str] | None = None
|
||||||
|
|
||||||
# Entity Properties
|
# Entity Properties
|
||||||
_attr_assumed_state: bool = False
|
_attr_assumed_state: bool = False
|
||||||
_attr_attribution: str | None = None
|
_attr_attribution: str | None = None
|
||||||
@@ -539,6 +544,7 @@ class Entity(
|
|||||||
_attr_extra_state_attributes: dict[str, Any]
|
_attr_extra_state_attributes: dict[str, Any]
|
||||||
_attr_force_update: bool
|
_attr_force_update: bool
|
||||||
_attr_icon: str | None
|
_attr_icon: str | None
|
||||||
|
_attr_included_unique_ids: list[str]
|
||||||
_attr_name: str | None
|
_attr_name: str | None
|
||||||
_attr_should_poll: bool = True
|
_attr_should_poll: bool = True
|
||||||
_attr_state: StateType = STATE_UNKNOWN
|
_attr_state: StateType = STATE_UNKNOWN
|
||||||
@@ -1085,6 +1091,21 @@ class Entity(
|
|||||||
available = self.available # only call self.available once per update cycle
|
available = self.available # only call self.available once per update cycle
|
||||||
state = self._stringify_state(available)
|
state = self._stringify_state(available)
|
||||||
if available:
|
if available:
|
||||||
|
if self.included_unique_ids is not None:
|
||||||
|
entity_registry = er.async_get(self.hass)
|
||||||
|
self._included_entities = [
|
||||||
|
entity_id
|
||||||
|
for included_id in self.included_unique_ids
|
||||||
|
if (
|
||||||
|
entity_id := entity_registry.async_get_entity_id(
|
||||||
|
self.platform.domain,
|
||||||
|
self.platform.platform_name,
|
||||||
|
included_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is not None
|
||||||
|
]
|
||||||
|
attr[ATTR_ENTITY_ID] = self._included_entities.copy()
|
||||||
if state_attributes := self.state_attributes:
|
if state_attributes := self.state_attributes:
|
||||||
attr |= state_attributes
|
attr |= state_attributes
|
||||||
if extra_state_attributes := self.extra_state_attributes:
|
if extra_state_attributes := self.extra_state_attributes:
|
||||||
@@ -1374,6 +1395,30 @@ class Entity(
|
|||||||
|
|
||||||
async def add_to_platform_finish(self) -> None:
|
async def add_to_platform_finish(self) -> None:
|
||||||
"""Finish adding an entity to a platform."""
|
"""Finish adding an entity to a platform."""
|
||||||
|
entity_registry = er.async_get(self.hass)
|
||||||
|
|
||||||
|
async def _handle_entity_registry_updated(event: Event[Any]) -> None:
|
||||||
|
"""Handle registry create or update event."""
|
||||||
|
if (
|
||||||
|
event.data["action"] in {"create", "update"}
|
||||||
|
and (entry := entity_registry.async_get(event.data["entity_id"]))
|
||||||
|
and self.included_unique_ids is not None
|
||||||
|
and entry.unique_id in self.included_unique_ids
|
||||||
|
) or (
|
||||||
|
event.data["action"] == "remove"
|
||||||
|
and self._included_entities is not None
|
||||||
|
and event.data["entity_id"] in self._included_entities
|
||||||
|
):
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
if self.included_unique_ids is not None:
|
||||||
|
self.async_on_remove(
|
||||||
|
self.hass.bus.async_listen(
|
||||||
|
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||||
|
_handle_entity_registry_updated,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
await self.async_internal_added_to_hass()
|
await self.async_internal_added_to_hass()
|
||||||
await self.async_added_to_hass()
|
await self.async_added_to_hass()
|
||||||
self._platform_state = EntityPlatformState.ADDED
|
self._platform_state = EntityPlatformState.ADDED
|
||||||
@@ -1633,6 +1678,16 @@ class Entity(
|
|||||||
self.hass, integration_domain=platform_name, module=type(self).__module__
|
self.hass, integration_domain=platform_name, module=type(self).__module__
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def included_unique_ids(self) -> list[str] | None:
|
||||||
|
"""Return the list of unique IDs if the entity represents a group.
|
||||||
|
|
||||||
|
The corresponding entities will be shown as members in the UI.
|
||||||
|
"""
|
||||||
|
if hasattr(self, "_attr_included_unique_ids"):
|
||||||
|
return self._attr_included_unique_ids
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ToggleEntityDescription(EntityDescription, frozen_or_thawed=True):
|
class ToggleEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||||
"""A class that describes toggle entities."""
|
"""A class that describes toggle entities."""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dataclasses
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from typing import Any
|
from typing import Any, final
|
||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
@@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION,
|
ATTR_ATTRIBUTION,
|
||||||
ATTR_DEVICE_CLASS,
|
ATTR_DEVICE_CLASS,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
ATTR_FRIENDLY_NAME,
|
ATTR_FRIENDLY_NAME,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
@@ -1878,6 +1879,7 @@ async def test_change_entity_id(
|
|||||||
self.remove_calls = []
|
self.remove_calls = []
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
|
await super().async_added_to_hass()
|
||||||
self.added_calls.append(None)
|
self.added_calls.append(None)
|
||||||
self.async_on_remove(lambda: result.append(1))
|
self.async_on_remove(lambda: result.append(1))
|
||||||
|
|
||||||
@@ -2896,3 +2898,103 @@ async def test_platform_state_write_from_init_unique_id(
|
|||||||
# The early attempt to write is interpreted as a unique ID collision
|
# The early attempt to write is interpreted as a unique ID collision
|
||||||
assert "Platform test_platform does not generate unique IDs." in caplog.text
|
assert "Platform test_platform does not generate unique IDs." in caplog.text
|
||||||
assert "Entity id already exists - ignoring: test.test" not in caplog.text
|
assert "Entity id already exists - ignoring: test.test" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_included_entities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test included entities are exposed via the entity_id attribute."""
|
||||||
|
|
||||||
|
entity_registry.async_get_or_create(
|
||||||
|
domain="hello",
|
||||||
|
platform="test",
|
||||||
|
unique_id="very_unique_oceans",
|
||||||
|
suggested_object_id="oceans",
|
||||||
|
)
|
||||||
|
entity_registry.async_get_or_create(
|
||||||
|
domain="hello",
|
||||||
|
platform="test",
|
||||||
|
unique_id="very_unique_continents",
|
||||||
|
suggested_object_id="continents",
|
||||||
|
)
|
||||||
|
entity_registry.async_get_or_create(
|
||||||
|
domain="hello",
|
||||||
|
platform="test",
|
||||||
|
unique_id="very_unique_moon",
|
||||||
|
suggested_object_id="moon",
|
||||||
|
)
|
||||||
|
|
||||||
|
class MockHelloBaseClass(entity.Entity):
|
||||||
|
"""Domain base entity platform domain Hello."""
|
||||||
|
|
||||||
|
@final
|
||||||
|
@property
|
||||||
|
def state_attributes(self) -> dict[str, Any]:
|
||||||
|
"""Return the state attributes."""
|
||||||
|
return {"extra": "beer"}
|
||||||
|
|
||||||
|
class MockHelloIncludedEntitiesClass(MockHelloBaseClass, entity.Entity):
|
||||||
|
"""Mock hello grouped entity class for a test integration."""
|
||||||
|
|
||||||
|
platform = MockEntityPlatform(hass, domain="hello", platform_name="test")
|
||||||
|
mock_entity = MockHelloIncludedEntitiesClass()
|
||||||
|
mock_entity.hass = hass
|
||||||
|
mock_entity.entity_id = "hello.universe"
|
||||||
|
mock_entity.unique_id = "very_unique_universe"
|
||||||
|
mock_entity._attr_included_unique_ids = [
|
||||||
|
"very_unique_continents",
|
||||||
|
"very_unique_oceans",
|
||||||
|
]
|
||||||
|
|
||||||
|
await platform.async_add_entities([mock_entity])
|
||||||
|
|
||||||
|
# Initiate mock grouped entity for hello domain
|
||||||
|
mock_entity.async_schedule_update_ha_state(True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(mock_entity.entity_id)
|
||||||
|
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.continents", "hello.oceans"]
|
||||||
|
|
||||||
|
# Add an entity to the group of included entities
|
||||||
|
mock_entity._attr_included_unique_ids = [
|
||||||
|
"very_unique_continents",
|
||||||
|
"very_unique_moon",
|
||||||
|
"very_unique_oceans",
|
||||||
|
]
|
||||||
|
mock_entity.async_write_ha_state()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(mock_entity.entity_id)
|
||||||
|
assert state.attributes.get("extra") == "beer"
|
||||||
|
assert state.attributes.get(ATTR_ENTITY_ID) == [
|
||||||
|
"hello.continents",
|
||||||
|
"hello.moon",
|
||||||
|
"hello.oceans",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Remove an entity from the group of included entities
|
||||||
|
mock_entity._attr_included_unique_ids = ["very_unique_moon", "very_unique_oceans"]
|
||||||
|
|
||||||
|
mock_entity.async_write_ha_state()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(mock_entity.entity_id)
|
||||||
|
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon", "hello.oceans"]
|
||||||
|
|
||||||
|
# Rename an included entity via the registry entity
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
entity_id="hello.moon", new_entity_id="hello.moon_light"
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(mock_entity.entity_id)
|
||||||
|
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon_light", "hello.oceans"]
|
||||||
|
|
||||||
|
# Remove an included entity from the registry entity
|
||||||
|
entity_registry.async_remove(entity_id="hello.oceans")
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(mock_entity.entity_id)
|
||||||
|
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon_light"]
|
||||||
|
|||||||
Reference in New Issue
Block a user