diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 6982e4d728f..3b7f6abdc9f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -3,7 +3,6 @@ import asyncio from functools import lru_cache, partial, wraps import inspect from itertools import groupby -import json import logging from operator import attrgetter import os @@ -20,8 +19,6 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.const import ( CONF_CLIENT_ID, - CONF_DEVICE, - CONF_NAME, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, @@ -35,12 +32,7 @@ from homeassistant.const import CONF_UNIQUE_ID # noqa: F401 from homeassistant.core import CoreState, Event, HassJob, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, - dispatcher_send, -) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -51,9 +43,6 @@ from homeassistant.util.logging import catch_log_exception from . import config_flow # noqa: F401 pylint: disable=unused-import from . import debug_info, discovery from .const import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_PAYLOAD, - ATTR_DISCOVERY_TOPIC, ATTR_PAYLOAD, ATTR_QOS, ATTR_RETAIN, @@ -68,8 +57,6 @@ from .const import ( DATA_MQTT_CONFIG, DEFAULT_BIRTH, DEFAULT_DISCOVERY, - DEFAULT_PAYLOAD_AVAILABLE, - DEFAULT_PAYLOAD_NOT_AVAILABLE, DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, @@ -79,16 +66,8 @@ from .const import ( MQTT_DISCONNECTED, PROTOCOL_311, ) -from .debug_info import log_messages -from .discovery import ( - LAST_DISCOVERY, - MQTT_DISCOVERY_DONE, - MQTT_DISCOVERY_UPDATED, - clear_discovery_hash, - set_discovery_hash, -) +from .discovery import LAST_DISCOVERY from .models import Message, MessageCallbackType, PublishPayloadType -from .subscription import async_subscribe_topics, async_unsubscribe_topics from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -108,20 +87,6 @@ CONF_TLS_VERSION = "tls_version" CONF_COMMAND_TOPIC = "command_topic" CONF_TOPIC = "topic" -CONF_AVAILABILITY = "availability" -CONF_AVAILABILITY_TOPIC = "availability_topic" -CONF_PAYLOAD_AVAILABLE = "payload_available" -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_MODEL = "model" -CONF_SW_VERSION = "sw_version" -CONF_VIA_DEVICE = "via_device" -CONF_DEPRECATED_VIA_HUB = "via_hub" PROTOCOL_31 = "3.1" @@ -158,16 +123,6 @@ PLATFORMS = [ ] -def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType: - """Validate that a device info entry has at least one identifying value.""" - if not value.get(CONF_IDENTIFIERS) and not value.get(CONF_CONNECTIONS): - raise vol.Invalid( - "Device must have at least one identifying value in " - "'identifiers' and/or 'connections'" - ) - return value - - CLIENT_KEY_AUTH_MSG = ( "client_key and client_cert must both be present in " "the MQTT broker configuration" @@ -243,69 +198,6 @@ CONFIG_SCHEMA = vol.Schema( SCHEMA_BASE = {vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA} -MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( - { - vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic, - vol.Optional( - CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE - ): cv.string, - vol.Optional( - CONF_PAYLOAD_NOT_AVAILABLE, default=DEFAULT_PAYLOAD_NOT_AVAILABLE - ): cv.string, - } -) - -MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema( - { - vol.Exclusive(CONF_AVAILABILITY, "availability"): vol.All( - cv.ensure_list, - [ - { - vol.Optional(CONF_TOPIC): valid_subscribe_topic, - vol.Optional( - CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE - ): cv.string, - vol.Optional( - CONF_PAYLOAD_NOT_AVAILABLE, - default=DEFAULT_PAYLOAD_NOT_AVAILABLE, - ): cv.string, - } - ], - ), - } -) - -MQTT_AVAILABILITY_SCHEMA = MQTT_AVAILABILITY_SINGLE_SCHEMA.extend( - MQTT_AVAILABILITY_LIST_SCHEMA.schema -) - -MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( - cv.deprecated(CONF_DEPRECATED_VIA_HUB, CONF_VIA_DEVICE), - vol.Schema( - { - vol.Optional(CONF_IDENTIFIERS, default=list): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_CONNECTIONS, default=list): vol.All( - cv.ensure_list, [vol.All(vol.Length(2), [cv.string])] - ), - vol.Optional(CONF_MANUFACTURER): cv.string, - vol.Optional(CONF_MODEL): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_SW_VERSION): cv.string, - vol.Optional(CONF_VIA_DEVICE): cv.string, - } - ), - validate_device_has_at_least_one_identifier, -) - -MQTT_JSON_ATTRS_SCHEMA = vol.Schema( - { - vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, - } -) - MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) # Sensor type platforms subscribe to MQTT events @@ -1085,347 +977,6 @@ def _matcher_for_topic(subscription: str) -> Any: return lambda topic: next(matcher.iter_match(topic), False) -class MqttAttributes(Entity): - """Mixin used for platforms that support JSON attributes.""" - - def __init__(self, config: dict) -> None: - """Initialize the JSON attributes mixin.""" - self._attributes = None - self._attributes_sub_state = None - self._attributes_config = config - - async def async_added_to_hass(self) -> None: - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._attributes_subscribe_topics() - - async def attributes_discovery_update(self, config: dict): - """Handle updated discovery message.""" - self._attributes_config = config - await self._attributes_subscribe_topics() - - async def _attributes_subscribe_topics(self): - """(Re)Subscribe to topics.""" - attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) - if attr_tpl is not None: - attr_tpl.hass = self.hass - - @callback - @log_messages(self.hass, self.entity_id) - def attributes_message_received(msg: Message) -> None: - try: - payload = msg.payload - if attr_tpl is not None: - payload = attr_tpl.async_render_with_possible_json_value(payload) - json_dict = json.loads(payload) - if isinstance(json_dict, dict): - self._attributes = json_dict - self.async_write_ha_state() - else: - _LOGGER.warning("JSON result was not a dictionary") - self._attributes = None - except ValueError: - _LOGGER.warning("Erroneous JSON: %s", payload) - self._attributes = None - - self._attributes_sub_state = await async_subscribe_topics( - self.hass, - self._attributes_sub_state, - { - CONF_JSON_ATTRS_TOPIC: { - "topic": self._attributes_config.get(CONF_JSON_ATTRS_TOPIC), - "msg_callback": attributes_message_received, - "qos": self._attributes_config.get(CONF_QOS), - } - }, - ) - - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._attributes_sub_state = await async_unsubscribe_topics( - self.hass, self._attributes_sub_state - ) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - -class MqttAvailability(Entity): - """Mixin used for platforms that report availability.""" - - def __init__(self, config: dict) -> None: - """Initialize the availability mixin.""" - self._availability_sub_state = None - self._available = False - self._availability_setup_from_config(config) - - async def async_added_to_hass(self) -> None: - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._availability_subscribe_topics() - self.async_on_remove( - async_dispatcher_connect(self.hass, MQTT_CONNECTED, self.async_mqtt_connect) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, MQTT_DISCONNECTED, self.async_mqtt_connect - ) - ) - - async def availability_discovery_update(self, config: dict): - """Handle updated discovery message.""" - self._availability_setup_from_config(config) - await self._availability_subscribe_topics() - - def _availability_setup_from_config(self, config): - """(Re)Setup.""" - self._avail_topics = {} - if CONF_AVAILABILITY_TOPIC in config: - self._avail_topics[config[CONF_AVAILABILITY_TOPIC]] = { - CONF_PAYLOAD_AVAILABLE: config[CONF_PAYLOAD_AVAILABLE], - CONF_PAYLOAD_NOT_AVAILABLE: config[CONF_PAYLOAD_NOT_AVAILABLE], - } - - if CONF_AVAILABILITY in config: - for avail in config[CONF_AVAILABILITY]: - self._avail_topics[avail[CONF_TOPIC]] = { - CONF_PAYLOAD_AVAILABLE: avail[CONF_PAYLOAD_AVAILABLE], - CONF_PAYLOAD_NOT_AVAILABLE: avail[CONF_PAYLOAD_NOT_AVAILABLE], - } - - self._avail_config = config - - async def _availability_subscribe_topics(self): - """(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.""" - topic = msg.topic - if msg.payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: - self._available = True - elif msg.payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: - self._available = False - - self.async_write_ha_state() - - topics = {} - for topic in self._avail_topics: - topics[f"availability_{topic}"] = { - "topic": topic, - "msg_callback": availability_message_received, - "qos": self._avail_config[CONF_QOS], - } - - self._availability_sub_state = await async_subscribe_topics( - self.hass, - self._availability_sub_state, - topics, - ) - - @callback - def async_mqtt_connect(self): - """Update state on connection/disconnection to MQTT broker.""" - if not self.hass.is_stopping: - self.async_write_ha_state() - - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._availability_sub_state = await async_unsubscribe_topics( - self.hass, self._availability_sub_state - ) - - @property - def available(self) -> bool: - """Return if the device is available.""" - if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping: - return False - return not self._avail_topics or self._available - - -async def cleanup_device_registry(hass, device_id): - """Remove device registry entry if there are no remaining entities or triggers.""" - # Local import to avoid circular dependencies - # pylint: disable=import-outside-toplevel - from . import device_trigger, tag - - device_registry = await hass.helpers.device_registry.async_get_registry() - entity_registry = await hass.helpers.entity_registry.async_get_registry() - if ( - device_id - and not hass.helpers.entity_registry.async_entries_for_device( - entity_registry, device_id, include_disabled_entities=True - ) - and not await device_trigger.async_get_triggers(hass, device_id) - and not tag.async_has_tags(hass, device_id) - ): - device_registry.async_remove_device(device_id) - - -class MqttDiscoveryUpdate(Entity): - """Mixin used to handle updated discovery message.""" - - def __init__(self, discovery_data, discovery_update=None) -> None: - """Initialize the discovery update mixin.""" - self._discovery_data = discovery_data - self._discovery_update = discovery_update - self._remove_signal = None - self._removed_from_hass = False - - async def async_added_to_hass(self) -> None: - """Subscribe to discovery updates.""" - await super().async_added_to_hass() - self._removed_from_hass = False - discovery_hash = ( - self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None - ) - - async def _async_remove_state_and_registry_entry(self) -> None: - """Remove entity's state and entity registry entry. - - Remove entity from entity registry if it is registered, this also removes the state. - If the entity is not in the entity registry, just remove the state. - """ - entity_registry = ( - await self.hass.helpers.entity_registry.async_get_registry() - ) - if entity_registry.async_is_registered(self.entity_id): - entity_entry = entity_registry.async_get(self.entity_id) - entity_registry.async_remove(self.entity_id) - await cleanup_device_registry(self.hass, entity_entry.device_id) - else: - await self.async_remove() - - async def discovery_callback(payload): - """Handle discovery update.""" - _LOGGER.info( - "Got update for entity with hash: %s '%s'", - discovery_hash, - payload, - ) - old_payload = self._discovery_data[ATTR_DISCOVERY_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) - self._cleanup_discovery_on_remove() - await _async_remove_state_and_registry_entry(self) - elif self._discovery_update: - if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: - # Non-empty, changed payload: Notify component - _LOGGER.info("Updating component: %s", self.entity_id) - await self._discovery_update(payload) - else: - # Non-empty, unchanged payload: Ignore to avoid changing states - _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) - async_dispatcher_send( - self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - - 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, for example when changing entity_id - set_discovery_hash(self.hass, discovery_hash) - self._remove_signal = async_dispatcher_connect( - self.hass, - MQTT_DISCOVERY_UPDATED.format(discovery_hash), - discovery_callback, - ) - async_dispatcher_send( - self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - - async def async_removed_from_registry(self) -> None: - """Clear retained discovery topic in broker.""" - if not self._removed_from_hass: - discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] - publish(self.hass, discovery_topic, "", retain=True) - - @callback - def add_to_platform_abort(self) -> None: - """Abort adding an entity to a platform.""" - if self._discovery_data: - discovery_hash = self._discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(self.hass, discovery_hash) - async_dispatcher_send( - self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - super().add_to_platform_abort() - - async def async_will_remove_from_hass(self) -> None: - """Stop listening to signal and cleanup discovery data..""" - self._cleanup_discovery_on_remove() - - def _cleanup_discovery_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 - - if self._remove_signal: - self._remove_signal() - self._remove_signal = None - - -def device_info_from_config(config): - """Return a device description for device registry.""" - if not config: - return None - - info = { - "identifiers": {(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]}, - "connections": {tuple(x) for x in config[CONF_CONNECTIONS]}, - } - - if CONF_MANUFACTURER in config: - info["manufacturer"] = config[CONF_MANUFACTURER] - - if CONF_MODEL in config: - info["model"] = config[CONF_MODEL] - - if CONF_NAME in config: - info["name"] = config[CONF_NAME] - - if CONF_SW_VERSION in config: - info["sw_version"] = config[CONF_SW_VERSION] - - if CONF_VIA_DEVICE in config: - info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE]) - - return info - - -class MqttEntityDeviceInfo(Entity): - """Mixin used for mqtt platforms that support the device registry.""" - - def __init__(self, device_config: Optional[ConfigType], config_entry=None) -> None: - """Initialize the device mixin.""" - self._device_config = device_config - self._config_entry = config_entry - - async def device_info_discovery_update(self, config: dict): - """Handle updated discovery message.""" - self._device_config = config.get(CONF_DEVICE) - device_registry = await self.hass.helpers.device_registry.async_get_registry() - config_entry_id = self._config_entry.entry_id - device_info = self.device_info - - if config_entry_id is not None and device_info is not None: - device_info["config_entry_id"] = config_entry_id - device_registry.async_get_or_create(**device_info) - - @property - def device_info(self): - """Return a device description for device registry.""" - return device_info_from_config(self._device_config) - - @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} ) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 46eaa912615..21234fbc5b4 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -37,22 +37,27 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -82,7 +87,7 @@ PLATFORM_SCHEMA = ( CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE ): cv.template, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, @@ -97,8 +102,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 17dcd301cd0..c604181bdad 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -31,21 +31,20 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import dt as dt_util -from . import ( - ATTR_DISCOVERY_HASH, - CONF_QOS, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, +from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -59,7 +58,7 @@ CONF_EXPIRE_AFTER = "expire_after" PLATFORM_SCHEMA = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -70,8 +69,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 20d68d1c4b0..3888fcd9663 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -15,20 +15,20 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ( - ATTR_DISCOVERY_HASH, - CONF_QOS, - DOMAIN, - PLATFORMS, +from . import CONF_QOS, DOMAIN, PLATFORMS, subscription +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -38,14 +38,14 @@ DEFAULT_NAME = "MQTT Camera" PLATFORM_SCHEMA = ( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 715658417b2..77503335ded 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -55,21 +55,26 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, DOMAIN, MQTT_BASE_PLATFORM_SCHEMA, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -174,7 +179,7 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( CONF_FAN_MODE_LIST, @@ -237,8 +242,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 59dedd7f475..6de560b52b1 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -41,22 +41,27 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -126,7 +131,7 @@ PLATFORM_SCHEMA = vol.All( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -167,8 +172,8 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema), + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema), validate_options, ) diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 1186a212243..423690d8e69 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -9,8 +9,9 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from . import ATTR_DISCOVERY_HASH, device_trigger +from . import device_trigger from .. import mqtt +from .const import ATTR_DISCOVERY_HASH from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 08a4871084c..9fa40c18037 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -25,17 +25,20 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from .. import ( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from .. import subscription from ... import mqtt from ..const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, +) _LOGGER = logging.getLogger(__name__) @@ -46,7 +49,7 @@ CONF_SOURCE_TYPE = "source_type" PLATFORM_SCHEMA_DISCOVERY = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, @@ -55,8 +58,8 @@ PLATFORM_SCHEMA_DISCOVERY = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 8a8b525da61..6a04fd48049 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -7,7 +7,13 @@ import voluptuous as vol from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.const import ( + CONF_DEVICE, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_PLATFORM, + CONF_TYPE, +) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -17,21 +23,18 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_TOPIC, - CONF_CONNECTIONS, - CONF_DEVICE, - CONF_IDENTIFIERS, - CONF_PAYLOAD, - CONF_QOS, - DOMAIN, - cleanup_device_registry, - debug_info, - trigger as mqtt_trigger, -) +from . import CONF_PAYLOAD, CONF_QOS, DOMAIN, debug_info, trigger as mqtt_trigger from .. import mqtt +from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .mixins import ( + CONF_CONNECTIONS, + CONF_IDENTIFIERS, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + cleanup_device_registry, + device_info_from_config, + validate_device_has_at_least_one_identifier, +) _LOGGER = logging.getLogger(__name__) @@ -62,13 +65,13 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_AUTOMATION_TYPE): str, - vol.Required(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_PAYLOAD, default=None): vol.Any(None, cv.string), vol.Required(CONF_TYPE): cv.string, vol.Required(CONF_SUBTYPE): cv.string, }, - mqtt.validate_device_has_at_least_one_identifier, + validate_device_has_at_least_one_identifier, ) DEVICE_TRIGGERS = "mqtt_device_triggers" @@ -172,7 +175,7 @@ async def _update_device(hass, config_entry, config): """Update device registry.""" device_registry = await hass.helpers.device_registry.async_get_registry() config_entry_id = config_entry.entry_id - device_info = mqtt.device_info_from_config(config[CONF_DEVICE]) + device_info = device_info_from_config(config[CONF_DEVICE]) if config_entry_id is not None and device_info is not None: device_info["config_entry_id"] = config_entry_id diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index cc635ab6e45..eb713655821 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -33,22 +33,27 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -80,7 +85,7 @@ OSCILLATION = "oscillation" PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): mqtt.valid_publish_topic, @@ -109,8 +114,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 1ab0888866c..cfeaaa0d7c9 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -11,7 +11,8 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .. import ATTR_DISCOVERY_HASH, DOMAIN, PLATFORMS +from .. import DOMAIN, PLATFORMS +from ..const import ATTR_DISCOVERY_HASH from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 00ad2671391..230efa5e60a 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -31,19 +31,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from ... import mqtt +from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from ... import mqtt -from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -114,7 +113,7 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -148,8 +147,8 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index bb10fd52ae7..16281da4169 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -42,19 +42,18 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from ... import mqtt +from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from ... import mqtt -from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE @@ -93,7 +92,7 @@ PLATFORM_SCHEMA_JSON = ( CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional( @@ -118,8 +117,8 @@ PLATFORM_SCHEMA_JSON = ( vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index e6b22da5af0..e33d169e47a 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -33,19 +33,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from ... import mqtt +from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from ... import mqtt -from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -77,7 +76,7 @@ PLATFORM_SCHEMA_TEMPLATE = ( vol.Optional(CONF_COLOR_TEMP_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_TEMPLATE): cv.template, vol.Optional(CONF_GREEN_TEMPLATE): cv.template, @@ -91,8 +90,8 @@ PLATFORM_SCHEMA_TEMPLATE = ( vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 70c771ba22d..72df487d22a 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -22,22 +22,27 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -57,7 +62,7 @@ DEFAULT_STATE_UNLOCKED = "UNLOCKED" PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, @@ -71,8 +76,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py new file mode 100644 index 00000000000..c627a254791 --- /dev/null +++ b/homeassistant/components/mqtt/mixins.py @@ -0,0 +1,472 @@ +"""MQTT component mixins and helpers.""" +import json +import logging +from typing import Optional + +import voluptuous as vol + +from homeassistant.const import CONF_DEVICE, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType + +from . import CONF_TOPIC, DATA_MQTT, debug_info, publish +from .const import ( + ATTR_DISCOVERY_HASH, + ATTR_DISCOVERY_PAYLOAD, + ATTR_DISCOVERY_TOPIC, + CONF_QOS, + DEFAULT_PAYLOAD_AVAILABLE, + DEFAULT_PAYLOAD_NOT_AVAILABLE, + DOMAIN, + MQTT_CONNECTED, + MQTT_DISCONNECTED, +) +from .debug_info import log_messages +from .discovery import ( + MQTT_DISCOVERY_DONE, + MQTT_DISCOVERY_UPDATED, + clear_discovery_hash, + set_discovery_hash, +) +from .models import Message +from .subscription import async_subscribe_topics, async_unsubscribe_topics +from .util import valid_subscribe_topic + +_LOGGER = logging.getLogger(__name__) + +CONF_AVAILABILITY = "availability" +CONF_AVAILABILITY_TOPIC = "availability_topic" +CONF_PAYLOAD_AVAILABLE = "payload_available" +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_MODEL = "model" +CONF_SW_VERSION = "sw_version" +CONF_VIA_DEVICE = "via_device" +CONF_DEPRECATED_VIA_HUB = "via_hub" + +MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( + { + vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic, + vol.Optional( + CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE + ): cv.string, + vol.Optional( + CONF_PAYLOAD_NOT_AVAILABLE, default=DEFAULT_PAYLOAD_NOT_AVAILABLE + ): cv.string, + } +) + +MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema( + { + vol.Exclusive(CONF_AVAILABILITY, "availability"): vol.All( + cv.ensure_list, + [ + { + vol.Optional(CONF_TOPIC): valid_subscribe_topic, + vol.Optional( + CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE + ): cv.string, + vol.Optional( + CONF_PAYLOAD_NOT_AVAILABLE, + default=DEFAULT_PAYLOAD_NOT_AVAILABLE, + ): cv.string, + } + ], + ), + } +) + +MQTT_AVAILABILITY_SCHEMA = MQTT_AVAILABILITY_SINGLE_SCHEMA.extend( + MQTT_AVAILABILITY_LIST_SCHEMA.schema +) + + +def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType: + """Validate that a device info entry has at least one identifying value.""" + if value.get(CONF_IDENTIFIERS) or value.get(CONF_CONNECTIONS): + return value + raise vol.Invalid( + "Device must have at least one identifying value in " + "'identifiers' and/or 'connections'" + ) + + +MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( + cv.deprecated(CONF_DEPRECATED_VIA_HUB, CONF_VIA_DEVICE), + vol.Schema( + { + vol.Optional(CONF_IDENTIFIERS, default=list): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_CONNECTIONS, default=list): vol.All( + cv.ensure_list, [vol.All(vol.Length(2), [cv.string])] + ), + vol.Optional(CONF_MANUFACTURER): cv.string, + vol.Optional(CONF_MODEL): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_SW_VERSION): cv.string, + vol.Optional(CONF_VIA_DEVICE): cv.string, + } + ), + validate_device_has_at_least_one_identifier, +) + +MQTT_JSON_ATTRS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, + } +) + + +class MqttAttributes(Entity): + """Mixin used for platforms that support JSON attributes.""" + + def __init__(self, config: dict) -> None: + """Initialize the JSON attributes mixin.""" + self._attributes = None + self._attributes_sub_state = None + self._attributes_config = config + + async def async_added_to_hass(self) -> None: + """Subscribe MQTT events.""" + await super().async_added_to_hass() + await self._attributes_subscribe_topics() + + async def attributes_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._attributes_config = config + await self._attributes_subscribe_topics() + + async def _attributes_subscribe_topics(self): + """(Re)Subscribe to topics.""" + attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) + if attr_tpl is not None: + attr_tpl.hass = self.hass + + @callback + @log_messages(self.hass, self.entity_id) + def attributes_message_received(msg: Message) -> None: + try: + payload = msg.payload + if attr_tpl is not None: + payload = attr_tpl.async_render_with_possible_json_value(payload) + json_dict = json.loads(payload) + if isinstance(json_dict, dict): + self._attributes = json_dict + self.async_write_ha_state() + else: + _LOGGER.warning("JSON result was not a dictionary") + self._attributes = None + except ValueError: + _LOGGER.warning("Erroneous JSON: %s", payload) + self._attributes = None + + self._attributes_sub_state = await async_subscribe_topics( + self.hass, + self._attributes_sub_state, + { + CONF_JSON_ATTRS_TOPIC: { + "topic": self._attributes_config.get(CONF_JSON_ATTRS_TOPIC), + "msg_callback": attributes_message_received, + "qos": self._attributes_config.get(CONF_QOS), + } + }, + ) + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._attributes_sub_state = await async_unsubscribe_topics( + self.hass, self._attributes_sub_state + ) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + +class MqttAvailability(Entity): + """Mixin used for platforms that report availability.""" + + def __init__(self, config: dict) -> None: + """Initialize the availability mixin.""" + self._availability_sub_state = None + self._available = False + self._availability_setup_from_config(config) + + async def async_added_to_hass(self) -> None: + """Subscribe MQTT events.""" + await super().async_added_to_hass() + await self._availability_subscribe_topics() + self.async_on_remove( + async_dispatcher_connect(self.hass, MQTT_CONNECTED, self.async_mqtt_connect) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, MQTT_DISCONNECTED, self.async_mqtt_connect + ) + ) + + async def availability_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._availability_setup_from_config(config) + await self._availability_subscribe_topics() + + def _availability_setup_from_config(self, config): + """(Re)Setup.""" + self._avail_topics = {} + if CONF_AVAILABILITY_TOPIC in config: + self._avail_topics[config[CONF_AVAILABILITY_TOPIC]] = { + CONF_PAYLOAD_AVAILABLE: config[CONF_PAYLOAD_AVAILABLE], + CONF_PAYLOAD_NOT_AVAILABLE: config[CONF_PAYLOAD_NOT_AVAILABLE], + } + + if CONF_AVAILABILITY in config: + for avail in config[CONF_AVAILABILITY]: + self._avail_topics[avail[CONF_TOPIC]] = { + CONF_PAYLOAD_AVAILABLE: avail[CONF_PAYLOAD_AVAILABLE], + CONF_PAYLOAD_NOT_AVAILABLE: avail[CONF_PAYLOAD_NOT_AVAILABLE], + } + + self._avail_config = config + + async def _availability_subscribe_topics(self): + """(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.""" + topic = msg.topic + if msg.payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: + self._available = True + elif msg.payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: + self._available = False + + self.async_write_ha_state() + + topics = { + f"availability_{topic}": { + "topic": topic, + "msg_callback": availability_message_received, + "qos": self._avail_config[CONF_QOS], + } + for topic in self._avail_topics + } + + self._availability_sub_state = await async_subscribe_topics( + self.hass, + self._availability_sub_state, + topics, + ) + + @callback + def async_mqtt_connect(self): + """Update state on connection/disconnection to MQTT broker.""" + if not self.hass.is_stopping: + self.async_write_ha_state() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._availability_sub_state = await async_unsubscribe_topics( + self.hass, self._availability_sub_state + ) + + @property + def available(self) -> bool: + """Return if the device is available.""" + if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping: + return False + return not self._avail_topics or self._available + + +async def cleanup_device_registry(hass, device_id): + """Remove device registry entry if there are no remaining entities or triggers.""" + # Local import to avoid circular dependencies + # pylint: disable=import-outside-toplevel + from . import device_trigger, tag + + device_registry = await hass.helpers.device_registry.async_get_registry() + entity_registry = await hass.helpers.entity_registry.async_get_registry() + if ( + device_id + and not hass.helpers.entity_registry.async_entries_for_device( + entity_registry, device_id, include_disabled_entities=True + ) + and not await device_trigger.async_get_triggers(hass, device_id) + and not tag.async_has_tags(hass, device_id) + ): + device_registry.async_remove_device(device_id) + + +class MqttDiscoveryUpdate(Entity): + """Mixin used to handle updated discovery message.""" + + def __init__(self, discovery_data, discovery_update=None) -> None: + """Initialize the discovery update mixin.""" + self._discovery_data = discovery_data + self._discovery_update = discovery_update + self._remove_signal = None + self._removed_from_hass = False + + async def async_added_to_hass(self) -> None: + """Subscribe to discovery updates.""" + await super().async_added_to_hass() + self._removed_from_hass = False + discovery_hash = ( + self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None + ) + + async def _async_remove_state_and_registry_entry(self) -> None: + """Remove entity's state and entity registry entry. + + Remove entity from entity registry if it is registered, this also removes the state. + If the entity is not in the entity registry, just remove the state. + """ + entity_registry = ( + await self.hass.helpers.entity_registry.async_get_registry() + ) + if entity_registry.async_is_registered(self.entity_id): + entity_entry = entity_registry.async_get(self.entity_id) + entity_registry.async_remove(self.entity_id) + await cleanup_device_registry(self.hass, entity_entry.device_id) + else: + await self.async_remove() + + async def discovery_callback(payload): + """Handle discovery update.""" + _LOGGER.info( + "Got update for entity with hash: %s '%s'", + discovery_hash, + payload, + ) + old_payload = self._discovery_data[ATTR_DISCOVERY_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) + self._cleanup_discovery_on_remove() + await _async_remove_state_and_registry_entry(self) + elif self._discovery_update: + if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: + # Non-empty, changed payload: Notify component + _LOGGER.info("Updating component: %s", self.entity_id) + await self._discovery_update(payload) + else: + # Non-empty, unchanged payload: Ignore to avoid changing states + _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + + 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, for example when changing entity_id + set_discovery_hash(self.hass, discovery_hash) + self._remove_signal = async_dispatcher_connect( + self.hass, + MQTT_DISCOVERY_UPDATED.format(discovery_hash), + discovery_callback, + ) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + + async def async_removed_from_registry(self) -> None: + """Clear retained discovery topic in broker.""" + if not self._removed_from_hass: + discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] + publish(self.hass, discovery_topic, "", retain=True) + + @callback + def add_to_platform_abort(self) -> None: + """Abort adding an entity to a platform.""" + if self._discovery_data: + discovery_hash = self._discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(self.hass, discovery_hash) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + super().add_to_platform_abort() + + async def async_will_remove_from_hass(self) -> None: + """Stop listening to signal and cleanup discovery data..""" + self._cleanup_discovery_on_remove() + + def _cleanup_discovery_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 + + if self._remove_signal: + self._remove_signal() + self._remove_signal = None + + +def device_info_from_config(config): + """Return a device description for device registry.""" + if not config: + return None + + info = { + "identifiers": {(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]}, + "connections": {tuple(x) for x in config[CONF_CONNECTIONS]}, + } + + if CONF_MANUFACTURER in config: + info["manufacturer"] = config[CONF_MANUFACTURER] + + if CONF_MODEL in config: + info["model"] = config[CONF_MODEL] + + if CONF_NAME in config: + info["name"] = config[CONF_NAME] + + if CONF_SW_VERSION in config: + info["sw_version"] = config[CONF_SW_VERSION] + + if CONF_VIA_DEVICE in config: + info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE]) + + return info + + +class MqttEntityDeviceInfo(Entity): + """Mixin used for mqtt platforms that support the device registry.""" + + def __init__(self, device_config: Optional[ConfigType], config_entry=None) -> None: + """Initialize the device mixin.""" + self._device_config = device_config + self._config_entry = config_entry + + async def device_info_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._device_config = config.get(CONF_DEVICE) + device_registry = await self.hass.helpers.device_registry.async_get_registry() + config_entry_id = self._config_entry.entry_id + device_info = self.device_info + + if config_entry_id is not None and device_info is not None: + device_info["config_entry_id"] = config_entry_id + device_registry.async_get_or_create(**device_info) + + @property + def device_info(self): + """Return a device description for device registry.""" + return device_info_from_config(self._device_config) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index bac70723eeb..7196b348a6f 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -23,21 +23,26 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -47,15 +52,15 @@ DEFAULT_OPTIMISTIC = False PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index eebcdc26bac..2923f512d02 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -14,18 +14,11 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ( - ATTR_DISCOVERY_HASH, - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - DOMAIN, - PLATFORMS, - MqttAvailability, - MqttDiscoveryUpdate, -) +from . import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN, PLATFORMS from .. import mqtt +from .const import ATTR_DISCOVERY_HASH from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import MQTT_AVAILABILITY_SCHEMA, MqttAvailability, MqttDiscoveryUpdate _LOGGER = logging.getLogger(__name__) @@ -41,7 +34,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } -).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +).extend(MQTT_AVAILABILITY_SCHEMA.schema) async def async_setup_platform( diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index cfcd46c9e95..5ddfc29d80f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -29,21 +29,20 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import dt as dt_util -from . import ( - ATTR_DISCOVERY_HASH, - CONF_QOS, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, +from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -54,7 +53,7 @@ DEFAULT_FORCE_UPDATE = False PLATFORM_SCHEMA = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -64,8 +63,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index e074cb819d2..35a436b0be8 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -27,22 +27,27 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -56,7 +61,7 @@ CONF_STATE_OFF = "state_off" PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -67,8 +72,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 1185f925b74..c4db7b5f4b9 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_PLATFORM, CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_DEVICE, CONF_PLATFORM, CONF_VALUE_TEMPLATE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.dispatcher import ( @@ -11,25 +11,23 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from . import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_TOPIC, - CONF_CONNECTIONS, - CONF_DEVICE, - CONF_IDENTIFIERS, - CONF_QOS, - CONF_TOPIC, - DOMAIN, - cleanup_device_registry, - subscription, -) +from . import CONF_QOS, CONF_TOPIC, DOMAIN, subscription from .. import mqtt +from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC from .discovery import ( MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, MQTT_DISCOVERY_UPDATED, clear_discovery_hash, ) +from .mixins import ( + CONF_CONNECTIONS, + CONF_IDENTIFIERS, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + cleanup_device_registry, + device_info_from_config, + validate_device_has_at_least_one_identifier, +) from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -39,12 +37,12 @@ TAGS = "mqtt_tags" PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_PLATFORM): "mqtt", vol.Required(CONF_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, - mqtt.validate_device_has_at_least_one_identifier, + validate_device_has_at_least_one_identifier, ) @@ -236,7 +234,7 @@ async def _update_device(hass, config_entry, config): """Update device registry.""" device_registry = await hass.helpers.device_registry.async_get_registry() config_entry_id = config_entry.entry_id - device_info = mqtt.device_info_from_config(config[CONF_DEVICE]) + device_info = device_info_from_config(config[CONF_DEVICE]) if config_entry_id is not None and device_info is not None: device_info["config_entry_id"] = config_entry_id diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 36e6df4ed1d..09f25eaf732 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -10,7 +10,8 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.reload import async_setup_reload_service -from .. import ATTR_DISCOVERY_HASH, DOMAIN as MQTT_DOMAIN, PLATFORMS +from .. import DOMAIN as MQTT_DOMAIN, PLATFORMS +from ..const import ATTR_DISCOVERY_HASH from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 65acc9afc71..dd156720a01 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -28,15 +28,18 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from .. import ( +from .. import subscription +from ... import mqtt +from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from ... import mqtt -from ..debug_info import log_messages from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) @@ -120,7 +123,7 @@ PLATFORM_SCHEMA_LEGACY = ( vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): mqtt.valid_publish_topic, vol.Inclusive(CONF_CLEANING_TEMPLATE, "cleaning"): cv.template, vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Inclusive(CONF_DOCKED_TEMPLATE, "docked"): cv.template, vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): mqtt.valid_publish_topic, vol.Inclusive(CONF_ERROR_TEMPLATE, "error"): cv.template, @@ -160,8 +163,8 @@ PLATFORM_SCHEMA_LEGACY = ( vol.Optional(mqtt.CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_VACUUM_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 5a8666e5a2e..536dd89b0fe 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -32,19 +32,18 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from ... import mqtt +from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from ... import mqtt -from ..debug_info import log_messages from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) @@ -120,7 +119,7 @@ DEFAULT_PAYLOAD_PAUSE = "pause" PLATFORM_SCHEMA_STATE = ( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] ), @@ -148,8 +147,8 @@ PLATFORM_SCHEMA_STATE = ( vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_VACUUM_SCHEMA.schema) ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 7288c3e4304..2907a0e4cfc 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import mqtt, websocket_api from homeassistant.components.mqtt import debug_info +from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.const import ( ATTR_DOMAIN, ATTR_SERVICE, @@ -241,12 +242,12 @@ def test_validate_publish_topic(): def test_entity_device_info_schema(): """Test MQTT entity device info validation.""" # just identifier - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": ["abcd"]}) - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": "abcd"}) + MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": ["abcd"]}) + MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": "abcd"}) # just connection - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({"connections": [["mac", "02:5b:26:a8:dc:12"]]}) + MQTT_ENTITY_DEVICE_INFO_SCHEMA({"connections": [["mac", "02:5b:26:a8:dc:12"]]}) # full device info - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "identifiers": ["helloworld", "hello"], "connections": [["mac", "02:5b:26:a8:dc:12"], ["zigbee", "zigbee_id"]], @@ -257,7 +258,7 @@ def test_entity_device_info_schema(): } ) # full device info with via_device - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "identifiers": ["helloworld", "hello"], "connections": [["mac", "02:5b:26:a8:dc:12"], ["zigbee", "zigbee_id"]], @@ -270,7 +271,7 @@ def test_entity_device_info_schema(): ) # no identifiers with pytest.raises(vol.Invalid): - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "manufacturer": "Whatever", "name": "Beer", @@ -280,7 +281,7 @@ def test_entity_device_info_schema(): ) # empty identifiers with pytest.raises(vol.Invalid): - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( {"identifiers": [], "connections": [], "name": "Beer"} )