Add origin info support for MQTT discovered items (#98782)

* Add integration info support for MQTT discovery.

* Moving logs to discovery

* Revert adding class property

* Rename to origin

* Follow up comments
This commit is contained in:
Jan Bouwhuis 2023-08-24 09:50:39 +02:00 committed by GitHub
parent fe164d06a7
commit 8b232047c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 185 additions and 20 deletions

View File

@ -111,6 +111,7 @@ ABBREVIATIONS = {
"mode_stat_tpl": "mode_state_template",
"modes": "modes",
"name": "name",
"o": "origin",
"obj_id": "object_id",
"off_dly": "off_delay",
"on_cmd_type": "on_command_type",
@ -275,3 +276,9 @@ DEVICE_ABBREVIATIONS = {
"sw": "sw_version",
"sa": "suggested_area",
}
ORIGIN_ABBREVIATIONS = {
"name": "name",
"sw": "sw_version",
"url": "support_url",
}

View File

@ -17,6 +17,7 @@ CONF_COMMAND_TOPIC = "command_topic"
CONF_DISCOVERY_PREFIX = "discovery_prefix"
CONF_ENCODING = "encoding"
CONF_KEEPALIVE = "keepalive"
CONF_ORIGIN = "origin"
CONF_QOS = ATTR_QOS
CONF_RETAIN = ATTR_RETAIN
CONF_SCHEMA = "schema"
@ -57,6 +58,19 @@ CONF_CLIENT_KEY = "client_key"
CONF_CLIENT_CERT = "client_cert"
CONF_TLS_INSECURE = "tls_insecure"
# Device and integration info options
CONF_IDENTIFIERS = "identifiers"
CONF_CONNECTIONS = "connections"
CONF_MANUFACTURER = "manufacturer"
CONF_HW_VERSION = "hw_version"
CONF_SW_VERSION = "sw_version"
CONF_VIA_DEVICE = "via_device"
CONF_DEPRECATED_VIA_HUB = "via_hub"
CONF_SUGGESTED_AREA = "suggested_area"
CONF_CONFIGURATION_URL = "configuration_url"
CONF_OBJECT_ID = "object_id"
CONF_SUPPORT_URL = "support_url"
DATA_MQTT = "mqtt"
DATA_MQTT_AVAILABLE = "mqtt_client_available"

View File

@ -9,8 +9,10 @@ import re
import time
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_PLATFORM
from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_PLATFORM
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResultType
import homeassistant.helpers.config_validation as cv
@ -24,16 +26,19 @@ from homeassistant.loader import async_get_mqtt
from homeassistant.util.json import json_loads_object
from .. import mqtt
from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS
from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS, ORIGIN_ABBREVIATIONS
from .const import (
ATTR_DISCOVERY_HASH,
ATTR_DISCOVERY_PAYLOAD,
ATTR_DISCOVERY_TOPIC,
CONF_AVAILABILITY,
CONF_ORIGIN,
CONF_SUPPORT_URL,
CONF_SW_VERSION,
CONF_TOPIC,
DOMAIN,
)
from .models import ReceiveMessage
from .models import MqttOriginInfo, ReceiveMessage
from .util import get_mqtt_data
_LOGGER = logging.getLogger(__name__)
@ -77,6 +82,16 @@ MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}"
TOPIC_BASE = "~"
MQTT_ORIGIN_INFO_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_SW_VERSION): cv.string,
vol.Optional(CONF_SUPPORT_URL): cv.configuration_url,
}
),
)
class MQTTDiscoveryPayload(dict[str, Any]):
"""Class to hold and MQTT discovery payload and discovery data."""
@ -94,6 +109,30 @@ def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) ->
get_mqtt_data(hass).discovery_already_discovered.add(discovery_hash)
@callback
def async_log_discovery_origin_info(
message: str, discovery_payload: MQTTDiscoveryPayload
) -> None:
"""Log information about the discovery and origin."""
if CONF_ORIGIN not in discovery_payload:
_LOGGER.info(message)
return
origin_info: MqttOriginInfo = discovery_payload[CONF_ORIGIN]
sw_version_log = ""
if sw_version := origin_info.get("sw_version"):
sw_version_log = f", version: {sw_version}"
support_url_log = ""
if support_url := origin_info.get("support_url"):
support_url_log = f", support URL: {support_url}"
_LOGGER.info(
"%s from external application %s%s%s",
message,
origin_info["name"],
sw_version_log,
support_url_log,
)
async def async_start( # noqa: C901
hass: HomeAssistant, discovery_topic: str, config_entry: ConfigEntry
) -> None:
@ -149,6 +188,22 @@ async def async_start( # noqa: C901
key = DEVICE_ABBREVIATIONS.get(key, key)
device[key] = device.pop(abbreviated_key)
if CONF_ORIGIN in discovery_payload:
origin_info: dict[str, Any] = discovery_payload[CONF_ORIGIN]
try:
for key in list(origin_info):
abbreviated_key = key
key = ORIGIN_ABBREVIATIONS.get(key, key)
origin_info[key] = origin_info.pop(abbreviated_key)
MQTT_ORIGIN_INFO_SCHEMA(discovery_payload[CONF_ORIGIN])
except Exception: # pylint: disable=broad-except
_LOGGER.warning(
"Unable to parse origin information "
"from discovery message, got %s",
discovery_payload[CONF_ORIGIN],
)
return
if CONF_AVAILABILITY in discovery_payload:
for availability_conf in cv.ensure_list(
discovery_payload[CONF_AVAILABILITY]
@ -246,17 +301,15 @@ async def async_start( # noqa: C901
if discovery_hash in mqtt_data.discovery_already_discovered:
# Dispatch update
_LOGGER.info(
"Component has already been discovered: %s %s, sending update",
component,
discovery_id,
)
message = f"Component has already been discovered: {component} {discovery_id}, sending update"
async_log_discovery_origin_info(message, payload)
async_dispatcher_send(
hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), payload
)
elif payload:
# Add component
_LOGGER.info("Found new component: %s %s", component, discovery_id)
message = f"Found new component: {component} {discovery_id}"
async_log_discovery_origin_info(message, payload)
mqtt_data.discovery_already_discovered.add(discovery_hash)
async_dispatcher_send(
hass, MQTT_DISCOVERY_NEW.format(component, "mqtt"), payload

View File

@ -69,9 +69,20 @@ from .const import (
ATTR_DISCOVERY_PAYLOAD,
ATTR_DISCOVERY_TOPIC,
CONF_AVAILABILITY,
CONF_CONFIGURATION_URL,
CONF_CONNECTIONS,
CONF_DEPRECATED_VIA_HUB,
CONF_ENCODING,
CONF_HW_VERSION,
CONF_IDENTIFIERS,
CONF_MANUFACTURER,
CONF_OBJECT_ID,
CONF_ORIGIN,
CONF_QOS,
CONF_SUGGESTED_AREA,
CONF_SW_VERSION,
CONF_TOPIC,
CONF_VIA_DEVICE,
DEFAULT_ENCODING,
DEFAULT_PAYLOAD_AVAILABLE,
DEFAULT_PAYLOAD_NOT_AVAILABLE,
@ -84,6 +95,7 @@ from .discovery import (
MQTT_DISCOVERY_DONE,
MQTT_DISCOVERY_NEW,
MQTT_DISCOVERY_UPDATED,
MQTT_ORIGIN_INFO_SCHEMA,
MQTTDiscoveryPayload,
clear_discovery_hash,
set_discovery_hash,
@ -119,17 +131,6 @@ CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available"
CONF_JSON_ATTRS_TOPIC = "json_attributes_topic"
CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template"
CONF_IDENTIFIERS = "identifiers"
CONF_CONNECTIONS = "connections"
CONF_MANUFACTURER = "manufacturer"
CONF_HW_VERSION = "hw_version"
CONF_SW_VERSION = "sw_version"
CONF_VIA_DEVICE = "via_device"
CONF_DEPRECATED_VIA_HUB = "via_hub"
CONF_SUGGESTED_AREA = "suggested_area"
CONF_CONFIGURATION_URL = "configuration_url"
CONF_OBJECT_ID = "object_id"
MQTT_ATTRIBUTES_BLOCKED = {
"assumed_state",
"available",
@ -228,6 +229,7 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(
MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_ORIGIN): MQTT_ORIGIN_INFO_SCHEMA,
vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ICON): cv.icon,

View File

@ -99,6 +99,16 @@ class PendingDiscovered(TypedDict):
unsub: CALLBACK_TYPE
class MqttOriginInfo(TypedDict, total=False):
"""Integration info of discovered entity."""
name: str
manufacturer: str
sw_version: str
hw_version: str
support_url: str
class MqttCommandTemplate:
"""Class for rendering MQTT payload with command templates."""

View File

@ -168,6 +168,83 @@ async def test_correct_config_discovery(
assert ("binary_sensor", "bla") in hass.data["mqtt"].discovery_already_discovered
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR])
async def test_discovery_integration_info(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test logging discovery of new and updated items."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Beer", "state_topic": "test-topic", "o": {"name": "bla2mqtt", "sw": "1.0" } }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.name == "Beer"
assert (
"Found new component: binary_sensor bla from external application bla2mqtt, version: 1.0"
in caplog.text
)
caplog.clear()
# Send an update and add support url
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
'{ "name": "Milk", "state_topic": "test-topic", "o": {"name": "bla2mqtt", "sw": "1.1", "url": "https://bla2mqtt.example.com/support" } }',
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is not None
assert state.name == "Milk"
assert (
"Component has already been discovered: binary_sensor bla, sending update from external application bla2mqtt, version: 1.1, support URL: https://bla2mqtt.example.com/support"
in caplog.text
)
@pytest.mark.parametrize(
"config_message",
[
'{ "name": "Beer", "state_topic": "test-topic", "o": "bla2mqtt" }',
'{ "name": "Beer", "state_topic": "test-topic", "o": 2.0 }',
'{ "name": "Beer", "state_topic": "test-topic", "o": null }',
'{ "name": "Beer", "state_topic": "test-topic", "o": {"sw": "bla2mqtt"} }',
],
)
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR])
async def test_discovery_with_invalid_integration_info(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
config_message: str,
) -> None:
"""Test sending in correct JSON."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/binary_sensor/bla/config",
config_message,
)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.beer")
assert state is None
assert (
"Unable to parse origin information from discovery message, got" in caplog.text
)
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.FAN])
async def test_discover_fan(
hass: HomeAssistant,
@ -1266,6 +1343,8 @@ ABBREVIATIONS_WHITE_LIST = [
"CONF_WILL_MESSAGE",
"CONF_WS_PATH",
"CONF_WS_HEADERS",
# Integration info
"CONF_SUPPORT_URL",
# Undocumented device configuration
"CONF_DEPRECATED_VIA_HUB",
"CONF_VIA_DEVICE",