mirror of
https://github.com/home-assistant/core.git
synced 2025-11-11 20:10:12 +00:00
Compare commits
26 Commits
copilot/ad
...
mqtt-entit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8be8022f9e | ||
|
|
1e1d6fa5c3 | ||
|
|
34cf72ea5f | ||
|
|
7c1fd889a8 | ||
|
|
c7158cbff7 | ||
|
|
a22d7d676e | ||
|
|
12f951c129 | ||
|
|
407bda7fbd | ||
|
|
c11bcb4215 | ||
|
|
623468b8ce | ||
|
|
59a5ac6629 | ||
|
|
7d9980921f | ||
|
|
64abaa6175 | ||
|
|
665bdb37fb | ||
|
|
507e32ce5a | ||
|
|
433dba2c60 | ||
|
|
523930bf71 | ||
|
|
f9c59af46d | ||
|
|
1ff1b5f936 | ||
|
|
4be3c3a52b | ||
|
|
4b42f67c04 | ||
|
|
29d633b577 | ||
|
|
c4d9a550ab | ||
|
|
a089dd3e18 | ||
|
|
73b337160f | ||
|
|
472cee0c0f |
@@ -73,6 +73,7 @@ ABBREVIATIONS = {
|
|||||||
"fan_mode_stat_t": "fan_mode_state_topic",
|
"fan_mode_stat_t": "fan_mode_state_topic",
|
||||||
"frc_upd": "force_update",
|
"frc_upd": "force_update",
|
||||||
"g_tpl": "green_template",
|
"g_tpl": "green_template",
|
||||||
|
"grp": "group",
|
||||||
"hs_cmd_t": "hs_command_topic",
|
"hs_cmd_t": "hs_command_topic",
|
||||||
"hs_cmd_tpl": "hs_command_template",
|
"hs_cmd_tpl": "hs_command_template",
|
||||||
"hs_stat_t": "hs_state_topic",
|
"hs_stat_t": "hs_state_topic",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
from .const import (
|
from .const import (
|
||||||
CONF_COMMAND_TOPIC,
|
CONF_COMMAND_TOPIC,
|
||||||
CONF_ENCODING,
|
CONF_ENCODING,
|
||||||
|
CONF_GROUP,
|
||||||
CONF_QOS,
|
CONF_QOS,
|
||||||
CONF_RETAIN,
|
CONF_RETAIN,
|
||||||
CONF_STATE_TOPIC,
|
CONF_STATE_TOPIC,
|
||||||
@@ -23,6 +24,7 @@ from .util import valid_publish_topic, valid_qos_schema, valid_subscribe_topic
|
|||||||
SCHEMA_BASE = {
|
SCHEMA_BASE = {
|
||||||
vol.Optional(CONF_QOS, default=DEFAULT_QOS): valid_qos_schema,
|
vol.Optional(CONF_QOS, default=DEFAULT_QOS): valid_qos_schema,
|
||||||
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
|
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)
|
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_TEMPLATE = "position_template"
|
||||||
CONF_GET_POSITION_TOPIC = "position_topic"
|
CONF_GET_POSITION_TOPIC = "position_topic"
|
||||||
CONF_GREEN_TEMPLATE = "green_template"
|
CONF_GREEN_TEMPLATE = "green_template"
|
||||||
|
CONF_GROUP = "group"
|
||||||
CONF_HS_COMMAND_TEMPLATE = "hs_command_template"
|
CONF_HS_COMMAND_TEMPLATE = "hs_command_template"
|
||||||
CONF_HS_COMMAND_TOPIC = "hs_command_topic"
|
CONF_HS_COMMAND_TOPIC = "hs_command_topic"
|
||||||
CONF_HS_STATE_TOPIC = "hs_state_topic"
|
CONF_HS_STATE_TOPIC = "hs_state_topic"
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ from .const import (
|
|||||||
CONF_ENABLED_BY_DEFAULT,
|
CONF_ENABLED_BY_DEFAULT,
|
||||||
CONF_ENCODING,
|
CONF_ENCODING,
|
||||||
CONF_ENTITY_PICTURE,
|
CONF_ENTITY_PICTURE,
|
||||||
|
CONF_GROUP,
|
||||||
CONF_HW_VERSION,
|
CONF_HW_VERSION,
|
||||||
CONF_IDENTIFIERS,
|
CONF_IDENTIFIERS,
|
||||||
CONF_JSON_ATTRS_TEMPLATE,
|
CONF_JSON_ATTRS_TEMPLATE,
|
||||||
@@ -136,6 +137,7 @@ MQTT_ATTRIBUTES_BLOCKED = {
|
|||||||
"device_class",
|
"device_class",
|
||||||
"device_info",
|
"device_info",
|
||||||
"entity_category",
|
"entity_category",
|
||||||
|
"entity_id",
|
||||||
"entity_picture",
|
"entity_picture",
|
||||||
"entity_registry_enabled_default",
|
"entity_registry_enabled_default",
|
||||||
"extra_state_attributes",
|
"extra_state_attributes",
|
||||||
@@ -475,6 +477,8 @@ class MqttAttributesMixin(Entity):
|
|||||||
def __init__(self, config: ConfigType) -> None:
|
def __init__(self, config: ConfigType) -> None:
|
||||||
"""Initialize the JSON attributes mixin."""
|
"""Initialize the JSON attributes mixin."""
|
||||||
self._attributes_sub_state: dict[str, EntitySubscription] = {}
|
self._attributes_sub_state: dict[str, EntitySubscription] = {}
|
||||||
|
if CONF_GROUP in config:
|
||||||
|
self._attr_included_unique_ids = config[CONF_GROUP]
|
||||||
self._attributes_config = config
|
self._attributes_config = config
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
@@ -546,7 +550,7 @@ class MqttAttributesMixin(Entity):
|
|||||||
_LOGGER.warning("Erroneous JSON: %s", payload)
|
_LOGGER.warning("Erroneous JSON: %s", payload)
|
||||||
else:
|
else:
|
||||||
if isinstance(json_dict, dict):
|
if isinstance(json_dict, dict):
|
||||||
filtered_dict = {
|
filtered_dict: dict[str, Any] = {
|
||||||
k: v
|
k: v
|
||||||
for k, v in json_dict.items()
|
for k, v in json_dict.items()
|
||||||
if k not in MQTT_ATTRIBUTES_BLOCKED
|
if k not in MQTT_ATTRIBUTES_BLOCKED
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -524,6 +525,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
|
||||||
|
|
||||||
|
# Remember we keep track of included entities
|
||||||
|
__init_track_included_entities: bool = False
|
||||||
|
|
||||||
# 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 +543,8 @@ 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_entities: list[str]
|
||||||
|
_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,8 @@ 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 hasattr(self, "_attr_included_entities"):
|
||||||
|
attr[ATTR_ENTITY_ID] = self._attr_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:
|
||||||
@@ -1376,6 +1384,7 @@ class Entity(
|
|||||||
"""Finish adding an entity to a platform."""
|
"""Finish adding an entity to a platform."""
|
||||||
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.async_set_included_entities()
|
||||||
self._platform_state = EntityPlatformState.ADDED
|
self._platform_state = EntityPlatformState.ADDED
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@@ -1633,6 +1642,59 @@ class Entity(
|
|||||||
self.hass, integration_domain=platform_name, module=type(self).__module__
|
self.hass, integration_domain=platform_name, module=type(self).__module__
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_set_included_entities(self) -> None:
|
||||||
|
"""Set the list of included entities identified by their unique IDs.
|
||||||
|
|
||||||
|
Integrations need to call this when the list of included unique IDs changes.
|
||||||
|
"""
|
||||||
|
if not self.included_unique_ids:
|
||||||
|
return
|
||||||
|
entity_registry = er.async_get(self.hass)
|
||||||
|
assert self.entity_id is not None
|
||||||
|
|
||||||
|
def _update_group_entity_ids() -> None:
|
||||||
|
self._attr_included_entities = []
|
||||||
|
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
|
||||||
|
):
|
||||||
|
self._attr_included_entities.append(entity_id)
|
||||||
|
|
||||||
|
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 hasattr(self, "_attr_included_entities")
|
||||||
|
and event.data["entity_id"] in self._attr_included_entities
|
||||||
|
):
|
||||||
|
_update_group_entity_ids()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
if not self.__init_track_included_entities:
|
||||||
|
self.async_on_remove(
|
||||||
|
self.hass.bus.async_listen(
|
||||||
|
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||||
|
_handle_entity_registry_updated,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.__init_track_included_entities = True
|
||||||
|
_update_group_entity_ids()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def included_unique_ids(self) -> list[str]:
|
||||||
|
"""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 []
|
||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ light:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import call, patch
|
from unittest.mock import call, patch
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ from homeassistant.const import (
|
|||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.util.json import json_loads
|
from homeassistant.util.json import json_loads
|
||||||
|
|
||||||
from .common import (
|
from .common import (
|
||||||
@@ -169,6 +171,39 @@ COLOR_MODES_CONFIG = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GROUP_MEMBER_1_TOPIC = "homeassistant/light/member_1/config"
|
||||||
|
GROUP_MEMBER_2_TOPIC = "homeassistant/light/member_2/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_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"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class JsonValidator:
|
class JsonValidator:
|
||||||
"""Helper to compare JSON."""
|
"""Helper to compare JSON."""
|
||||||
@@ -1859,6 +1894,86 @@ async def test_white_scale(
|
|||||||
assert state.attributes.get("brightness") == 129
|
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("entity_id") == ["light.member1", "light.member2"]
|
||||||
|
|
||||||
|
|
||||||
|
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("entity_id") == ["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("entity_id") == ["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("entity_id") == ["light.member2_updated"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
[
|
[
|
||||||
@@ -2040,7 +2155,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
|
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the setting of attribute via MQTT with JSON payload."""
|
"""Test the setting of attribute via MQTT with JSON payload."""
|
||||||
@@ -2049,6 +2164,51 @@ 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("entity_id") == ["light.member_1", "light.member_2"]
|
||||||
|
|
||||||
|
|
||||||
async def test_setting_blocked_attribute_via_mqtt_json_message(
|
async def test_setting_blocked_attribute_via_mqtt_json_message(
|
||||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -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,107 @@ 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_set_included_entities()
|
||||||
|
|
||||||
|
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("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_set_included_entities()
|
||||||
|
|
||||||
|
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.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