mirror of
https://github.com/home-assistant/core.git
synced 2026-02-20 20:59:14 +00:00
Compare commits
16 Commits
trane_clim
...
mqtt-entit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3796be6e6c | ||
|
|
bd1d6b747f | ||
|
|
60829089b5 | ||
|
|
fd32b138f2 | ||
|
|
cdf2b61e89 | ||
|
|
03792e88f8 | ||
|
|
564f59b127 | ||
|
|
e864cb23e3 | ||
|
|
1a50cb3852 | ||
|
|
d5dc4d93fe | ||
|
|
29ca5adb7c | ||
|
|
4579949ee1 | ||
|
|
be8dcd49ce | ||
|
|
5575cd2ddc | ||
|
|
597d70fa6f | ||
|
|
477bd7d32c |
@@ -20,9 +20,6 @@ from homeassistant.const import (
|
||||
CONF_ENTITIES,
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
SERVICE_LOCK,
|
||||
SERVICE_OPEN,
|
||||
SERVICE_UNLOCK,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
@@ -32,6 +29,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.group import GenericGroup
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .entity import GroupEntity
|
||||
@@ -117,47 +115,13 @@ class LockGroup(GroupEntity, LockEntity):
|
||||
) -> None:
|
||||
"""Initialize a lock group."""
|
||||
self._entity_ids = entity_ids
|
||||
self.group = GenericGroup(self, entity_ids)
|
||||
self._attr_supported_features = LockEntityFeature.OPEN
|
||||
|
||||
self._attr_name = name
|
||||
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
|
||||
self._attr_unique_id = unique_id
|
||||
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
"""Forward the lock command to all locks in the group."""
|
||||
data = {ATTR_ENTITY_ID: self._entity_ids}
|
||||
_LOGGER.debug("Forwarded lock command: %s", data)
|
||||
|
||||
await self.hass.services.async_call(
|
||||
LOCK_DOMAIN,
|
||||
SERVICE_LOCK,
|
||||
data,
|
||||
blocking=True,
|
||||
context=self._context,
|
||||
)
|
||||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Forward the unlock command to all locks in the group."""
|
||||
data = {ATTR_ENTITY_ID: self._entity_ids}
|
||||
await self.hass.services.async_call(
|
||||
LOCK_DOMAIN,
|
||||
SERVICE_UNLOCK,
|
||||
data,
|
||||
blocking=True,
|
||||
context=self._context,
|
||||
)
|
||||
|
||||
async def async_open(self, **kwargs: Any) -> None:
|
||||
"""Forward the open command to all locks in the group."""
|
||||
data = {ATTR_ENTITY_ID: self._entity_ids}
|
||||
await self.hass.services.async_call(
|
||||
LOCK_DOMAIN,
|
||||
SERVICE_OPEN,
|
||||
data,
|
||||
blocking=True,
|
||||
context=self._context,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_update_group_state(self) -> None:
|
||||
"""Query all members and determine the lock group state."""
|
||||
|
||||
@@ -73,6 +73,7 @@ ABBREVIATIONS = {
|
||||
"fan_mode_stat_t": "fan_mode_state_topic",
|
||||
"frc_upd": "force_update",
|
||||
"g_tpl": "green_template",
|
||||
"grp": "group",
|
||||
"hs_cmd_t": "hs_command_topic",
|
||||
"hs_cmd_tpl": "hs_command_template",
|
||||
"hs_stat_t": "hs_state_topic",
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_GROUP,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
@@ -23,6 +24,7 @@ from .util import valid_publish_topic, valid_qos_schema, valid_subscribe_topic
|
||||
SCHEMA_BASE = {
|
||||
vol.Optional(CONF_QOS, default=DEFAULT_QOS): valid_qos_schema,
|
||||
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
|
||||
vol.Optional(CONF_GROUP): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
|
||||
MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE)
|
||||
|
||||
@@ -110,6 +110,7 @@ CONF_FLASH_TIME_SHORT = "flash_time_short"
|
||||
CONF_GET_POSITION_TEMPLATE = "position_template"
|
||||
CONF_GET_POSITION_TOPIC = "position_topic"
|
||||
CONF_GREEN_TEMPLATE = "green_template"
|
||||
CONF_GROUP = "group"
|
||||
CONF_HS_COMMAND_TEMPLATE = "hs_command_template"
|
||||
CONF_HS_COMMAND_TOPIC = "hs_command_topic"
|
||||
CONF_HS_STATE_TOPIC = "hs_state_topic"
|
||||
|
||||
@@ -49,6 +49,7 @@ from homeassistant.helpers.event import (
|
||||
async_track_device_registry_updated_event,
|
||||
async_track_entity_registry_updated_event,
|
||||
)
|
||||
from homeassistant.helpers.group import IntegrationSpecificGroup
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
|
||||
from homeassistant.helpers.typing import (
|
||||
@@ -79,6 +80,7 @@ from .const import (
|
||||
CONF_ENABLED_BY_DEFAULT,
|
||||
CONF_ENCODING,
|
||||
CONF_ENTITY_PICTURE,
|
||||
CONF_GROUP,
|
||||
CONF_HW_VERSION,
|
||||
CONF_IDENTIFIERS,
|
||||
CONF_JSON_ATTRS_TEMPLATE,
|
||||
@@ -136,6 +138,7 @@ MQTT_ATTRIBUTES_BLOCKED = {
|
||||
"device_class",
|
||||
"device_info",
|
||||
"entity_category",
|
||||
"entity_id",
|
||||
"entity_picture",
|
||||
"entity_registry_enabled_default",
|
||||
"extra_state_attributes",
|
||||
@@ -463,7 +466,7 @@ def async_setup_entity_entry_helper(
|
||||
|
||||
|
||||
class MqttAttributesMixin(Entity):
|
||||
"""Mixin used for platforms that support JSON attributes."""
|
||||
"""Mixin used for platforms that support JSON attributes and group entities."""
|
||||
|
||||
_attributes_extra_blocked: frozenset[str] = frozenset()
|
||||
_attr_tpl: Callable[[ReceivePayloadType], ReceivePayloadType] | None = None
|
||||
@@ -471,10 +474,13 @@ class MqttAttributesMixin(Entity):
|
||||
[MessageCallbackType, set[str] | None, ReceiveMessage], None
|
||||
]
|
||||
_process_update_extra_state_attributes: Callable[[dict[str, Any]], None]
|
||||
group: IntegrationSpecificGroup
|
||||
|
||||
def __init__(self, config: ConfigType) -> None:
|
||||
"""Initialize the JSON attributes mixin."""
|
||||
"""Initialize the JSON attributes and handle group entities."""
|
||||
self._attributes_sub_state: dict[str, EntitySubscription] = {}
|
||||
if CONF_GROUP in config:
|
||||
self.group = IntegrationSpecificGroup(self, config[CONF_GROUP])
|
||||
self._attributes_config = config
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -485,6 +491,8 @@ class MqttAttributesMixin(Entity):
|
||||
|
||||
def attributes_prepare_discovery_update(self, config: DiscoveryInfoType) -> None:
|
||||
"""Handle updated discovery message."""
|
||||
if hasattr(self, "group") and CONF_GROUP in config:
|
||||
self.group.included_unique_ids = config[CONF_GROUP]
|
||||
self._attributes_config = config
|
||||
self._attributes_prepare_subscribe_topics()
|
||||
|
||||
@@ -546,7 +554,7 @@ class MqttAttributesMixin(Entity):
|
||||
_LOGGER.warning("Erroneous JSON: %s", payload)
|
||||
else:
|
||||
if isinstance(json_dict, dict):
|
||||
filtered_dict = {
|
||||
filtered_dict: dict[str, Any] = {
|
||||
k: v
|
||||
for k, v in json_dict.items()
|
||||
if k not in MQTT_ATTRIBUTES_BLOCKED
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.core import State, callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.group import IntegrationSpecificGroup
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
|
||||
@@ -51,6 +52,18 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
|
||||
meta = self.entity_data.entity.info_object
|
||||
self._attr_unique_id = meta.unique_id
|
||||
|
||||
if self.entity_data.is_group_entity:
|
||||
group_proxy = self.entity_data.group_proxy
|
||||
assert group_proxy is not None
|
||||
platform = self.entity_data.entity.PLATFORM
|
||||
unique_ids = [
|
||||
entity.info_object.unique_id
|
||||
for member in group_proxy.group.members
|
||||
for entity in member.device.platform_entities.values()
|
||||
if platform == entity.PLATFORM
|
||||
]
|
||||
self.group = IntegrationSpecificGroup(self, unique_ids)
|
||||
|
||||
if meta.entity_category is not None:
|
||||
self._attr_entity_category = EntityCategory(meta.entity_category)
|
||||
|
||||
|
||||
@@ -332,6 +332,9 @@ ATTR_NAME: Final = "name"
|
||||
# Contains one string or a list of strings, each being an entity id
|
||||
ATTR_ENTITY_ID: Final = "entity_id"
|
||||
|
||||
# Contains a list of entity ids that are members of a group
|
||||
ATTR_GROUP_ENTITIES: Final = "group_entities"
|
||||
|
||||
# Contains one string, the config entry ID
|
||||
ATTR_CONFIG_ENTRY_ID: Final = "config_entry_id"
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_GROUP_ENTITIES,
|
||||
ATTR_ICON,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
@@ -54,13 +55,15 @@ from homeassistant.loader import async_suggest_report_issue, bind_hass
|
||||
from homeassistant.util import ensure_unique_string, slugify
|
||||
from homeassistant.util.frozen_dataclass_compat import FrozenOrThawed
|
||||
|
||||
from . import device_registry as dr, entity_registry as er, singleton
|
||||
from . import device_registry as dr, entity_registry as er
|
||||
from .device_registry import DeviceInfo, EventDeviceRegistryUpdatedData
|
||||
from .event import (
|
||||
async_track_device_registry_updated_event,
|
||||
async_track_entity_registry_updated_event,
|
||||
)
|
||||
from .frame import report_non_thread_safe_operation
|
||||
from .frame import report_non_thread_safe_operation, report_usage
|
||||
from .group import Group
|
||||
from .singleton import singleton
|
||||
from .typing import UNDEFINED, StateType, UndefinedType
|
||||
|
||||
timer = time.time
|
||||
@@ -90,7 +93,7 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
@singleton.singleton(DATA_ENTITY_SOURCE)
|
||||
@singleton(DATA_ENTITY_SOURCE)
|
||||
def entity_sources(hass: HomeAssistant) -> dict[str, EntityInfo]:
|
||||
"""Get the entity sources.
|
||||
|
||||
@@ -457,6 +460,10 @@ class Entity(
|
||||
# Only handled internally, never to be used by integrations.
|
||||
internal_integration_suggested_object_id: str | None
|
||||
|
||||
# A group information in case the entity represents a group
|
||||
group: Group | None
|
||||
__group: Group | None = None
|
||||
|
||||
# If we reported if this entity was slow
|
||||
_slow_reported = False
|
||||
|
||||
@@ -1064,6 +1071,12 @@ class Entity(
|
||||
entry = self.registry_entry
|
||||
|
||||
capability_attr = self.capability_attributes
|
||||
if self.__group is not None:
|
||||
capability_attr = capability_attr.copy() if capability_attr else {}
|
||||
capability_attr[ATTR_GROUP_ENTITIES] = (
|
||||
self.__group.included_entity_ids.copy()
|
||||
)
|
||||
|
||||
attr = capability_attr.copy() if capability_attr else {}
|
||||
|
||||
available = self.available # only call self.available once per update cycle
|
||||
@@ -1503,6 +1516,17 @@ class Entity(
|
||||
)
|
||||
self._async_subscribe_device_updates()
|
||||
|
||||
if hasattr(self, "group") and self.group is not None:
|
||||
if not isinstance(self.group, Group):
|
||||
report_usage( # type: ignore[unreachable]
|
||||
f"sets a `group` attribute on entity {self.entity_id} which is "
|
||||
"not a `Group` instance",
|
||||
breaks_in_ha_version="2027.2",
|
||||
)
|
||||
else:
|
||||
self.__group = self.group
|
||||
self.__group.async_added_to_hass()
|
||||
|
||||
async def async_internal_will_remove_from_hass(self) -> None:
|
||||
"""Run when entity will be removed from hass.
|
||||
|
||||
@@ -1513,6 +1537,9 @@ class Entity(
|
||||
if self.platform:
|
||||
del entity_sources(self.hass)[self.entity_id]
|
||||
|
||||
if self.__group is not None:
|
||||
self.__group.async_will_remove_from_hass()
|
||||
|
||||
@callback
|
||||
def _async_registry_updated(
|
||||
self, event: Event[er.EventEntityRegistryUpdatedData]
|
||||
|
||||
@@ -3,19 +3,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
|
||||
from . import entity_registry as er
|
||||
from .singleton import singleton
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .entity import Entity
|
||||
|
||||
DATA_GROUP_ENTITIES = "group_entities"
|
||||
ENTITY_PREFIX = "group."
|
||||
|
||||
|
||||
class Group:
|
||||
"""A group base class."""
|
||||
|
||||
_entity: Entity
|
||||
|
||||
def __init__(self, entity: Entity) -> None:
|
||||
"""Initialize the group."""
|
||||
self._entity = entity
|
||||
|
||||
@property
|
||||
def included_entity_ids(self) -> list[str]:
|
||||
"""Return the list of entity IDs."""
|
||||
raise NotImplementedError
|
||||
|
||||
@callback
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Handle when the entity is added to hass."""
|
||||
entity = self._entity
|
||||
get_group_entities(entity.hass)[entity.entity_id] = entity
|
||||
|
||||
@callback
|
||||
def async_will_remove_from_hass(self) -> None:
|
||||
"""Handle when the entity will be removed from hass."""
|
||||
entity = self._entity
|
||||
del get_group_entities(entity.hass)[entity.entity_id]
|
||||
|
||||
|
||||
class GenericGroup(Group):
|
||||
"""A generic group."""
|
||||
|
||||
def __init__(self, entity: Entity, included_entity_ids: list[str]) -> None:
|
||||
"""Initialize the group."""
|
||||
super().__init__(entity)
|
||||
self._included_entity_ids = included_entity_ids
|
||||
|
||||
@cached_property
|
||||
def included_entity_ids(self) -> list[str]:
|
||||
"""Return the list of entity IDs."""
|
||||
return self._included_entity_ids
|
||||
|
||||
|
||||
class IntegrationSpecificGroup(Group):
|
||||
"""An integration-specific group."""
|
||||
|
||||
_included_entity_ids: list[str] | None = None
|
||||
_included_unique_ids: list[str]
|
||||
|
||||
def __init__(self, entity: Entity, included_unique_ids: list[str]) -> None:
|
||||
"""Initialize the group."""
|
||||
super().__init__(entity)
|
||||
self._included_unique_ids = included_unique_ids
|
||||
|
||||
@cached_property
|
||||
def included_entity_ids(self) -> list[str]:
|
||||
"""Return the list of entity IDs."""
|
||||
entity_registry = er.async_get(self._entity.hass)
|
||||
self._included_entity_ids = [
|
||||
entity_id
|
||||
for unique_id in self.included_unique_ids
|
||||
if (
|
||||
entity_id := entity_registry.async_get_entity_id(
|
||||
self._entity.platform.domain,
|
||||
self._entity.platform.platform_name,
|
||||
unique_id,
|
||||
)
|
||||
)
|
||||
is not None
|
||||
]
|
||||
return self._included_entity_ids
|
||||
|
||||
@property
|
||||
def included_unique_ids(self) -> list[str]:
|
||||
"""Return the list of unique IDs."""
|
||||
return self._included_unique_ids
|
||||
|
||||
@included_unique_ids.setter
|
||||
def included_unique_ids(self, value: list[str]) -> None:
|
||||
"""Set the list of unique IDs."""
|
||||
self._included_unique_ids = value
|
||||
if self._included_entity_ids is not None:
|
||||
self._included_entity_ids = None
|
||||
del self.included_entity_ids
|
||||
|
||||
@callback
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Handle when the entity is added to hass."""
|
||||
super().async_added_to_hass()
|
||||
|
||||
entity = self._entity
|
||||
entity_registry = er.async_get(entity.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 entry.unique_id in self.included_unique_ids
|
||||
) or (
|
||||
event.data["action"] == "remove"
|
||||
and self._included_entity_ids is not None
|
||||
and event.data["entity_id"] in self._included_entity_ids
|
||||
):
|
||||
if self._included_entity_ids is not None:
|
||||
self._included_entity_ids = None
|
||||
del self.included_entity_ids
|
||||
entity.async_write_ha_state()
|
||||
|
||||
entity.async_on_remove(
|
||||
entity.hass.bus.async_listen(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
_handle_entity_registry_updated,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@singleton(DATA_GROUP_ENTITIES)
|
||||
def get_group_entities(hass: HomeAssistant) -> dict[str, Entity]:
|
||||
"""Get the group entities.
|
||||
|
||||
Items are added to this dict by Group.async_added_to_hass and
|
||||
removed by Group.async_will_remove_from_hass.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
def expand_entity_ids(hass: HomeAssistant, entity_ids: Iterable[Any]) -> list[str]:
|
||||
"""Return entity_ids with group entity ids replaced by their members.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
group_entities = get_group_entities(hass)
|
||||
|
||||
found_ids: list[str] = []
|
||||
for entity_id in entity_ids:
|
||||
if not isinstance(entity_id, str) or entity_id in (
|
||||
@@ -25,8 +162,22 @@ def expand_entity_ids(hass: HomeAssistant, entity_ids: Iterable[Any]) -> list[st
|
||||
continue
|
||||
|
||||
entity_id = entity_id.lower()
|
||||
|
||||
# If entity_id points at a group, expand it
|
||||
if entity_id.startswith(ENTITY_PREFIX):
|
||||
if (entity := group_entities.get(entity_id)) is not None and isinstance(
|
||||
entity.group, GenericGroup
|
||||
):
|
||||
child_entities = entity.group.included_entity_ids
|
||||
if entity_id in child_entities:
|
||||
child_entities = list(child_entities)
|
||||
child_entities.remove(entity_id)
|
||||
found_ids.extend(
|
||||
ent_id
|
||||
for ent_id in expand_entity_ids(hass, child_entities)
|
||||
if ent_id not in found_ids
|
||||
)
|
||||
# If entity_id points at an old-style group, expand it
|
||||
elif entity_id.startswith(ENTITY_PREFIX):
|
||||
child_entities = get_entity_ids(hass, entity_id)
|
||||
if entity_id in child_entities:
|
||||
child_entities = list(child_entities)
|
||||
|
||||
@@ -82,6 +82,7 @@ light:
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import call, patch
|
||||
|
||||
@@ -100,6 +101,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.json import json_loads
|
||||
|
||||
from .common import (
|
||||
@@ -169,6 +171,60 @@ COLOR_MODES_CONFIG = {
|
||||
}
|
||||
}
|
||||
|
||||
GROUP_MEMBER_1_TOPIC = "homeassistant/light/member_1/config"
|
||||
GROUP_MEMBER_2_TOPIC = "homeassistant/light/member_2/config"
|
||||
GROUP_MEMBER_3_TOPIC = "homeassistant/light/member_3/config"
|
||||
GROUP_TOPIC = "homeassistant/light/group/config"
|
||||
GROUP_DISCOVERY_MEMBER_1_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-member1",
|
||||
"unique_id": "very_unique_member1",
|
||||
"name": "member1",
|
||||
"default_entity_id": "light.member1",
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_MEMBER_2_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-member2",
|
||||
"unique_id": "very_unique_member2",
|
||||
"name": "member2",
|
||||
"default_entity_id": "light.member2",
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_MEMBER_3_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-member3",
|
||||
"unique_id": "very_unique_member3",
|
||||
"name": "member3",
|
||||
"default_entity_id": "light.member3",
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_LIGHT_GROUP_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-group",
|
||||
"state_topic": "test-state-topic-group",
|
||||
"unique_id": "very_unique_group",
|
||||
"name": "group",
|
||||
"default_entity_id": "light.group",
|
||||
"group": ["very_unique_member1", "very_unique_member2"],
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_LIGHT_GROUP_CONFIG_EXPANDED = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-group",
|
||||
"state_topic": "test-state-topic-group",
|
||||
"unique_id": "very_unique_group",
|
||||
"name": "group",
|
||||
"default_entity_id": "light.group",
|
||||
"group": ["very_unique_member1", "very_unique_member2", "very_unique_member3"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class JsonValidator:
|
||||
"""Helper to compare JSON."""
|
||||
@@ -1859,6 +1915,114 @@ async def test_white_scale(
|
||||
assert state.attributes.get("brightness") == 129
|
||||
|
||||
|
||||
async def test_light_group_discovery_members_before_group(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the discovery of a light group and linked entity IDs.
|
||||
|
||||
The members are discovered first, so they are known in the entity registry.
|
||||
"""
|
||||
await mqtt_mock_entry()
|
||||
# Discover light group members
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, GROUP_DISCOVERY_MEMBER_1_CONFIG)
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_2_TOPIC, GROUP_DISCOVERY_MEMBER_2_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Discover group
|
||||
async_fire_mqtt_message(hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is not None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") == [
|
||||
"light.member1",
|
||||
"light.member2",
|
||||
]
|
||||
|
||||
# Now create and discover a new member
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_3_TOPIC, GROUP_DISCOVERY_MEMBER_3_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Update the group discovery
|
||||
async_fire_mqtt_message(
|
||||
hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG_EXPANDED
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is not None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
assert hass.states.get("light.member3") is not None
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") == [
|
||||
"light.member1",
|
||||
"light.member2",
|
||||
"light.member3",
|
||||
]
|
||||
|
||||
|
||||
async def test_light_group_discovery_group_before_members(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the discovery of a light group and linked entity IDs.
|
||||
|
||||
The group is discovered first, so the group members are
|
||||
not (all) known yet in the entity registry.
|
||||
The entity property should be updates as soon as member entities
|
||||
are discovered, updated or removed.
|
||||
"""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
# Discover group
|
||||
async_fire_mqtt_message(hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Discover light group members
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, GROUP_DISCOVERY_MEMBER_1_CONFIG)
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_2_TOPIC, GROUP_DISCOVERY_MEMBER_2_CONFIG)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is not None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") == [
|
||||
"light.member1",
|
||||
"light.member2",
|
||||
]
|
||||
|
||||
# Remove member 1
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, "")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") == ["light.member2"]
|
||||
|
||||
# Rename member 2
|
||||
entity_registry.async_update_entity(
|
||||
"light.member2", new_entity_id="light.member2_updated"
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") == ["light.member2_updated"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
@@ -2040,7 +2204,7 @@ async def test_custom_availability_payload(
|
||||
)
|
||||
|
||||
|
||||
async def test_setting_attribute_via_mqtt_json_message(
|
||||
async def test_setting_attribute_via_mqtt_json_message_single_light(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the setting of attribute via MQTT with JSON payload."""
|
||||
@@ -2049,6 +2213,54 @@ async def test_setting_attribute_via_mqtt_json_message(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
help_custom_config(
|
||||
light.DOMAIN,
|
||||
DEFAULT_CONFIG,
|
||||
(
|
||||
{
|
||||
"unique_id": "very_unique_member_1",
|
||||
"name": "Part 1",
|
||||
"default_entity_id": "light.member_1",
|
||||
},
|
||||
{
|
||||
"unique_id": "very_unique_member_2",
|
||||
"name": "Part 2",
|
||||
"default_entity_id": "light.member_2",
|
||||
},
|
||||
{
|
||||
"unique_id": "very_unique_group",
|
||||
"name": "My group",
|
||||
"default_entity_id": "light.my_group",
|
||||
"json_attributes_topic": "attr-topic",
|
||||
"group": [
|
||||
"very_unique_member_1",
|
||||
"very_unique_member_2",
|
||||
"member_3_not_exists",
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_setting_attribute_via_mqtt_json_message_light_group(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the setting of attribute via MQTT with JSON payload."""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
async_fire_mqtt_message(hass, "attr-topic", '{ "val": "100" }')
|
||||
state = hass.states.get("light.my_group")
|
||||
|
||||
assert state and state.attributes.get("val") == "100"
|
||||
assert state.attributes.get("group_entities") == [
|
||||
"light.member_1",
|
||||
"light.member_2",
|
||||
]
|
||||
|
||||
|
||||
async def test_setting_blocked_attribute_via_mqtt_json_message(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
"""Test the group helper."""
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_GROUP_ENTITIES, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import group
|
||||
from homeassistant.helpers import entity_registry as er, group
|
||||
from homeassistant.helpers.group import (
|
||||
GenericGroup,
|
||||
IntegrationSpecificGroup,
|
||||
get_group_entities,
|
||||
)
|
||||
|
||||
from tests.common import MockEntity, MockEntityPlatform
|
||||
|
||||
|
||||
async def test_expand_entity_ids(hass: HomeAssistant) -> None:
|
||||
@@ -104,3 +111,349 @@ async def test_get_entity_ids_with_non_existing_group_name(hass: HomeAssistant)
|
||||
async def test_get_entity_ids_with_non_group_state(hass: HomeAssistant) -> None:
|
||||
"""Test get_entity_ids with a non group state."""
|
||||
assert group.get_entity_ids(hass, "switch.AC") == []
|
||||
|
||||
|
||||
async def test_get_group_entities(hass: HomeAssistant) -> None:
|
||||
"""Test get_group_entities returns registered group entities."""
|
||||
assert get_group_entities(hass) == {}
|
||||
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.test_group", unique_id="test_group_1")
|
||||
ent.group = GenericGroup(ent, ["light.bulb1", "light.bulb2"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
group_entities = get_group_entities(hass)
|
||||
assert "light.test_group" in group_entities
|
||||
assert group_entities["light.test_group"] is ent
|
||||
|
||||
|
||||
async def test_group_entity_removed_from_registry(hass: HomeAssistant) -> None:
|
||||
"""Test group entity is removed from get_group_entities on removal."""
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.test_group", unique_id="test_group_2")
|
||||
ent.group = GenericGroup(ent, ["light.bulb1", "light.bulb2"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
assert "light.test_group" in get_group_entities(hass)
|
||||
|
||||
await platform.async_remove_entity(ent.entity_id)
|
||||
await hass.async_block_till_done()
|
||||
assert "light.test_group" not in get_group_entities(hass)
|
||||
|
||||
|
||||
async def test_multiple_group_entities(hass: HomeAssistant) -> None:
|
||||
"""Test multiple group entities can be registered and work independently."""
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent1 = MockEntity(entity_id="light.group1", unique_id="multi_1")
|
||||
ent1.group = GenericGroup(ent1, ["light.a", "light.b"])
|
||||
|
||||
ent2 = MockEntity(entity_id="light.group2", unique_id="multi_2")
|
||||
ent2.group = GenericGroup(ent2, ["light.c", "light.d"])
|
||||
|
||||
await platform.async_add_entities([ent1, ent2])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
group_entities = get_group_entities(hass)
|
||||
assert "light.group1" in group_entities
|
||||
assert "light.group2" in group_entities
|
||||
|
||||
expanded1 = group.expand_entity_ids(hass, ["light.group1"])
|
||||
expanded2 = group.expand_entity_ids(hass, ["light.group2"])
|
||||
|
||||
assert sorted(expanded1) == ["light.a", "light.b"]
|
||||
assert sorted(expanded2) == ["light.c", "light.d"]
|
||||
|
||||
|
||||
async def test_generic_group_included_entity_ids(hass: HomeAssistant) -> None:
|
||||
"""Test GenericGroup included_entity_ids property."""
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.test_group")
|
||||
ent.group = GenericGroup(ent, ["light.bulb1", "light.bulb2"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ent.group.included_entity_ids == ["light.bulb1", "light.bulb2"]
|
||||
|
||||
|
||||
async def test_expand_entity_ids_with_generic_group(hass: HomeAssistant) -> None:
|
||||
"""Test expand_entity_ids with GenericGroup entities."""
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.living_room_group", unique_id="living_room")
|
||||
ent.group = GenericGroup(ent, ["light.lamp1", "light.lamp2", "light.lamp3"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set("light.lamp1", STATE_ON)
|
||||
hass.states.async_set("light.lamp2", STATE_OFF)
|
||||
hass.states.async_set("light.lamp3", STATE_ON)
|
||||
|
||||
expanded = group.expand_entity_ids(hass, ["light.living_room_group"])
|
||||
assert sorted(expanded) == ["light.lamp1", "light.lamp2", "light.lamp3"]
|
||||
|
||||
|
||||
async def test_expand_entity_ids_with_generic_group_recursive(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test expand_entity_ids with nested GenericGroup entities."""
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
inner_group = MockEntity(entity_id="light.inner_group", unique_id="inner")
|
||||
inner_group.group = GenericGroup(inner_group, ["light.lamp1", "light.lamp2"])
|
||||
|
||||
outer_group = MockEntity(entity_id="light.outer_group", unique_id="outer")
|
||||
outer_group.group = GenericGroup(outer_group, ["light.inner_group", "light.lamp3"])
|
||||
|
||||
await platform.async_add_entities([inner_group, outer_group])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
expanded = group.expand_entity_ids(hass, ["light.outer_group"])
|
||||
assert sorted(expanded) == ["light.lamp1", "light.lamp2", "light.lamp3"]
|
||||
|
||||
|
||||
async def test_expand_entity_ids_with_generic_group_self_reference(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test expand_entity_ids handles GenericGroup with self-reference."""
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.self_ref_group", unique_id="self_ref")
|
||||
ent.group = GenericGroup(
|
||||
ent, ["light.self_ref_group", "light.bulb1", "light.bulb2"]
|
||||
)
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
expanded = group.expand_entity_ids(hass, ["light.self_ref_group"])
|
||||
assert sorted(expanded) == ["light.bulb1", "light.bulb2"]
|
||||
|
||||
|
||||
async def test_entity_group_attribute_in_state(hass: HomeAssistant) -> None:
|
||||
"""Test ATTR_GROUP_ENTITIES is included in entity state attributes."""
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.group_with_attrs", unique_id="attrs_test")
|
||||
ent.group = GenericGroup(ent, ["light.lamp1", "light.lamp2"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.group_with_attrs")
|
||||
assert state is not None
|
||||
assert ATTR_GROUP_ENTITIES in state.attributes
|
||||
assert state.attributes[ATTR_GROUP_ENTITIES] == ["light.lamp1", "light.lamp2"]
|
||||
|
||||
|
||||
async def test_integration_specific_group_included_entity_ids(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test IntegrationSpecificGroup resolves entity IDs from unique IDs."""
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_1", suggested_object_id="member1"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_2", suggested_object_id="member2"
|
||||
)
|
||||
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.integration_group", unique_id="int_group")
|
||||
ent.group = IntegrationSpecificGroup(ent, ["unique_1", "unique_2"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert sorted(ent.group.included_entity_ids) == ["light.member1", "light.member2"]
|
||||
|
||||
|
||||
async def test_integration_specific_group_missing_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test IntegrationSpecificGroup handles missing entities."""
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_1", suggested_object_id="member1"
|
||||
)
|
||||
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.partial_group", unique_id="partial")
|
||||
ent.group = IntegrationSpecificGroup(
|
||||
ent, ["unique_1", "unique_2", "unique_missing"]
|
||||
)
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ent.group.included_entity_ids == ["light.member1"]
|
||||
|
||||
|
||||
async def test_integration_specific_group_included_unique_ids_setter(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test IntegrationSpecificGroup included_unique_ids setter clears cache."""
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_1", suggested_object_id="member1"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_2", suggested_object_id="member2"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_3", suggested_object_id="member3"
|
||||
)
|
||||
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.dynamic_group", unique_id="dynamic")
|
||||
ent.group = IntegrationSpecificGroup(ent, ["unique_1"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
assert ent.group.included_entity_ids == ["light.member1"]
|
||||
|
||||
ent.group.included_unique_ids = ["unique_2", "unique_3"]
|
||||
|
||||
assert sorted(ent.group.included_entity_ids) == ["light.member2", "light.member3"]
|
||||
|
||||
|
||||
async def test_integration_specific_group_member_added(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test IntegrationSpecificGroup updates when member is added to registry."""
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_1", suggested_object_id="member1"
|
||||
)
|
||||
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.registry_group", unique_id="reg_group")
|
||||
ent.group = IntegrationSpecificGroup(ent, ["unique_1", "unique_2"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
assert ent.group.included_entity_ids == ["light.member1"]
|
||||
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_2", suggested_object_id="member2"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert sorted(ent.group.included_entity_ids) == ["light.member1", "light.member2"]
|
||||
|
||||
|
||||
async def test_integration_specific_group_member_removed(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test IntegrationSpecificGroup updates when member is removed from registry."""
|
||||
entry1 = entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_1", suggested_object_id="member1"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_2", suggested_object_id="member2"
|
||||
)
|
||||
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.remove_group", unique_id="rem_group")
|
||||
ent.group = IntegrationSpecificGroup(ent, ["unique_1", "unique_2"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert sorted(ent.group.included_entity_ids) == ["light.member1", "light.member2"]
|
||||
|
||||
entity_registry.async_remove(entry1.entity_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ent.group.included_entity_ids == ["light.member2"]
|
||||
|
||||
|
||||
async def test_integration_specific_group_member_renamed(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test IntegrationSpecificGroup updates when member entity_id is renamed."""
|
||||
entry = entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_1", suggested_object_id="original_name"
|
||||
)
|
||||
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.group", unique_id="grp")
|
||||
ent.group = IntegrationSpecificGroup(ent, ["unique_1"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
assert ent.group.included_entity_ids == ["light.original_name"]
|
||||
|
||||
entity_registry.async_update_entity(entry.entity_id, new_entity_id="light.new_name")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ent.group.included_entity_ids == ["light.new_name"]
|
||||
|
||||
|
||||
async def test_integration_specific_group_attribute_in_state(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test ATTR_GROUP_ENTITIES is included in IntegrationSpecificGroup state."""
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_1", suggested_object_id="member1"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_2", suggested_object_id="member2"
|
||||
)
|
||||
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.int_group_attrs", unique_id="int_attrs")
|
||||
ent.group = IntegrationSpecificGroup(ent, ["unique_1", "unique_2"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.int_group_attrs")
|
||||
assert state is not None
|
||||
assert ATTR_GROUP_ENTITIES in state.attributes
|
||||
assert sorted(state.attributes[ATTR_GROUP_ENTITIES]) == [
|
||||
"light.member1",
|
||||
"light.member2",
|
||||
]
|
||||
|
||||
|
||||
async def test_expand_entity_ids_integration_specific_group_not_expanded(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test expand_entity_ids doesn't expand IntegrationSpecificGroup."""
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_1", suggested_object_id="member1"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"light", "test", "unique_2", suggested_object_id="member2"
|
||||
)
|
||||
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="test")
|
||||
|
||||
ent = MockEntity(entity_id="light.int_specific_group", unique_id="int_spec")
|
||||
ent.group = IntegrationSpecificGroup(ent, ["unique_1", "unique_2"])
|
||||
|
||||
await platform.async_add_entities([ent])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
expanded = group.expand_entity_ids(hass, ["light.int_specific_group"])
|
||||
assert expanded == ["light.int_specific_group"]
|
||||
|
||||
Reference in New Issue
Block a user