mirror of
https://github.com/home-assistant/core.git
synced 2026-07-09 07:45:11 +01:00
Allow MQTT entities to be hidden by default (#168832)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -273,6 +273,7 @@ ABBREVIATIONS = {
|
||||
"l_ver_t": "latest_version_topic",
|
||||
"l_ver_tpl": "latest_version_template",
|
||||
"pl_inst": "payload_install",
|
||||
"vis": "visible_by_default",
|
||||
}
|
||||
|
||||
DEVICE_ABBREVIATIONS = {
|
||||
|
||||
@@ -246,6 +246,7 @@ CONF_TILT_STATE_OPTIMISTIC = "tilt_optimistic"
|
||||
CONF_TRANSITION = "transition"
|
||||
CONF_URL_TEMPLATE = "url_template"
|
||||
CONF_URL_TOPIC = "url_topic"
|
||||
CONF_VISIBLE_BY_DEFAULT = "visible_by_default"
|
||||
CONF_XY_COMMAND_TEMPLATE = "xy_command_template"
|
||||
CONF_XY_COMMAND_TOPIC = "xy_command_topic"
|
||||
CONF_XY_STATE_TOPIC = "xy_state_topic"
|
||||
|
||||
@@ -95,6 +95,7 @@ from .const import (
|
||||
CONF_SW_VERSION,
|
||||
CONF_TOPIC,
|
||||
CONF_VIA_DEVICE,
|
||||
CONF_VISIBLE_BY_DEFAULT,
|
||||
DOMAIN,
|
||||
MQTT_CONNECTION_STATE,
|
||||
)
|
||||
@@ -1428,20 +1429,44 @@ class MqttEntity(
|
||||
# Plan to update the entity_id based on `default_entity_id`
|
||||
# if a deleted entity was found
|
||||
self._update_registry_entity_id = self.entity_id
|
||||
|
||||
if (
|
||||
self._config[CONF_ENABLED_BY_DEFAULT]
|
||||
and deleted_entry
|
||||
and deleted_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
reenable_condition := (
|
||||
deleted_entry
|
||||
and self._config[CONF_ENABLED_BY_DEFAULT]
|
||||
and deleted_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
)
|
||||
) or (
|
||||
deleted_entry
|
||||
and self._config[CONF_VISIBLE_BY_DEFAULT]
|
||||
and deleted_entry.hidden_by is not None
|
||||
):
|
||||
# Enable previous deleted entity and enable it,
|
||||
# if it was not disabled by the user
|
||||
# Enable previous deleted entity,
|
||||
# if it was not disabled by the user.
|
||||
# Only reset hidden by flag if it was not hidden by the user.
|
||||
if (
|
||||
deleted_entry.hidden_by is er.RegistryEntryHider.USER
|
||||
and self._config[CONF_VISIBLE_BY_DEFAULT]
|
||||
):
|
||||
_LOGGER.info(
|
||||
"Restored entity %s was configured as visible by default, "
|
||||
"but was hidden by the user before, and will remain hidden",
|
||||
self.entity_id,
|
||||
)
|
||||
if deleted_entry.hidden_by is er.RegistryEntryHider.USER:
|
||||
hidden_by: er.RegistryEntryHider | None = er.RegistryEntryHider.USER
|
||||
else:
|
||||
hidden_by = (
|
||||
None
|
||||
if self._config[CONF_VISIBLE_BY_DEFAULT]
|
||||
else er.RegistryEntryHider.INTEGRATION
|
||||
)
|
||||
recreated_entry = entity_registry.async_get_or_create(
|
||||
entity_platform, DOMAIN, self.unique_id
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
recreated_entry.entity_id,
|
||||
disabled_by=None,
|
||||
disabled_by=None if reenable_condition else UNDEFINED,
|
||||
hidden_by=hidden_by,
|
||||
)
|
||||
|
||||
if discovery_data is None:
|
||||
@@ -1590,6 +1615,9 @@ class MqttEntity(
|
||||
self._attr_entity_registry_enabled_default = bool(
|
||||
config.get(CONF_ENABLED_BY_DEFAULT, True)
|
||||
)
|
||||
self._attr_entity_registry_visible_default = bool(
|
||||
config.get(CONF_VISIBLE_BY_DEFAULT, True)
|
||||
)
|
||||
self._attr_icon = config.get(CONF_ICON)
|
||||
self._attr_entity_picture = config.get(CONF_ENTITY_PICTURE)
|
||||
# Set the entity name if needed
|
||||
|
||||
@@ -52,6 +52,7 @@ from .const import (
|
||||
CONF_SW_VERSION,
|
||||
CONF_TOPIC,
|
||||
CONF_VIA_DEVICE,
|
||||
CONF_VISIBLE_BY_DEFAULT,
|
||||
DEFAULT_PAYLOAD_AVAILABLE,
|
||||
DEFAULT_PAYLOAD_NOT_AVAILABLE,
|
||||
ENTITY_PLATFORMS,
|
||||
@@ -184,6 +185,7 @@ MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
|
||||
vol.Optional(CONF_DEFAULT_ENTITY_ID): cv.string,
|
||||
vol.Optional(CONF_MESSAGE_EXPIRY_INTERVAL): valid_message_expiry_interval,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_VISIBLE_BY_DEFAULT, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -527,13 +527,13 @@ async def test_registry_not_enabled_by_default(
|
||||
assert entry.disabled
|
||||
|
||||
|
||||
async def test_registry_enable_not_enabled_by_default_entity(
|
||||
async def test_registry_enable_not_enabled_or_visible_by_default_entity(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test enabling an entity that was not enabled by default."""
|
||||
"""Test enabling an entity that was not enabled or not visible by default."""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
discovery_topic = "homeassistant/sensor/bla/config"
|
||||
@@ -542,6 +542,7 @@ async def test_registry_enable_not_enabled_by_default_entity(
|
||||
"name": None,
|
||||
"state_topic": "state-topic",
|
||||
"enabled_by_default": False,
|
||||
"visible_by_default": True,
|
||||
"unique_id": "very_unique",
|
||||
"default_entity_id": "sensor.test",
|
||||
"device": {"identifiers": "very_unique_device", "name": "test"},
|
||||
@@ -552,6 +553,7 @@ async def test_registry_enable_not_enabled_by_default_entity(
|
||||
"name": None,
|
||||
"state_topic": "state-topic",
|
||||
"enabled_by_default": True,
|
||||
"visible_by_default": False,
|
||||
"unique_id": "very_unique",
|
||||
"default_entity_id": "sensor.test",
|
||||
"device": {"identifiers": "very_unique_device", "name": "test"},
|
||||
@@ -562,6 +564,7 @@ async def test_registry_enable_not_enabled_by_default_entity(
|
||||
"name": None,
|
||||
"state_topic": "state-topic",
|
||||
"enabled_by_default": True,
|
||||
"visible_by_default": True,
|
||||
"unique_id": "very_unique",
|
||||
"default_entity_id": "sensor.test_new",
|
||||
"device": {"identifiers": "very_unique_device", "name": "test"},
|
||||
@@ -575,6 +578,7 @@ async def test_registry_enable_not_enabled_by_default_entity(
|
||||
entry = entity_registry.async_get("sensor.test")
|
||||
assert entry is not None
|
||||
assert entry.disabled
|
||||
assert entry.hidden is False
|
||||
assert (device_id := entry.device_id)
|
||||
assert device_registry.async_get(device_id) is not None
|
||||
|
||||
@@ -588,6 +592,7 @@ async def test_registry_enable_not_enabled_by_default_entity(
|
||||
assert device_registry.async_get(device_id) is None
|
||||
|
||||
# Rediscover the previous deleted entity and allow it to be enabled
|
||||
# but not visible by default
|
||||
async_fire_mqtt_message(hass, discovery_topic, config_enabled)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test")
|
||||
@@ -595,10 +600,12 @@ async def test_registry_enable_not_enabled_by_default_entity(
|
||||
entry = entity_registry.async_get("sensor.test")
|
||||
assert entry is not None
|
||||
assert not entry.disabled
|
||||
assert entry.hidden is True
|
||||
assert device_registry.async_get(device_id) is not None
|
||||
|
||||
# Update entity to not be enabled by default
|
||||
# The entity should stay available as it was enabled before
|
||||
# Also it should remain hidden
|
||||
async_fire_mqtt_message(hass, discovery_topic, config_disabled)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test")
|
||||
@@ -606,6 +613,7 @@ async def test_registry_enable_not_enabled_by_default_entity(
|
||||
entry = entity_registry.async_get("sensor.test")
|
||||
assert entry is not None
|
||||
assert not entry.disabled
|
||||
assert entry.hidden is True
|
||||
assert device_registry.async_get(device_id) is not None
|
||||
|
||||
# Delete the entity again
|
||||
@@ -617,12 +625,14 @@ async def test_registry_enable_not_enabled_by_default_entity(
|
||||
assert device_registry.async_get(device_id) is None
|
||||
|
||||
# Repeat the re-discovery, with a new entity name
|
||||
# The entity should be enabled and visible by default now
|
||||
async_fire_mqtt_message(hass, discovery_topic, config_enabled_new_entity_name)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test_new")
|
||||
assert state is not None
|
||||
entry = entity_registry.async_get("sensor.test_new")
|
||||
assert entry is not None
|
||||
assert entry.hidden is False
|
||||
assert not entry.disabled
|
||||
assert device_registry.async_get(device_id) is not None
|
||||
|
||||
@@ -645,9 +655,174 @@ async def test_registry_enable_not_enabled_by_default_entity(
|
||||
entry = entity_registry.async_get("sensor.test_new")
|
||||
assert entry is not None
|
||||
assert entry.disabled
|
||||
assert entry.hidden is False
|
||||
assert device_registry.async_get(device_id) is not None
|
||||
|
||||
|
||||
async def test_visible_by_default_with_user_override(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test visible_by_default is respected but user hidden_by override is preserved."""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
discovery_topic = "homeassistant/sensor/bla/config"
|
||||
config_visible = json.json_dumps(
|
||||
{
|
||||
"name": None,
|
||||
"state_topic": "state-topic",
|
||||
"visible_by_default": True,
|
||||
"unique_id": "very_unique",
|
||||
"default_entity_id": "sensor.test",
|
||||
"device": {"identifiers": "very_unique_device", "name": "test"},
|
||||
}
|
||||
)
|
||||
config_hidden = json.json_dumps(
|
||||
{
|
||||
"name": None,
|
||||
"state_topic": "state-topic",
|
||||
"visible_by_default": False,
|
||||
"unique_id": "very_unique",
|
||||
"default_entity_id": "sensor.test",
|
||||
"device": {"identifiers": "very_unique_device", "name": "test"},
|
||||
}
|
||||
)
|
||||
|
||||
async_fire_mqtt_message(hass, discovery_topic, config_hidden)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test")
|
||||
assert state is not None
|
||||
entry = entity_registry.async_get("sensor.test")
|
||||
assert entry is not None
|
||||
assert not entry.disabled
|
||||
assert entry.hidden
|
||||
assert entry.hidden_by is er.RegistryEntryHider.INTEGRATION
|
||||
assert (device_id := entry.device_id)
|
||||
assert device_registry.async_get(device_id) is not None
|
||||
|
||||
# Remove the entity and device
|
||||
# At this stage no entry existed during the initialization
|
||||
async_fire_mqtt_message(hass, discovery_topic, "")
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
entry = entity_registry.async_get("sensor.test")
|
||||
assert entry is None
|
||||
# Assert device is cleaned up
|
||||
assert device_registry.async_get(device_id) is None
|
||||
|
||||
# Rediscover the previous deleted entity, now visible by default
|
||||
async_fire_mqtt_message(hass, discovery_topic, config_visible)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test")
|
||||
assert state is not None
|
||||
entry = entity_registry.async_get("sensor.test")
|
||||
assert entry is not None
|
||||
assert not entry.hidden
|
||||
assert entry.hidden_by is None
|
||||
assert device_registry.async_get(device_id) is not None
|
||||
|
||||
# Mock the user hides the entity
|
||||
entity_registry.async_update_entity(
|
||||
"sensor.test", hidden_by=er.RegistryEntryHider.USER
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Remove the entity and device,
|
||||
# the hidden by user flag should be preserved in the deleted entities registry
|
||||
async_fire_mqtt_message(hass, discovery_topic, "")
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
entry = entity_registry.async_get("sensor.test")
|
||||
assert entry is None
|
||||
# Assert device is cleaned up
|
||||
assert device_registry.async_get(device_id) is None
|
||||
|
||||
# Rediscover again and assert the entity remains hidden
|
||||
# because it was hidden by the user, even though visible_by_default is True
|
||||
async_fire_mqtt_message(hass, discovery_topic, config_visible)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test")
|
||||
assert state is not None
|
||||
entry = entity_registry.async_get("sensor.test")
|
||||
assert entry is not None
|
||||
assert entry.hidden
|
||||
assert entry.hidden_by is er.RegistryEntryHider.USER
|
||||
assert device_registry.async_get(device_id) is not None
|
||||
assert (
|
||||
"Restored entity sensor.test was configured as visible by default, "
|
||||
"but was hidden by the user before, and will remain hidden" in caplog.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
{
|
||||
mqtt.DOMAIN: {
|
||||
sensor.DOMAIN: {
|
||||
"name": "test",
|
||||
"state_topic": "state-topic",
|
||||
"unique_id": "very_unique",
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
mqtt.DOMAIN: {
|
||||
sensor.DOMAIN: {
|
||||
"name": "test",
|
||||
"state_topic": "state-topic",
|
||||
"visible_by_default": True,
|
||||
"unique_id": "very_unique",
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_registry_visible_by_default(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test an entity is visible with visible_by_default set or without."""
|
||||
await mqtt_mock_entry()
|
||||
state = hass.states.get("sensor.test")
|
||||
assert state is not None
|
||||
entry = entity_registry.async_get("sensor.test")
|
||||
assert not entry.disabled
|
||||
assert entry.hidden is False
|
||||
assert entry.hidden_by is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
{
|
||||
mqtt.DOMAIN: {
|
||||
sensor.DOMAIN: {
|
||||
"name": "test",
|
||||
"state_topic": "state-topic",
|
||||
"visible_by_default": False,
|
||||
"unique_id": "very_unique",
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_registry_not_visible_by_default(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test an entity that is not visible by default."""
|
||||
await mqtt_mock_entry()
|
||||
state = hass.states.get("sensor.test")
|
||||
assert state is not None
|
||||
entry = entity_registry.async_get("sensor.test")
|
||||
assert entry.hidden is True
|
||||
assert entry.hidden_by is er.RegistryEntryHider.INTEGRATION
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mqtt_config_subentries_data",
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user