diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ee14fe432b5..bc59be0d1f3 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -45,7 +45,8 @@ from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow -from . import config_flow, discovery, server # noqa: F401 pylint: disable=unused-import +from . import config_flow # noqa: F401 pylint: disable=unused-import +from . import debug_info, discovery, server from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC, @@ -56,6 +57,7 @@ from .const import ( DEFAULT_QOS, PROTOCOL_311, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash from .models import Message, MessageCallbackType, PublishPayloadType from .subscription import async_subscribe_topics, async_unsubscribe_topics @@ -513,6 +515,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_subscribe) websocket_api.async_register_command(hass, websocket_remove_device) + websocket_api.async_register_command(hass, websocket_mqtt_info) if conf is None: # If we have a config entry, setup is done by that config entry. @@ -1058,6 +1061,7 @@ class MqttAttributes(Entity): attr_tpl.hass = self.hass @callback + @log_messages(self.hass, self.entity_id) def attributes_message_received(msg: Message) -> None: try: payload = msg.payload @@ -1122,6 +1126,7 @@ class MqttAvailability(Entity): """(Re)Subscribe to topics.""" @callback + @log_messages(self.hass, self.entity_id) def availability_message_received(msg: Message) -> None: """Handle a new received MQTT availability message.""" if msg.payload == self._avail_config[CONF_PAYLOAD_AVAILABLE]: @@ -1207,6 +1212,7 @@ class MqttDiscoveryUpdate(Entity): _LOGGER.info( "Got update for entity with hash: %s '%s'", discovery_hash, payload, ) + debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id) if not payload: # Empty payload: Remove component _LOGGER.info("Removing component: %s", self.entity_id) @@ -1219,6 +1225,9 @@ class MqttDiscoveryUpdate(Entity): await self._discovery_update(payload) if discovery_hash: + debug_info.add_entity_discovery_data( + self.hass, self._discovery_data, self.entity_id + ) # Set in case the entity has been removed and is re-added set_discovery_hash(self.hass, discovery_hash) self._remove_signal = async_dispatcher_connect( @@ -1242,6 +1251,7 @@ class MqttDiscoveryUpdate(Entity): def _cleanup_on_remove(self) -> None: """Stop listening to signal and cleanup discovery data.""" if self._discovery_data and not self._removed_from_hass: + debug_info.remove_entity_data(self.hass, self.entity_id) clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH]) self._removed_from_hass = True @@ -1303,6 +1313,18 @@ class MqttEntityDeviceInfo(Entity): return device_info_from_config(self._device_config) +@websocket_api.websocket_command( + {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} +) +@websocket_api.async_response +async def websocket_mqtt_info(hass, connection, msg): + """Get MQTT debug info for device.""" + device_id = msg["device_id"] + mqtt_info = await debug_info.info_for_device(hass, device_id) + + connection.send_result(msg["id"], mqtt_info) + + @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/remove", vol.Required("device_id"): str} ) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 6044ec2af6e..5d1fe2e2505 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -4,6 +4,7 @@ CONF_DISCOVERY = "discovery" DEFAULT_DISCOVERY = False ATTR_DISCOVERY_HASH = "discovery_hash" +ATTR_DISCOVERY_PAYLOAD = "discovery_payload" ATTR_DISCOVERY_TOPIC = "discovery_topic" CONF_STATE_TOPIC = "state_topic" PROTOCOL_311 = "3.1.1" diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py new file mode 100644 index 00000000000..ec4ff1676bb --- /dev/null +++ b/homeassistant/components/mqtt/debug_info.py @@ -0,0 +1,146 @@ +"""Helper to handle a set of topics to subscribe to.""" +from collections import deque +from functools import wraps +import logging +from typing import Any + +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC +from .models import MessageCallbackType + +_LOGGER = logging.getLogger(__name__) + +DATA_MQTT_DEBUG_INFO = "mqtt_debug_info" +STORED_MESSAGES = 10 + + +def log_messages(hass: HomeAssistantType, entity_id: str) -> MessageCallbackType: + """Wrap an MQTT message callback to support message logging.""" + + def _log_message(msg): + """Log message.""" + debug_info = hass.data[DATA_MQTT_DEBUG_INFO] + messages = debug_info["entities"][entity_id]["topics"][msg.topic] + messages.append(msg.payload) + + def _decorator(msg_callback: MessageCallbackType): + @wraps(msg_callback) + def wrapper(msg: Any) -> None: + """Log message.""" + _log_message(msg) + msg_callback(msg) + + setattr(wrapper, "__entity_id", entity_id) + return wrapper + + return _decorator + + +def add_topic(hass, message_callback, topic): + """Prepare debug data for topic.""" + entity_id = getattr(message_callback, "__entity_id", None) + if entity_id: + debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + entity_info = debug_info["entities"].setdefault( + entity_id, {"topics": {}, "discovery_data": {}} + ) + entity_info["topics"][topic] = deque([], STORED_MESSAGES) + + +def remove_topic(hass, message_callback, topic): + """Remove debug data for topic.""" + entity_id = getattr(message_callback, "__entity_id", None) + if entity_id and entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]: + hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["topics"].pop(topic) + + +def add_entity_discovery_data(hass, discovery_data, entity_id): + """Add discovery data.""" + debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + entity_info = debug_info["entities"].setdefault( + entity_id, {"topics": {}, "discovery_data": {}} + ) + entity_info["discovery_data"] = discovery_data + + +def update_entity_discovery_data(hass, discovery_payload, entity_id): + """Update discovery data.""" + entity_info = hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id] + entity_info["discovery_data"][ATTR_DISCOVERY_PAYLOAD] = discovery_payload + + +def remove_entity_data(hass, entity_id): + """Remove discovery data.""" + hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id) + + +def add_trigger_discovery_data(hass, discovery_hash, discovery_data, device_id): + """Add discovery data.""" + debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + debug_info["triggers"][discovery_hash] = { + "device_id": device_id, + "discovery_data": discovery_data, + } + + +def update_trigger_discovery_data(hass, discovery_hash, discovery_payload): + """Update discovery data.""" + trigger_info = hass.data[DATA_MQTT_DEBUG_INFO]["triggers"][discovery_hash] + trigger_info["discovery_data"][ATTR_DISCOVERY_PAYLOAD] = discovery_payload + + +def remove_trigger_discovery_data(hass, discovery_hash): + """Remove discovery data.""" + hass.data[DATA_MQTT_DEBUG_INFO]["triggers"][discovery_hash]["discovery_data"] = None + + +async def info_for_device(hass, device_id): + """Get debug info for a device.""" + mqtt_info = {"entities": [], "triggers": []} + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = hass.helpers.entity_registry.async_entries_for_device( + entity_registry, device_id + ) + mqtt_debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + for entry in entries: + if entry.entity_id not in mqtt_debug_info["entities"]: + continue + + entity_info = mqtt_debug_info["entities"][entry.entity_id] + topics = [ + {"topic": topic, "messages": list(messages)} + for topic, messages in entity_info["topics"].items() + ] + discovery_data = { + "topic": entity_info["discovery_data"].get(ATTR_DISCOVERY_TOPIC, ""), + "payload": entity_info["discovery_data"].get(ATTR_DISCOVERY_PAYLOAD, ""), + } + mqtt_info["entities"].append( + { + "entity_id": entry.entity_id, + "topics": topics, + "discovery_data": discovery_data, + } + ) + + for trigger in mqtt_debug_info["triggers"].values(): + if trigger["device_id"] != device_id: + continue + + discovery_data = { + "topic": trigger["discovery_data"][ATTR_DISCOVERY_TOPIC], + "payload": trigger["discovery_data"][ATTR_DISCOVERY_PAYLOAD], + } + mqtt_info["triggers"].append({"discovery_data": discovery_data}) + + return mqtt_info diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 88c635ae3a8..3b65243d078 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -26,6 +26,7 @@ from . import ( CONF_QOS, DOMAIN, cleanup_device_registry, + debug_info, ) from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash @@ -183,6 +184,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): if not payload: # Empty payload: Remove trigger _LOGGER.info("Removing trigger: %s", discovery_hash) + debug_info.remove_trigger_discovery_data(hass, discovery_hash) if discovery_id in hass.data[DEVICE_TRIGGERS]: device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] device_trigger.detach_trigger() @@ -192,6 +194,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): else: # Non-empty payload: Update trigger _LOGGER.info("Updating trigger: %s", discovery_hash) + debug_info.update_trigger_discovery_data(hass, discovery_hash, payload) config = TRIGGER_DISCOVERY_SCHEMA(payload) await _update_device(hass, config_entry, config) device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] @@ -230,6 +233,9 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger( config, discovery_hash, remove_signal ) + debug_info.add_trigger_discovery_data( + hass, discovery_hash, discovery_data, device.id + ) async def async_device_removed(hass: HomeAssistant, device_id: str): @@ -241,6 +247,7 @@ async def async_device_removed(hass: HomeAssistant, device_id: str): discovery_hash = device_trigger.discovery_data[ATTR_DISCOVERY_HASH] discovery_topic = device_trigger.discovery_data[ATTR_DISCOVERY_TOPIC] + debug_info.remove_trigger_discovery_data(hass, discovery_hash) device_trigger.detach_trigger() clear_discovery_hash(hass, discovery_hash) device_trigger.remove_signal() diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 3bcd8594ebe..689b279c5e7 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -11,7 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS -from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC +from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC _LOGGER = logging.getLogger(__name__) @@ -135,6 +135,7 @@ async def async_start( setattr(payload, "__configuration_source__", f"MQTT (topic: '{topic}')") discovery_data = { ATTR_DISCOVERY_HASH: discovery_hash, + ATTR_DISCOVERY_PAYLOAD: payload, ATTR_DISCOVERY_TOPIC: topic, } setattr(payload, "discovery_data", discovery_data) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 7be923927ca..2704c5ae3a1 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -35,6 +35,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -137,6 +138,7 @@ class MqttSensor( template.hass = self.hass @callback + @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" payload = msg.payload diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index be48a769a23..b4793a49dca 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -8,6 +8,7 @@ from homeassistant.components import mqtt from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass +from . import debug_info from .const import DEFAULT_QOS from .models import MessageCallbackType @@ -18,6 +19,7 @@ _LOGGER = logging.getLogger(__name__) class EntitySubscription: """Class to hold data about an active entity topic subscription.""" + hass = attr.ib(type=HomeAssistantType) topic = attr.ib(type=str) message_callback = attr.ib(type=MessageCallbackType) unsubscribe_callback = attr.ib(type=Optional[Callable[[], None]]) @@ -31,11 +33,16 @@ class EntitySubscription: if other is not None and other.unsubscribe_callback is not None: other.unsubscribe_callback() + # Clear debug data if it exists + debug_info.remove_topic(self.hass, other.message_callback, other.topic) if self.topic is None: # We were asked to remove the subscription or not to create it return + # Prepare debug data + debug_info.add_topic(self.hass, self.message_callback, self.topic) + self.unsubscribe_callback = await mqtt.async_subscribe( hass, self.topic, self.message_callback, self.qos, self.encoding ) @@ -77,6 +84,7 @@ async def async_subscribe_topics( unsubscribe_callback=None, qos=value.get("qos", DEFAULT_QOS), encoding=value.get("encoding", "utf-8"), + hass=hass, ) # Get the current subscription state current = current_subscriptions.pop(key, None) @@ -87,6 +95,8 @@ async def async_subscribe_topics( for remaining in current_subscriptions.values(): if remaining.unsubscribe_callback is not None: remaining.unsubscribe_callback() + # Clear debug data if it exists + debug_info.remove_topic(hass, remaining.message_callback, remaining.topic) return new_state diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 2f437174299..0d1b892611d 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -4,6 +4,7 @@ import json from unittest.mock import ANY from homeassistant.components import mqtt +from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE @@ -519,3 +520,232 @@ async def help_test_entity_id_update_discovery_update( async_fire_mqtt_message(hass, f"{topic}_2", "online") state = hass.states.get(f"{domain}.milk") assert state.state != STATE_UNAVAILABLE + + +async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + + +async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, config): + """Test debug_info message overflow. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + + for i in range(0, debug_info.STORED_MESSAGES + 1): + async_fire_mqtt_message(hass, "test-topic", f"{i}") + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert ( + len(debug_info_data["entities"][0]["topics"][0]["messages"]) + == debug_info.STORED_MESSAGES + ) + messages = [f"{i}" for i in range(1, debug_info.STORED_MESSAGES + 1)] + assert {"topic": "test-topic", "messages": messages} in debug_info_data["entities"][ + 0 + ]["topics"] + + +async def help_test_entity_debug_info_message( + hass, mqtt_mock, domain, config, topic=None, payload=None +): + """Test debug_info message overflow. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + if topic is None: + # Add default topic to config + config["state_topic"] = "state-topic" + topic = "state-topic" + + if payload is None: + payload = "ON" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) >= 1 + assert {"topic": topic, "messages": []} in debug_info_data["entities"][0]["topics"] + + async_fire_mqtt_message(hass, topic, payload) + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) >= 1 + assert {"topic": topic, "messages": [payload]} in debug_info_data["entities"][0][ + "topics" + ] + + +async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.test" + entity_id = debug_info_data["entities"][0]["entity_id"] + + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", "") + await hass.async_block_till_done() + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 0 + assert len(debug_info_data["triggers"]) == 0 + assert entity_id not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] + + +async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, config): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + dev_registry = await hass.helpers.device_registry.async_get_registry() + ent_registry = mock_registry(hass, {}) + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.test" + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + + ent_registry.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk") + await hass.async_block_till_done() + await hass.async_block_till_done() + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.milk" + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + assert ( + f"{domain}.test" not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] + ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 6766002717d..7274badbed9 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -4,7 +4,7 @@ import json import pytest import homeassistant.components.automation as automation -from homeassistant.components.mqtt import DOMAIN +from homeassistant.components.mqtt import DOMAIN, debug_info from homeassistant.components.mqtt.device_trigger import async_attach_trigger from homeassistant.components.mqtt.discovery import async_start from homeassistant.setup import async_setup_component @@ -1104,3 +1104,44 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo # Verify device registry entry is cleared device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) assert device_entry is None + + +async def test_trigger_debug_info(hass, mqtt_mock): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + "platform": "mqtt", + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo", + "subtype": "bar", + "device": { + "connections": [["mac", "02:5b:26:a8:dc:12"]], + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", + }, + } + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device(set(), {("mac", "02:5b:26:a8:dc:12")}) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 0 + assert len(debug_info_data["triggers"]) == 1 + assert ( + debug_info_data["triggers"][0]["discovery_data"]["topic"] + == "homeassistant/device_automation/bla/config" + ) + assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 7d06c62b915..7aa185c2c39 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,5 +1,6 @@ """The tests for the MQTT component.""" from datetime import timedelta +import json import ssl import unittest from unittest import mock @@ -934,3 +935,48 @@ async def test_mqtt_ws_remove_non_mqtt_device( response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND + + +async def test_mqtt_ws_get_device_debug_info( + hass, device_reg, hass_ws_client, mqtt_mock +): + """Test MQTT websocket device debug info.""" + config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, config_entry) + + config = { + "device": {"identifiers": ["0AFFD2"]}, + "platform": "mqtt", + "state_topic": "foobar/sensor", + "unique_id": "unique", + } + data = json.dumps(config) + + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + # Verify device entry is created + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_entry is not None + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 5, "type": "mqtt/device/debug_info", "device_id": device_entry.id} + ) + response = await client.receive_json() + assert response["success"] + expected_result = { + "entities": [ + { + "entity_id": "sensor.mqtt_sensor", + "topics": [{"topic": "foobar/sensor", "messages": []}], + "discovery_data": { + "payload": config, + "topic": "homeassistant/sensor/bla/config", + }, + } + ], + "triggers": [], + } + assert response["result"] == expected_result diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 061e53250cb..34d3c33f8d7 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -19,6 +19,11 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info, + help_test_entity_debug_info_max_messages, + help_test_entity_debug_info_message, + help_test_entity_debug_info_remove, + help_test_entity_debug_info_update_entity_id, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -437,3 +442,36 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock): device = registry.async_get_device({("mqtt", "helloworld")}, set()) assert device is not None assert device.via_device_id == hub.id + + +async def test_entity_debug_info(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + + +async def test_entity_debug_info_max_messages(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info_max_messages( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_remove(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info_remove( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_update_entity_id(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info_update_entity_id( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + )