Add MQTT debug info (#33461)

* Add MQTT debug info

* Tweaks

* Tweaks
This commit is contained in:
Erik Montnemery 2020-04-01 19:00:40 +02:00 committed by GitHub
parent eff9b2a1a0
commit fb93b79b12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 547 additions and 3 deletions

View file

@ -45,7 +45,8 @@ from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.util.logging import catch_log_exception from homeassistant.util.logging import catch_log_exception
# Loading the config flow file will register the flow # 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 ( from .const import (
ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_HASH,
ATTR_DISCOVERY_TOPIC, ATTR_DISCOVERY_TOPIC,
@ -56,6 +57,7 @@ from .const import (
DEFAULT_QOS, DEFAULT_QOS,
PROTOCOL_311, PROTOCOL_311,
) )
from .debug_info import log_messages
from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash
from .models import Message, MessageCallbackType, PublishPayloadType from .models import Message, MessageCallbackType, PublishPayloadType
from .subscription import async_subscribe_topics, async_unsubscribe_topics 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_subscribe)
websocket_api.async_register_command(hass, websocket_remove_device) websocket_api.async_register_command(hass, websocket_remove_device)
websocket_api.async_register_command(hass, websocket_mqtt_info)
if conf is None: if conf is None:
# If we have a config entry, setup is done by that config entry. # 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 attr_tpl.hass = self.hass
@callback @callback
@log_messages(self.hass, self.entity_id)
def attributes_message_received(msg: Message) -> None: def attributes_message_received(msg: Message) -> None:
try: try:
payload = msg.payload payload = msg.payload
@ -1122,6 +1126,7 @@ class MqttAvailability(Entity):
"""(Re)Subscribe to topics.""" """(Re)Subscribe to topics."""
@callback @callback
@log_messages(self.hass, self.entity_id)
def availability_message_received(msg: Message) -> None: def availability_message_received(msg: Message) -> None:
"""Handle a new received MQTT availability message.""" """Handle a new received MQTT availability message."""
if msg.payload == self._avail_config[CONF_PAYLOAD_AVAILABLE]: if msg.payload == self._avail_config[CONF_PAYLOAD_AVAILABLE]:
@ -1207,6 +1212,7 @@ class MqttDiscoveryUpdate(Entity):
_LOGGER.info( _LOGGER.info(
"Got update for entity with hash: %s '%s'", discovery_hash, payload, "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: if not payload:
# Empty payload: Remove component # Empty payload: Remove component
_LOGGER.info("Removing component: %s", self.entity_id) _LOGGER.info("Removing component: %s", self.entity_id)
@ -1219,6 +1225,9 @@ class MqttDiscoveryUpdate(Entity):
await self._discovery_update(payload) await self._discovery_update(payload)
if discovery_hash: 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 in case the entity has been removed and is re-added
set_discovery_hash(self.hass, discovery_hash) set_discovery_hash(self.hass, discovery_hash)
self._remove_signal = async_dispatcher_connect( self._remove_signal = async_dispatcher_connect(
@ -1242,6 +1251,7 @@ class MqttDiscoveryUpdate(Entity):
def _cleanup_on_remove(self) -> None: def _cleanup_on_remove(self) -> None:
"""Stop listening to signal and cleanup discovery data.""" """Stop listening to signal and cleanup discovery data."""
if self._discovery_data and not self._removed_from_hass: 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]) clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH])
self._removed_from_hass = True self._removed_from_hass = True
@ -1303,6 +1313,18 @@ class MqttEntityDeviceInfo(Entity):
return device_info_from_config(self._device_config) 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( @websocket_api.websocket_command(
{vol.Required("type"): "mqtt/device/remove", vol.Required("device_id"): str} {vol.Required("type"): "mqtt/device/remove", vol.Required("device_id"): str}
) )

View file

@ -4,6 +4,7 @@ CONF_DISCOVERY = "discovery"
DEFAULT_DISCOVERY = False DEFAULT_DISCOVERY = False
ATTR_DISCOVERY_HASH = "discovery_hash" ATTR_DISCOVERY_HASH = "discovery_hash"
ATTR_DISCOVERY_PAYLOAD = "discovery_payload"
ATTR_DISCOVERY_TOPIC = "discovery_topic" ATTR_DISCOVERY_TOPIC = "discovery_topic"
CONF_STATE_TOPIC = "state_topic" CONF_STATE_TOPIC = "state_topic"
PROTOCOL_311 = "3.1.1" PROTOCOL_311 = "3.1.1"

View file

@ -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

View file

@ -26,6 +26,7 @@ from . import (
CONF_QOS, CONF_QOS,
DOMAIN, DOMAIN,
cleanup_device_registry, cleanup_device_registry,
debug_info,
) )
from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash 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: if not payload:
# Empty payload: Remove trigger # Empty payload: Remove trigger
_LOGGER.info("Removing trigger: %s", discovery_hash) _LOGGER.info("Removing trigger: %s", discovery_hash)
debug_info.remove_trigger_discovery_data(hass, discovery_hash)
if discovery_id in hass.data[DEVICE_TRIGGERS]: if discovery_id in hass.data[DEVICE_TRIGGERS]:
device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id]
device_trigger.detach_trigger() device_trigger.detach_trigger()
@ -192,6 +194,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data):
else: else:
# Non-empty payload: Update trigger # Non-empty payload: Update trigger
_LOGGER.info("Updating trigger: %s", discovery_hash) _LOGGER.info("Updating trigger: %s", discovery_hash)
debug_info.update_trigger_discovery_data(hass, discovery_hash, payload)
config = TRIGGER_DISCOVERY_SCHEMA(payload) config = TRIGGER_DISCOVERY_SCHEMA(payload)
await _update_device(hass, config_entry, config) await _update_device(hass, config_entry, config)
device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] 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( await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger(
config, discovery_hash, remove_signal 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): 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_hash = device_trigger.discovery_data[ATTR_DISCOVERY_HASH]
discovery_topic = device_trigger.discovery_data[ATTR_DISCOVERY_TOPIC] discovery_topic = device_trigger.discovery_data[ATTR_DISCOVERY_TOPIC]
debug_info.remove_trigger_discovery_data(hass, discovery_hash)
device_trigger.detach_trigger() device_trigger.detach_trigger()
clear_discovery_hash(hass, discovery_hash) clear_discovery_hash(hass, discovery_hash)
device_trigger.remove_signal() device_trigger.remove_signal()

View file

@ -11,7 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS 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__) _LOGGER = logging.getLogger(__name__)
@ -135,6 +135,7 @@ async def async_start(
setattr(payload, "__configuration_source__", f"MQTT (topic: '{topic}')") setattr(payload, "__configuration_source__", f"MQTT (topic: '{topic}')")
discovery_data = { discovery_data = {
ATTR_DISCOVERY_HASH: discovery_hash, ATTR_DISCOVERY_HASH: discovery_hash,
ATTR_DISCOVERY_PAYLOAD: payload,
ATTR_DISCOVERY_TOPIC: topic, ATTR_DISCOVERY_TOPIC: topic,
} }
setattr(payload, "discovery_data", discovery_data) setattr(payload, "discovery_data", discovery_data)

View file

@ -35,6 +35,7 @@ from . import (
MqttEntityDeviceInfo, MqttEntityDeviceInfo,
subscription, subscription,
) )
from .debug_info import log_messages
from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -137,6 +138,7 @@ class MqttSensor(
template.hass = self.hass template.hass = self.hass
@callback @callback
@log_messages(self.hass, self.entity_id)
def message_received(msg): def message_received(msg):
"""Handle new MQTT messages.""" """Handle new MQTT messages."""
payload = msg.payload payload = msg.payload

View file

@ -8,6 +8,7 @@ from homeassistant.components import mqtt
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from . import debug_info
from .const import DEFAULT_QOS from .const import DEFAULT_QOS
from .models import MessageCallbackType from .models import MessageCallbackType
@ -18,6 +19,7 @@ _LOGGER = logging.getLogger(__name__)
class EntitySubscription: class EntitySubscription:
"""Class to hold data about an active entity topic subscription.""" """Class to hold data about an active entity topic subscription."""
hass = attr.ib(type=HomeAssistantType)
topic = attr.ib(type=str) topic = attr.ib(type=str)
message_callback = attr.ib(type=MessageCallbackType) message_callback = attr.ib(type=MessageCallbackType)
unsubscribe_callback = attr.ib(type=Optional[Callable[[], None]]) 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: if other is not None and other.unsubscribe_callback is not None:
other.unsubscribe_callback() 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: if self.topic is None:
# We were asked to remove the subscription or not to create it # We were asked to remove the subscription or not to create it
return return
# Prepare debug data
debug_info.add_topic(self.hass, self.message_callback, self.topic)
self.unsubscribe_callback = await mqtt.async_subscribe( self.unsubscribe_callback = await mqtt.async_subscribe(
hass, self.topic, self.message_callback, self.qos, self.encoding hass, self.topic, self.message_callback, self.qos, self.encoding
) )
@ -77,6 +84,7 @@ async def async_subscribe_topics(
unsubscribe_callback=None, unsubscribe_callback=None,
qos=value.get("qos", DEFAULT_QOS), qos=value.get("qos", DEFAULT_QOS),
encoding=value.get("encoding", "utf-8"), encoding=value.get("encoding", "utf-8"),
hass=hass,
) )
# Get the current subscription state # Get the current subscription state
current = current_subscriptions.pop(key, None) current = current_subscriptions.pop(key, None)
@ -87,6 +95,8 @@ async def async_subscribe_topics(
for remaining in current_subscriptions.values(): for remaining in current_subscriptions.values():
if remaining.unsubscribe_callback is not None: if remaining.unsubscribe_callback is not None:
remaining.unsubscribe_callback() remaining.unsubscribe_callback()
# Clear debug data if it exists
debug_info.remove_topic(hass, remaining.message_callback, remaining.topic)
return new_state return new_state

View file

@ -4,6 +4,7 @@ import json
from unittest.mock import ANY from unittest.mock import ANY
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.components.mqtt import debug_info
from homeassistant.components.mqtt.discovery import async_start from homeassistant.components.mqtt.discovery import async_start
from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE 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") async_fire_mqtt_message(hass, f"{topic}_2", "online")
state = hass.states.get(f"{domain}.milk") state = hass.states.get(f"{domain}.milk")
assert state.state != STATE_UNAVAILABLE 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"]
)

View file

@ -4,7 +4,7 @@ import json
import pytest import pytest
import homeassistant.components.automation as automation 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.device_trigger import async_attach_trigger
from homeassistant.components.mqtt.discovery import async_start from homeassistant.components.mqtt.discovery import async_start
from homeassistant.setup import async_setup_component 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 # Verify device registry entry is cleared
device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set())
assert device_entry is None 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

View file

@ -1,5 +1,6 @@
"""The tests for the MQTT component.""" """The tests for the MQTT component."""
from datetime import timedelta from datetime import timedelta
import json
import ssl import ssl
import unittest import unittest
from unittest import mock from unittest import mock
@ -934,3 +935,48 @@ async def test_mqtt_ws_remove_non_mqtt_device(
response = await client.receive_json() response = await client.receive_json()
assert not response["success"] assert not response["success"]
assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND 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

View file

@ -19,6 +19,11 @@ from .test_common import (
help_test_discovery_removal, help_test_discovery_removal,
help_test_discovery_update, help_test_discovery_update,
help_test_discovery_update_attr, 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_remove,
help_test_entity_device_info_update, help_test_entity_device_info_update,
help_test_entity_device_info_with_connection, 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()) device = registry.async_get_device({("mqtt", "helloworld")}, set())
assert device is not None assert device is not None
assert device.via_device_id == hub.id 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
)