core/tests/components/mqtt/test_discovery.py

1282 lines
44 KiB
Python

"""The tests for the MQTT discovery."""
import json
from pathlib import Path
import re
from unittest.mock import AsyncMock, call, patch
import pytest
from homeassistant import config_entries
from homeassistant.components import mqtt
from homeassistant.components.mqtt.abbreviations import (
ABBREVIATIONS,
DEVICE_ABBREVIATIONS,
)
from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start
from homeassistant.const import (
EVENT_STATE_CHANGED,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
import homeassistant.core as ha
from homeassistant.setup import async_setup_component
from tests.common import (
MockConfigEntry,
async_fire_mqtt_message,
mock_device_registry,
mock_entity_platform,
mock_registry,
)
@pytest.fixture
def device_reg(hass):
"""Return an empty, loaded, registry."""
return mock_device_registry(hass)
@pytest.fixture
def entity_reg(hass):
"""Return an empty, loaded, registry."""
return mock_registry(hass)
@pytest.mark.parametrize(
"mqtt_config",
[{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}],
)
async def test_subscribing_config_topic(hass, mqtt_mock):
"""Test setting up discovery."""
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),
],
)
async def test_invalid_topic(hass, mqtt_mock, caplog, topic, log):
"""Test sending to invalid topic."""
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()
async def test_invalid_json(hass, mqtt_mock, caplog):
"""Test sending in invalid JSON."""
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, "homeassistant/binary_sensor/bla/config", "not json"
)
await hass.async_block_till_done()
assert "Unable to parse JSON" in caplog.text
assert not mock_dispatcher_send.called
async def test_only_valid_components(hass, mqtt_mock, caplog):
"""Test for a valid component."""
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, mqtt_mock, caplog):
"""Test sending in correct JSON."""
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[ALREADY_DISCOVERED]
async def test_discover_fan(hass, mqtt_mock, caplog):
"""Test discovering an MQTT fan."""
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[ALREADY_DISCOVERED]
async def test_discover_climate(hass, mqtt_mock, caplog):
"""Test discovering an MQTT climate component."""
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[ALREADY_DISCOVERED]
async def test_discover_alarm_control_panel(hass, mqtt_mock, caplog):
"""Test discovering an MQTT alarm control panel component."""
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[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/vacuum/object/bla/config",
'{ "name": "Hello World 17", "obj_id": "hello_id", "state_topic": "test-topic", "schema": "legacy" }',
"vacuum.hello_id",
"Hello World 17",
"vacuum",
),
(
"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, mqtt_mock, caplog, topic, config, entity_id, name, domain
):
"""Test discovering an MQTT entity with object_id."""
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[ALREADY_DISCOVERED]
async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog):
"""Test sending in correct JSON with optional node_id included."""
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[ALREADY_DISCOVERED]
async def test_non_duplicate_discovery(hass, mqtt_mock, caplog):
"""Test for a non duplicate component."""
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, mqtt_mock, caplog):
"""Test removal of component through empty discovery message."""
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, mqtt_mock, caplog):
"""Test rediscover of removed component."""
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, mqtt_mock, caplog):
"""Test immediate rediscover of removed component."""
events = []
@ha.callback
def callback(event):
"""Verify event got called."""
events.append(event)
hass.bus.async_listen(EVENT_STATE_CHANGED, callback)
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, mqtt_mock, caplog):
"""Test immediate rediscover of removed component."""
events = []
@ha.callback
def callback(event):
"""Verify event got called."""
events.append(event)
hass.bus.async_listen(EVENT_STATE_CHANGED, 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, mqtt_mock, caplog):
"""Test immediate reconfigure of added component."""
events = []
@ha.callback
def callback(event):
"""Verify event got called."""
events.append(event)
hass.bus.async_listen(EVENT_STATE_CHANGED, 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, mqtt_mock, caplog):
"""Test for a non duplicate component."""
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
async def test_cleanup_device(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock):
"""Test discvered device is cleaned up when entry removed from device."""
assert await async_setup_component(hass, "config", {})
ws_client = await hass_ws_client(hass)
data = (
'{ "device":{"identifiers":["0AFFD2"]},'
' "state_topic": "foobar/sensor",'
' "unique_id": "unique" }'
)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
await hass.async_block_till_done()
# Verify device and registry entries are created
device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")})
assert device_entry is not None
entity_entry = entity_reg.async_get("sensor.mqtt_sensor")
assert entity_entry is not None
state = hass.states.get("sensor.mqtt_sensor")
assert state is not None
# Remove MQTT from the device
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
await ws_client.send_json(
{
"id": 6,
"type": "config/device_registry/remove_config_entry",
"config_entry_id": mqtt_config_entry.entry_id,
"device_id": device_entry.id,
}
)
response = await ws_client.receive_json()
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_reg.async_get_device({("mqtt", "0AFFD2")})
assert device_entry is None
entity_entry = entity_reg.async_get("sensor.mqtt_sensor")
assert entity_entry is None
# Verify state is removed
state = hass.states.get("sensor.mqtt_sensor")
assert state is None
await hass.async_block_till_done()
# Verify retained discovery topic has been cleared
mqtt_mock.async_publish.assert_called_once_with(
"homeassistant/sensor/bla/config", "", 0, True
)
async def test_cleanup_device_mqtt(hass, device_reg, entity_reg, mqtt_mock):
"""Test discvered device is cleaned up when removed through MQTT."""
data = (
'{ "device":{"identifiers":["0AFFD2"]},'
' "state_topic": "foobar/sensor",'
' "unique_id": "unique" }'
)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
await hass.async_block_till_done()
# Verify device and registry entries are created
device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")})
assert device_entry is not None
entity_entry = entity_reg.async_get("sensor.mqtt_sensor")
assert entity_entry is not None
state = hass.states.get("sensor.mqtt_sensor")
assert state is not None
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", "")
await hass.async_block_till_done()
await hass.async_block_till_done()
# Verify device and registry entries are cleared
device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")})
assert device_entry is None
entity_entry = entity_reg.async_get("sensor.mqtt_sensor")
assert entity_entry is None
# Verify state is removed
state = hass.states.get("sensor.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_cleanup_device_multiple_config_entries(
hass, hass_ws_client, device_reg, entity_reg, mqtt_mock
):
"""Test discovered device is cleaned up when entry removed from device."""
assert await async_setup_component(hass, "config", {})
ws_client = await hass_ws_client(hass)
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.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_reg.async_get_device(set(), {("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_reg.async_get("sensor.mqtt_sensor")
assert entity_entry is not None
state = hass.states.get("sensor.mqtt_sensor")
assert state is not None
# Remove MQTT from the device
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
await ws_client.send_json(
{
"id": 6,
"type": "config/device_registry/remove_config_entry",
"config_entry_id": mqtt_config_entry.entry_id,
"device_id": device_entry.id,
}
)
response = await ws_client.receive_json()
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_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")})
assert device_entry is not None
entity_entry = entity_reg.async_get("sensor.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.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", "", 0, True),
call("homeassistant/tag/bla/config", "", 0, True),
call("homeassistant/device_automation/bla/config", "", 0, True),
],
any_order=True,
)
async def test_cleanup_device_multiple_config_entries_mqtt(
hass, device_reg, entity_reg, mqtt_mock
):
"""Test discovered device is cleaned up when removed through MQTT."""
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.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_reg.async_get_device(set(), {("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_reg.async_get("sensor.mqtt_sensor")
assert entity_entry is not None
state = hass.states.get("sensor.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_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")})
assert device_entry is not None
entity_entry = entity_reg.async_get("sensor.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.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, mqtt_mock, caplog):
"""Test expansion of abbreviated discovery payload."""
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"'
" },"
" {"
' "topic":"avail_item2/~",'
' "payload_available": "available",'
' "payload_not_available": "not_available"'
" }"
" ],"
' "dev":{'
' "ids":["5706DF"],'
' "name":"DiscoveryExpansionTest1 Device",'
' "mdl":"Generic",'
' "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.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[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.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.state == STATE_UNAVAILABLE
async def test_discovery_expansion_2(hass, mqtt_mock, caplog):
"""Test expansion of abbreviated discovery payload."""
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"'
" },"
' "dev":{'
' "ids":["5706DF"],'
' "name":"DiscoveryExpansionTest1 Device",'
' "mdl":"Generic",'
' "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.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[ALREADY_DISCOVERED]
assert state.state == STATE_UNKNOWN
@pytest.mark.no_fail_on_log_exception
async def test_discovery_expansion_3(hass, mqtt_mock, caplog):
"""Test expansion of broken discovery payload."""
data = (
'{ "~": "some/base/topic",'
' "name": "DiscoveryExpansionTest1",'
' "stat_t": "test_topic/~",'
' "cmd_t": "~/test_topic",'
' "availability": "incorrect",'
' "dev":{'
' "ids":["5706DF"],'
' "name":"DiscoveryExpansionTest1 Device",'
' "mdl":"Generic",'
' "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 (
"voluptuous.error.MultipleInvalid: expected a dictionary @ data['availability'][0]"
in caplog.text
)
async def test_discovery_expansion_without_encoding_and_value_template_1(
hass, mqtt_mock, caplog
):
"""Test expansion of raw availability payload with a template as list."""
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",'
' "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.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[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.state == STATE_UNAVAILABLE
async def test_discovery_expansion_without_encoding_and_value_template_2(
hass, mqtt_mock, caplog
):
"""Test expansion of raw availability payload with a template directly."""
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",'
' "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.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[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.state == STATE_UNAVAILABLE
ABBREVIATIONS_WHITE_LIST = [
# MQTT client/server/trigger settings
"CONF_BIRTH_MESSAGE",
"CONF_BROKER",
"CONF_CERTIFICATE",
"CONF_CLIENT_CERT",
"CONF_CLIENT_ID",
"CONF_CLIENT_KEY",
"CONF_DISCOVERY",
"CONF_DISCOVERY_ID",
"CONF_DISCOVERY_PREFIX",
"CONF_EMBEDDED",
"CONF_KEEPALIVE",
"CONF_TLS_INSECURE",
"CONF_TLS_VERSION",
"CONF_WILL_MESSAGE",
# 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",
]
async def test_missing_discover_abbreviations(hass, mqtt_mock, caplog):
"""Check MQTT platforms for missing abbreviations."""
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 == "trigger.py":
continue
with open(fil) as file:
matches = re.findall(regex, file.read())
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
):
missing.append(
"{}: no abbreviation for {} ({})".format(
fil, match[1], match[0]
)
)
assert not missing
async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog):
"""Test no implicit state topic for switch."""
data = '{ "name": "Test1",' ' "command_topic": "cmnd"' "}"
async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data)
await hass.async_block_till_done()
assert "implicit state_topic is deprecated" not in caplog.text
state = hass.states.get("switch.Test1")
assert state is not None
assert state.name == "Test1"
assert ("switch", "bla") in hass.data[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.state == STATE_UNKNOWN
@pytest.mark.parametrize(
"mqtt_config",
[
{
mqtt.CONF_BROKER: "mock-broker",
mqtt.CONF_DISCOVERY_PREFIX: "my_home/homeassistant/register",
}
],
)
async def test_complex_discovery_topic_prefix(hass, mqtt_mock, caplog):
"""Tests handling of discovery topic prefix with multiple slashes."""
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[ALREADY_DISCOVERED]
async def test_mqtt_integration_discovery_subscribe_unsubscribe(
hass, mqtt_client_mock, mqtt_mock
):
"""Check MQTT integration discovery subscribe and unsubscribe."""
mock_entity_platform(hass, "config_flow.comp", 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()
mqtt_client_mock.subscribe.assert_any_call("comp/discovery/#", 0)
assert not mqtt_client_mock.unsubscribe.called
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
async def async_step_mqtt(self, discovery_info):
"""Test mqtt step."""
return self.async_abort(reason="already_configured")
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
mqtt_client_mock.subscribe.assert_any_call("comp/discovery/#", 0)
assert not mqtt_client_mock.unsubscribe.called
async_fire_mqtt_message(hass, "comp/discovery/bla/config", "")
await hass.async_block_till_done()
mqtt_client_mock.unsubscribe.assert_called_once_with("comp/discovery/#")
mqtt_client_mock.unsubscribe.reset_mock()
async_fire_mqtt_message(hass, "comp/discovery/bla/config", "")
await hass.async_block_till_done()
assert not mqtt_client_mock.unsubscribe.called
async def test_mqtt_discovery_unsubscribe_once(hass, mqtt_client_mock, mqtt_mock):
"""Check MQTT integration discovery unsubscribe once."""
mock_entity_platform(hass, "config_flow.comp", 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()
mqtt_client_mock.subscribe.assert_any_call("comp/discovery/#", 0)
assert not mqtt_client_mock.unsubscribe.called
class TestFlow(config_entries.ConfigFlow):
"""Test flow."""
async def async_step_mqtt(self, discovery_info):
"""Test mqtt step."""
return self.async_abort(reason="already_configured")
with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
async_fire_mqtt_message(hass, "comp/discovery/bla/config", "")
async_fire_mqtt_message(hass, "comp/discovery/bla/config", "")
await hass.async_block_till_done()
await hass.async_block_till_done()
mqtt_client_mock.unsubscribe.assert_called_once_with("comp/discovery/#")