"""Support for MQTT discovery.""" import asyncio import json import logging import re from homeassistant.components import mqtt from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.helpers.discovery import async_load_platform 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, CONF_STATE_TOPIC _LOGGER = logging.getLogger(__name__) TOPIC_MATCHER = re.compile( r"(?P\w+)/(?:(?P[a-zA-Z0-9_-]+)/)" r"?(?P[a-zA-Z0-9_-]+)/config" ) SUPPORTED_COMPONENTS = [ "alarm_control_panel", "binary_sensor", "camera", "climate", "cover", "fan", "light", "lock", "sensor", "switch", "vacuum", ] CONFIG_ENTRY_COMPONENTS = [ "alarm_control_panel", "binary_sensor", "camera", "climate", "cover", "fan", "light", "lock", "sensor", "switch", "vacuum", ] DEPRECATED_PLATFORM_TO_SCHEMA = { "light": {"mqtt_json": "json", "mqtt_template": "template"} } # These components require state_topic to be set. # If not specified, infer state_topic from discovery topic. IMPLICIT_STATE_TOPIC_COMPONENTS = ["alarm_control_panel", "binary_sensor", "sensor"] ALREADY_DISCOVERED = "mqtt_discovered_components" DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}" MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" TOPIC_BASE = "~" def clear_discovery_hash(hass, discovery_hash): """Clear entry in ALREADY_DISCOVERED list.""" del hass.data[ALREADY_DISCOVERED][discovery_hash] class MQTTConfig(dict): """Dummy class to allow adding attributes.""" pass async def async_start( hass: HomeAssistantType, discovery_topic, hass_config, config_entry=None ) -> bool: """Initialize of MQTT Discovery.""" async def async_device_message_received(msg): """Process the received message.""" payload = msg.payload topic = msg.topic topic_trimmed = topic.replace("{}/".format(discovery_topic), "", 1) match = TOPIC_MATCHER.match(topic_trimmed) if not match: return component, node_id, object_id = match.groups() if component not in SUPPORTED_COMPONENTS: _LOGGER.warning("Integration %s is not supported", component) return if payload: try: payload = json.loads(payload) except ValueError: _LOGGER.warning("Unable to parse JSON %s: '%s'", object_id, payload) return payload = MQTTConfig(payload) for key in list(payload.keys()): abbreviated_key = key key = ABBREVIATIONS.get(key, key) payload[key] = payload.pop(abbreviated_key) if CONF_DEVICE in payload: device = payload[CONF_DEVICE] for key in list(device.keys()): abbreviated_key = key key = DEVICE_ABBREVIATIONS.get(key, key) device[key] = device.pop(abbreviated_key) if TOPIC_BASE in payload: base = payload.pop(TOPIC_BASE) for key, value in payload.items(): if isinstance(value, str) and value: if value[0] == TOPIC_BASE and key.endswith("_topic"): payload[key] = "{}{}".format(base, value[1:]) if value[-1] == TOPIC_BASE and key.endswith("_topic"): payload[key] = "{}{}".format(value[:-1], base) # If present, the node_id will be included in the discovered object id discovery_id = " ".join((node_id, object_id)) if node_id else object_id discovery_hash = (component, discovery_id) if payload: # Attach MQTT topic to the payload, used for debug prints setattr( payload, "__configuration_source__", "MQTT (topic: '{}')".format(topic) ) if CONF_PLATFORM in payload and "schema" not in payload: platform = payload[CONF_PLATFORM] if ( component in DEPRECATED_PLATFORM_TO_SCHEMA and platform in DEPRECATED_PLATFORM_TO_SCHEMA[component] ): schema = DEPRECATED_PLATFORM_TO_SCHEMA[component][platform] payload["schema"] = schema _LOGGER.warning( '"platform": "%s" is deprecated, ' 'replace with "schema":"%s"', platform, schema, ) payload[CONF_PLATFORM] = "mqtt" if ( CONF_STATE_TOPIC not in payload and component in IMPLICIT_STATE_TOPIC_COMPONENTS ): # state_topic not specified, infer from discovery topic payload[CONF_STATE_TOPIC] = "{}/{}/{}{}/state".format( discovery_topic, component, "%s/" % node_id if node_id else "", object_id, ) _LOGGER.warning( 'implicit %s is deprecated, add "%s":"%s" to ' "%s discovery message", CONF_STATE_TOPIC, CONF_STATE_TOPIC, payload[CONF_STATE_TOPIC], topic, ) payload[ATTR_DISCOVERY_HASH] = discovery_hash if ALREADY_DISCOVERED not in hass.data: hass.data[ALREADY_DISCOVERED] = {} if discovery_hash in hass.data[ALREADY_DISCOVERED]: # Dispatch update _LOGGER.info( "Component has already been discovered: %s %s, sending update", component, discovery_id, ) 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) hass.data[ALREADY_DISCOVERED][discovery_hash] = None if component not in CONFIG_ENTRY_COMPONENTS: await async_load_platform(hass, component, "mqtt", payload, hass_config) return config_entries_key = "{}.{}".format(component, "mqtt") async with hass.data[DATA_CONFIG_ENTRY_LOCK]: if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: await hass.config_entries.async_forward_entry_setup( config_entry, component ) hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) async_dispatcher_send( hass, MQTT_DISCOVERY_NEW.format(component, "mqtt"), payload ) hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() await mqtt.async_subscribe( hass, discovery_topic + "/#", async_device_message_received, 0 ) return True