core/tests/components/mqtt/test_discovery.py
Jan Bouwhuis 585892f067
Allow MQTT device based auto discovery (#109030)
* Add MQTT device based auto discovery

* Respect override of component options over shared ones

* Add state_topic, command_topic, qos and encoding as shared options

* Add shared option test

* Rename device.py to schemas.py

* Remove unused legacy `platform` attribute to avoid confusion

* Split validation device and origin info

* Require `origin` info on device based discovery

* Log origin info for only once for device discovery

* Fix tests and linters

* ruff

* speed up _replace_all_abbreviations

* Fix imports and merging errors - add slots attr

* Fix unrelated const changes

* More unrelated changes

* join string

* fix merge

* Undo move

* Adjust logger statement

* fix task storm to load platforms

* Revert "fix task storm to load platforms"

This reverts commit 8f12a5f2511ab872880a186f5a4605c8bae80c7d.

* bail if logging is disabled

* Correct mixup object_id and node_id

* Auto migrate entities to device discovery

* Add device discovery test for device_trigger

* Add migration support for non entity platforms

* Use helper to remove discovery payload

* Fix tests after update branch

* Add discovery migration test

* Refactor

* Repair after rebase

* Fix discovery is broken after migration

* Improve comments

* More comment improvements

* Split long lines

* Add comment to indicate payload dict can be empty

* typo

* Add walrus and update comment

* Add tag to migration test

* Join try blocks

* Refactor

* Cleanup not used attribute

* Refactor

* Move _replace_all_abbreviations out of try block

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2024-05-29 11:12:05 +02:00

2465 lines
83 KiB
Python

"""The tests for the MQTT discovery."""
import asyncio
import copy
import json
from pathlib import Path
import re
from typing import Any
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
import pytest
from homeassistant import config_entries
from homeassistant.components import mqtt
from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.components.mqtt.abbreviations import (
ABBREVIATIONS,
DEVICE_ABBREVIATIONS,
)
from homeassistant.components.mqtt.discovery import (
MQTT_DISCOVERY_DONE,
MQTT_DISCOVERY_NEW,
MQTT_DISCOVERY_UPDATED,
MQTTDiscoveryPayload,
async_start,
)
from homeassistant.const import (
EVENT_STATE_CHANGED,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from homeassistant.setup import async_setup_component
from homeassistant.util.signal_type import SignalTypeFormat
from .test_common import help_all_subscribe_calls, help_test_unload_config_entry
from .test_tag import DEFAULT_TAG_ID, DEFAULT_TAG_SCAN
from tests.common import (
MockConfigEntry,
async_capture_events,
async_fire_mqtt_message,
async_get_device_automations,
mock_config_flow,
mock_platform,
)
from tests.typing import (
MqttMockHAClientGenerator,
MqttMockPahoClient,
WebSocketGenerator,
)
@pytest.mark.parametrize(
"mqtt_config_entry_data",
[{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}],
)
async def test_subscribing_config_topic(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test setting up discovery."""
mqtt_mock = await mqtt_mock_entry()
entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
discovery_topic = "homeassistant"
await async_start(hass, discovery_topic, entry)
call_args1 = mqtt_mock.async_subscribe.mock_calls[0][1]
assert call_args1[2] == 0
call_args2 = mqtt_mock.async_subscribe.mock_calls[1][1]
assert call_args2[2] == 0
topics = [call_args1[0], call_args2[0]]
assert discovery_topic + "/+/+/config" in topics
assert discovery_topic + "/+/+/+/config" in topics
@pytest.mark.parametrize(
("topic", "log"),
[
("homeassistant/binary_sensor/bla/not_config", False),
("homeassistant/binary_sensor/rörkrökare/config", True),
("homeassistant/device/bla/not_config", False),
("homeassistant/device/rörkrökare/config", True),
],
)
async def test_invalid_topic(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
topic: str,
log: bool,
) -> None:
"""Test sending to invalid topic."""
await mqtt_mock_entry()
with patch(
"homeassistant.components.mqtt.discovery.async_dispatcher_send"
) as mock_dispatcher_send:
mock_dispatcher_send = AsyncMock(return_value=None)
async_fire_mqtt_message(hass, topic, "{}")
await hass.async_block_till_done()
assert not mock_dispatcher_send.called
if log:
assert (
f"Received message on illegal discovery topic '{topic}'" in caplog.text
)
else:
assert "Received message on illegal discovery topic'" not in caplog.text
caplog.clear()
@pytest.mark.parametrize(
"discovery_topic",
["homeassistant/binary_sensor/bla/config", "homeassistant/device/bla/config"],
)
async def test_invalid_json(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
discovery_topic: str,
) -> None:
"""Test sending in invalid JSON."""
await mqtt_mock_entry()
with patch(
"homeassistant.components.mqtt.discovery.async_dispatcher_send"
) as mock_dispatcher_send:
mock_dispatcher_send = AsyncMock(return_value=None)
async_fire_mqtt_message(hass, discovery_topic, "not json")
await hass.async_block_till_done()
assert "Unable to parse JSON" in caplog.text
assert not mock_dispatcher_send.called
@pytest.mark.parametrize(
"domain", ["tag", "device_automation", Platform.SENSOR, Platform.LIGHT]
)
@pytest.mark.no_fail_on_log_exception
async def test_discovery_schema_error(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
domain: Platform | str,
) -> None:
"""Test unexpected error JSON config."""
with patch(
f"homeassistant.components.mqtt.{domain}.DISCOVERY_SCHEMA",
side_effect=AttributeError("Attribute abc not found"),
):
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
f"homeassistant/{domain}/bla/config",
'{"name": "Beer", "some_topic": "bla"}',
)
await hass.async_block_till_done()
assert "AttributeError: Attribute abc not found" in caplog.text
async def test_invalid_config(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test sending in JSON that violates the platform schema."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/alarm_control_panel/bla/config",
'{"name": "abc", "state_topic": "home/alarm", '
'"command_topic": "home/alarm/set", '
'"qos": "some_invalid_value"}',
)
await hass.async_block_till_done()
assert "Error 'expected int for dictionary value @ data['qos']'" in caplog.text
async def test_invalid_device_discovery_config(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test sending in JSON that violates the discovery schema if device or platform key is missing."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/device/bla/config",
'{ "o": {"name": "foobar"}, "cmp": '
'{ "acp1": {"name": "abc", "state_topic": "home/alarm", '
'"command_topic": "home/alarm/set", '
'"platform":"alarm_control_panel"}}}',
)
await hass.async_block_till_done()
assert (
"Invalid MQTT device discovery payload for bla, "
"required key not provided @ data['device']" in caplog.text
)
caplog.clear()
async_fire_mqtt_message(
hass,
"homeassistant/device/bla/config",
'{ "o": {"name": "foobar"}, "dev": {"identifiers": ["ABDE03"]}, '
'"cmp": { "acp1": {"name": "abc", "state_topic": "home/alarm", '
'"command_topic": "home/alarm/set" }}}',
)
await hass.async_block_till_done()
assert (
"Invalid MQTT device discovery payload for bla, "
"required key not provided @ data['components']['acp1']['platform']"
in caplog.text
)
async def test_only_valid_components(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test for a valid component."""
await mqtt_mock_entry()
with patch(
"homeassistant.components.mqtt.discovery.async_dispatcher_send"
) as mock_dispatcher_send:
invalid_component = "timer"
mock_dispatcher_send = AsyncMock(return_value=None)
async_fire_mqtt_message(
hass, f"homeassistant/{invalid_component}/bla/config", "{}"
)
await hass.async_block_till_done()
assert f"Integration {invalid_component} is not supported" in caplog.text
assert not mock_dispatcher_send.called
async def test_correct_config_discovery(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test sending in correct JSON."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.name == "Beer"
assert ("binary_sensor", "bla") in hass.data["mqtt"].discovery_already_discovered
@pytest.mark.parametrize(
("discovery_topic", "payloads", "discovery_id"),
[
(
"homeassistant/binary_sensor/bla/config",
(
'{"name":"Beer","state_topic": "test-topic",'
'"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}',
'{"name":"Milk","state_topic": "test-topic",'
'"o":{"name":"bla2mqtt","sw":"1.1",'
'"url":"https://bla2mqtt.example.com/support"},'
'"dev":{"identifiers":["bla"]}}',
),
"bla",
),
(
"homeassistant/device/bla/config",
(
'{"cmp":{"bin_sens1":{"platform":"binary_sensor",'
'"name":"Beer","state_topic": "test-topic"}},'
'"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}',
'{"cmp":{"bin_sens1":{"platform":"binary_sensor",'
'"name":"Milk","state_topic": "test-topic"}},'
'"o":{"name":"bla2mqtt","sw":"1.1",'
'"url":"https://bla2mqtt.example.com/support"},'
'"dev":{"identifiers":["bla"]}}',
),
"bla bin_sens1",
),
],
)
async def test_discovery_integration_info(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
discovery_topic: str,
payloads: tuple[str, str],
discovery_id: str,
) -> None:
"""Test discovery of integration info."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
discovery_topic,
payloads[0],
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.name == "Beer"
assert (
"Processing device discovery for 'bla' from external "
"application bla2mqtt, version: 1.0"
in caplog.text
or f"Found new component: binary_sensor {discovery_id} from external application bla2mqtt, version: 1.0"
in caplog.text
)
caplog.clear()
# Send an update and add support url
async_fire_mqtt_message(
hass,
discovery_topic,
payloads[1],
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.name == "Milk"
assert (
f"Component has already been discovered: binary_sensor {discovery_id}"
in caplog.text
)
@pytest.mark.parametrize(
("single_configs", "device_discovery_topic", "device_config"),
[
(
[
(
"homeassistant/device_automation/0AFFD2/bla1/config",
{
"device": {"identifiers": ["0AFFD2"]},
"automation_type": "trigger",
"payload": "short_press",
"topic": "foobar/triggers/button1",
"type": "button_short_press",
"subtype": "button_1",
},
),
(
"homeassistant/sensor/0AFFD2/bla2/config",
{
"device": {"identifiers": ["0AFFD2"]},
"state_topic": "foobar/sensors/bla2/state",
},
),
(
"homeassistant/tag/0AFFD2/bla3/config",
{
"device": {"identifiers": ["0AFFD2"]},
"topic": "foobar/tags/bla3/see",
},
),
],
"homeassistant/device/0AFFD2/config",
{
"device": {"identifiers": ["0AFFD2"]},
"o": {"name": "foobar"},
"cmp": {
"bla1": {
"platform": "device_automation",
"automation_type": "trigger",
"payload": "short_press",
"topic": "foobar/triggers/button1",
"type": "button_short_press",
"subtype": "button_1",
},
"bla2": {
"platform": "sensor",
"state_topic": "foobar/sensors/bla2/state",
},
"bla3": {
"platform": "tag",
"topic": "foobar/tags/bla3/see",
},
},
},
)
],
)
async def test_discovery_migration(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
tag_mock: AsyncMock,
single_configs: list[tuple[str, dict[str, Any]]],
device_discovery_topic: str,
device_config: dict[str, Any],
) -> None:
"""Test the migration of single discovery to device discovery."""
mock_mqtt = await mqtt_mock_entry()
publish_mock: MagicMock = mock_mqtt._mqttc.publish
# Discovery single config schema
for discovery_topic, config in single_configs:
payload = json.dumps(config)
async_fire_mqtt_message(
hass,
discovery_topic,
payload,
)
await hass.async_block_till_done()
await hass.async_block_till_done()
async def check_discovered_items():
# Check the device_trigger was discovered
device_entry = device_registry.async_get_device(
identifiers={("mqtt", "0AFFD2")}
)
assert device_entry is not None
triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, device_entry.id
)
assert len(triggers) == 1
# Check the sensor was discovered
state = hass.states.get("sensor.mqtt_sensor")
assert state is not None
# Check the tag works
async_fire_mqtt_message(hass, "foobar/tags/bla3/see", DEFAULT_TAG_SCAN)
await hass.async_block_till_done()
tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id)
tag_mock.reset_mock()
await check_discovered_items()
# Migrate to device based discovery
payload = json.dumps(device_config)
async_fire_mqtt_message(
hass,
device_discovery_topic,
payload,
)
await hass.async_block_till_done()
# Test the single discovery topics are reset and `None` is published
await check_discovered_items()
assert len(publish_mock.mock_calls) == len(single_configs)
published_topics = {call[1][0] for call in publish_mock.mock_calls}
expected_topics = {item[0] for item in single_configs}
assert published_topics == expected_topics
published_payloads = [call[1][1] for call in publish_mock.mock_calls]
assert published_payloads == [None, None, None]
@pytest.mark.parametrize(
("discovery_topic", "payload", "discovery_id"),
[
(
"homeassistant/binary_sensor/bla/config",
'{"name":"Beer","state_topic": "test-topic",'
'"avty": {"topic": "avty-topic"},'
'"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}',
"bla",
),
(
"homeassistant/device/bla/config",
'{"cmp":{"bin_sens1":{"platform":"binary_sensor",'
'"name":"Beer","state_topic": "test-topic"}},'
'"avty": {"topic": "avty-topic"},'
'"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}',
"bin_sens1 bla",
),
],
)
async def test_discovery_availability(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
discovery_topic: str,
payload: str,
discovery_id: str,
) -> None:
"""Test device discovery with shared availability mapping."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
discovery_topic,
payload,
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.name == "Beer"
assert state.state == STATE_UNAVAILABLE
async_fire_mqtt_message(
hass,
"avty-topic",
"online",
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.state == STATE_UNKNOWN
async_fire_mqtt_message(
hass,
"test-topic",
"ON",
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.state == STATE_ON
@pytest.mark.parametrize(
("discovery_topic", "payload", "discovery_id"),
[
(
"homeassistant/device/bla/config",
'{"cmp":{"bin_sens1":{"platform":"binary_sensor",'
'"avty": {"topic": "avty-topic-component"},'
'"name":"Beer","state_topic": "test-topic"}},'
'"avty": {"topic": "avty-topic-device"},'
'"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}',
"bin_sens1 bla",
),
(
"homeassistant/device/bla/config",
'{"cmp":{"bin_sens1":{"platform":"binary_sensor",'
'"availability_topic": "avty-topic-component",'
'"name":"Beer","state_topic": "test-topic"}},'
'"availability_topic": "avty-topic-device",'
'"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}',
"bin_sens1 bla",
),
],
)
async def test_discovery_component_availability_overridden(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
discovery_topic: str,
payload: str,
discovery_id: str,
) -> None:
"""Test device discovery with overridden shared availability mapping."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
discovery_topic,
payload,
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.name == "Beer"
assert state.state == STATE_UNAVAILABLE
async_fire_mqtt_message(
hass,
"avty-topic-device",
"online",
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.state == STATE_UNAVAILABLE
async_fire_mqtt_message(
hass,
"avty-topic-component",
"online",
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.state == STATE_UNKNOWN
async_fire_mqtt_message(
hass,
"test-topic",
"ON",
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.state == STATE_ON
@pytest.mark.parametrize(
("discovery_topic", "config_message", "error_message"),
[
(
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic", "o": "bla2mqtt" }',
"Unable to parse origin information from discovery message",
),
(
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic", "o": 2.0 }',
"Unable to parse origin information from discovery message",
),
(
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic", "o": null }',
"Unable to parse origin information from discovery message",
),
(
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic", "o": {"sw": "bla2mqtt"} }',
"Unable to parse origin information from discovery message",
),
(
"homeassistant/device/bla/config",
'{"dev":{"identifiers":["bs1"]},"cmp":{"bs1":'
'{"platform":"binary_sensor","name":"Beer","state_topic":"test-topic"}'
'},"o": "bla2mqtt"'
"}",
"Invalid MQTT device discovery payload for bla, "
"expected a dictionary for dictionary value @ data['origin']",
),
(
"homeassistant/device/bla/config",
'{"dev":{"identifiers":["bs1"]},"cmp":{"bs1":'
'{"platform":"binary_sensor","name":"Beer","state_topic":"test-topic"}'
'},"o": 2.0'
"}",
"Invalid MQTT device discovery payload for bla, "
"expected a dictionary for dictionary value @ data['origin']",
),
(
"homeassistant/device/bla/config",
'{"dev":{"identifiers":["bs1"]},"cmp":{"bs1":'
'{"platform":"binary_sensor","name":"Beer","state_topic":"test-topic"}'
'},"o": null'
"}",
"Invalid MQTT device discovery payload for bla, "
"expected a dictionary for dictionary value @ data['origin']",
),
(
"homeassistant/device/bla/config",
'{"dev":{"identifiers":["bs1"]},"cmp":{"bs1":'
'{"platform":"binary_sensor","name":"Beer","state_topic":"test-topic"}'
'},"o": {"sw": "bla2mqtt"}'
"}",
"Invalid MQTT device discovery payload for bla, "
"required key not provided @ data['origin']['name']",
),
],
)
async def test_discovery_with_invalid_integration_info(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
discovery_topic: str,
config_message: str,
error_message: str,
) -> None:
"""Test sending in correct JSON."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
discovery_topic,
config_message,
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is None
assert error_message in caplog.text
async def test_discover_fan(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test discovering an MQTT fan."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/fan/bla/config",
'{ "name": "Beer", "command_topic": "test_topic" }',
)
await hass.async_block_till_done()
state = hass.states.get("fan.beer")
assert state is not None
assert state.name == "Beer"
assert ("fan", "bla") in hass.data["mqtt"].discovery_already_discovered
async def test_discover_climate(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test discovering an MQTT climate component."""
await mqtt_mock_entry()
data = (
'{ "name": "ClimateTest",'
' "current_temperature_topic": "climate/bla/current_temp",'
' "temperature_command_topic": "climate/bla/target_temp" }'
)
async_fire_mqtt_message(hass, "homeassistant/climate/bla/config", data)
await hass.async_block_till_done()
state = hass.states.get("climate.ClimateTest")
assert state is not None
assert state.name == "ClimateTest"
assert ("climate", "bla") in hass.data["mqtt"].discovery_already_discovered
async def test_discover_alarm_control_panel(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test discovering an MQTT alarm control panel component."""
await mqtt_mock_entry()
data = (
'{ "name": "AlarmControlPanelTest",'
' "state_topic": "test_topic",'
' "command_topic": "test_topic" }'
)
async_fire_mqtt_message(hass, "homeassistant/alarm_control_panel/bla/config", data)
await hass.async_block_till_done()
state = hass.states.get("alarm_control_panel.AlarmControlPanelTest")
assert state is not None
assert state.name == "AlarmControlPanelTest"
assert ("alarm_control_panel", "bla") in hass.data[
"mqtt"
].discovery_already_discovered
@pytest.mark.parametrize(
("topic", "config", "entity_id", "name", "domain"),
[
(
"homeassistant/alarm_control_panel/object/bla/config",
'{ "name": "Hello World 1", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"alarm_control_panel.hello_id",
"Hello World 1",
"alarm_control_panel",
),
(
"homeassistant/binary_sensor/object/bla/config",
'{ "name": "Hello World 2", "obj_id": "hello_id", "state_topic": "test-topic" }',
"binary_sensor.hello_id",
"Hello World 2",
"binary_sensor",
),
(
"homeassistant/button/object/bla/config",
'{ "name": "Hello World button", "obj_id": "hello_id", "command_topic": "test-topic" }',
"button.hello_id",
"Hello World button",
"button",
),
(
"homeassistant/camera/object/bla/config",
'{ "name": "Hello World 3", "obj_id": "hello_id", "state_topic": "test-topic", "topic": "test-topic" }',
"camera.hello_id",
"Hello World 3",
"camera",
),
(
"homeassistant/climate/object/bla/config",
'{ "name": "Hello World 4", "obj_id": "hello_id", "state_topic": "test-topic" }',
"climate.hello_id",
"Hello World 4",
"climate",
),
(
"homeassistant/cover/object/bla/config",
'{ "name": "Hello World 5", "obj_id": "hello_id", "state_topic": "test-topic" }',
"cover.hello_id",
"Hello World 5",
"cover",
),
(
"homeassistant/fan/object/bla/config",
'{ "name": "Hello World 6", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"fan.hello_id",
"Hello World 6",
"fan",
),
(
"homeassistant/humidifier/object/bla/config",
'{ "name": "Hello World 7", "obj_id": "hello_id", "state_topic": "test-topic", "target_humidity_command_topic": "test-topic", "command_topic": "test-topic" }',
"humidifier.hello_id",
"Hello World 7",
"humidifier",
),
(
"homeassistant/number/object/bla/config",
'{ "name": "Hello World 8", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"number.hello_id",
"Hello World 8",
"number",
),
(
"homeassistant/scene/object/bla/config",
'{ "name": "Hello World 9", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"scene.hello_id",
"Hello World 9",
"scene",
),
(
"homeassistant/select/object/bla/config",
'{ "name": "Hello World 10", "obj_id": "hello_id", "state_topic": "test-topic", "options": [ "opt1", "opt2" ], "command_topic": "test-topic" }',
"select.hello_id",
"Hello World 10",
"select",
),
(
"homeassistant/sensor/object/bla/config",
'{ "name": "Hello World 11", "obj_id": "hello_id", "state_topic": "test-topic" }',
"sensor.hello_id",
"Hello World 11",
"sensor",
),
(
"homeassistant/switch/object/bla/config",
'{ "name": "Hello World 12", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"switch.hello_id",
"Hello World 12",
"switch",
),
(
"homeassistant/light/object/bla/config",
'{ "name": "Hello World 13", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"light.hello_id",
"Hello World 13",
"light",
),
(
"homeassistant/light/object/bla/config",
'{ "name": "Hello World 14", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic", "schema": "json" }',
"light.hello_id",
"Hello World 14",
"light",
),
(
"homeassistant/light/object/bla/config",
'{ "name": "Hello World 15", "obj_id": "hello_id", "state_topic": "test-topic", "command_off_template": "template", "command_on_template": "template", "command_topic": "test-topic", "schema": "template" }',
"light.hello_id",
"Hello World 15",
"light",
),
(
"homeassistant/vacuum/object/bla/config",
'{ "name": "Hello World 16", "obj_id": "hello_id", "state_topic": "test-topic", "schema": "state" }',
"vacuum.hello_id",
"Hello World 16",
"vacuum",
),
(
"homeassistant/valve/object/bla/config",
'{ "name": "Hello World 17", "obj_id": "hello_id", "state_topic": "test-topic" }',
"valve.hello_id",
"Hello World 17",
"valve",
),
(
"homeassistant/lock/object/bla/config",
'{ "name": "Hello World 18", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"lock.hello_id",
"Hello World 18",
"lock",
),
(
"homeassistant/device_tracker/object/bla/config",
'{ "name": "Hello World 19", "obj_id": "hello_id", "state_topic": "test-topic" }',
"device_tracker.hello_id",
"Hello World 19",
"device_tracker",
),
],
)
async def test_discovery_with_object_id(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
topic: str,
config: str,
entity_id: str,
name: str,
domain: str,
) -> None:
"""Test discovering an MQTT entity with object_id."""
await mqtt_mock_entry()
async_fire_mqtt_message(hass, topic, config)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
assert state.name == name
assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered
async def test_discovery_incl_nodeid(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test sending in correct JSON with optional node_id included."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/my_node_id/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.name == "Beer"
assert ("binary_sensor", "my_node_id bla") in hass.data[
"mqtt"
].discovery_already_discovered
async def test_non_duplicate_discovery(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test for a non duplicate component."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
)
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
state_duplicate = hass.states.get("binary_sensor.beer1")
assert state is not None
assert state.name == "Beer"
assert state_duplicate is None
assert "Component has already been discovered: binary_sensor bla" in caplog.text
async def test_removal(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test removal of component through empty discovery message."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "")
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is None
async def test_rediscover(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test rediscover of removed component."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "")
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is None
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
async def test_rapid_rediscover(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test immediate rediscover of removed component."""
await mqtt_mock_entry()
events = async_capture_events(hass, EVENT_STATE_CHANGED)
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert len(events) == 1
# Removal immediately followed by rediscover
async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "")
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
)
async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "")
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Milk", "state_topic": "test-topic" }',
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("binary_sensor")) == 1
state = hass.states.get("binary_sensor.milk")
assert state is not None
assert len(events) == 5
# Remove the entity
assert events[1].data["entity_id"] == "binary_sensor.beer"
assert events[1].data["new_state"] is None
# Add the entity
assert events[2].data["entity_id"] == "binary_sensor.beer"
assert events[2].data["old_state"] is None
# Remove the entity
assert events[3].data["entity_id"] == "binary_sensor.beer"
assert events[3].data["new_state"] is None
# Add the entity
assert events[4].data["entity_id"] == "binary_sensor.milk"
assert events[4].data["old_state"] is None
async def test_rapid_rediscover_unique(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test immediate rediscover of removed component."""
await mqtt_mock_entry()
events = []
@callback
def test_callback(event: Event) -> None:
"""Verify event got called."""
events.append(event)
hass.bus.async_listen(EVENT_STATE_CHANGED, test_callback)
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla2/config",
'{ "name": "Ale", "state_topic": "test-topic", "unique_id": "very_unique" }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.ale")
assert state is not None
assert len(events) == 1
# Duplicate unique_id, immediately followed by correct unique_id
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic", "unique_id": "very_unique" }',
)
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic", "unique_id": "even_uniquer" }',
)
async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "")
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Milk", "state_topic": "test-topic", "unique_id": "even_uniquer" }',
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("binary_sensor")) == 2
state = hass.states.get("binary_sensor.ale")
assert state is not None
state = hass.states.get("binary_sensor.milk")
assert state is not None
assert len(events) == 4
# Add the entity
assert events[1].data["entity_id"] == "binary_sensor.beer"
assert events[1].data["old_state"] is None
# Remove the entity
assert events[2].data["entity_id"] == "binary_sensor.beer"
assert events[2].data["new_state"] is None
# Add the entity
assert events[3].data["entity_id"] == "binary_sensor.milk"
assert events[3].data["old_state"] is None
async def test_rapid_reconfigure(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test immediate reconfigure of added component."""
await mqtt_mock_entry()
events = []
@callback
def test_callback(event: Event) -> None:
"""Verify event got called."""
events.append(event)
hass.bus.async_listen(EVENT_STATE_CHANGED, test_callback)
# Discovery immediately followed by reconfig
async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "")
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic1" }',
)
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Milk", "state_topic": "test-topic2" }',
)
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Wine", "state_topic": "test-topic3" }',
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("binary_sensor")) == 1
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert len(events) == 3
# Add the entity
assert events[0].data["entity_id"] == "binary_sensor.beer"
assert events[0].data["old_state"] is None
assert events[0].data["new_state"].attributes["friendly_name"] == "Beer"
# Update the entity
assert events[1].data["entity_id"] == "binary_sensor.beer"
assert events[1].data["new_state"] is not None
assert events[1].data["old_state"] is not None
assert events[1].data["new_state"].attributes["friendly_name"] == "Milk"
# Update the entity
assert events[2].data["entity_id"] == "binary_sensor.beer"
assert events[2].data["new_state"] is not None
assert events[2].data["old_state"] is not None
assert events[2].data["new_state"].attributes["friendly_name"] == "Wine"
async def test_duplicate_removal(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test for a non duplicate component."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic" }',
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "")
await hass.async_block_till_done()
assert "Component has already been discovered: binary_sensor bla" in caplog.text
caplog.clear()
async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "")
await hass.async_block_till_done()
assert "Component has already been discovered: binary_sensor bla" not in caplog.text
@pytest.mark.parametrize(
("discovery_topic", "discovery_payload", "entity_ids"),
[
(
"homeassistant/sensor/bla/config",
'{ "device":{"identifiers":["0AFFD2"]},'
' "state_topic": "foobar/sensor",'
' "unique_id": "unique" }',
["sensor.none_mqtt_sensor"],
),
(
"homeassistant/device/bla/config",
'{ "device":{"identifiers":["0AFFD2"]},'
' "o": {"name": "foobar"},'
' "cmp": {"sens1": {'
' "platform": "sensor",'
' "name": "sensor1",'
' "state_topic": "foobar/sensor1",'
' "unique_id": "unique1"'
' },"sens2": {'
' "platform": "sensor",'
' "name": "sensor2",'
' "state_topic": "foobar/sensor2",'
' "unique_id": "unique2"'
"}}}",
["sensor.none_sensor1", "sensor.none_sensor2"],
),
],
)
async def test_cleanup_device(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
discovery_topic: str,
discovery_payload: str,
entity_ids: list[str],
) -> None:
"""Test discovered device is cleaned up when entry removed from device."""
mqtt_mock = await mqtt_mock_entry()
assert await async_setup_component(hass, "config", {})
ws_client = await hass_ws_client(hass)
async_fire_mqtt_message(hass, discovery_topic, discovery_payload)
await hass.async_block_till_done()
# Verify device and registry entries are created
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is not None
for entity_id in entity_ids:
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry is not None
state = hass.states.get(entity_id)
assert state is not None
# Remove MQTT from the device
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
response = await ws_client.remove_device(
device_entry.id, mqtt_config_entry.entry_id
)
assert response["success"]
await hass.async_block_till_done()
await hass.async_block_till_done()
# Verify device and registry entries are cleared
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is None
entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor")
assert entity_entry is None
# Verify state is removed
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state is None
await hass.async_block_till_done()
# Verify retained discovery topic has been cleared
mqtt_mock.async_publish.assert_called_with(discovery_topic, None, 0, True)
@pytest.mark.parametrize(
("discovery_topic", "discovery_payload", "entity_ids"),
[
(
"homeassistant/sensor/bla/config",
'{ "device":{"identifiers":["0AFFD2"]},'
' "state_topic": "foobar/sensor",'
' "unique_id": "unique" }',
["sensor.none_mqtt_sensor"],
),
(
"homeassistant/device/bla/config",
'{ "device":{"identifiers":["0AFFD2"]},'
' "o": {"name": "foobar"},'
' "cmp": {"sens1": {'
' "platform": "sensor",'
' "name": "sensor1",'
' "state_topic": "foobar/sensor1",'
' "unique_id": "unique1"'
' },"sens2": {'
' "platform": "sensor",'
' "name": "sensor2",'
' "state_topic": "foobar/sensor2",'
' "unique_id": "unique2"'
"}}}",
["sensor.none_sensor1", "sensor.none_sensor2"],
),
],
)
async def test_cleanup_device_mqtt(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
discovery_topic: str,
discovery_payload: str,
entity_ids: list[str],
) -> None:
"""Test discovered device is cleaned up when removed through MQTT."""
mqtt_mock = await mqtt_mock_entry()
# set up an existing sensor first
data = (
'{ "device":{"identifiers":["0AFFD3"]},'
' "name": "sensor_base",'
' "state_topic": "foobar/sensor",'
' "unique_id": "unique_base" }'
)
base_discovery_topic = "homeassistant/sensor/bla_base/config"
base_entity_id = "sensor.none_sensor_base"
async_fire_mqtt_message(hass, base_discovery_topic, data)
await hass.async_block_till_done()
# Verify the base entity has been created and it has a state
base_device_entry = device_registry.async_get_device(
identifiers={("mqtt", "0AFFD3")}
)
assert base_device_entry is not None
entity_entry = entity_registry.async_get(base_entity_id)
assert entity_entry is not None
state = hass.states.get(base_entity_id)
assert state is not None
async_fire_mqtt_message(hass, discovery_topic, discovery_payload)
await hass.async_block_till_done()
# Verify device and registry entries are created
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is not None
for entity_id in entity_ids:
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry is not None
state = hass.states.get(entity_id)
assert state is not None
async_fire_mqtt_message(hass, discovery_topic, "")
await hass.async_block_till_done()
await hass.async_block_till_done()
# Verify device and registry entries are cleared
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is None
for entity_id in entity_ids:
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry is None
# Verify state is removed
state = hass.states.get(entity_id)
assert state is None
await hass.async_block_till_done()
# Verify retained discovery topics have not been cleared again
mqtt_mock.async_publish.assert_not_called()
# Verify the base entity still exists and it has a state
base_device_entry = device_registry.async_get_device(
identifiers={("mqtt", "0AFFD3")}
)
assert base_device_entry is not None
entity_entry = entity_registry.async_get(base_entity_id)
assert entity_entry is not None
state = hass.states.get(base_entity_id)
assert state is not None
async def test_cleanup_device_mqtt_device_discovery(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test discovered device is cleaned up partly when removed through MQTT."""
await mqtt_mock_entry()
discovery_topic = "homeassistant/device/bla/config"
discovery_payload = (
'{ "device":{"identifiers":["0AFFD2"]},'
' "o": {"name": "foobar"},'
' "cmp": {"sens1": {'
' "platform": "sensor",'
' "name": "sensor1",'
' "state_topic": "foobar/sensor1",'
' "unique_id": "unique1"'
' },"sens2": {'
' "platform": "sensor",'
' "name": "sensor2",'
' "state_topic": "foobar/sensor2",'
' "unique_id": "unique2"'
"}}}"
)
entity_ids = ["sensor.none_sensor1", "sensor.none_sensor2"]
async_fire_mqtt_message(hass, discovery_topic, discovery_payload)
await hass.async_block_till_done()
# Verify device and registry entries are created
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is not None
for entity_id in entity_ids:
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry is not None
state = hass.states.get(entity_id)
assert state is not None
# Do update and remove sensor 2 from device
discovery_payload_update1 = (
'{ "device":{"identifiers":["0AFFD2"]},'
' "o": {"name": "foobar"},'
' "cmp": {"sens1": {'
' "platform": "sensor",'
' "name": "sensor1",'
' "state_topic": "foobar/sensor1",'
' "unique_id": "unique1"'
' },"sens2": {'
' "platform": "sensor"'
"}}}"
)
async_fire_mqtt_message(hass, discovery_topic, discovery_payload_update1)
await hass.async_block_till_done()
state = hass.states.get(entity_ids[0])
assert state is not None
state = hass.states.get(entity_ids[1])
assert state is None
# Repeating the update
async_fire_mqtt_message(hass, discovery_topic, discovery_payload_update1)
await hass.async_block_till_done()
state = hass.states.get(entity_ids[0])
assert state is not None
state = hass.states.get(entity_ids[1])
assert state is None
# Removing last sensor
discovery_payload_update2 = (
'{ "device":{"identifiers":["0AFFD2"]},'
' "o": {"name": "foobar"},'
' "cmp": {"sens1": {'
' "platform": "sensor"'
' },"sens2": {'
' "platform": "sensor"'
"}}}"
)
async_fire_mqtt_message(hass, discovery_topic, discovery_payload_update2)
await hass.async_block_till_done()
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
# Verify the device entry was removed with the last sensor
assert device_entry is None
for entity_id in entity_ids:
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry is None
state = hass.states.get(entity_id)
assert state is None
# Repeating the update
async_fire_mqtt_message(hass, discovery_topic, discovery_payload_update2)
await hass.async_block_till_done()
# Clear the empty discovery payload and verify there was nothing to cleanup
async_fire_mqtt_message(hass, discovery_topic, "")
await hass.async_block_till_done()
assert "No device components to cleanup" in caplog.text
async def test_cleanup_device_multiple_config_entries(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test discovered device is cleaned up when entry removed from device."""
assert await async_setup_component(hass, "config", {})
await hass.async_block_till_done()
mqtt_mock = await mqtt_mock_entry()
ws_client = await hass_ws_client(hass)
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={("mac", "12:34:56:AB:CD:EF")},
)
assert device_entry is not None
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
sensor_config = {
"device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]},
"state_topic": "foobar/sensor",
"unique_id": "unique",
}
tag_config = {
"device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]},
"topic": "test-topic",
}
trigger_config = {
"automation_type": "trigger",
"topic": "test-topic",
"type": "foo",
"subtype": "bar",
"device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]},
}
sensor_data = json.dumps(sensor_config)
tag_data = json.dumps(tag_config)
trigger_data = json.dumps(trigger_config)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", sensor_data)
async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", tag_data)
async_fire_mqtt_message(
hass, "homeassistant/device_automation/bla/config", trigger_data
)
await hass.async_block_till_done()
# Verify device and registry entries are created
device_entry = device_registry.async_get_device(
connections={("mac", "12:34:56:AB:CD:EF")}
)
assert device_entry is not None
assert device_entry.config_entries == {
mqtt_config_entry.entry_id,
config_entry.entry_id,
}
entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor")
assert entity_entry is not None
state = hass.states.get("sensor.none_mqtt_sensor")
assert state is not None
# Remove MQTT from the device
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
response = await ws_client.remove_device(
device_entry.id, mqtt_config_entry.entry_id
)
assert response["success"]
await hass.async_block_till_done()
await hass.async_block_till_done()
# Verify device is still there but entity is cleared
device_entry = device_registry.async_get_device(
connections={("mac", "12:34:56:AB:CD:EF")}
)
assert device_entry is not None
entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor")
assert device_entry.config_entries == {config_entry.entry_id}
assert entity_entry is None
# Verify state is removed
state = hass.states.get("sensor.none_mqtt_sensor")
assert state is None
await hass.async_block_till_done()
# Verify retained discovery topic has been cleared
mqtt_mock.async_publish.assert_has_calls(
[
call("homeassistant/sensor/bla/config", None, 0, True),
call("homeassistant/tag/bla/config", None, 0, True),
call("homeassistant/device_automation/bla/config", None, 0, True),
],
any_order=True,
)
async def test_cleanup_device_multiple_config_entries_mqtt(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test discovered device is cleaned up when removed through MQTT."""
mqtt_mock = await mqtt_mock_entry()
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={("mac", "12:34:56:AB:CD:EF")},
)
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
sensor_config = {
"device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]},
"state_topic": "foobar/sensor",
"unique_id": "unique",
}
tag_config = {
"device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]},
"topic": "test-topic",
}
trigger_config = {
"automation_type": "trigger",
"topic": "test-topic",
"type": "foo",
"subtype": "bar",
"device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]},
}
sensor_data = json.dumps(sensor_config)
tag_data = json.dumps(tag_config)
trigger_data = json.dumps(trigger_config)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", sensor_data)
async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", tag_data)
async_fire_mqtt_message(
hass, "homeassistant/device_automation/bla/config", trigger_data
)
await hass.async_block_till_done()
# Verify device and registry entries are created
device_entry = device_registry.async_get_device(
connections={("mac", "12:34:56:AB:CD:EF")}
)
assert device_entry is not None
assert device_entry.config_entries == {
mqtt_config_entry.entry_id,
config_entry.entry_id,
}
entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor")
assert entity_entry is not None
state = hass.states.get("sensor.none_mqtt_sensor")
assert state is not None
# Send MQTT messages to remove
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", "")
async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", "")
async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", "")
await hass.async_block_till_done()
await hass.async_block_till_done()
# Verify device is still there but entity is cleared
device_entry = device_registry.async_get_device(
connections={("mac", "12:34:56:AB:CD:EF")}
)
assert device_entry is not None
entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor")
assert device_entry.config_entries == {config_entry.entry_id}
assert entity_entry is None
# Verify state is removed
state = hass.states.get("sensor.none_mqtt_sensor")
assert state is None
await hass.async_block_till_done()
# Verify retained discovery topics have not been cleared again
mqtt_mock.async_publish.assert_not_called()
async def test_discovery_expansion(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test expansion of abbreviated discovery payload."""
await mqtt_mock_entry()
data = (
'{ "~": "some/base/topic",'
' "name": "DiscoveryExpansionTest1",'
' "stat_t": "test_topic/~",'
' "cmd_t": "~/test_topic",'
' "availability": ['
" {"
' "topic":"~/avail_item1",'
' "payload_available": "available",'
' "payload_not_available": "not_available"'
" },"
" {"
' "t":"avail_item2/~",'
' "pl_avail": "available",'
' "pl_not_avail": "not_available"'
" }"
" ],"
' "dev":{'
' "ids":["5706DF"],'
' "name":"DiscoveryExpansionTest1 Device",'
' "mdl":"Generic",'
' "hw":"rev1",'
' "sw":"1.2.3.4",'
' "mf":"None",'
' "sa":"default_area"'
" }"
"}"
)
async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data)
await hass.async_block_till_done()
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state and state.state == STATE_UNAVAILABLE
async_fire_mqtt_message(hass, "avail_item2/some/base/topic", "available")
await hass.async_block_till_done()
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state is not None
assert state.name == "DiscoveryExpansionTest1"
assert ("switch", "bla") in hass.data["mqtt"].discovery_already_discovered
assert state.state == STATE_UNKNOWN
async_fire_mqtt_message(hass, "test_topic/some/base/topic", "ON")
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state and state.state == STATE_ON
async_fire_mqtt_message(hass, "some/base/topic/avail_item1", "not_available")
await hass.async_block_till_done()
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state and state.state == STATE_UNAVAILABLE
async def test_discovery_expansion_2(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test expansion of abbreviated discovery payload."""
await mqtt_mock_entry()
data = (
'{ "~": "some/base/topic",'
' "name": "DiscoveryExpansionTest1",'
' "stat_t": "test_topic/~",'
' "cmd_t": "~/test_topic",'
' "availability": {'
' "t":"~/avail_item1",'
' "pl_avail": "available",'
' "pl_not_avail": "not_available"'
" },"
' "dev":{'
' "ids":["5706DF"],'
' "name":"DiscoveryExpansionTest1 Device",'
' "mdl":"Generic",'
' "hw":"rev1",'
' "sw":"1.2.3.4",'
' "mf":"None",'
' "sa":"default_area"'
" }"
"}"
)
async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data)
await hass.async_block_till_done()
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state and state.state == STATE_UNAVAILABLE
async_fire_mqtt_message(hass, "some/base/topic/avail_item1", "available")
await hass.async_block_till_done()
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state is not None
assert state.name == "DiscoveryExpansionTest1"
assert ("switch", "bla") in hass.data["mqtt"].discovery_already_discovered
assert state.state == STATE_UNKNOWN
async def test_discovery_expansion_3(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test expansion of broken discovery payload."""
await mqtt_mock_entry()
data = (
'{ "~": "some/base/topic",'
' "name": "DiscoveryExpansionTest1",'
' "stat_t": "test_topic/~",'
' "cmd_t": "~/test_topic",'
' "availability": "incorrect",'
' "dev":{'
' "ids":["5706DF"],'
' "name":"DiscoveryExpansionTest1 Device",'
' "mdl":"Generic",'
' "hw":"rev1",'
' "sw":"1.2.3.4",'
' "mf":"None",'
' "sa":"default_area"'
" }"
"}"
)
async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data)
await hass.async_block_till_done()
assert hass.states.get("switch.DiscoveryExpansionTest1") is None
# Make sure the malformed availability data does not trip up discovery by asserting
# there are schema valdiation errors in the log
assert "expected a dictionary @ data['availability'][0]" in caplog.text
async def test_discovery_expansion_without_encoding_and_value_template_1(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test expansion of raw availability payload with a template as list."""
await mqtt_mock_entry()
data = (
'{ "~": "some/base/topic",'
' "name": "DiscoveryExpansionTest1",'
' "stat_t": "test_topic/~",'
' "cmd_t": "~/test_topic",'
' "encoding":"",'
' "availability": [{'
' "topic":"~/avail_item1",'
' "payload_available": "1",'
' "payload_not_available": "0",'
' "value_template":"{{value|unpack(\'b\')}}"'
" }],"
' "dev":{'
' "ids":["5706DF"],'
' "name":"DiscoveryExpansionTest1 Device",'
' "mdl":"Generic",'
' "hw":"rev1",'
' "sw":"1.2.3.4",'
' "mf":"None",'
' "sa":"default_area"'
" }"
"}"
)
async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data)
await hass.async_block_till_done()
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state and state.state == STATE_UNAVAILABLE
async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x01")
await hass.async_block_till_done()
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state is not None
assert state.name == "DiscoveryExpansionTest1"
assert ("switch", "bla") in hass.data["mqtt"].discovery_already_discovered
assert state.state == STATE_UNKNOWN
async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00")
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state and state.state == STATE_UNAVAILABLE
async def test_discovery_expansion_without_encoding_and_value_template_2(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test expansion of raw availability payload with a template directly."""
await mqtt_mock_entry()
data = (
'{ "~": "some/base/topic",'
' "name": "DiscoveryExpansionTest1",'
' "stat_t": "test_topic/~",'
' "cmd_t": "~/test_topic",'
' "availability_topic":"~/avail_item1",'
' "payload_available": "1",'
' "payload_not_available": "0",'
' "encoding":"",'
' "availability_template":"{{ value | unpack(\'b\') }}",'
' "dev":{'
' "ids":["5706DF"],'
' "name":"DiscoveryExpansionTest1 Device",'
' "mdl":"Generic",'
' "hw":"rev1",'
' "sw":"1.2.3.4",'
' "mf":"None",'
' "sa":"default_area"'
" }"
"}"
)
async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data)
await hass.async_block_till_done()
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state and state.state == STATE_UNAVAILABLE
async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x01")
await hass.async_block_till_done()
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state is not None
assert state.name == "DiscoveryExpansionTest1"
assert ("switch", "bla") in hass.data["mqtt"].discovery_already_discovered
assert state.state == STATE_UNKNOWN
async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00")
state = hass.states.get("switch.DiscoveryExpansionTest1")
assert state and state.state == STATE_UNAVAILABLE
ABBREVIATIONS_WHITE_LIST = [
# MQTT client/server/trigger settings
# Integration info
"CONF_SUPPORT_URL",
# Undocumented device configuration
"CONF_DEPRECATED_VIA_HUB",
"CONF_VIA_DEVICE",
# Already short
"CONF_FAN_MODE_LIST",
"CONF_HOLD_LIST",
"CONF_HS",
"CONF_MODE_LIST",
"CONF_PRECISION",
"CONF_QOS",
"CONF_SCHEMA",
"CONF_SWING_MODE_LIST",
"CONF_TEMP_STEP",
# Removed
"CONF_WHITE_VALUE",
]
EXCLUDED_MODULES = {
"const.py",
"config.py",
"config_flow.py",
"device_trigger.py",
"trigger.py",
}
async def test_missing_discover_abbreviations(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Check MQTT platforms for missing abbreviations."""
await mqtt_mock_entry()
missing = []
regex = re.compile(r"(CONF_[a-zA-Z\d_]*) *= *[\'\"]([a-zA-Z\d_]*)[\'\"]")
for fil in Path(mqtt.__file__).parent.rglob("*.py"):
if fil.name in EXCLUDED_MODULES:
continue
with open(fil, encoding="utf-8") as file:
matches = re.findall(regex, file.read())
missing.extend(
f"{fil}: no abbreviation for {match[1]} ({match[0]})"
for match in matches
if match[1] not in ABBREVIATIONS.values()
and match[1] not in DEVICE_ABBREVIATIONS.values()
and match[0] not in ABBREVIATIONS_WHITE_LIST
)
assert not missing
async def test_no_implicit_state_topic_switch(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test no implicit state topic for switch."""
await mqtt_mock_entry()
data = '{ "name": "Test1", "command_topic": "cmnd" }'
async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data)
await hass.async_block_till_done()
state = hass.states.get("switch.Test1")
assert state is not None
assert state.name == "Test1"
assert ("switch", "bla") in hass.data["mqtt"].discovery_already_discovered
assert state.state == STATE_UNKNOWN
assert state.attributes["assumed_state"] is True
async_fire_mqtt_message(hass, "homeassistant/switch/bla/state", "ON")
state = hass.states.get("switch.Test1")
assert state and state.state == STATE_UNKNOWN
@pytest.mark.parametrize(
"mqtt_config_entry_data",
[
{
mqtt.CONF_BROKER: "mock-broker",
mqtt.CONF_DISCOVERY_PREFIX: "my_home/homeassistant/register",
}
],
)
async def test_complex_discovery_topic_prefix(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:
"""Tests handling of discovery topic prefix with multiple slashes."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
("my_home/homeassistant/register/binary_sensor/node1/object1/config"),
'{ "name": "Beer", "state_topic": "test-topic" }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.name == "Beer"
assert ("binary_sensor", "node1 object1") in hass.data[
"mqtt"
].discovery_already_discovered
@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0)
@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0)
@patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0)
async def test_mqtt_integration_discovery_subscribe_unsubscribe(
hass: HomeAssistant,
mqtt_client_mock: MqttMockPahoClient,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Check MQTT integration discovery subscribe and unsubscribe."""
mqtt_mock = await mqtt_mock_entry()
mock_platform(hass, "comp.config_flow", None)
entry = hass.config_entries.async_entries("mqtt")[0]
mqtt_mock().connected = True
with patch(
"homeassistant.components.mqtt.discovery.async_get_mqtt",
return_value={"comp": ["comp/discovery/#"]},
):
await async_start(hass, "homeassistant", entry)
await hass.async_block_till_done()
await hass.async_block_till_done()
await hass.async_block_till_done()
assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock)
assert not mqtt_client_mock.unsubscribe.called
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult:
"""Test mqtt step."""
return self.async_abort(reason="already_configured")
assert not mqtt_client_mock.unsubscribe.called
wait_unsub = asyncio.Event()
def _mock_unsubscribe(topics: list[str]) -> tuple[int, int]:
wait_unsub.set()
return (0, 0)
with (
mock_config_flow("comp", TestFlow),
patch.object(mqtt_client_mock, "unsubscribe", side_effect=_mock_unsubscribe),
):
async_fire_mqtt_message(hass, "comp/discovery/bla/config", "")
await wait_unsub.wait()
mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"])
@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0)
@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0)
@patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0)
async def test_mqtt_discovery_unsubscribe_once(
hass: HomeAssistant,
mqtt_client_mock: MqttMockPahoClient,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Check MQTT integration discovery unsubscribe once."""
mqtt_mock = await mqtt_mock_entry()
mock_platform(hass, "comp.config_flow", None)
entry = hass.config_entries.async_entries("mqtt")[0]
mqtt_mock().connected = True
with patch(
"homeassistant.components.mqtt.discovery.async_get_mqtt",
return_value={"comp": ["comp/discovery/#"]},
):
await async_start(hass, "homeassistant", entry)
await hass.async_block_till_done()
await hass.async_block_till_done()
await hass.async_block_till_done()
assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock)
assert not mqtt_client_mock.unsubscribe.called
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult:
"""Test mqtt step."""
await asyncio.sleep(0.1)
return self.async_abort(reason="already_configured")
with mock_config_flow("comp", TestFlow):
async_fire_mqtt_message(hass, "comp/discovery/bla/config", "")
async_fire_mqtt_message(hass, "comp/discovery/bla/config", "")
await asyncio.sleep(0.1)
await hass.async_block_till_done()
await hass.async_block_till_done()
mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"])
async def test_clear_config_topic_disabled_entity(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
device_registry: dr.DeviceRegistry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the discovery topic is removed when a disabled entity is removed."""
mqtt_mock = await mqtt_mock_entry()
# discover an entity that is not enabled by default
config = {
"state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/",
"unique_id": "sbfspot_12345",
"enabled_by_default": False,
"device": {
"identifiers": ["sbfspot_12345"],
"name": "abc123",
"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.abc123_sbfspot_12345") is None # disabled
assert hass.states.get("sensor.abc123_sbfspot_12345_1") is not None # enabled
assert hass.states.get("sensor.abc123_sbfspot_12345_2") is None # not unique
# Verify device is created
device_entry = device_registry.async_get_device(
connections={("mac", "12:34:56:AB:CD:EF")}
)
assert device_entry is not None
# Remove the device from the registry
device_registry.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", None, 0, True)
in mqtt_mock.async_publish.mock_calls
)
assert (
call("homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config", None, 0, True)
in mqtt_mock.async_publish.mock_calls
)
async def test_clean_up_registry_monitoring(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
device_registry: dr.DeviceRegistry,
tmp_path: Path,
) -> None:
"""Test registry monitoring hook is removed after a reload."""
await mqtt_mock_entry()
hooks: dict = hass.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_registry.async_get_device(
connections={("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)
assert len(hooks) == 0
async def test_unique_id_collission_has_priority(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
entity_registry: er.EntityRegistry,
) -> None:
"""Test the unique_id collision detection has priority over registry disabled items."""
await mqtt_mock_entry()
config = {
"state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/",
"unique_id": "sbfspot_12345",
"enabled_by_default": False,
"device": {
"identifiers": ["sbfspot_12345"],
"name": "abc123",
"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.abc123_sbfspot_12345_1") is None # not enabled
assert hass.states.get("sensor.abc123_sbfspot_12345_2") is None # not unique
# Verify the first entity is created
assert entity_registry.async_get("sensor.abc123_sbfspot_12345_1") is not None
# Verify the second entity is not created because it is not unique
assert entity_registry.async_get("sensor.abc123_sbfspot_12345_2") is None
async def test_update_with_bad_config_not_breaks_discovery(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:
"""Test a bad update does not break discovery."""
await mqtt_mock_entry()
# discover a sensor
config1 = {
"name": "sbfspot_12345",
"state_topic": "homeassistant_test/sensor/sbfspot_0/state",
}
async_fire_mqtt_message(
hass,
"homeassistant/sensor/sbfspot_0/config",
json.dumps(config1),
)
await hass.async_block_till_done()
assert hass.states.get("sensor.sbfspot_12345") is not None
# update with a breaking config
config2 = {
"name": "sbfspot_12345",
"availability": 1,
"state_topic": "homeassistant_test/sensor/sbfspot_0/state",
}
async_fire_mqtt_message(
hass,
"homeassistant/sensor/sbfspot_0/config",
json.dumps(config2),
)
await hass.async_block_till_done()
# update the state topic
config3 = {
"name": "sbfspot_12345",
"state_topic": "homeassistant_test/sensor/sbfspot_0/new_state_topic",
}
async_fire_mqtt_message(
hass,
"homeassistant/sensor/sbfspot_0/config",
json.dumps(config3),
)
await hass.async_block_till_done()
# Send an update for the state
async_fire_mqtt_message(
hass,
"homeassistant_test/sensor/sbfspot_0/new_state_topic",
"new_value",
)
await hass.async_block_till_done()
state = hass.states.get("sensor.sbfspot_12345")
assert state and state.state == "new_value"
@pytest.mark.parametrize(
"signal_message",
[
MQTT_DISCOVERY_NEW,
MQTT_DISCOVERY_UPDATED,
MQTT_DISCOVERY_DONE,
],
)
async def test_discovery_dispatcher_signal_type_messages(
hass: HomeAssistant, signal_message: SignalTypeFormat[MQTTDiscoveryPayload]
) -> None:
"""Test discovery dispatcher messages."""
domain_id_tuple = ("sensor", "very_unique")
test_data = {"name": "test", "state_topic": "test-topic"}
calls = []
def _callback(*args) -> None:
calls.append(*args)
unsub = async_dispatcher_connect(
hass, signal_message.format(*domain_id_tuple), _callback
)
async_dispatcher_send(hass, signal_message.format(*domain_id_tuple), test_data)
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0] == test_data
unsub()
@pytest.mark.parametrize(
("discovery_topic", "discovery_payload", "entity_ids"),
[
(
"homeassistant/device/bla/config",
'{ "device":{"identifiers":["0AFFD2"]},'
' "o": {"name": "foobar"},'
' "state_topic": "foobar/sensor-shared",'
' "cmp": {"sens1": {'
' "platform": "sensor",'
' "name": "sensor1",'
' "unique_id": "unique1"'
' },"sens2": {'
' "platform": "sensor",'
' "name": "sensor2",'
' "unique_id": "unique2"'
' },"sens3": {'
' "platform": "sensor",'
' "name": "sensor3",'
' "state_topic": "foobar/sensor3",'
' "unique_id": "unique3"'
"}}}",
["sensor.none_sensor1", "sensor.none_sensor2", "sensor.none_sensor3"],
),
],
)
async def test_shared_state_topic(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
discovery_topic: str,
discovery_payload: str,
entity_ids: list[str],
) -> None:
"""Test a shared state_topic can be used."""
await mqtt_mock_entry()
async_fire_mqtt_message(hass, discovery_topic, discovery_payload)
await hass.async_block_till_done()
# Verify device and registry entries are created
device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")})
assert device_entry is not None
for entity_id in entity_ids:
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry is not None
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNKNOWN
async_fire_mqtt_message(hass, "foobar/sensor-shared", "New state")
entity_id = entity_ids[0]
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "New state"
entity_id = entity_ids[1]
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "New state"
entity_id = entity_ids[2]
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNKNOWN
async_fire_mqtt_message(hass, "foobar/sensor3", "New state3")
entity_id = entity_ids[2]
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "New state3"