Clear MQTT discovery topic when a disabled entity is removed (#77757)

* Cleanup discovery on entity removal

* Add test

* Cleanup and test

* Test with clearing payload not unique id

* Address comments

* Tests cover and typing

* Just pass hass

* reuse code

* Follow up comments revert changes to cover tests

* Add test unique_id has priority over disabled

* Update homeassistant/components/mqtt/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Jan Bouwhuis
2022-09-09 22:37:27 -04:00
committed by Paulus Schoutsen
co-authored by Erik Montnemery
parent d670df74cb
commit 21f6b50f7c
4 changed files with 232 additions and 6 deletions
+170
View File
@@ -1,4 +1,5 @@
"""The tests for the MQTT discovery."""
import copy
import json
from pathlib import Path
import re
@@ -23,6 +24,8 @@ from homeassistant.const import (
import homeassistant.core as ha
from homeassistant.setup import async_setup_component
from .test_common import help_test_unload_config_entry
from tests.common import (
MockConfigEntry,
async_capture_events,
@@ -1356,3 +1359,170 @@ async def test_mqtt_discovery_unsubscribe_once(
await hass.async_block_till_done()
await hass.async_block_till_done()
mqtt_client_mock.unsubscribe.assert_called_once_with("comp/discovery/#")
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR])
async def test_clear_config_topic_disabled_entity(
hass, mqtt_mock_entry_no_yaml_config, device_reg, caplog
):
"""Test the discovery topic is removed when a disabled entity is removed."""
mqtt_mock = await mqtt_mock_entry_no_yaml_config()
# discover an entity that is not enabled by default
config = {
"name": "sbfspot_12345",
"state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/",
"unique_id": "sbfspot_12345",
"enabled_by_default": False,
"device": {
"identifiers": ["sbfspot_12345"],
"name": "sbfspot_12345",
"sw_version": "1.0",
"connections": [["mac", "12:34:56:AB:CD:EF"]],
},
}
async_fire_mqtt_message(
hass,
"homeassistant/sensor/sbfspot_0/sbfspot_12345/config",
json.dumps(config),
)
await hass.async_block_till_done()
# discover an entity that is not unique (part 1), will be added
config_not_unique1 = copy.deepcopy(config)
config_not_unique1["name"] = "sbfspot_12345_1"
config_not_unique1["unique_id"] = "not_unique"
config_not_unique1.pop("enabled_by_default")
async_fire_mqtt_message(
hass,
"homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config",
json.dumps(config_not_unique1),
)
# discover an entity that is not unique (part 2), will not be added
config_not_unique2 = copy.deepcopy(config_not_unique1)
config_not_unique2["name"] = "sbfspot_12345_2"
async_fire_mqtt_message(
hass,
"homeassistant/sensor/sbfspot_0/sbfspot_12345_2/config",
json.dumps(config_not_unique2),
)
await hass.async_block_till_done()
assert "Platform mqtt does not generate unique IDs" in caplog.text
assert hass.states.get("sensor.sbfspot_12345") is None # disabled
assert hass.states.get("sensor.sbfspot_12345_1") is not None # enabled
assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique
# Verify device is created
device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")})
assert device_entry is not None
# Remove the device from the registry
device_reg.async_remove_device(device_entry.id)
await hass.async_block_till_done()
await hass.async_block_till_done()
# Assert all valid discovery topics are cleared
assert mqtt_mock.async_publish.call_count == 2
assert (
call("homeassistant/sensor/sbfspot_0/sbfspot_12345/config", "", 0, True)
in mqtt_mock.async_publish.mock_calls
)
assert (
call("homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config", "", 0, True)
in mqtt_mock.async_publish.mock_calls
)
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR])
async def test_clean_up_registry_monitoring(
hass, mqtt_mock_entry_no_yaml_config, device_reg, tmp_path
):
"""Test registry monitoring hook is removed after a reload."""
await mqtt_mock_entry_no_yaml_config()
hooks: dict = hass.data[mqtt.const.DATA_MQTT_DISCOVERY_REGISTRY_HOOKS]
# discover an entity that is not enabled by default
config1 = {
"name": "sbfspot_12345",
"state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/",
"unique_id": "sbfspot_12345",
"enabled_by_default": False,
"device": {
"identifiers": ["sbfspot_12345"],
"name": "sbfspot_12345",
"sw_version": "1.0",
"connections": [["mac", "12:34:56:AB:CD:EF"]],
},
}
# Publish it config
# Since it is not enabled_by_default the sensor will not be loaded
# it should register a hook for monitoring the entiry registry
async_fire_mqtt_message(
hass,
"homeassistant/sensor/sbfspot_0/sbfspot_12345/config",
json.dumps(config1),
)
await hass.async_block_till_done()
assert len(hooks) == 1
# Publish it again no new monitor should be started
async_fire_mqtt_message(
hass,
"homeassistant/sensor/sbfspot_0/sbfspot_12345/config",
json.dumps(config1),
)
await hass.async_block_till_done()
assert len(hooks) == 1
# Verify device is created
device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")})
assert device_entry is not None
# Enload the entry
# The monitoring should be cleared
await help_test_unload_config_entry(hass, tmp_path, {})
assert len(hooks) == 0
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR])
async def test_unique_id_collission_has_priority(
hass, mqtt_mock_entry_no_yaml_config, entity_reg
):
"""Test tehe unique_id collision detection has priority over registry disabled items."""
await mqtt_mock_entry_no_yaml_config()
config = {
"name": "sbfspot_12345",
"state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/",
"unique_id": "sbfspot_12345",
"enabled_by_default": False,
"device": {
"identifiers": ["sbfspot_12345"],
"name": "sbfspot_12345",
"sw_version": "1.0",
"connections": [["mac", "12:34:56:AB:CD:EF"]],
},
}
# discover an entity that is not unique and disabled by default (part 1), will be added
config_not_unique1 = copy.deepcopy(config)
config_not_unique1["name"] = "sbfspot_12345_1"
config_not_unique1["unique_id"] = "not_unique"
async_fire_mqtt_message(
hass,
"homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config",
json.dumps(config_not_unique1),
)
# discover an entity that is not unique (part 2), will not be added, and the registry entry is cleared
config_not_unique2 = copy.deepcopy(config_not_unique1)
config_not_unique2["name"] = "sbfspot_12345_2"
async_fire_mqtt_message(
hass,
"homeassistant/sensor/sbfspot_0/sbfspot_12345_2/config",
json.dumps(config_not_unique2),
)
await hass.async_block_till_done()
assert hass.states.get("sensor.sbfspot_12345_1") is None # not enabled
assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique
# Verify the first entity is created
assert entity_reg.async_get("sensor.sbfspot_12345_1") is not None
# Verify the second entity is not created because it is not unique
assert entity_reg.async_get("sensor.sbfspot_12345_2") is None