Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions homeassistant/components/mqtt/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import Event, HassJobType, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.device_registry import (
DeviceEntry,
DeviceInfo,
Expand Down Expand Up @@ -1367,6 +1371,7 @@ class MqttEntity(
_default_name: str | None
_entity_id_format: str
_update_registry_entity_id: str | None = None
_update_registry_area_id: str | None = None

def __init__(
self,
Expand Down Expand Up @@ -1427,6 +1432,24 @@ def _init_entity_registry(self, discovery_data: DiscoveryInfoType | None) -> Non
# if a deleted entity was found
self._update_registry_entity_id = self.entity_id

if (
deleted_entry
and (
device_info := device_info_from_specifications(
self._config.get(CONF_DEVICE)
)
)
is not None
and dr.async_get(self.hass).deleted_devices.get_entry(
identifiers=device_info["identifiers"],
connections=device_info["connections"],
)
and (suggested_area := device_info.get(ATTR_SUGGESTED_AREA))
):
self._update_registry_area_id = (
ar.async_get(self.hass).async_get_or_create(suggested_area).id
)

if (
self._config[CONF_ENABLED_BY_DEFAULT]
and deleted_entry
Expand All @@ -1437,8 +1460,7 @@ def _init_entity_registry(self, discovery_data: DiscoveryInfoType | None) -> Non
entity_platform, DOMAIN, self.unique_id
)
entity_registry.async_update_entity(
recreated_entry.entity_id,
disabled_by=None,
recreated_entry.entity_id, disabled_by=None
)

if discovery_data is None:
Expand Down Expand Up @@ -1466,12 +1488,26 @@ def _init_entity_registry(self, discovery_data: DiscoveryInfoType | None) -> Non
@final
async def async_added_to_hass(self) -> None:
"""Subscribe to MQTT events."""
if self._update_registry_entity_id is not None:
if (
self._update_registry_entity_id is not None
or self._update_registry_area_id is not None
):
entity_registry = er.async_get(self.hass)
entity_registry.async_update_entity(
self.entity_id, new_entity_id=self._update_registry_entity_id
entry = entity_registry.async_update_entity(
self.entity_id,
new_entity_id=self._update_registry_entity_id or UNDEFINED,
area_id=self._update_registry_area_id or UNDEFINED,
)
if (
self._update_registry_area_id is not None
and entry.device_id is not None
):
device_registry = dr.async_get(self.hass)
device_registry.async_update_device(
entry.device_id, area_id=self._update_registry_area_id or UNDEFINED
)
self._update_registry_entity_id = None
self._update_registry_area_id = None

await super().async_added_to_hass()
await self._async_finish_update_config()
Expand Down
53 changes: 39 additions & 14 deletions tests/components/mqtt/test_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,13 +526,17 @@ 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_reconfigure_disabled_or_removed_mqtt_device(
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 reconfiguring an entity that was not enabled or removed.

Assert if a disabled entity can be re-enabled,
and that the suggested area can be changed or deleted devices.
"""
await mqtt_mock_entry()

discovery_topic = "homeassistant/sensor/bla/config"
Expand All @@ -543,7 +547,11 @@ async def test_registry_enable_not_enabled_by_default_entity(
"enabled_by_default": False,
"unique_id": "very_unique",
"default_entity_id": "sensor.test",
"device": {"identifiers": "very_unique_device", "name": "test"},
"device": {
"identifiers": "very_unique_device",
"name": "test",
"suggested_area": "Kitchen",
},
}
)
config_enabled = json.json_dumps(
Expand All @@ -553,7 +561,11 @@ async def test_registry_enable_not_enabled_by_default_entity(
"enabled_by_default": True,
"unique_id": "very_unique",
"default_entity_id": "sensor.test",
"device": {"identifiers": "very_unique_device", "name": "test"},
"device": {
"identifiers": "very_unique_device",
"name": "test",
"suggested_area": "Bedroom",
},
}
)
config_enabled_new_entity_name = json.json_dumps(
Expand All @@ -563,19 +575,24 @@ async def test_registry_enable_not_enabled_by_default_entity(
"enabled_by_default": True,
"unique_id": "very_unique",
"default_entity_id": "sensor.test_new",
"device": {"identifiers": "very_unique_device", "name": "test"},
"device": {
"identifiers": "very_unique_device",
"name": "test",
"suggested_area": "Attic",
},
}
)

async_fire_mqtt_message(hass, discovery_topic, config_disabled)
await hass.async_block_till_done()
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get("sensor.test")
assert state is None
entry = entity_registry.async_get("sensor.test")
assert entry is not None
assert entry.disabled
assert (device_id := entry.device_id)
assert device_registry.async_get(device_id) is not None
device_registry_item = device_registry.async_get(device_id)
assert device_registry_item.area_id == "kitchen"

# Remove the entity and device
# At this stage no entry existed during the initialization
Expand All @@ -588,24 +605,30 @@ async def test_registry_enable_not_enabled_by_default_entity(

# Rediscover the previous deleted entity and allow it to be enabled
async_fire_mqtt_message(hass, discovery_topic, config_enabled)
await hass.async_block_till_done()
await hass.async_block_till_done(wait_background_tasks=True)
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 device_registry.async_get(device_id) is not None
assert entry.area_id == "bedroom"
assert (device_id := entry.device_id)
device_registry_item = device_registry.async_get(device_id)
assert device_registry_item.area_id == "bedroom"

# Update entity to not be enabled by default
# The entity should stay available as it was enabled before
# The area should not update
async_fire_mqtt_message(hass, discovery_topic, config_disabled)
await hass.async_block_till_done()
await hass.async_block_till_done(wait_background_tasks=True)
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 device_registry.async_get(device_id) is not None
assert entry.area_id == "bedroom"
device_registry_item = device_registry.async_get(device_id)
assert device_registry_item.area_id == "bedroom"

# Delete the entity again
async_fire_mqtt_message(hass, discovery_topic, "")
Expand All @@ -615,15 +638,17 @@ async def test_registry_enable_not_enabled_by_default_entity(
# Assert device is cleaned up
assert device_registry.async_get(device_id) is None

# Repeat the re-discovery, with a new entity name
# Repeat the re-discovery, with a new entity name and suggested area
async_fire_mqtt_message(hass, discovery_topic, config_enabled_new_entity_name)
await hass.async_block_till_done()
await hass.async_block_till_done(wait_background_tasks=True)
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.area_id == "attic"
assert not entry.disabled
assert device_registry.async_get(device_id) is not None
device_registry_item = device_registry.async_get(device_id)
assert device_registry_item.area_id == "attic"


@pytest.mark.parametrize(
Expand Down
Loading