From 24e148ab8ea8b497c7b5589f3e323e7df11c395f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 30 May 2022 14:26:01 +0200 Subject: [PATCH 001/107] Cleanup and use new MQTT_BASE_SCHEMA constants (#72283) * Use new MQTT_BASE_SCHEMA constants * Update constants for mqtt_room and manual_mqtt * Revert removing platform key --- .../manual_mqtt/alarm_control_panel.py | 2 +- homeassistant/components/mqtt/__init__.py | 18 ------------------ .../components/mqtt/device_automation.py | 6 ++++-- .../components/mqtt/device_trigger.py | 19 +++++++++++++------ homeassistant/components/mqtt/tag.py | 2 +- homeassistant/components/mqtt_room/sensor.py | 2 +- 6 files changed, 20 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 730d7ae1f9e..5b74af49a91 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -110,7 +110,7 @@ def _state_schema(state): PLATFORM_SCHEMA = vol.Schema( vol.All( - mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + mqtt.MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "manual_mqtt", vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 1728dd7f2c7..46eb7052f4f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -261,24 +261,6 @@ SCHEMA_BASE = { MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) -# Will be removed when all platforms support a modern platform schema -MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) -# Will be removed when all platforms support a modern platform schema -MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - } -) -# Will be removed when all platforms support a modern platform schema -MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, - } -) - # Sensor type platforms subscribe to MQTT events MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( { diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index cafbd66b098..002ae6e3991 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -3,6 +3,8 @@ import functools import voluptuous as vol +import homeassistant.helpers.config_validation as cv + from . import device_trigger from .. import mqtt from .mixins import async_setup_entry_helper @@ -12,10 +14,10 @@ AUTOMATION_TYPES = [AUTOMATION_TYPE_TRIGGER] AUTOMATION_TYPES_SCHEMA = vol.In(AUTOMATION_TYPES) CONF_AUTOMATION_TYPE = "automation_type" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( {vol.Required(CONF_AUTOMATION_TYPE): AUTOMATION_TYPES_SCHEMA}, extra=vol.ALLOW_EXTRA, -) +).extend(mqtt.MQTT_BASE_SCHEMA.schema) async def async_setup_entry(hass, config_entry): diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 42ffcee1644..2c6c6ecc3ba 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -30,7 +30,14 @@ from homeassistant.helpers.typing import ConfigType from . import debug_info, trigger as mqtt_trigger from .. import mqtt -from .const import ATTR_DISCOVERY_HASH, CONF_PAYLOAD, CONF_QOS, CONF_TOPIC, DOMAIN +from .const import ( + ATTR_DISCOVERY_HASH, + CONF_ENCODING, + CONF_PAYLOAD, + CONF_QOS, + CONF_TOPIC, + DOMAIN, +) from .discovery import MQTT_DISCOVERY_DONE from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -64,7 +71,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) -TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_AUTOMATION_TYPE): str, vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -94,10 +101,10 @@ class TriggerInstance: async def async_attach_trigger(self) -> None: """Attach MQTT trigger.""" mqtt_config = { - mqtt_trigger.CONF_PLATFORM: mqtt.DOMAIN, - mqtt_trigger.CONF_TOPIC: self.trigger.topic, - mqtt_trigger.CONF_ENCODING: DEFAULT_ENCODING, - mqtt_trigger.CONF_QOS: self.trigger.qos, + CONF_PLATFORM: mqtt.DOMAIN, + CONF_TOPIC: self.trigger.topic, + CONF_ENCODING: DEFAULT_ENCODING, + CONF_QOS: self.trigger.qos, } if self.trigger.payload: mqtt_config[CONF_PAYLOAD] = self.trigger.payload diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 5bfbbd73bce..25e49524b8f 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -30,7 +30,7 @@ LOG_NAME = "Tag" TAG = "tag" TAGS = "mqtt_tags" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_PLATFORM): "mqtt", diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 8f7455eb998..54de561c11e 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_AWAY_TIMEOUT, default=DEFAULT_AWAY_TIMEOUT): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, } -).extend(mqtt.MQTT_RO_PLATFORM_SCHEMA.schema) +).extend(mqtt.MQTT_RO_SCHEMA.schema) MQTT_PAYLOAD = vol.Schema( vol.All( From 9295cc4df9633ef2b5c8dde79ea5a1cdeab24357 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 May 2022 09:32:44 +0200 Subject: [PATCH 002/107] Move MQTT config schemas and client to separate modules (#71995) * Move MQTT config schemas and client to separate modules * Update integrations depending on MQTT --- .../manual_mqtt/alarm_control_panel.py | 2 +- homeassistant/components/mqtt/__init__.py | 958 +----------------- .../components/mqtt/alarm_control_panel.py | 14 +- .../components/mqtt/binary_sensor.py | 7 +- homeassistant/components/mqtt/button.py | 11 +- homeassistant/components/mqtt/camera.py | 7 +- homeassistant/components/mqtt/client.py | 659 ++++++++++++ homeassistant/components/mqtt/climate.py | 58 +- homeassistant/components/mqtt/config.py | 148 +++ homeassistant/components/mqtt/config_flow.py | 2 +- homeassistant/components/mqtt/const.py | 26 +- homeassistant/components/mqtt/cover.py | 20 +- .../components/mqtt/device_automation.py | 4 +- .../mqtt/device_tracker/schema_discovery.py | 7 +- .../mqtt/device_tracker/schema_yaml.py | 10 +- .../components/mqtt/device_trigger.py | 6 +- homeassistant/components/mqtt/fan.py | 24 +- homeassistant/components/mqtt/humidifier.py | 18 +- .../components/mqtt/light/schema_basic.py | 48 +- .../components/mqtt/light/schema_json.py | 11 +- .../components/mqtt/light/schema_template.py | 7 +- homeassistant/components/mqtt/lock.py | 7 +- homeassistant/components/mqtt/mixins.py | 14 +- homeassistant/components/mqtt/models.py | 126 ++- homeassistant/components/mqtt/number.py | 7 +- homeassistant/components/mqtt/scene.py | 10 +- homeassistant/components/mqtt/select.py | 7 +- homeassistant/components/mqtt/sensor.py | 10 +- homeassistant/components/mqtt/siren.py | 7 +- homeassistant/components/mqtt/switch.py | 7 +- homeassistant/components/mqtt/tag.py | 8 +- .../components/mqtt/vacuum/schema_legacy.py | 28 +- .../components/mqtt/vacuum/schema_state.py | 15 +- .../components/mqtt_json/device_tracker.py | 2 +- homeassistant/components/mqtt_room/sensor.py | 2 +- tests/components/mqtt/test_cover.py | 2 +- tests/components/mqtt/test_init.py | 18 +- tests/components/mqtt/test_legacy_vacuum.py | 2 +- tests/components/mqtt/test_state_vacuum.py | 2 +- 39 files changed, 1213 insertions(+), 1108 deletions(-) create mode 100644 homeassistant/components/mqtt/client.py create mode 100644 homeassistant/components/mqtt/config.py diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 5b74af49a91..67675a44e22 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -110,7 +110,7 @@ def _state_schema(state): PLATFORM_SCHEMA = vol.Schema( vol.All( - mqtt.MQTT_BASE_SCHEMA.extend( + mqtt.config.MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "manual_mqtt", vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 46eb7052f4f..e21885d2585 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,23 +1,13 @@ """Support for MQTT message handling.""" from __future__ import annotations -from ast import literal_eval import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Callable from dataclasses import dataclass import datetime as dt -from functools import lru_cache, partial, wraps -import inspect -from itertools import groupby import logging -from operator import attrgetter -import ssl -import time -from typing import TYPE_CHECKING, Any, Union, cast -import uuid +from typing import Any, cast -import attr -import certifi import jinja2 import voluptuous as vol @@ -25,120 +15,73 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_NAME, - CONF_CLIENT_ID, CONF_DISCOVERY, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, - CONF_PROTOCOL, CONF_USERNAME, - CONF_VALUE_TEMPLATE, - EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, - Platform, -) -from homeassistant.core import ( - CoreState, - Event, - HassJob, - HomeAssistant, - ServiceCall, - callback, ) +from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback from homeassistant.data_entry_flow import BaseServiceInfo -from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized +from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from homeassistant.loader import bind_hass -from homeassistant.util import dt as dt_util -from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.util.logging import catch_log_exception +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow from . import debug_info, discovery -from .const import ( +from .client import ( # noqa: F401 + MQTT, + async_publish, + async_subscribe, + publish, + subscribe, +) +from .config import CONFIG_SCHEMA_BASE, DEFAULT_VALUES, DEPRECATED_CONFIG_KEYS +from .const import ( # noqa: F401 ATTR_PAYLOAD, ATTR_QOS, ATTR_RETAIN, ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, - CONF_CERTIFICATE, - CONF_CLIENT_CERT, - CONF_CLIENT_KEY, CONF_COMMAND_TOPIC, - CONF_ENCODING, + CONF_DISCOVERY_PREFIX, CONF_QOS, - CONF_RETAIN, CONF_STATE_TOPIC, - CONF_TLS_INSECURE, CONF_TLS_VERSION, CONF_TOPIC, CONF_WILL_MESSAGE, CONFIG_ENTRY_IS_SETUP, DATA_CONFIG_ENTRY_LOCK, + DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, - DEFAULT_BIRTH, - DEFAULT_DISCOVERY, DEFAULT_ENCODING, - DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, - DEFAULT_WILL, DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - PROTOCOL_31, - PROTOCOL_311, + PLATFORMS, ) -from .discovery import LAST_DISCOVERY -from .models import ( - AsyncMessageCallbackType, - MessageCallbackType, - PublishMessage, +from .models import ( # noqa: F401 + MqttCommandTemplate, + MqttValueTemplate, PublishPayloadType, ReceiveMessage, ReceivePayloadType, ) from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic -if TYPE_CHECKING: - # Only import for paho-mqtt type checking here, imports are done locally - # because integrations should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt - _LOGGER = logging.getLogger(__name__) -_SENTINEL = object() - -DATA_MQTT = "mqtt" - SERVICE_PUBLISH = "publish" SERVICE_DUMP = "dump" -CONF_DISCOVERY_PREFIX = "discovery_prefix" -CONF_KEEPALIVE = "keepalive" - -DEFAULT_PORT = 1883 -DEFAULT_KEEPALIVE = 60 -DEFAULT_PROTOCOL = PROTOCOL_311 -DEFAULT_TLS_PROTOCOL = "auto" - -DEFAULT_VALUES = { - CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, - CONF_DISCOVERY: DEFAULT_DISCOVERY, - CONF_PORT: DEFAULT_PORT, - CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, - CONF_WILL_MESSAGE: DEFAULT_WILL, -} - MANDATORY_DEFAULT_VALUES = (CONF_PORT,) ATTR_TOPIC_TEMPLATE = "topic_template" @@ -150,93 +93,6 @@ CONNECTION_SUCCESS = "connection_success" CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" -DISCOVERY_COOLDOWN = 2 -TIMEOUT_ACK = 10 - -PLATFORMS = [ - Platform.ALARM_CONTROL_PANEL, - Platform.BINARY_SENSOR, - Platform.BUTTON, - Platform.CAMERA, - Platform.CLIMATE, - Platform.DEVICE_TRACKER, - Platform.COVER, - Platform.FAN, - Platform.HUMIDIFIER, - Platform.LIGHT, - Platform.LOCK, - Platform.NUMBER, - Platform.SELECT, - Platform.SCENE, - Platform.SENSOR, - Platform.SIREN, - Platform.SWITCH, - Platform.VACUUM, -] - -CLIENT_KEY_AUTH_MSG = ( - "client_key and client_cert must both be present in " - "the MQTT broker configuration" -) - -MQTT_WILL_BIRTH_SCHEMA = vol.Schema( - { - vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, - vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - }, - required=True, -) - -PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( - {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} -) - -CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( - { - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( - vol.Coerce(int), vol.Range(min=15) - ), - vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), - vol.Inclusive( - CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Inclusive( - CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) - ), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY): cv.boolean, - # discovery_prefix must be a valid publish topic because if no - # state topic is specified, it will be created with the given prefix. - vol.Optional( - CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX - ): valid_publish_topic, - } -) - -DEPRECATED_CONFIG_KEYS = [ - CONF_BIRTH_MESSAGE, - CONF_BROKER, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_PORT, - CONF_TLS_VERSION, - CONF_USERNAME, - CONF_WILL_MESSAGE, -] - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -254,29 +110,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SCHEMA_BASE = { - vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, -} - -MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) - -# Sensor type platforms subscribe to MQTT events -MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( - { - vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - } -) - -# Switch type platforms publish to MQTT and may subscribe -MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( - { - vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, - } -) # Service call validation schema MQTT_PUBLISH_SCHEMA = vol.All( @@ -295,124 +128,6 @@ MQTT_PUBLISH_SCHEMA = vol.All( ) -SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None - - -class MqttCommandTemplate: - """Class for rendering MQTT payload with command templates.""" - - def __init__( - self, - command_template: template.Template | None, - *, - hass: HomeAssistant | None = None, - entity: Entity | None = None, - ) -> None: - """Instantiate a command template.""" - self._attr_command_template = command_template - if command_template is None: - return - - self._entity = entity - - command_template.hass = hass - - if entity: - command_template.hass = entity.hass - - @callback - def async_render( - self, - value: PublishPayloadType = None, - variables: TemplateVarsType = None, - ) -> PublishPayloadType: - """Render or convert the command template with given value or variables.""" - - def _convert_outgoing_payload( - payload: PublishPayloadType, - ) -> PublishPayloadType: - """Ensure correct raw MQTT payload is passed as bytes for publishing.""" - if isinstance(payload, str): - try: - native_object = literal_eval(payload) - if isinstance(native_object, bytes): - return native_object - - except (ValueError, TypeError, SyntaxError, MemoryError): - pass - - return payload - - if self._attr_command_template is None: - return value - - values = {"value": value} - if self._entity: - values[ATTR_ENTITY_ID] = self._entity.entity_id - values[ATTR_NAME] = self._entity.name - if variables is not None: - values.update(variables) - return _convert_outgoing_payload( - self._attr_command_template.async_render(values, parse_result=False) - ) - - -class MqttValueTemplate: - """Class for rendering MQTT value template with possible json values.""" - - def __init__( - self, - value_template: template.Template | None, - *, - hass: HomeAssistant | None = None, - entity: Entity | None = None, - config_attributes: TemplateVarsType = None, - ) -> None: - """Instantiate a value template.""" - self._value_template = value_template - self._config_attributes = config_attributes - if value_template is None: - return - - value_template.hass = hass - self._entity = entity - - if entity: - value_template.hass = entity.hass - - @callback - def async_render_with_possible_json_value( - self, - payload: ReceivePayloadType, - default: ReceivePayloadType | object = _SENTINEL, - variables: TemplateVarsType = None, - ) -> ReceivePayloadType: - """Render with possible json value or pass-though a received MQTT value.""" - if self._value_template is None: - return payload - - values: dict[str, Any] = {} - - if variables is not None: - values.update(variables) - - if self._config_attributes is not None: - values.update(self._config_attributes) - - if self._entity: - values[ATTR_ENTITY_ID] = self._entity.entity_id - values[ATTR_NAME] = self._entity.name - - if default == _SENTINEL: - return self._value_template.async_render_with_possible_json_value( - payload, variables=values - ) - - return self._value_template.async_render_with_possible_json_value( - payload, default, variables=values - ) - - @dataclass class MqttServiceInfo(BaseServiceInfo): """Prepared info from mqtt entries.""" @@ -425,163 +140,6 @@ class MqttServiceInfo(BaseServiceInfo): timestamp: dt.datetime -def publish( - hass: HomeAssistant, - topic: str, - payload: PublishPayloadType, - qos: int | None = 0, - retain: bool | None = False, - encoding: str | None = DEFAULT_ENCODING, -) -> None: - """Publish message to a MQTT topic.""" - hass.add_job(async_publish, hass, topic, payload, qos, retain, encoding) - - -async def async_publish( - hass: HomeAssistant, - topic: str, - payload: PublishPayloadType, - qos: int | None = 0, - retain: bool | None = False, - encoding: str | None = DEFAULT_ENCODING, -) -> None: - """Publish message to a MQTT topic.""" - - outgoing_payload = payload - if not isinstance(payload, bytes): - if not encoding: - _LOGGER.error( - "Can't pass-through payload for publishing %s on %s with no encoding set, need 'bytes' got %s", - payload, - topic, - type(payload), - ) - return - outgoing_payload = str(payload) - if encoding != DEFAULT_ENCODING: - # a string is encoded as utf-8 by default, other encoding requires bytes as payload - try: - outgoing_payload = outgoing_payload.encode(encoding) - except (AttributeError, LookupError, UnicodeEncodeError): - _LOGGER.error( - "Can't encode payload for publishing %s on %s with encoding %s", - payload, - topic, - encoding, - ) - return - - await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain) - - -AsyncDeprecatedMessageCallbackType = Callable[ - [str, ReceivePayloadType, int], Awaitable[None] -] -DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None] - - -def wrap_msg_callback( - msg_callback: AsyncDeprecatedMessageCallbackType | DeprecatedMessageCallbackType, -) -> AsyncMessageCallbackType | MessageCallbackType: - """Wrap an MQTT message callback to support deprecated signature.""" - # Check for partials to properly determine if coroutine function - check_func = msg_callback - while isinstance(check_func, partial): - check_func = check_func.func - - wrapper_func: AsyncMessageCallbackType | MessageCallbackType - if asyncio.iscoroutinefunction(check_func): - - @wraps(msg_callback) - async def async_wrapper(msg: ReceiveMessage) -> None: - """Call with deprecated signature.""" - await cast(AsyncDeprecatedMessageCallbackType, msg_callback)( - msg.topic, msg.payload, msg.qos - ) - - wrapper_func = async_wrapper - else: - - @wraps(msg_callback) - def wrapper(msg: ReceiveMessage) -> None: - """Call with deprecated signature.""" - msg_callback(msg.topic, msg.payload, msg.qos) - - wrapper_func = wrapper - return wrapper_func - - -@bind_hass -async def async_subscribe( - hass: HomeAssistant, - topic: str, - msg_callback: AsyncMessageCallbackType - | MessageCallbackType - | DeprecatedMessageCallbackType - | AsyncDeprecatedMessageCallbackType, - qos: int = DEFAULT_QOS, - encoding: str | None = "utf-8", -): - """Subscribe to an MQTT topic. - - Call the return value to unsubscribe. - """ - # Count callback parameters which don't have a default value - non_default = 0 - if msg_callback: - non_default = sum( - p.default == inspect.Parameter.empty - for _, p in inspect.signature(msg_callback).parameters.items() - ) - - wrapped_msg_callback = msg_callback - # If we have 3 parameters with no default value, wrap the callback - if non_default == 3: - module = inspect.getmodule(msg_callback) - _LOGGER.warning( - "Signature of MQTT msg_callback '%s.%s' is deprecated", - module.__name__ if module else "", - msg_callback.__name__, - ) - wrapped_msg_callback = wrap_msg_callback( - cast(DeprecatedMessageCallbackType, msg_callback) - ) - - async_remove = await hass.data[DATA_MQTT].async_subscribe( - topic, - catch_log_exception( - wrapped_msg_callback, - lambda msg: ( - f"Exception in {msg_callback.__name__} when handling msg on " - f"'{msg.topic}': '{msg.payload}'" - ), - ), - qos, - encoding, - ) - return async_remove - - -@bind_hass -def subscribe( - hass: HomeAssistant, - topic: str, - msg_callback: MessageCallbackType, - qos: int = DEFAULT_QOS, - encoding: str = "utf-8", -) -> Callable[[], None]: - """Subscribe to an MQTT topic.""" - async_remove = asyncio.run_coroutine_threadsafe( - async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop - ).result() - - def remove(): - """Remove listener convert.""" - run_callback_threadsafe(hass.loop, async_remove).result() - - return remove - - async def _async_setup_discovery( hass: HomeAssistant, conf: ConfigType, config_entry ) -> None: @@ -649,6 +207,26 @@ def _merge_extended_config(entry, conf): return {**conf, **entry.data} +async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle signals of config entry being updated. + + Causes for this is config entry options changing. + """ + mqtt_client = hass.data[DATA_MQTT] + + if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: + conf = CONFIG_SCHEMA_BASE(dict(entry.data)) + + mqtt_client.conf = _merge_extended_config(entry, conf) + await mqtt_client.async_disconnect() + mqtt_client.init_client() + await mqtt_client.async_connect() + + await discovery.async_stop(hass) + if mqtt_client.conf.get(CONF_DISCOVERY): + await _async_setup_discovery(hass, mqtt_client.conf, entry) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options @@ -685,6 +263,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, conf, ) + entry.add_update_listener(_async_config_entry_updated) await hass.data[DATA_MQTT].async_connect() @@ -813,459 +392,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -@attr.s(slots=True, frozen=True) -class Subscription: - """Class to hold data about an active subscription.""" - - topic: str = attr.ib() - matcher: Any = attr.ib() - job: HassJob = attr.ib() - qos: int = attr.ib(default=0) - encoding: str | None = attr.ib(default="utf-8") - - -class MqttClientSetup: - """Helper class to setup the paho mqtt client from config.""" - - def __init__(self, config: ConfigType) -> None: - """Initialize the MQTT client setup helper.""" - - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - - if config[CONF_PROTOCOL] == PROTOCOL_31: - proto = mqtt.MQTTv31 - else: - proto = mqtt.MQTTv311 - - if (client_id := config.get(CONF_CLIENT_ID)) is None: - # PAHO MQTT relies on the MQTT server to generate random client IDs. - # However, that feature is not mandatory so we generate our own. - client_id = mqtt.base62(uuid.uuid4().int, padding=22) - self._client = mqtt.Client(client_id, protocol=proto) - - # Enable logging - self._client.enable_logger() - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - if username is not None: - self._client.username_pw_set(username, password) - - if (certificate := config.get(CONF_CERTIFICATE)) == "auto": - certificate = certifi.where() - - client_key = config.get(CONF_CLIENT_KEY) - client_cert = config.get(CONF_CLIENT_CERT) - tls_insecure = config.get(CONF_TLS_INSECURE) - if certificate is not None: - self._client.tls_set( - certificate, - certfile=client_cert, - keyfile=client_key, - tls_version=ssl.PROTOCOL_TLS, - ) - - if tls_insecure is not None: - self._client.tls_insecure_set(tls_insecure) - - @property - def client(self) -> mqtt.Client: - """Return the paho MQTT client.""" - return self._client - - -class MQTT: - """Home Assistant MQTT client.""" - - def __init__( - self, - hass: HomeAssistant, - config_entry, - conf, - ) -> None: - """Initialize Home Assistant MQTT client.""" - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - - self.hass = hass - self.config_entry = config_entry - self.conf = conf - self.subscriptions: list[Subscription] = [] - self.connected = False - self._ha_started = asyncio.Event() - self._last_subscribe = time.time() - self._mqttc: mqtt.Client = None - self._paho_lock = asyncio.Lock() - - self._pending_operations: dict[str, asyncio.Event] = {} - - if self.hass.state == CoreState.running: - self._ha_started.set() - else: - - @callback - def ha_started(_): - self._ha_started.set() - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) - - self.init_client() - self.config_entry.add_update_listener(self.async_config_entry_updated) - - @staticmethod - async def async_config_entry_updated( - hass: HomeAssistant, entry: ConfigEntry - ) -> None: - """Handle signals of config entry being updated. - - This is a static method because a class method (bound method), can not be used with weak references. - Causes for this is config entry options changing. - """ - self = hass.data[DATA_MQTT] - - if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: - conf = CONFIG_SCHEMA_BASE(dict(entry.data)) - - self.conf = _merge_extended_config(entry, conf) - await self.async_disconnect() - self.init_client() - await self.async_connect() - - await discovery.async_stop(hass) - if self.conf.get(CONF_DISCOVERY): - await _async_setup_discovery(hass, self.conf, entry) - - def init_client(self): - """Initialize paho client.""" - self._mqttc = MqttClientSetup(self.conf).client - self._mqttc.on_connect = self._mqtt_on_connect - self._mqttc.on_disconnect = self._mqtt_on_disconnect - self._mqttc.on_message = self._mqtt_on_message - self._mqttc.on_publish = self._mqtt_on_callback - self._mqttc.on_subscribe = self._mqtt_on_callback - self._mqttc.on_unsubscribe = self._mqtt_on_callback - - if ( - CONF_WILL_MESSAGE in self.conf - and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE] - ): - will_message = PublishMessage(**self.conf[CONF_WILL_MESSAGE]) - else: - will_message = None - - if will_message is not None: - self._mqttc.will_set( - topic=will_message.topic, - payload=will_message.payload, - qos=will_message.qos, - retain=will_message.retain, - ) - - async def async_publish( - self, topic: str, payload: PublishPayloadType, qos: int, retain: bool - ) -> None: - """Publish a MQTT message.""" - async with self._paho_lock: - msg_info = await self.hass.async_add_executor_job( - self._mqttc.publish, topic, payload, qos, retain - ) - _LOGGER.debug( - "Transmitting message on %s: '%s', mid: %s", - topic, - payload, - msg_info.mid, - ) - _raise_on_error(msg_info.rc) - await self._wait_for_mid(msg_info.mid) - - async def async_connect(self) -> None: - """Connect to the host. Does not process messages yet.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - result: int | None = None - try: - result = await self.hass.async_add_executor_job( - self._mqttc.connect, - self.conf[CONF_BROKER], - self.conf[CONF_PORT], - self.conf[CONF_KEEPALIVE], - ) - except OSError as err: - _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) - - if result is not None and result != 0: - _LOGGER.error( - "Failed to connect to MQTT server: %s", mqtt.error_string(result) - ) - - self._mqttc.loop_start() - - async def async_disconnect(self): - """Stop the MQTT client.""" - - def stop(): - """Stop the MQTT client.""" - # Do not disconnect, we want the broker to always publish will - self._mqttc.loop_stop() - - await self.hass.async_add_executor_job(stop) - - async def async_subscribe( - self, - topic: str, - msg_callback: MessageCallbackType, - qos: int, - encoding: str | None = None, - ) -> Callable[[], None]: - """Set up a subscription to a topic with the provided qos. - - This method is a coroutine. - """ - if not isinstance(topic, str): - raise HomeAssistantError("Topic needs to be a string!") - - subscription = Subscription( - topic, _matcher_for_topic(topic), HassJob(msg_callback), qos, encoding - ) - self.subscriptions.append(subscription) - self._matching_subscriptions.cache_clear() - - # Only subscribe if currently connected. - if self.connected: - self._last_subscribe = time.time() - await self._async_perform_subscription(topic, qos) - - @callback - def async_remove() -> None: - """Remove subscription.""" - if subscription not in self.subscriptions: - raise HomeAssistantError("Can't remove subscription twice") - self.subscriptions.remove(subscription) - self._matching_subscriptions.cache_clear() - - # Only unsubscribe if currently connected. - if self.connected: - self.hass.async_create_task(self._async_unsubscribe(topic)) - - return async_remove - - async def _async_unsubscribe(self, topic: str) -> None: - """Unsubscribe from a topic. - - This method is a coroutine. - """ - if any(other.topic == topic for other in self.subscriptions): - # Other subscriptions on topic remaining - don't unsubscribe. - return - - async with self._paho_lock: - result: int | None = None - result, mid = await self.hass.async_add_executor_job( - self._mqttc.unsubscribe, topic - ) - _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) - _raise_on_error(result) - await self._wait_for_mid(mid) - - async def _async_perform_subscription(self, topic: str, qos: int) -> None: - """Perform a paho-mqtt subscription.""" - async with self._paho_lock: - result: int | None = None - result, mid = await self.hass.async_add_executor_job( - self._mqttc.subscribe, topic, qos - ) - _LOGGER.debug("Subscribing to %s, mid: %s", topic, mid) - _raise_on_error(result) - await self._wait_for_mid(mid) - - def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: - """On connect callback. - - Resubscribe to all topics we were subscribed to and publish birth - message. - """ - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - if result_code != mqtt.CONNACK_ACCEPTED: - _LOGGER.error( - "Unable to connect to the MQTT broker: %s", - mqtt.connack_string(result_code), - ) - return - - self.connected = True - dispatcher_send(self.hass, MQTT_CONNECTED) - _LOGGER.info( - "Connected to MQTT server %s:%s (%s)", - self.conf[CONF_BROKER], - self.conf[CONF_PORT], - result_code, - ) - - # Group subscriptions to only re-subscribe once for each topic. - keyfunc = attrgetter("topic") - for topic, subs in groupby(sorted(self.subscriptions, key=keyfunc), keyfunc): - # Re-subscribe with the highest requested qos - max_qos = max(subscription.qos for subscription in subs) - self.hass.add_job(self._async_perform_subscription, topic, max_qos) - - if ( - CONF_BIRTH_MESSAGE in self.conf - and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE] - ): - - async def publish_birth_message(birth_message): - await self._ha_started.wait() # Wait for Home Assistant to start - await self._discovery_cooldown() # Wait for MQTT discovery to cool down - await self.async_publish( - topic=birth_message.topic, - payload=birth_message.payload, - qos=birth_message.qos, - retain=birth_message.retain, - ) - - birth_message = PublishMessage(**self.conf[CONF_BIRTH_MESSAGE]) - asyncio.run_coroutine_threadsafe( - publish_birth_message(birth_message), self.hass.loop - ) - - def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: - """Message received callback.""" - self.hass.add_job(self._mqtt_handle_message, msg) - - @lru_cache(2048) - def _matching_subscriptions(self, topic): - subscriptions = [] - for subscription in self.subscriptions: - if subscription.matcher(topic): - subscriptions.append(subscription) - return subscriptions - - @callback - def _mqtt_handle_message(self, msg) -> None: - _LOGGER.debug( - "Received message on %s%s: %s", - msg.topic, - " (retained)" if msg.retain else "", - msg.payload[0:8192], - ) - timestamp = dt_util.utcnow() - - subscriptions = self._matching_subscriptions(msg.topic) - - for subscription in subscriptions: - - payload: SubscribePayloadType = msg.payload - if subscription.encoding is not None: - try: - payload = msg.payload.decode(subscription.encoding) - except (AttributeError, UnicodeDecodeError): - _LOGGER.warning( - "Can't decode payload %s on %s with encoding %s (for %s)", - msg.payload[0:8192], - msg.topic, - subscription.encoding, - subscription.job, - ) - continue - - self.hass.async_run_hass_job( - subscription.job, - ReceiveMessage( - msg.topic, - payload, - msg.qos, - msg.retain, - subscription.topic, - timestamp, - ), - ) - - def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None: - """Publish / Subscribe / Unsubscribe callback.""" - self.hass.add_job(self._mqtt_handle_mid, mid) - - @callback - def _mqtt_handle_mid(self, mid) -> None: - # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid - # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() - self._pending_operations[mid].set() - - def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: - """Disconnected callback.""" - self.connected = False - dispatcher_send(self.hass, MQTT_DISCONNECTED) - _LOGGER.warning( - "Disconnected from MQTT server %s:%s (%s)", - self.conf[CONF_BROKER], - self.conf[CONF_PORT], - result_code, - ) - - async def _wait_for_mid(self, mid): - """Wait for ACK from broker.""" - # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid - # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() - try: - await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) - except asyncio.TimeoutError: - _LOGGER.warning( - "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid - ) - finally: - del self._pending_operations[mid] - - async def _discovery_cooldown(self): - now = time.time() - # Reset discovery and subscribe cooldowns - self.hass.data[LAST_DISCOVERY] = now - self._last_subscribe = now - - last_discovery = self.hass.data[LAST_DISCOVERY] - last_subscribe = self._last_subscribe - wait_until = max( - last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN - ) - while now < wait_until: - await asyncio.sleep(wait_until - now) - now = time.time() - last_discovery = self.hass.data[LAST_DISCOVERY] - last_subscribe = self._last_subscribe - wait_until = max( - last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN - ) - - -def _raise_on_error(result_code: int | None) -> None: - """Raise error if error result.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - if result_code is not None and result_code != 0: - raise HomeAssistantError( - f"Error talking to MQTT: {mqtt.error_string(result_code)}" - ) - - -def _matcher_for_topic(subscription: str) -> Any: - # pylint: disable-next=import-outside-toplevel - from paho.mqtt.matcher import MQTTMatcher - - matcher = MQTTMatcher() - matcher[subscription] = True - - return lambda topic: next(matcher.iter_match(topic), False) - - @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 06c013ec744..c20fbb7c657 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -31,8 +31,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -50,6 +50,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -85,7 +87,7 @@ DEFAULT_NAME = "MQTT Alarm" REMOTE_CODE = "REMOTE_CODE" REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, @@ -94,7 +96,7 @@ PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( vol.Optional( CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE ): cv.template, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, 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, @@ -107,8 +109,8 @@ PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( ): cv.string, vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, vol.Optional(CONF_PAYLOAD_TRIGGER, default=DEFAULT_TRIGGER): cv.string, - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, - vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index b9ab190cc9b..1cb90d6c903 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -34,8 +34,8 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RO_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, PAYLOAD_NONE from .debug_info import log_messages from .mixins import ( @@ -47,6 +47,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate _LOGGER = logging.getLogger(__name__) @@ -57,7 +58,7 @@ DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False CONF_EXPIRE_AFTER = "expire_after" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 47e96ff3e1a..b50856d20c1 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -15,8 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate -from .. import mqtt +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -32,19 +31,21 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate +from .util import valid_publish_topic CONF_PAYLOAD_PRESS = "payload_press" DEFAULT_NAME = "MQTT Button" DEFAULT_PAYLOAD_PRESS = "PRESS" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): button.DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string, - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 2e5d95ebda4..ae38e07d17a 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription -from .. import mqtt +from .config import MQTT_BASE_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC from .debug_info import log_messages from .mixins import ( @@ -28,6 +28,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .util import valid_subscribe_topic DEFAULT_NAME = "MQTT Camera" @@ -40,10 +41,10 @@ MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( } ) -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_TOPIC): valid_subscribe_topic, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py new file mode 100644 index 00000000000..66699372516 --- /dev/null +++ b/homeassistant/components/mqtt/client.py @@ -0,0 +1,659 @@ +"""Support for MQTT message handling.""" +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +from functools import lru_cache, partial, wraps +import inspect +from itertools import groupby +import logging +from operator import attrgetter +import ssl +import time +from typing import TYPE_CHECKING, Any, Union, cast +import uuid + +import attr +import certifi + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STARTED, +) +from homeassistant.core import CoreState, HassJob, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import bind_hass +from homeassistant.util import dt as dt_util +from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.logging import catch_log_exception + +from .const import ( + ATTR_TOPIC, + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, + CONF_KEEPALIVE, + CONF_TLS_INSECURE, + CONF_WILL_MESSAGE, + DATA_MQTT, + DEFAULT_ENCODING, + DEFAULT_QOS, + MQTT_CONNECTED, + MQTT_DISCONNECTED, + PROTOCOL_31, +) +from .discovery import LAST_DISCOVERY +from .models import ( + AsyncMessageCallbackType, + MessageCallbackType, + PublishMessage, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) + +if TYPE_CHECKING: + # Only import for paho-mqtt type checking here, imports are done locally + # because integrations should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt + +_LOGGER = logging.getLogger(__name__) + +DISCOVERY_COOLDOWN = 2 +TIMEOUT_ACK = 10 + +SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None + + +def publish( + hass: HomeAssistant, + topic: str, + payload: PublishPayloadType, + qos: int | None = 0, + retain: bool | None = False, + encoding: str | None = DEFAULT_ENCODING, +) -> None: + """Publish message to a MQTT topic.""" + hass.add_job(async_publish, hass, topic, payload, qos, retain, encoding) + + +async def async_publish( + hass: HomeAssistant, + topic: str, + payload: PublishPayloadType, + qos: int | None = 0, + retain: bool | None = False, + encoding: str | None = DEFAULT_ENCODING, +) -> None: + """Publish message to a MQTT topic.""" + + outgoing_payload = payload + if not isinstance(payload, bytes): + if not encoding: + _LOGGER.error( + "Can't pass-through payload for publishing %s on %s with no encoding set, need 'bytes' got %s", + payload, + topic, + type(payload), + ) + return + outgoing_payload = str(payload) + if encoding != DEFAULT_ENCODING: + # a string is encoded as utf-8 by default, other encoding requires bytes as payload + try: + outgoing_payload = outgoing_payload.encode(encoding) + except (AttributeError, LookupError, UnicodeEncodeError): + _LOGGER.error( + "Can't encode payload for publishing %s on %s with encoding %s", + payload, + topic, + encoding, + ) + return + + await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain) + + +AsyncDeprecatedMessageCallbackType = Callable[ + [str, ReceivePayloadType, int], Awaitable[None] +] +DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None] + + +def wrap_msg_callback( + msg_callback: AsyncDeprecatedMessageCallbackType | DeprecatedMessageCallbackType, +) -> AsyncMessageCallbackType | MessageCallbackType: + """Wrap an MQTT message callback to support deprecated signature.""" + # Check for partials to properly determine if coroutine function + check_func = msg_callback + while isinstance(check_func, partial): + check_func = check_func.func + + wrapper_func: AsyncMessageCallbackType | MessageCallbackType + if asyncio.iscoroutinefunction(check_func): + + @wraps(msg_callback) + async def async_wrapper(msg: ReceiveMessage) -> None: + """Call with deprecated signature.""" + await cast(AsyncDeprecatedMessageCallbackType, msg_callback)( + msg.topic, msg.payload, msg.qos + ) + + wrapper_func = async_wrapper + else: + + @wraps(msg_callback) + def wrapper(msg: ReceiveMessage) -> None: + """Call with deprecated signature.""" + msg_callback(msg.topic, msg.payload, msg.qos) + + wrapper_func = wrapper + return wrapper_func + + +@bind_hass +async def async_subscribe( + hass: HomeAssistant, + topic: str, + msg_callback: AsyncMessageCallbackType + | MessageCallbackType + | DeprecatedMessageCallbackType + | AsyncDeprecatedMessageCallbackType, + qos: int = DEFAULT_QOS, + encoding: str | None = "utf-8", +): + """Subscribe to an MQTT topic. + + Call the return value to unsubscribe. + """ + # Count callback parameters which don't have a default value + non_default = 0 + if msg_callback: + non_default = sum( + p.default == inspect.Parameter.empty + for _, p in inspect.signature(msg_callback).parameters.items() + ) + + wrapped_msg_callback = msg_callback + # If we have 3 parameters with no default value, wrap the callback + if non_default == 3: + module = inspect.getmodule(msg_callback) + _LOGGER.warning( + "Signature of MQTT msg_callback '%s.%s' is deprecated", + module.__name__ if module else "", + msg_callback.__name__, + ) + wrapped_msg_callback = wrap_msg_callback( + cast(DeprecatedMessageCallbackType, msg_callback) + ) + + async_remove = await hass.data[DATA_MQTT].async_subscribe( + topic, + catch_log_exception( + wrapped_msg_callback, + lambda msg: ( + f"Exception in {msg_callback.__name__} when handling msg on " + f"'{msg.topic}': '{msg.payload}'" + ), + ), + qos, + encoding, + ) + return async_remove + + +@bind_hass +def subscribe( + hass: HomeAssistant, + topic: str, + msg_callback: MessageCallbackType, + qos: int = DEFAULT_QOS, + encoding: str = "utf-8", +) -> Callable[[], None]: + """Subscribe to an MQTT topic.""" + async_remove = asyncio.run_coroutine_threadsafe( + async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop + ).result() + + def remove(): + """Remove listener convert.""" + run_callback_threadsafe(hass.loop, async_remove).result() + + return remove + + +@attr.s(slots=True, frozen=True) +class Subscription: + """Class to hold data about an active subscription.""" + + topic: str = attr.ib() + matcher: Any = attr.ib() + job: HassJob = attr.ib() + qos: int = attr.ib(default=0) + encoding: str | None = attr.ib(default="utf-8") + + +class MqttClientSetup: + """Helper class to setup the paho mqtt client from config.""" + + def __init__(self, config: ConfigType) -> None: + """Initialize the MQTT client setup helper.""" + + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + + if config[CONF_PROTOCOL] == PROTOCOL_31: + proto = mqtt.MQTTv31 + else: + proto = mqtt.MQTTv311 + + if (client_id := config.get(CONF_CLIENT_ID)) is None: + # PAHO MQTT relies on the MQTT server to generate random client IDs. + # However, that feature is not mandatory so we generate our own. + client_id = mqtt.base62(uuid.uuid4().int, padding=22) + self._client = mqtt.Client(client_id, protocol=proto) + + # Enable logging + self._client.enable_logger() + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + if username is not None: + self._client.username_pw_set(username, password) + + if (certificate := config.get(CONF_CERTIFICATE)) == "auto": + certificate = certifi.where() + + client_key = config.get(CONF_CLIENT_KEY) + client_cert = config.get(CONF_CLIENT_CERT) + tls_insecure = config.get(CONF_TLS_INSECURE) + if certificate is not None: + self._client.tls_set( + certificate, + certfile=client_cert, + keyfile=client_key, + tls_version=ssl.PROTOCOL_TLS, + ) + + if tls_insecure is not None: + self._client.tls_insecure_set(tls_insecure) + + @property + def client(self) -> mqtt.Client: + """Return the paho MQTT client.""" + return self._client + + +class MQTT: + """Home Assistant MQTT client.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry, + conf, + ) -> None: + """Initialize Home Assistant MQTT client.""" + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + + self.hass = hass + self.config_entry = config_entry + self.conf = conf + self.subscriptions: list[Subscription] = [] + self.connected = False + self._ha_started = asyncio.Event() + self._last_subscribe = time.time() + self._mqttc: mqtt.Client = None + self._paho_lock = asyncio.Lock() + + self._pending_operations: dict[str, asyncio.Event] = {} + + if self.hass.state == CoreState.running: + self._ha_started.set() + else: + + @callback + def ha_started(_): + self._ha_started.set() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) + + self.init_client() + + def init_client(self): + """Initialize paho client.""" + self._mqttc = MqttClientSetup(self.conf).client + self._mqttc.on_connect = self._mqtt_on_connect + self._mqttc.on_disconnect = self._mqtt_on_disconnect + self._mqttc.on_message = self._mqtt_on_message + self._mqttc.on_publish = self._mqtt_on_callback + self._mqttc.on_subscribe = self._mqtt_on_callback + self._mqttc.on_unsubscribe = self._mqtt_on_callback + + if ( + CONF_WILL_MESSAGE in self.conf + and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE] + ): + will_message = PublishMessage(**self.conf[CONF_WILL_MESSAGE]) + else: + will_message = None + + if will_message is not None: + self._mqttc.will_set( + topic=will_message.topic, + payload=will_message.payload, + qos=will_message.qos, + retain=will_message.retain, + ) + + async def async_publish( + self, topic: str, payload: PublishPayloadType, qos: int, retain: bool + ) -> None: + """Publish a MQTT message.""" + async with self._paho_lock: + msg_info = await self.hass.async_add_executor_job( + self._mqttc.publish, topic, payload, qos, retain + ) + _LOGGER.debug( + "Transmitting message on %s: '%s', mid: %s", + topic, + payload, + msg_info.mid, + ) + _raise_on_error(msg_info.rc) + await self._wait_for_mid(msg_info.mid) + + async def async_connect(self) -> None: + """Connect to the host. Does not process messages yet.""" + # pylint: disable-next=import-outside-toplevel + import paho.mqtt.client as mqtt + + result: int | None = None + try: + result = await self.hass.async_add_executor_job( + self._mqttc.connect, + self.conf[CONF_BROKER], + self.conf[CONF_PORT], + self.conf[CONF_KEEPALIVE], + ) + except OSError as err: + _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) + + if result is not None and result != 0: + _LOGGER.error( + "Failed to connect to MQTT server: %s", mqtt.error_string(result) + ) + + self._mqttc.loop_start() + + async def async_disconnect(self): + """Stop the MQTT client.""" + + def stop(): + """Stop the MQTT client.""" + # Do not disconnect, we want the broker to always publish will + self._mqttc.loop_stop() + + await self.hass.async_add_executor_job(stop) + + async def async_subscribe( + self, + topic: str, + msg_callback: MessageCallbackType, + qos: int, + encoding: str | None = None, + ) -> Callable[[], None]: + """Set up a subscription to a topic with the provided qos. + + This method is a coroutine. + """ + if not isinstance(topic, str): + raise HomeAssistantError("Topic needs to be a string!") + + subscription = Subscription( + topic, _matcher_for_topic(topic), HassJob(msg_callback), qos, encoding + ) + self.subscriptions.append(subscription) + self._matching_subscriptions.cache_clear() + + # Only subscribe if currently connected. + if self.connected: + self._last_subscribe = time.time() + await self._async_perform_subscription(topic, qos) + + @callback + def async_remove() -> None: + """Remove subscription.""" + if subscription not in self.subscriptions: + raise HomeAssistantError("Can't remove subscription twice") + self.subscriptions.remove(subscription) + self._matching_subscriptions.cache_clear() + + # Only unsubscribe if currently connected. + if self.connected: + self.hass.async_create_task(self._async_unsubscribe(topic)) + + return async_remove + + async def _async_unsubscribe(self, topic: str) -> None: + """Unsubscribe from a topic. + + This method is a coroutine. + """ + if any(other.topic == topic for other in self.subscriptions): + # Other subscriptions on topic remaining - don't unsubscribe. + return + + async with self._paho_lock: + result: int | None = None + result, mid = await self.hass.async_add_executor_job( + self._mqttc.unsubscribe, topic + ) + _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) + _raise_on_error(result) + await self._wait_for_mid(mid) + + async def _async_perform_subscription(self, topic: str, qos: int) -> None: + """Perform a paho-mqtt subscription.""" + async with self._paho_lock: + result: int | None = None + result, mid = await self.hass.async_add_executor_job( + self._mqttc.subscribe, topic, qos + ) + _LOGGER.debug("Subscribing to %s, mid: %s", topic, mid) + _raise_on_error(result) + await self._wait_for_mid(mid) + + def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: + """On connect callback. + + Resubscribe to all topics we were subscribed to and publish birth + message. + """ + # pylint: disable-next=import-outside-toplevel + import paho.mqtt.client as mqtt + + if result_code != mqtt.CONNACK_ACCEPTED: + _LOGGER.error( + "Unable to connect to the MQTT broker: %s", + mqtt.connack_string(result_code), + ) + return + + self.connected = True + dispatcher_send(self.hass, MQTT_CONNECTED) + _LOGGER.info( + "Connected to MQTT server %s:%s (%s)", + self.conf[CONF_BROKER], + self.conf[CONF_PORT], + result_code, + ) + + # Group subscriptions to only re-subscribe once for each topic. + keyfunc = attrgetter("topic") + for topic, subs in groupby(sorted(self.subscriptions, key=keyfunc), keyfunc): + # Re-subscribe with the highest requested qos + max_qos = max(subscription.qos for subscription in subs) + self.hass.add_job(self._async_perform_subscription, topic, max_qos) + + if ( + CONF_BIRTH_MESSAGE in self.conf + and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE] + ): + + async def publish_birth_message(birth_message): + await self._ha_started.wait() # Wait for Home Assistant to start + await self._discovery_cooldown() # Wait for MQTT discovery to cool down + await self.async_publish( + topic=birth_message.topic, + payload=birth_message.payload, + qos=birth_message.qos, + retain=birth_message.retain, + ) + + birth_message = PublishMessage(**self.conf[CONF_BIRTH_MESSAGE]) + asyncio.run_coroutine_threadsafe( + publish_birth_message(birth_message), self.hass.loop + ) + + def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: + """Message received callback.""" + self.hass.add_job(self._mqtt_handle_message, msg) + + @lru_cache(2048) + def _matching_subscriptions(self, topic): + subscriptions = [] + for subscription in self.subscriptions: + if subscription.matcher(topic): + subscriptions.append(subscription) + return subscriptions + + @callback + def _mqtt_handle_message(self, msg) -> None: + _LOGGER.debug( + "Received message on %s%s: %s", + msg.topic, + " (retained)" if msg.retain else "", + msg.payload[0:8192], + ) + timestamp = dt_util.utcnow() + + subscriptions = self._matching_subscriptions(msg.topic) + + for subscription in subscriptions: + + payload: SubscribePayloadType = msg.payload + if subscription.encoding is not None: + try: + payload = msg.payload.decode(subscription.encoding) + except (AttributeError, UnicodeDecodeError): + _LOGGER.warning( + "Can't decode payload %s on %s with encoding %s (for %s)", + msg.payload[0:8192], + msg.topic, + subscription.encoding, + subscription.job, + ) + continue + + self.hass.async_run_hass_job( + subscription.job, + ReceiveMessage( + msg.topic, + payload, + msg.qos, + msg.retain, + subscription.topic, + timestamp, + ), + ) + + def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None: + """Publish / Subscribe / Unsubscribe callback.""" + self.hass.add_job(self._mqtt_handle_mid, mid) + + @callback + def _mqtt_handle_mid(self, mid) -> None: + # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid + # may be executed first. + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() + self._pending_operations[mid].set() + + def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: + """Disconnected callback.""" + self.connected = False + dispatcher_send(self.hass, MQTT_DISCONNECTED) + _LOGGER.warning( + "Disconnected from MQTT server %s:%s (%s)", + self.conf[CONF_BROKER], + self.conf[CONF_PORT], + result_code, + ) + + async def _wait_for_mid(self, mid): + """Wait for ACK from broker.""" + # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid + # may be executed first. + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() + try: + await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) + except asyncio.TimeoutError: + _LOGGER.warning( + "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid + ) + finally: + del self._pending_operations[mid] + + async def _discovery_cooldown(self): + now = time.time() + # Reset discovery and subscribe cooldowns + self.hass.data[LAST_DISCOVERY] = now + self._last_subscribe = now + + last_discovery = self.hass.data[LAST_DISCOVERY] + last_subscribe = self._last_subscribe + wait_until = max( + last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN + ) + while now < wait_until: + await asyncio.sleep(wait_until - now) + now = time.time() + last_discovery = self.hass.data[LAST_DISCOVERY] + last_subscribe = self._last_subscribe + wait_until = max( + last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN + ) + + +def _raise_on_error(result_code: int | None) -> None: + """Raise error if error result.""" + # pylint: disable-next=import-outside-toplevel + import paho.mqtt.client as mqtt + + if result_code is not None and result_code != 0: + raise HomeAssistantError( + f"Error talking to MQTT: {mqtt.error_string(result_code)}" + ) + + +def _matcher_for_topic(subscription: str) -> Any: + # pylint: disable-next=import-outside-toplevel + from paho.mqtt.matcher import MQTTMatcher + + matcher = MQTTMatcher() + matcher[subscription] = True + + return lambda topic: next(matcher.iter_match(topic), False) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 52465bbba24..64b462359be 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -44,8 +44,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, PAYLOAD_NONE from .debug_info import log_messages from .mixins import ( @@ -56,6 +56,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -232,33 +234,33 @@ def valid_preset_mode_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic, # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, - vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_CURRENT_TEMP_TOPIC): valid_subscribe_topic, vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_FAN_MODE_LIST, default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_FAN_MODE_STATE_TOPIC): valid_subscribe_topic, # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_HOLD_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HOLD_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_HOLD_LIST): cv.ensure_list, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_MODE_LIST, default=[ @@ -271,54 +273,54 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( ], ): cv.ensure_list, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, - vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_POWER_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PRECISION): vol.In( [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 vol.Optional(CONF_SEND_IF_OFF): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, - vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_ACTION_TOPIC): valid_subscribe_topic, # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together vol.Inclusive( CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" - ): mqtt.valid_publish_topic, + ): valid_publish_topic, vol.Inclusive( CONF_PRESET_MODES_LIST, "preset_modes", default=[] ): cv.ensure_list, vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_SWING_MODE_LIST, default=[SWING_ON, SWING_OFF] ): cv.ensure_list, vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SWING_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMP_INITIAL, default=21): cv.positive_int, vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_TEMP_HIGH_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_LOW_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py new file mode 100644 index 00000000000..4f84d911418 --- /dev/null +++ b/homeassistant/components/mqtt/config.py @@ -0,0 +1,148 @@ +"""Support for MQTT message handling.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, + CONF_VALUE_TEMPLATE, +) +from homeassistant.helpers import config_validation as cv + +from .const import ( + ATTR_PAYLOAD, + ATTR_QOS, + ATTR_RETAIN, + ATTR_TOPIC, + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, + CONF_COMMAND_TOPIC, + CONF_DISCOVERY_PREFIX, + CONF_ENCODING, + CONF_KEEPALIVE, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + CONF_TLS_INSECURE, + CONF_TLS_VERSION, + CONF_WILL_MESSAGE, + DEFAULT_BIRTH, + DEFAULT_DISCOVERY, + DEFAULT_ENCODING, + DEFAULT_PREFIX, + DEFAULT_QOS, + DEFAULT_RETAIN, + DEFAULT_WILL, + PLATFORMS, + PROTOCOL_31, + PROTOCOL_311, +) +from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic + +DEFAULT_PORT = 1883 +DEFAULT_KEEPALIVE = 60 +DEFAULT_PROTOCOL = PROTOCOL_311 +DEFAULT_TLS_PROTOCOL = "auto" + +DEFAULT_VALUES = { + CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, + CONF_DISCOVERY: DEFAULT_DISCOVERY, + CONF_PORT: DEFAULT_PORT, + CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, + CONF_WILL_MESSAGE: DEFAULT_WILL, +} + +CLIENT_KEY_AUTH_MSG = ( + "client_key and client_cert must both be present in " + "the MQTT broker configuration" +) + +MQTT_WILL_BIRTH_SCHEMA = vol.Schema( + { + vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, + vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + }, + required=True, +) + +PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( + {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} +) + +CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( + { + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( + vol.Coerce(int), vol.Range(min=15) + ), + vol.Optional(CONF_BROKER): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), + vol.Inclusive( + CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Inclusive( + CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Optional(CONF_TLS_INSECURE): cv.boolean, + vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( + cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) + ), + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. + vol.Optional( + CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX + ): valid_publish_topic, + } +) + +DEPRECATED_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_TLS_VERSION, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] + +SCHEMA_BASE = { + vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, +} + +MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) + +# Sensor type platforms subscribe to MQTT events +MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( + { + vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) + +# Switch type platforms publish to MQTT and may subscribe +MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( + { + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, + } +) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 6697b17dfdc..7137b93118f 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResult -from . import MqttClientSetup +from .client import MqttClientSetup from .const import ( ATTR_PAYLOAD, ATTR_QOS, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 106d0310158..2f7e27e7252 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -1,5 +1,5 @@ """Constants used by multiple MQTT modules.""" -from homeassistant.const import CONF_PAYLOAD +from homeassistant.const import CONF_PAYLOAD, Platform ATTR_DISCOVERY_HASH = "discovery_hash" ATTR_DISCOVERY_PAYLOAD = "discovery_payload" @@ -14,7 +14,9 @@ CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" CONF_COMMAND_TEMPLATE = "command_template" CONF_COMMAND_TOPIC = "command_topic" +CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_ENCODING = "encoding" +CONF_KEEPALIVE = "keepalive" CONF_QOS = ATTR_QOS CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" @@ -30,6 +32,7 @@ CONF_TLS_VERSION = "tls_version" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" +DATA_MQTT = "mqtt" DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" @@ -66,3 +69,24 @@ PAYLOAD_NONE = "None" PROTOCOL_31 = "3.1" PROTOCOL_311 = "3.1.1" + +PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.CLIMATE, + Platform.DEVICE_TRACKER, + Platform.COVER, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.LIGHT, + Platform.LOCK, + Platform.NUMBER, + Platform.SELECT, + Platform.SCENE, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, + Platform.VACUUM, +] diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 8e36329946a..5814f3e43f7 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -33,8 +33,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_BASE_SCHEMA from .const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -51,6 +51,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -152,11 +154,11 @@ def validate_options(value): return value -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_GET_POSITION_TOPIC): valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): vol.Any( @@ -172,24 +174,24 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_POSITION_OPEN, default=DEFAULT_POSITION_OPEN): int, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template, - vol.Optional(CONF_SET_POSITION_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SET_POSITION_TOPIC): valid_publish_topic, vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string, vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string, vol.Optional(CONF_STATE_STOPPED, default=DEFAULT_STATE_STOPPED): cv.string, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional( CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION ): int, - vol.Optional(CONF_TILT_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TILT_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int, vol.Optional(CONF_TILT_MIN, default=DEFAULT_TILT_MIN): int, vol.Optional(CONF_TILT_OPEN_POSITION, default=DEFAULT_TILT_OPEN_POSITION): int, vol.Optional( CONF_TILT_STATE_OPTIMISTIC, default=DEFAULT_TILT_OPTIMISTIC ): cv.boolean, - vol.Optional(CONF_TILT_STATUS_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TILT_STATUS_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TILT_STATUS_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_GET_POSITION_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 002ae6e3991..0646a5bda0c 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -6,7 +6,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from . import device_trigger -from .. import mqtt +from .config import MQTT_BASE_SCHEMA from .mixins import async_setup_entry_helper AUTOMATION_TYPE_TRIGGER = "trigger" @@ -17,7 +17,7 @@ CONF_AUTOMATION_TYPE = "automation_type" PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( {vol.Required(CONF_AUTOMATION_TYPE): AUTOMATION_TYPES_SCHEMA}, extra=vol.ALLOW_EXTRA, -).extend(mqtt.MQTT_BASE_SCHEMA.schema) +).extend(MQTT_BASE_SCHEMA.schema) async def async_setup_entry(hass, config_entry): diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index aa7506bd5e3..1b48e15b80e 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -19,8 +19,8 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .. import MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_RO_SCHEMA from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import ( @@ -29,12 +29,13 @@ from ..mixins import ( async_get_platform_config_from_yaml, async_setup_entry_helper, ) +from ..models import MqttValueTemplate CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, diff --git a/homeassistant/components/mqtt/device_tracker/schema_yaml.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py index f871ac89c2d..2dfa5b7134c 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_yaml.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -7,16 +7,18 @@ from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from ... import mqtt +from ..client import async_subscribe +from ..config import SCHEMA_BASE from ..const import CONF_QOS +from ..util import valid_subscribe_topic CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( +PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend( { - vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, + vol.Required(CONF_DEVICES): {cv.string: valid_subscribe_topic}, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), @@ -50,6 +52,6 @@ async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info= hass.async_create_task(async_see(**see_args)) - await mqtt.async_subscribe(hass, topic, async_message_received, qos) + await async_subscribe(hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 2c6c6ecc3ba..0b4bcbfcbc2 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -29,7 +29,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from . import debug_info, trigger as mqtt_trigger -from .. import mqtt +from .config import MQTT_BASE_SCHEMA from .const import ( ATTR_DISCOVERY_HASH, CONF_ENCODING, @@ -71,7 +71,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) -TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( +TRIGGER_DISCOVERY_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_AUTOMATION_TYPE): str, vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -101,7 +101,7 @@ class TriggerInstance: async def async_attach_trigger(self) -> None: """Attach MQTT trigger.""" mqtt_config = { - CONF_PLATFORM: mqtt.DOMAIN, + CONF_PLATFORM: DOMAIN, CONF_TOPIC: self.trigger.topic, CONF_ENCODING: DEFAULT_ENCODING, CONF_QOS: self.trigger.qos, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f2b738cd2bb..f72b0bdf689 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -34,8 +34,8 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -55,6 +55,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" @@ -125,28 +127,28 @@ def valid_preset_mode_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_OSCILLATION_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_OSCILLATION_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_OSCILLATION_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_PERCENTAGE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_PERCENTAGE_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PERCENTAGE_VALUE_TEMPLATE): cv.template, # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together vol.Inclusive( CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" - ): mqtt.valid_publish_topic, + ): valid_publish_topic, vol.Inclusive( CONF_PRESET_MODES_LIST, "preset_modes", default=[] ): cv.ensure_list, vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template, vol.Optional( CONF_SPEED_RANGE_MIN, default=DEFAULT_SPEED_RANGE_MIN @@ -168,8 +170,8 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( vol.Optional( CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD ): cv.string, - vol.Optional(CONF_SPEED_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_SPEED_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SPEED_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_SPEED_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, } diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index f6d4aa01dab..000a9b9700e 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -30,8 +30,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -51,6 +51,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic CONF_AVAILABLE_MODES_LIST = "modes" CONF_DEVICE_CLASS = "device_class" @@ -103,15 +105,13 @@ def valid_humidity_range_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { # CONF_AVAIALABLE_MODES_LIST and CONF_MODE_COMMAND_TOPIC must be used together vol.Inclusive( CONF_AVAILABLE_MODES_LIST, "available_modes", default=[] ): cv.ensure_list, - vol.Inclusive( - CONF_MODE_COMMAND_TOPIC, "available_modes" - ): mqtt.valid_publish_topic, + vol.Inclusive(CONF_MODE_COMMAND_TOPIC, "available_modes"): valid_publish_topic, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional( CONF_DEVICE_CLASS, default=HumidifierDeviceClass.HUMIDIFIER @@ -119,14 +119,14 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( [HumidifierDeviceClass.HUMIDIFIER, HumidifierDeviceClass.DEHUMIDIFIER] ), vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, - vol.Required(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Required(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE): cv.template, vol.Optional( CONF_TARGET_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY @@ -135,7 +135,7 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( CONF_TARGET_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY ): cv.positive_int, vol.Optional(CONF_TARGET_HUMIDITY_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): valid_subscribe_topic, vol.Optional( CONF_PAYLOAD_RESET_HUMIDITY, default=DEFAULT_PAYLOAD_RESET ): cv.string, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index eb4ec264981..1c94fa82f73 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -42,8 +42,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import MqttCommandTemplate, MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_RW_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -55,6 +55,8 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..models import MqttCommandTemplate, MqttValueTemplate +from ..util import valid_publish_topic, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -156,28 +158,28 @@ VALUE_TEMPLATE_KEYS = [ ] _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_SCHEMA.extend( + MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_COLOR_MODE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COLOR_TEMP_COMMAND_TEMPLATE): cv.template, - 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_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_EFFECT_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_EFFECT_COMMAND_TOPIC): 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, + vol.Optional(CONF_EFFECT_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_HS_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_HS_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HS_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_HS_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, @@ -189,30 +191,30 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_RGB_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGB_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RGB_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_RGBW_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_RGBW_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RGBW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBW_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RGBW_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_RGBW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_RGBWW_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_RGBWW_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBWW_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RGBWW_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_WHITE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_WHITE_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( vol.Coerce(int), vol.Range(min=1) ), - vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_XY_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_XY_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_XY_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_XY_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, }, ) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 2049818ab31..be49f1ad2e3 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -51,7 +51,7 @@ from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util from .. import subscription -from ... import mqtt +from ..config import DEFAULT_QOS, DEFAULT_RETAIN, MQTT_RW_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -61,6 +61,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..util import valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE, MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -103,7 +104,7 @@ def valid_color_configuration(config): _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_SCHEMA.extend( + MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean, vol.Optional( @@ -126,12 +127,12 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_MIN_MIREDS): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.All( + vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) ), - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All( cv.ensure_list, [vol.In(VALID_COLOR_MODES)], diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 0165bfc8efa..779f2f17e24 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -31,8 +31,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_RW_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -43,6 +43,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..models import MqttValueTemplate from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -67,7 +68,7 @@ CONF_RED_TEMPLATE = "red_template" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_SCHEMA.extend( + MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BLUE_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 5dc0a974d26..0cfd1d2b70f 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -15,8 +15,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -33,6 +33,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" @@ -56,7 +57,7 @@ MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( } ) -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index a46debeae54..694fae0b3c0 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -49,14 +49,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import ( - DATA_MQTT, - PLATFORMS, - MqttValueTemplate, - async_publish, - debug_info, - subscription, -) +from . import debug_info, subscription +from .client import async_publish from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, @@ -65,6 +59,7 @@ from .const import ( CONF_ENCODING, CONF_QOS, CONF_TOPIC, + DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, DEFAULT_ENCODING, @@ -73,6 +68,7 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, + PLATFORMS, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -82,7 +78,7 @@ from .discovery import ( clear_discovery_hash, set_discovery_hash, ) -from .models import PublishPayloadType, ReceiveMessage +from .models import MqttValueTemplate, PublishPayloadType, ReceiveMessage from .subscription import ( async_prepare_subscribe_topics, async_subscribe_topics, diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 9cec65d7254..9bce6baab8b 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -1,12 +1,21 @@ """Models used by multiple MQTT modules.""" from __future__ import annotations +from ast import literal_eval from collections.abc import Awaitable, Callable import datetime as dt -from typing import Union +from typing import Any, Union import attr +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import template +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import TemplateVarsType + +_SENTINEL = object() + PublishPayloadType = Union[str, bytes, int, float, None] ReceivePayloadType = Union[str, bytes] @@ -35,3 +44,118 @@ class ReceiveMessage: AsyncMessageCallbackType = Callable[[ReceiveMessage], Awaitable[None]] MessageCallbackType = Callable[[ReceiveMessage], None] + + +class MqttCommandTemplate: + """Class for rendering MQTT payload with command templates.""" + + def __init__( + self, + command_template: template.Template | None, + *, + hass: HomeAssistant | None = None, + entity: Entity | None = None, + ) -> None: + """Instantiate a command template.""" + self._attr_command_template = command_template + if command_template is None: + return + + self._entity = entity + + command_template.hass = hass + + if entity: + command_template.hass = entity.hass + + @callback + def async_render( + self, + value: PublishPayloadType = None, + variables: TemplateVarsType = None, + ) -> PublishPayloadType: + """Render or convert the command template with given value or variables.""" + + def _convert_outgoing_payload( + payload: PublishPayloadType, + ) -> PublishPayloadType: + """Ensure correct raw MQTT payload is passed as bytes for publishing.""" + if isinstance(payload, str): + try: + native_object = literal_eval(payload) + if isinstance(native_object, bytes): + return native_object + + except (ValueError, TypeError, SyntaxError, MemoryError): + pass + + return payload + + if self._attr_command_template is None: + return value + + values = {"value": value} + if self._entity: + values[ATTR_ENTITY_ID] = self._entity.entity_id + values[ATTR_NAME] = self._entity.name + if variables is not None: + values.update(variables) + return _convert_outgoing_payload( + self._attr_command_template.async_render(values, parse_result=False) + ) + + +class MqttValueTemplate: + """Class for rendering MQTT value template with possible json values.""" + + def __init__( + self, + value_template: template.Template | None, + *, + hass: HomeAssistant | None = None, + entity: Entity | None = None, + config_attributes: TemplateVarsType = None, + ) -> None: + """Instantiate a value template.""" + self._value_template = value_template + self._config_attributes = config_attributes + if value_template is None: + return + + value_template.hass = hass + self._entity = entity + + if entity: + value_template.hass = entity.hass + + @callback + def async_render_with_possible_json_value( + self, + payload: ReceivePayloadType, + default: ReceivePayloadType | object = _SENTINEL, + variables: TemplateVarsType = None, + ) -> ReceivePayloadType: + """Render with possible json value or pass-though a received MQTT value.""" + if self._value_template is None: + return payload + + values: dict[str, Any] = {} + + if variables is not None: + values.update(variables) + + if self._config_attributes is not None: + values.update(self._config_attributes) + + if self._entity: + values[ATTR_ENTITY_ID] = self._entity.entity_id + values[ATTR_NAME] = self._entity.name + + if default == _SENTINEL: + return self._value_template.async_render_with_possible_json_value( + payload, variables=values + ) + + return self._value_template.async_render_with_possible_json_value( + payload, default, variables=values + ) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 001f9f4f668..6ea1f0959f6 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -27,8 +27,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -46,6 +46,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate _LOGGER = logging.getLogger(__name__) @@ -75,7 +76,7 @@ def validate_config(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 98c692ceaff..ce8f0b0a3e8 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -15,7 +15,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .. import mqtt +from .client import async_publish +from .config import MQTT_BASE_SCHEMA from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from .mixins import ( CONF_ENABLED_BY_DEFAULT, @@ -27,13 +28,14 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .util import valid_publish_topic DEFAULT_NAME = "MQTT Scene" DEFAULT_RETAIN = False -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON): cv.string, @@ -128,7 +130,7 @@ class MqttScene( This method is a coroutine. """ - await mqtt.async_publish( + await async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON], diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 0765eb7f176..75e1b4e8efd 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -17,8 +17,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -36,6 +36,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,7 @@ MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset( ) -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index d865d90c4ee..4dd1ad4d95f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -34,8 +34,8 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RO_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC from .debug_info import log_messages from .mixins import ( @@ -47,6 +47,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate +from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -89,12 +91,12 @@ def validate_options(conf): return conf -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RO_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( { 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, - vol.Optional(CONF_LAST_RESET_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_LAST_RESET_TOPIC): valid_subscribe_topic, vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index c3a41c3618e..1ecf2c37dbf 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -35,8 +35,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -57,6 +57,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate DEFAULT_NAME = "MQTT Siren" DEFAULT_PAYLOAD_ON = "ON" @@ -74,7 +75,7 @@ CONF_SUPPORT_VOLUME_SET = "support_volume_set" STATE = "state" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_AVAILABLE_TONES): cv.ensure_list, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index f5f8363eb33..c20ddfe5151 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -24,8 +24,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -43,6 +43,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" @@ -51,7 +52,7 @@ DEFAULT_OPTIMISTIC = False CONF_STATE_ON = "state_on" CONF_STATE_OFF = "state_off" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 25e49524b8f..9452d5fc259 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -11,8 +11,8 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_BASE_SCHEMA from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -21,7 +21,7 @@ from .mixins import ( send_discovery_done, update_device, ) -from .models import ReceiveMessage +from .models import MqttValueTemplate, ReceiveMessage from .subscription import EntitySubscription from .util import valid_subscribe_topic @@ -30,7 +30,7 @@ LOG_NAME = "Tag" TAG = "tag" TAGS = "mqtt_tags" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_PLATFORM): "mqtt", diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index eb5e01b6251..f25131c43b7 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -15,11 +15,13 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from .. import MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_BASE_SCHEMA from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema +from ..models import MqttValueTemplate +from ..util import valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -96,25 +98,23 @@ MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED = MQTT_VACUUM_ATTRIBUTES_BLOCKED | frozens ) PLATFORM_SCHEMA_LEGACY_MODERN = ( - mqtt.MQTT_BASE_SCHEMA.extend( + MQTT_BASE_SCHEMA.extend( { vol.Inclusive(CONF_BATTERY_LEVEL_TEMPLATE, "battery"): cv.template, - vol.Inclusive( - CONF_BATTERY_LEVEL_TOPIC, "battery" - ): mqtt.valid_publish_topic, + vol.Inclusive(CONF_BATTERY_LEVEL_TOPIC, "battery"): valid_publish_topic, vol.Inclusive(CONF_CHARGING_TEMPLATE, "charging"): cv.template, - vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): valid_publish_topic, vol.Inclusive(CONF_CLEANING_TEMPLATE, "cleaning"): cv.template, - vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): valid_publish_topic, vol.Inclusive(CONF_DOCKED_TEMPLATE, "docked"): cv.template, - vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): valid_publish_topic, vol.Inclusive(CONF_ERROR_TEMPLATE, "error"): cv.template, - vol.Inclusive(CONF_ERROR_TOPIC, "error"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_ERROR_TOPIC, "error"): valid_publish_topic, vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] ), vol.Inclusive(CONF_FAN_SPEED_TEMPLATE, "fan_speed"): cv.template, - vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): valid_publish_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional( CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT @@ -135,12 +135,12 @@ PLATFORM_SCHEMA_LEGACY_MODERN = ( vol.Optional( CONF_PAYLOAD_TURN_ON, default=DEFAULT_PAYLOAD_TURN_ON ): cv.string, - vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic, vol.Optional( CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 7aa7be07797..3d670780994 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -23,7 +23,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from .. import subscription -from ... import mqtt +from ..config import MQTT_BASE_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -33,6 +33,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema +from ..util import valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -105,7 +106,7 @@ DEFAULT_PAYLOAD_START = "start" DEFAULT_PAYLOAD_PAUSE = "pause" PLATFORM_SCHEMA_STATE_MODERN = ( - mqtt.MQTT_BASE_SCHEMA.extend( + MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] @@ -123,13 +124,13 @@ PLATFORM_SCHEMA_STATE_MODERN = ( vol.Optional(CONF_PAYLOAD_START, default=DEFAULT_PAYLOAD_START): cv.string, vol.Optional(CONF_PAYLOAD_PAUSE, default=DEFAULT_PAYLOAD_PAUSE): cv.string, vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, - vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic, + vol.Optional(CONF_STATE_TOPIC): valid_publish_topic, vol.Optional( CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) @@ -178,7 +179,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): supported_feature_strings, STRING_TO_SERVICE ) self._fan_speed_list = config[CONF_FAN_SPEED_LIST] - self._command_topic = config.get(mqtt.CONF_COMMAND_TOPIC) + self._command_topic = config.get(CONF_COMMAND_TOPIC) self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) self._send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index 505fa3bd809..1d99e6d7b6f 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -35,7 +35,7 @@ GPS_JSON_PAYLOAD_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.config.SCHEMA_BASE).extend( {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}} ) diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 54de561c11e..276695d8edd 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_AWAY_TIMEOUT, default=DEFAULT_AWAY_TIMEOUT): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, } -).extend(mqtt.MQTT_RO_SCHEMA.schema) +).extend(mqtt.config.MQTT_RO_SCHEMA.schema) MQTT_PAYLOAD = vol.Schema( vol.All( diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 285af765ab4..e130b820c1b 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, ) -from homeassistant.components.mqtt import CONF_STATE_TOPIC +from homeassistant.components.mqtt.const import CONF_STATE_TOPIC from homeassistant.components.mqtt.cover import ( CONF_GET_POSITION_TEMPLATE, CONF_GET_POSITION_TOPIC, diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 07c39d70df0..aa0bfb82608 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1123,7 +1123,7 @@ async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_m assert mqtt_client_mock.subscribe.call_count == 1 mqtt_client_mock.on_disconnect(None, None, 0) - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0): mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() assert mqtt_client_mock.subscribe.call_count == 2 @@ -1157,7 +1157,7 @@ async def test_restore_all_active_subscriptions_on_reconnect( assert mqtt_client_mock.unsubscribe.call_count == 0 mqtt_client_mock.on_disconnect(None, None, 0) - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0): mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() @@ -1188,7 +1188,7 @@ async def test_logs_error_if_no_connect_broker( ) -@patch("homeassistant.components.mqtt.TIMEOUT_ACK", 0.3) +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) async def test_handle_mqtt_on_callback(hass, caplog, mqtt_mock, mqtt_client_mock): """Test receiving an ACK callback before waiting for it.""" # Simulate an ACK for mid == 1, this will call mqtt_mock._mqtt_handle_mid(mid) @@ -1331,7 +1331,7 @@ async def test_setup_mqtt_client_protocol(hass): """Test MQTT client protocol setup.""" entry = MockConfigEntry( domain=mqtt.DOMAIN, - data={mqtt.CONF_BROKER: "test-broker", mqtt.CONF_PROTOCOL: "3.1"}, + data={mqtt.CONF_BROKER: "test-broker", mqtt.config.CONF_PROTOCOL: "3.1"}, ) with patch("paho.mqtt.client.Client") as mock_client: mock_client.on_connect(return_value=0) @@ -1341,7 +1341,7 @@ async def test_setup_mqtt_client_protocol(hass): assert mock_client.call_args[1]["protocol"] == 3 -@patch("homeassistant.components.mqtt.TIMEOUT_ACK", 0.2) +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) async def test_handle_mqtt_timeout_on_callback(hass, caplog): """Test publish without receiving an ACK callback.""" mid = 0 @@ -1486,7 +1486,7 @@ async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "birth", wait_birth) mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -1516,7 +1516,7 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -1532,7 +1532,7 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): ) async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): """Test disabling birth message.""" - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() await asyncio.sleep(0.2) @@ -1580,7 +1580,7 @@ async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config, mqtt_m """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index f451079e0f0..b7a3b5f2118 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.components import vacuum -from homeassistant.components.mqtt import CONF_COMMAND_TOPIC +from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_legacy import ( diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 3f752f1b528..c1017446eff 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.components import vacuum -from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC +from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum from homeassistant.components.mqtt.vacuum.const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from homeassistant.components.mqtt.vacuum.schema import services_to_strings From caa79d8462358e1f07b8d3685a2acb5c10ac97d1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 2 Jun 2022 14:24:46 +0200 Subject: [PATCH 003/107] Update MQTT tests to use the config entry setup (#72373) * New testframework and tests for fan platform * Merge test_common_new to test_common * Add alarm_control_panel * Add binary_sensor * Add button * Add camera * Add climate * Add config_flow * Add cover * Add device_tracker_disovery * Add device_trigger * Add diagnostics * Add discovery * Add humidifier * Add init * Add lecacy_vacuum * Add light_json * Add light_template * Add light * Add lock * Add number * Add scene * Add select * Add sensor * Add siren * Add state_vacuum * Add subscription * Add switch * Add tag * Add trigger * Add missed tests * Add another missed test * Add device_tracker * Remove commented out code * Correct tests according comments * Improve mqtt_mock_entry and recover tests * Split fixtures with and without yaml setup * Update fixtures manual_mqtt * Update fixtures mqtt_json * Fix test tasmota * Update fixture mqtt_room * Revert fixture changes, improve test * re-add test --- .../manual_mqtt/test_alarm_control_panel.py | 117 ++++-- .../mqtt/test_alarm_control_panel.py | 248 +++++++---- tests/components/mqtt/test_binary_sensor.py | 231 +++++++---- tests/components/mqtt/test_button.py | 146 ++++--- tests/components/mqtt/test_camera.py | 133 +++--- tests/components/mqtt/test_climate.py | 296 +++++++++----- tests/components/mqtt/test_common.py | 145 +++++-- tests/components/mqtt/test_config_flow.py | 11 +- tests/components/mqtt/test_cover.py | 385 ++++++++++++------ tests/components/mqtt/test_device_tracker.py | 34 +- .../mqtt/test_device_tracker_discovery.py | 58 ++- tests/components/mqtt/test_device_trigger.py | 102 +++-- tests/components/mqtt/test_diagnostics.py | 10 +- tests/components/mqtt/test_discovery.py | 111 +++-- tests/components/mqtt/test_fan.py | 214 +++++++--- tests/components/mqtt/test_humidifier.py | 202 ++++++--- tests/components/mqtt/test_init.py | 280 +++++++++---- tests/components/mqtt/test_legacy_vacuum.py | 204 ++++++---- tests/components/mqtt/test_light.py | 290 +++++++++---- tests/components/mqtt/test_light_json.py | 247 +++++++---- tests/components/mqtt/test_light_template.py | 260 +++++++----- tests/components/mqtt/test_lock.py | 169 +++++--- tests/components/mqtt/test_number.py | 171 +++++--- tests/components/mqtt/test_scene.py | 70 +++- tests/components/mqtt/test_select.py | 170 +++++--- tests/components/mqtt/test_sensor.py | 280 ++++++++----- tests/components/mqtt/test_siren.py | 181 +++++--- tests/components/mqtt/test_state_vacuum.py | 153 ++++--- tests/components/mqtt/test_subscription.py | 19 +- tests/components/mqtt/test_switch.py | 174 +++++--- tests/components/mqtt/test_tag.py | 72 +++- tests/components/mqtt/test_trigger.py | 11 +- .../mqtt_json/test_device_tracker.py | 2 +- tests/components/mqtt_room/test_sensor.py | 2 +- tests/components/tasmota/test_discovery.py | 6 +- tests/conftest.py | 100 ++++- 36 files changed, 3612 insertions(+), 1692 deletions(-) diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 3572077bd8e..4296f76d741 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -26,9 +26,9 @@ from tests.components.alarm_control_panel import common CODE = "HELLO_CODE" -async def test_fail_setup_without_state_topic(hass, mqtt_mock): +async def test_fail_setup_without_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test for failing with no state topic.""" - with assert_setup_component(0) as config: + with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -42,9 +42,9 @@ async def test_fail_setup_without_state_topic(hass, mqtt_mock): assert not config[alarm_control_panel.DOMAIN] -async def test_fail_setup_without_command_topic(hass, mqtt_mock): +async def test_fail_setup_without_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test failing with no command topic.""" - with assert_setup_component(0): + with assert_setup_component(0, alarm_control_panel.DOMAIN): assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -57,7 +57,7 @@ async def test_fail_setup_without_command_topic(hass, mqtt_mock): ) -async def test_arm_home_no_pending(hass, mqtt_mock): +async def test_arm_home_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -86,7 +86,9 @@ async def test_arm_home_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_home_no_pending_when_code_not_req(hass, mqtt_mock): +async def test_arm_home_no_pending_when_code_not_req( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm home method.""" assert await async_setup_component( hass, @@ -116,7 +118,7 @@ async def test_arm_home_no_pending_when_code_not_req(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_home_with_pending(hass, mqtt_mock): +async def test_arm_home_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -158,7 +160,7 @@ async def test_arm_home_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_home_with_invalid_code(hass, mqtt_mock): +async def test_arm_home_with_invalid_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm home without a valid code.""" assert await async_setup_component( hass, @@ -187,7 +189,7 @@ async def test_arm_home_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_arm_away_no_pending(hass, mqtt_mock): +async def test_arm_away_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -216,7 +218,9 @@ async def test_arm_away_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_away_no_pending_when_code_not_req(hass, mqtt_mock): +async def test_arm_away_no_pending_when_code_not_req( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm home method.""" assert await async_setup_component( hass, @@ -246,7 +250,7 @@ async def test_arm_away_no_pending_when_code_not_req(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_home_with_template_code(hass, mqtt_mock): +async def test_arm_home_with_template_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm with a template-based code.""" assert await async_setup_component( hass, @@ -276,7 +280,7 @@ async def test_arm_home_with_template_code(hass, mqtt_mock): assert state.state == STATE_ALARM_ARMED_HOME -async def test_arm_away_with_pending(hass, mqtt_mock): +async def test_arm_away_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -318,7 +322,7 @@ async def test_arm_away_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_away_with_invalid_code(hass, mqtt_mock): +async def test_arm_away_with_invalid_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm away without a valid code.""" assert await async_setup_component( hass, @@ -347,7 +351,7 @@ async def test_arm_away_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_arm_night_no_pending(hass, mqtt_mock): +async def test_arm_night_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm night method.""" assert await async_setup_component( hass, @@ -376,7 +380,9 @@ async def test_arm_night_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_arm_night_no_pending_when_code_not_req(hass, mqtt_mock): +async def test_arm_night_no_pending_when_code_not_req( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm night method.""" assert await async_setup_component( hass, @@ -406,7 +412,7 @@ async def test_arm_night_no_pending_when_code_not_req(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_arm_night_with_pending(hass, mqtt_mock): +async def test_arm_night_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm night method.""" assert await async_setup_component( hass, @@ -454,7 +460,7 @@ async def test_arm_night_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_arm_night_with_invalid_code(hass, mqtt_mock): +async def test_arm_night_with_invalid_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm night without a valid code.""" assert await async_setup_component( hass, @@ -483,7 +489,7 @@ async def test_arm_night_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_no_pending(hass, mqtt_mock): +async def test_trigger_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test triggering when no pending submitted method.""" assert await async_setup_component( hass, @@ -521,7 +527,7 @@ async def test_trigger_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED -async def test_trigger_with_delay(hass, mqtt_mock): +async def test_trigger_with_delay(hass, mqtt_mock_entry_with_yaml_config): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -569,7 +575,7 @@ async def test_trigger_with_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_zero_trigger_time(hass, mqtt_mock): +async def test_trigger_zero_trigger_time(hass, mqtt_mock_entry_with_yaml_config): """Test disabled trigger.""" assert await async_setup_component( hass, @@ -598,7 +604,9 @@ async def test_trigger_zero_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_zero_trigger_time_with_pending(hass, mqtt_mock): +async def test_trigger_zero_trigger_time_with_pending( + hass, mqtt_mock_entry_with_yaml_config +): """Test disabled trigger.""" assert await async_setup_component( hass, @@ -627,7 +635,7 @@ async def test_trigger_zero_trigger_time_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_pending(hass, mqtt_mock): +async def test_trigger_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -679,7 +687,9 @@ async def test_trigger_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_disarm_after_trigger(hass, mqtt_mock): +async def test_trigger_with_disarm_after_trigger( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarm after trigger.""" assert await async_setup_component( hass, @@ -718,7 +728,9 @@ async def test_trigger_with_disarm_after_trigger(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_zero_specific_trigger_time(hass, mqtt_mock): +async def test_trigger_with_zero_specific_trigger_time( + hass, mqtt_mock_entry_with_yaml_config +): """Test trigger method.""" assert await async_setup_component( hass, @@ -748,7 +760,9 @@ async def test_trigger_with_zero_specific_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_unused_zero_specific_trigger_time(hass, mqtt_mock): +async def test_trigger_with_unused_zero_specific_trigger_time( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarm after trigger.""" assert await async_setup_component( hass, @@ -788,7 +802,9 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_specific_trigger_time(hass, mqtt_mock): +async def test_trigger_with_specific_trigger_time( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarm after trigger.""" assert await async_setup_component( hass, @@ -827,7 +843,9 @@ async def test_trigger_with_specific_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock): +async def test_back_to_back_trigger_with_no_disarm_after_trigger( + hass, mqtt_mock_entry_with_yaml_config +): """Test no disarm after back to back trigger.""" assert await async_setup_component( hass, @@ -886,7 +904,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_disarm_while_pending_trigger(hass, mqtt_mock): +async def test_disarm_while_pending_trigger(hass, mqtt_mock_entry_with_yaml_config): """Test disarming while pending state.""" assert await async_setup_component( hass, @@ -929,7 +947,9 @@ async def test_disarm_while_pending_trigger(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_disarm_during_trigger_with_invalid_code(hass, mqtt_mock): +async def test_disarm_during_trigger_with_invalid_code( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarming while code is invalid.""" assert await async_setup_component( hass, @@ -973,7 +993,9 @@ async def test_disarm_during_trigger_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED -async def test_trigger_with_unused_specific_delay(hass, mqtt_mock): +async def test_trigger_with_unused_specific_delay( + hass, mqtt_mock_entry_with_yaml_config +): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1022,7 +1044,7 @@ async def test_trigger_with_unused_specific_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_with_specific_delay(hass, mqtt_mock): +async def test_trigger_with_specific_delay(hass, mqtt_mock_entry_with_yaml_config): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1071,7 +1093,7 @@ async def test_trigger_with_specific_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_with_pending_and_delay(hass, mqtt_mock): +async def test_trigger_with_pending_and_delay(hass, mqtt_mock_entry_with_yaml_config): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1132,7 +1154,9 @@ async def test_trigger_with_pending_and_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_with_pending_and_specific_delay(hass, mqtt_mock): +async def test_trigger_with_pending_and_specific_delay( + hass, mqtt_mock_entry_with_yaml_config +): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1194,7 +1218,7 @@ async def test_trigger_with_pending_and_specific_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_armed_home_with_specific_pending(hass, mqtt_mock): +async def test_armed_home_with_specific_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -1230,7 +1254,7 @@ async def test_armed_home_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_armed_away_with_specific_pending(hass, mqtt_mock): +async def test_armed_away_with_specific_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -1266,7 +1290,9 @@ async def test_armed_away_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_armed_night_with_specific_pending(hass, mqtt_mock): +async def test_armed_night_with_specific_pending( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm home method.""" assert await async_setup_component( hass, @@ -1302,7 +1328,7 @@ async def test_armed_night_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_trigger_with_specific_pending(hass, mqtt_mock): +async def test_trigger_with_specific_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -1350,7 +1376,7 @@ async def test_trigger_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_arm_away_after_disabled_disarmed(hass, mqtt_mock): +async def test_arm_away_after_disabled_disarmed(hass, mqtt_mock_entry_with_yaml_config): """Test pending state with and without zero trigger time.""" assert await async_setup_component( hass, @@ -1417,7 +1443,7 @@ async def test_arm_away_after_disabled_disarmed(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_disarm_with_template_code(hass, mqtt_mock): +async def test_disarm_with_template_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to disarm with a valid or invalid template-based code.""" assert await async_setup_component( hass, @@ -1459,7 +1485,7 @@ async def test_disarm_with_template_code(hass, mqtt_mock): assert state.state == STATE_ALARM_DISARMED -async def test_arm_home_via_command_topic(hass, mqtt_mock): +async def test_arm_home_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test arming home via command topic.""" assert await async_setup_component( hass, @@ -1498,7 +1524,7 @@ async def test_arm_home_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_away_via_command_topic(hass, mqtt_mock): +async def test_arm_away_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test arming away via command topic.""" assert await async_setup_component( hass, @@ -1537,7 +1563,7 @@ async def test_arm_away_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_night_via_command_topic(hass, mqtt_mock): +async def test_arm_night_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test arming night via command topic.""" assert await async_setup_component( hass, @@ -1576,7 +1602,7 @@ async def test_arm_night_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_disarm_pending_via_command_topic(hass, mqtt_mock): +async def test_disarm_pending_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test disarming pending alarm via command topic.""" assert await async_setup_component( hass, @@ -1610,7 +1636,9 @@ async def test_disarm_pending_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_state_changes_are_published_to_mqtt(hass, mqtt_mock): +async def test_state_changes_are_published_to_mqtt( + hass, mqtt_mock_entry_with_yaml_config +): """Test publishing of MQTT messages when state changes.""" assert await async_setup_component( hass, @@ -1630,6 +1658,7 @@ async def test_state_changes_are_published_to_mqtt(hass, mqtt_mock): # Component should send disarmed alarm state on startup await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() mqtt_mock.async_publish.assert_called_once_with( "alarm/state", STATE_ALARM_DISARMED, 0, True ) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index f4d76d5474c..2b013ddf8dd 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -112,9 +112,9 @@ DEFAULT_CONFIG_REMOTE_CODE_TEXT = { } -async def test_fail_setup_without_state_topic(hass, mqtt_mock): +async def test_fail_setup_without_state_topic(hass, mqtt_mock_entry_no_yaml_config): """Test for failing with no state topic.""" - with assert_setup_component(0) as config: + with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -125,12 +125,14 @@ async def test_fail_setup_without_state_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert not config[alarm_control_panel.DOMAIN] -async def test_fail_setup_without_command_topic(hass, mqtt_mock): +async def test_fail_setup_without_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test failing with no command topic.""" - with assert_setup_component(0): + with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -141,9 +143,12 @@ async def test_fail_setup_without_command_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() + assert not config[alarm_control_panel.DOMAIN] -async def test_update_state_via_state_topic(hass, mqtt_mock): +async def test_update_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test updating with via state topic.""" assert await async_setup_component( hass, @@ -151,6 +156,7 @@ async def test_update_state_via_state_topic(hass, mqtt_mock): DEFAULT_CONFIG, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity_id = "alarm_control_panel.test" @@ -172,7 +178,9 @@ async def test_update_state_via_state_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == state -async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): +async def test_ignore_update_state_if_unknown_via_state_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test ignoring updates via state topic.""" assert await async_setup_component( hass, @@ -180,6 +188,7 @@ async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): DEFAULT_CONFIG, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity_id = "alarm_control_panel.test" @@ -201,7 +210,9 @@ async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_no_code( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when no code is configured.""" assert await async_setup_component( hass, @@ -209,6 +220,7 @@ async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): DEFAULT_CONFIG, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( alarm_control_panel.DOMAIN, @@ -232,7 +244,9 @@ async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_with_code( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when code is configured.""" assert await async_setup_component( hass, @@ -240,6 +254,7 @@ async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): DEFAULT_CONFIG_CODE, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish @@ -282,7 +297,9 @@ async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_with_remote_code( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when remode code is configured.""" assert await async_setup_component( hass, @@ -290,6 +307,7 @@ async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): DEFAULT_CONFIG_REMOTE_CODE, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish @@ -323,7 +341,9 @@ async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_with_remote_code_text( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when remote text code is configured.""" assert await async_setup_component( hass, @@ -331,6 +351,7 @@ async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payl DEFAULT_CONFIG_REMOTE_CODE_TEXT, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish @@ -365,7 +386,7 @@ async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payl ], ) async def test_publish_mqtt_with_code_required_false( - hass, mqtt_mock, service, payload, disable_code + hass, mqtt_mock_entry_with_yaml_config, service, payload, disable_code ): """Test publishing of MQTT messages when code is configured. @@ -380,6 +401,7 @@ async def test_publish_mqtt_with_code_required_false( config, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # No code provided, should publish await hass.services.async_call( @@ -412,7 +434,9 @@ async def test_publish_mqtt_with_code_required_false( mqtt_mock.reset_mock() -async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): +async def test_disarm_publishes_mqtt_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test publishing of MQTT messages while disarmed. When command_template set to output json @@ -428,6 +452,7 @@ async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): config, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_alarm_disarm(hass, "0123") mqtt_mock.async_publish.assert_called_once_with( @@ -435,7 +460,9 @@ async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): ) -async def test_update_state_via_state_topic_template(hass, mqtt_mock): +async def test_update_state_via_state_topic_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test updating with template_value via state topic.""" assert await async_setup_component( hass, @@ -456,6 +483,7 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert state.state == STATE_UNKNOWN @@ -466,13 +494,14 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): assert state.state == STATE_ALARM_ARMED_AWAY -async def test_attributes_code_number(hass, mqtt_mock): +async def test_attributes_code_number(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) config[alarm_control_panel.DOMAIN]["code"] = CODE_NUMBER assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert ( @@ -481,13 +510,14 @@ async def test_attributes_code_number(hass, mqtt_mock): ) -async def test_attributes_remote_code_number(hass, mqtt_mock): +async def test_attributes_remote_code_number(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG_REMOTE_CODE) config[alarm_control_panel.DOMAIN]["code"] = "REMOTE_CODE" assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert ( @@ -496,13 +526,14 @@ async def test_attributes_remote_code_number(hass, mqtt_mock): ) -async def test_attributes_code_text(hass, mqtt_mock): +async def test_attributes_code_text(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) config[alarm_control_panel.DOMAIN]["code"] = CODE_TEXT assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert ( @@ -511,81 +542,121 @@ async def test_attributes_code_text(hass, mqtt_mock): ) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, MQTT_ALARM_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one alarm per unique_id.""" config = { alarm_control_panel.DOMAIN: [ @@ -605,18 +676,22 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, alarm_control_panel.DOMAIN, config) - - -async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): - """Test removal of discovered alarm_control_panel.""" - data = json.dumps(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, config ) -async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog): +async def test_discovery_removal_alarm(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test removal of discovered alarm_control_panel.""" + data = json.dumps(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, data + ) + + +async def test_discovery_update_alarm_topic_and_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) @@ -639,7 +714,7 @@ async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, config1, @@ -649,7 +724,9 @@ async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog ) -async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): +async def test_discovery_update_alarm_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) @@ -670,7 +747,7 @@ async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, config1, @@ -680,7 +757,9 @@ async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_alarm(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_alarm( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" @@ -690,12 +769,17 @@ async def test_discovery_update_unchanged_alarm(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.alarm_control_panel.MqttAlarm.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -704,7 +788,12 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, data2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + data1, + data2, ) @@ -715,11 +804,13 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ("state_topic", "disarmed"), ], ) -async def test_encoding_subscribable_topics(hass, mqtt_mock, caplog, topic, value): +async def test_encoding_subscribable_topics( + hass, mqtt_mock_entry_with_yaml_config, caplog, topic, value +): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG[alarm_control_panel.DOMAIN], @@ -728,53 +819,62 @@ async def test_encoding_subscribable_topics(hass, mqtt_mock, caplog, topic, valu ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT alarm control panel device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT alarm control panel device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, alarm_control_panel.SERVICE_ALARM_DISARM, @@ -807,7 +907,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -823,7 +923,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -837,11 +937,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = alarm_control_panel.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index e4a48b07940..37bb783d354 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -62,7 +62,9 @@ DEFAULT_CONFIG = { } -async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires_availability_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -79,6 +81,7 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE @@ -89,10 +92,12 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass) -async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -108,15 +113,16 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # State should be unavailable since expire_after is defined and > 0 state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass) -async def expires_helper(hass, mqtt_mock, caplog): +async def expires_helper(hass): """Run the basic expiry code.""" realnow = dt_util.utcnow() now = datetime(realnow.year + 1, 1, 1, 1, tzinfo=dt_util.UTC) @@ -168,9 +174,10 @@ async def expires_helper(hass, mqtt_mock, caplog): async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test that binary_sensor with expire_after set behaves correctly on discovery and discovery update.""" + await mqtt_mock_entry_no_yaml_config() config = { "name": "Test", "state_topic": "test-topic", @@ -247,7 +254,9 @@ async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor( assert state.state == STATE_UNAVAILABLE -async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -263,6 +272,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") @@ -281,7 +291,9 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): +async def test_invalid_sensor_value_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -297,6 +309,7 @@ async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") @@ -319,7 +332,9 @@ async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): assert "No matching payload found for entity" in caplog.text -async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_message_and_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -337,6 +352,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_moc }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -351,7 +367,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_moc async def test_setting_sensor_value_via_mqtt_message_and_template2( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -369,6 +385,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -388,7 +405,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_encoding( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test processing a raw value via MQTT.""" assert await async_setup_component( @@ -407,6 +424,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_ }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -421,7 +439,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_ async def test_setting_sensor_value_via_mqtt_message_empty_template( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -439,6 +457,7 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -453,7 +472,7 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( assert state.state == STATE_ON -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of a valid sensor class.""" assert await async_setup_component( hass, @@ -468,12 +487,13 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.attributes.get("device_class") == "motion" -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test the setting of an invalid sensor class.""" assert await async_setup_component( hass, @@ -488,40 +508,43 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("binary_sensor.test") assert state is None -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_force_update_disabled(hass, mqtt_mock): +async def test_force_update_disabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -537,6 +560,7 @@ async def test_force_update_disabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -556,7 +580,7 @@ async def test_force_update_disabled(hass, mqtt_mock): assert len(events) == 1 -async def test_force_update_enabled(hass, mqtt_mock): +async def test_force_update_enabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -573,6 +597,7 @@ async def test_force_update_enabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -592,7 +617,7 @@ async def test_force_update_enabled(hass, mqtt_mock): assert len(events) == 2 -async def test_off_delay(hass, mqtt_mock): +async def test_off_delay(hass, mqtt_mock_entry_with_yaml_config): """Test off_delay option.""" assert await async_setup_component( hass, @@ -610,6 +635,7 @@ async def test_off_delay(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -639,42 +665,60 @@ async def test_off_delay(hass, mqtt_mock): assert len(events) == 3 -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one sensor per unique_id.""" config = { binary_sensor.DOMAIN: [ @@ -692,18 +736,24 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, binary_sensor.DOMAIN, config) - - -async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): - """Test removal of discovered binary_sensor.""" - data = json.dumps(DEFAULT_CONFIG[binary_sensor.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, config ) -async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_removal_binary_sensor( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test removal of discovered binary_sensor.""" + data = json.dumps(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, data + ) + + +async def test_discovery_update_binary_sensor_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) @@ -728,7 +778,7 @@ async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, ca await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, config1, @@ -738,7 +788,9 @@ async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, ca ) -async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): +async def test_discovery_update_binary_sensor_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) @@ -761,7 +813,7 @@ async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, config1, @@ -785,12 +837,18 @@ async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG[binary_sensor.DOMAIN], @@ -801,7 +859,9 @@ async def test_encoding_subscribable_topics( ) -async def test_discovery_update_unchanged_binary_sensor(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_binary_sensor( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config1["name"] = "Beer" @@ -811,74 +871,90 @@ async def test_discovery_update_unchanged_binary_sensor(hass, mqtt_mock, caplog) "homeassistant.components.mqtt.binary_sensor.MqttBinarySensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + binary_sensor.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "off_delay": -1 }' data2 = '{ "name": "Milk",' ' "state_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, data2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + binary_sensor.DOMAIN, + data1, + data2, ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG, None + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, + None, ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = binary_sensor.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -893,7 +969,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): [("ON", "on", "OFF", "off"), ("OFF", "off", "ON", "on")], ) async def test_cleanup_triggers_and_restoring_state( - hass, mqtt_mock, caplog, tmp_path, freezer, payload1, state1, payload2, state2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + tmp_path, + freezer, + payload1, + state1, + payload2, + state2, ): """Test cleanup old triggers at reloading and restoring the state.""" domain = binary_sensor.DOMAIN @@ -914,6 +998,8 @@ async def test_cleanup_triggers_and_restoring_state( {binary_sensor.DOMAIN: [config1, config2]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + async_fire_mqtt_message(hass, "test-topic1", payload1) state = hass.states.get("binary_sensor.test1") assert state.state == state1 @@ -951,7 +1037,7 @@ async def test_cleanup_triggers_and_restoring_state( async def test_skip_restoring_state_with_over_due_expire_trigger( - hass, mqtt_mock, caplog, freezer + hass, mqtt_mock_entry_with_yaml_config, caplog, freezer ): """Test restoring a state with over due expire timer.""" @@ -973,6 +1059,7 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ), assert_setup_component(1, domain): assert await async_setup_component(hass, domain, {domain: config3}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert "Skip state recovery after reload for binary_sensor.test3" in caplog.text diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 941f08e541c..35deccf2bfe 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -42,7 +42,7 @@ DEFAULT_CONFIG = { @pytest.mark.freeze_time("2021-11-08 13:31:44+00:00") -async def test_sending_mqtt_commands(hass, mqtt_mock): +async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" assert await async_setup_component( hass, @@ -59,6 +59,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("button.test_button") assert state.state == STATE_UNKNOWN @@ -79,7 +80,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): assert state.state == "2021-11-08T13:31:44+00:00" -async def test_command_template(hass, mqtt_mock): +async def test_command_template(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of MQTT commands through a command template.""" assert await async_setup_component( hass, @@ -95,6 +96,7 @@ async def test_command_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("button.test") assert state.state == STATE_UNKNOWN @@ -113,21 +115,23 @@ async def test_command_template(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { button.DOMAIN: { @@ -139,11 +143,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, button.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + button.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { button.DOMAIN: { @@ -155,53 +165,67 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, button.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + button.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG, None ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one button per unique_id.""" config = { button.DOMAIN: [ @@ -219,16 +243,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, button.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, config + ) -async def test_discovery_removal_button(hass, mqtt_mock, caplog): +async def test_discovery_removal_button(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered button.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, button.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, data + ) -async def test_discovery_update_button(hass, mqtt_mock, caplog): +async def test_discovery_update_button(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered button.""" config1 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) @@ -237,7 +265,7 @@ async def test_discovery_update_button(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, config1, @@ -245,7 +273,9 @@ async def test_discovery_update_button(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_button(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_button( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered button.""" data1 = ( '{ "name": "Beer",' @@ -256,60 +286,65 @@ async def test_discovery_update_unchanged_button(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.button.MqttButton.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, button.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + button.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, button.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG, button.SERVICE_PRESS, @@ -318,7 +353,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test device_class option with invalid value.""" assert await async_setup_component( hass, @@ -333,12 +368,13 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("button.test") assert state is None -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test device_class option with valid values.""" assert await async_setup_component( hass, @@ -366,6 +402,7 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("button.test_1") assert state.attributes["device_class"] == button.ButtonDeviceClass.UPDATE @@ -382,7 +419,14 @@ async def test_valid_device_class(hass, mqtt_mock): ], ) async def test_publishing_with_custom_encoding( - hass, mqtt_mock, caplog, service, topic, parameters, payload, template + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + service, + topic, + parameters, + payload, + template, ): """Test publishing MQTT payload with different encoding.""" domain = button.DOMAIN @@ -390,7 +434,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -402,11 +446,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = button.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 204103152a7..54d829ce9f9 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -46,7 +46,9 @@ DEFAULT_CONFIG = { } -async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): +async def test_run_camera_setup( + hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config +): """Test that it fetches the given payload.""" topic = "test/camera" await async_setup_component( @@ -55,6 +57,7 @@ async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): {"camera": {"platform": "mqtt", "topic": topic, "name": "Test Camera"}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() url = hass.states.get("camera.test_camera").attributes["entity_picture"] @@ -67,7 +70,9 @@ async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): assert body == "beer" -async def test_run_camera_b64_encoded(hass, hass_client_no_auth, mqtt_mock): +async def test_run_camera_b64_encoded( + hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config +): """Test that it fetches the given encoded payload.""" topic = "test/camera" await async_setup_component( @@ -83,6 +88,7 @@ async def test_run_camera_b64_encoded(hass, hass_client_no_auth, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() url = hass.states.get("camera.test_camera").attributes["entity_picture"] @@ -95,77 +101,91 @@ async def test_run_camera_b64_encoded(hass, hass_client_no_auth, mqtt_mock): assert body == "grass" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, MQTT_CAMERA_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + camera.DOMAIN, + DEFAULT_CONFIG, + MQTT_CAMERA_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one camera per unique_id.""" config = { camera.DOMAIN: [ @@ -183,94 +203,109 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, camera.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, config + ) -async def test_discovery_removal_camera(hass, mqtt_mock, caplog): +async def test_discovery_removal_camera(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered camera.""" data = json.dumps(DEFAULT_CONFIG[camera.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, camera.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, data + ) -async def test_discovery_update_camera(hass, mqtt_mock, caplog): +async def test_discovery_update_camera(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered camera.""" config1 = {"name": "Beer", "topic": "test_topic"} config2 = {"name": "Milk", "topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, camera.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_camera(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_camera( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered camera.""" data1 = '{ "name": "Beer", "topic": "test_topic"}' with patch( "homeassistant.components.mqtt.camera.MqttCamera.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, camera.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + camera.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "topic": "test_topic"}' await help_test_discovery_broken( - hass, mqtt_mock, caplog, camera.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, ["test_topic"] + hass, + mqtt_mock_entry_with_yaml_config, + camera.DOMAIN, + DEFAULT_CONFIG, + ["test_topic"], ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG, None, @@ -279,11 +314,13 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = camera.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 98af86248e4..77843cee777 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -106,10 +106,11 @@ DEFAULT_LEGACY_CONFIG = { } -async def test_setup_params(hass, mqtt_mock): +async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config): """Test the initial parameters.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") == 21 @@ -120,12 +121,15 @@ async def test_setup_params(hass, mqtt_mock): assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP -async def test_preset_none_in_preset_modes(hass, mqtt_mock, caplog): +async def test_preset_none_in_preset_modes( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test the preset mode payload reset configuration.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) config["preset_modes"].append("none") assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert "Invalid config for [climate.mqtt]: not a valid value" in caplog.text state = hass.states.get(ENTITY_CLIMATE) assert state is None @@ -145,21 +149,23 @@ async def test_preset_none_in_preset_modes(hass, mqtt_mock, caplog): ], ) async def test_preset_modes_deprecation_guard( - hass, mqtt_mock, caplog, parameter, config_value + hass, mqtt_mock_entry_no_yaml_config, caplog, parameter, config_value ): """Test the configuration for invalid legacy parameters.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) config[parameter] = config_value assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state is None -async def test_supported_features(hass, mqtt_mock): +async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test the supported_features.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) support = ( @@ -174,10 +180,11 @@ async def test_supported_features(hass, mqtt_mock): assert state.attributes.get("supported_features") == support -async def test_get_hvac_modes(hass, mqtt_mock): +async def test_get_hvac_modes(hass, mqtt_mock_entry_with_yaml_config): """Test that the operation list returns the correct modes.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) modes = state.attributes.get("hvac_modes") @@ -191,13 +198,16 @@ async def test_get_hvac_modes(hass, mqtt_mock): ] == modes -async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): +async def test_set_operation_bad_attr_and_state( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting operation mode without required attribute. Also check the state. """ assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -210,10 +220,11 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): assert state.state == "off" -async def test_set_operation(hass, mqtt_mock): +async def test_set_operation(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -224,12 +235,13 @@ async def test_set_operation(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("mode-topic", "cool", 0, False) -async def test_set_operation_pessimistic(hass, mqtt_mock): +async def test_set_operation_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting operation mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["mode_state_topic"] = "mode-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "unknown" @@ -247,12 +259,13 @@ async def test_set_operation_pessimistic(hass, mqtt_mock): assert state.state == "cool" -async def test_set_operation_with_power_command(hass, mqtt_mock): +async def test_set_operation_with_power_command(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode with power command enabled.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["power_command_topic"] = "power-command" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -273,10 +286,11 @@ async def test_set_operation_with_power_command(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_set_fan_mode_bad_attr(hass, mqtt_mock, caplog): +async def test_set_fan_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting fan mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") == "low" @@ -289,12 +303,13 @@ async def test_set_fan_mode_bad_attr(hass, mqtt_mock, caplog): assert state.attributes.get("fan_mode") == "low" -async def test_set_fan_mode_pessimistic(hass, mqtt_mock): +async def test_set_fan_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["fan_mode_state_topic"] = "fan-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") is None @@ -312,10 +327,11 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock): assert state.attributes.get("fan_mode") == "high" -async def test_set_fan_mode(hass, mqtt_mock): +async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") == "low" @@ -335,13 +351,14 @@ async def test_set_fan_mode(hass, mqtt_mock): ], ) async def test_set_fan_mode_send_if_off( - hass, mqtt_mock, send_if_off, assert_async_publish + hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish ): """Test setting of fan mode if the hvac is off.""" config = copy.deepcopy(DEFAULT_CONFIG) config[CLIMATE_DOMAIN].update(send_if_off) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() assert hass.states.get(ENTITY_CLIMATE) is not None # Turn on HVAC @@ -362,10 +379,11 @@ async def test_set_fan_mode_send_if_off( mqtt_mock.async_publish.assert_has_calls(assert_async_publish) -async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): +async def test_set_swing_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting swing mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") == "off" @@ -378,12 +396,13 @@ async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): assert state.attributes.get("swing_mode") == "off" -async def test_set_swing_pessimistic(hass, mqtt_mock): +async def test_set_swing_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting swing mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["swing_mode_state_topic"] = "swing-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") is None @@ -401,10 +420,11 @@ async def test_set_swing_pessimistic(hass, mqtt_mock): assert state.attributes.get("swing_mode") == "on" -async def test_set_swing(hass, mqtt_mock): +async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new swing mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") == "off" @@ -424,13 +444,14 @@ async def test_set_swing(hass, mqtt_mock): ], ) async def test_set_swing_mode_send_if_off( - hass, mqtt_mock, send_if_off, assert_async_publish + hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish ): """Test setting of swing mode if the hvac is off.""" config = copy.deepcopy(DEFAULT_CONFIG) config[CLIMATE_DOMAIN].update(send_if_off) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() assert hass.states.get(ENTITY_CLIMATE) is not None # Turn on HVAC @@ -451,10 +472,11 @@ async def test_set_swing_mode_send_if_off( mqtt_mock.async_publish.assert_has_calls(assert_async_publish) -async def test_set_target_temperature(hass, mqtt_mock): +async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test setting the target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") == 21 @@ -497,13 +519,14 @@ async def test_set_target_temperature(hass, mqtt_mock): ], ) async def test_set_target_temperature_send_if_off( - hass, mqtt_mock, send_if_off, assert_async_publish + hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish ): """Test setting of target temperature if the hvac is off.""" config = copy.deepcopy(DEFAULT_CONFIG) config[CLIMATE_DOMAIN].update(send_if_off) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() assert hass.states.get(ENTITY_CLIMATE) is not None # Turn on HVAC @@ -526,12 +549,15 @@ async def test_set_target_temperature_send_if_off( mqtt_mock.async_publish.assert_has_calls(assert_async_publish) -async def test_set_target_temperature_pessimistic(hass, mqtt_mock): +async def test_set_target_temperature_pessimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_state_topic"] = "temperature-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") is None @@ -549,10 +575,11 @@ async def test_set_target_temperature_pessimistic(hass, mqtt_mock): assert state.attributes.get("temperature") == 1701 -async def test_set_target_temperature_low_high(hass, mqtt_mock): +async def test_set_target_temperature_low_high(hass, mqtt_mock_entry_with_yaml_config): """Test setting the low/high target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, target_temp_low=20, target_temp_high=23, entity_id=ENTITY_CLIMATE @@ -564,13 +591,16 @@ async def test_set_target_temperature_low_high(hass, mqtt_mock): mqtt_mock.async_publish.assert_any_call("temperature-high-topic", "23.0", 0, False) -async def test_set_target_temperature_low_highpessimistic(hass, mqtt_mock): +async def test_set_target_temperature_low_highpessimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting the low/high target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_low_state_topic"] = "temperature-low-state" config["climate"]["temperature_high_state_topic"] = "temperature-high-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("target_temp_low") is None @@ -601,24 +631,26 @@ async def test_set_target_temperature_low_highpessimistic(hass, mqtt_mock): assert state.attributes.get("target_temp_high") == 1703 -async def test_receive_mqtt_temperature(hass, mqtt_mock): +async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test getting the current temperature via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["current_temperature_topic"] = "current_temperature" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "current_temperature", "47") state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("current_temperature") == 47 -async def test_handle_action_received(hass, mqtt_mock): +async def test_handle_action_received(hass, mqtt_mock_entry_with_yaml_config): """Test getting the action received via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["action_topic"] = "action" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # Cycle through valid modes and also check for wrong input such as "None" (str(None)) async_fire_mqtt_message(hass, "action", "None") @@ -635,11 +667,14 @@ async def test_handle_action_received(hass, mqtt_mock): assert hvac_action == action -async def test_set_preset_mode_optimistic(hass, mqtt_mock, caplog): +async def test_set_preset_mode_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting of the preset mode.""" config = copy.deepcopy(DEFAULT_CONFIG) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -680,12 +715,15 @@ async def test_set_preset_mode_optimistic(hass, mqtt_mock, caplog): assert "'invalid' is not a valid preset mode" in caplog.text -async def test_set_preset_mode_pessimistic(hass, mqtt_mock, caplog): +async def test_set_preset_mode_pessimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting of the preset mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["preset_mode_state_topic"] = "preset-mode-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -725,12 +763,13 @@ async def test_set_preset_mode_pessimistic(hass, mqtt_mock, caplog): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_away_mode_pessimistic(hass, mqtt_mock): +async def test_set_away_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the away mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["away_mode_state_topic"] = "away-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -753,7 +792,7 @@ async def test_set_away_mode_pessimistic(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_away_mode(hass, mqtt_mock): +async def test_set_away_mode(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the away mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["payload_on"] = "AN" @@ -761,6 +800,7 @@ async def test_set_away_mode(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -795,12 +835,13 @@ async def test_set_away_mode(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_hold_pessimistic(hass, mqtt_mock): +async def test_set_hold_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["hold_state_topic"] = "hold-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("hold_mode") is None @@ -819,10 +860,11 @@ async def test_set_hold_pessimistic(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_hold(hass, mqtt_mock): +async def test_set_hold(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -851,10 +893,11 @@ async def test_set_hold(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_away(hass, mqtt_mock): +async def test_set_preset_away(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode and away mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == PRESET_NONE @@ -885,13 +928,14 @@ async def test_set_preset_away(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_away_pessimistic(hass, mqtt_mock): +async def test_set_preset_away_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode and away mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["hold_state_topic"] = "hold-state" config["climate"]["away_mode_state_topic"] = "away-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == PRESET_NONE @@ -936,10 +980,11 @@ async def test_set_preset_away_pessimistic(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_mode_twice(hass, mqtt_mock): +async def test_set_preset_mode_twice(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the same mode twice only publishes once.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -952,12 +997,13 @@ async def test_set_preset_mode_twice(hass, mqtt_mock): assert state.attributes.get("preset_mode") == "hold-on" -async def test_set_aux_pessimistic(hass, mqtt_mock): +async def test_set_aux_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["aux_state_topic"] = "aux-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("aux_heat") == "off" @@ -979,10 +1025,11 @@ async def test_set_aux_pessimistic(hass, mqtt_mock): assert state.attributes.get("aux_heat") == "off" -async def test_set_aux(hass, mqtt_mock): +async def test_set_aux(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("aux_heat") == "off" @@ -998,35 +1045,39 @@ async def test_set_aux(hass, mqtt_mock): assert state.attributes.get("aux_heat") == "off" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, caplog): +async def test_get_target_temperature_low_high_with_templates( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test getting temperature high/low with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_low_state_topic"] = "temperature-state" @@ -1036,6 +1087,7 @@ async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, c assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) @@ -1060,7 +1112,7 @@ async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, c assert state.attributes.get("target_temp_high") == 1032 -async def test_get_with_templates(hass, mqtt_mock, caplog): +async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test getting various attributes with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) # By default, just unquote the JSON-strings @@ -1081,6 +1133,7 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): config["climate"]["preset_mode_state_topic"] = "current-preset-mode" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # Operation Mode state = hass.states.get(ENTITY_CLIMATE) @@ -1159,7 +1212,9 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog): +async def test_get_with_hold_and_away_mode_and_templates( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test getting various for hold and away mode attributes with templates.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["mode_state_topic"] = "mode-state" @@ -1172,6 +1227,7 @@ async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # Operation Mode state = hass.states.get(ENTITY_CLIMATE) @@ -1206,7 +1262,7 @@ async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog assert state.attributes.get("preset_mode") == "somemode" -async def test_set_and_templates(hass, mqtt_mock, caplog): +async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting various attributes with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) # Create simple templates @@ -1220,6 +1276,7 @@ async def test_set_and_templates(hass, mqtt_mock, caplog): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # Fan Mode await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) @@ -1284,7 +1341,9 @@ async def test_set_and_templates(hass, mqtt_mock, caplog): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplog): +async def test_set_with_away_and_hold_modes_and_templates( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting various attributes on hold and away mode with templates.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) # Create simple templates @@ -1292,6 +1351,7 @@ async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplo assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # Hold Mode await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) @@ -1303,13 +1363,14 @@ async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplo assert state.attributes.get("preset_mode") == PRESET_ECO -async def test_min_temp_custom(hass, mqtt_mock): +async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom min temp.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["min_temp"] = 26 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) min_temp = state.attributes.get("min_temp") @@ -1318,13 +1379,14 @@ async def test_min_temp_custom(hass, mqtt_mock): assert state.attributes.get("min_temp") == 26 -async def test_max_temp_custom(hass, mqtt_mock): +async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom max temp.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["max_temp"] = 60 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) max_temp = state.attributes.get("max_temp") @@ -1333,13 +1395,14 @@ async def test_max_temp_custom(hass, mqtt_mock): assert max_temp == 60 -async def test_temp_step_custom(hass, mqtt_mock): +async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom temp step.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temp_step"] = 0.01 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) temp_step = state.attributes.get("target_temp_step") @@ -1348,7 +1411,7 @@ async def test_temp_step_custom(hass, mqtt_mock): assert temp_step == 0.01 -async def test_temperature_unit(hass, mqtt_mock): +async def test_temperature_unit(hass, mqtt_mock_entry_with_yaml_config): """Test that setting temperature unit converts temperature values.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_unit"] = "F" @@ -1356,6 +1419,7 @@ async def test_temperature_unit(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "current_temperature", "77") @@ -1363,49 +1427,61 @@ async def test_temperature_unit(hass, mqtt_mock): assert state.attributes.get("current_temperature") == 25 -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG, MQTT_CLIMATE_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + CLIMATE_DOMAIN, + DEFAULT_CONFIG, + MQTT_CLIMATE_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one climate per unique_id.""" config = { CLIMATE_DOMAIN: [ @@ -1425,7 +1501,9 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, CLIMATE_DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, config + ) @pytest.mark.parametrize( @@ -1449,7 +1527,13 @@ async def test_unique_id(hass, mqtt_mock): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) @@ -1460,7 +1544,7 @@ async def test_encoding_subscribable_topics( del config["preset_mode_command_topic"] await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, config, @@ -1471,71 +1555,80 @@ async def test_encoding_subscribable_topics( ) -async def test_discovery_removal_climate(hass, mqtt_mock, caplog): +async def test_discovery_removal_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered climate.""" data = json.dumps(DEFAULT_CONFIG[CLIMATE_DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, data + ) -async def test_discovery_update_climate(hass, mqtt_mock, caplog): +async def test_discovery_update_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered climate.""" config1 = {"name": "Beer"} config2 = {"name": "Milk"} await help_test_discovery_update( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_climate(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_climate( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered climate.""" data1 = '{ "name": "Beer" }' with patch( "homeassistant.components.mqtt.climate.MqttClimate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + CLIMATE_DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "power_command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "power_command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { CLIMATE_DOMAIN: { @@ -1546,18 +1639,22 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock): } } await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, CLIMATE_DOMAIN, config, ["test-topic", "avty-topic"] + hass, + mqtt_mock_entry_with_yaml_config, + CLIMATE_DOMAIN, + config, + ["test-topic", "avty-topic"], ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { CLIMATE_DOMAIN: { @@ -1569,7 +1666,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): } await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, config, climate.SERVICE_TURN_ON, @@ -1579,10 +1676,11 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_precision_default(hass, mqtt_mock): +async def test_precision_default(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to tenths works as intended.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -1592,12 +1690,13 @@ async def test_precision_default(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_precision_halves(hass, mqtt_mock): +async def test_precision_halves(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to halves works as intended.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["precision"] = 0.5 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -1607,12 +1706,13 @@ async def test_precision_halves(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_precision_whole(hass, mqtt_mock): +async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to whole works as intended.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["precision"] = 1.0 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -1721,7 +1821,7 @@ async def test_precision_whole(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1738,7 +1838,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1750,11 +1850,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = CLIMATE_DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index b5bb5732617..50cf7beb0e0 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -46,10 +46,13 @@ DEFAULT_CONFIG_DEVICE_INFO_MAC = { _SENTINEL = object() -async def help_test_availability_when_connection_lost(hass, mqtt_mock, domain, config): +async def help_test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config, domain, config +): """Test availability after MQTT disconnection.""" assert await async_setup_component(hass, domain, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state != STATE_UNAVAILABLE @@ -62,11 +65,14 @@ async def help_test_availability_when_connection_lost(hass, mqtt_mock, domain, c assert state.state == STATE_UNAVAILABLE -async def help_test_availability_without_topic(hass, mqtt_mock, domain, config): +async def help_test_availability_without_topic( + hass, mqtt_mock_entry_with_yaml_config, domain, config +): """Test availability without defined availability topic.""" assert "availability_topic" not in config[domain] assert await async_setup_component(hass, domain, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state != STATE_UNAVAILABLE @@ -74,7 +80,7 @@ async def help_test_availability_without_topic(hass, mqtt_mock, domain, config): async def help_test_default_availability_payload( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -94,6 +100,7 @@ async def help_test_default_availability_payload( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -124,7 +131,7 @@ async def help_test_default_availability_payload( async def help_test_default_availability_list_payload( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -147,6 +154,7 @@ async def help_test_default_availability_list_payload( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -189,7 +197,7 @@ async def help_test_default_availability_list_payload( async def help_test_default_availability_list_payload_all( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -213,6 +221,7 @@ async def help_test_default_availability_list_payload_all( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -256,7 +265,7 @@ async def help_test_default_availability_list_payload_all( async def help_test_default_availability_list_payload_any( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -280,6 +289,7 @@ async def help_test_default_availability_list_payload_any( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -318,7 +328,7 @@ async def help_test_default_availability_list_payload_any( async def help_test_default_availability_list_single( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -342,6 +352,7 @@ async def help_test_default_availability_list_single( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state is None @@ -353,7 +364,7 @@ async def help_test_default_availability_list_single( async def help_test_custom_availability_payload( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -375,6 +386,7 @@ async def help_test_custom_availability_payload( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -405,7 +417,7 @@ async def help_test_custom_availability_payload( async def help_test_discovery_update_availability( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, domain, config, no_assumed_state=False, @@ -416,6 +428,7 @@ async def help_test_discovery_update_availability( This is a test helper for the MQTTAvailability mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add availability settings to config config1 = copy.deepcopy(config) config1[domain]["availability_topic"] = "availability-topic1" @@ -484,7 +497,7 @@ async def help_test_discovery_update_availability( async def help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, domain, config + hass, mqtt_mock_entry_with_yaml_config, domain, config ): """Test the setting of attribute via MQTT with JSON payload. @@ -499,6 +512,7 @@ async def help_test_setting_attribute_via_mqtt_json_message( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "attr-topic", '{ "val": "100" }') state = hass.states.get(f"{domain}.test") @@ -507,12 +521,13 @@ async def help_test_setting_attribute_via_mqtt_json_message( async def help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, domain, config, extra_blocked_attributes + hass, mqtt_mock_entry_no_yaml_config, domain, config, extra_blocked_attributes ): """Test the setting of blocked attribute via MQTT with JSON payload. This is a test helper for the MqttAttributes mixin. """ + await mqtt_mock_entry_no_yaml_config() extra_blocked_attributes = extra_blocked_attributes or [] # Add JSON attributes settings to config @@ -534,7 +549,9 @@ async def help_test_setting_blocked_attribute_via_mqtt_json_message( assert state.attributes.get(attr) != val -async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, config): +async def help_test_setting_attribute_with_template( + hass, mqtt_mock_entry_with_yaml_config, domain, config +): """Test the setting of attribute via MQTT with JSON payload. This is a test helper for the MqttAttributes mixin. @@ -549,6 +566,7 @@ async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, con config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, "attr-topic", json.dumps({"Timer1": {"Arm": 0, "Time": "22:18"}}) @@ -560,7 +578,7 @@ async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, con async def help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, domain, config + hass, mqtt_mock_entry_with_yaml_config, caplog, domain, config ): """Test attributes get extracted from a JSON result. @@ -575,6 +593,7 @@ async def help_test_update_with_json_attrs_not_dict( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "attr-topic", '[ "list", "of", "things"]') state = hass.states.get(f"{domain}.test") @@ -584,7 +603,7 @@ async def help_test_update_with_json_attrs_not_dict( async def help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, domain, config + hass, mqtt_mock_entry_with_yaml_config, caplog, domain, config ): """Test JSON validation of attributes. @@ -599,6 +618,7 @@ async def help_test_update_with_json_attrs_bad_JSON( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "attr-topic", "This is not JSON") @@ -607,11 +627,14 @@ async def help_test_update_with_json_attrs_bad_JSON( assert "Erroneous JSON: This is not JSON" in caplog.text -async def help_test_discovery_update_attr(hass, mqtt_mock, caplog, domain, config): +async def help_test_discovery_update_attr( + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, config +): """Test update of discovered MQTTAttributes. This is a test helper for the MqttAttributes mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add JSON attributes settings to config config1 = copy.deepcopy(config) config1[domain]["json_attributes_topic"] = "attr-topic1" @@ -641,18 +664,22 @@ async def help_test_discovery_update_attr(hass, mqtt_mock, caplog, domain, confi assert state.attributes.get("val") == "75" -async def help_test_unique_id(hass, mqtt_mock, domain, config): +async def help_test_unique_id(hass, mqtt_mock_entry_with_yaml_config, domain, config): """Test unique id option only creates one entity per unique_id.""" assert await async_setup_component(hass, domain, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert len(hass.states.async_entity_ids(domain)) == 1 -async def help_test_discovery_removal(hass, mqtt_mock, caplog, domain, data): +async def help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, data +): """Test removal of discovered component. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() @@ -669,7 +696,7 @@ async def help_test_discovery_removal(hass, mqtt_mock, caplog, domain, data): async def help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, domain, discovery_config1, @@ -681,6 +708,7 @@ async def help_test_discovery_update( This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add some future configuration to the configurations config1 = copy.deepcopy(discovery_config1) config1["some_future_option_1"] = "future_option_1" @@ -730,12 +758,13 @@ async def help_test_discovery_update( async def help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, domain, data1, discovery_update + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, data1, discovery_update ): """Test update of discovered component without changes. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) await hass.async_block_till_done() @@ -749,8 +778,11 @@ async def help_test_discovery_update_unchanged( assert not discovery_update.called -async def help_test_discovery_broken(hass, mqtt_mock, caplog, domain, data1, data2): +async def help_test_discovery_broken( + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, data1, data2 +): """Test handling of bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) await hass.async_block_till_done() @@ -769,7 +801,7 @@ async def help_test_discovery_broken(hass, mqtt_mock, caplog, domain, data1, dat async def help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -849,6 +881,7 @@ async def help_test_encoding_subscribable_topics( hass, domain, {domain: [config1, config2, config3]} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() expected_result = attribute_value or value @@ -899,11 +932,14 @@ async def help_test_encoding_subscribable_topics( pass -async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_with_identifier( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry integration. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -926,11 +962,14 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, assert device.configuration_url == "http://example.com" -async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_with_connection( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry integration. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_MAC) @@ -955,8 +994,11 @@ async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, assert device.configuration_url == "http://example.com" -async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_remove( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry remove.""" + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -981,11 +1023,14 @@ async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") -async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_update( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry update. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1012,7 +1057,7 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): async def help_test_entity_id_update_subscriptions( - hass, mqtt_mock, domain, config, topics=None + hass, mqtt_mock_entry_with_yaml_config, domain, config, topics=None ): """Test MQTT subscriptions are managed when entity_id is updated.""" # Add unique_id to config @@ -1026,16 +1071,18 @@ async def help_test_entity_id_update_subscriptions( topics = ["avty-topic", "test-topic"] assert len(topics) > 0 registry = mock_registry(hass, {}) + assert await async_setup_component( hass, domain, config, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state is not None - assert mqtt_mock.async_subscribe.call_count == len(topics) + assert mqtt_mock.async_subscribe.call_count == len(topics) + 3 for topic in topics: mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mqtt_mock.async_subscribe.reset_mock() @@ -1053,10 +1100,11 @@ async def help_test_entity_id_update_subscriptions( async def help_test_entity_id_update_discovery_update( - hass, mqtt_mock, domain, config, topic=None + hass, mqtt_mock_entry_no_yaml_config, domain, config, topic=None ): """Test MQTT discovery update after entity_id is updated.""" # Add unique_id to config + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(config) config[domain]["unique_id"] = "TOTALLY_UNIQUE" @@ -1093,11 +1141,14 @@ async def help_test_entity_id_update_discovery_update( assert state.state != STATE_UNAVAILABLE -async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1127,11 +1178,14 @@ async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): assert len(debug_info_data["triggers"]) == 0 -async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info_max_messages( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info message overflow. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1181,7 +1235,7 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf async def help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, domain, config, service, @@ -1196,6 +1250,7 @@ async def help_test_entity_debug_info_message( This is a test helper for MQTT debug_info. """ # Add device settings to config + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1290,11 +1345,14 @@ async def help_test_entity_debug_info_message( assert debug_info_data["entities"][0]["transmitted"] == expected_transmissions -async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info_remove( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1333,11 +1391,14 @@ async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): 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): +async def help_test_entity_debug_info_update_entity_id( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1389,8 +1450,11 @@ async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, ) -async def help_test_entity_disabled_by_default(hass, mqtt_mock, domain, config): +async def help_test_entity_disabled_by_default( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry remove.""" + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1425,8 +1489,11 @@ async def help_test_entity_disabled_by_default(hass, mqtt_mock, domain, config): assert not dev_registry.async_get_device({("mqtt", "helloworld")}) -async def help_test_entity_category(hass, mqtt_mock, domain, config): +async def help_test_entity_category( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry remove.""" + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1468,7 +1535,7 @@ async def help_test_entity_category(hass, mqtt_mock, domain, config): async def help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1519,6 +1586,7 @@ async def help_test_publishing_with_custom_encoding( {domain: setup_config}, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # 1) test with default encoding await hass.services.async_call( @@ -1602,7 +1670,9 @@ async def help_test_reload_with_config(hass, caplog, tmp_path, domain, config): assert "" in caplog.text -async def help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config): +async def help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config +): """Test reloading an MQTT platform.""" # Create and test an old config of 2 entities based on the config supplied old_config_1 = copy.deepcopy(config) @@ -1614,6 +1684,7 @@ async def help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config hass, domain, {domain: [old_config_1, old_config_2]} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get(f"{domain}.test_old_1") assert hass.states.get(f"{domain}.test_old_2") diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index f0e02ad8a3a..c9784a81f80 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -264,8 +264,9 @@ async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_set assert len(mock_finish_setup.mock_calls) == 1 -async def test_option_flow(hass, mqtt_mock, mock_try_connection): +async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connection): """Test config flow options.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_try_connection.return_value = True config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] config_entry.data = { @@ -336,8 +337,11 @@ async def test_option_flow(hass, mqtt_mock, mock_try_connection): assert mqtt_mock.async_connect.call_count == 1 -async def test_disable_birth_will(hass, mqtt_mock, mock_try_connection): +async def test_disable_birth_will( + hass, mqtt_mock_entry_no_yaml_config, mock_try_connection +): """Test disabling birth and will.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_try_connection.return_value = True config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] config_entry.data = { @@ -417,9 +421,10 @@ def get_suggested(schema, key): async def test_option_flow_default_suggested_values( - hass, mqtt_mock, mock_try_connection_success + hass, mqtt_mock_entry_no_yaml_config, mock_try_connection_success ): """Test config flow options has default/suggested values.""" + await mqtt_mock_entry_no_yaml_config() config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] config_entry.data = { mqtt.CONF_BROKER: "test-broker", diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index e130b820c1b..5796c12f3cf 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -83,7 +83,7 @@ DEFAULT_CONFIG = { } -async def test_state_via_state_topic(hass, mqtt_mock): +async def test_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -102,6 +102,7 @@ async def test_state_via_state_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -118,7 +119,9 @@ async def test_state_via_state_topic(hass, mqtt_mock): assert state.state == STATE_OPEN -async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_mock): +async def test_opening_and_closing_state_via_custom_state_payload( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling opening and closing state via a custom payload.""" assert await async_setup_component( hass, @@ -139,6 +142,7 @@ async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_moc }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -160,7 +164,9 @@ async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_moc assert state.state == STATE_CLOSED -async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): +async def test_open_closed_state_from_position_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the state after setting the position using optimistic mode.""" assert await async_setup_component( hass, @@ -180,6 +186,7 @@ async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -207,7 +214,7 @@ async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_position_via_position_topic(hass, mqtt_mock): +async def test_position_via_position_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -228,6 +235,7 @@ async def test_position_via_position_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -244,7 +252,7 @@ async def test_position_via_position_topic(hass, mqtt_mock): assert state.state == STATE_OPEN -async def test_state_via_template(hass, mqtt_mock): +async def test_state_via_template(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -266,6 +274,7 @@ async def test_state_via_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -281,7 +290,7 @@ async def test_state_via_template(hass, mqtt_mock): assert state.state == STATE_CLOSED -async def test_state_via_template_and_entity_id(hass, mqtt_mock): +async def test_state_via_template_and_entity_id(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -303,6 +312,7 @@ async def test_state_via_template_and_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -320,7 +330,9 @@ async def test_state_via_template_and_entity_id(hass, mqtt_mock): assert state.state == STATE_CLOSED -async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): +async def test_state_via_template_with_json_value( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic with JSON value.""" assert await async_setup_component( hass, @@ -337,6 +349,7 @@ async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -359,7 +372,9 @@ async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): ) in caplog.text -async def test_position_via_template_and_entity_id(hass, mqtt_mock): +async def test_position_via_template_and_entity_id( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -381,6 +396,7 @@ async def test_position_via_template_and_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -411,7 +427,9 @@ async def test_position_via_template_and_entity_id(hass, mqtt_mock): ({"tilt_command_topic": "abc", "tilt_status_topic": "abc"}, False), ], ) -async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): +async def test_optimistic_flag( + hass, mqtt_mock_entry_with_yaml_config, config, assumed_state +): """Test assumed_state is set correctly.""" assert await async_setup_component( hass, @@ -419,6 +437,7 @@ async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): {cover.DOMAIN: {**config, "platform": "mqtt", "name": "test", "qos": 0}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -428,7 +447,7 @@ async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): assert ATTR_ASSUMED_STATE not in state.attributes -async def test_optimistic_state_change(hass, mqtt_mock): +async def test_optimistic_state_change(hass, mqtt_mock_entry_with_yaml_config): """Test changing state optimistically.""" assert await async_setup_component( hass, @@ -443,6 +462,7 @@ async def test_optimistic_state_change(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -484,7 +504,9 @@ async def test_optimistic_state_change(hass, mqtt_mock): assert state.state == STATE_CLOSED -async def test_optimistic_state_change_with_position(hass, mqtt_mock): +async def test_optimistic_state_change_with_position( + hass, mqtt_mock_entry_with_yaml_config +): """Test changing state optimistically.""" assert await async_setup_component( hass, @@ -501,6 +523,7 @@ async def test_optimistic_state_change_with_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -547,7 +570,7 @@ async def test_optimistic_state_change_with_position(hass, mqtt_mock): assert state.attributes.get(ATTR_CURRENT_POSITION) == 0 -async def test_send_open_cover_command(hass, mqtt_mock): +async def test_send_open_cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of open_cover.""" assert await async_setup_component( hass, @@ -563,6 +586,7 @@ async def test_send_open_cover_command(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -576,7 +600,7 @@ async def test_send_open_cover_command(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_send_close_cover_command(hass, mqtt_mock): +async def test_send_close_cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of close_cover.""" assert await async_setup_component( hass, @@ -592,6 +616,7 @@ async def test_send_close_cover_command(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -605,7 +630,7 @@ async def test_send_close_cover_command(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_send_stop__cover_command(hass, mqtt_mock): +async def test_send_stop__cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of stop_cover.""" assert await async_setup_component( hass, @@ -621,6 +646,7 @@ async def test_send_stop__cover_command(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -634,7 +660,7 @@ async def test_send_stop__cover_command(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_current_cover_position(hass, mqtt_mock): +async def test_current_cover_position(hass, mqtt_mock_entry_with_yaml_config): """Test the current cover position.""" assert await async_setup_component( hass, @@ -654,6 +680,7 @@ async def test_current_cover_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_POSITION not in state_attributes_dict @@ -685,7 +712,7 @@ async def test_current_cover_position(hass, mqtt_mock): assert current_cover_position == 100 -async def test_current_cover_position_inverted(hass, mqtt_mock): +async def test_current_cover_position_inverted(hass, mqtt_mock_entry_with_yaml_config): """Test the current cover position.""" assert await async_setup_component( hass, @@ -705,6 +732,7 @@ async def test_current_cover_position_inverted(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_POSITION not in state_attributes_dict @@ -747,7 +775,7 @@ async def test_current_cover_position_inverted(hass, mqtt_mock): assert hass.states.get("cover.test").state == STATE_CLOSED -async def test_optimistic_position(hass, mqtt_mock): +async def test_optimistic_position(hass, mqtt_mock_entry_no_yaml_config): """Test optimistic position is not supported.""" assert await async_setup_component( hass, @@ -762,12 +790,13 @@ async def test_optimistic_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("cover.test") assert state is None -async def test_position_update(hass, mqtt_mock): +async def test_position_update(hass, mqtt_mock_entry_with_yaml_config): """Test cover position update from received MQTT message.""" assert await async_setup_component( hass, @@ -788,6 +817,7 @@ async def test_position_update(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_POSITION not in state_attributes_dict @@ -809,7 +839,7 @@ async def test_position_update(hass, mqtt_mock): [("{{position-1}}", 43, "42"), ("{{100-62}}", 100, "38")], ) async def test_set_position_templated( - hass, mqtt_mock, pos_template, pos_call, pos_message + hass, mqtt_mock_entry_with_yaml_config, pos_template, pos_call, pos_message ): """Test setting cover position via template.""" assert await async_setup_component( @@ -832,6 +862,7 @@ async def test_set_position_templated( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -845,7 +876,9 @@ async def test_set_position_templated( ) -async def test_set_position_templated_and_attributes(hass, mqtt_mock): +async def test_set_position_templated_and_attributes( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting cover position via template and using entities attributes.""" assert await async_setup_component( hass, @@ -876,6 +909,7 @@ async def test_set_position_templated_and_attributes(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -887,7 +921,7 @@ async def test_set_position_templated_and_attributes(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("set-position-topic", "5", 0, False) -async def test_set_tilt_templated(hass, mqtt_mock): +async def test_set_tilt_templated(hass, mqtt_mock_entry_with_yaml_config): """Test setting cover tilt position via template.""" assert await async_setup_component( hass, @@ -911,6 +945,7 @@ async def test_set_tilt_templated(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -924,7 +959,9 @@ async def test_set_tilt_templated(hass, mqtt_mock): ) -async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): +async def test_set_tilt_templated_and_attributes( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting cover tilt position via template and using entities attributes.""" assert await async_setup_component( hass, @@ -952,6 +989,7 @@ async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1010,7 +1048,7 @@ async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): ) -async def test_set_position_untemplated(hass, mqtt_mock): +async def test_set_position_untemplated(hass, mqtt_mock_entry_with_yaml_config): """Test setting cover position via template.""" assert await async_setup_component( hass, @@ -1029,6 +1067,7 @@ async def test_set_position_untemplated(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1040,7 +1079,9 @@ async def test_set_position_untemplated(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("position-topic", "62", 0, False) -async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock): +async def test_set_position_untemplated_custom_percentage_range( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting cover position via template.""" assert await async_setup_component( hass, @@ -1061,6 +1102,7 @@ async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock) }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1072,7 +1114,7 @@ async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock) mqtt_mock.async_publish.assert_called_once_with("position-topic", "62", 0, False) -async def test_no_command_topic(hass, mqtt_mock): +async def test_no_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test with no command topic.""" assert await async_setup_component( hass, @@ -1091,11 +1133,12 @@ async def test_no_command_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 240 -async def test_no_payload_close(hass, mqtt_mock): +async def test_no_payload_close(hass, mqtt_mock_entry_with_yaml_config): """Test with no close payload.""" assert await async_setup_component( hass, @@ -1113,11 +1156,12 @@ async def test_no_payload_close(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 9 -async def test_no_payload_open(hass, mqtt_mock): +async def test_no_payload_open(hass, mqtt_mock_entry_with_yaml_config): """Test with no open payload.""" assert await async_setup_component( hass, @@ -1135,11 +1179,12 @@ async def test_no_payload_open(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 10 -async def test_no_payload_stop(hass, mqtt_mock): +async def test_no_payload_stop(hass, mqtt_mock_entry_with_yaml_config): """Test with no stop payload.""" assert await async_setup_component( hass, @@ -1157,11 +1202,12 @@ async def test_no_payload_stop(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 3 -async def test_with_command_topic_and_tilt(hass, mqtt_mock): +async def test_with_command_topic_and_tilt(hass, mqtt_mock_entry_with_yaml_config): """Test with command topic and tilt config.""" assert await async_setup_component( hass, @@ -1181,11 +1227,12 @@ async def test_with_command_topic_and_tilt(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 251 -async def test_tilt_defaults(hass, mqtt_mock): +async def test_tilt_defaults(hass, mqtt_mock_entry_with_yaml_config): """Test the defaults.""" assert await async_setup_component( hass, @@ -1206,6 +1253,7 @@ async def test_tilt_defaults(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_TILT_POSITION in state_attributes_dict @@ -1216,7 +1264,7 @@ async def test_tilt_defaults(hass, mqtt_mock): assert current_cover_position == STATE_UNKNOWN -async def test_tilt_via_invocation_defaults(hass, mqtt_mock): +async def test_tilt_via_invocation_defaults(hass, mqtt_mock_entry_with_yaml_config): """Test tilt defaults on close/open.""" assert await async_setup_component( hass, @@ -1237,6 +1285,7 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1298,7 +1347,7 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("tilt-command-topic", "0", 0, False) -async def test_tilt_given_value(hass, mqtt_mock): +async def test_tilt_given_value(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, @@ -1321,6 +1370,7 @@ async def test_tilt_given_value(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1386,7 +1436,7 @@ async def test_tilt_given_value(hass, mqtt_mock): ) -async def test_tilt_given_value_optimistic(hass, mqtt_mock): +async def test_tilt_given_value_optimistic(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, @@ -1410,6 +1460,7 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1462,7 +1513,7 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock): ) -async def test_tilt_given_value_altered_range(hass, mqtt_mock): +async def test_tilt_given_value_altered_range(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, @@ -1488,6 +1539,7 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1538,7 +1590,7 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock): ) -async def test_tilt_via_topic(hass, mqtt_mock): +async def test_tilt_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test tilt by updating status via MQTT.""" assert await async_setup_component( hass, @@ -1559,6 +1611,7 @@ async def test_tilt_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1575,7 +1628,7 @@ async def test_tilt_via_topic(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_via_topic_template(hass, mqtt_mock): +async def test_tilt_via_topic_template(hass, mqtt_mock_entry_with_yaml_config): """Test tilt by updating status via MQTT and template.""" assert await async_setup_component( hass, @@ -1599,6 +1652,7 @@ async def test_tilt_via_topic_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "99") @@ -1615,7 +1669,9 @@ async def test_tilt_via_topic_template(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_via_topic_template_json_value(hass, mqtt_mock, caplog): +async def test_tilt_via_topic_template_json_value( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test tilt by updating status via MQTT and template with JSON value.""" assert await async_setup_component( hass, @@ -1639,6 +1695,7 @@ async def test_tilt_via_topic_template_json_value(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", '{"Var1": 9, "Var2": 30}') @@ -1661,7 +1718,7 @@ async def test_tilt_via_topic_template_json_value(hass, mqtt_mock, caplog): ) in caplog.text -async def test_tilt_via_topic_altered_range(hass, mqtt_mock): +async def test_tilt_via_topic_altered_range(hass, mqtt_mock_entry_with_yaml_config): """Test tilt status via MQTT with altered tilt range.""" assert await async_setup_component( hass, @@ -1684,6 +1741,7 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1707,7 +1765,9 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_status_out_of_range_warning(hass, caplog, mqtt_mock): +async def test_tilt_status_out_of_range_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT tilt out of range warning message.""" assert await async_setup_component( hass, @@ -1730,6 +1790,7 @@ async def test_tilt_status_out_of_range_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "60") @@ -1738,7 +1799,9 @@ async def test_tilt_status_out_of_range_warning(hass, caplog, mqtt_mock): ) in caplog.text -async def test_tilt_status_not_numeric_warning(hass, caplog, mqtt_mock): +async def test_tilt_status_not_numeric_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT tilt not numeric warning message.""" assert await async_setup_component( hass, @@ -1761,13 +1824,16 @@ async def test_tilt_status_not_numeric_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "abc") assert ("Payload 'abc' is not numeric") in caplog.text -async def test_tilt_via_topic_altered_range_inverted(hass, mqtt_mock): +async def test_tilt_via_topic_altered_range_inverted( + hass, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT with altered tilt range and inverted tilt position.""" assert await async_setup_component( hass, @@ -1790,6 +1856,7 @@ async def test_tilt_via_topic_altered_range_inverted(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1813,7 +1880,9 @@ async def test_tilt_via_topic_altered_range_inverted(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): +async def test_tilt_via_topic_template_altered_range( + hass, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT and template with altered tilt range.""" assert await async_setup_component( hass, @@ -1839,6 +1908,7 @@ async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "99") @@ -1862,7 +1932,7 @@ async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_position(hass, mqtt_mock): +async def test_tilt_position(hass, mqtt_mock_entry_with_yaml_config): """Test tilt via method invocation.""" assert await async_setup_component( hass, @@ -1883,6 +1953,7 @@ async def test_tilt_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1896,7 +1967,7 @@ async def test_tilt_position(hass, mqtt_mock): ) -async def test_tilt_position_templated(hass, mqtt_mock): +async def test_tilt_position_templated(hass, mqtt_mock_entry_with_yaml_config): """Test tilt position via template.""" assert await async_setup_component( hass, @@ -1918,6 +1989,7 @@ async def test_tilt_position_templated(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1931,7 +2003,7 @@ async def test_tilt_position_templated(hass, mqtt_mock): ) -async def test_tilt_position_altered_range(hass, mqtt_mock): +async def test_tilt_position_altered_range(hass, mqtt_mock_entry_with_yaml_config): """Test tilt via method invocation with altered range.""" assert await async_setup_component( hass, @@ -1956,6 +2028,7 @@ async def test_tilt_position_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1969,7 +2042,7 @@ async def test_tilt_position_altered_range(hass, mqtt_mock): ) -async def test_find_percentage_in_range_defaults(hass, mqtt_mock): +async def test_find_percentage_in_range_defaults(hass): """Test find percentage in range with default range.""" mqtt_cover = MqttCover( hass, @@ -2012,7 +2085,7 @@ async def test_find_percentage_in_range_defaults(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(44, "cover") == 44 -async def test_find_percentage_in_range_altered(hass, mqtt_mock): +async def test_find_percentage_in_range_altered(hass): """Test find percentage in range with altered range.""" mqtt_cover = MqttCover( hass, @@ -2055,7 +2128,7 @@ async def test_find_percentage_in_range_altered(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(120, "cover") == 40 -async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): +async def test_find_percentage_in_range_defaults_inverted(hass): """Test find percentage in range with default range but inverted.""" mqtt_cover = MqttCover( hass, @@ -2098,7 +2171,7 @@ async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(44, "cover") == 56 -async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): +async def test_find_percentage_in_range_altered_inverted(hass): """Test find percentage in range with altered range and inverted.""" mqtt_cover = MqttCover( hass, @@ -2141,7 +2214,7 @@ async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(120, "cover") == 60 -async def test_find_in_range_defaults(hass, mqtt_mock): +async def test_find_in_range_defaults(hass): """Test find in range with default range.""" mqtt_cover = MqttCover( hass, @@ -2184,7 +2257,7 @@ async def test_find_in_range_defaults(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(44, "cover") == 44 -async def test_find_in_range_altered(hass, mqtt_mock): +async def test_find_in_range_altered(hass): """Test find in range with altered range.""" mqtt_cover = MqttCover( hass, @@ -2227,7 +2300,7 @@ async def test_find_in_range_altered(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(40, "cover") == 120 -async def test_find_in_range_defaults_inverted(hass, mqtt_mock): +async def test_find_in_range_defaults_inverted(hass): """Test find in range with default range but inverted.""" mqtt_cover = MqttCover( hass, @@ -2270,7 +2343,7 @@ async def test_find_in_range_defaults_inverted(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(56, "cover") == 44 -async def test_find_in_range_altered_inverted(hass, mqtt_mock): +async def test_find_in_range_altered_inverted(hass): """Test find in range with altered range and inverted.""" mqtt_cover = MqttCover( hass, @@ -2313,35 +2386,37 @@ async def test_find_in_range_altered_inverted(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(60, "cover") == 120 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of a valid device class.""" assert await async_setup_component( hass, @@ -2356,12 +2431,13 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.attributes.get("device_class") == "garage" -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test the setting of an invalid device class.""" assert await async_setup_component( hass, @@ -2376,54 +2452,67 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("cover.test") assert state is None -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG, MQTT_COVER_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + cover.DOMAIN, + DEFAULT_CONFIG, + MQTT_COVER_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one cover per id.""" config = { cover.DOMAIN: [ @@ -2441,92 +2530,103 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, cover.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, config + ) -async def test_discovery_removal_cover(hass, mqtt_mock, caplog): +async def test_discovery_removal_cover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered cover.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, cover.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, data + ) -async def test_discovery_update_cover(hass, mqtt_mock, caplog): +async def test_discovery_update_cover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered cover.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, cover.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_cover(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_cover( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered cover.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.cover.MqttCover.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, cover.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + cover.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, cover.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG, SERVICE_OPEN_COVER, @@ -2535,7 +2635,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): async def test_state_and_position_topics_state_not_set_via_position_topic( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test state is not set via position topic when both state and position topics are set.""" assert await async_setup_component( @@ -2557,6 +2657,7 @@ async def test_state_and_position_topics_state_not_set_via_position_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -2593,7 +2694,9 @@ async def test_state_and_position_topics_state_not_set_via_position_topic( assert state.state == STATE_CLOSED -async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): +async def test_set_state_via_position_using_stopped_state( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via position topic using stopped state.""" assert await async_setup_component( hass, @@ -2615,6 +2718,7 @@ async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -2646,7 +2750,9 @@ async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): assert state.state == STATE_OPEN -async def test_position_via_position_topic_template(hass, mqtt_mock): +async def test_position_via_position_topic_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test position by updating status via position template.""" assert await async_setup_component( hass, @@ -2664,6 +2770,7 @@ async def test_position_via_position_topic_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "99") @@ -2680,7 +2787,9 @@ async def test_position_via_position_topic_template(hass, mqtt_mock): assert current_cover_position_position == 50 -async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, caplog): +async def test_position_via_position_topic_template_json_value( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test position by updating status via position template with a JSON value.""" assert await async_setup_component( hass, @@ -2698,6 +2807,7 @@ async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", '{"Var1": 9, "Var2": 60}') @@ -2720,7 +2830,7 @@ async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, ) in caplog.text -async def test_position_template_with_entity_id(hass, mqtt_mock): +async def test_position_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_config): """Test position by updating status via position template.""" assert await async_setup_component( hass, @@ -2743,6 +2853,7 @@ async def test_position_template_with_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "10") @@ -2759,7 +2870,9 @@ async def test_position_template_with_entity_id(hass, mqtt_mock): assert current_cover_position_position == 20 -async def test_position_via_position_topic_template_return_json(hass, mqtt_mock): +async def test_position_via_position_topic_template_return_json( + hass, mqtt_mock_entry_with_yaml_config +): """Test position by updating status via position template and returning json.""" assert await async_setup_component( hass, @@ -2777,6 +2890,7 @@ async def test_position_via_position_topic_template_return_json(hass, mqtt_mock) }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "55") @@ -2787,7 +2901,7 @@ async def test_position_via_position_topic_template_return_json(hass, mqtt_mock) async def test_position_via_position_topic_template_return_json_warning( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_with_yaml_config ): """Test position by updating status via position template returning json without position attribute.""" assert await async_setup_component( @@ -2806,6 +2920,7 @@ async def test_position_via_position_topic_template_return_json_warning( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "55") @@ -2816,7 +2931,7 @@ async def test_position_via_position_topic_template_return_json_warning( async def test_position_and_tilt_via_position_topic_template_return_json( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test position and tilt by updating the position via position template.""" assert await async_setup_component( @@ -2836,6 +2951,7 @@ async def test_position_and_tilt_via_position_topic_template_return_json( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "0") @@ -2857,7 +2973,9 @@ async def test_position_and_tilt_via_position_topic_template_return_json( assert current_cover_position == 99 and current_tilt_position == 49 -async def test_position_via_position_topic_template_all_variables(hass, mqtt_mock): +async def test_position_via_position_topic_template_all_variables( + hass, mqtt_mock_entry_with_yaml_config +): """Test position by updating status via position template.""" assert await async_setup_component( hass, @@ -2886,6 +3004,7 @@ async def test_position_via_position_topic_template_all_variables(hass, mqtt_moc }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "0") @@ -2901,7 +3020,9 @@ async def test_position_via_position_topic_template_all_variables(hass, mqtt_moc assert current_cover_position == 100 -async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): +async def test_set_state_via_stopped_state_no_position_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via stopped state when no position topic.""" assert await async_setup_component( hass, @@ -2923,6 +3044,7 @@ async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "state-topic", "OPEN") @@ -2951,7 +3073,7 @@ async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): async def test_position_via_position_topic_template_return_invalid_json( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_with_yaml_config ): """Test position by updating status via position template and returning invalid json.""" assert await async_setup_component( @@ -2970,6 +3092,7 @@ async def test_position_via_position_topic_template_return_invalid_json( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "55") @@ -2977,7 +3100,7 @@ async def test_position_via_position_topic_template_return_invalid_json( async def test_set_position_topic_without_get_position_topic_error( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when set_position_topic is used without position_topic.""" assert await async_setup_component( @@ -2994,13 +3117,16 @@ async def test_set_position_topic_without_get_position_topic_error( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) in caplog.text -async def test_value_template_without_state_topic_error(hass, caplog, mqtt_mock): +async def test_value_template_without_state_topic_error( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test error when value_template is used and state_topic is missing.""" assert await async_setup_component( hass, @@ -3015,13 +3141,16 @@ async def test_value_template_without_state_topic_error(hass, caplog, mqtt_mock) }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." ) in caplog.text -async def test_position_template_without_position_topic_error(hass, caplog, mqtt_mock): +async def test_position_template_without_position_topic_error( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test error when position_template is used and position_topic is missing.""" assert await async_setup_component( hass, @@ -3036,6 +3165,7 @@ async def test_position_template_without_position_topic_error(hass, caplog, mqtt }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." @@ -3044,7 +3174,7 @@ async def test_position_template_without_position_topic_error(hass, caplog, mqtt async def test_set_position_template_without_set_position_topic( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when set_position_template is used and set_position_topic is missing.""" assert await async_setup_component( @@ -3060,6 +3190,7 @@ async def test_set_position_template_without_set_position_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." @@ -3068,7 +3199,7 @@ async def test_set_position_template_without_set_position_topic( async def test_tilt_command_template_without_tilt_command_topic( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when tilt_command_template is used and tilt_command_topic is missing.""" assert await async_setup_component( @@ -3084,6 +3215,7 @@ async def test_tilt_command_template_without_tilt_command_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." @@ -3092,7 +3224,7 @@ async def test_tilt_command_template_without_tilt_command_topic( async def test_tilt_status_template_without_tilt_status_topic_topic( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when tilt_status_template is used and tilt_status_topic is missing.""" assert await async_setup_component( @@ -3108,6 +3240,7 @@ async def test_tilt_status_template_without_tilt_status_topic_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." @@ -3143,7 +3276,7 @@ async def test_tilt_status_template_without_tilt_status_topic_topic( ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -3158,7 +3291,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -3170,11 +3303,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = cover.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -3194,12 +3329,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG[cover.DOMAIN], diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 020fbad6166..34042105af2 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -11,7 +11,9 @@ from tests.common import async_fire_mqtt_message # Deprecated in HA Core 2022.6 -async def test_legacy_ensure_device_tracker_platform_validation(hass, mqtt_mock): +async def test_legacy_ensure_device_tracker_platform_validation( + hass, mqtt_mock_entry_with_yaml_config +): """Test if platform validation was done.""" async def mock_setup_scanner(hass, config, see, discovery_info=None): @@ -29,12 +31,17 @@ async def test_legacy_ensure_device_tracker_platform_validation(hass, mqtt_mock) assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert mock_sp.call_count == 1 # Deprecated in HA Core 2022.6 -async def test_legacy_new_message(hass, mock_device_tracker_conf, mqtt_mock): +async def test_legacy_new_message( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config +): """Test new message.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" @@ -51,9 +58,10 @@ async def test_legacy_new_message(hass, mock_device_tracker_conf, mqtt_mock): # Deprecated in HA Core 2022.6 async def test_legacy_single_level_wildcard_topic( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test single level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/+/paulus" @@ -73,9 +81,10 @@ async def test_legacy_single_level_wildcard_topic( # Deprecated in HA Core 2022.6 async def test_legacy_multi_level_wildcard_topic( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test multi level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/#" @@ -95,9 +104,10 @@ async def test_legacy_multi_level_wildcard_topic( # Deprecated in HA Core 2022.6 async def test_legacy_single_level_wildcard_topic_not_matching( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test not matching single level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/+/paulus" @@ -117,9 +127,10 @@ async def test_legacy_single_level_wildcard_topic_not_matching( # Deprecated in HA Core 2022.6 async def test_legacy_multi_level_wildcard_topic_not_matching( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test not matching multi level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/#" @@ -139,9 +150,10 @@ async def test_legacy_multi_level_wildcard_topic_not_matching( # Deprecated in HA Core 2022.6 async def test_legacy_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" @@ -172,9 +184,10 @@ async def test_legacy_matching_custom_payload_for_home_and_not_home( # Deprecated in HA Core 2022.6 async def test_legacy_not_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test not matching payload does not set state to home or not_home.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" @@ -202,8 +215,11 @@ async def test_legacy_not_matching_custom_payload_for_home_and_not_home( # Deprecated in HA Core 2022.6 -async def test_legacy_matching_source_type(hass, mock_device_tracker_conf, mqtt_mock): +async def test_legacy_matching_source_type( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config +): """Test setting source type.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index f8ee94b58f9..31853ad1dee 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -33,8 +33,9 @@ def entity_reg(hass): return mock_registry(hass) -async def test_discover_device_tracker(hass, mqtt_mock, caplog): +async def test_discover_device_tracker(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT device tracker component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -50,8 +51,9 @@ async def test_discover_device_tracker(hass, mqtt_mock, caplog): @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -74,8 +76,11 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): assert state.name == "Beer" -async def test_non_duplicate_device_tracker_discovery(hass, mqtt_mock, caplog): +async def test_non_duplicate_device_tracker_discovery( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -97,8 +102,9 @@ async def test_non_duplicate_device_tracker_discovery(hass, mqtt_mock, caplog): assert "Component has already been discovered: device_tracker bla" in caplog.text -async def test_device_tracker_removal(hass, mqtt_mock, caplog): +async def test_device_tracker_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of component through empty discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -114,8 +120,9 @@ async def test_device_tracker_removal(hass, mqtt_mock, caplog): assert state is None -async def test_device_tracker_rediscover(hass, mqtt_mock, caplog): +async def test_device_tracker_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test rediscover of removed component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -140,8 +147,11 @@ async def test_device_tracker_rediscover(hass, mqtt_mock, caplog): assert state is not None -async def test_duplicate_device_tracker_removal(hass, mqtt_mock, caplog): +async def test_duplicate_device_tracker_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -160,8 +170,11 @@ async def test_duplicate_device_tracker_removal(hass, mqtt_mock, caplog): ) -async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): +async def test_device_tracker_discovery_update( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test for a discovery update event.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -186,10 +199,12 @@ async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): async def test_cleanup_device_tracker( - hass, hass_ws_client, device_reg, entity_reg, mqtt_mock + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test discovered device is cleaned up when removed from registry.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) async_fire_mqtt_message( @@ -242,8 +257,11 @@ async def test_cleanup_device_tracker( ) -async def test_setting_device_tracker_value_via_mqtt_message(hass, mqtt_mock, caplog): +async def test_setting_device_tracker_value_via_mqtt_message( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -266,9 +284,10 @@ async def test_setting_device_tracker_value_via_mqtt_message(hass, mqtt_mock, ca async def test_setting_device_tracker_value_via_mqtt_message_and_template( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -290,9 +309,10 @@ async def test_setting_device_tracker_value_via_mqtt_message_and_template( async def test_setting_device_tracker_value_via_mqtt_message_and_template2( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -317,9 +337,10 @@ async def test_setting_device_tracker_value_via_mqtt_message_and_template2( async def test_setting_device_tracker_location_via_mqtt_message( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the location via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -337,9 +358,10 @@ async def test_setting_device_tracker_location_via_mqtt_message( async def test_setting_device_tracker_location_via_lat_lon_message( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the latitude and longitude via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -391,8 +413,14 @@ async def test_setting_device_tracker_location_via_lat_lon_message( assert state.state == STATE_UNKNOWN -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, device_tracker.DOMAIN, DEFAULT_CONFIG, None + hass, + mqtt_mock_entry_no_yaml_config, + device_tracker.DOMAIN, + DEFAULT_CONFIG, + None, ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index ef5dd22692f..fe08c85a853 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -39,8 +39,11 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_get_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test we get the expected triggers from a discovered mqtt device.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -70,8 +73,11 @@ async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock): assert_lists_same(triggers, expected_triggers) -async def test_get_unknown_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_get_unknown_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test we don't get unknown triggers.""" + await mqtt_mock_entry_no_yaml_config() # Discover a sensor (without device triggers) data1 = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -112,8 +118,11 @@ async def test_get_unknown_triggers(hass, device_reg, entity_reg, mqtt_mock): assert_lists_same(triggers, []) -async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_get_non_existing_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test getting non existing triggers.""" + await mqtt_mock_entry_no_yaml_config() # Discover a sensor (without device triggers) data1 = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -131,8 +140,11 @@ async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock @pytest.mark.no_fail_on_log_exception -async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_discover_bad_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() # Test sending bad data data0 = ( '{ "automation_type":"trigger",' @@ -176,8 +188,11 @@ async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): assert_lists_same(triggers, expected_triggers) -async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_update_remove_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test triggers can be updated and removed.""" + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "device": {"identifiers": ["0AFFD2"]}, @@ -240,8 +255,11 @@ async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): assert device_entry is None -async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock): +async def test_if_fires_on_mqtt_message( + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config +): """Test triggers firing.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -313,8 +331,11 @@ async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock): assert calls[1].data["some"] == "long_press" -async def test_if_fires_on_mqtt_message_template(hass, device_reg, calls, mqtt_mock): +async def test_if_fires_on_mqtt_message_template( + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config +): """Test triggers firing.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -389,9 +410,10 @@ async def test_if_fires_on_mqtt_message_template(hass, device_reg, calls, mqtt_m async def test_if_fires_on_mqtt_message_late_discover( - hass, device_reg, calls, mqtt_mock + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers firing of MQTT device triggers discovered after setup.""" + await mqtt_mock_entry_no_yaml_config() data0 = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -472,9 +494,10 @@ async def test_if_fires_on_mqtt_message_late_discover( async def test_if_fires_on_mqtt_message_after_update( - hass, device_reg, calls, mqtt_mock + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers firing after update.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -515,6 +538,7 @@ async def test_if_fires_on_mqtt_message_after_update( ] }, ) + await hass.async_block_till_done() # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "") @@ -546,8 +570,11 @@ async def test_if_fires_on_mqtt_message_after_update( assert len(calls) == 3 -async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): +async def test_no_resubscribe_same_topic( + hass, device_reg, mqtt_mock_entry_no_yaml_config +): """Test subscription to topics without change.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -589,9 +616,10 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( - hass, device_reg, calls, mqtt_mock + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers not firing after removal.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -625,6 +653,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( ] }, ) + await hass.async_block_till_done() # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") @@ -649,10 +678,13 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( async def test_not_fires_on_mqtt_message_after_remove_from_registry( - hass, hass_ws_client, device_reg, calls, mqtt_mock + hass, hass_ws_client, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers not firing after removal.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() + ws_client = await hass_ws_client(hass) data1 = ( @@ -713,8 +745,9 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( assert len(calls) == 1 -async def test_attach_remove(hass, device_reg, mqtt_mock): +async def test_attach_remove(hass, device_reg, mqtt_mock_entry_no_yaml_config): """Test attach and removal of trigger.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -766,8 +799,9 @@ async def test_attach_remove(hass, device_reg, mqtt_mock): assert len(calls) == 1 -async def test_attach_remove_late(hass, device_reg, mqtt_mock): +async def test_attach_remove_late(hass, device_reg, mqtt_mock_entry_no_yaml_config): """Test attach and removal of trigger .""" + await mqtt_mock_entry_no_yaml_config() data0 = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -827,8 +861,9 @@ async def test_attach_remove_late(hass, device_reg, mqtt_mock): assert len(calls) == 1 -async def test_attach_remove_late2(hass, device_reg, mqtt_mock): +async def test_attach_remove_late2(hass, device_reg, mqtt_mock_entry_no_yaml_config): """Test attach and removal of trigger .""" + await mqtt_mock_entry_no_yaml_config() data0 = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -882,8 +917,9 @@ async def test_attach_remove_late2(hass, device_reg, mqtt_mock): assert len(calls) == 0 -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -915,8 +951,9 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -946,8 +983,9 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) config = { @@ -983,8 +1021,11 @@ async def test_entity_device_info_update(hass, mqtt_mock): assert device.name == "Milk" -async def test_cleanup_trigger(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_trigger( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test trigger discovery topic is cleaned when device is removed from registry.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -1034,8 +1075,11 @@ async def test_cleanup_trigger(hass, hass_ws_client, device_reg, entity_reg, mqt ) -async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry when trigger is removed.""" + await mqtt_mock_entry_no_yaml_config() config = { "automation_type": "trigger", "topic": "test-topic", @@ -1065,8 +1109,11 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): assert device_entry is None -async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_several_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry when the last trigger is removed.""" + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "topic": "test-topic", @@ -1122,11 +1169,14 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt assert device_entry is None -async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_with_entity1( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry for device with entity. Trigger removed first, then entity. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "topic": "test-topic", @@ -1178,11 +1228,14 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo assert device_entry is None -async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_with_entity2( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry for device with entity. Entity removed first, then trigger. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "topic": "test-topic", @@ -1234,11 +1287,12 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo assert device_entry is None -async def test_trigger_debug_info(hass, mqtt_mock): +async def test_trigger_debug_info(hass, mqtt_mock_entry_no_yaml_config): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) config1 = { diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index bbd42a20c87..65399a22f70 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -37,8 +37,11 @@ def device_reg(hass): return mock_device_registry(hass) -async def test_entry_diagnostics(hass, device_reg, hass_client, mqtt_mock): +async def test_entry_diagnostics( + hass, device_reg, hass_client, mqtt_mock_entry_no_yaml_config +): """Test config entry diagnostics.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] mqtt_mock.connected = True @@ -154,8 +157,11 @@ async def test_entry_diagnostics(hass, device_reg, hass_client, mqtt_mock): } ], ) -async def test_redact_diagnostics(hass, device_reg, hass_client, mqtt_mock): +async def test_redact_diagnostics( + hass, device_reg, hass_client, mqtt_mock_entry_no_yaml_config +): """Test redacting diagnostics.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() expected_config = dict(default_config) expected_config["password"] = "**REDACTED**" expected_config["username"] = "**REDACTED**" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 9215ab651b2..df20dc031d0 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -48,8 +48,9 @@ def entity_reg(hass): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -async def test_subscribing_config_topic(hass, mqtt_mock): +async def test_subscribing_config_topic(hass, mqtt_mock_entry_no_yaml_config): """Test setting up discovery.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] discovery_topic = "homeassistant" @@ -71,8 +72,9 @@ async def test_subscribing_config_topic(hass, mqtt_mock): ("homeassistant/binary_sensor/rörkrökare/config", True), ], ) -async def test_invalid_topic(hass, mqtt_mock, caplog, topic, log): +async def test_invalid_topic(hass, mqtt_mock_entry_no_yaml_config, caplog, topic, log): """Test sending to invalid topic.""" + await mqtt_mock_entry_no_yaml_config() with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: @@ -90,8 +92,9 @@ async def test_invalid_topic(hass, mqtt_mock, caplog, topic, log): caplog.clear() -async def test_invalid_json(hass, mqtt_mock, caplog): +async def test_invalid_json(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in invalid JSON.""" + await mqtt_mock_entry_no_yaml_config() with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: @@ -106,8 +109,9 @@ async def test_invalid_json(hass, mqtt_mock, caplog): assert not mock_dispatcher_send.called -async def test_only_valid_components(hass, mqtt_mock, caplog): +async def test_only_valid_components(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a valid component.""" + await mqtt_mock_entry_no_yaml_config() with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: @@ -127,8 +131,9 @@ async def test_only_valid_components(hass, mqtt_mock, caplog): assert not mock_dispatcher_send.called -async def test_correct_config_discovery(hass, mqtt_mock, caplog): +async def test_correct_config_discovery(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in correct JSON.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -143,8 +148,9 @@ async def test_correct_config_discovery(hass, mqtt_mock, caplog): assert ("binary_sensor", "bla") in hass.data[ALREADY_DISCOVERED] -async def test_discover_fan(hass, mqtt_mock, caplog): +async def test_discover_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT fan.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/fan/bla/config", @@ -159,8 +165,9 @@ async def test_discover_fan(hass, mqtt_mock, caplog): assert ("fan", "bla") in hass.data[ALREADY_DISCOVERED] -async def test_discover_climate(hass, mqtt_mock, caplog): +async def test_discover_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT climate component.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "name": "ClimateTest",' ' "current_temperature_topic": "climate/bla/current_temp",' @@ -177,8 +184,11 @@ async def test_discover_climate(hass, mqtt_mock, caplog): assert ("climate", "bla") in hass.data[ALREADY_DISCOVERED] -async def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): +async def test_discover_alarm_control_panel( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test discovering an MQTT alarm control panel component.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "name": "AlarmControlPanelTest",' ' "state_topic": "test_topic",' @@ -341,9 +351,10 @@ async def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): ], ) async def test_discovery_with_object_id( - hass, mqtt_mock, caplog, topic, config, entity_id, name, domain + hass, mqtt_mock_entry_no_yaml_config, caplog, topic, config, entity_id, name, domain ): """Test discovering an MQTT entity with object_id.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, topic, config) await hass.async_block_till_done() @@ -354,8 +365,9 @@ async def test_discovery_with_object_id( assert (domain, "object bla") in hass.data[ALREADY_DISCOVERED] -async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): +async def test_discovery_incl_nodeid(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in correct JSON with optional node_id included.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/my_node_id/bla/config", @@ -370,8 +382,9 @@ async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): assert ("binary_sensor", "my_node_id bla") in hass.data[ALREADY_DISCOVERED] -async def test_non_duplicate_discovery(hass, mqtt_mock, caplog): +async def test_non_duplicate_discovery(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -393,8 +406,9 @@ async def test_non_duplicate_discovery(hass, mqtt_mock, caplog): assert "Component has already been discovered: binary_sensor bla" in caplog.text -async def test_removal(hass, mqtt_mock, caplog): +async def test_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of component through empty discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -410,8 +424,9 @@ async def test_removal(hass, mqtt_mock, caplog): assert state is None -async def test_rediscover(hass, mqtt_mock, caplog): +async def test_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test rediscover of removed component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -436,9 +451,9 @@ async def test_rediscover(hass, mqtt_mock, caplog): assert state is not None -async def test_rapid_rediscover(hass, mqtt_mock, caplog): +async def test_rapid_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate rediscover of removed component.""" - + await mqtt_mock_entry_no_yaml_config() events = async_capture_events(hass, EVENT_STATE_CHANGED) async_fire_mqtt_message( @@ -485,9 +500,9 @@ async def test_rapid_rediscover(hass, mqtt_mock, caplog): assert events[4].data["old_state"] is None -async def test_rapid_rediscover_unique(hass, mqtt_mock, caplog): +async def test_rapid_rediscover_unique(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate rediscover of removed component.""" - + await mqtt_mock_entry_no_yaml_config() events = [] @ha.callback @@ -544,9 +559,9 @@ async def test_rapid_rediscover_unique(hass, mqtt_mock, caplog): assert events[3].data["old_state"] is None -async def test_rapid_reconfigure(hass, mqtt_mock, caplog): +async def test_rapid_reconfigure(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate reconfigure of added component.""" - + await mqtt_mock_entry_no_yaml_config() events = [] @ha.callback @@ -596,8 +611,9 @@ async def test_rapid_reconfigure(hass, mqtt_mock, caplog): assert events[2].data["new_state"].attributes["friendly_name"] == "Wine" -async def test_duplicate_removal(hass, mqtt_mock, caplog): +async def test_duplicate_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -614,8 +630,11 @@ async def test_duplicate_removal(hass, mqtt_mock, caplog): assert "Component has already been discovered: binary_sensor bla" not in caplog.text -async def test_cleanup_device(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test discvered device is cleaned up when entry removed from device.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -669,8 +688,11 @@ async def test_cleanup_device(hass, hass_ws_client, device_reg, entity_reg, mqtt ) -async def test_cleanup_device_mqtt(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_mqtt( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test discvered device is cleaned up when removed through MQTT.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() data = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -709,10 +731,12 @@ async def test_cleanup_device_mqtt(hass, device_reg, entity_reg, mqtt_mock): async def test_cleanup_device_multiple_config_entries( - hass, hass_ws_client, device_reg, entity_reg, mqtt_mock + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test discovered device is cleaned up when entry removed from device.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) config_entry = MockConfigEntry(domain="test", data={}) @@ -804,9 +828,10 @@ async def test_cleanup_device_multiple_config_entries( async def test_cleanup_device_multiple_config_entries_mqtt( - hass, device_reg, entity_reg, mqtt_mock + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test discovered device is cleaned up when removed through MQTT.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -880,8 +905,9 @@ async def test_cleanup_device_multiple_config_entries_mqtt( mqtt_mock.async_publish.assert_not_called() -async def test_discovery_expansion(hass, mqtt_mock, caplog): +async def test_discovery_expansion(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of abbreviated discovery payload.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -937,8 +963,9 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): assert state.state == STATE_UNAVAILABLE -async def test_discovery_expansion_2(hass, mqtt_mock, caplog): +async def test_discovery_expansion_2(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of abbreviated discovery payload.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -977,8 +1004,9 @@ async def test_discovery_expansion_2(hass, mqtt_mock, caplog): @pytest.mark.no_fail_on_log_exception -async def test_discovery_expansion_3(hass, mqtt_mock, caplog): +async def test_discovery_expansion_3(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of broken discovery payload.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -1008,9 +1036,10 @@ async def test_discovery_expansion_3(hass, mqtt_mock, caplog): async def test_discovery_expansion_without_encoding_and_value_template_1( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test expansion of raw availability payload with a template as list.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -1056,9 +1085,10 @@ async def test_discovery_expansion_without_encoding_and_value_template_1( async def test_discovery_expansion_without_encoding_and_value_template_2( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test expansion of raw availability payload with a template directly.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -1133,8 +1163,11 @@ ABBREVIATIONS_WHITE_LIST = [ ] -async def test_missing_discover_abbreviations(hass, mqtt_mock, caplog): +async def test_missing_discover_abbreviations( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Check MQTT platforms for missing abbreviations.""" + await mqtt_mock_entry_no_yaml_config() missing = [] regex = re.compile(r"(CONF_[a-zA-Z\d_]*) *= *[\'\"]([a-zA-Z\d_]*)[\'\"]") for fil in Path(mqtt.__file__).parent.rglob("*.py"): @@ -1157,8 +1190,11 @@ async def test_missing_discover_abbreviations(hass, mqtt_mock, caplog): assert not missing -async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog): +async def test_no_implicit_state_topic_switch( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test no implicit state topic for switch.""" + await mqtt_mock_entry_no_yaml_config() data = '{ "name": "Test1",' ' "command_topic": "cmnd"' "}" async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) @@ -1187,8 +1223,11 @@ async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog): } ], ) -async def test_complex_discovery_topic_prefix(hass, mqtt_mock, caplog): +async def test_complex_discovery_topic_prefix( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Tests handling of discovery topic prefix with multiple slashes.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, ("my_home/homeassistant/register/binary_sensor/node1/object1/config"), @@ -1204,9 +1243,10 @@ async def test_complex_discovery_topic_prefix(hass, mqtt_mock, caplog): async def test_mqtt_integration_discovery_subscribe_unsubscribe( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Check MQTT integration discovery subscribe and unsubscribe.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_entity_platform(hass, "config_flow.comp", None) entry = hass.config_entries.async_entries("mqtt")[0] @@ -1243,8 +1283,11 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( assert not mqtt_client_mock.unsubscribe.called -async def test_mqtt_discovery_unsubscribe_once(hass, mqtt_client_mock, mqtt_mock): +async def test_mqtt_discovery_unsubscribe_once( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Check MQTT integration discovery unsubscribe once.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_entity_platform(hass, "config_flow.comp", None) entry = hass.config_entries.async_entries("mqtt")[0] diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 1a533db63c0..145edf5ac7d 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -74,16 +74,25 @@ DEFAULT_CONFIG = { } -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test if command fails with command topic.""" assert await async_setup_component( hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "mqtt", "name": "test"}} ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("fan.test") is None + assert ( + "Invalid config for [fan.mqtt]: required key not provided @ data['command_topic']" + in caplog.text + ) -async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -120,6 +129,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -202,7 +212,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): async def test_controlling_state_via_topic_with_different_speed_range( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic using an alternate speed range.""" assert await async_setup_component( @@ -241,6 +251,7 @@ async def test_controlling_state_via_topic_with_different_speed_range( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "percentage-state-topic1", "100") state = hass.states.get("fan.test1") @@ -264,7 +275,7 @@ async def test_controlling_state_via_topic_with_different_speed_range( async def test_controlling_state_via_topic_no_percentage_topics( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic without percentage topics.""" assert await async_setup_component( @@ -289,6 +300,7 @@ async def test_controlling_state_via_topic_no_percentage_topics( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -318,7 +330,9 @@ async def test_controlling_state_via_topic_no_percentage_topics( caplog.clear() -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic and JSON message (percentage mode).""" assert await async_setup_component( hass, @@ -353,6 +367,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -421,7 +436,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap async def test_controlling_state_via_topic_and_json_message_shared_topic( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic and JSON message using a shared topic.""" assert await async_setup_component( @@ -457,6 +472,7 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -509,7 +525,9 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( caplog.clear() -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -535,6 +553,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -630,7 +649,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock): +async def test_sending_mqtt_commands_with_alternate_speed_range( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic using an alternate speed range.""" assert await async_setup_component( hass, @@ -668,6 +689,7 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock) }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_percentage(hass, "fan.test1", 0) mqtt_mock.async_publish.assert_called_once_with( @@ -734,7 +756,9 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock) assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_optimistic_no_legacy( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic without legacy speed command topic.""" assert await async_setup_component( hass, @@ -755,6 +779,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -862,7 +887,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c await common.async_turn_on(hass, "fan.test", preset_mode="freaking-high") -async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): +async def test_sending_mqtt_command_templates_( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic without legacy speed command topic.""" assert await async_setup_component( hass, @@ -888,6 +915,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1002,7 +1030,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test optimistic mode without state topic without percentage command topic.""" assert await async_setup_component( @@ -1025,6 +1053,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1061,7 +1090,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode with state topic and turn on attributes.""" assert await async_setup_component( hass, @@ -1088,6 +1119,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1305,7 +1337,13 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[fan.DOMAIN]) @@ -1315,7 +1353,7 @@ async def test_encoding_subscribable_topics( config[CONF_OSCILLATION_COMMAND_TOPIC] = "fan/some_oscillation_command_topic" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, config, @@ -1326,7 +1364,7 @@ async def test_encoding_subscribable_topics( ) -async def test_attributes(hass, mqtt_mock, caplog): +async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test attributes.""" assert await async_setup_component( hass, @@ -1347,6 +1385,7 @@ async def test_attributes(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1376,7 +1415,7 @@ async def test_attributes(hass, mqtt_mock, caplog): assert state.attributes.get(fan.ATTR_OSCILLATING) is False -async def test_supported_features(hass, mqtt_mock): +async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -1498,6 +1537,7 @@ async def test_supported_features(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test1") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 @@ -1548,77 +1588,103 @@ async def test_supported_features(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, MQTT_FAN_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + MQTT_FAN_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one fan per id.""" config = { fan.DOMAIN: [ @@ -1638,89 +1704,107 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, fan.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, config + ) -async def test_discovery_removal_fan(hass, mqtt_mock, caplog): +async def test_discovery_removal_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered fan.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, fan.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, data + ) -async def test_discovery_update_fan(hass, mqtt_mock, caplog): +async def test_discovery_update_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered fan.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, fan.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_fan(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_fan( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered fan.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.fan.MqttFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, fan.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + fan.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken(hass, mqtt_mock, caplog, fan.DOMAIN, data1, data2) + + await help_test_discovery_broken( + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, data1, data2 + ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, fan.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + fan.SERVICE_TURN_ON, ) @@ -1766,7 +1850,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1782,7 +1866,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1794,11 +1878,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = fan.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index bc00d3afffb..ea9a6edf0e3 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -116,7 +116,7 @@ async def async_set_humidity( await hass.services.async_call(DOMAIN, SERVICE_SET_HUMIDITY, data, blocking=True) -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test if command fails with command topic.""" assert await async_setup_component( hass, @@ -124,10 +124,13 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): {humidifier.DOMAIN: {"platform": "mqtt", "name": "test"}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("humidifier.test") is None -async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -158,6 +161,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -228,7 +232,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): assert state.state == STATE_UNKNOWN -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -255,6 +261,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -314,7 +321,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap async def test_controlling_state_via_topic_and_json_message_shared_topic( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic and JSON message using a shared topic.""" assert await async_setup_component( @@ -342,6 +349,7 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -390,7 +398,9 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( caplog.clear() -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -413,6 +423,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -483,7 +494,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): +async def test_sending_mqtt_command_templates_( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Testing command templates with optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -507,6 +520,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -577,7 +591,9 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode with state topic and turn on attributes.""" assert await async_setup_component( hass, @@ -602,6 +618,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -701,7 +718,13 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[humidifier.DOMAIN]) @@ -709,7 +732,7 @@ async def test_encoding_subscribable_topics( config[CONF_MODE_COMMAND_TOPIC] = "humidifier/some_mode_command_topic" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, config, @@ -720,7 +743,7 @@ async def test_encoding_subscribable_topics( ) -async def test_attributes(hass, mqtt_mock, caplog): +async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test attributes.""" assert await async_setup_component( hass, @@ -740,6 +763,7 @@ async def test_attributes(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -765,7 +789,7 @@ async def test_attributes(hass, mqtt_mock, caplog): assert state.attributes.get(humidifier.ATTR_MODE) is None -async def test_invalid_configurations(hass, mqtt_mock, caplog): +async def test_invalid_configurations(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test invalid configurations.""" assert await async_setup_component( hass, @@ -834,6 +858,7 @@ async def test_invalid_configurations(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("humidifier.test_valid_1") is not None assert hass.states.get("humidifier.test_valid_2") is not None assert hass.states.get("humidifier.test_valid_3") is not None @@ -847,7 +872,7 @@ async def test_invalid_configurations(hass, mqtt_mock, caplog): assert hass.states.get("humidifier.test_invalid_mode_is_reset") is None -async def test_supported_features(hass, mqtt_mock): +async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test supported features.""" assert await async_setup_component( hass, @@ -896,6 +921,7 @@ async def test_supported_features(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test1") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 @@ -916,81 +942,111 @@ async def test_supported_features(hass, mqtt_mock): assert state is None -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + humidifier.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + humidifier.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG, MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + humidifier.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + humidifier.DOMAIN, + DEFAULT_CONFIG, ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one fan per id.""" config = { humidifier.DOMAIN: [ @@ -1012,16 +1068,24 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, humidifier.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, config + ) -async def test_discovery_removal_humidifier(hass, mqtt_mock, caplog): +async def test_discovery_removal_humidifier( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test removal of discovered humidifier.""" data = '{ "name": "test", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, humidifier.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, data + ) -async def test_discovery_update_humidifier(hass, mqtt_mock, caplog): +async def test_discovery_update_humidifier( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered humidifier.""" config1 = { "name": "Beer", @@ -1034,77 +1098,93 @@ async def test_discovery_update_humidifier(hass, mqtt_mock, caplog): "target_humidity_command_topic": "test-topic2", } await help_test_discovery_update( - hass, mqtt_mock, caplog, humidifier.DOMAIN, config1, config2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + humidifier.DOMAIN, + config1, + config2, ) -async def test_discovery_update_unchanged_humidifier(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_humidifier( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered humidifier.""" data1 = '{ "name": "Beer", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' with patch( "homeassistant.components.mqtt.fan.MqttFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + humidifier.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, humidifier.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + humidifier.DOMAIN, + DEFAULT_CONFIG, + humidifier.SERVICE_TURN_ON, ) @@ -1143,7 +1223,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1159,7 +1239,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1171,11 +1251,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = humidifier.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index aa0bfb82608..861b50d2f71 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -96,22 +96,27 @@ def record_calls(calls): async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Test if client is connected after mqtt init on bootstrap.""" + await mqtt_mock_entry_no_yaml_config() assert mqtt_client_mock.connect.call_count == 1 -async def test_mqtt_disconnects_on_home_assistant_stop(hass, mqtt_mock): +async def test_mqtt_disconnects_on_home_assistant_stop( + hass, mqtt_mock_entry_no_yaml_config +): """Test if client stops on HA stop.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() assert mqtt_mock.async_disconnect.called -async def test_publish(hass, mqtt_mock): +async def test_publish(hass, mqtt_mock_entry_no_yaml_config): """Test the publish function.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await mqtt.async_publish(hass, "test-topic", "test-payload") await hass.async_block_till_done() assert mqtt_mock.async_publish.called @@ -208,7 +213,7 @@ async def test_command_template_value(hass): assert cmd_tpl.async_render(None, variables=variables) == "beer" -async def test_command_template_variables(hass, mqtt_mock): +async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config): """Test the rendering of enitity_variables.""" topic = "test/select" @@ -232,6 +237,7 @@ async def test_command_template_variables(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" @@ -291,8 +297,11 @@ async def test_value_template_value(hass): assert val_tpl.async_render_with_possible_json_value('{"id": 4321}') == "4321" -async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock): +async def test_service_call_without_topic_does_not_publish( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call if topic is missing.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() with pytest.raises(vol.Invalid): await hass.services.async_call( mqtt.DOMAIN, @@ -304,12 +313,13 @@ async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock): async def test_service_call_with_topic_and_topic_template_does_not_publish( - hass, mqtt_mock + hass, mqtt_mock_entry_no_yaml_config ): """Test the service call with topic/topic template. If both 'topic' and 'topic_template' are provided then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() topic = "test/topic" topic_template = "test/{{ 'topic' }}" with pytest.raises(vol.Invalid): @@ -327,9 +337,10 @@ async def test_service_call_with_topic_and_topic_template_does_not_publish( async def test_service_call_with_invalid_topic_template_does_not_publish( - hass, mqtt_mock + hass, mqtt_mock_entry_no_yaml_config ): """Test the service call with a problematic topic template.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -342,11 +353,14 @@ async def test_service_call_with_invalid_topic_template_does_not_publish( assert not mqtt_mock.async_publish.called -async def test_service_call_with_template_topic_renders_template(hass, mqtt_mock): +async def test_service_call_with_template_topic_renders_template( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with rendered topic template. If 'topic_template' is provided and 'topic' is not, then render it. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -360,11 +374,14 @@ async def test_service_call_with_template_topic_renders_template(hass, mqtt_mock assert mqtt_mock.async_publish.call_args[0][0] == "test/2" -async def test_service_call_with_template_topic_renders_invalid_topic(hass, mqtt_mock): +async def test_service_call_with_template_topic_renders_invalid_topic( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with rendered, invalid topic template. If a wildcard topic is rendered, then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -378,12 +395,13 @@ async def test_service_call_with_template_topic_renders_invalid_topic(hass, mqtt async def test_service_call_with_invalid_rendered_template_topic_doesnt_render_template( - hass, mqtt_mock + hass, mqtt_mock_entry_no_yaml_config ): """Test the service call with unrendered template. If both 'payload' and 'payload_template' are provided then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() payload = "not a template" payload_template = "a template" with pytest.raises(vol.Invalid): @@ -400,11 +418,14 @@ async def test_service_call_with_invalid_rendered_template_topic_doesnt_render_t assert not mqtt_mock.async_publish.called -async def test_service_call_with_template_payload_renders_template(hass, mqtt_mock): +async def test_service_call_with_template_payload_renders_template( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with rendered template. If 'payload_template' is provided and 'payload' is not, then render it. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -429,8 +450,9 @@ async def test_service_call_with_template_payload_renders_template(hass, mqtt_mo mqtt_mock.reset_mock() -async def test_service_call_with_bad_template(hass, mqtt_mock): +async def test_service_call_with_bad_template(hass, mqtt_mock_entry_no_yaml_config): """Test the service call with a bad template does not publish.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -440,11 +462,14 @@ async def test_service_call_with_bad_template(hass, mqtt_mock): assert not mqtt_mock.async_publish.called -async def test_service_call_with_payload_doesnt_render_template(hass, mqtt_mock): +async def test_service_call_with_payload_doesnt_render_template( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with unrendered template. If both 'payload' and 'payload_template' are provided then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() payload = "not a template" payload_template = "a template" with pytest.raises(vol.Invalid): @@ -461,11 +486,14 @@ async def test_service_call_with_payload_doesnt_render_template(hass, mqtt_mock) assert not mqtt_mock.async_publish.called -async def test_service_call_with_ascii_qos_retain_flags(hass, mqtt_mock): +async def test_service_call_with_ascii_qos_retain_flags( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with args that can be misinterpreted. Empty payload message and ascii formatted qos and retain flags. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -665,9 +693,10 @@ def test_entity_device_info_schema(): async def test_receiving_non_utf8_message_gets_logged( - hass, mqtt_mock, calls, record_calls, caplog + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls, caplog ): """Test receiving a non utf8 encoded message.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_mqtt_message(hass, "test-topic", b"\x9a") @@ -679,9 +708,10 @@ async def test_receiving_non_utf8_message_gets_logged( async def test_all_subscriptions_run_when_decode_fails( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test all other subscriptions still run when decode fails for one.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") await mqtt.async_subscribe(hass, "test-topic", record_calls) @@ -691,8 +721,11 @@ async def test_all_subscriptions_run_when_decode_fails( assert len(calls) == 1 -async def test_subscribe_topic(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of a topic.""" + await mqtt_mock_entry_no_yaml_config() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_mqtt_message(hass, "test-topic", "test-payload") @@ -714,8 +747,11 @@ async def test_subscribe_topic(hass, mqtt_mock, calls, record_calls): unsub() -async def test_subscribe_topic_non_async(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_non_async( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of a topic using the non-async function.""" + await mqtt_mock_entry_no_yaml_config() unsub = await hass.async_add_executor_job( mqtt.subscribe, hass, "test-topic", record_calls ) @@ -736,14 +772,18 @@ async def test_subscribe_topic_non_async(hass, mqtt_mock, calls, record_calls): assert len(calls) == 1 -async def test_subscribe_bad_topic(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_bad_topic( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of a topic.""" + await mqtt_mock_entry_no_yaml_config() with pytest.raises(HomeAssistantError): await mqtt.async_subscribe(hass, 55, record_calls) -async def test_subscribe_deprecated(hass, mqtt_mock): +async def test_subscribe_deprecated(hass, mqtt_mock_entry_no_yaml_config): """Test the subscription of a topic using deprecated callback signature.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() @callback def record_calls(topic, payload, qos): @@ -789,8 +829,9 @@ async def test_subscribe_deprecated(hass, mqtt_mock): assert len(calls) == 1 -async def test_subscribe_deprecated_async(hass, mqtt_mock): +async def test_subscribe_deprecated_async(hass, mqtt_mock_entry_no_yaml_config): """Test the subscription of a topic using deprecated coroutine signature.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() def async_record_calls(topic, payload, qos): """Record calls.""" @@ -835,8 +876,11 @@ async def test_subscribe_deprecated_async(hass, mqtt_mock): assert len(calls) == 1 -async def test_subscribe_topic_not_match(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_not_match( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test if subscribed topic is not a match.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_mqtt_message(hass, "another-test-topic", "test-payload") @@ -845,8 +889,11 @@ async def test_subscribe_topic_not_match(hass, mqtt_mock, calls, record_calls): assert len(calls) == 0 -async def test_subscribe_topic_level_wildcard(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_level_wildcard( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") @@ -858,9 +905,10 @@ async def test_subscribe_topic_level_wildcard(hass, mqtt_mock, calls, record_cal async def test_subscribe_topic_level_wildcard_no_subtree_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") @@ -870,9 +918,10 @@ async def test_subscribe_topic_level_wildcard_no_subtree_match( async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "test-topic-123", "test-payload") @@ -882,9 +931,10 @@ async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( async def test_subscribe_topic_subtree_wildcard_subtree_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") @@ -896,9 +946,10 @@ async def test_subscribe_topic_subtree_wildcard_subtree_topic( async def test_subscribe_topic_subtree_wildcard_root_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "test-topic", "test-payload") @@ -910,9 +961,10 @@ async def test_subscribe_topic_subtree_wildcard_root_topic( async def test_subscribe_topic_subtree_wildcard_no_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "another-test-topic", "test-payload") @@ -922,9 +974,10 @@ async def test_subscribe_topic_subtree_wildcard_no_match( async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") @@ -936,9 +989,10 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") @@ -950,9 +1004,10 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") @@ -962,9 +1017,10 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") @@ -973,8 +1029,11 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( assert len(calls) == 0 -async def test_subscribe_topic_sys_root(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_sys_root( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of $ root topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") @@ -986,9 +1045,10 @@ async def test_subscribe_topic_sys_root(hass, mqtt_mock, calls, record_calls): async def test_subscribe_topic_sys_root_and_wildcard_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of $ root and wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") @@ -1000,9 +1060,10 @@ async def test_subscribe_topic_sys_root_and_wildcard_topic( async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of $ root and wildcard subtree topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") @@ -1013,8 +1074,11 @@ async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( assert calls[0][0].payload == "test-payload" -async def test_subscribe_special_characters(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_special_characters( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription to topics with special characters.""" + await mqtt_mock_entry_no_yaml_config() topic = "/test-topic/$(.)[^]{-}" payload = "p4y.l[]a|> ?" @@ -1027,13 +1091,16 @@ async def test_subscribe_special_characters(hass, mqtt_mock, calls, record_calls assert calls[0][0].payload == payload -async def test_subscribe_same_topic(hass, mqtt_client_mock, mqtt_mock): +async def test_subscribe_same_topic( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """ Test subscring to same topic twice and simulate retained messages. When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again for it to resend any retained messages. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1061,9 +1128,10 @@ async def test_subscribe_same_topic(hass, mqtt_client_mock, mqtt_mock): async def test_not_calling_unsubscribe_with_active_subscribers( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1077,8 +1145,9 @@ async def test_not_calling_unsubscribe_with_active_subscribers( assert not mqtt_client_mock.unsubscribe.called -async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock): +async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1113,8 +1182,11 @@ async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_mock): +async def test_restore_subscriptions_on_reconnect( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test subscriptions are restored on reconnect.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1134,9 +1206,10 @@ async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_m [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_restore_all_active_subscriptions_on_reconnect( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Test active subscriptions are restored correctly on reconnect.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1176,9 +1249,10 @@ async def test_initial_setup_logs_error(hass, caplog, mqtt_client_mock): async def test_logs_error_if_no_connect_broker( - hass, caplog, mqtt_mock, mqtt_client_mock + hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock ): """Test for setup failure if connection to broker is missing.""" + await mqtt_mock_entry_no_yaml_config() # test with rc = 3 -> broker unavailable mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 3) await hass.async_block_till_done() @@ -1189,8 +1263,11 @@ async def test_logs_error_if_no_connect_broker( @patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) -async def test_handle_mqtt_on_callback(hass, caplog, mqtt_mock, mqtt_client_mock): +async def test_handle_mqtt_on_callback( + hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock +): """Test receiving an ACK callback before waiting for it.""" + await mqtt_mock_entry_no_yaml_config() # Simulate an ACK for mid == 1, this will call mqtt_mock._mqtt_handle_mid(mid) mqtt_client_mock.on_publish(mqtt_client_mock, None, 1) await hass.async_block_till_done() @@ -1224,8 +1301,11 @@ async def test_publish_error(hass, caplog): assert "Failed to connect to MQTT server: Out of memory." in caplog.text -async def test_handle_message_callback(hass, caplog, mqtt_mock, mqtt_client_mock): +async def test_handle_message_callback( + hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock +): """Test for handling an incoming message callback.""" + await mqtt_mock_entry_no_yaml_config() msg = ReceiveMessage("some-topic", b"test-payload", 0, False) mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) await mqtt.async_subscribe(hass, "some-topic", lambda *args: 0) @@ -1478,8 +1558,11 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): } ], ) -async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): +async def test_custom_birth_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test sending birth message.""" + await mqtt_mock_entry_no_yaml_config() birth = asyncio.Event() async def wait_birth(topic, payload, qos): @@ -1508,8 +1591,11 @@ async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): } ], ) -async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): +async def test_default_birth_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test sending birth message.""" + await mqtt_mock_entry_no_yaml_config() birth = asyncio.Event() async def wait_birth(topic, payload, qos): @@ -1530,8 +1616,9 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], ) -async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): +async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): """Test disabling birth message.""" + await mqtt_mock_entry_no_yaml_config() with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -1553,8 +1640,12 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): } ], ) -async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config, mqtt_mock): +async def test_delayed_birth_message( + hass, mqtt_client_mock, mqtt_config, mqtt_mock_entry_no_yaml_config +): """Test sending birth message does not happen until Home Assistant starts.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() + hass.state = CoreState.starting birth = asyncio.Event() @@ -1610,15 +1701,23 @@ async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config, mqtt_m } ], ) -async def test_custom_will_message(hass, mqtt_client_mock, mqtt_mock): +async def test_custom_will_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test will message.""" + await mqtt_mock_entry_no_yaml_config() + mqtt_client_mock.will_set.assert_called_with( topic="death", payload="death", qos=0, retain=False ) -async def test_default_will_message(hass, mqtt_client_mock, mqtt_mock): +async def test_default_will_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test will message.""" + await mqtt_mock_entry_no_yaml_config() + mqtt_client_mock.will_set.assert_called_with( topic="homeassistant/status", payload="offline", qos=0, retain=False ) @@ -1628,8 +1727,10 @@ async def test_default_will_message(hass, mqtt_client_mock, mqtt_mock): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], ) -async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock): +async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): """Test will message.""" + await mqtt_mock_entry_no_yaml_config() + mqtt_client_mock.will_set.assert_not_called() @@ -1643,8 +1744,12 @@ async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock): } ], ) -async def test_mqtt_subscribes_topics_on_connect(hass, mqtt_client_mock, mqtt_mock): +async def test_mqtt_subscribes_topics_on_connect( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test subscription to topic on connect.""" + await mqtt_mock_entry_no_yaml_config() + await mqtt.async_subscribe(hass, "topic/test", None) await mqtt.async_subscribe(hass, "home/sensor", None, 2) await mqtt.async_subscribe(hass, "still/pending", None) @@ -1662,7 +1767,9 @@ async def test_mqtt_subscribes_topics_on_connect(hass, mqtt_client_mock, mqtt_mo assert calls == expected -async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mock): +async def test_setup_entry_with_config_override( + hass, device_reg, mqtt_mock_entry_with_yaml_config +): """Test if the MQTT component loads with no config and config entry can be setup.""" data = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -1672,6 +1779,8 @@ async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mo # mqtt present in yaml config assert await async_setup_component(hass, mqtt.DOMAIN, {}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # User sets up a config entry entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) @@ -1734,8 +1843,11 @@ async def test_fail_no_broker(hass, device_reg, mqtt_client_mock, caplog): @pytest.mark.no_fail_on_log_exception -async def test_message_callback_exception_gets_logged(hass, caplog, mqtt_mock): +async def test_message_callback_exception_gets_logged( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test exception raised by message handler.""" + await mqtt_mock_entry_no_yaml_config() @callback def bad_handler(*args): @@ -1752,8 +1864,11 @@ async def test_message_callback_exception_gets_logged(hass, caplog, mqtt_mock): ) -async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): +async def test_mqtt_ws_subscription( + hass, hass_ws_client, mqtt_mock_entry_no_yaml_config +): """Test MQTT websocket subscription.""" + await mqtt_mock_entry_no_yaml_config() client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "mqtt/subscribe", "topic": "test-topic"}) response = await client.receive_json() @@ -1782,9 +1897,10 @@ async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): async def test_mqtt_ws_subscription_not_admin( - hass, hass_ws_client, mqtt_mock, hass_read_only_access_token + hass, hass_ws_client, mqtt_mock_entry_no_yaml_config, hass_read_only_access_token ): """Test MQTT websocket user is not admin.""" + await mqtt_mock_entry_no_yaml_config() client = await hass_ws_client(hass, access_token=hass_read_only_access_token) await client.send_json({"id": 5, "type": "mqtt/subscribe", "topic": "test-topic"}) response = await client.receive_json() @@ -1793,8 +1909,9 @@ async def test_mqtt_ws_subscription_not_admin( assert response["error"]["message"] == "Unauthorized" -async def test_dump_service(hass, mqtt_mock): +async def test_dump_service(hass, mqtt_mock_entry_no_yaml_config): """Test that we can dump a topic.""" + await mqtt_mock_entry_no_yaml_config() mopen = mock_open() await hass.services.async_call( @@ -1814,10 +1931,12 @@ async def test_dump_service(hass, mqtt_mock): async def test_mqtt_ws_remove_discovered_device( - hass, device_reg, entity_reg, hass_ws_client, mqtt_mock + hass, device_reg, entity_reg, hass_ws_client, mqtt_mock_entry_no_yaml_config ): """Test MQTT websocket device removal.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() data = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -1851,9 +1970,10 @@ async def test_mqtt_ws_remove_discovered_device( async def test_mqtt_ws_get_device_debug_info( - hass, device_reg, hass_ws_client, mqtt_mock + hass, device_reg, hass_ws_client, mqtt_mock_entry_no_yaml_config ): """Test MQTT websocket device debug info.""" + await mqtt_mock_entry_no_yaml_config() config_sensor = { "device": {"identifiers": ["0AFFD2"]}, "platform": "mqtt", @@ -1913,9 +2033,10 @@ async def test_mqtt_ws_get_device_debug_info( async def test_mqtt_ws_get_device_debug_info_binary( - hass, device_reg, hass_ws_client, mqtt_mock + hass, device_reg, hass_ws_client, mqtt_mock_entry_no_yaml_config ): """Test MQTT websocket device debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["0AFFD2"]}, "platform": "mqtt", @@ -1975,8 +2096,9 @@ async def test_mqtt_ws_get_device_debug_info_binary( assert response["result"] == expected_result -async def test_debug_info_multiple_devices(hass, mqtt_mock): +async def test_debug_info_multiple_devices(hass, mqtt_mock_entry_no_yaml_config): """Test we get correct debug_info when multiple devices are present.""" + await mqtt_mock_entry_no_yaml_config() devices = [ { "domain": "sensor", @@ -2054,8 +2176,11 @@ async def test_debug_info_multiple_devices(hass, mqtt_mock): assert discovery_data["payload"] == d["config"] -async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): +async def test_debug_info_multiple_entities_triggers( + hass, mqtt_mock_entry_no_yaml_config +): """Test we get correct debug_info for a device with multiple entities and triggers.""" + await mqtt_mock_entry_no_yaml_config() config = [ { "domain": "sensor", @@ -2137,8 +2262,11 @@ async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): } in discovery_data -async def test_debug_info_non_mqtt(hass, device_reg, entity_reg, mqtt_mock): +async def test_debug_info_non_mqtt( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test we get empty debug_info for a device with non MQTT entities.""" + await mqtt_mock_entry_no_yaml_config() DOMAIN = "sensor" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -2164,8 +2292,9 @@ async def test_debug_info_non_mqtt(hass, device_reg, entity_reg, mqtt_mock): assert len(debug_info_data["triggers"]) == 0 -async def test_debug_info_wildcard(hass, mqtt_mock): +async def test_debug_info_wildcard(hass, mqtt_mock_entry_no_yaml_config): """Test debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2210,8 +2339,9 @@ async def test_debug_info_wildcard(hass, mqtt_mock): } in debug_info_data["entities"][0]["subscriptions"] -async def test_debug_info_filter_same(hass, mqtt_mock): +async def test_debug_info_filter_same(hass, mqtt_mock_entry_no_yaml_config): """Test debug info removes messages with same timestamp.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2268,8 +2398,9 @@ async def test_debug_info_filter_same(hass, mqtt_mock): } == debug_info_data["entities"][0]["subscriptions"][0] -async def test_debug_info_same_topic(hass, mqtt_mock): +async def test_debug_info_same_topic(hass, mqtt_mock_entry_no_yaml_config): """Test debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2320,8 +2451,9 @@ async def test_debug_info_same_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) -async def test_debug_info_qos_retain(hass, mqtt_mock): +async def test_debug_info_qos_retain(hass, mqtt_mock_entry_no_yaml_config): """Test debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2377,8 +2509,10 @@ async def test_debug_info_qos_retain(hass, mqtt_mock): } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] -async def test_publish_json_from_template(hass, mqtt_mock): +async def test_publish_json_from_template(hass, mqtt_mock_entry_no_yaml_config): """Test the publishing of call to services.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() + test_str = "{'valid': 'python', 'invalid': 'json'}" test_str_tpl = "{'valid': '{{ \"python\" }}', 'invalid': 'json'}" @@ -2424,8 +2558,11 @@ async def test_publish_json_from_template(hass, mqtt_mock): assert mqtt_mock.async_publish.call_args[0][1] == test_str -async def test_subscribe_connection_status(hass, mqtt_mock, mqtt_client_mock): +async def test_subscribe_connection_status( + hass, mqtt_mock_entry_no_yaml_config, mqtt_client_mock +): """Test connextion status subscription.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mqtt_connected_calls = [] @callback @@ -2459,7 +2596,9 @@ async def test_subscribe_connection_status(hass, mqtt_mock, mqtt_client_mock): assert mqtt_connected_calls[1] is False -async def test_one_deprecation_warning_per_platform(hass, mqtt_mock, caplog): +async def test_one_deprecation_warning_per_platform( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test a deprecation warning is is logged once per platform.""" platform = "light" config = {"platform": "mqtt", "command_topic": "test-topic"} @@ -2469,6 +2608,7 @@ async def test_one_deprecation_warning_per_platform(hass, mqtt_mock, caplog): config2["name"] = "test2" await async_setup_component(hass, platform, {platform: [config1, config2]}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() count = 0 for record in caplog.records: if record.levelname == "WARNING" and ( diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index b7a3b5f2118..43b6e839904 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -89,12 +89,13 @@ DEFAULT_CONFIG = { DEFAULT_CONFIG_2 = {vacuum.DOMAIN: {"platform": "mqtt", "name": "test"}} -async def test_default_supported_features(hass, mqtt_mock): +async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" assert await async_setup_component( hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( @@ -110,7 +111,7 @@ async def test_default_supported_features(hass, mqtt_mock): ) -async def test_all_commands(hass, mqtt_mock): +async def test_all_commands(hass, mqtt_mock_entry_with_yaml_config): """Test simple commands to the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -119,6 +120,7 @@ async def test_all_commands(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_turn_on(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_called_once_with( @@ -189,7 +191,9 @@ async def test_all_commands(hass, mqtt_mock): } -async def test_commands_without_supported_features(hass, mqtt_mock): +async def test_commands_without_supported_features( + hass, mqtt_mock_entry_with_yaml_config +): """Test commands which are not supported by the vacuum.""" config = deepcopy(DEFAULT_CONFIG) services = mqttvacuum.STRING_TO_SERVICE["status"] @@ -199,6 +203,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_turn_on(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() @@ -237,7 +242,9 @@ async def test_commands_without_supported_features(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_attributes_without_supported_features(hass, mqtt_mock): +async def test_attributes_without_supported_features( + hass, mqtt_mock_entry_with_yaml_config +): """Test attributes which are not supported by the vacuum.""" config = deepcopy(DEFAULT_CONFIG) services = mqttvacuum.STRING_TO_SERVICE["turn_on"] @@ -247,6 +254,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54, @@ -264,7 +272,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None -async def test_status(hass, mqtt_mock): +async def test_status(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -273,6 +281,7 @@ async def test_status(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54, @@ -304,7 +313,7 @@ async def test_status(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED) == "min" -async def test_status_battery(hass, mqtt_mock): +async def test_status_battery(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -313,6 +322,7 @@ async def test_status_battery(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54 @@ -322,7 +332,7 @@ async def test_status_battery(hass, mqtt_mock): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" -async def test_status_cleaning(hass, mqtt_mock): +async def test_status_cleaning(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -331,6 +341,7 @@ async def test_status_cleaning(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "cleaning": true @@ -340,7 +351,7 @@ async def test_status_cleaning(hass, mqtt_mock): assert state.state == STATE_ON -async def test_status_docked(hass, mqtt_mock): +async def test_status_docked(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -349,6 +360,7 @@ async def test_status_docked(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "docked": true @@ -358,7 +370,7 @@ async def test_status_docked(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_status_charging(hass, mqtt_mock): +async def test_status_charging(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -367,6 +379,7 @@ async def test_status_charging(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "charging": true @@ -376,7 +389,7 @@ async def test_status_charging(hass, mqtt_mock): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-outline" -async def test_status_fan_speed(hass, mqtt_mock): +async def test_status_fan_speed(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -385,6 +398,7 @@ async def test_status_fan_speed(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "fan_speed": "max" @@ -394,7 +408,7 @@ async def test_status_fan_speed(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED) == "max" -async def test_status_fan_speed_list(hass, mqtt_mock): +async def test_status_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -403,12 +417,13 @@ async def test_status_fan_speed_list(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state.attributes.get(ATTR_FAN_SPEED_LIST) == ["min", "medium", "high", "max"] -async def test_status_no_fan_speed_list(hass, mqtt_mock): +async def test_status_no_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum. If the vacuum doesn't support fan speed, fan speed list should be None. @@ -421,12 +436,13 @@ async def test_status_no_fan_speed_list(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None -async def test_status_error(hass, mqtt_mock): +async def test_status_error(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -435,6 +451,7 @@ async def test_status_error(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "error": "Error1" @@ -451,7 +468,7 @@ async def test_status_error(hass, mqtt_mock): assert state.attributes.get(ATTR_STATUS) == "Stopped" -async def test_battery_template(hass, mqtt_mock): +async def test_battery_template(hass, mqtt_mock_entry_with_yaml_config): """Test that you can use non-default templates for battery_level.""" config = deepcopy(DEFAULT_CONFIG) config.update( @@ -466,6 +483,7 @@ async def test_battery_template(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "retroroomba/battery_level", "54") state = hass.states.get("vacuum.mqtttest") @@ -473,7 +491,7 @@ async def test_battery_template(hass, mqtt_mock): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" -async def test_status_invalid_json(hass, mqtt_mock): +async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -482,6 +500,7 @@ async def test_status_invalid_json(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "vacuum/state", '{"asdfasas false}') state = hass.states.get("vacuum.mqtttest") @@ -489,153 +508,169 @@ async def test_status_invalid_json(hass, mqtt_mock): assert state.attributes.get(ATTR_STATUS) == "Stopped" -async def test_missing_battery_template(hass, mqtt_mock): +async def test_missing_battery_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_charging_template(hass, mqtt_mock): +async def test_missing_charging_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_CHARGING_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_cleaning_template(hass, mqtt_mock): +async def test_missing_cleaning_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_CLEANING_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_docked_template(hass, mqtt_mock): +async def test_missing_docked_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_DOCKED_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_error_template(hass, mqtt_mock): +async def test_missing_error_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_ERROR_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_fan_speed_template(hass, mqtt_mock): +async def test_missing_fan_speed_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_FAN_SPEED_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2, MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one vacuum per unique_id.""" config = { vacuum.DOMAIN: [ @@ -653,74 +688,85 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, vacuum.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, config + ) -async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_removal_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered vacuum.""" data = json.dumps(DEFAULT_CONFIG_2[vacuum.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, vacuum.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data + ) -async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered vacuum.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, vacuum.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_vacuum( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered vacuum.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.vacuum.schema_legacy.MqttVacuum.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + vacuum.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "command_topic": "test_topic#" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { vacuum.DOMAIN: { @@ -733,18 +779,22 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock): } } await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, vacuum.DOMAIN, config, ["test-topic", "avty-topic"] + hass, + mqtt_mock_entry_with_yaml_config, + vacuum.DOMAIN, + config, + ["test-topic", "avty-topic"], ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { vacuum.DOMAIN: { @@ -757,7 +807,11 @@ async def test_entity_debug_info_message(hass, mqtt_mock): } } await help_test_entity_debug_info_message( - hass, mqtt_mock, vacuum.DOMAIN, config, vacuum.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + vacuum.DOMAIN, + config, + vacuum.SERVICE_TURN_ON, ) @@ -803,7 +857,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -824,7 +878,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -836,11 +890,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN config = DEFAULT_CONFIG - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -872,7 +928,13 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = deepcopy(DEFAULT_CONFIG) @@ -892,7 +954,7 @@ async def test_encoding_subscribable_topics( await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, config, diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 957178da14f..08d5432ba27 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -251,16 +251,17 @@ DEFAULT_CONFIG = { } -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test if command fails with command topic.""" assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {"platform": "mqtt", "name": "test"}} ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None -async def test_legacy_rgb_white_light(hass, mqtt_mock): +async def test_legacy_rgb_white_light(hass, mqtt_mock_entry_with_yaml_config): """Test legacy RGB + white light flags brightness support.""" assert await async_setup_component( hass, @@ -276,6 +277,7 @@ async def test_legacy_rgb_white_light(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") expected_features = ( @@ -286,7 +288,9 @@ async def test_legacy_rgb_white_light(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["hs", "rgbw"] -async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock): +async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( + hass, mqtt_mock_entry_with_yaml_config +): """Test if there is no color and brightness if no topic.""" assert await async_setup_component( hass, @@ -301,6 +305,7 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -343,7 +348,9 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt assert state.state == STATE_UNKNOWN -async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): +async def test_legacy_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling of the state via topic for legacy light (white_value).""" config = { light.DOMAIN: { @@ -374,6 +381,7 @@ async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -472,7 +480,7 @@ async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling of the state via topic.""" config = { light.DOMAIN: { @@ -505,6 +513,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -593,7 +602,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): +async def test_legacy_invalid_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test handling of empty data via topic.""" config = { light.DOMAIN: { @@ -623,6 +634,7 @@ async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -709,7 +721,7 @@ async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): assert light_state.attributes["white_value"] == 255 -async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): +async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test handling of empty data via topic.""" config = { light.DOMAIN: { @@ -742,6 +754,7 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -843,7 +856,7 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): assert light_state.attributes["color_temp"] == 153 -async def test_brightness_controlling_scale(hass, mqtt_mock): +async def test_brightness_controlling_scale(hass, mqtt_mock_entry_with_yaml_config): """Test the brightness controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -865,6 +878,7 @@ async def test_brightness_controlling_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -890,7 +904,9 @@ async def test_brightness_controlling_scale(hass, mqtt_mock): assert light_state.attributes["brightness"] == 255 -async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): +async def test_brightness_from_rgb_controlling_scale( + hass, mqtt_mock_entry_with_yaml_config +): """Test the brightness controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -911,6 +927,7 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -929,7 +946,9 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): assert state.attributes.get("brightness") == 127 -async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): +async def test_legacy_white_value_controlling_scale( + hass, mqtt_mock_entry_with_yaml_config +): """Test the white_value controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -951,6 +970,7 @@ async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -976,7 +996,9 @@ async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): assert light_state.attributes["white_value"] == 255 -async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock): +async def test_legacy_controlling_state_via_topic_with_templates( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the state with a template.""" config = { light.DOMAIN: { @@ -1011,6 +1033,7 @@ async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1065,7 +1088,9 @@ async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock assert state.state == STATE_UNKNOWN -async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): +async def test_controlling_state_via_topic_with_templates( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the state with a template.""" config = { light.DOMAIN: { @@ -1104,6 +1129,7 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1165,7 +1191,9 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_legacy_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { @@ -1204,6 +1232,7 @@ async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ), assert_setup_component(1, light.DOMAIN): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1295,7 +1324,9 @@ async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes["color_temp"] == 125 -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { @@ -1334,6 +1365,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ), assert_setup_component(1, light.DOMAIN): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1484,7 +1516,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_rgb_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of RGB command with template.""" config = { light.DOMAIN: { @@ -1502,6 +1536,7 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1521,7 +1556,9 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): assert state.attributes["rgb_color"] == (255, 128, 64) -async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_rgbw_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of RGBW command with template.""" config = { light.DOMAIN: { @@ -1539,6 +1576,7 @@ async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1558,7 +1596,9 @@ async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): assert state.attributes["rgbw_color"] == (255, 128, 64, 32) -async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_rgbww_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of RGBWW command with template.""" config = { light.DOMAIN: { @@ -1576,6 +1616,7 @@ async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1595,7 +1636,9 @@ async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): assert state.attributes["rgbww_color"] == (255, 128, 64, 32, 16) -async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_color_temp_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of Color Temp command with template.""" config = { light.DOMAIN: { @@ -1612,6 +1655,7 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1631,7 +1675,7 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): assert state.attributes["color_temp"] == 100 -async def test_on_command_first(hass, mqtt_mock): +async def test_on_command_first(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent before brightness.""" config = { light.DOMAIN: { @@ -1645,6 +1689,7 @@ async def test_on_command_first(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1667,7 +1712,7 @@ async def test_on_command_first(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_last(hass, mqtt_mock): +async def test_on_command_last(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent after brightness.""" config = { light.DOMAIN: { @@ -1680,6 +1725,7 @@ async def test_on_command_last(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1702,7 +1748,7 @@ async def test_on_command_last(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_brightness(hass, mqtt_mock): +async def test_on_command_brightness(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent as only brightness.""" config = { light.DOMAIN: { @@ -1717,6 +1763,7 @@ async def test_on_command_brightness(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1757,7 +1804,7 @@ async def test_on_command_brightness(hass, mqtt_mock): ) -async def test_on_command_brightness_scaled(hass, mqtt_mock): +async def test_on_command_brightness_scaled(hass, mqtt_mock_entry_with_yaml_config): """Test brightness scale.""" config = { light.DOMAIN: { @@ -1773,6 +1820,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1827,7 +1875,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): ) -async def test_legacy_on_command_rgb(hass, mqtt_mock): +async def test_legacy_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode.""" config = { light.DOMAIN: { @@ -1841,6 +1889,7 @@ async def test_legacy_on_command_rgb(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1918,7 +1967,7 @@ async def test_legacy_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgb(hass, mqtt_mock): +async def test_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode.""" config = { light.DOMAIN: { @@ -1931,6 +1980,7 @@ async def test_on_command_rgb(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2008,7 +2058,7 @@ async def test_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgbw(hass, mqtt_mock): +async def test_on_command_rgbw(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBW brightness mode.""" config = { light.DOMAIN: { @@ -2021,6 +2071,7 @@ async def test_on_command_rgbw(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2098,7 +2149,7 @@ async def test_on_command_rgbw(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgbww(hass, mqtt_mock): +async def test_on_command_rgbww(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBWW brightness mode.""" config = { light.DOMAIN: { @@ -2111,6 +2162,7 @@ async def test_on_command_rgbww(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2188,7 +2240,7 @@ async def test_on_command_rgbww(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgb_template(hass, mqtt_mock): +async def test_on_command_rgb_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode with RGB template.""" config = { light.DOMAIN: { @@ -2202,6 +2254,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2225,7 +2278,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_rgbw_template(hass, mqtt_mock): +async def test_on_command_rgbw_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBW brightness mode with RGBW template.""" config = { light.DOMAIN: { @@ -2239,6 +2292,7 @@ async def test_on_command_rgbw_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2261,7 +2315,7 @@ async def test_on_command_rgbw_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_rgbww_template(hass, mqtt_mock): +async def test_on_command_rgbww_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBWW brightness mode with RGBWW template.""" config = { light.DOMAIN: { @@ -2275,6 +2329,7 @@ async def test_on_command_rgbww_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2298,7 +2353,7 @@ async def test_on_command_rgbww_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_white(hass, mqtt_mock): +async def test_on_command_white(hass, mqtt_mock_entry_with_yaml_config): """Test sending commands for RGB + white light.""" config = { light.DOMAIN: { @@ -2324,6 +2379,7 @@ async def test_on_command_white(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2375,7 +2431,7 @@ async def test_on_command_white(hass, mqtt_mock): ) -async def test_explicit_color_mode(hass, mqtt_mock): +async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): """Test explicit color mode over mqtt.""" config = { light.DOMAIN: { @@ -2409,6 +2465,7 @@ async def test_explicit_color_mode(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2525,7 +2582,7 @@ async def test_explicit_color_mode(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_explicit_color_mode_templated(hass, mqtt_mock): +async def test_explicit_color_mode_templated(hass, mqtt_mock_entry_with_yaml_config): """Test templated explicit color mode over mqtt.""" config = { light.DOMAIN: { @@ -2550,6 +2607,7 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2606,7 +2664,7 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_white_state_update(hass, mqtt_mock): +async def test_white_state_update(hass, mqtt_mock_entry_with_yaml_config): """Test state updates for RGB + white light.""" config = { light.DOMAIN: { @@ -2636,6 +2694,7 @@ async def test_white_state_update(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2670,7 +2729,7 @@ async def test_white_state_update(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_effect(hass, mqtt_mock): +async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test effect.""" config = { light.DOMAIN: { @@ -2684,6 +2743,7 @@ async def test_effect(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2707,77 +2767,91 @@ async def test_effect(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { light.DOMAIN: [ @@ -2797,21 +2871,26 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, light.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config + ) -async def test_discovery_removal_light(hass, mqtt_mock, caplog): +async def test_discovery_removal_light(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered light.""" data = ( '{ "name": "test",' ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data + ) -async def test_discovery_deprecated(hass, mqtt_mock, caplog): +async def test_discovery_deprecated(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovery of mqtt light with deprecated platform option.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "name": "Beer",' ' "platform": "mqtt",' ' "command_topic": "test_topic"}' ) @@ -2822,7 +2901,9 @@ async def test_discovery_deprecated(hass, mqtt_mock, caplog): assert state.name == "Beer" -async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog): +async def test_discovery_update_light_topic_and_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -3073,7 +3154,7 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, config1, @@ -3083,7 +3164,9 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog ) -async def test_discovery_update_light_template(hass, mqtt_mock, caplog): +async def test_discovery_update_light_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -3292,7 +3375,7 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, config1, @@ -3302,7 +3385,9 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_light( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" data1 = ( '{ "name": "Beer",' @@ -3313,12 +3398,17 @@ async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.light.schema_basic.MqttLight.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -3327,60 +3417,64 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, light.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + light.SERVICE_TURN_ON, ) -async def test_max_mireds(hass, mqtt_mock): +async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { @@ -3394,6 +3488,7 @@ async def test_max_mireds(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 @@ -3488,7 +3583,7 @@ async def test_max_mireds(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -3508,7 +3603,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -3522,11 +3617,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -3568,7 +3665,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value, init_payload + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) @@ -3587,7 +3691,7 @@ async def test_encoding_subscribable_topics( await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, config, @@ -3599,7 +3703,9 @@ async def test_encoding_subscribable_topics( ) -async def test_sending_mqtt_brightness_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_brightness_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of Brightness command with template.""" config = { light.DOMAIN: { @@ -3616,6 +3722,7 @@ async def test_sending_mqtt_brightness_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -3635,7 +3742,9 @@ async def test_sending_mqtt_brightness_command_with_template(hass, mqtt_mock): assert state.attributes["brightness"] == 100 -async def test_sending_mqtt_effect_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_effect_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of Effect command with template.""" config = { light.DOMAIN: { @@ -3654,6 +3763,7 @@ async def test_sending_mqtt_effect_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 962bf534370..c24c5e87937 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -162,7 +162,7 @@ class JsonValidator: return json.loads(self.jsondata) == json.loads(other) -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test if setup fails with no command topic.""" assert await async_setup_component( hass, @@ -170,11 +170,14 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): {light.DOMAIN: {"platform": "mqtt", "schema": "json", "name": "test"}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None @pytest.mark.parametrize("deprecated", ("color_temp", "hs", "rgb", "white_value", "xy")) -async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): +async def test_fail_setup_if_color_mode_deprecated( + hass, mqtt_mock_entry_no_yaml_config, deprecated +): """Test if setup fails if color mode is combined with deprecated config keys.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] @@ -196,6 +199,7 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): config, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None @@ -203,7 +207,7 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): "supported_color_modes", [["onoff", "rgb"], ["brightness", "rgb"], ["unknown"]] ) async def test_fail_setup_if_color_modes_invalid( - hass, mqtt_mock, supported_color_modes + hass, mqtt_mock_entry_no_yaml_config, supported_color_modes ): """Test if setup fails if supported color modes is invalid.""" config = { @@ -223,10 +227,11 @@ async def test_fail_setup_if_color_modes_invalid( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None -async def test_rgb_light(hass, mqtt_mock): +async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): """Test RGB light flags brightness support.""" assert await async_setup_component( hass, @@ -242,6 +247,7 @@ async def test_rgb_light(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") expected_features = ( @@ -253,7 +259,9 @@ async def test_rgb_light(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features -async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_mock): +async def test_no_color_brightness_color_temp_white_val_if_no_topics( + hass, mqtt_mock_entry_with_yaml_config +): """Test for no RGB, brightness, color temp, effect, white val or XY.""" assert await async_setup_component( hass, @@ -269,6 +277,7 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -305,7 +314,7 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ assert state.state == STATE_UNKNOWN -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling of the state via topic.""" assert await async_setup_component( hass, @@ -329,6 +338,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -434,7 +444,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert light_state.attributes.get("white_value") == 155 -async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic2( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling of the state via topic for a light supporting color mode.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] @@ -457,6 +469,7 @@ async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -602,7 +615,9 @@ async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): assert "Invalid or incomplete color value received" in caplog.text -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" fake_state = ha.State( "light.test", @@ -641,6 +656,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -745,7 +761,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes["xy_color"] == (0.611, 0.375) -async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic2( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode for a light supporting color mode.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] fake_state = ha.State( @@ -783,6 +801,7 @@ async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -953,7 +972,7 @@ async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_sending_hs_color(hass, mqtt_mock): +async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends hs color parameters.""" assert await async_setup_component( hass, @@ -971,6 +990,7 @@ async def test_sending_hs_color(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1018,7 +1038,7 @@ async def test_sending_hs_color(hass, mqtt_mock): ) -async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): +async def test_sending_rgb_color_no_brightness(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends rgb color parameters.""" assert await async_setup_component( hass, @@ -1034,6 +1054,7 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1071,7 +1092,7 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): ) -async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock): +async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends rgb color parameters.""" supported_color_modes = ["rgb", "rgbw", "rgbww"] assert await async_setup_component( @@ -1089,6 +1110,7 @@ async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1148,7 +1170,9 @@ async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock): ) -async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): +async def test_sending_rgb_color_with_brightness( + hass, mqtt_mock_entry_with_yaml_config +): """Test light.turn_on with hs color sends rgb color parameters.""" assert await async_setup_component( hass, @@ -1166,6 +1190,7 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1218,7 +1243,9 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): ) -async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): +async def test_sending_rgb_color_with_scaled_brightness( + hass, mqtt_mock_entry_with_yaml_config +): """Test light.turn_on with hs color sends rgb color parameters.""" assert await async_setup_component( hass, @@ -1237,6 +1264,7 @@ async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1289,7 +1317,7 @@ async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): ) -async def test_sending_xy_color(hass, mqtt_mock): +async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends xy color parameters.""" assert await async_setup_component( hass, @@ -1307,6 +1335,7 @@ async def test_sending_xy_color(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1353,7 +1382,7 @@ async def test_sending_xy_color(hass, mqtt_mock): ) -async def test_effect(hass, mqtt_mock): +async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test for effect being sent when included.""" assert await async_setup_component( hass, @@ -1370,6 +1399,7 @@ async def test_effect(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1415,7 +1445,7 @@ async def test_effect(hass, mqtt_mock): assert state.attributes.get("effect") == "colorloop" -async def test_flash_short_and_long(hass, mqtt_mock): +async def test_flash_short_and_long(hass, mqtt_mock_entry_with_yaml_config): """Test for flash length being sent when included.""" assert await async_setup_component( hass, @@ -1433,6 +1463,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1476,7 +1507,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_transition(hass, mqtt_mock): +async def test_transition(hass, mqtt_mock_entry_with_yaml_config): """Test for transition time being sent when included.""" assert await async_setup_component( hass, @@ -1492,6 +1523,7 @@ async def test_transition(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1522,7 +1554,7 @@ async def test_transition(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_brightness_scale(hass, mqtt_mock): +async def test_brightness_scale(hass, mqtt_mock_entry_with_yaml_config): """Test for brightness scaling.""" assert await async_setup_component( hass, @@ -1540,6 +1572,7 @@ async def test_brightness_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1563,7 +1596,7 @@ async def test_brightness_scale(hass, mqtt_mock): assert state.attributes.get("brightness") == 255 -async def test_invalid_values(hass, mqtt_mock): +async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): """Test that invalid color/brightness/white/etc. values are ignored.""" assert await async_setup_component( hass, @@ -1584,6 +1617,7 @@ async def test_invalid_values(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1700,77 +1734,95 @@ async def test_invalid_values(hass, mqtt_mock): assert state.attributes.get("color_temp") == 100 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { light.DOMAIN: [ @@ -1792,16 +1844,24 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, light.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config + ) -async def test_discovery_removal(hass, mqtt_mock, caplog): +async def test_discovery_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered mqtt_json lights.""" data = '{ "name": "test",' ' "schema": "json",' ' "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) + await help_test_discovery_removal( + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data, + ) -async def test_discovery_update_light(hass, mqtt_mock, caplog): +async def test_discovery_update_light(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -1816,11 +1876,18 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): "command_topic": "test_topic", } await help_test_discovery_update( - hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + config1, + config2, ) -async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_light( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" data1 = ( '{ "name": "Beer",' @@ -1832,12 +1899,17 @@ async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.light.schema_json.MqttLightJson.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -1847,57 +1919,80 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + data2, ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG, light.SERVICE_TURN_ON, @@ -1906,7 +2001,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_max_mireds(hass, mqtt_mock): +async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { @@ -1921,6 +2016,7 @@ async def test_max_mireds(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 @@ -1952,7 +2048,7 @@ async def test_max_mireds(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1972,7 +2068,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1986,11 +2082,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -2013,7 +2111,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value, init_payload + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) @@ -2028,7 +2133,7 @@ async def test_encoding_subscribable_topics( ] await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, config, diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index a88fc094f6d..0d4b95e9152 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -90,69 +90,53 @@ DEFAULT_CONFIG = { } -async def test_setup_fails(hass, mqtt_mock): +@pytest.mark.parametrize( + "test_config", + [ + ({"platform": "mqtt", "schema": "template", "name": "test"},), + ( + { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_topic", + }, + ), + ( + { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_topic", + "command_on_template": "on", + }, + ), + ( + { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_topic", + "command_off_template": "off", + }, + ), + ], +) +async def test_setup_fails(hass, mqtt_mock_entry_no_yaml_config, test_config): """Test that setup fails with missing required configuration items.""" - with assert_setup_component(0, light.DOMAIN): + with assert_setup_component(0, light.DOMAIN) as setup_config: assert await async_setup_component( hass, light.DOMAIN, - {light.DOMAIN: {"platform": "mqtt", "schema": "template", "name": "test"}}, - ) - await hass.async_block_till_done() - assert hass.states.get("light.test") is None - - with assert_setup_component(0, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_topic", - } - }, - ) - await hass.async_block_till_done() - assert hass.states.get("light.test") is None - - with assert_setup_component(0, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_topic", - "command_on_template": "on", - } - }, - ) - await hass.async_block_till_done() - assert hass.states.get("light.test") is None - - with assert_setup_component(0, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_topic", - "command_off_template": "off", - } - }, + {light.DOMAIN: test_config}, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() + assert not setup_config[light.DOMAIN] assert hass.states.get("light.test") is None -async def test_rgb_light(hass, mqtt_mock): +async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): """Test RGB light flags brightness support.""" assert await async_setup_component( hass, @@ -172,6 +156,7 @@ async def test_rgb_light(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -184,7 +169,7 @@ async def test_rgb_light(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features -async def test_state_change_via_topic(hass, mqtt_mock): +async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test state change via topic.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -210,6 +195,7 @@ async def test_state_change_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -240,7 +226,7 @@ async def test_state_change_via_topic(hass, mqtt_mock): async def test_state_brightness_color_effect_temp_white_change_via_topic( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test state, bri, color, effect, color temp, white val change.""" with assert_setup_component(1, light.DOMAIN): @@ -276,6 +262,7 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -340,7 +327,9 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( assert light_state.attributes.get("effect") == "rainbow" -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" fake_state = ha.State( "light.test", @@ -391,6 +380,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -495,7 +485,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): async def test_sending_mqtt_commands_non_optimistic_brightness_template( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test the sending of command in optimistic mode.""" with assert_setup_component(1, light.DOMAIN): @@ -532,6 +522,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -626,7 +617,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( state = hass.states.get("light.test") -async def test_effect(hass, mqtt_mock): +async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test effect sent over MQTT in optimistic mode.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -646,6 +637,7 @@ async def test_effect(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -678,7 +670,7 @@ async def test_effect(hass, mqtt_mock): assert state.attributes.get("effect") == "colorloop" -async def test_flash(hass, mqtt_mock): +async def test_flash(hass, mqtt_mock_entry_with_yaml_config): """Test flash sent over MQTT in optimistic mode.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -697,6 +689,7 @@ async def test_flash(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -726,7 +719,7 @@ async def test_flash(hass, mqtt_mock): assert state.state == STATE_ON -async def test_transition(hass, mqtt_mock): +async def test_transition(hass, mqtt_mock_entry_with_yaml_config): """Test for transition time being sent when included.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -745,6 +738,7 @@ async def test_transition(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -767,7 +761,7 @@ async def test_transition(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_invalid_values(hass, mqtt_mock): +async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): """Test that invalid values are ignored.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -801,6 +795,7 @@ async def test_invalid_values(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -867,77 +862,91 @@ async def test_invalid_values(hass, mqtt_mock): assert state.attributes.get("effect") == "rainbow" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { light.DOMAIN: [ @@ -961,10 +970,12 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, light.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config + ) -async def test_discovery_removal(hass, mqtt_mock, caplog): +async def test_discovery_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered mqtt_json lights.""" data = ( '{ "name": "test",' @@ -973,10 +984,12 @@ async def test_discovery_removal(hass, mqtt_mock, caplog): ' "command_on_template": "on",' ' "command_off_template": "off"}' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data + ) -async def test_discovery_update_light(hass, mqtt_mock, caplog): +async def test_discovery_update_light(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -995,11 +1008,13 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): "command_off_template": "off", } await help_test_discovery_update( - hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_light( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" data1 = ( '{ "name": "Beer",' @@ -1013,12 +1028,17 @@ async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.light.schema_template.MqttLightTemplate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -1030,53 +1050,53 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_off_template": "off"}' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { light.DOMAIN: { @@ -1090,11 +1110,15 @@ async def test_entity_debug_info_message(hass, mqtt_mock): } } await help_test_entity_debug_info_message( - hass, mqtt_mock, light.DOMAIN, config, light.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + config, + light.SERVICE_TURN_ON, ) -async def test_max_mireds(hass, mqtt_mock): +async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { @@ -1111,6 +1135,7 @@ async def test_max_mireds(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 @@ -1142,7 +1167,7 @@ async def test_max_mireds(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1162,7 +1187,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1176,11 +1201,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -1197,14 +1224,21 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value, init_payload + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) config["state_template"] = "{{ value }}" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, config, diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index ef752ef8749..b48557efc8f 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -58,7 +58,7 @@ DEFAULT_CONFIG = { } -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -77,6 +77,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -94,7 +95,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state is STATE_UNLOCKED -async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): +async def test_controlling_non_default_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -113,6 +116,7 @@ async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -129,7 +133,9 @@ async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): assert state.state is STATE_UNLOCKED -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -149,6 +155,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -165,7 +172,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): async def test_controlling_non_default_state_via_topic_and_json_message( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( @@ -186,6 +193,7 @@ async def test_controlling_non_default_state_via_topic_and_json_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -201,7 +209,9 @@ async def test_controlling_non_default_state_via_topic_and_json_message( assert state.state is STATE_UNLOCKED -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -219,6 +229,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -245,7 +256,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -265,6 +278,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -291,7 +305,9 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_support_open_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test open function of the lock without state topic.""" assert await async_setup_component( hass, @@ -310,6 +326,7 @@ async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -348,7 +365,7 @@ async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test open function of the lock without state topic.""" assert await async_setup_component( @@ -370,6 +387,7 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -407,77 +425,91 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG, MQTT_LOCK_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + LOCK_DOMAIN, + DEFAULT_CONFIG, + MQTT_LOCK_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one lock per unique_id.""" config = { LOCK_DOMAIN: [ @@ -497,16 +529,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, LOCK_DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, config + ) -async def test_discovery_removal_lock(hass, mqtt_mock, caplog): +async def test_discovery_removal_lock(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered lock.""" data = '{ "name": "test",' ' "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, LOCK_DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, data + ) -async def test_discovery_update_lock(hass, mqtt_mock, caplog): +async def test_discovery_update_lock(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered lock.""" config1 = { "name": "Beer", @@ -521,11 +557,13 @@ async def test_discovery_update_lock(hass, mqtt_mock, caplog): "availability_topic": "availability_topic2", } await help_test_discovery_update( - hass, mqtt_mock, caplog, LOCK_DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_lock(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_lock( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered lock.""" data1 = ( '{ "name": "Beer",' @@ -536,65 +574,72 @@ async def test_discovery_update_unchanged_lock(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.lock.MqttLock.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, LOCK_DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + LOCK_DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' - await help_test_discovery_broken(hass, mqtt_mock, caplog, LOCK_DOMAIN, data1, data2) + await help_test_discovery_broken( + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, data1, data2 + ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG, SERVICE_LOCK, @@ -616,7 +661,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -630,7 +675,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -642,11 +687,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = LOCK_DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -663,12 +710,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG[LOCK_DOMAIN], diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 4eb8fdec351..a49b6de198d 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -64,7 +64,7 @@ DEFAULT_CONFIG = { } -async def test_run_number_setup(hass, mqtt_mock): +async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload.""" topic = "test/number" await async_setup_component( @@ -82,6 +82,7 @@ async def test_run_number_setup(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "10") @@ -108,7 +109,7 @@ async def test_run_number_setup(hass, mqtt_mock): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "my unit" -async def test_value_template(hass, mqtt_mock): +async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload with a template.""" topic = "test/number" await async_setup_component( @@ -125,6 +126,7 @@ async def test_value_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, '{"val":10}') @@ -148,7 +150,7 @@ async def test_value_template(hass, mqtt_mock): assert state.state == "unknown" -async def test_run_number_service_optimistic(hass, mqtt_mock): +async def test_run_number_service_optimistic(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in optimistic mode.""" topic = "test/number" @@ -170,6 +172,7 @@ async def test_run_number_service_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.state == "3" @@ -215,7 +218,9 @@ async def test_run_number_service_optimistic(hass, mqtt_mock): assert state.state == "42.1" -async def test_run_number_service_optimistic_with_command_template(hass, mqtt_mock): +async def test_run_number_service_optimistic_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in optimistic mode and with a command_template.""" topic = "test/number" @@ -238,6 +243,7 @@ async def test_run_number_service_optimistic_with_command_template(hass, mqtt_mo }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.state == "3" @@ -285,7 +291,7 @@ async def test_run_number_service_optimistic_with_command_template(hass, mqtt_mo assert state.state == "42.1" -async def test_run_number_service(hass, mqtt_mock): +async def test_run_number_service(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in non optimistic mode.""" cmd_topic = "test/number/set" state_topic = "test/number" @@ -303,6 +309,7 @@ async def test_run_number_service(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "32") state = hass.states.get("number.test_number") @@ -319,7 +326,9 @@ async def test_run_number_service(hass, mqtt_mock): assert state.state == "32" -async def test_run_number_service_with_command_template(hass, mqtt_mock): +async def test_run_number_service_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in non optimistic mode and with a command_template.""" cmd_topic = "test/number/set" state_topic = "test/number" @@ -338,6 +347,7 @@ async def test_run_number_service_with_command_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "32") state = hass.states.get("number.test_number") @@ -356,77 +366,91 @@ async def test_run_number_service_with_command_template(hass, mqtt_mock): assert state.state == "32" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, MQTT_NUMBER_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + number.DOMAIN, + DEFAULT_CONFIG, + MQTT_NUMBER_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one number per unique_id.""" config = { number.DOMAIN: [ @@ -446,16 +470,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, number.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, config + ) -async def test_discovery_removal_number(hass, mqtt_mock, caplog): +async def test_discovery_removal_number(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered number.""" data = json.dumps(DEFAULT_CONFIG[number.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, number.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, data + ) -async def test_discovery_update_number(hass, mqtt_mock, caplog): +async def test_discovery_update_number(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered number.""" config1 = { "name": "Beer", @@ -469,11 +497,13 @@ async def test_discovery_update_number(hass, mqtt_mock, caplog): } await help_test_discovery_update( - hass, mqtt_mock, caplog, number.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_number(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_number( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered number.""" data1 = ( '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic"}' @@ -482,12 +512,17 @@ async def test_discovery_update_unchanged_number(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.number.MqttNumber.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, number.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + number.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -495,57 +530,57 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, number.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG, SERVICE_SET_VALUE, @@ -555,7 +590,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_min_max_step_attributes(hass, mqtt_mock): +async def test_min_max_step_attributes(hass, mqtt_mock_entry_with_yaml_config): """Test min/max/step attributes.""" topic = "test/number" await async_setup_component( @@ -574,6 +609,7 @@ async def test_min_max_step_attributes(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.attributes.get(ATTR_MIN) == 5 @@ -581,7 +617,7 @@ async def test_min_max_step_attributes(hass, mqtt_mock): assert state.attributes.get(ATTR_STEP) == 20 -async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock): +async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock_entry_no_yaml_config): """Test invalid min/max attributes.""" topic = "test/number" await async_setup_component( @@ -599,11 +635,14 @@ async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert f"'{CONF_MAX}' must be > '{CONF_MIN}'" in caplog.text -async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): +async def test_mqtt_payload_not_a_number_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test warning for MQTT payload which is not a number.""" topic = "test/number" await async_setup_component( @@ -619,6 +658,7 @@ async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "not_a_number") @@ -627,7 +667,9 @@ async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): assert "Payload 'not_a_number' is not a Number" in caplog.text -async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): +async def test_mqtt_payload_out_of_range_error( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test error when MQTT payload is out of min/max range.""" topic = "test/number" await async_setup_component( @@ -645,6 +687,7 @@ async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "115.5") @@ -669,7 +712,7 @@ async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -683,7 +726,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -695,11 +738,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = number.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -717,12 +762,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, "number", DEFAULT_CONFIG["number"], diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 15bbd3964e6..eb5cb94df2d 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -34,7 +34,7 @@ DEFAULT_CONFIG = { } -async def test_sending_mqtt_commands(hass, mqtt_mock): +async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" fake_state = ha.State("scene.test", STATE_UNKNOWN) @@ -55,6 +55,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("scene.test") assert state.state == STATE_UNKNOWN @@ -67,21 +68,23 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): ) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, scene.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, scene.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { scene.DOMAIN: { @@ -93,11 +96,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, scene.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + scene.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { scene.DOMAIN: { @@ -109,11 +118,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, scene.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + scene.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one scene per unique_id.""" config = { scene.DOMAIN: [ @@ -131,16 +146,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, scene.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, config + ) -async def test_discovery_removal_scene(hass, mqtt_mock, caplog): +async def test_discovery_removal_scene(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered scene.""" data = '{ "name": "test",' ' "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, scene.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, scene.DOMAIN, data + ) -async def test_discovery_update_payload(hass, mqtt_mock, caplog): +async def test_discovery_update_payload(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered scene.""" config1 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) @@ -151,7 +170,7 @@ async def test_discovery_update_payload(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, scene.DOMAIN, config1, @@ -159,32 +178,41 @@ async def test_discovery_update_payload(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_scene(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_scene( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered scene.""" data1 = '{ "name": "Beer",' ' "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.scene.MqttScene.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, scene.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + scene.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, scene.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, scene.DOMAIN, data1, data2 ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = scene.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index cf5abf55854..888dd301018 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -59,7 +59,7 @@ DEFAULT_CONFIG = { } -async def test_run_select_setup(hass, mqtt_mock): +async def test_run_select_setup(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload.""" topic = "test/select" await async_setup_component( @@ -76,6 +76,7 @@ async def test_run_select_setup(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "milk") @@ -92,7 +93,7 @@ async def test_run_select_setup(hass, mqtt_mock): assert state.state == "beer" -async def test_value_template(hass, mqtt_mock): +async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload with a template.""" topic = "test/select" await async_setup_component( @@ -110,6 +111,7 @@ async def test_value_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, '{"val":"milk"}') @@ -133,7 +135,7 @@ async def test_value_template(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_run_select_service_optimistic(hass, mqtt_mock): +async def test_run_select_service_optimistic(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in optimistic mode.""" topic = "test/select" @@ -156,6 +158,7 @@ async def test_run_select_service_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" @@ -174,7 +177,9 @@ async def test_run_select_service_optimistic(hass, mqtt_mock): assert state.state == "beer" -async def test_run_select_service_optimistic_with_command_template(hass, mqtt_mock): +async def test_run_select_service_optimistic_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in optimistic mode and with a command_template.""" topic = "test/select" @@ -198,6 +203,7 @@ async def test_run_select_service_optimistic_with_command_template(hass, mqtt_mo }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" @@ -218,7 +224,7 @@ async def test_run_select_service_optimistic_with_command_template(hass, mqtt_mo assert state.state == "beer" -async def test_run_select_service(hass, mqtt_mock): +async def test_run_select_service(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in non optimistic mode.""" cmd_topic = "test/select/set" state_topic = "test/select" @@ -237,6 +243,7 @@ async def test_run_select_service(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "beer") state = hass.states.get("select.test_select") @@ -253,7 +260,9 @@ async def test_run_select_service(hass, mqtt_mock): assert state.state == "beer" -async def test_run_select_service_with_command_template(hass, mqtt_mock): +async def test_run_select_service_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in non optimistic mode and with a command_template.""" cmd_topic = "test/select/set" state_topic = "test/select" @@ -273,6 +282,7 @@ async def test_run_select_service_with_command_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "beer") state = hass.states.get("select.test_select") @@ -289,77 +299,91 @@ async def test_run_select_service_with_command_template(hass, mqtt_mock): ) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG, MQTT_SELECT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + select.DOMAIN, + DEFAULT_CONFIG, + MQTT_SELECT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one select per unique_id.""" config = { select.DOMAIN: [ @@ -381,16 +405,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, select.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, config + ) -async def test_discovery_removal_select(hass, mqtt_mock, caplog): +async def test_discovery_removal_select(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered select.""" data = json.dumps(DEFAULT_CONFIG[select.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, select.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, data + ) -async def test_discovery_update_select(hass, mqtt_mock, caplog): +async def test_discovery_update_select(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered select.""" config1 = { "name": "Beer", @@ -406,79 +434,86 @@ async def test_discovery_update_select(hass, mqtt_mock, caplog): } await help_test_discovery_update( - hass, mqtt_mock, caplog, select.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_select(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_select( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered select.""" data1 = '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' with patch( "homeassistant.components.mqtt.select.MqttSelect.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, select.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + select.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' await help_test_discovery_broken( - hass, mqtt_mock, caplog, select.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG, select.SERVICE_SELECT_OPTION, @@ -489,7 +524,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): @pytest.mark.parametrize("options", [["milk", "beer"], ["milk"], []]) -async def test_options_attributes(hass, mqtt_mock, options): +async def test_options_attributes(hass, mqtt_mock_entry_with_yaml_config, options): """Test options attribute.""" topic = "test/select" await async_setup_component( @@ -506,12 +541,15 @@ async def test_options_attributes(hass, mqtt_mock, options): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.attributes.get(ATTR_OPTIONS) == options -async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): +async def test_mqtt_payload_not_an_option_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test warning for MQTT payload which is not a valid option.""" topic = "test/select" await async_setup_component( @@ -528,6 +566,7 @@ async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "öl") @@ -552,7 +591,14 @@ async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): ], ) async def test_publishing_with_custom_encoding( - hass, mqtt_mock, caplog, service, topic, parameters, payload, template + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + service, + topic, + parameters, + payload, + template, ): """Test publishing MQTT payload with different encoding.""" domain = select.DOMAIN @@ -561,7 +607,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -573,11 +619,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = select.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -595,14 +643,20 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG["select"]) config["options"] = ["milk", "beer"] await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, "select", config, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index befb5785cdd..894ecc32ecc 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -72,7 +72,9 @@ DEFAULT_CONFIG = { } -async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -87,6 +89,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", "100") state = hass.states.get("sensor.test") @@ -122,7 +125,13 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): ], ) async def test_setting_sensor_native_value_handling_via_mqtt_message( - hass, mqtt_mock, caplog, device_class, native_value, state_value, log + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + device_class, + native_value, + state_value, + log, ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -138,6 +147,7 @@ async def test_setting_sensor_native_value_handling_via_mqtt_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", native_value) state = hass.states.get("sensor.test") @@ -147,7 +157,9 @@ async def test_setting_sensor_native_value_handling_via_mqtt_message( assert log == ("Invalid state message" in caplog.text) -async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires_availability_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -164,6 +176,7 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE @@ -174,10 +187,12 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass, caplog) -async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -194,15 +209,16 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # State should be unavailable since expire_after is defined and > 0 state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass, caplog) -async def expires_helper(hass, mqtt_mock, caplog): +async def expires_helper(hass, caplog): """Run the basic expiry code.""" realnow = dt_util.utcnow() now = datetime(realnow.year + 1, 1, 1, 1, tzinfo=dt_util.UTC) @@ -253,7 +269,9 @@ async def expires_helper(hass, mqtt_mock, caplog): assert state.state == STATE_UNAVAILABLE -async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, @@ -269,6 +287,7 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", '{ "val": "100" }') state = hass.states.get("sensor.test") @@ -277,7 +296,7 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_state( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test the setting of the value via MQTT with fall back to current state.""" assert await async_setup_component( @@ -294,6 +313,7 @@ async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_st }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, "test-topic", '{ "val": "valcontent", "par": "parcontent" }' @@ -308,7 +328,9 @@ async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_st assert state.state == "valcontent-parcontent" -async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock, caplog): +async def test_setting_sensor_last_reset_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( hass, @@ -325,6 +347,7 @@ async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock, caplo }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "last-reset-topic", "2020-01-02 08:11:00") state = hass.states.get("sensor.test") @@ -338,7 +361,7 @@ async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock, caplo @pytest.mark.parametrize("datestring", ["2020-21-02 08:11:00", "Hello there!"]) async def test_setting_sensor_bad_last_reset_via_mqtt_message( - hass, caplog, datestring, mqtt_mock + hass, caplog, datestring, mqtt_mock_entry_with_yaml_config ): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( @@ -356,6 +379,7 @@ async def test_setting_sensor_bad_last_reset_via_mqtt_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "last-reset-topic", datestring) state = hass.states.get("sensor.test") @@ -364,7 +388,7 @@ async def test_setting_sensor_bad_last_reset_via_mqtt_message( async def test_setting_sensor_empty_last_reset_via_mqtt_message( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_with_yaml_config ): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( @@ -382,6 +406,7 @@ async def test_setting_sensor_empty_last_reset_via_mqtt_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "last-reset-topic", "") state = hass.states.get("sensor.test") @@ -389,7 +414,9 @@ async def test_setting_sensor_empty_last_reset_via_mqtt_message( assert "Ignoring empty last_reset message" in caplog.text -async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_sensor_last_reset_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, @@ -407,6 +434,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, "last-reset-topic", '{ "last_reset": "2020-01-02 08:11:00" }' @@ -417,7 +445,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): @pytest.mark.parametrize("extra", [{}, {"last_reset_topic": "test-topic"}]) async def test_setting_sensor_last_reset_via_mqtt_json_message_2( - hass, mqtt_mock, caplog, extra + hass, mqtt_mock_entry_with_yaml_config, caplog, extra ): """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( @@ -439,6 +467,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message_2( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, @@ -455,7 +484,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message_2( ) -async def test_force_update_disabled(hass, mqtt_mock): +async def test_force_update_disabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -470,6 +499,7 @@ async def test_force_update_disabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -488,7 +518,7 @@ async def test_force_update_disabled(hass, mqtt_mock): assert len(events) == 1 -async def test_force_update_enabled(hass, mqtt_mock): +async def test_force_update_enabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -504,6 +534,7 @@ async def test_force_update_enabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -522,70 +553,80 @@ async def test_force_update_enabled(hass, mqtt_mock): assert len(events) == 2 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_payload(hass, mqtt_mock): +async def test_default_availability_list_payload( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_payload_all(hass, mqtt_mock): +async def test_default_availability_list_payload_all( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_all( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_payload_any(hass, mqtt_mock): +async def test_default_availability_list_payload_any( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_any( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_single(hass, mqtt_mock, caplog): +async def test_default_availability_list_single( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test availability list and availability_topic are mutually exclusive.""" await help_test_default_availability_list_single( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_availability(hass, mqtt_mock): +async def test_discovery_update_availability(hass, mqtt_mock_entry_no_yaml_config): """Test availability discovery update.""" await help_test_discovery_update_availability( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test device_class option with invalid value.""" assert await async_setup_component( hass, @@ -600,12 +641,13 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("sensor.test") assert state is None -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test device_class option with valid values.""" assert await async_setup_component( hass, @@ -623,6 +665,7 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("sensor.test_1") assert state.attributes["device_class"] == "temperature" @@ -630,7 +673,7 @@ async def test_valid_device_class(hass, mqtt_mock): assert "device_class" not in state.attributes -async def test_invalid_state_class(hass, mqtt_mock): +async def test_invalid_state_class(hass, mqtt_mock_entry_no_yaml_config): """Test state_class option with invalid value.""" assert await async_setup_component( hass, @@ -645,12 +688,13 @@ async def test_invalid_state_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("sensor.test") assert state is None -async def test_valid_state_class(hass, mqtt_mock): +async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config): """Test state_class option with valid values.""" assert await async_setup_component( hass, @@ -668,6 +712,7 @@ async def test_valid_state_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("sensor.test_1") assert state.attributes["state_class"] == "measurement" @@ -675,49 +720,61 @@ async def test_valid_state_class(hass, mqtt_mock): assert "state_class" not in state.attributes -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG, MQTT_SENSOR_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + sensor.DOMAIN, + DEFAULT_CONFIG, + MQTT_SENSOR_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one sensor per unique_id.""" config = { sensor.DOMAIN: [ @@ -735,16 +792,22 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, sensor.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, config + ) -async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): +async def test_discovery_removal_sensor(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered sensor.""" data = '{ "name": "test", "state_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, sensor.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, data + ) -async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_update_sensor_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered sensor.""" config = {"name": "test", "state_topic": "test_topic"} config1 = copy.deepcopy(config) @@ -767,7 +830,7 @@ async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, config1, @@ -777,7 +840,9 @@ async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): +async def test_discovery_update_sensor_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered sensor.""" config = {"name": "test", "state_topic": "test_topic"} config1 = copy.deepcopy(config) @@ -798,7 +863,7 @@ async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, config1, @@ -808,71 +873,79 @@ async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_sensor(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_sensor( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered sensor.""" data1 = '{ "name": "Beer", "state_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.sensor.MqttSensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, sensor.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + sensor.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "state_topic": "test_topic#" }' data2 = '{ "name": "Milk", "state_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, sensor.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_hub(hass, mqtt_mock): +async def test_entity_device_info_with_hub(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) hub = registry.async_get_or_create( config_entry_id="123", @@ -899,53 +972,57 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock): assert device.via_device_id == hub.id -async def test_entity_debug_info(hass, mqtt_mock): +async def test_entity_debug_info(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" - await help_test_entity_debug_info(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_debug_info( + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + ) -async def test_entity_debug_info_max_messages(hass, mqtt_mock): +async def test_entity_debug_info_max_messages(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_max_messages( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG, None ) -async def test_entity_debug_info_remove(hass, mqtt_mock): +async def test_entity_debug_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_remove( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_update_entity_id(hass, mqtt_mock): +async def test_entity_debug_info_update_entity_id(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_update_entity_id( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_disabled_by_default(hass, mqtt_mock): +async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): """Test entity disabled by default.""" await help_test_entity_disabled_by_default( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @pytest.mark.no_fail_on_log_exception -async def test_entity_category(hass, mqtt_mock): +async def test_entity_category(hass, mqtt_mock_entry_no_yaml_config): """Test entity category.""" - await help_test_entity_category(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_category( + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + ) -async def test_value_template_with_entity_id(hass, mqtt_mock): +async def test_value_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_config): """Test the access to attributes in value_template via the entity_id.""" assert await async_setup_component( hass, @@ -966,6 +1043,7 @@ async def test_value_template_with_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", "100") state = hass.states.get("sensor.test") @@ -973,11 +1051,13 @@ async def test_value_template_with_entity_id(hass, mqtt_mock): assert state.state == "101" -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = sensor.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -988,7 +1068,7 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_cleanup_triggers_and_restoring_state( - hass, mqtt_mock, caplog, tmp_path, freezer + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, freezer ): """Test cleanup old triggers at reloading and restoring the state.""" domain = sensor.DOMAIN @@ -1014,6 +1094,7 @@ async def test_cleanup_triggers_and_restoring_state( {domain: [config1, config2]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic1", "100") state = hass.states.get("sensor.test1") assert state.state == "38" # 100 °F -> 38 °C @@ -1053,7 +1134,7 @@ async def test_cleanup_triggers_and_restoring_state( async def test_skip_restoring_state_with_over_due_expire_trigger( - hass, mqtt_mock, caplog, freezer + hass, mqtt_mock_entry_with_yaml_config, caplog, freezer ): """Test restoring a state with over due expire timer.""" @@ -1081,6 +1162,7 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ): assert await async_setup_component(hass, domain, {domain: config3}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert "Skip state recovery after reload for sensor.test3" in caplog.text @@ -1092,12 +1174,18 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG[sensor.DOMAIN], diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 197ed34b7e4..2db2060c133 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -70,7 +70,7 @@ async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: await hass.services.async_call(siren.DOMAIN, SERVICE_TURN_OFF, data, blocking=True) -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -87,6 +87,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -103,7 +104,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending MQTT commands in optimistic mode.""" assert await async_setup_component( hass, @@ -120,6 +123,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_OFF @@ -143,7 +147,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -161,6 +167,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -181,7 +188,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap async def test_controlling_state_and_attributes_with_json_message_without_template( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic and JSON message without a value template.""" assert await async_setup_component( @@ -200,6 +207,7 @@ async def test_controlling_state_and_attributes_with_json_message_without_templa }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -262,7 +270,9 @@ async def test_controlling_state_and_attributes_with_json_message_without_templa ) -async def test_filtering_not_supported_attributes_optimistic(hass, mqtt_mock): +async def test_filtering_not_supported_attributes_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting attributes with support flags optimistic.""" config = { "platform": "mqtt", @@ -285,6 +295,7 @@ async def test_filtering_not_supported_attributes_optimistic(hass, mqtt_mock): {siren.DOMAIN: [config1, config2, config3]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state1 = hass.states.get("siren.test1") assert state1.state == STATE_OFF @@ -345,7 +356,9 @@ async def test_filtering_not_supported_attributes_optimistic(hass, mqtt_mock): assert state3.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 -async def test_filtering_not_supported_attributes_via_state(hass, mqtt_mock): +async def test_filtering_not_supported_attributes_via_state( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting attributes with support flags via state.""" config = { "platform": "mqtt", @@ -371,6 +384,7 @@ async def test_filtering_not_supported_attributes_via_state(hass, mqtt_mock): {siren.DOMAIN: [config1, config2, config3]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state1 = hass.states.get("siren.test1") assert state1.state == STATE_UNKNOWN @@ -422,21 +436,23 @@ async def test_filtering_not_supported_attributes_via_state(hass, mqtt_mock): assert state3.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { siren.DOMAIN: { @@ -450,11 +466,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, siren.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + siren.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { siren.DOMAIN: { @@ -468,11 +490,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, siren.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + siren.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_state_payload(hass, mqtt_mock): +async def test_custom_state_payload(hass, mqtt_mock_entry_with_yaml_config): """Test the state payload.""" assert await async_setup_component( hass, @@ -491,6 +519,7 @@ async def test_custom_state_payload(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -507,49 +536,57 @@ async def test_custom_state_payload(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG, {} + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG, {} ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one siren per unique_id.""" config = { siren.DOMAIN: [ @@ -569,20 +606,26 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, siren.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, config + ) -async def test_discovery_removal_siren(hass, mqtt_mock, caplog): +async def test_discovery_removal_siren(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered siren.""" data = ( '{ "name": "test",' ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, siren.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, data + ) -async def test_discovery_update_siren_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_update_siren_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered siren.""" config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) @@ -607,7 +650,7 @@ async def test_discovery_update_siren_topic_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, config1, @@ -617,7 +660,9 @@ async def test_discovery_update_siren_topic_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_siren_template(hass, mqtt_mock, caplog): +async def test_discovery_update_siren_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered siren.""" config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) @@ -640,7 +685,7 @@ async def test_discovery_update_siren_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, config1, @@ -650,7 +695,7 @@ async def test_discovery_update_siren_template(hass, mqtt_mock, caplog): ) -async def test_command_templates(hass, mqtt_mock, caplog): +async def test_command_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test siren with command templates optimistic.""" config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) config1["name"] = "Beer" @@ -669,6 +714,7 @@ async def test_command_templates(hass, mqtt_mock, caplog): {siren.DOMAIN: [config1, config2]}, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state1 = hass.states.get("siren.beer") assert state1.state == STATE_OFF @@ -729,7 +775,9 @@ async def test_command_templates(hass, mqtt_mock, caplog): mqtt_mock.reset_mock() -async def test_discovery_update_unchanged_siren(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_siren( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered siren.""" data1 = ( '{ "name": "Beer",' @@ -741,12 +789,17 @@ async def test_discovery_update_unchanged_siren(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.siren.MqttSiren.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, siren.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + siren.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -755,57 +808,57 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, siren.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG, siren.SERVICE_TURN_ON, @@ -834,7 +887,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -849,7 +902,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -861,11 +914,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = siren.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -882,12 +937,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG[siren.DOMAIN], diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index c1017446eff..f20a881dda1 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -87,12 +87,13 @@ DEFAULT_CONFIG_2 = { } -async def test_default_supported_features(hass, mqtt_mock): +async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" assert await async_setup_component( hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( @@ -100,7 +101,7 @@ async def test_default_supported_features(hass, mqtt_mock): ) -async def test_all_commands(hass, mqtt_mock): +async def test_all_commands(hass, mqtt_mock_entry_with_yaml_config): """Test simple commands send to the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -109,6 +110,7 @@ async def test_all_commands(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True @@ -171,7 +173,9 @@ async def test_all_commands(hass, mqtt_mock): } -async def test_commands_without_supported_features(hass, mqtt_mock): +async def test_commands_without_supported_features( + hass, mqtt_mock_entry_with_yaml_config +): """Test commands which are not supported by the vacuum.""" config = deepcopy(DEFAULT_CONFIG) services = mqttvacuum.STRING_TO_SERVICE["status"] @@ -181,6 +185,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True @@ -228,7 +233,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): mqtt_mock.async_publish.assert_not_called() -async def test_status(hass, mqtt_mock): +async def test_status(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -237,6 +242,7 @@ async def test_status(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state.state == STATE_UNKNOWN @@ -272,7 +278,7 @@ async def test_status(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_no_fan_vacuum(hass, mqtt_mock): +async def test_no_fan_vacuum(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum when fan is not supported.""" config = deepcopy(DEFAULT_CONFIG) del config[mqttvacuum.CONF_FAN_SPEED_LIST] @@ -282,6 +288,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54, @@ -323,7 +330,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): @pytest.mark.no_fail_on_log_exception -async def test_status_invalid_json(hass, mqtt_mock): +async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -332,83 +339,98 @@ async def test_status_invalid_json(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "vacuum/state", '{"asdfasas false}') state = hass.states.get("vacuum.mqtttest") assert state.state == STATE_UNKNOWN -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2, MQTT_VACUUM_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + vacuum.DOMAIN, + DEFAULT_CONFIG_2, + MQTT_VACUUM_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one vacuum per unique_id.""" config = { vacuum.DOMAIN: [ @@ -428,92 +450,103 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, vacuum.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, config + ) -async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_removal_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered vacuum.""" data = '{ "schema": "state", "name": "test", "command_topic": "test_topic"}' - await help_test_discovery_removal(hass, mqtt_mock, caplog, vacuum.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data + ) -async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered vacuum.""" config1 = {"schema": "state", "name": "Beer", "command_topic": "test_topic"} config2 = {"schema": "state", "name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, vacuum.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_vacuum( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered vacuum.""" data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic"}' with patch( "homeassistant.components.mqtt.vacuum.schema_state.MqttStateVacuum.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + vacuum.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic#"}' data2 = '{ "schema": "state", "name": "Milk", "command_topic": "test_topic"}' await help_test_discovery_broken( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2, vacuum.SERVICE_START, @@ -564,7 +597,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -590,7 +623,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -602,11 +635,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN config = DEFAULT_CONFIG - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -634,12 +669,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG, diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index e2ffc602ddd..7c1663b9c09 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -11,8 +11,9 @@ from homeassistant.core import callback from tests.common import async_fire_mqtt_message -async def test_subscribe_topics(hass, mqtt_mock, caplog): +async def test_subscribe_topics(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test subscription to topics.""" + await mqtt_mock_entry_no_yaml_config() calls1 = [] @callback @@ -59,8 +60,9 @@ async def test_subscribe_topics(hass, mqtt_mock, caplog): assert len(calls2) == 1 -async def test_modify_topics(hass, mqtt_mock, caplog): +async def test_modify_topics(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test modification of topics.""" + await mqtt_mock_entry_no_yaml_config() calls1 = [] @callback @@ -121,8 +123,9 @@ async def test_modify_topics(hass, mqtt_mock, caplog): assert len(calls2) == 1 -async def test_qos_encoding_default(hass, mqtt_mock, caplog): +async def test_qos_encoding_default(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test default qos and encoding.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() @callback def msg_callback(*args): @@ -136,11 +139,12 @@ async def test_qos_encoding_default(hass, mqtt_mock, caplog): {"test_topic1": {"topic": "test-topic1", "msg_callback": msg_callback}}, ) await async_subscribe_topics(hass, sub_state) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic1", ANY, 0, "utf-8") + mqtt_mock.async_subscribe.assert_called_with("test-topic1", ANY, 0, "utf-8") -async def test_qos_encoding_custom(hass, mqtt_mock, caplog): +async def test_qos_encoding_custom(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test custom qos and encoding.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() @callback def msg_callback(*args): @@ -161,11 +165,12 @@ async def test_qos_encoding_custom(hass, mqtt_mock, caplog): }, ) await async_subscribe_topics(hass, sub_state) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic1", ANY, 1, "utf-16") + mqtt_mock.async_subscribe.assert_called_with("test-topic1", ANY, 1, "utf-16") -async def test_no_change(hass, mqtt_mock, caplog): +async def test_no_change(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test subscription to topics without change.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() calls = [] diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 699f0de87f0..b217bf40c22 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -53,7 +53,7 @@ DEFAULT_CONFIG = { } -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -71,6 +71,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN @@ -93,7 +94,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending MQTT commands in optimistic mode.""" fake_state = ha.State("switch.test", "on") @@ -116,6 +119,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_ON @@ -139,7 +143,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_sending_inital_state_and_optimistic(hass, mqtt_mock): +async def test_sending_inital_state_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the initial state in optimistic mode.""" assert await async_setup_component( hass, @@ -153,13 +159,16 @@ async def test_sending_inital_state_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -177,6 +186,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN @@ -197,21 +207,23 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { switch.DOMAIN: { @@ -225,11 +237,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, switch.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + switch.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { switch.DOMAIN: { @@ -243,11 +261,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, switch.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + switch.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_state_payload(hass, mqtt_mock): +async def test_custom_state_payload(hass, mqtt_mock_entry_with_yaml_config): """Test the state payload.""" assert await async_setup_component( hass, @@ -266,6 +290,7 @@ async def test_custom_state_payload(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN @@ -282,49 +307,57 @@ async def test_custom_state_payload(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, {} + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG, {} ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one switch per unique_id.""" config = { switch.DOMAIN: [ @@ -344,20 +377,26 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, switch.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, config + ) -async def test_discovery_removal_switch(hass, mqtt_mock, caplog): +async def test_discovery_removal_switch(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered switch.""" data = ( '{ "name": "test",' ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, switch.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, data + ) -async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_update_switch_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) @@ -382,7 +421,7 @@ async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, config1, @@ -392,7 +431,9 @@ async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): +async def test_discovery_update_switch_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) @@ -415,7 +456,7 @@ async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, config1, @@ -425,7 +466,9 @@ async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_switch(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_switch( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered switch.""" data1 = ( '{ "name": "Beer",' @@ -437,12 +480,17 @@ async def test_discovery_update_unchanged_switch(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.switch.MqttSwitch.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, switch.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + switch.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -451,56 +499,60 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, switch.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, switch.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + switch.DOMAIN, + DEFAULT_CONFIG, + switch.SERVICE_TURN_ON, ) @@ -525,7 +577,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -539,7 +591,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -551,11 +603,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = switch.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -572,12 +626,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG[switch.DOMAIN], diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 99e2fffc085..09be31011f2 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -62,8 +62,11 @@ def tag_mock(): @pytest.mark.no_fail_on_log_exception -async def test_discover_bad_tag(hass, device_reg, entity_reg, mqtt_mock, tag_mock): +async def test_discover_bad_tag( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config, tag_mock +): """Test bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) # Test sending bad data @@ -84,9 +87,10 @@ async def test_discover_bad_tag(hass, device_reg, entity_reg, mqtt_mock, tag_moc async def test_if_fires_on_mqtt_message_with_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning, with device.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -100,9 +104,10 @@ async def test_if_fires_on_mqtt_message_with_device( async def test_if_fires_on_mqtt_message_without_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning, without device.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -115,9 +120,10 @@ async def test_if_fires_on_mqtt_message_without_device( async def test_if_fires_on_mqtt_message_with_template( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning, with device.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_JSON) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -130,8 +136,9 @@ async def test_if_fires_on_mqtt_message_with_template( tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) -async def test_strip_tag_id(hass, device_reg, mqtt_mock, tag_mock): +async def test_strip_tag_id(hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock): """Test strip whitespace from tag_id.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -144,9 +151,10 @@ async def test_strip_tag_id(hass, device_reg, mqtt_mock, tag_mock): async def test_if_fires_on_mqtt_message_after_update_with_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after update.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) config1["some_future_option_1"] = "future_option_1" config2 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) @@ -190,9 +198,10 @@ async def test_if_fires_on_mqtt_message_after_update_with_device( async def test_if_fires_on_mqtt_message_after_update_without_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after update.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG) config2 = copy.deepcopy(DEFAULT_CONFIG) config2["topic"] = "foobar/tag_scanned2" @@ -233,9 +242,10 @@ async def test_if_fires_on_mqtt_message_after_update_without_device( async def test_if_fires_on_mqtt_message_after_update_with_template( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after update.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG_JSON) config2 = copy.deepcopy(DEFAULT_CONFIG_JSON) config2["value_template"] = "{{ value_json.RDM6300.UID }}" @@ -277,8 +287,11 @@ async def test_if_fires_on_mqtt_message_after_update_with_template( tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) -async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): +async def test_no_resubscribe_same_topic( + hass, device_reg, mqtt_mock_entry_no_yaml_config +): """Test subscription to topics without change.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -292,9 +305,10 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after removal.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -325,9 +339,10 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_without_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning not firing after removal.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -360,11 +375,13 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( hass, hass_ws_client, device_reg, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, tag_mock, ): """Test tag scanning after removal.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) @@ -397,8 +414,9 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( tag_mock.assert_not_called() -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -427,8 +445,9 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -455,8 +474,9 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) config = { @@ -489,9 +509,13 @@ async def test_entity_device_info_update(hass, mqtt_mock): assert device.name == "Milk" -async def test_cleanup_tag(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_tag( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test tag discovery topic is cleaned when device is removed from registry.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) mqtt_entry = hass.config_entries.async_entries("mqtt")[0] @@ -566,8 +590,11 @@ async def test_cleanup_tag(hass, hass_ws_client, device_reg, entity_reg, mqtt_mo ) -async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry when tag is removed.""" + await mqtt_mock_entry_no_yaml_config() config = { "topic": "test-topic", "device": {"identifiers": ["helloworld"]}, @@ -590,9 +617,10 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): async def test_cleanup_device_several_tags( - hass, device_reg, entity_reg, mqtt_mock, tag_mock + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test removal from device registry when the last tag is removed.""" + await mqtt_mock_entry_no_yaml_config() config1 = { "topic": "test-topic1", "device": {"identifiers": ["helloworld"]}, @@ -634,12 +662,13 @@ async def test_cleanup_device_several_tags( async def test_cleanup_device_with_entity_and_trigger_1( - hass, device_reg, entity_reg, mqtt_mock + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test removal from device registry for device with tag, entity and trigger. Tag removed first, then trigger and entity. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "topic": "test-topic", "device": {"identifiers": ["helloworld"]}, @@ -697,11 +726,14 @@ async def test_cleanup_device_with_entity_and_trigger_1( assert device_entry is None -async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_with_entity2( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry for device with tag, entity and trigger. Trigger and entity removed first, then tag. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "topic": "test-topic", "device": {"identifiers": ["helloworld"]}, diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index c2c77dcddd8..a4079558c34 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -18,9 +18,10 @@ def calls(hass): @pytest.fixture(autouse=True) -def setup_comp(hass, mqtt_mock): +async def setup_comp(hass, mqtt_mock_entry_no_yaml_config): """Initialize components.""" mock_component(hass, "group") + return await mqtt_mock_entry_no_yaml_config() async def test_if_fires_on_topic_match(hass, calls): @@ -213,7 +214,7 @@ async def test_if_not_fires_on_topic_but_no_payload_match(hass, calls): assert len(calls) == 0 -async def test_encoding_default(hass, calls, mqtt_mock): +async def test_encoding_default(hass, calls, setup_comp): """Test default encoding.""" assert await async_setup_component( hass, @@ -226,10 +227,10 @@ async def test_encoding_default(hass, calls, mqtt_mock): }, ) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic", ANY, 0, "utf-8") + setup_comp.async_subscribe.assert_called_with("test-topic", ANY, 0, "utf-8") -async def test_encoding_custom(hass, calls, mqtt_mock): +async def test_encoding_custom(hass, calls, setup_comp): """Test default encoding.""" assert await async_setup_component( hass, @@ -242,4 +243,4 @@ async def test_encoding_custom(hass, calls, mqtt_mock): }, ) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic", ANY, 0, None) + setup_comp.async_subscribe.assert_called_with("test-topic", ANY, 0, None) diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index d17484cc5e9..b0cba664250 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -26,7 +26,7 @@ LOCATION_MESSAGE_INCOMPLETE = {"longitude": 2.0} @pytest.fixture(autouse=True) -def setup_comp(hass, mqtt_mock): +async def setup_comp(hass, mqtt_mock_entry_with_yaml_config): """Initialize components.""" yaml_devices = hass.config.path(YAML_DEVICES) yield diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index c3b8704c754..b17a2bed457 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -47,7 +47,7 @@ async def assert_distance(hass, distance): assert state.attributes.get("distance") == distance -async def test_room_update(hass, mqtt_mock): +async def test_room_update(hass, mqtt_mock_entry_with_yaml_config): """Test the updating between rooms.""" assert await async_setup_component( hass, diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 0b7e3726482..a97b877d819 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -1,7 +1,7 @@ """The tests for the MQTT discovery.""" import copy import json -from unittest.mock import patch +from unittest.mock import ANY, patch from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED @@ -19,9 +19,7 @@ async def test_subscribing_config_topic(hass, mqtt_mock, setup_tasmota): discovery_topic = DEFAULT_PREFIX assert mqtt_mock.async_subscribe.called - call_args = mqtt_mock.async_subscribe.mock_calls[0][1] - assert call_args[0] == discovery_topic + "/#" - assert call_args[2] == 0 + mqtt_mock.async_subscribe.assert_any_call(discovery_topic + "/#", ANY, 0, "utf-8") async def test_future_discovery_message(hass, mqtt_mock, caplog): diff --git a/tests/conftest.py b/tests/conftest.py index 8ea6e114e9a..97b1a959d2c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager import functools import logging import ssl @@ -547,8 +548,19 @@ def mqtt_client_mock(hass): @pytest.fixture -async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): +async def mqtt_mock( + hass, + mqtt_client_mock, + mqtt_config, + mqtt_mock_entry_no_yaml_config, +): """Fixture to mock MQTT component.""" + return await mqtt_mock_entry_no_yaml_config() + + +@asynccontextmanager +async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config): + """Fixture to mock a delayed setup of the MQTT config entry.""" if mqtt_config is None: mqtt_config = {mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}} @@ -557,29 +569,79 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): entry = MockConfigEntry( data=mqtt_config, domain=mqtt.DOMAIN, - title="Tasmota", + title="MQTT", ) - entry.add_to_hass(hass) - # Do not forward the entry setup to the components here - with patch("homeassistant.components.mqtt.PLATFORMS", []): - assert await hass.config_entries.async_setup(entry.entry_id) + + real_mqtt = mqtt.MQTT + real_mqtt_instance = None + mock_mqtt_instance = None + + async def _setup_mqtt_entry(setup_entry): + """Set up the MQTT config entry.""" + assert await setup_entry(hass, entry) + + # Assert that MQTT is setup + assert real_mqtt_instance is not None, "MQTT was not setup correctly" + mock_mqtt_instance.conf = real_mqtt_instance.conf # For diagnostics + mock_mqtt_instance._mqttc = mqtt_client_mock + + # connected set to True to get a more realistic behavior when subscribing + mock_mqtt_instance.connected = True + + hass.helpers.dispatcher.async_dispatcher_send(mqtt.MQTT_CONNECTED) await hass.async_block_till_done() - mqtt_component_mock = MagicMock( - return_value=hass.data["mqtt"], - spec_set=hass.data["mqtt"], - wraps=hass.data["mqtt"], - ) - mqtt_component_mock.conf = hass.data["mqtt"].conf # For diagnostics - mqtt_component_mock._mqttc = mqtt_client_mock - # connected set to True to get a more realistics behavior when subscribing - hass.data["mqtt"].connected = True + return mock_mqtt_instance - hass.data["mqtt"] = mqtt_component_mock - component = hass.data["mqtt"] - component.reset_mock() - return component + def create_mock_mqtt(*args, **kwargs): + """Create a mock based on mqtt.MQTT.""" + nonlocal mock_mqtt_instance + nonlocal real_mqtt_instance + real_mqtt_instance = real_mqtt(*args, **kwargs) + mock_mqtt_instance = MagicMock( + return_value=real_mqtt_instance, + spec_set=real_mqtt_instance, + wraps=real_mqtt_instance, + ) + return mock_mqtt_instance + + with patch("homeassistant.components.mqtt.MQTT", side_effect=create_mock_mqtt): + yield _setup_mqtt_entry + + +@pytest.fixture +async def mqtt_mock_entry_no_yaml_config(hass, mqtt_client_mock, mqtt_config): + """Set up an MQTT config entry without MQTT yaml config.""" + + async def _async_setup_config_entry(hass, entry): + """Help set up the config entry.""" + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return True + + async def _setup_mqtt_entry(): + """Set up the MQTT config entry.""" + return await mqtt_mock_entry(_async_setup_config_entry) + + async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + yield _setup_mqtt_entry + + +@pytest.fixture +async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): + """Set up an MQTT config entry with MQTT yaml config.""" + + async def _async_do_not_setup_config_entry(hass, entry): + """Do nothing.""" + return True + + async def _setup_mqtt_entry(): + """Set up the MQTT config entry.""" + return await mqtt_mock_entry(_async_do_not_setup_config_entry) + + async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + yield _setup_mqtt_entry @pytest.fixture(autouse=True) From b74bd1aa0a01f1d853c0b2be611b8876e1cb7357 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 2 Jun 2022 00:12:38 -0500 Subject: [PATCH 004/107] Remove announce workaround for Sonos (#72854) --- homeassistant/components/sonos/media_player.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index cd129d82843..f331f980bb4 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -25,7 +25,6 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, - ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -544,9 +543,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """ # Use 'replace' as the default enqueue option enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) - if kwargs.get(ATTR_MEDIA_ANNOUNCE): - # Temporary workaround until announce support is added - enqueue = MediaPlayerEnqueue.PLAY if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) From 880590da642ab0e5180754c9b19769c0704ab0a4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 2 Jun 2022 00:04:14 +0200 Subject: [PATCH 005/107] Update frontend to 20220601.0 (#72855) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index d9e80b4eff8..7d07bbd543c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220531.0"], + "requirements": ["home-assistant-frontend==20220601.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ad2539233f4..076b58b5185 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220531.0 +home-assistant-frontend==20220601.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 6341528b09a..e91ff265ef7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220531.0 +home-assistant-frontend==20220601.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5767c1e468..964290f6cf8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220531.0 +home-assistant-frontend==20220601.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 01b3da1554247fda7eeb4465dca4acbc641df86b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Jun 2022 19:13:09 -1000 Subject: [PATCH 006/107] Ensure recorder shuts down when its startup future is canceled out from under it (#72866) --- homeassistant/components/recorder/core.py | 14 +++++++++++--- tests/components/recorder/test_init.py | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 7df4cf57e56..7a096a9c404 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable +from concurrent.futures import CancelledError import contextlib from datetime import datetime, timedelta import logging @@ -518,9 +519,16 @@ class Recorder(threading.Thread): def _wait_startup_or_shutdown(self) -> object | None: """Wait for startup or shutdown before starting.""" - return asyncio.run_coroutine_threadsafe( - self._async_wait_for_started(), self.hass.loop - ).result() + try: + return asyncio.run_coroutine_threadsafe( + self._async_wait_for_started(), self.hass.loop + ).result() + except CancelledError as ex: + _LOGGER.warning( + "Recorder startup was externally canceled before it could complete: %s", + ex, + ) + return SHUTDOWN_TASK def run(self) -> None: """Start processing events to save.""" diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 41c4428ee5e..87dbce3ba3b 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -118,6 +118,26 @@ async def test_shutdown_before_startup_finishes( assert run_info.end is not None +async def test_canceled_before_startup_finishes( + hass: HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, + caplog: pytest.LogCaptureFixture, +): + """Test recorder shuts down when its startup future is canceled out from under it.""" + hass.state = CoreState.not_running + await async_setup_recorder_instance(hass) + instance = get_instance(hass) + await instance.async_db_ready + instance._hass_started.cancel() + with patch.object(instance, "engine"): + await hass.async_block_till_done() + await hass.async_add_executor_job(instance.join) + assert ( + "Recorder startup was externally canceled before it could complete" + in caplog.text + ) + + async def test_shutdown_closes_connections(hass, recorder_mock): """Test shutdown closes connections.""" From 8558ea2f9ad582327207b428f251505d00308774 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Jun 2022 19:12:00 -1000 Subject: [PATCH 007/107] Fix logbook not setting up with an recorder filter that has empty fields (#72869) --- homeassistant/components/recorder/filters.py | 2 +- tests/components/logbook/test_init.py | 38 +++++++++++++++++++- tests/components/recorder/test_filters.py | 20 +++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 0ceb013d8c5..3077f7f57f3 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -36,7 +36,7 @@ def extract_include_exclude_filter_conf(conf: ConfigType) -> dict[str, Any]: """ return { filter_type: { - matcher: set(conf.get(filter_type, {}).get(matcher, [])) + matcher: set(conf.get(filter_type, {}).get(matcher) or []) for matcher in FITLER_MATCHERS } for filter_type in FILTER_TYPES diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 2903f29f5dc..d33bbd5b8ac 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -11,7 +11,7 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol -from homeassistant.components import logbook +from homeassistant.components import logbook, recorder from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.logbook.models import LazyEventPartialState @@ -2796,3 +2796,39 @@ async def test_get_events_with_context_state(hass, hass_ws_client, recorder_mock assert results[3]["context_state"] == "off" assert results[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" assert "context_event_type" not in results[3] + + +async def test_logbook_with_empty_config(hass, recorder_mock): + """Test we handle a empty configuration.""" + assert await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: {}, + recorder.DOMAIN: {}, + }, + ) + await hass.async_block_till_done() + + +async def test_logbook_with_non_iterable_entity_filter(hass, recorder_mock): + """Test we handle a non-iterable entity filter.""" + assert await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: ["light.additional_excluded"], + } + }, + recorder.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: None, + CONF_DOMAINS: None, + CONF_ENTITY_GLOBS: None, + } + }, + }, + ) + await hass.async_block_till_done() diff --git a/tests/components/recorder/test_filters.py b/tests/components/recorder/test_filters.py index fa80df6e345..5c0afa10f9d 100644 --- a/tests/components/recorder/test_filters.py +++ b/tests/components/recorder/test_filters.py @@ -12,6 +12,13 @@ from homeassistant.helpers.entityfilter import ( CONF_INCLUDE, ) +EMPTY_INCLUDE_FILTER = { + CONF_INCLUDE: { + CONF_DOMAINS: None, + CONF_ENTITIES: None, + CONF_ENTITY_GLOBS: None, + } +} SIMPLE_INCLUDE_FILTER = { CONF_INCLUDE: { CONF_DOMAINS: ["homeassistant"], @@ -87,6 +94,19 @@ def test_extract_include_exclude_filter_conf(): assert SIMPLE_INCLUDE_EXCLUDE_FILTER[CONF_EXCLUDE][CONF_ENTITIES] != { "cover.altered" } + empty_include_filter = extract_include_exclude_filter_conf(EMPTY_INCLUDE_FILTER) + assert empty_include_filter == { + CONF_EXCLUDE: { + CONF_DOMAINS: set(), + CONF_ENTITIES: set(), + CONF_ENTITY_GLOBS: set(), + }, + CONF_INCLUDE: { + CONF_DOMAINS: set(), + CONF_ENTITIES: set(), + CONF_ENTITY_GLOBS: set(), + }, + } def test_merge_include_exclude_filters(): From b28b204b8643ec0bf09e371b7c3a2d5366cd0d86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 05:39:53 -1000 Subject: [PATCH 008/107] Only present history_stats state as unknown if the time is in the future (#72880) --- homeassistant/components/history_stats/data.py | 9 +++------ tests/components/history_stats/test_sensor.py | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 8153557422d..5466498fc32 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -73,7 +73,8 @@ class HistoryStats: # History cannot tell the future self._history_current_period = [] self._previous_run_before_start = True - + self._state = HistoryStatsState(None, None, self._period) + return self._state # # We avoid querying the database if the below did NOT happen: # @@ -82,7 +83,7 @@ class HistoryStats: # - The period shrank in size # - The previous period ended before now # - elif ( + if ( not self._previous_run_before_start and current_period_start_timestamp == previous_period_start_timestamp and ( @@ -117,10 +118,6 @@ class HistoryStats: ) self._previous_run_before_start = False - if not self._history_current_period: - self._state = HistoryStatsState(None, None, self._period) - return self._state - hours_matched, match_count = self._async_compute_hours_and_changes( now_timestamp, current_period_start_timestamp, diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index b375a8f63c4..f824ee552ca 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -357,7 +357,7 @@ async def test_measure_multiple(hass, recorder_mock): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.5" - assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == "0.0" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "50.0" From 54b94c48269f2519a0e1f06b82c82a7b0e839cb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 02:01:06 -1000 Subject: [PATCH 009/107] Fix migration of MySQL data when InnoDB is not being used (#72893) Fixes #72883 --- homeassistant/components/recorder/migration.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index bc636d34b10..cc5af684566 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -715,14 +715,13 @@ def _apply_update( # noqa: C901 if engine.dialect.name == SupportedDialect.MYSQL: # Ensure the row format is dynamic or the index # unique will be too large - with session_scope(session=session_maker()) as session: - connection = session.connection() - # This is safe to run multiple times and fast since the table is small - connection.execute( - text( - "ALTER TABLE statistics_meta ENGINE=InnoDB, ROW_FORMAT=DYNAMIC" + with contextlib.suppress(SQLAlchemyError): + with session_scope(session=session_maker()) as session: + connection = session.connection() + # This is safe to run multiple times and fast since the table is small + connection.execute( + text("ALTER TABLE statistics_meta ROW_FORMAT=DYNAMIC") ) - ) try: _create_index( session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" From a4297c04116c343955b4580cc93483b7f7414806 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 11:54:06 -1000 Subject: [PATCH 010/107] Fix performance of logbook entity and devices queries with large MySQL databases (#72898) --- .../components/logbook/queries/common.py | 20 +++++++-- .../components/logbook/queries/devices.py | 32 +++++++++----- .../components/logbook/queries/entities.py | 39 ++++++++++------ .../logbook/queries/entities_and_devices.py | 44 ++++++++++++------- homeassistant/components/recorder/models.py | 2 + 5 files changed, 93 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index 6049d6beb81..a7a4f84a59e 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -12,9 +12,11 @@ from sqlalchemy.sql.selectable import Select from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.recorder.models import ( + EVENTS_CONTEXT_ID_INDEX, OLD_FORMAT_ATTRS_JSON, OLD_STATE, SHARED_ATTRS_JSON, + STATES_CONTEXT_ID_INDEX, EventData, Events, StateAttributes, @@ -121,9 +123,7 @@ def select_events_context_only() -> Select: By marking them as context_only we know they are only for linking context ids and we can avoid processing them. """ - return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY).outerjoin( - EventData, (Events.data_id == EventData.data_id) - ) + return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY) def select_states_context_only() -> Select: @@ -252,3 +252,17 @@ def _not_uom_attributes_matcher() -> ClauseList: return ~StateAttributes.shared_attrs.like( UNIT_OF_MEASUREMENT_JSON_LIKE ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE) + + +def apply_states_context_hints(query: Query) -> Query: + """Force mysql to use the right index on large context_id selects.""" + return query.with_hint( + States, f"FORCE INDEX ({STATES_CONTEXT_ID_INDEX})", dialect_name="mysql" + ) + + +def apply_events_context_hints(query: Query) -> Query: + """Force mysql to use the right index on large context_id selects.""" + return query.with_hint( + Events, f"FORCE INDEX ({EVENTS_CONTEXT_ID_INDEX})", dialect_name="mysql" + ) diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index 64a6477017e..88e9f50a42c 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -4,15 +4,22 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt -from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy import lambda_stmt, select from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import DEVICE_ID_IN_EVENT, Events, States +from homeassistant.components.recorder.models import ( + DEVICE_ID_IN_EVENT, + EventData, + Events, + States, +) from .common import ( + apply_events_context_hints, + apply_states_context_hints, select_events_context_id_subquery, select_events_context_only, select_events_without_states, @@ -27,13 +34,10 @@ def _select_device_id_context_ids_sub_query( json_quotable_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple devices.""" - return select( - union_all( - select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quotable_device_ids) - ), - ).c.context_id + inner = select_events_context_id_subquery(start_day, end_day, event_types).where( + apply_event_device_id_matchers(json_quotable_device_ids) ) + return select(inner.c.context_id).group_by(inner.c.context_id) def _apply_devices_context_union( @@ -51,8 +55,16 @@ def _apply_devices_context_union( json_quotable_device_ids, ).cte() return query.union_all( - select_events_context_only().where(Events.context_id.in_(devices_cte.select())), - select_states_context_only().where(States.context_id.in_(devices_cte.select())), + apply_events_context_hints( + select_events_context_only() + .select_from(devices_cte) + .outerjoin(Events, devices_cte.c.context_id == Events.context_id) + ).outerjoin(EventData, (Events.data_id == EventData.data_id)), + apply_states_context_hints( + select_states_context_only() + .select_from(devices_cte) + .outerjoin(States, devices_cte.c.context_id == States.context_id) + ), ) diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 4fb211688f3..8de4a5eaf64 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -14,11 +14,14 @@ from homeassistant.components.recorder.models import ( ENTITY_ID_IN_EVENT, ENTITY_ID_LAST_UPDATED_INDEX, OLD_ENTITY_ID_IN_EVENT, + EventData, Events, States, ) from .common import ( + apply_events_context_hints, + apply_states_context_hints, apply_states_filters, select_events_context_id_subquery, select_events_context_only, @@ -36,16 +39,15 @@ def _select_entities_context_ids_sub_query( json_quotable_entity_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities.""" - return select( - union_all( - select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) - ), - apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id.in_(entity_ids)), - ).c.context_id + union = union_all( + select_events_context_id_subquery(start_day, end_day, event_types).where( + apply_event_entity_id_matchers(json_quotable_entity_ids) + ), + apply_entities_hints(select(States.context_id)) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id.in_(entity_ids)), ) + return select(union.c.context_id).group_by(union.c.context_id) def _apply_entities_context_union( @@ -64,14 +66,23 @@ def _apply_entities_context_union( entity_ids, json_quotable_entity_ids, ).cte() + # We used to optimize this to exclude rows we already in the union with + # a States.entity_id.not_in(entity_ids) but that made the + # query much slower on MySQL, and since we already filter them away + # in the python code anyways since they will have context_only + # set on them the impact is minimal. return query.union_all( states_query_for_entity_ids(start_day, end_day, entity_ids), - select_events_context_only().where( - Events.context_id.in_(entities_cte.select()) + apply_events_context_hints( + select_events_context_only() + .select_from(entities_cte) + .outerjoin(Events, entities_cte.c.context_id == Events.context_id) + ).outerjoin(EventData, (Events.data_id == EventData.data_id)), + apply_states_context_hints( + select_states_context_only() + .select_from(entities_cte) + .outerjoin(States, entities_cte.c.context_id == States.context_id) ), - select_states_context_only() - .where(States.entity_id.not_in(entity_ids)) - .where(States.context_id.in_(entities_cte.select())), ) diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index d1c86ddbec5..1c4271422b7 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -10,9 +10,11 @@ from sqlalchemy.orm import Query from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import Events, States +from homeassistant.components.recorder.models import EventData, Events, States from .common import ( + apply_events_context_hints, + apply_states_context_hints, select_events_context_id_subquery, select_events_context_only, select_events_without_states, @@ -35,18 +37,17 @@ def _select_entities_device_id_context_ids_sub_query( json_quotable_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities and multiple devices.""" - return select( - union_all( - select_events_context_id_subquery(start_day, end_day, event_types).where( - _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids - ) - ), - apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id.in_(entity_ids)), - ).c.context_id + union = union_all( + select_events_context_id_subquery(start_day, end_day, event_types).where( + _apply_event_entity_id_device_id_matchers( + json_quotable_entity_ids, json_quotable_device_ids + ) + ), + apply_entities_hints(select(States.context_id)) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id.in_(entity_ids)), ) + return select(union.c.context_id).group_by(union.c.context_id) def _apply_entities_devices_context_union( @@ -66,14 +67,23 @@ def _apply_entities_devices_context_union( json_quotable_entity_ids, json_quotable_device_ids, ).cte() + # We used to optimize this to exclude rows we already in the union with + # a States.entity_id.not_in(entity_ids) but that made the + # query much slower on MySQL, and since we already filter them away + # in the python code anyways since they will have context_only + # set on them the impact is minimal. return query.union_all( states_query_for_entity_ids(start_day, end_day, entity_ids), - select_events_context_only().where( - Events.context_id.in_(devices_entities_cte.select()) + apply_events_context_hints( + select_events_context_only() + .select_from(devices_entities_cte) + .outerjoin(Events, devices_entities_cte.c.context_id == Events.context_id) + ).outerjoin(EventData, (Events.data_id == EventData.data_id)), + apply_states_context_hints( + select_states_context_only() + .select_from(devices_entities_cte) + .outerjoin(States, devices_entities_cte.c.context_id == States.context_id) ), - select_states_context_only() - .where(States.entity_id.not_in(entity_ids)) - .where(States.context_id.in_(devices_entities_cte.select())), ) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 70c816c2af5..8db648f15a8 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -93,6 +93,8 @@ TABLES_TO_CHECK = [ LAST_UPDATED_INDEX = "ix_states_last_updated" ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" +EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" +STATES_CONTEXT_ID_INDEX = "ix_states_context_id" EMPTY_JSON_OBJECT = "{}" From 2b77db259775fbadaeab032ad73888c42c41cfce Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Jun 2022 20:32:31 +0200 Subject: [PATCH 011/107] Fix reload of MQTT yaml config (#72901) --- .../components/mqtt/alarm_control_panel.py | 18 +++--- .../components/mqtt/binary_sensor.py | 18 +++--- homeassistant/components/mqtt/button.py | 18 +++--- homeassistant/components/mqtt/camera.py | 18 +++--- homeassistant/components/mqtt/climate.py | 18 +++--- homeassistant/components/mqtt/cover.py | 18 +++--- homeassistant/components/mqtt/fan.py | 18 +++--- homeassistant/components/mqtt/humidifier.py | 21 +++---- .../components/mqtt/light/__init__.py | 18 +++--- homeassistant/components/mqtt/lock.py | 18 +++--- homeassistant/components/mqtt/mixins.py | 58 ++++++++++++++++--- homeassistant/components/mqtt/number.py | 18 +++--- homeassistant/components/mqtt/scene.py | 18 +++--- homeassistant/components/mqtt/select.py | 18 +++--- homeassistant/components/mqtt/sensor.py | 18 +++--- homeassistant/components/mqtt/siren.py | 18 +++--- homeassistant/components/mqtt/switch.py | 18 +++--- .../components/mqtt/vacuum/__init__.py | 18 +++--- tests/components/mqtt/test_binary_sensor.py | 2 +- tests/components/mqtt/test_common.py | 53 ++++++++++++----- tests/components/mqtt/test_sensor.py | 2 +- 21 files changed, 241 insertions(+), 183 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c20fbb7c657..c0c6f9732d7 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -1,7 +1,6 @@ """This platform enables the possibility to control a MQTT alarm.""" from __future__ import annotations -import asyncio import functools import logging import re @@ -45,8 +44,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -133,7 +132,11 @@ async def async_setup_platform( """Set up MQTT alarm control panel configured under the alarm_control_panel key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, alarm.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + alarm.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -144,13 +147,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 1cb90d6c903..cec065e20f2 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,7 +1,6 @@ """Support for MQTT binary sensors.""" from __future__ import annotations -import asyncio from datetime import timedelta import functools import logging @@ -42,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -88,7 +87,11 @@ async def async_setup_platform( """Set up MQTT binary sensor configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, binary_sensor.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + binary_sensor.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -99,12 +102,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index b50856d20c1..afa9900db35 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -1,7 +1,6 @@ """Support for MQTT buttons.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -26,8 +25,8 @@ from .const import ( from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -68,7 +67,11 @@ async def async_setup_platform( """Set up MQTT button configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, button.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + button.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -79,12 +82,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index ae38e07d17a..86db828b111 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,7 +1,6 @@ """Camera that loads a picture from an MQTT topic.""" from __future__ import annotations -import asyncio from base64 import b64decode import functools @@ -23,8 +22,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -66,7 +65,11 @@ async def async_setup_platform( """Set up MQTT camera configured under the camera platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, camera.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + camera.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -77,12 +80,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 64b462359be..bdcc82f2c39 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -1,7 +1,6 @@ """Support for MQTT climate devices.""" from __future__ import annotations -import asyncio import functools import logging @@ -51,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -377,7 +376,11 @@ async def async_setup_platform( """Set up MQTT climate configured under the fan platform key (deprecated).""" # The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, climate.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + climate.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -388,12 +391,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 5814f3e43f7..325433817c0 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -1,7 +1,6 @@ """Support for MQTT cover devices.""" from __future__ import annotations -import asyncio import functools from json import JSONDecodeError, loads as json_loads import logging @@ -46,8 +45,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -227,7 +226,11 @@ async def async_setup_platform( """Set up MQTT covers configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, cover.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + cover.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -238,13 +241,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f72b0bdf689..d0b4ff10692 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,7 +1,6 @@ """Support for MQTT fans.""" from __future__ import annotations -import asyncio import functools import logging import math @@ -50,8 +49,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -217,7 +216,11 @@ async def async_setup_platform( """Set up MQTT fans configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, fan.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + fan.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -228,13 +231,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 000a9b9700e..1c9ec5dc201 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -1,7 +1,6 @@ """Support for MQTT humidifiers.""" from __future__ import annotations -import asyncio import functools import logging @@ -46,8 +45,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -173,7 +172,11 @@ async def async_setup_platform( """Set up MQTT humidifier configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, humidifier.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + humidifier.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -184,14 +187,12 @@ async def async_setup_entry( ) -> None: """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN ) - ) # setup for discovery + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index ab2a3462615..158ea6ffa0d 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,7 +1,6 @@ """Support for MQTT lights.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -14,8 +13,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -97,7 +96,11 @@ async def async_setup_platform( """Set up MQTT light through configuration.yaml (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, light.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + light.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -108,13 +111,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT lights configured under the light platform key (deprecated).""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 0cfd1d2b70f..862e76635f7 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -1,7 +1,6 @@ """Support for MQTT locks.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -28,8 +27,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -88,7 +87,11 @@ async def async_setup_platform( """Set up MQTT locks configured under the lock platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, lock.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + lock.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -99,13 +102,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 694fae0b3c0..b0f17cc335b 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -2,6 +2,7 @@ from __future__ import annotations from abc import abstractmethod +import asyncio from collections.abc import Callable import json import logging @@ -27,10 +28,11 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, + discovery, entity_registry as er, ) from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED @@ -46,7 +48,10 @@ from homeassistant.helpers.entity import ( async_generate_entity_id, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.reload import ( + async_integration_yaml_config, + async_setup_reload_service, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription @@ -260,8 +265,44 @@ class SetupEntity(Protocol): """Define setup_entities type.""" +async def async_setup_platform_discovery( + hass: HomeAssistant, platform_domain: str, schema: vol.Schema +) -> CALLBACK_TYPE: + """Set up platform discovery for manual config.""" + + async def _async_discover_entities(event: Event | None) -> None: + """Discover entities for a platform.""" + if event: + # The platform has been reloaded + config_yaml = await async_integration_yaml_config(hass, DOMAIN) + if not config_yaml: + return + config_yaml = config_yaml.get(DOMAIN, {}) + else: + config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) + if not config_yaml: + return + if platform_domain not in config_yaml: + return + await asyncio.gather( + *( + discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) + for config in await async_get_platform_config_from_yaml( + hass, platform_domain, schema, config_yaml + ) + ) + ) + + unsub = hass.bus.async_listen("event_mqtt_reloaded", _async_discover_entities) + await _async_discover_entities(None) + return unsub + + async def async_get_platform_config_from_yaml( - hass: HomeAssistant, domain: str, schema: vol.Schema + hass: HomeAssistant, + platform_domain: str, + schema: vol.Schema, + config_yaml: ConfigType = None, ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" @@ -275,12 +316,15 @@ async def async_get_platform_config_from_yaml( try: validated_config.append(schema(config_item)) except vol.MultipleInvalid as err: - async_log_exception(err, domain, config_item, hass) + async_log_exception(err, platform_domain, config_item, hass) return validated_config - config_yaml: ConfigType = hass.data.get(DATA_MQTT_CONFIG, {}) - if not (platform_configs := config_yaml.get(domain)): + if config_yaml is None: + config_yaml = hass.data.get(DATA_MQTT_CONFIG) + if not config_yaml: + return [] + if not (platform_configs := config_yaml.get(platform_domain)): return [] return async_validate_config(hass, platform_configs) @@ -310,7 +354,7 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema): async def async_setup_platform_helper( hass: HomeAssistant, platform_domain: str, - config: ConfigType, + config: ConfigType | DiscoveryInfoType, async_add_entities: AddEntitiesCallback, async_setup_entities: SetupEntity, ) -> None: diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 6ea1f0959f6..1404dc86a3c 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -1,7 +1,6 @@ """Configure number in a device through MQTT topic.""" from __future__ import annotations -import asyncio import functools import logging @@ -41,8 +40,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -119,7 +118,11 @@ async def async_setup_platform( """Set up MQTT number configured under the number platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, number.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + number.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -130,12 +133,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index ce8f0b0a3e8..9c4a212bd8e 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -1,7 +1,6 @@ """Support for MQTT scenes.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -23,8 +22,8 @@ from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -65,7 +64,11 @@ async def async_setup_platform( """Set up MQTT scene configured under the scene platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, scene.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + scene.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -76,13 +79,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 75e1b4e8efd..994c11653b7 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -1,7 +1,6 @@ """Configure select in a device through MQTT topic.""" from __future__ import annotations -import asyncio import functools import logging @@ -31,8 +30,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -80,7 +79,11 @@ async def async_setup_platform( """Set up MQTT select configured under the select platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, select.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + select.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -91,12 +94,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 4dd1ad4d95f..f9e0b5151bb 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,7 +1,6 @@ """Support for MQTT sensors.""" from __future__ import annotations -import asyncio from datetime import timedelta import functools import logging @@ -42,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -133,7 +132,11 @@ async def async_setup_platform( """Set up MQTT sensors configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, sensor.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + sensor.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -144,12 +147,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 1ecf2c37dbf..fef2a4fb3dd 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -1,7 +1,6 @@ """Support for MQTT sirens.""" from __future__ import annotations -import asyncio import copy import functools import json @@ -52,8 +51,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -129,7 +128,11 @@ async def async_setup_platform( """Set up MQTT sirens configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, siren.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + siren.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -140,13 +143,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index c20ddfe5151..be7fc655e1e 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,7 +1,6 @@ """Support for MQTT switches.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -38,8 +37,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -83,7 +82,11 @@ async def async_setup_platform( """Set up MQTT switch configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, switch.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + switch.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -94,12 +97,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 34205ab7780..206a15a024a 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -1,7 +1,6 @@ """Support for MQTT vacuums.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -13,8 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, ) from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE @@ -77,7 +76,11 @@ async def async_setup_platform( """Set up MQTT vacuum through configuration.yaml.""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, vacuum.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + vacuum.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -88,12 +91,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 37bb783d354..ebb1d78138f 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1011,7 +1011,7 @@ async def test_cleanup_triggers_and_restoring_state( freezer.move_to("2022-02-02 12:01:10+01:00") await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [config1, config2] + hass, caplog, tmp_path, {domain: [config1, config2]} ) assert "Clean up expire after trigger for binary_sensor.test1" in caplog.text assert "Clean up expire after trigger for binary_sensor.test2" not in caplog.text diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 50cf7beb0e0..24482129f3d 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1651,10 +1651,10 @@ async def help_test_publishing_with_custom_encoding( mqtt_mock.async_publish.reset_mock() -async def help_test_reload_with_config(hass, caplog, tmp_path, domain, config): +async def help_test_reload_with_config(hass, caplog, tmp_path, config): """Test reloading with supplied config.""" new_yaml_config_file = tmp_path / "configuration.yaml" - new_yaml_config = yaml.dump({domain: config}) + new_yaml_config = yaml.dump(config) new_yaml_config_file.write_text(new_yaml_config) assert new_yaml_config_file.read_text() == new_yaml_config @@ -1679,16 +1679,27 @@ async def help_test_reloadable( old_config_1["name"] = "test_old_1" old_config_2 = copy.deepcopy(config) old_config_2["name"] = "test_old_2" + old_config_3 = copy.deepcopy(config) + old_config_3["name"] = "test_old_3" + old_config_3.pop("platform") + old_config_4 = copy.deepcopy(config) + old_config_4["name"] = "test_old_4" + old_config_4.pop("platform") - assert await async_setup_component( - hass, domain, {domain: [old_config_1, old_config_2]} - ) + old_config = { + domain: [old_config_1, old_config_2], + "mqtt": {domain: [old_config_3, old_config_4]}, + } + + assert await async_setup_component(hass, domain, old_config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() assert hass.states.get(f"{domain}.test_old_1") assert hass.states.get(f"{domain}.test_old_2") - assert len(hass.states.async_all(domain)) == 2 + assert hass.states.get(f"{domain}.test_old_3") + assert hass.states.get(f"{domain}.test_old_4") + assert len(hass.states.async_all(domain)) == 4 # Create temporary fixture for configuration.yaml based on the supplied config and # test a reload with this new config @@ -1698,16 +1709,31 @@ async def help_test_reloadable( new_config_2["name"] = "test_new_2" new_config_3 = copy.deepcopy(config) new_config_3["name"] = "test_new_3" + new_config_3.pop("platform") + new_config_4 = copy.deepcopy(config) + new_config_4["name"] = "test_new_4" + new_config_4.pop("platform") + new_config_5 = copy.deepcopy(config) + new_config_5["name"] = "test_new_5" + new_config_6 = copy.deepcopy(config) + new_config_6["name"] = "test_new_6" + new_config_6.pop("platform") - await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [new_config_1, new_config_2, new_config_3] - ) + new_config = { + domain: [new_config_1, new_config_2, new_config_5], + "mqtt": {domain: [new_config_3, new_config_4, new_config_6]}, + } - assert len(hass.states.async_all(domain)) == 3 + await help_test_reload_with_config(hass, caplog, tmp_path, new_config) + + assert len(hass.states.async_all(domain)) == 6 assert hass.states.get(f"{domain}.test_new_1") assert hass.states.get(f"{domain}.test_new_2") assert hass.states.get(f"{domain}.test_new_3") + assert hass.states.get(f"{domain}.test_new_4") + assert hass.states.get(f"{domain}.test_new_5") + assert hass.states.get(f"{domain}.test_new_6") async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): @@ -1752,9 +1778,10 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): new_config_3 = copy.deepcopy(config) new_config_3["name"] = "test_new_3" - await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [new_config_1, new_config_2, new_config_3] - ) + new_config = { + domain: [new_config_1, new_config_2, new_config_3], + } + await help_test_reload_with_config(hass, caplog, tmp_path, new_config) assert len(hass.states.async_all(domain)) == 3 diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 894ecc32ecc..7081ae45993 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1106,7 +1106,7 @@ async def test_cleanup_triggers_and_restoring_state( freezer.move_to("2022-02-02 12:01:10+01:00") await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [config1, config2] + hass, caplog, tmp_path, {domain: [config1, config2]} ) await hass.async_block_till_done() From 12e6f143a431f1a26f8ae1897dbe20a7da27516c Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 2 Jun 2022 23:21:22 +0800 Subject: [PATCH 012/107] Bump yolink-api to 0.0.6 (#72903) * Bump yolink-api to 0.0.6 * update testcase --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/yolink/test_config_flow.py | 6 ++---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index a89934154e9..7fb78a4974b 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.5"], + "requirements": ["yolink-api==0.0.6"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index e91ff265ef7..d51e776863d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.5 +yolink-api==0.0.6 # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 964290f6cf8..f7b246c64d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1638,7 +1638,7 @@ yalexs==1.1.25 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.5 +yolink-api==0.0.6 # homeassistant.components.youless youless-api==0.16 diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index 5d6bb8fd727..e224bc3e1d2 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -3,6 +3,8 @@ import asyncio from http import HTTPStatus from unittest.mock import patch +from yolink.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import application_credentials from homeassistant.core import HomeAssistant @@ -12,11 +14,7 @@ from tests.common import MockConfigEntry CLIENT_ID = "12345" CLIENT_SECRET = "6789" -YOLINK_HOST = "api.yosmart.com" -YOLINK_HTTP_HOST = f"http://{YOLINK_HOST}" DOMAIN = "yolink" -OAUTH2_AUTHORIZE = f"{YOLINK_HTTP_HOST}/oauth/v2/authorization.htm" -OAUTH2_TOKEN = f"{YOLINK_HTTP_HOST}/open/yolink/token" async def test_abort_if_no_configuration(hass): From 0e985284c9a43ecaaf9b7202780cb99072a7af8d Mon Sep 17 00:00:00 2001 From: nojocodex <76249511+nojocodex@users.noreply.github.com> Date: Thu, 2 Jun 2022 19:49:08 +0200 Subject: [PATCH 013/107] Fix logging & exit code reporting to S6 on HA shutdown (#72921) --- rootfs/etc/services.d/home-assistant/finish | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rootfs/etc/services.d/home-assistant/finish b/rootfs/etc/services.d/home-assistant/finish index 115d8352618..057957a9c03 100755 --- a/rootfs/etc/services.d/home-assistant/finish +++ b/rootfs/etc/services.d/home-assistant/finish @@ -2,24 +2,24 @@ # ============================================================================== # Take down the S6 supervision tree when Home Assistant fails # ============================================================================== -declare RESTART_EXIT_CODE 100 -declare SIGNAL_EXIT_CODE 256 -declare SIGTERM 15 +declare RESTART_EXIT_CODE=100 +declare SIGNAL_EXIT_CODE=256 +declare SIGTERM=15 declare APP_EXIT_CODE=${1} -declare SYS_EXIT_CODE=${2+x} +declare SIGNAL_NO=${2} declare NEW_EXIT_CODE= -bashio::log.info "Home Assistant Core finish process exit code ${1}" +bashio::log.info "Home Assistant Core finish process exit code ${APP_EXIT_CODE}" if [[ ${APP_EXIT_CODE} -eq ${RESTART_EXIT_CODE} ]]; then exit 0 elif [[ ${APP_EXIT_CODE} -eq ${SIGNAL_EXIT_CODE} ]]; then - bashio::log.info "Home Assistant Core finish process received signal ${APP_EXIT_CODE}" + bashio::log.info "Home Assistant Core finish process received signal ${SIGNAL_NO}" - NEW_EXIT_CODE=$((128 + SYS_EXIT_CODE)) + NEW_EXIT_CODE=$((128 + SIGNAL_NO)) echo ${NEW_EXIT_CODE} > /run/s6-linux-init-container-results/exitcode - if [[ ${NEW_EXIT_CODE} -eq ${SIGTERM} ]]; then + if [[ ${SIGNAL_NO} -eq ${SIGTERM} ]]; then /run/s6/basedir/bin/halt fi else From f1bcfedf84f3fbbe03eceb456b1f520f4fa8631e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 2 Jun 2022 08:40:13 -0700 Subject: [PATCH 014/107] Fix bug in caldav and avoid unnecessary copy of dataclass (#72922) --- homeassistant/components/caldav/calendar.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index c5b2b3a790a..17a8d5deb2f 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -1,7 +1,6 @@ """Support for WebDav Calendar.""" from __future__ import annotations -import copy from datetime import datetime, timedelta import logging import re @@ -143,15 +142,13 @@ class WebDavCalendarEntity(CalendarEntity): def update(self): """Update event data.""" self.data.update() - event = copy.deepcopy(self.data.event) - if event is None: - self._event = event - return - (summary, offset) = extract_offset(event.summary, OFFSET) - event.summary = summary - self._event = event + self._event = self.data.event self._attr_extra_state_attributes = { - "offset_reached": is_offset_reached(event.start_datetime_local, offset) + "offset_reached": is_offset_reached( + self._event.start_datetime_local, self.data.offset + ) + if self._event + else False } @@ -165,6 +162,7 @@ class WebDavCalendarData: self.include_all_day = include_all_day self.search = search self.event = None + self.offset = None async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime @@ -264,13 +262,15 @@ class WebDavCalendarData: return # Populate the entity attributes with the event values + (summary, offset) = extract_offset(vevent.summary.value, OFFSET) self.event = CalendarEvent( - summary=vevent.summary.value, + summary=summary, start=vevent.dtstart.value, end=self.get_end_date(vevent), location=self.get_attr_value(vevent, "location"), description=self.get_attr_value(vevent, "description"), ) + self.offset = offset @staticmethod def is_matching(vevent, search): From f5e0363117f66458299fdf1ac5f1bbc86d8a97fd Mon Sep 17 00:00:00 2001 From: Khole Date: Thu, 2 Jun 2022 22:54:26 +0100 Subject: [PATCH 015/107] Fix Hive authentication (#72929) --- homeassistant/components/hive/__init__.py | 9 ++------- homeassistant/components/hive/config_flow.py | 1 + homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hive/test_config_flow.py | 15 +++++++++++++++ 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 292bbe62ae1..f3ed9674fcd 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -75,14 +75,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hive from a config entry.""" - websession = aiohttp_client.async_get_clientsession(hass) + web_session = aiohttp_client.async_get_clientsession(hass) hive_config = dict(entry.data) - hive = Hive( - websession, - deviceGroupKey=hive_config["device_data"][0], - deviceKey=hive_config["device_data"][1], - devicePassword=hive_config["device_data"][2], - ) + hive = Hive(web_session) hive_config["options"] = {} hive_config["options"].update( diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index 9c391f13294..c713a3011f4 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -102,6 +102,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): raise UnknownHiveError # Setup the config entry + await self.hive_auth.device_registration("Home Assistant") self.data["tokens"] = self.tokens self.data["device_data"] = await self.hive_auth.getDeviceData() if self.context["source"] == config_entries.SOURCE_REAUTH: diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 472adc137ba..d8cd56abe0b 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.4"], + "requirements": ["pyhiveapi==0.5.5"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index d51e776863d..ad38e5c5fb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.4 +pyhiveapi==0.5.5 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7b246c64d2..94b978ece80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.4 +pyhiveapi==0.5.5 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index bb567b0bdfc..51ceec43ad2 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -33,6 +33,9 @@ async def test_import_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ @@ -93,6 +96,9 @@ async def test_user_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ @@ -172,6 +178,9 @@ async def test_user_flow_2fa(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ @@ -256,6 +265,9 @@ async def test_reauth_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -361,6 +373,9 @@ async def test_user_flow_2fa_send_new_code(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ From 6a8a97b57cb7f3789aad5bae4976c6bee488fc34 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Jun 2022 16:15:04 -0700 Subject: [PATCH 016/107] Only sync when HA is started up as we already sync at startup (#72940) --- homeassistant/components/cloud/google_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index a0a68aaf84a..81f00b69b23 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -39,7 +39,6 @@ class CloudGoogleConfig(AbstractConfig): self._cur_entity_prefs = self._prefs.google_entity_configs self._cur_default_expose = self._prefs.google_default_expose self._sync_entities_lock = asyncio.Lock() - self._sync_on_started = False @property def enabled(self): @@ -224,7 +223,7 @@ class CloudGoogleConfig(AbstractConfig): self._cur_entity_prefs = prefs.google_entity_configs self._cur_default_expose = prefs.google_default_expose - if sync_entities: + if sync_entities and self.hass.is_running: await self.async_sync_entities_all() @callback From 69e8f5bb988b615d10ca42ea5e8030076238d142 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Jun 2022 16:20:09 -0700 Subject: [PATCH 017/107] Bumped version to 2022.6.1 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 428aa84d5fb..08ab335a33b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 7f1e739803b..aeafbd53565 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0 +version = 2022.6.1 url = https://www.home-assistant.io/ [options] From 0d9330c39e24ec3723c0150d3f7fe9d4dd8c97b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 17:52:53 -1000 Subject: [PATCH 018/107] Fix misalignments between sql based filtering with the entityfilter based filtering (#72936) --- homeassistant/components/recorder/filters.py | 110 +++- homeassistant/components/recorder/history.py | 6 +- tests/components/history/test_init.py | 19 +- tests/components/logbook/test_init.py | 22 +- .../test_filters_with_entityfilter.py | 516 ++++++++++++++++++ 5 files changed, 620 insertions(+), 53 deletions(-) create mode 100644 tests/components/recorder/test_filters_with_entityfilter.py diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 3077f7f57f3..835496c2d6e 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -88,14 +88,32 @@ class Filters: self.included_domains: Iterable[str] = [] self.included_entity_globs: Iterable[str] = [] + def __repr__(self) -> str: + """Return human readable excludes/includes.""" + return ( + f"" + ) + @property def has_config(self) -> bool: """Determine if there is any filter configuration.""" + return bool(self._have_exclude or self._have_include) + + @property + def _have_exclude(self) -> bool: return bool( self.excluded_entities or self.excluded_domains or self.excluded_entity_globs - or self.included_entities + ) + + @property + def _have_include(self) -> bool: + return bool( + self.included_entities or self.included_domains or self.included_entity_globs ) @@ -103,36 +121,67 @@ class Filters: def _generate_filter_for_columns( self, columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: - includes = [] - if self.included_domains: - includes.append(_domain_matcher(self.included_domains, columns, encoder)) - if self.included_entities: - includes.append(_entity_matcher(self.included_entities, columns, encoder)) - if self.included_entity_globs: - includes.append( - _globs_to_like(self.included_entity_globs, columns, encoder) - ) + """Generate a filter from pre-comuted sets and pattern lists. - excludes = [] - if self.excluded_domains: - excludes.append(_domain_matcher(self.excluded_domains, columns, encoder)) - if self.excluded_entities: - excludes.append(_entity_matcher(self.excluded_entities, columns, encoder)) - if self.excluded_entity_globs: - excludes.append( - _globs_to_like(self.excluded_entity_globs, columns, encoder) - ) + This must match exactly how homeassistant.helpers.entityfilter works. + """ + i_domains = _domain_matcher(self.included_domains, columns, encoder) + i_entities = _entity_matcher(self.included_entities, columns, encoder) + i_entity_globs = _globs_to_like(self.included_entity_globs, columns, encoder) + includes = [i_domains, i_entities, i_entity_globs] - if not includes and not excludes: + e_domains = _domain_matcher(self.excluded_domains, columns, encoder) + e_entities = _entity_matcher(self.excluded_entities, columns, encoder) + e_entity_globs = _globs_to_like(self.excluded_entity_globs, columns, encoder) + excludes = [e_domains, e_entities, e_entity_globs] + + have_exclude = self._have_exclude + have_include = self._have_include + + # Case 1 - no includes or excludes - pass all entities + if not have_include and not have_exclude: return None - if includes and not excludes: + # Case 2 - includes, no excludes - only include specified entities + if have_include and not have_exclude: return or_(*includes).self_group() - if not includes and excludes: + # Case 3 - excludes, no includes - only exclude specified entities + if not have_include and have_exclude: return not_(or_(*excludes).self_group()) - return or_(*includes).self_group() & not_(or_(*excludes).self_group()) + # Case 4 - both includes and excludes specified + # Case 4a - include domain or glob specified + # - if domain is included, pass if entity not excluded + # - if glob is included, pass if entity and domain not excluded + # - if domain and glob are not included, pass if entity is included + # note: if both include domain matches then exclude domains ignored. + # If glob matches then exclude domains and glob checked + if self.included_domains or self.included_entity_globs: + return or_( + (i_domains & ~(e_entities | e_entity_globs)), + ( + ~i_domains + & or_( + (i_entity_globs & ~(or_(*excludes))), + (~i_entity_globs & i_entities), + ) + ), + ).self_group() + + # Case 4b - exclude domain or glob specified, include has no domain or glob + # In this one case the traditional include logic is inverted. Even though an + # include is specified since its only a list of entity IDs its used only to + # expose specific entities excluded by domain or glob. Any entities not + # excluded are then presumed included. Logic is as follows + # - if domain or glob is excluded, pass if entity is included + # - if domain is not excluded, pass if entity not excluded by ID + if self.excluded_domains or self.excluded_entity_globs: + return (not_(or_(*excludes)) | i_entities).self_group() + + # Case 4c - neither include or exclude domain specified + # - Only pass if entity is included. Ignore entity excludes. + return i_entities def states_entity_filter(self) -> ClauseList: """Generate the entity filter query.""" @@ -158,29 +207,32 @@ def _globs_to_like( glob_strs: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: """Translate glob to sql.""" - return or_( + matchers = [ cast(column, Text()).like( encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" ) for glob_str in glob_strs for column in columns - ) + ] + return or_(*matchers) if matchers else or_(False) def _entity_matcher( entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: - return or_( + matchers = [ cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) for column in columns - ) + ] + return or_(*matchers) if matchers else or_(False) def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: - return or_( + matchers = [ cast(column, Text()).like(encoder(f"{domain}.%")) for domain in domains for column in columns - ) + ] + return or_(*matchers) if matchers else or_(False) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 7e8e97eafd4..49796bd0158 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -237,7 +237,9 @@ def _significant_states_stmt( stmt += _ignore_domains_filter if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt += lambda q: q.filter(entity_filter) + stmt = stmt.add_criteria( + lambda q: q.filter(entity_filter), track_on=[filters] + ) stmt += lambda q: q.filter(States.last_updated > start_time) if end_time: @@ -529,7 +531,7 @@ def _get_states_for_all_stmt( stmt += _ignore_domains_filter if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt += lambda q: q.filter(entity_filter) + stmt = stmt.add_criteria(lambda q: q.filter(entity_filter), track_on=[filters]) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index cbc5e86c37e..9dc7af59a38 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -246,15 +246,11 @@ def test_get_significant_states_exclude(hass_history): def test_get_significant_states_exclude_include_entity(hass_history): """Test significant states when excluding domains and include entities. - We should not get back every thermostat and media player test changes. + We should not get back every thermostat change unless its specifically included """ hass = hass_history zero, four, states = record_states(hass) - del states["media_player.test2"] - del states["media_player.test3"] - del states["thermostat.test"] del states["thermostat.test2"] - del states["script.can_cancel_this_one"] config = history.CONFIG_SCHEMA( { @@ -340,14 +336,12 @@ def test_get_significant_states_include(hass_history): def test_get_significant_states_include_exclude_domain(hass_history): """Test if significant states when excluding and including domains. - We should not get back any changes since we include only the - media_player domain but also exclude it. + We should get back all the media_player domain changes + only since the include wins over the exclude but will + exclude everything else. """ hass = hass_history zero, four, states = record_states(hass) - del states["media_player.test"] - del states["media_player.test2"] - del states["media_player.test3"] del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] @@ -372,7 +366,6 @@ def test_get_significant_states_include_exclude_entity(hass_history): """ hass = hass_history zero, four, states = record_states(hass) - del states["media_player.test"] del states["media_player.test2"] del states["media_player.test3"] del states["thermostat.test"] @@ -394,12 +387,12 @@ def test_get_significant_states_include_exclude_entity(hass_history): def test_get_significant_states_include_exclude(hass_history): """Test if significant states when in/excluding domains and entities. - We should only get back changes of the media_player.test2 entity. + We should get back changes of the media_player.test2, media_player.test3, + and thermostat.test. """ hass = hass_history zero, four, states = record_states(hass) del states["media_player.test"] - del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index d33bbd5b8ac..651a00fb0cf 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2037,7 +2037,7 @@ async def test_include_events_domain_glob(hass, hass_client, recorder_mock): _assert_entry(entries[3], name="included", entity_id=entity_id3) -async def test_include_exclude_events(hass, hass_client, recorder_mock): +async def test_include_exclude_events_no_globs(hass, hass_client, recorder_mock): """Test if events are filtered if include and exclude is configured.""" entity_id = "switch.bla" entity_id2 = "sensor.blu" @@ -2082,13 +2082,15 @@ async def test_include_exclude_events(hass, hass_client, recorder_mock): client = await hass_client() entries = await _async_fetch_logbook(client) - assert len(entries) == 4 + assert len(entries) == 6 _assert_entry( entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN ) - _assert_entry(entries[1], name="blu", entity_id=entity_id2, state="10") - _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="20") - _assert_entry(entries[3], name="keep", entity_id=entity_id4, state="10") + _assert_entry(entries[1], name="bla", entity_id=entity_id, state="10") + _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="10") + _assert_entry(entries[3], name="bla", entity_id=entity_id, state="20") + _assert_entry(entries[4], name="blu", entity_id=entity_id2, state="20") + _assert_entry(entries[5], name="keep", entity_id=entity_id4, state="10") async def test_include_exclude_events_with_glob_filters( @@ -2145,13 +2147,15 @@ async def test_include_exclude_events_with_glob_filters( client = await hass_client() entries = await _async_fetch_logbook(client) - assert len(entries) == 4 + assert len(entries) == 6 _assert_entry( entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN ) - _assert_entry(entries[1], name="blu", entity_id=entity_id2, state="10") - _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="20") - _assert_entry(entries[3], name="included", entity_id=entity_id4, state="30") + _assert_entry(entries[1], name="bla", entity_id=entity_id, state="10") + _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="10") + _assert_entry(entries[3], name="bla", entity_id=entity_id, state="20") + _assert_entry(entries[4], name="blu", entity_id=entity_id2, state="20") + _assert_entry(entries[5], name="included", entity_id=entity_id4, state="30") async def test_empty_config(hass, hass_client, recorder_mock): diff --git a/tests/components/recorder/test_filters_with_entityfilter.py b/tests/components/recorder/test_filters_with_entityfilter.py new file mode 100644 index 00000000000..0758d6fdc95 --- /dev/null +++ b/tests/components/recorder/test_filters_with_entityfilter.py @@ -0,0 +1,516 @@ +"""The tests for the recorder filter matching the EntityFilter component.""" +import json + +from sqlalchemy import select +from sqlalchemy.engine.row import Row + +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.filters import ( + Filters, + extract_include_exclude_filter_conf, + sqlalchemy_filter_from_include_exclude_conf, +) +from homeassistant.components.recorder.models import EventData, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entityfilter import ( + CONF_DOMAINS, + CONF_ENTITIES, + CONF_ENTITY_GLOBS, + CONF_EXCLUDE, + CONF_INCLUDE, + convert_include_exclude_filter, +) + +from .common import async_wait_recording_done + + +async def _async_get_states_and_events_with_filter( + hass: HomeAssistant, sqlalchemy_filter: Filters, entity_ids: set[str] +) -> tuple[list[Row], list[Row]]: + """Get states from the database based on a filter.""" + for entity_id in entity_ids: + hass.states.async_set(entity_id, STATE_ON) + hass.bus.async_fire("any", {ATTR_ENTITY_ID: entity_id}) + + await async_wait_recording_done(hass) + + def _get_states_with_session(): + with session_scope(hass=hass) as session: + return session.execute( + select(States.entity_id).filter( + sqlalchemy_filter.states_entity_filter() + ) + ).all() + + filtered_states_entity_ids = { + row[0] + for row in await get_instance(hass).async_add_executor_job( + _get_states_with_session + ) + } + + def _get_events_with_session(): + with session_scope(hass=hass) as session: + return session.execute( + select(EventData.shared_data).filter( + sqlalchemy_filter.events_entity_filter() + ) + ).all() + + filtered_events_entity_ids = set() + for row in await get_instance(hass).async_add_executor_job( + _get_events_with_session + ): + event_data = json.loads(row[0]) + if ATTR_ENTITY_ID not in event_data: + continue + filtered_events_entity_ids.add(json.loads(row[0])[ATTR_ENTITY_ID]) + + return filtered_states_entity_ids, filtered_events_entity_ids + + +async def test_included_and_excluded_simple_case_no_domains(hass, recorder_mock): + """Test filters with included and excluded without domains.""" + filter_accept = {"sensor.kitchen4", "switch.kitchen"} + filter_reject = { + "light.any", + "switch.other", + "cover.any", + "sensor.weather5", + "light.kitchen", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITY_GLOBS: ["sensor.kitchen*"], + CONF_ENTITIES: ["switch.kitchen"], + }, + CONF_EXCLUDE: { + CONF_ENTITY_GLOBS: ["sensor.weather*"], + CONF_ENTITIES: ["light.kitchen"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + assert not entity_filter.explicitly_included("light.any") + assert not entity_filter.explicitly_included("switch.other") + assert entity_filter.explicitly_included("sensor.kitchen4") + assert entity_filter.explicitly_included("switch.kitchen") + + assert not entity_filter.explicitly_excluded("light.any") + assert not entity_filter.explicitly_excluded("switch.other") + assert entity_filter.explicitly_excluded("sensor.weather5") + assert entity_filter.explicitly_excluded("light.kitchen") + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_simple_case_no_globs(hass, recorder_mock): + """Test filters with included and excluded without globs.""" + filter_accept = {"switch.bla", "sensor.blu", "sensor.keep"} + filter_reject = {"sensor.bli"} + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["sensor", "homeassistant"], + CONF_ENTITIES: ["switch.bla"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["switch"], + CONF_ENTITIES: ["sensor.bli"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_simple_case_without_underscores( + hass, recorder_mock +): + """Test filters with included and excluded without underscores.""" + filter_accept = {"light.any", "sensor.kitchen4", "switch.kitchen"} + filter_reject = {"switch.other", "cover.any", "sensor.weather5", "light.kitchen"} + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["light"], + CONF_ENTITY_GLOBS: ["sensor.kitchen*"], + CONF_ENTITIES: ["switch.kitchen"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["cover"], + CONF_ENTITY_GLOBS: ["sensor.weather*"], + CONF_ENTITIES: ["light.kitchen"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + assert not entity_filter.explicitly_included("light.any") + assert not entity_filter.explicitly_included("switch.other") + assert entity_filter.explicitly_included("sensor.kitchen4") + assert entity_filter.explicitly_included("switch.kitchen") + + assert not entity_filter.explicitly_excluded("light.any") + assert not entity_filter.explicitly_excluded("switch.other") + assert entity_filter.explicitly_excluded("sensor.weather5") + assert entity_filter.explicitly_excluded("light.kitchen") + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_simple_case_with_underscores(hass, recorder_mock): + """Test filters with included and excluded with underscores.""" + filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"} + filter_reject = {"switch.other", "cover.any", "sensor.weather_5", "light.kitchen"} + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["light"], + CONF_ENTITY_GLOBS: ["sensor.kitchen_*"], + CONF_ENTITIES: ["switch.kitchen"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["cover"], + CONF_ENTITY_GLOBS: ["sensor.weather_*"], + CONF_ENTITIES: ["light.kitchen"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + assert not entity_filter.explicitly_included("light.any") + assert not entity_filter.explicitly_included("switch.other") + assert entity_filter.explicitly_included("sensor.kitchen_4") + assert entity_filter.explicitly_included("switch.kitchen") + + assert not entity_filter.explicitly_excluded("light.any") + assert not entity_filter.explicitly_excluded("switch.other") + assert entity_filter.explicitly_excluded("sensor.weather_5") + assert entity_filter.explicitly_excluded("light.kitchen") + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_complex_case(hass, recorder_mock): + """Test filters with included and excluded with a complex filter.""" + filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"} + filter_reject = { + "camera.one", + "notify.any", + "automation.update_readme", + "automation.update_utilities_cost", + "binary_sensor.iss", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITIES: ["group.trackers"], + }, + CONF_EXCLUDE: { + CONF_ENTITIES: [ + "automation.update_readme", + "automation.update_utilities_cost", + "binary_sensor.iss", + ], + CONF_DOMAINS: [ + "camera", + "group", + "media_player", + "notify", + "scene", + "sun", + "zone", + ], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_entities_and_excluded_domain(hass, recorder_mock): + """Test filters with included entities and excluded domain.""" + filter_accept = { + "media_player.test", + "media_player.test3", + "thermostat.test", + "zone.home", + "script.can_cancel_this_one", + } + filter_reject = { + "thermostat.test2", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITIES: ["media_player.test", "thermostat.test"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_same_domain_included_excluded(hass, recorder_mock): + """Test filters with the same domain included and excluded.""" + filter_accept = { + "media_player.test", + "media_player.test3", + } + filter_reject = { + "thermostat.test2", + "thermostat.test", + "zone.home", + "script.can_cancel_this_one", + } + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["media_player"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["media_player"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_same_entity_included_excluded(hass, recorder_mock): + """Test filters with the same entity included and excluded.""" + filter_accept = { + "media_player.test", + } + filter_reject = { + "media_player.test3", + "thermostat.test2", + "thermostat.test", + "zone.home", + "script.can_cancel_this_one", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITIES: ["media_player.test"], + }, + CONF_EXCLUDE: { + CONF_ENTITIES: ["media_player.test"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_same_entity_included_excluded_include_domain_wins(hass, recorder_mock): + """Test filters with domain and entities and the include domain wins.""" + filter_accept = { + "media_player.test2", + "media_player.test3", + "thermostat.test", + } + filter_reject = { + "thermostat.test2", + "zone.home", + "script.can_cancel_this_one", + } + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["media_player"], + CONF_ENTITIES: ["thermostat.test"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) From ff687a8248124c3bc804e04d650d36386ed9dd2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 17:51:27 -1000 Subject: [PATCH 019/107] Only create auto comfort entities for BAF devices that support them (#72948) --- homeassistant/components/baf/climate.py | 2 +- homeassistant/components/baf/manifest.json | 2 +- homeassistant/components/baf/number.py | 47 ++++++++++++---------- homeassistant/components/baf/sensor.py | 6 ++- homeassistant/components/baf/switch.py | 7 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 40 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py index f785d18e06f..d4ed4ac4337 100644 --- a/homeassistant/components/baf/climate.py +++ b/homeassistant/components/baf/climate.py @@ -26,7 +26,7 @@ async def async_setup_entry( ) -> None: """Set up BAF fan auto comfort.""" data: BAFData = hass.data[DOMAIN][entry.entry_id] - if data.device.has_fan: + if data.device.has_fan and data.device.has_auto_comfort: async_add_entities( [BAFAutoComfort(data.device, f"{data.device.name} Auto Comfort")] ) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 9dfc35685e3..8143c35410e 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.3.0"], + "requirements": ["aiobafi6==0.5.0"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py index 84358e79669..32a3ea5e693 100644 --- a/homeassistant/components/baf/number.py +++ b/homeassistant/components/baf/number.py @@ -36,27 +36,7 @@ class BAFNumberDescription(NumberEntityDescription, BAFNumberDescriptionMixin): """Class describing BAF sensor entities.""" -FAN_NUMBER_DESCRIPTIONS = ( - BAFNumberDescription( - key="return_to_auto_timeout", - name="Return to Auto Timeout", - min_value=ONE_MIN_SECS, - max_value=HALF_DAY_SECS, - entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, - value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout), - mode=NumberMode.SLIDER, - ), - BAFNumberDescription( - key="motion_sense_timeout", - name="Motion Sense Timeout", - min_value=ONE_MIN_SECS, - max_value=ONE_DAY_SECS, - entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, - value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout), - mode=NumberMode.SLIDER, - ), +AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="comfort_min_speed", name="Auto Comfort Minimum Speed", @@ -86,6 +66,29 @@ FAN_NUMBER_DESCRIPTIONS = ( ), ) +FAN_NUMBER_DESCRIPTIONS = ( + BAFNumberDescription( + key="return_to_auto_timeout", + name="Return to Auto Timeout", + min_value=ONE_MIN_SECS, + max_value=HALF_DAY_SECS, + entity_category=EntityCategory.CONFIG, + unit_of_measurement=TIME_SECONDS, + value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout), + mode=NumberMode.SLIDER, + ), + BAFNumberDescription( + key="motion_sense_timeout", + name="Motion Sense Timeout", + min_value=ONE_MIN_SECS, + max_value=ONE_DAY_SECS, + entity_category=EntityCategory.CONFIG, + unit_of_measurement=TIME_SECONDS, + value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout), + mode=NumberMode.SLIDER, + ), +) + LIGHT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="light_return_to_auto_timeout", @@ -125,6 +128,8 @@ async def async_setup_entry( descriptions.extend(FAN_NUMBER_DESCRIPTIONS) if device.has_light: descriptions.extend(LIGHT_NUMBER_DESCRIPTIONS) + if device.has_auto_comfort: + descriptions.extend(AUTO_COMFORT_NUMBER_DESCRIPTIONS) async_add_entities(BAFNumber(device, description) for description in descriptions) diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py index 0f4239962cf..7b93b22fe2f 100644 --- a/homeassistant/components/baf/sensor.py +++ b/homeassistant/components/baf/sensor.py @@ -39,7 +39,7 @@ class BAFSensorDescription( """Class describing BAF sensor entities.""" -BASE_SENSORS = ( +AUTO_COMFORT_SENSORS = ( BAFSensorDescription( key="temperature", name="Temperature", @@ -103,10 +103,12 @@ async def async_setup_entry( """Set up BAF fan sensors.""" data: BAFData = hass.data[DOMAIN][entry.entry_id] device = data.device - sensors_descriptions = list(BASE_SENSORS) + sensors_descriptions: list[BAFSensorDescription] = [] for description in DEFINED_ONLY_SENSORS: if getattr(device, description.key): sensors_descriptions.append(description) + if device.has_auto_comfort: + sensors_descriptions.extend(AUTO_COMFORT_SENSORS) if device.has_fan: sensors_descriptions.extend(FAN_SENSORS) async_add_entities( diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py index 6cefa0db65d..44671e68458 100644 --- a/homeassistant/components/baf/switch.py +++ b/homeassistant/components/baf/switch.py @@ -48,13 +48,16 @@ BASE_SWITCHES = [ ), ] -FAN_SWITCHES = [ +AUTO_COMFORT_SWITCHES = [ BAFSwitchDescription( key="comfort_heat_assist_enable", name="Auto Comfort Heat Assist", entity_category=EntityCategory.CONFIG, value_fn=lambda device: cast(Optional[bool], device.comfort_heat_assist_enable), ), +] + +FAN_SWITCHES = [ BAFSwitchDescription( key="fan_beep_enable", name="Beep", @@ -120,6 +123,8 @@ async def async_setup_entry( descriptions.extend(FAN_SWITCHES) if device.has_light: descriptions.extend(LIGHT_SWITCHES) + if device.has_auto_comfort: + descriptions.extend(AUTO_COMFORT_SWITCHES) async_add_entities(BAFSwitch(device, description) for description in descriptions) diff --git a/requirements_all.txt b/requirements_all.txt index ad38e5c5fb1..791875978ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -122,7 +122,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.3.0 +aiobafi6==0.5.0 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94b978ece80..4fa7c37963d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.3.0 +aiobafi6==0.5.0 # homeassistant.components.aws aiobotocore==2.1.0 From e0ca5bafda8b1288129a7e5675ece48d9490d14e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Jun 2022 10:04:46 -1000 Subject: [PATCH 020/107] Fix statistics_during_period being incorrectly cached (#72947) --- .../components/recorder/statistics.py | 7 +- tests/components/history/test_init.py | 136 ++++++++++++++++++ 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4bed39fee4a..39fcb954ee9 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -984,7 +984,6 @@ def _reduce_statistics_per_month( def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, - statistic_ids: list[str] | None, metadata_ids: list[int] | None, table: type[Statistics | StatisticsShortTerm], ) -> StatementLambdaElement: @@ -1002,7 +1001,7 @@ def _statistics_during_period_stmt( if end_time is not None: stmt += lambda q: q.filter(table.start < end_time) - if statistic_ids is not None: + if metadata_ids: stmt += lambda q: q.filter(table.metadata_id.in_(metadata_ids)) stmt += lambda q: q.order_by(table.metadata_id, table.start) @@ -1038,9 +1037,7 @@ def statistics_during_period( else: table = Statistics - stmt = _statistics_during_period_stmt( - start_time, end_time, statistic_ids, metadata_ids, table - ) + stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids, table) stats = execute_stmt_lambda_element(session, stmt) if not stats: diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 9dc7af59a38..8c0a80719a8 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,6 +5,7 @@ from http import HTTPStatus import json from unittest.mock import patch, sentinel +from freezegun import freeze_time import pytest from pytest import approx @@ -928,6 +929,141 @@ async def test_statistics_during_period( } +@pytest.mark.parametrize( + "units, attributes, state, value", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), + ], +) +async def test_statistics_during_period_in_the_past( + hass, hass_ws_client, recorder_mock, units, attributes, state, value +): + """Test statistics_during_period in the past.""" + hass.config.set_time_zone("UTC") + now = dt_util.utcnow().replace() + + hass.config.units = units + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + + past = now - timedelta(days=3) + + with freeze_time(past): + hass.states.async_set("sensor.test", state, attributes=attributes) + await async_wait_recording_done(hass) + + sensor_state = hass.states.get("sensor.test") + assert sensor_state.last_updated == past + + stats_top_of_hour = past.replace(minute=0, second=0, microsecond=0) + stats_start = past.replace(minute=55) + do_adhoc_statistics(hass, start=stats_start) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + past = now - timedelta(days=3) + await client.send_json( + { + "id": 3, + "type": "history/statistics_during_period", + "start_time": past.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": stats_start.isoformat(), + "end": (stats_start + timedelta(minutes=5)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + start_of_day = stats_top_of_hour.replace(hour=0, minute=0) + await client.send_json( + { + "id": 4, + "type": "history/statistics_during_period", + "start_time": stats_top_of_hour.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": start_of_day.isoformat(), + "end": (start_of_day + timedelta(days=1)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + await client.send_json( + { + "id": 5, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + async def test_statistics_during_period_bad_start_time( hass, hass_ws_client, recorder_mock ): From 73536c07d7a13e5f7c5a2c5d6c07d8b4ec2c5d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Beamonte?= Date: Fri, 3 Jun 2022 09:27:10 -0400 Subject: [PATCH 021/107] Allow `log` template function to return specified `default` on math domain error (#72960) Fix regression for logarithm template --- homeassistant/helpers/template.py | 14 +++++++------- tests/helpers/test_template.py | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1a8febf5ac2..053beab307e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1369,19 +1369,19 @@ def multiply(value, amount, default=_SENTINEL): def logarithm(value, base=math.e, default=_SENTINEL): """Filter and function to get logarithm of the value with a specific base.""" - try: - value_float = float(value) - except (ValueError, TypeError): - if default is _SENTINEL: - raise_no_default("log", value) - return default try: base_float = float(base) except (ValueError, TypeError): if default is _SENTINEL: raise_no_default("log", base) return default - return math.log(value_float, base_float) + try: + value_float = float(value) + return math.log(value_float, base_float) + except (ValueError, TypeError): + if default is _SENTINEL: + raise_no_default("log", value) + return default def sine(value, default=_SENTINEL): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index ddda17c20ac..a1fd3e73f59 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -447,6 +447,8 @@ def test_logarithm(hass): assert render(hass, "{{ 'no_number' | log(10, default=1) }}") == 1 assert render(hass, "{{ log('no_number', 10, 1) }}") == 1 assert render(hass, "{{ log('no_number', 10, default=1) }}") == 1 + assert render(hass, "{{ log(0, 10, 1) }}") == 1 + assert render(hass, "{{ log(0, 10, default=1) }}") == 1 def test_sine(hass): From 9f8fe7fca6dfee9fca4533c93ec81899190199b6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 3 Jun 2022 13:57:59 +0200 Subject: [PATCH 022/107] Bump pynetgear to 0.10.4 (#72965) bump pynetgear to 0.10.4 --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index ae0824c82a5..f65f5aa6686 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.0"], + "requirements": ["pynetgear==0.10.4"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 791875978ef..454423a6dca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1673,7 +1673,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.0 +pynetgear==0.10.4 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4fa7c37963d..b6426228b42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1131,7 +1131,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.0 +pynetgear==0.10.4 # homeassistant.components.nina pynina==0.1.8 From 65cb82765baaace47b8933d4413b542fff9062f8 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 3 Jun 2022 21:59:10 +0200 Subject: [PATCH 023/107] Bump bimmer_connected to 0.9.4 (#72973) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 75ac3e982e8..cd6daa83705 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.9.3"], + "requirements": ["bimmer_connected==0.9.4"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 454423a6dca..811d63d8e2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -394,7 +394,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.3 +bimmer_connected==0.9.4 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6426228b42..67fc6dfe509 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,7 +309,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.3 +bimmer_connected==0.9.4 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 5c512ad5cbbaf5315ebbfc5601dcd0c96add4630 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 3 Jun 2022 22:05:37 +0200 Subject: [PATCH 024/107] fjaraskupan: Don't filter anything in backend (#72988) --- homeassistant/components/fjaraskupan/__init__.py | 4 ++-- homeassistant/components/fjaraskupan/config_flow.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index ec4528bc079..4c4f19403a6 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -9,7 +9,7 @@ import logging from bleak import BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from fjaraskupan import DEVICE_NAME, Device, State, device_filter +from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -90,7 +90,7 @@ class EntryState: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"Pattern": DEVICE_NAME, "DuplicateData": True}) + scanner = BleakScanner(filters={"DuplicateData": True}) state = EntryState(scanner, {}) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index 3af34c0eef6..ffac366500b 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -7,7 +7,7 @@ import async_timeout from bleak import BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from fjaraskupan import DEVICE_NAME, device_filter +from fjaraskupan import device_filter from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow @@ -28,7 +28,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: async with BleakScanner( detection_callback=detection, - filters={"Pattern": DEVICE_NAME, "DuplicateData": True}, + filters={"DuplicateData": True}, ): try: async with async_timeout.timeout(CONST_WAIT_TIME): From 6a3b74adf64a848ee20fc1ad2d0c148676792a71 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 3 Jun 2022 11:53:23 -0500 Subject: [PATCH 025/107] Check ISY994 climate for unknown humidity value on Z-Wave Thermostat (#72990) Check ISY994 climate for unknown humidity on Z-Wave Thermostat Update to #72670 to compare the property value and not the parent object. Should actually fix #72628 --- homeassistant/components/isy994/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index d68395f14da..9f4c52258a7 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -117,7 +117,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): """Return the current humidity.""" if not (humidity := self._node.aux_properties.get(PROP_HUMIDITY)): return None - if humidity == ISY_VALUE_UNKNOWN: + if humidity.value == ISY_VALUE_UNKNOWN: return None return int(humidity.value) From 085eee88c97426fd989260f5845f380c78899c81 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 3 Jun 2022 16:33:12 -0700 Subject: [PATCH 026/107] Fix google calendar bug where expired tokens are not refreshed (#72994) --- homeassistant/components/google/api.py | 7 +++++-- tests/components/google/conftest.py | 6 ++++-- tests/components/google/test_config_flow.py | 14 ++++++++------ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 4bb9de5d581..a4cda1ff41a 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import datetime import logging -import time from typing import Any, cast import aiohttp @@ -50,12 +49,16 @@ class DeviceAuth(AuthImplementation): async def async_resolve_external_data(self, external_data: Any) -> dict: """Resolve a Google API Credentials object to Home Assistant token.""" creds: Credentials = external_data[DEVICE_AUTH_CREDS] + delta = creds.token_expiry.replace(tzinfo=datetime.timezone.utc) - dt.utcnow() + _LOGGER.debug( + "Token expires at %s (in %s)", creds.token_expiry, delta.total_seconds() + ) return { "access_token": creds.access_token, "refresh_token": creds.refresh_token, "scope": " ".join(creds.scopes), "token_type": "Bearer", - "expires_in": creds.token_expiry.timestamp() - time.time(), + "expires_in": delta.total_seconds(), } diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index b5566450913..68176493445 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -16,7 +16,6 @@ from homeassistant.components.google import CONF_TRACK_NEW, DOMAIN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -136,7 +135,10 @@ def token_scopes() -> list[str]: @pytest.fixture def token_expiry() -> datetime.datetime: """Expiration time for credentials used in the test.""" - return utcnow() + datetime.timedelta(days=7) + # OAuth library returns an offset-naive timestamp + return datetime.datetime.fromtimestamp( + datetime.datetime.utcnow().timestamp() + ) + datetime.timedelta(hours=1) @pytest.fixture diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 8ac017fcba4..a346b02e6c2 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import Mock, patch from aiohttp.client_exceptions import ClientError +from freezegun.api import FrozenDateTimeFactory from oauth2client.client import ( FlowExchangeError, OAuth2Credentials, @@ -94,11 +95,13 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() +@pytest.mark.freeze_time("2022-06-03 15:19:59-00:00") async def test_full_flow_yaml_creds( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, component_setup: ComponentSetup, + freezer: FrozenDateTimeFactory, ) -> None: """Test successful creds setup.""" assert await component_setup() @@ -115,8 +118,8 @@ async def test_full_flow_yaml_creds( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: # Run one tick to invoke the credential exchange check - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + freezer.tick(CODE_CHECK_ALARM_TIMEDELTA) + await fire_alarm(hass, datetime.datetime.utcnow()) await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] @@ -127,12 +130,11 @@ async def test_full_flow_yaml_creds( assert "data" in result data = result["data"] assert "token" in data - assert 0 < data["token"]["expires_in"] < 8 * 86400 assert ( - datetime.datetime.now().timestamp() - <= data["token"]["expires_at"] - < (datetime.datetime.now() + datetime.timedelta(days=8)).timestamp() + data["token"]["expires_in"] + == 60 * 60 - CODE_CHECK_ALARM_TIMEDELTA.total_seconds() ) + assert data["token"]["expires_at"] == 1654273199.0 data["token"].pop("expires_at") data["token"].pop("expires_in") assert data == { From f3136c811c41e94f1614331d2208f562292dc90f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 3 Jun 2022 17:03:21 -0500 Subject: [PATCH 027/107] Provide Sonos media position if duration not available (#73001) --- homeassistant/components/sonos/media.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index e3d8f043d4b..9608356ba64 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -205,13 +205,15 @@ class SonosMedia: self, position_info: dict[str, int], force_update: bool = False ) -> None: """Update state when playing music tracks.""" - if (duration := position_info.get(DURATION_SECONDS)) == 0: + duration = position_info.get(DURATION_SECONDS) + current_position = position_info.get(POSITION_SECONDS) + + if not (duration or current_position): self.clear_position() return should_update = force_update self.duration = duration - current_position = position_info.get(POSITION_SECONDS) # player started reporting position? if current_position is not None and self.position is None: From 10fb3035d6a59fcdb1d6076223779b183f9048b8 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 4 Jun 2022 07:52:39 +0200 Subject: [PATCH 028/107] Bump pypck to 0.7.15 (#73009) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 412ef74e3b8..eea72a0e508 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,7 +3,7 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.14"], + "requirements": ["pypck==0.7.15"], "codeowners": ["@alengwenus"], "iot_class": "local_push", "loggers": ["pypck"] diff --git a/requirements_all.txt b/requirements_all.txt index 811d63d8e2b..4c848eef637 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1735,7 +1735,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.14 +pypck==0.7.15 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67fc6dfe509..3efb7dd9878 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1178,7 +1178,7 @@ pyowm==3.2.0 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.14 +pypck==0.7.15 # homeassistant.components.plaato pyplaato==0.0.18 From 373634cc50d67fb47b444deaab98939f55d0ab59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 09:54:10 -1000 Subject: [PATCH 029/107] Fix missing historical context data in logbook for MySQL and PostgreSQL (#73011) --- homeassistant/components/recorder/filters.py | 29 ++++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 835496c2d6e..90851e9f251 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Iterable import json from typing import Any -from sqlalchemy import JSON, Column, Text, cast, not_, or_ +from sqlalchemy import Column, Text, cast, not_, or_ from sqlalchemy.sql.elements import ClauseList from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE @@ -16,6 +16,7 @@ from .models import ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT, States DOMAIN = "history" HISTORY_FILTERS = "history_filters" +JSON_NULL = json.dumps(None) GLOB_TO_SQL_CHARS = { ord("*"): "%", @@ -196,7 +197,17 @@ class Filters: """Generate the entity filter query.""" _encoder = json.dumps return or_( - (ENTITY_ID_IN_EVENT == JSON.NULL) & (OLD_ENTITY_ID_IN_EVENT == JSON.NULL), + # sqlalchemy's SQLite json implementation always + # wraps everything with JSON_QUOTE so it resolves to 'null' + # when its empty + # + # For MySQL and PostgreSQL it will resolve to a literal + # NULL when its empty + # + ((ENTITY_ID_IN_EVENT == JSON_NULL) | ENTITY_ID_IN_EVENT.is_(None)) + & ( + (OLD_ENTITY_ID_IN_EVENT == JSON_NULL) | OLD_ENTITY_ID_IN_EVENT.is_(None) + ), self._generate_filter_for_columns( (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT), _encoder ).self_group(), @@ -208,8 +219,11 @@ def _globs_to_like( ) -> ClauseList: """Translate glob to sql.""" matchers = [ - cast(column, Text()).like( - encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" + ( + column.is_not(None) + & cast(column, Text()).like( + encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" + ) ) for glob_str in glob_strs for column in columns @@ -221,7 +235,10 @@ def _entity_matcher( entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) + ( + column.is_not(None) + & cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) + ) for column in columns ] return or_(*matchers) if matchers else or_(False) @@ -231,7 +248,7 @@ def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - cast(column, Text()).like(encoder(f"{domain}.%")) + (column.is_not(None) & cast(column, Text()).like(encoder(f"{domain}.%"))) for domain in domains for column in columns ] From b401f165830b31eb7c17516ab8407ffdfad7fc41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 09:47:30 -1000 Subject: [PATCH 030/107] Fix history stats not comparing all times in UTC (#73040) --- homeassistant/components/history_stats/data.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 5466498fc32..3b17c715c97 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -67,9 +67,10 @@ class HistoryStats: current_period_end_timestamp = floored_timestamp(current_period_end) previous_period_start_timestamp = floored_timestamp(previous_period_start) previous_period_end_timestamp = floored_timestamp(previous_period_end) - now_timestamp = floored_timestamp(datetime.datetime.now()) + utc_now = dt_util.utcnow() + now_timestamp = floored_timestamp(utc_now) - if now_timestamp < current_period_start_timestamp: + if current_period_start > utc_now: # History cannot tell the future self._history_current_period = [] self._previous_run_before_start = True From d9a41d10ffbc2a9f7d0aca8673d81e143993b438 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Jun 2022 13:03:51 -0700 Subject: [PATCH 031/107] Bumped version to 2022.6.2 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 08ab335a33b..23ada7591ee 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index aeafbd53565..08bd61e7382 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.1 +version = 2022.6.2 url = https://www.home-assistant.io/ [options] From 39c6a57c3579ca21d47415ea8c4164d7e95a452b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 6 Jun 2022 03:39:42 +0200 Subject: [PATCH 032/107] Throttle multiple requests to the velux gateway (#72974) --- homeassistant/components/velux/cover.py | 11 ++++++----- homeassistant/components/velux/light.py | 2 ++ homeassistant/components/velux/scene.py | 2 ++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index f4f449509a0..26cccfce6ce 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -17,6 +17,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_VELUX, VeluxEntity +PARALLEL_UPDATES = 1 + async def async_setup_platform( hass: HomeAssistant, @@ -97,12 +99,11 @@ class VeluxCover(VeluxEntity, CoverEntity): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - if ATTR_POSITION in kwargs: - position_percent = 100 - kwargs[ATTR_POSITION] + position_percent = 100 - kwargs[ATTR_POSITION] - await self.node.set_position( - Position(position_percent=position_percent), wait_for_completion=False - ) + await self.node.set_position( + Position(position_percent=position_percent), wait_for_completion=False + ) async def async_stop_cover(self, **kwargs): """Stop the cover.""" diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index e6ca8ea5c2b..f8a52fc05c1 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -10,6 +10,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_VELUX, VeluxEntity +PARALLEL_UPDATES = 1 + async def async_setup_platform( hass: HomeAssistant, diff --git a/homeassistant/components/velux/scene.py b/homeassistant/components/velux/scene.py index 324bae027fc..20f94c74f0b 100644 --- a/homeassistant/components/velux/scene.py +++ b/homeassistant/components/velux/scene.py @@ -10,6 +10,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import _LOGGER, DATA_VELUX +PARALLEL_UPDATES = 1 + async def async_setup_platform( hass: HomeAssistant, From b50e3d5ce7838abe72799087f69756d9a2e0cbfc Mon Sep 17 00:00:00 2001 From: hesselonline Date: Mon, 6 Jun 2022 03:31:09 +0200 Subject: [PATCH 033/107] Bump wallbox to 0.4.9 (#72978) --- .../components/wallbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wallbox/__init__.py | 39 ++++++++++++---- tests/components/wallbox/test_config_flow.py | 44 +++++-------------- tests/components/wallbox/test_init.py | 18 ++------ tests/components/wallbox/test_lock.py | 26 ++--------- tests/components/wallbox/test_number.py | 27 ++---------- tests/components/wallbox/test_switch.py | 29 +++--------- 9 files changed, 61 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index 2a4978b1cc1..914adda980a 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -3,7 +3,7 @@ "name": "Wallbox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wallbox", - "requirements": ["wallbox==0.4.4"], + "requirements": ["wallbox==0.4.9"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/requirements_all.txt b/requirements_all.txt index 4c848eef637..adf129b1250 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2418,7 +2418,7 @@ vultr==0.1.2 wakeonlan==2.0.1 # homeassistant.components.wallbox -wallbox==0.4.4 +wallbox==0.4.9 # homeassistant.components.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3efb7dd9878..2d9b0100da4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1591,7 +1591,7 @@ vultr==0.1.2 wakeonlan==2.0.1 # homeassistant.components.wallbox -wallbox==0.4.4 +wallbox==0.4.9 # homeassistant.components.folder_watcher watchdog==2.1.8 diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 2b35bb76b2f..8c979c42ebe 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -26,7 +26,7 @@ from homeassistant.components.wallbox.const import ( from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import ERROR, JWT, STATUS, TTL, USER_ID +from .const import ERROR, STATUS, TTL, USER_ID from tests.common import MockConfigEntry @@ -54,11 +54,32 @@ test_response = json.loads( authorisation_response = json.loads( json.dumps( { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, + "data": { + "attributes": { + "token": "fakekeyhere", + USER_ID: 12345, + TTL: 145656758, + ERROR: "false", + STATUS: 200, + } + } + } + ) +) + + +authorisation_response_unauthorised = json.loads( + json.dumps( + { + "data": { + "attributes": { + "token": "fakekeyhere", + USER_ID: 12345, + TTL: 145656758, + ERROR: "false", + STATUS: 404, + } + } } ) ) @@ -81,7 +102,7 @@ async def setup_integration(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.OK, ) @@ -107,7 +128,7 @@ async def setup_integration_connection_error(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.FORBIDDEN, ) @@ -133,7 +154,7 @@ async def setup_integration_read_only(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.OK, ) diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index a3e6a724eef..68f70878592 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -18,8 +18,12 @@ from homeassistant.components.wallbox.const import ( ) from homeassistant.core import HomeAssistant -from tests.components.wallbox import entry, setup_integration -from tests.components.wallbox.const import ERROR, JWT, STATUS, TTL, USER_ID +from tests.components.wallbox import ( + authorisation_response, + authorisation_response_unauthorised, + entry, + setup_integration, +) test_response = json.loads( json.dumps( @@ -34,30 +38,6 @@ test_response = json.loads( ) ) -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) - -authorisation_response_unauthorised = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 404, - } - ) -) - async def test_show_set_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" @@ -77,7 +57,7 @@ async def test_form_cannot_authenticate(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.FORBIDDEN, ) @@ -107,7 +87,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response_unauthorised, status_code=HTTPStatus.NOT_FOUND, ) @@ -137,7 +117,7 @@ async def test_form_validate_input(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.OK, ) @@ -166,8 +146,8 @@ async def test_form_reauth(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", - text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + "https://user-api.wall-box.com/users/signin", + json=authorisation_response, status_code=200, ) mock_request.get( @@ -206,7 +186,7 @@ async def test_form_reauth_invalid(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', status_code=200, ) diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index b8862531a82..5080bab87ea 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -11,24 +11,12 @@ from . import test_response from tests.components.wallbox import ( DOMAIN, + authorisation_response, entry, setup_integration, setup_integration_connection_error, setup_integration_read_only, ) -from tests.components.wallbox.const import ERROR, JWT, STATUS, TTL, USER_ID - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) async def test_wallbox_setup_unload_entry(hass: HomeAssistant) -> None: @@ -59,7 +47,7 @@ async def test_wallbox_refresh_failed_invalid_auth(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=403, ) @@ -85,7 +73,7 @@ async def test_wallbox_refresh_failed_connection_error(hass: HomeAssistant) -> N with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py index d8b2fcf182c..fbcb07b0e90 100644 --- a/tests/components/wallbox/test_lock.py +++ b/tests/components/wallbox/test_lock.py @@ -10,30 +10,12 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from tests.components.wallbox import ( + authorisation_response, entry, setup_integration, setup_integration_read_only, ) -from tests.components.wallbox.const import ( - ERROR, - JWT, - MOCK_LOCK_ENTITY_ID, - STATUS, - TTL, - USER_ID, -) - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) +from tests.components.wallbox.const import MOCK_LOCK_ENTITY_ID async def test_wallbox_lock_class(hass: HomeAssistant) -> None: @@ -47,7 +29,7 @@ async def test_wallbox_lock_class(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -85,7 +67,7 @@ async def test_wallbox_lock_class_connection_error(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index 5c024d0f4ac..c8e8b29f28b 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -9,27 +9,8 @@ from homeassistant.components.wallbox import CHARGER_MAX_CHARGING_CURRENT_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from tests.components.wallbox import entry, setup_integration -from tests.components.wallbox.const import ( - ERROR, - JWT, - MOCK_NUMBER_ENTITY_ID, - STATUS, - TTL, - USER_ID, -) - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) +from tests.components.wallbox import authorisation_response, entry, setup_integration +from tests.components.wallbox.const import MOCK_NUMBER_ENTITY_ID async def test_wallbox_number_class(hass: HomeAssistant) -> None: @@ -39,7 +20,7 @@ async def test_wallbox_number_class(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -68,7 +49,7 @@ async def test_wallbox_number_class_connection_error(hass: HomeAssistant) -> Non with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) diff --git a/tests/components/wallbox/test_switch.py b/tests/components/wallbox/test_switch.py index 6ade320319a..c57fee0353f 100644 --- a/tests/components/wallbox/test_switch.py +++ b/tests/components/wallbox/test_switch.py @@ -10,27 +10,8 @@ from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from tests.components.wallbox import entry, setup_integration -from tests.components.wallbox.const import ( - ERROR, - JWT, - MOCK_SWITCH_ENTITY_ID, - STATUS, - TTL, - USER_ID, -) - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) +from tests.components.wallbox import authorisation_response, entry, setup_integration +from tests.components.wallbox.const import MOCK_SWITCH_ENTITY_ID async def test_wallbox_switch_class(hass: HomeAssistant) -> None: @@ -44,7 +25,7 @@ async def test_wallbox_switch_class(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -82,7 +63,7 @@ async def test_wallbox_switch_class_connection_error(hass: HomeAssistant) -> Non with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -121,7 +102,7 @@ async def test_wallbox_switch_class_authentication_error(hass: HomeAssistant) -> with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) From bd8424d184ddf2726f851f0ce1cf01614f03fddf Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 6 Jun 2022 13:20:16 +0200 Subject: [PATCH 034/107] Fix fibaro cover detection (#72986) --- homeassistant/components/fibaro/cover.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index fc6d0a67d3c..e898cd7ead9 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -46,6 +46,8 @@ class FibaroCover(FibaroDevice, CoverEntity): self._attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE ) + if "stop" in self.fibaro_device.actions: + self._attr_supported_features |= CoverEntityFeature.STOP @staticmethod def bound(position): From 854b0dbb2d8260c90eb2855578520a1adf791d9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 18:13:31 -1000 Subject: [PATCH 035/107] Reduce branching in generated lambda_stmts (#73042) --- homeassistant/components/recorder/history.py | 33 ++++-- .../components/recorder/statistics.py | 103 ++++++++++++------ tests/components/recorder/test_statistics.py | 62 +++++++++++ 3 files changed, 153 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 49796bd0158..5dd5c0d3040 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -15,6 +15,7 @@ from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Subquery from homeassistant.components import recorder from homeassistant.components.websocket_api.const import ( @@ -485,6 +486,25 @@ def _get_states_for_entites_stmt( return stmt +def _generate_most_recent_states_by_date( + run_start: datetime, + utc_point_in_time: datetime, +) -> Subquery: + """Generate the sub query for the most recent states by data.""" + return ( + select( + States.entity_id.label("max_entity_id"), + func.max(States.last_updated).label("max_last_updated"), + ) + .filter( + (States.last_updated >= run_start) + & (States.last_updated < utc_point_in_time) + ) + .group_by(States.entity_id) + .subquery() + ) + + def _get_states_for_all_stmt( schema_version: int, run_start: datetime, @@ -500,17 +520,8 @@ def _get_states_for_all_stmt( # query, then filter out unwanted domains as well as applying the custom filter. # This filtering can't be done in the inner query because the domain column is # not indexed and we can't control what's in the custom filter. - most_recent_states_by_date = ( - select( - States.entity_id.label("max_entity_id"), - func.max(States.last_updated).label("max_last_updated"), - ) - .filter( - (States.last_updated >= run_start) - & (States.last_updated < utc_point_in_time) - ) - .group_by(States.entity_id) - .subquery() + most_recent_states_by_date = _generate_most_recent_states_by_date( + run_start, utc_point_in_time ) stmt += lambda q: q.where( States.state_id diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 39fcb954ee9..012b34ec0ef 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -20,6 +20,7 @@ from sqlalchemy.exc import SQLAlchemyError, StatementError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal_column, true from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Subquery import voluptuous as vol from homeassistant.const import ( @@ -484,14 +485,13 @@ def _compile_hourly_statistics_summary_mean_stmt( start_time: datetime, end_time: datetime ) -> StatementLambdaElement: """Generate the summary mean statement for hourly statistics.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN)) - stmt += ( - lambda q: q.filter(StatisticsShortTerm.start >= start_time) + return lambda_stmt( + lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN) + .filter(StatisticsShortTerm.start >= start_time) .filter(StatisticsShortTerm.start < end_time) .group_by(StatisticsShortTerm.metadata_id) .order_by(StatisticsShortTerm.metadata_id) ) - return stmt def compile_hourly_statistics( @@ -985,26 +985,43 @@ def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, - table: type[Statistics | StatisticsShortTerm], ) -> StatementLambdaElement: """Prepare a database query for statistics during a given period. This prepares a lambda_stmt query, so we don't insert the parameters yet. """ - if table == StatisticsShortTerm: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) - else: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS)) - - stmt += lambda q: q.filter(table.start >= start_time) - + stmt = lambda_stmt( + lambda: select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) + ) if end_time is not None: - stmt += lambda q: q.filter(table.start < end_time) - + stmt += lambda q: q.filter(Statistics.start < end_time) if metadata_ids: - stmt += lambda q: q.filter(table.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.filter(Statistics.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) + return stmt - stmt += lambda q: q.order_by(table.metadata_id, table.start) + +def _statistics_during_period_stmt_short_term( + start_time: datetime, + end_time: datetime | None, + metadata_ids: list[int] | None, +) -> StatementLambdaElement: + """Prepare a database query for short term statistics during a given period. + + This prepares a lambda_stmt query, so we don't insert the parameters yet. + """ + stmt = lambda_stmt( + lambda: select(*QUERY_STATISTICS_SHORT_TERM).filter( + StatisticsShortTerm.start >= start_time + ) + ) + if end_time is not None: + stmt += lambda q: q.filter(StatisticsShortTerm.start < end_time) + if metadata_ids: + stmt += lambda q: q.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.order_by( + StatisticsShortTerm.metadata_id, StatisticsShortTerm.start + ) return stmt @@ -1034,10 +1051,12 @@ def statistics_during_period( if period == "5minute": table = StatisticsShortTerm + stmt = _statistics_during_period_stmt_short_term( + start_time, end_time, metadata_ids + ) else: table = Statistics - - stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids, table) + stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids) stats = execute_stmt_lambda_element(session, stmt) if not stats: @@ -1069,19 +1088,27 @@ def statistics_during_period( def _get_last_statistics_stmt( metadata_id: int, number_of_stats: int, - table: type[Statistics | StatisticsShortTerm], ) -> StatementLambdaElement: """Generate a statement for number_of_stats statistics for a given statistic_id.""" - if table == StatisticsShortTerm: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) - else: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS)) - stmt += ( - lambda q: q.filter_by(metadata_id=metadata_id) - .order_by(table.metadata_id, table.start.desc()) + return lambda_stmt( + lambda: select(*QUERY_STATISTICS) + .filter_by(metadata_id=metadata_id) + .order_by(Statistics.metadata_id, Statistics.start.desc()) + .limit(number_of_stats) + ) + + +def _get_last_statistics_short_term_stmt( + metadata_id: int, + number_of_stats: int, +) -> StatementLambdaElement: + """Generate a statement for number_of_stats short term statistics for a given statistic_id.""" + return lambda_stmt( + lambda: select(*QUERY_STATISTICS_SHORT_TERM) + .filter_by(metadata_id=metadata_id) + .order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc()) .limit(number_of_stats) ) - return stmt def _get_last_statistics( @@ -1099,7 +1126,10 @@ def _get_last_statistics( if not metadata: return {} metadata_id = metadata[statistic_id][0] - stmt = _get_last_statistics_stmt(metadata_id, number_of_stats, table) + if table == Statistics: + stmt = _get_last_statistics_stmt(metadata_id, number_of_stats) + else: + stmt = _get_last_statistics_short_term_stmt(metadata_id, number_of_stats) stats = execute_stmt_lambda_element(session, stmt) if not stats: @@ -1136,12 +1166,9 @@ def get_last_short_term_statistics( ) -def _latest_short_term_statistics_stmt( - metadata_ids: list[int], -) -> StatementLambdaElement: - """Create the statement for finding the latest short term stat rows.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) - most_recent_statistic_row = ( +def _generate_most_recent_statistic_row(metadata_ids: list[int]) -> Subquery: + """Generate the subquery to find the most recent statistic row.""" + return ( select( StatisticsShortTerm.metadata_id, func.max(StatisticsShortTerm.start).label("start_max"), @@ -1149,6 +1176,14 @@ def _latest_short_term_statistics_stmt( .where(StatisticsShortTerm.metadata_id.in_(metadata_ids)) .group_by(StatisticsShortTerm.metadata_id) ).subquery() + + +def _latest_short_term_statistics_stmt( + metadata_ids: list[int], +) -> StatementLambdaElement: + """Create the statement for finding the latest short term stat rows.""" + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) + most_recent_statistic_row = _generate_most_recent_statistic_row(metadata_ids) stmt += lambda s: s.join( most_recent_statistic_row, ( diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 882f00d2940..97e64716f49 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -100,6 +100,15 @@ def test_compile_hourly_statistics(hass_recorder): stats = statistics_during_period(hass, zero, period="5minute") assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + # Test statistics_during_period with a far future start and end date + future = dt_util.as_utc(dt_util.parse_datetime("2221-11-01 00:00:00")) + stats = statistics_during_period(hass, future, end_time=future, period="5minute") + assert stats == {} + + # Test statistics_during_period with a far future end date + stats = statistics_during_period(hass, zero, end_time=future, period="5minute") + assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + stats = statistics_during_period( hass, zero, statistic_ids=["sensor.test2"], period="5minute" ) @@ -814,6 +823,59 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): ] } + stats = statistics_during_period( + hass, + start_time=zero, + statistic_ids=["not", "the", "same", "test:total_energy_import"], + period="month", + ) + sep_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + sep_end = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + oct_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + oct_end = dt_util.as_utc(dt_util.parse_datetime("2021-11-01 00:00:00")) + assert stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": sep_start.isoformat(), + "end": sep_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + { + "statistic_id": "test:total_energy_import", + "start": oct_start.isoformat(), + "end": oct_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(3.0), + "sum": approx(5.0), + }, + ] + } + + # Use 5minute to ensure table switch works + stats = statistics_during_period( + hass, + start_time=zero, + statistic_ids=["test:total_energy_import", "with_other"], + period="5minute", + ) + assert stats == {} + + # Ensure future date has not data + future = dt_util.as_utc(dt_util.parse_datetime("2221-11-01 00:00:00")) + stats = statistics_during_period( + hass, start_time=future, end_time=future, period="month" + ) + assert stats == {} + dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) From 06a2fe94d371ae1ff4e053c2ef47135cf9ac3f7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 18:14:47 -1000 Subject: [PATCH 036/107] Send an empty logbook response when all requested entity_ids are filtered away (#73046) --- .../components/logbook/websocket_api.py | 38 ++++- .../components/logbook/test_websocket_api.py | 157 +++++++++++++++++- 2 files changed, 184 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 1af44440803..b27ae65b70c 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -67,6 +67,23 @@ async def _async_wait_for_recorder_sync(hass: HomeAssistant) -> None: ) +@callback +def _async_send_empty_response( + connection: ActiveConnection, msg_id: int, start_time: dt, end_time: dt | None +) -> None: + """Send an empty response. + + The current case for this is when they ask for entity_ids + that will all be filtered away because they have UOMs or + state_class. + """ + connection.send_result(msg_id) + stream_end_time = end_time or dt_util.utcnow() + empty_stream_message = _generate_stream_message([], start_time, stream_end_time) + empty_response = messages.event_message(msg_id, empty_stream_message) + connection.send_message(JSON_DUMP(empty_response)) + + async def _async_send_historical_events( hass: HomeAssistant, connection: ActiveConnection, @@ -171,6 +188,17 @@ async def _async_get_ws_stream_events( ) +def _generate_stream_message( + events: list[dict[str, Any]], start_day: dt, end_day: dt +) -> dict[str, Any]: + """Generate a logbook stream message response.""" + return { + "events": events, + "start_time": dt_util.utc_to_timestamp(start_day), + "end_time": dt_util.utc_to_timestamp(end_day), + } + + def _ws_stream_get_events( msg_id: int, start_day: dt, @@ -184,11 +212,7 @@ def _ws_stream_get_events( last_time = None if events: last_time = dt_util.utc_from_timestamp(events[-1]["when"]) - message = { - "events": events, - "start_time": dt_util.utc_to_timestamp(start_day), - "end_time": dt_util.utc_to_timestamp(end_day), - } + message = _generate_stream_message(events, start_day, end_day) if partial: # This is a hint to consumers of the api that # we are about to send a another block of historical @@ -275,6 +299,10 @@ async def ws_event_stream( entity_ids = msg.get("entity_ids") if entity_ids: entity_ids = async_filter_entities(hass, entity_ids) + if not entity_ids: + _async_send_empty_response(connection, msg_id, start_time, end_time) + return + event_types = async_determine_event_types(hass, entity_ids, device_ids) event_processor = EventProcessor( hass, diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 2dd08ec44ce..2623a5b17d5 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2102,11 +2102,17 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie ] ) await async_wait_recording_done(hass) + entity_ids = ("sensor.uom", "sensor.uom_two") + + def _cycle_entities(): + for entity_id in entity_ids: + for state in ("1", "2", "3"): + hass.states.async_set( + entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} + ) init_count = sum(hass.bus.async_listeners().values()) - hass.states.async_set("sensor.uom", "1", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "2", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "3", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + _cycle_entities() await async_wait_recording_done(hass) websocket_client = await hass_ws_client() @@ -2124,9 +2130,61 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie assert msg["type"] == TYPE_RESULT assert msg["success"] - hass.states.async_set("sensor.uom", "1", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "2", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "3", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + _cycle_entities() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + + await websocket_client.close() + await hass.async_block_till_done() + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_all_entities_have_uom_multiple( + hass, recorder_mock, hass_ws_client +): + """Test logbook stream with specific request for multiple entities that are always filtered.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + entity_ids = ("sensor.uom", "sensor.uom_two") + + def _cycle_entities(): + for entity_id in entity_ids: + for state in ("1", "2", "3"): + hass.states.async_set( + entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} + ) + + init_count = sum(hass.bus.async_listeners().values()) + _cycle_entities() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": [*entity_ids], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + _cycle_entities() msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 @@ -2138,3 +2196,90 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie # Check our listener got unsubscribed assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_entities_some_have_uom_multiple( + hass, recorder_mock, hass_ws_client +): + """Test logbook stream with uom filtered entities and non-fitlered entities.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + filtered_entity_ids = ("sensor.uom", "sensor.uom_two") + non_filtered_entity_ids = ("sensor.keep", "sensor.keep_two") + + def _cycle_entities(): + for entity_id in filtered_entity_ids: + for state in ("1", "2", "3"): + hass.states.async_set( + entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} + ) + for entity_id in non_filtered_entity_ids: + for state in (STATE_ON, STATE_OFF): + hass.states.async_set(entity_id, state) + + init_count = sum(hass.bus.async_listeners().values()) + _cycle_entities() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": [*filtered_entity_ids, *non_filtered_entity_ids], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + _cycle_entities() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["partial"] is True + assert msg["event"]["events"] == [ + {"entity_id": "sensor.keep", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, + ] + + _cycle_entities() + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + assert "partial" not in msg["event"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + {"entity_id": "sensor.keep", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, + ] + assert "partial" not in msg["event"] + + await websocket_client.close() + await hass.async_block_till_done() + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count From 7bdada789854f4f8446e4865aed22a15e031618a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 16:34:04 -1000 Subject: [PATCH 037/107] Bump aiolookup to 0.1.1 (#73048) --- homeassistant/components/lookin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lookin/manifest.json b/homeassistant/components/lookin/manifest.json index 7cf70540372..b58eb254f8f 100644 --- a/homeassistant/components/lookin/manifest.json +++ b/homeassistant/components/lookin/manifest.json @@ -3,7 +3,7 @@ "name": "LOOKin", "documentation": "https://www.home-assistant.io/integrations/lookin/", "codeowners": ["@ANMalko", "@bdraco"], - "requirements": ["aiolookin==0.1.0"], + "requirements": ["aiolookin==0.1.1"], "zeroconf": ["_lookin._tcp.local."], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index adf129b1250..dc272f3d949 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,7 +187,7 @@ aiolifx==0.7.1 aiolifx_effects==0.2.2 # homeassistant.components.lookin -aiolookin==0.1.0 +aiolookin==0.1.1 # homeassistant.components.lyric aiolyric==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d9b0100da4..ce2396ec6ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -159,7 +159,7 @@ aiohue==4.4.1 aiokafka==0.6.0 # homeassistant.components.lookin -aiolookin==0.1.0 +aiolookin==0.1.1 # homeassistant.components.lyric aiolyric==1.0.8 From 1e59ce19f5e1cd1d87f1c7a3bd201cf5c83173c6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Jun 2022 09:13:54 -0600 Subject: [PATCH 038/107] Bump simplisafe-python to 2022.06.0 (#73054) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index f62da735f92..4da8f09eff8 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2022.05.2"], + "requirements": ["simplisafe-python==2022.06.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index dc272f3d949..baac079e585 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.05.2 +simplisafe-python==2022.06.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce2396ec6ab..20fd6b229b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1425,7 +1425,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.05.2 +simplisafe-python==2022.06.0 # homeassistant.components.slack slackclient==2.5.0 From ca05cde6ba416592b5bd5cdb0fc2ce29066579e7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Jun 2022 19:33:27 -0600 Subject: [PATCH 039/107] Fix unhandled exception when RainMachine coordinator data doesn't exist (#73055) --- homeassistant/components/rainmachine/binary_sensor.py | 7 +++++-- homeassistant/components/rainmachine/sensor.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 1818222a8f4..7a13515db3b 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -139,8 +139,11 @@ async def async_setup_entry( entry, coordinator, controller, description ) for description in BINARY_SENSOR_DESCRIPTIONS - if (coordinator := coordinators[description.api_category]) is not None - and key_exists(coordinator.data, description.data_key) + if ( + (coordinator := coordinators[description.api_category]) is not None + and coordinator.data + and key_exists(coordinator.data, description.data_key) + ) ] ) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 522c57cf7a2..cc37189aa49 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -133,8 +133,11 @@ async def async_setup_entry( entry, coordinator, controller, description ) for description in SENSOR_DESCRIPTIONS - if (coordinator := coordinators[description.api_category]) is not None - and key_exists(coordinator.data, description.data_key) + if ( + (coordinator := coordinators[description.api_category]) is not None + and coordinator.data + and key_exists(coordinator.data, description.data_key) + ) ] zone_coordinator = coordinators[DATA_ZONES] From 22bdeab1e78f7081dedbbba92f457d663e668da7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Jun 2022 09:13:43 -0600 Subject: [PATCH 040/107] Bump regenmaschine to 2022.06.0 (#73056) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 98dc9a6c877..a61283ea298 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.05.1"], + "requirements": ["regenmaschine==2022.06.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index baac079e585..27667d6b0fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.05.1 +regenmaschine==2022.06.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20fd6b229b5..6867d9d84c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ rachiopy==1.0.3 radios==0.1.1 # homeassistant.components.rainmachine -regenmaschine==2022.05.1 +regenmaschine==2022.06.0 # homeassistant.components.renault renault-api==0.1.11 From c47774e273531570d3179d899fe0c93327163cf1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 19:06:49 -1000 Subject: [PATCH 041/107] Fix incompatiblity with live logbook and google_assistant (#73063) --- homeassistant/components/logbook/const.py | 10 +- homeassistant/components/logbook/helpers.py | 179 ++++++++++++------ homeassistant/components/logbook/processor.py | 33 +--- .../components/logbook/websocket_api.py | 14 +- tests/components/logbook/common.py | 1 - .../components/logbook/test_websocket_api.py | 153 +++++++++++++-- 6 files changed, 271 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/logbook/const.py b/homeassistant/components/logbook/const.py index 3f0c6599724..d20acb553cc 100644 --- a/homeassistant/components/logbook/const.py +++ b/homeassistant/components/logbook/const.py @@ -30,13 +30,11 @@ LOGBOOK_ENTRY_NAME = "name" LOGBOOK_ENTRY_STATE = "state" LOGBOOK_ENTRY_WHEN = "when" -ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE} -ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY = { - EVENT_LOGBOOK_ENTRY, - EVENT_AUTOMATION_TRIGGERED, - EVENT_SCRIPT_STARTED, -} +# Automation events that can affect an entity_id or device_id +AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED} +# Events that are built-in to the logbook or core +BUILT_IN_EVENTS = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE} LOGBOOK_FILTERS = "logbook_filters" LOGBOOK_ENTITIES_FILTER = "entities_filter" diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index de021994b8d..eec60ebe740 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -7,6 +7,7 @@ from typing import Any from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( ATTR_DEVICE_ID, + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, EVENT_LOGBOOK_ENTRY, @@ -21,13 +22,10 @@ from homeassistant.core import ( is_callback, ) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_state_change_event -from .const import ( - ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, - DOMAIN, - ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY, -) +from .const import AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LazyEventPartialState @@ -41,6 +39,25 @@ def async_filter_entities(hass: HomeAssistant, entity_ids: list[str]) -> list[st ] +@callback +def _async_config_entries_for_ids( + hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None +) -> set[str]: + """Find the config entry ids for a set of entities or devices.""" + config_entry_ids: set[str] = set() + if entity_ids: + eng_reg = er.async_get(hass) + for entity_id in entity_ids: + if (entry := eng_reg.async_get(entity_id)) and entry.config_entry_id: + config_entry_ids.add(entry.config_entry_id) + if device_ids: + dev_reg = dr.async_get(hass) + for device_id in device_ids: + if (device := dev_reg.async_get(device_id)) and device.config_entries: + config_entry_ids |= device.config_entries + return config_entry_ids + + def async_determine_event_types( hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None ) -> tuple[str, ...]: @@ -49,42 +66,91 @@ def async_determine_event_types( str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] ] = hass.data.get(DOMAIN, {}) if not entity_ids and not device_ids: - return (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events) - config_entry_ids: set[str] = set() - intrested_event_types: set[str] = set() + return (*BUILT_IN_EVENTS, *external_events) + interested_domains: set[str] = set() + for entry_id in _async_config_entries_for_ids(hass, entity_ids, device_ids): + if entry := hass.config_entries.async_get_entry(entry_id): + interested_domains.add(entry.domain) + + # + # automations and scripts can refer to entities or devices + # but they do not have a config entry so we need + # to add them since we have historically included + # them when matching only on entities + # + intrested_event_types: set[str] = { + external_event + for external_event, domain_call in external_events.items() + if domain_call[0] in interested_domains + } | AUTOMATION_EVENTS if entity_ids: - # - # Home Assistant doesn't allow firing events from - # entities so we have a limited list to check - # - # automations and scripts can refer to entities - # but they do not have a config entry so we need - # to add them. - # - # We also allow entity_ids to be recorded via - # manual logbook entries. - # - intrested_event_types |= ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY + # We also allow entity_ids to be recorded via manual logbook entries. + intrested_event_types.add(EVENT_LOGBOOK_ENTRY) - if device_ids: - dev_reg = dr.async_get(hass) - for device_id in device_ids: - if (device := dev_reg.async_get(device_id)) and device.config_entries: - config_entry_ids |= device.config_entries - interested_domains: set[str] = set() - for entry_id in config_entry_ids: - if entry := hass.config_entries.async_get_entry(entry_id): - interested_domains.add(entry.domain) - for external_event, domain_call in external_events.items(): - if domain_call[0] in interested_domains: - intrested_event_types.add(external_event) + return tuple(intrested_event_types) - return tuple( - event_type - for event_type in (EVENT_LOGBOOK_ENTRY, *external_events) - if event_type in intrested_event_types - ) + +@callback +def extract_attr(source: dict[str, Any], attr: str) -> list[str]: + """Extract an attribute as a list or string.""" + if (value := source.get(attr)) is None: + return [] + if isinstance(value, list): + return value + return str(value).split(",") + + +@callback +def event_forwarder_filtered( + target: Callable[[Event], None], + entities_filter: EntityFilter | None, + entity_ids: list[str] | None, + device_ids: list[str] | None, +) -> Callable[[Event], None]: + """Make a callable to filter events.""" + if not entities_filter and not entity_ids and not device_ids: + # No filter + # - Script Trace (context ids) + # - Automation Trace (context ids) + return target + + if entities_filter: + # We have an entity filter: + # - Logbook panel + + @callback + def _forward_events_filtered_by_entities_filter(event: Event) -> None: + assert entities_filter is not None + event_data = event.data + entity_ids = extract_attr(event_data, ATTR_ENTITY_ID) + if entity_ids and not any( + entities_filter(entity_id) for entity_id in entity_ids + ): + return + domain = event_data.get(ATTR_DOMAIN) + if domain and not entities_filter(f"{domain}._"): + return + target(event) + + return _forward_events_filtered_by_entities_filter + + # We are filtering on entity_ids and/or device_ids: + # - Areas + # - Devices + # - Logbook Card + entity_ids_set = set(entity_ids) if entity_ids else set() + device_ids_set = set(device_ids) if device_ids else set() + + @callback + def _forward_events_filtered_by_device_entity_ids(event: Event) -> None: + event_data = event.data + if entity_ids_set.intersection( + extract_attr(event_data, ATTR_ENTITY_ID) + ) or device_ids_set.intersection(extract_attr(event_data, ATTR_DEVICE_ID)): + target(event) + + return _forward_events_filtered_by_device_entity_ids @callback @@ -93,6 +159,7 @@ def async_subscribe_events( subscriptions: list[CALLBACK_TYPE], target: Callable[[Event], None], event_types: tuple[str, ...], + entities_filter: EntityFilter | None, entity_ids: list[str] | None, device_ids: list[str] | None, ) -> None: @@ -103,41 +170,31 @@ def async_subscribe_events( """ ent_reg = er.async_get(hass) assert is_callback(target), "target must be a callback" - event_forwarder = target - - if entity_ids or device_ids: - entity_ids_set = set(entity_ids) if entity_ids else set() - device_ids_set = set(device_ids) if device_ids else set() - - @callback - def _forward_events_filtered(event: Event) -> None: - event_data = event.data - if ( - entity_ids_set and event_data.get(ATTR_ENTITY_ID) in entity_ids_set - ) or (device_ids_set and event_data.get(ATTR_DEVICE_ID) in device_ids_set): - target(event) - - event_forwarder = _forward_events_filtered - + event_forwarder = event_forwarder_filtered( + target, entities_filter, entity_ids, device_ids + ) for event_type in event_types: subscriptions.append( hass.bus.async_listen(event_type, event_forwarder, run_immediately=True) ) - @callback - def _forward_state_events_filtered(event: Event) -> None: - if event.data.get("old_state") is None or event.data.get("new_state") is None: - return - state: State = event.data["new_state"] - if not _is_state_filtered(ent_reg, state): - target(event) - if device_ids and not entity_ids: # No entities to subscribe to but we are filtering # on device ids so we do not want to get any state # changed events return + @callback + def _forward_state_events_filtered(event: Event) -> None: + if event.data.get("old_state") is None or event.data.get("new_state") is None: + return + state: State = event.data["new_state"] + if _is_state_filtered(ent_reg, state) or ( + entities_filter and not entities_filter(state.entity_id) + ): + return + target(event) + if entity_ids: subscriptions.append( async_track_state_change_event( diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index b3a43c2ca35..595268e5582 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -5,8 +5,6 @@ from collections.abc import Callable, Generator from contextlib import suppress from dataclasses import dataclass from datetime import datetime as dt -import logging -import re from typing import Any from sqlalchemy.engine.row import Row @@ -30,7 +28,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entityfilter import EntityFilter import homeassistant.util.dt as dt_util from .const import ( @@ -46,7 +43,6 @@ from .const import ( CONTEXT_STATE, CONTEXT_USER_ID, DOMAIN, - LOGBOOK_ENTITIES_FILTER, LOGBOOK_ENTRY_DOMAIN, LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_ICON, @@ -62,11 +58,6 @@ from .models import EventAsRow, LazyEventPartialState, async_event_to_row from .queries import statement_for_request from .queries.common import PSUEDO_EVENT_STATE_CHANGED -_LOGGER = logging.getLogger(__name__) - -ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') -DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') - @dataclass class LogbookRun: @@ -106,10 +97,6 @@ class EventProcessor: self.device_ids = device_ids self.context_id = context_id self.filters: Filters | None = hass.data[LOGBOOK_FILTERS] - if self.limited_select: - self.entities_filter: EntityFilter | Callable[[str], bool] | None = None - else: - self.entities_filter = hass.data[LOGBOOK_ENTITIES_FILTER] format_time = ( _row_time_fired_timestamp if timestamp else _row_time_fired_isoformat ) @@ -183,7 +170,6 @@ class EventProcessor: return list( _humanify( row_generator, - self.entities_filter, self.ent_reg, self.logbook_run, self.context_augmenter, @@ -193,7 +179,6 @@ class EventProcessor: def _humanify( rows: Generator[Row | EventAsRow, None, None], - entities_filter: EntityFilter | Callable[[str], bool] | None, ent_reg: er.EntityRegistry, logbook_run: LogbookRun, context_augmenter: ContextAugmenter, @@ -208,29 +193,13 @@ def _humanify( include_entity_name = logbook_run.include_entity_name format_time = logbook_run.format_time - def _keep_row(row: EventAsRow) -> bool: - """Check if the entity_filter rejects a row.""" - assert entities_filter is not None - if entity_id := row.entity_id: - return entities_filter(entity_id) - if entity_id := row.data.get(ATTR_ENTITY_ID): - return entities_filter(entity_id) - if domain := row.data.get(ATTR_DOMAIN): - return entities_filter(f"{domain}._") - return True - # Process rows for row in rows: context_id = context_lookup.memorize(row) if row.context_only: continue event_type = row.event_type - if event_type == EVENT_CALL_SERVICE or ( - entities_filter - # We literally mean is EventAsRow not a subclass of EventAsRow - and type(row) is EventAsRow # pylint: disable=unidiomatic-typecheck - and not _keep_row(row) - ): + if event_type == EVENT_CALL_SERVICE: continue if event_type is PSUEDO_EVENT_STATE_CHANGED: entity_id = row.entity_id diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index b27ae65b70c..a8f9bc50920 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -16,9 +16,11 @@ from homeassistant.components.websocket_api import messages from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util +from .const import LOGBOOK_ENTITIES_FILTER from .helpers import ( async_determine_event_types, async_filter_entities, @@ -365,8 +367,18 @@ async def ws_event_stream( ) _unsub() + entities_filter: EntityFilter | None = None + if not event_processor.limited_select: + entities_filter = hass.data[LOGBOOK_ENTITIES_FILTER] + async_subscribe_events( - hass, subscriptions, _queue_or_cancel, event_types, entity_ids, device_ids + hass, + subscriptions, + _queue_or_cancel, + event_types, + entities_filter, + entity_ids, + device_ids, ) subscriptions_setup_complete_time = dt_util.utcnow() connection.subscriptions[msg_id] = _unsub diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index b88c3854967..a41f983bfed 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -68,7 +68,6 @@ def mock_humanify(hass_, rows): return list( processor._humanify( rows, - None, ent_reg, logbook_run, context_augmenter, diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 2623a5b17d5..ae1f7968e3b 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -27,8 +27,8 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, State -from homeassistant.helpers import device_registry +from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -51,22 +51,8 @@ def set_utc(hass): hass.config.set_time_zone("UTC") -async def _async_mock_device_with_logbook_platform(hass): - """Mock an integration that provides a device that are described by the logbook.""" - entry = MockConfigEntry(domain="test", data={"first": True}, options=None) - entry.add_to_hass(hass) - dev_reg = device_registry.async_get(hass) - device = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers={("bridgeid", "0123")}, - sw_version="sw-version", - name="device name", - manufacturer="manufacturer", - model="model", - suggested_area="Game Room", - ) - +@callback +async def _async_mock_logbook_platform(hass: HomeAssistant) -> None: class MockLogbookPlatform: """Mock a logbook platform.""" @@ -90,6 +76,40 @@ async def _async_mock_device_with_logbook_platform(hass): async_describe_event("test", "mock_event", async_describe_test_event) await logbook._process_logbook_platform(hass, "test", MockLogbookPlatform) + + +async def _async_mock_entity_with_logbook_platform(hass): + """Mock an integration that provides an entity that are described by the logbook.""" + entry = MockConfigEntry(domain="test", data={"first": True}, options=None) + entry.add_to_hass(hass) + ent_reg = entity_registry.async_get(hass) + entry = ent_reg.async_get_or_create( + platform="test", + domain="sensor", + config_entry=entry, + unique_id="1234", + suggested_object_id="test", + ) + await _async_mock_logbook_platform(hass) + return entry + + +async def _async_mock_device_with_logbook_platform(hass): + """Mock an integration that provides a device that are described by the logbook.""" + entry = MockConfigEntry(domain="test", data={"first": True}, options=None) + entry.add_to_hass(hass) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + sw_version="sw-version", + name="device name", + manufacturer="manufacturer", + model="model", + suggested_area="Game Room", + ) + await _async_mock_logbook_platform(hass) return device @@ -1786,6 +1806,103 @@ async def test_event_stream_bad_start_time(hass, hass_ws_client, recorder_mock): assert response["error"]["code"] == "invalid_start_time" +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_logbook_stream_match_multiple_entities( + hass, recorder_mock, hass_ws_client +): + """Test logbook stream with a described integration that uses multiple entities.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + entry = await _async_mock_entity_with_logbook_platform(hass) + entity_id = entry.entity_id + hass.states.async_set(entity_id, STATE_ON) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": [entity_id], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # There are no answers to our initial query + # so we get an empty reply. This is to ensure + # consumers of the api know there are no results + # and its not a failure case. This is useful + # in the frontend so we can tell the user there + # are no results vs waiting for them to appear + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + await async_wait_recording_done(hass) + + hass.states.async_set("binary_sensor.should_not_appear", STATE_ON) + hass.states.async_set("binary_sensor.should_not_appear", STATE_OFF) + context = core.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + hass.bus.async_fire( + "mock_event", {"entity_id": ["sensor.any", entity_id]}, context=context + ) + hass.bus.async_fire("mock_event", {"entity_id": [f"sensor.any,{entity_id}"]}) + hass.bus.async_fire("mock_event", {"entity_id": ["sensor.no_match", "light.off"]}) + hass.states.async_set(entity_id, STATE_OFF, context=context) + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "domain": "test", + "message": "is on fire", + "name": "device name", + "when": ANY, + }, + { + "context_domain": "test", + "context_event_type": "mock_event", + "context_message": "is on fire", + "context_name": "device name", + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "entity_id": "sensor.test", + "state": "off", + "when": ANY, + }, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + async def test_event_stream_bad_end_time(hass, hass_ws_client, recorder_mock): """Test event_stream bad end time.""" await async_setup_component(hass, "logbook", {}) From 792ebbb600059ddb0e31b88ff054274e2156eeb6 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Sun, 5 Jun 2022 22:27:21 -0400 Subject: [PATCH 042/107] Fix elk attributes not being json serializable (#73096) * Fix jsonifying. * Only serialize Enums --- homeassistant/components/elkm1/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 9a7c7dbc43a..cf1cac3bdb3 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from enum import Enum import logging import re from types import MappingProxyType @@ -481,7 +482,10 @@ class ElkEntity(Entity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the default attributes of the element.""" - return {**self._element.as_dict(), **self.initial_attrs()} + dict_as_str = {} + for key, val in self._element.as_dict().items(): + dict_as_str[key] = val.value if isinstance(val, Enum) else val + return {**dict_as_str, **self.initial_attrs()} @property def available(self) -> bool: From 93aad108a7b080b30250da14ad26b725e37edbd2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 21:25:26 -1000 Subject: [PATCH 043/107] Mark counter domain as continuous to exclude it from logbook (#73101) --- homeassistant/components/logbook/const.py | 9 ++++ homeassistant/components/logbook/helpers.py | 9 ++-- .../components/logbook/queries/common.py | 45 +++++++++++++------ homeassistant/components/recorder/filters.py | 9 +++- tests/components/logbook/test_init.py | 6 +++ .../components/logbook/test_websocket_api.py | 8 +++- 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/logbook/const.py b/homeassistant/components/logbook/const.py index d20acb553cc..e1abd987659 100644 --- a/homeassistant/components/logbook/const.py +++ b/homeassistant/components/logbook/const.py @@ -2,9 +2,18 @@ from __future__ import annotations from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED +from homeassistant.components.counter import DOMAIN as COUNTER_DOMAIN +from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.script import EVENT_SCRIPT_STARTED +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import EVENT_CALL_SERVICE, EVENT_LOGBOOK_ENTRY +# Domains that are always continuous +ALWAYS_CONTINUOUS_DOMAINS = {COUNTER_DOMAIN, PROXIMITY_DOMAIN} + +# Domains that are continuous if there is a UOM set on the entity +CONDITIONALLY_CONTINUOUS_DOMAINS = {SENSOR_DOMAIN} + ATTR_MESSAGE = "message" DOMAIN = "logbook" diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index eec60ebe740..ef322c44e05 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -20,12 +20,13 @@ from homeassistant.core import ( State, callback, is_callback, + split_entity_id, ) from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_state_change_event -from .const import AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN +from .const import ALWAYS_CONTINUOUS_DOMAINS, AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LazyEventPartialState @@ -235,7 +236,8 @@ def _is_state_filtered(ent_reg: er.EntityRegistry, state: State) -> bool: we only get significant changes (state.last_changed != state.last_updated) """ return bool( - state.last_changed != state.last_updated + split_entity_id(state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or state.last_changed != state.last_updated or ATTR_UNIT_OF_MEASUREMENT in state.attributes or is_sensor_continuous(ent_reg, state.entity_id) ) @@ -250,7 +252,8 @@ def _is_entity_id_filtered( from the database when a list of entities is requested. """ return bool( - (state := hass.states.get(entity_id)) + split_entity_id(entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or (state := hass.states.get(entity_id)) and (ATTR_UNIT_OF_MEASUREMENT in state.attributes) or is_sensor_continuous(ent_reg, entity_id) ) diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index a7a4f84a59e..56925b60e62 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -10,7 +10,7 @@ from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.expression import literal from sqlalchemy.sql.selectable import Select -from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN +from homeassistant.components.recorder.filters import like_domain_matchers from homeassistant.components.recorder.models import ( EVENTS_CONTEXT_ID_INDEX, OLD_FORMAT_ATTRS_JSON, @@ -22,15 +22,19 @@ from homeassistant.components.recorder.models import ( StateAttributes, States, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN} -CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] +from ..const import ALWAYS_CONTINUOUS_DOMAINS, CONDITIONALLY_CONTINUOUS_DOMAINS + +# Domains that are continuous if there is a UOM set on the entity +CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers( + CONDITIONALLY_CONTINUOUS_DOMAINS +) +# Domains that are always continuous +ALWAYS_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers(ALWAYS_CONTINUOUS_DOMAINS) UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" - PSUEDO_EVENT_STATE_CHANGED = None # Since we don't store event_types and None # and we don't store state_changed in events @@ -220,29 +224,44 @@ def _missing_state_matcher() -> sqlalchemy.and_: def _not_continuous_entity_matcher() -> sqlalchemy.or_: """Match non continuous entities.""" return sqlalchemy.or_( - _not_continuous_domain_matcher(), + # First exclude domains that may be continuous + _not_possible_continuous_domain_matcher(), + # But let in the entities in the possible continuous domains + # that are not actually continuous sensors because they lack a UOM sqlalchemy.and_( - _continuous_domain_matcher, _not_uom_attributes_matcher() + _conditionally_continuous_domain_matcher, _not_uom_attributes_matcher() ).self_group(), ) -def _not_continuous_domain_matcher() -> sqlalchemy.and_: - """Match not continuous domains.""" +def _not_possible_continuous_domain_matcher() -> sqlalchemy.and_: + """Match not continuous domains. + + This matches domain that are always considered continuous + and domains that are conditionally (if they have a UOM) + continuous domains. + """ return sqlalchemy.and_( *[ ~States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + for entity_domain in ( + *ALWAYS_CONTINUOUS_ENTITY_ID_LIKE, + *CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE, + ) ], ).self_group() -def _continuous_domain_matcher() -> sqlalchemy.or_: - """Match continuous domains.""" +def _conditionally_continuous_domain_matcher() -> sqlalchemy.or_: + """Match conditionally continuous domains. + + This matches domain that are only considered + continuous if a UOM is set. + """ return sqlalchemy.or_( *[ States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + for entity_domain in CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE ], ).self_group() diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 90851e9f251..0b3e0e68030 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -248,8 +248,13 @@ def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - (column.is_not(None) & cast(column, Text()).like(encoder(f"{domain}.%"))) - for domain in domains + (column.is_not(None) & cast(column, Text()).like(encoder(domain_matcher))) + for domain_matcher in like_domain_matchers(domains) for column in columns ] return or_(*matchers) if matchers else or_(False) + + +def like_domain_matchers(domains: Iterable[str]) -> list[str]: + """Convert a list of domains to sql LIKE matchers.""" + return [f"{domain}.%" for domain in domains] diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 651a00fb0cf..d16b3476d84 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -745,6 +745,12 @@ async def test_filter_continuous_sensor_values( entity_id_third = "light.bla" hass.states.async_set(entity_id_third, STATE_OFF, {"unit_of_measurement": "foo"}) hass.states.async_set(entity_id_third, STATE_ON, {"unit_of_measurement": "foo"}) + entity_id_proximity = "proximity.bla" + hass.states.async_set(entity_id_proximity, STATE_OFF) + hass.states.async_set(entity_id_proximity, STATE_ON) + entity_id_counter = "counter.bla" + hass.states.async_set(entity_id_counter, STATE_OFF) + hass.states.async_set(entity_id_counter, STATE_ON) await async_wait_recording_done(hass) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index ae1f7968e3b..4df2f456eb6 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2209,7 +2209,9 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) -async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_client): +async def test_subscribe_all_entities_are_continuous( + hass, recorder_mock, hass_ws_client +): """Test subscribe/unsubscribe logbook stream with entities that are always filtered.""" now = dt_util.utcnow() await asyncio.gather( @@ -2227,6 +2229,8 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie hass.states.async_set( entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} ) + hass.states.async_set("counter.any", state) + hass.states.async_set("proximity.any", state) init_count = sum(hass.bus.async_listeners().values()) _cycle_entities() @@ -2238,7 +2242,7 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie "id": 7, "type": "logbook/event_stream", "start_time": now.isoformat(), - "entity_ids": ["sensor.uom"], + "entity_ids": ["sensor.uom", "counter.any", "proximity.any"], } ) From eef79e291218702a6b74a25fe9eac5c250d711e0 Mon Sep 17 00:00:00 2001 From: lymanepp <4195527+lymanepp@users.noreply.github.com> Date: Mon, 6 Jun 2022 00:10:33 -0400 Subject: [PATCH 044/107] Tomorrowio utc fix (#73102) * Discard past data using local time instead of UTC * Tweak changes to fix tests * Cleanup --- homeassistant/components/tomorrowio/weather.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index bf687f8bdca..52eedff49b1 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -198,13 +198,16 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): max_forecasts = MAX_FORECASTS[self.forecast_type] forecast_count = 0 + # Convert utcnow to local to be compatible with tests + today = dt_util.as_local(dt_util.utcnow()).date() + # Set default values (in cases where keys don't exist), None will be # returned. Override properties per forecast type as needed for forecast in raw_forecasts: forecast_dt = dt_util.parse_datetime(forecast[TMRW_ATTR_TIMESTAMP]) # Throw out past data - if forecast_dt.date() < dt_util.utcnow().date(): + if dt_util.as_local(forecast_dt).date() < today: continue values = forecast["values"] From 54ff6ddd416458a5c859c81984ec07eeade48c44 Mon Sep 17 00:00:00 2001 From: Igor Loborec Date: Mon, 6 Jun 2022 03:27:46 +0100 Subject: [PATCH 045/107] Remove available property from Kodi (#73103) --- homeassistant/components/kodi/media_player.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index e19ffc6219c..2b509ed0e08 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -636,11 +636,6 @@ class KodiEntity(MediaPlayerEntity): return None - @property - def available(self): - """Return True if entity is available.""" - return not self._connect_error - async def async_turn_on(self): """Turn the media player on.""" _LOGGER.debug("Firing event to turn on device") From 2f3232f087b16b548f9825f2d0aef3cff21cbc4b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Jun 2022 12:18:57 -0700 Subject: [PATCH 046/107] Bumped version to 2022.6.3 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 23ada7591ee..4287c7e96f8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 08bd61e7382..a16b51c03d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.2 +version = 2022.6.3 url = https://www.home-assistant.io/ [options] From 33f282af46697df1b90cda1ac449a87c522edb81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Jun 2022 12:49:15 -0700 Subject: [PATCH 047/107] Point iAlarm XR at PyPI fork (#73143) --- homeassistant/components/ialarm_xr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm_xr/manifest.json b/homeassistant/components/ialarm_xr/manifest.json index f863f360242..5befca3b95d 100644 --- a/homeassistant/components/ialarm_xr/manifest.json +++ b/homeassistant/components/ialarm_xr/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm_xr", "name": "Antifurto365 iAlarmXR", "documentation": "https://www.home-assistant.io/integrations/ialarm_xr", - "requirements": ["pyialarmxr==1.0.18"], + "requirements": ["pyialarmxr-homeassistant==1.0.18"], "codeowners": ["@bigmoby"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 27667d6b0fe..e99b2ce353b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1550,7 +1550,7 @@ pyhomeworks==0.0.6 pyialarm==1.9.0 # homeassistant.components.ialarm_xr -pyialarmxr==1.0.18 +pyialarmxr-homeassistant==1.0.18 # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6867d9d84c0..3dfb583ad2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1038,7 +1038,7 @@ pyhomematic==0.1.77 pyialarm==1.9.0 # homeassistant.components.ialarm_xr -pyialarmxr==1.0.18 +pyialarmxr-homeassistant==1.0.18 # homeassistant.components.icloud pyicloud==1.0.0 From a886c6110d920f7b78a35aac64ccd6b0ad6d0a02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Jun 2022 09:50:52 -1000 Subject: [PATCH 048/107] Fix state_changes_during_period history query when no entities are passed (#73139) --- homeassistant/components/recorder/history.py | 17 ++++++------ tests/components/recorder/test_history.py | 29 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 5dd5c0d3040..37285f66d1d 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -352,7 +352,8 @@ def _state_changed_during_period_stmt( ) if end_time: stmt += lambda q: q.filter(States.last_updated < end_time) - stmt += lambda q: q.filter(States.entity_id == entity_id) + if entity_id: + stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id @@ -378,6 +379,7 @@ def state_changes_during_period( ) -> MutableMapping[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" entity_id = entity_id.lower() if entity_id is not None else None + entity_ids = [entity_id] if entity_id is not None else None with session_scope(hass=hass) as session: stmt = _state_changed_during_period_stmt( @@ -392,8 +394,6 @@ def state_changes_during_period( states = execute_stmt_lambda_element( session, stmt, None if entity_id else start_time, end_time ) - entity_ids = [entity_id] if entity_id is not None else None - return cast( MutableMapping[str, list[State]], _sorted_states_to_dict( @@ -408,14 +408,16 @@ def state_changes_during_period( def _get_last_state_changes_stmt( - schema_version: int, number_of_states: int, entity_id: str + schema_version: int, number_of_states: int, entity_id: str | None ) -> StatementLambdaElement: stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, False, include_last_changed=False ) stmt += lambda q: q.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) - ).filter(States.entity_id == entity_id) + ) + if entity_id: + stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id @@ -427,19 +429,18 @@ def _get_last_state_changes_stmt( def get_last_state_changes( - hass: HomeAssistant, number_of_states: int, entity_id: str + hass: HomeAssistant, number_of_states: int, entity_id: str | None ) -> MutableMapping[str, list[State]]: """Return the last number_of_states.""" start_time = dt_util.utcnow() entity_id = entity_id.lower() if entity_id is not None else None + entity_ids = [entity_id] if entity_id is not None else None with session_scope(hass=hass) as session: stmt = _get_last_state_changes_stmt( _schema_version(hass), number_of_states, entity_id ) states = list(execute_stmt_lambda_element(session, stmt)) - entity_ids = [entity_id] if entity_id is not None else None - return cast( MutableMapping[str, list[State]], _sorted_states_to_dict( diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index da6c3a8af35..ee02ffbec49 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -878,3 +878,32 @@ async def test_get_full_significant_states_handles_empty_last_changed( assert db_sensor_one_states[0].last_updated is not None assert db_sensor_one_states[1].last_updated is not None assert db_sensor_one_states[0].last_updated != db_sensor_one_states[1].last_updated + + +def test_state_changes_during_period_multiple_entities_single_test(hass_recorder): + """Test state change during period with multiple entities in the same test. + + This test ensures the sqlalchemy query cache does not + generate incorrect results. + """ + hass = hass_recorder() + start = dt_util.utcnow() + test_entites = {f"sensor.{i}": str(i) for i in range(30)} + for entity_id, value in test_entites.items(): + hass.states.set(entity_id, value) + + wait_recording_done(hass) + end = dt_util.utcnow() + + hist = history.state_changes_during_period(hass, start, end, None) + for entity_id, value in test_entites.items(): + hist[entity_id][0].state == value + + for entity_id, value in test_entites.items(): + hist = history.state_changes_during_period(hass, start, end, entity_id) + assert len(hist) == 1 + hist[entity_id][0].state == value + + hist = history.state_changes_during_period(hass, start, end, None) + for entity_id, value in test_entites.items(): + hist[entity_id][0].state == value From 4678466560b7490e8fcfc3206c0db59dd7c4b074 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 20:59:00 -1000 Subject: [PATCH 049/107] Remove unused code from logbook (#72950) --- homeassistant/components/logbook/processor.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 595268e5582..82225df8364 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -386,12 +386,6 @@ def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: return False -def _row_event_data_extract(row: Row | EventAsRow, extractor: re.Pattern) -> str | None: - """Extract from event_data row.""" - result = extractor.search(row.shared_data or row.event_data or "") - return result.group(1) if result else None - - def _row_time_fired_isoformat(row: Row | EventAsRow) -> str: """Convert the row timed_fired to isoformat.""" return process_timestamp_to_utc_isoformat(row.time_fired or dt_util.utcnow()) From 70473df2fe4d8df1c2921b9af055035a399968de Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 6 Jun 2022 17:18:07 -0500 Subject: [PATCH 050/107] Fix errors when unjoining multiple Sonos devices simultaneously (#73133) --- .../components/sonos/media_player.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index f331f980bb4..938a651c34d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -751,17 +751,23 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_content_type, ) - def join_players(self, group_members): + async def async_join_players(self, group_members): """Join `group_members` as a player group with the current player.""" - speakers = [] - for entity_id in group_members: - if speaker := self.hass.data[DATA_SONOS].entity_id_mappings.get(entity_id): - speakers.append(speaker) - else: - raise HomeAssistantError(f"Not a known Sonos entity_id: {entity_id}") + async with self.hass.data[DATA_SONOS].topology_condition: + speakers = [] + for entity_id in group_members: + if speaker := self.hass.data[DATA_SONOS].entity_id_mappings.get( + entity_id + ): + speakers.append(speaker) + else: + raise HomeAssistantError( + f"Not a known Sonos entity_id: {entity_id}" + ) - self.speaker.join(speakers) + await self.hass.async_add_executor_job(self.speaker.join, speakers) - def unjoin_player(self): + async def async_unjoin_player(self): """Remove this player from any group.""" - self.speaker.unjoin() + async with self.hass.data[DATA_SONOS].topology_condition: + await self.hass.async_add_executor_job(self.speaker.unjoin) From f4ed7720de3ccdc893a67c9308251f6585da5017 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 6 Jun 2022 23:46:52 +0200 Subject: [PATCH 051/107] Bump async-upnp-client==0.31.1 (#73135) Co-authored-by: J. Nick Koston --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 15beea714da..cc72f5d4778 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.30.1"], + "requirements": ["async-upnp-client==0.31.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 21329440788..590d1b8370a 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.30.1"], + "requirements": ["async-upnp-client==0.31.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index fd97eb12e54..ce65af7d8bb 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.0.1", - "async-upnp-client==0.30.1" + "async-upnp-client==0.31.1" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 5cbd3d0d10e..f0db05d9015 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.30.1"], + "requirements": ["async-upnp-client==0.31.1"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 2e76dac4adb..dc87e73fdee 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.30.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.31.1", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman", "@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 5ec224498be..57d32f315ba 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.10", "async-upnp-client==0.30.1"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.31.1"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 076b58b5185..bec79680e0d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.11 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.30.1 +async-upnp-client==0.31.1 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index e99b2ce353b..9da35195c7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -336,7 +336,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.30.1 +async-upnp-client==0.31.1 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3dfb583ad2e..49613212cf7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -278,7 +278,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.30.1 +async-upnp-client==0.31.1 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From e886d3712471a42ac101098caacb72a5070746a2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 8 Jun 2022 01:01:44 +0200 Subject: [PATCH 052/107] Use default None for voltage property of FritzDevice in Fritz!Smarthome (#73141) use default None for device.voltage --- homeassistant/components/fritzbox/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index e590f14ce89..2ae7f9dccc8 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -96,7 +96,9 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.voltage / 1000 if device.voltage else 0.0, + native_value=lambda device: device.voltage / 1000 + if getattr(device, "voltage", None) + else 0.0, ), FritzSensorEntityDescription( key="electric_current", @@ -106,7 +108,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] native_value=lambda device: device.power / device.voltage - if device.power and device.voltage + if device.power and getattr(device, "voltage", None) else 0.0, ), FritzSensorEntityDescription( From fa56e3633d383c508ac358f75b9f531133afabd8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Jun 2022 11:02:08 +1200 Subject: [PATCH 053/107] Fix KeyError from ESPHome media players on startup (#73149) --- .../components/esphome/media_player.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index f9027142ae2..d7ce73976e7 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -25,7 +25,12 @@ from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import EsphomeEntity, EsphomeEnumMapper, platform_async_setup_entry +from . import ( + EsphomeEntity, + EsphomeEnumMapper, + esphome_state_property, + platform_async_setup_entry, +) async def async_setup_entry( @@ -54,6 +59,10 @@ _STATES: EsphomeEnumMapper[MediaPlayerState, str] = EsphomeEnumMapper( ) +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# pylint: disable=invalid-overridden-method + + class EsphomeMediaPlayer( EsphomeEntity[MediaPlayerInfo, MediaPlayerEntityState], MediaPlayerEntity ): @@ -61,17 +70,17 @@ class EsphomeMediaPlayer( _attr_device_class = MediaPlayerDeviceClass.SPEAKER - @property + @esphome_state_property def state(self) -> str | None: """Return current state.""" return _STATES.from_esphome(self._state.state) - @property + @esphome_state_property def is_volume_muted(self) -> bool: """Return true if volume is muted.""" return self._state.muted - @property + @esphome_state_property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" return self._state.volume From af248fa386ba0de4bb7131927bd2a244941bfcfa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 7 Jun 2022 17:14:40 -0600 Subject: [PATCH 054/107] Fix bugs with RainMachine zone run time sensors (#73179) --- .../components/rainmachine/sensor.py | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index cc37189aa49..1144ceea159 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -4,6 +4,8 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta +from regenmaschine.controller import Controller + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -13,8 +15,9 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity import EntityCategory, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow from . import RainMachineEntity @@ -205,16 +208,33 @@ class ZoneTimeRemainingSensor(RainMachineEntity, SensorEntity): entity_description: RainMachineSensorDescriptionUid + def __init__( + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator, + controller: Controller, + description: EntityDescription, + ) -> None: + """Initialize.""" + super().__init__(entry, coordinator, controller, description) + + self._running_or_queued: bool = False + @callback def update_from_latest_data(self) -> None: """Update the state.""" data = self.coordinator.data[self.entity_description.uid] now = utcnow() - if RUN_STATE_MAP.get(data["state"]) != RunStates.RUNNING: - # If the zone isn't actively running, return immediately: + if RUN_STATE_MAP.get(data["state"]) == RunStates.NOT_RUNNING: + if self._running_or_queued: + # If we go from running to not running, update the state to be right + # now (i.e., the time the zone stopped running): + self._attr_native_value = now + self._running_or_queued = False return + self._running_or_queued = True new_timestamp = now + timedelta(seconds=data["remaining"]) if self._attr_native_value: From d6b1a7ca68735f568dcd7b1a0c14c7c2c902d71c Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 8 Jun 2022 01:11:38 +0200 Subject: [PATCH 055/107] Fix creating unique IDs for WiFi switches in Fritz!Tools (#73183) --- homeassistant/components/fritz/switch.py | 11 +- tests/components/fritz/conftest.py | 12 +- tests/components/fritz/const.py | 3 +- tests/components/fritz/test_button.py | 2 +- tests/components/fritz/test_config_flow.py | 2 +- tests/components/fritz/test_diagnostics.py | 2 +- tests/components/fritz/test_init.py | 2 +- tests/components/fritz/test_sensor.py | 2 +- tests/components/fritz/test_switch.py | 189 +++++++++++++++++++++ tests/components/fritz/test_update.py | 2 +- 10 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 tests/components/fritz/test_switch.py diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index cac6e735a81..a45cd347463 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -169,7 +169,16 @@ def wifi_entities_list( } for i, network in networks.copy().items(): networks[i]["switch_name"] = network["ssid"] - if len([j for j, n in networks.items() if n["ssid"] == network["ssid"]]) > 1: + if ( + len( + [ + j + for j, n in networks.items() + if slugify(n["ssid"]) == slugify(network["ssid"]) + ] + ) + > 1 + ): networks[i]["switch_name"] += f" ({WIFI_STANDARD[i]})" _LOGGER.debug("WiFi networks list: %s", networks) diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index b073335f20a..edbde883f56 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -1,4 +1,4 @@ -"""Common stuff for AVM Fritz!Box tests.""" +"""Common stuff for Fritz!Tools tests.""" import logging from unittest.mock import MagicMock, patch @@ -73,13 +73,19 @@ class FritzHostMock(FritzHosts): return MOCK_MESH_DATA +@pytest.fixture(name="fc_data") +def fc_data_mock(): + """Fixture for default fc_data.""" + return MOCK_FB_SERVICES + + @pytest.fixture() -def fc_class_mock(): +def fc_class_mock(fc_data): """Fixture that sets up a mocked FritzConnection class.""" with patch( "homeassistant.components.fritz.common.FritzConnection", autospec=True ) as result: - result.return_value = FritzConnectionMock(MOCK_FB_SERVICES) + result.return_value = FritzConnectionMock(fc_data) yield result diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index f8f6f8370d7..32f2211d16b 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -1,4 +1,4 @@ -"""Common stuff for AVM Fritz!Box tests.""" +"""Common stuff for Fritz!Tools tests.""" from homeassistant.components import ssdp from homeassistant.components.fritz.const import DOMAIN from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN @@ -194,6 +194,7 @@ MOCK_FB_SERVICES: dict[str, dict] = { }, } + MOCK_MESH_DATA = { "schema_version": "1.9", "nodes": [ diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py index a6ff579958a..a2bd6132731 100644 --- a/tests/components/fritz/test_button.py +++ b/tests/components/fritz/test_button.py @@ -1,4 +1,4 @@ -"""Tests for Shelly button platform.""" +"""Tests for Fritz!Tools button platform.""" from unittest.mock import patch import pytest diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 6d014782842..619f06f5493 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -1,4 +1,4 @@ -"""Tests for AVM Fritz!Box config flow.""" +"""Tests for Fritz!Tools config flow.""" import dataclasses from unittest.mock import patch diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py index a4b4942c375..2c0b42d6bdf 100644 --- a/tests/components/fritz/test_diagnostics.py +++ b/tests/components/fritz/test_diagnostics.py @@ -1,4 +1,4 @@ -"""Tests for the AVM Fritz!Box integration.""" +"""Tests for Fritz!Tools diagnostics platform.""" from __future__ import annotations from aiohttp import ClientSession diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py index fd67321d235..9f7a17de900 100644 --- a/tests/components/fritz/test_init.py +++ b/tests/components/fritz/test_init.py @@ -1,4 +1,4 @@ -"""Tests for AVM Fritz!Box.""" +"""Tests for Fritz!Tools.""" from unittest.mock import patch from fritzconnection.core.exceptions import FritzSecurityError diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 31e142a3e47..73a7c1068ae 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -1,4 +1,4 @@ -"""Tests for Shelly button platform.""" +"""Tests for Fritz!Tools sensor platform.""" from __future__ import annotations from datetime import timedelta diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py new file mode 100644 index 00000000000..1e744bb62e6 --- /dev/null +++ b/tests/components/fritz/test_switch.py @@ -0,0 +1,189 @@ +"""Tests for Fritz!Tools switch platform.""" +from __future__ import annotations + +import pytest + +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_FB_SERVICES, MOCK_USER_DATA + +from tests.common import MockConfigEntry + +MOCK_WLANCONFIGS_SAME_SSID: dict[str, dict] = { + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 13, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:12", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, + "WLANConfiguration2": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 52, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:13", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, +} +MOCK_WLANCONFIGS_DIFF_SSID: dict[str, dict] = { + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 13, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:12", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, + "WLANConfiguration2": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 52, + "NewSSID": "WiFi2", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:13", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, +} +MOCK_WLANCONFIGS_DIFF2_SSID: dict[str, dict] = { + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 13, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:12", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, + "WLANConfiguration2": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 52, + "NewSSID": "WiFi+", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:13", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, +} + + +@pytest.mark.parametrize( + "fc_data, expected_wifi_names", + [ + ( + {**MOCK_FB_SERVICES, **MOCK_WLANCONFIGS_SAME_SSID}, + ["WiFi (2.4Ghz)", "WiFi (5Ghz)"], + ), + ({**MOCK_FB_SERVICES, **MOCK_WLANCONFIGS_DIFF_SSID}, ["WiFi", "WiFi2"]), + ( + {**MOCK_FB_SERVICES, **MOCK_WLANCONFIGS_DIFF2_SSID}, + ["WiFi (2.4Ghz)", "WiFi+ (5Ghz)"], + ), + ], +) +async def test_switch_setup( + hass: HomeAssistant, + expected_wifi_names: list[str], + fc_class_mock, + fh_class_mock, +): + """Test setup of Fritz!Tools switches.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + switches = hass.states.async_all(Platform.SWITCH) + assert len(switches) == 3 + assert switches[0].name == f"Mock Title Wi-Fi {expected_wifi_names[0]}" + assert switches[1].name == f"Mock Title Wi-Fi {expected_wifi_names[1]}" + assert switches[2].name == "printer Internet Access" diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py index 2261ef3ef9b..32bdf361847 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -1,4 +1,4 @@ -"""The tests for the Fritzbox update entity.""" +"""Tests for Fritz!Tools update platform.""" from unittest.mock import patch From bc7cf1f6493cec7d71d752bb2134fd9913bb47b6 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 7 Jun 2022 14:39:15 -0700 Subject: [PATCH 056/107] Bump pywemo to 0.9.1 (#73186) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 40bb8161d90..b324ba060ea 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.8.1"], + "requirements": ["pywemo==0.9.1"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 9da35195c7e..d0ac6af8c74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2023,7 +2023,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.8.1 +pywemo==0.9.1 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49613212cf7..24916a4ac0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1343,7 +1343,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.8.1 +pywemo==0.9.1 # homeassistant.components.wilight pywilight==0.0.70 From d63569da82f7188ec6a5c8051a6def0dbb19b25e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Jun 2022 13:15:50 -1000 Subject: [PATCH 057/107] Remove sqlalchemy lambda_stmt usage from history, logbook, and statistics (#73191) --- .../components/logbook/queries/__init__.py | 26 ++-- .../components/logbook/queries/all.py | 20 ++- .../components/logbook/queries/devices.py | 42 +++--- .../components/logbook/queries/entities.py | 47 +++---- .../logbook/queries/entities_and_devices.py | 59 ++++---- homeassistant/components/recorder/history.py | 130 ++++++++---------- .../components/recorder/statistics.py | 99 ++++++------- homeassistant/components/recorder/util.py | 10 +- tests/components/recorder/test_util.py | 23 ++-- 9 files changed, 203 insertions(+), 253 deletions(-) diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index 3c027823612..a59ebc94b87 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -2,8 +2,9 @@ from __future__ import annotations from datetime import datetime as dt +import json -from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select from homeassistant.components.recorder.filters import Filters @@ -21,7 +22,7 @@ def statement_for_request( device_ids: list[str] | None = None, filters: Filters | None = None, context_id: str | None = None, -) -> StatementLambdaElement: +) -> Select: """Generate the logbook statement for a logbook request.""" # No entities: logbook sends everything for the timeframe @@ -38,41 +39,36 @@ def statement_for_request( context_id, ) - # sqlalchemy caches object quoting, the - # json quotable ones must be a different - # object from the non-json ones to prevent - # sqlalchemy from quoting them incorrectly - # entities and devices: logbook sends everything for the timeframe for the entities and devices if entity_ids and device_ids: - json_quotable_entity_ids = list(entity_ids) - json_quotable_device_ids = list(device_ids) + json_quoted_entity_ids = [json.dumps(entity_id) for entity_id in entity_ids] + json_quoted_device_ids = [json.dumps(device_id) for device_id in device_ids] return entities_devices_stmt( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, + json_quoted_entity_ids, + json_quoted_device_ids, ) # entities: logbook sends everything for the timeframe for the entities if entity_ids: - json_quotable_entity_ids = list(entity_ids) + json_quoted_entity_ids = [json.dumps(entity_id) for entity_id in entity_ids] return entities_stmt( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, + json_quoted_entity_ids, ) # devices: logbook sends everything for the timeframe for the devices assert device_ids is not None - json_quotable_device_ids = list(device_ids) + json_quoted_device_ids = [json.dumps(device_id) for device_id in device_ids] return devices_stmt( start_day, end_day, event_types, - json_quotable_device_ids, + json_quoted_device_ids, ) diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py index d321578f545..730b66eef52 100644 --- a/homeassistant/components/logbook/queries/all.py +++ b/homeassistant/components/logbook/queries/all.py @@ -3,10 +3,9 @@ from __future__ import annotations from datetime import datetime as dt -from sqlalchemy import lambda_stmt from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList -from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select from homeassistant.components.recorder.models import LAST_UPDATED_INDEX, Events, States @@ -25,32 +24,29 @@ def all_stmt( states_entity_filter: ClauseList | None = None, events_entity_filter: ClauseList | None = None, context_id: str | None = None, -) -> StatementLambdaElement: +) -> Select: """Generate a logbook query for all entities.""" - stmt = lambda_stmt( - lambda: select_events_without_states(start_day, end_day, event_types) - ) + stmt = select_events_without_states(start_day, end_day, event_types) if context_id is not None: # Once all the old `state_changed` events # are gone from the database remove the # _legacy_select_events_context_id() - stmt += lambda s: s.where(Events.context_id == context_id).union_all( + stmt = stmt.where(Events.context_id == context_id).union_all( _states_query_for_context_id(start_day, end_day, context_id), legacy_select_events_context_id(start_day, end_day, context_id), ) else: if events_entity_filter is not None: - stmt += lambda s: s.where(events_entity_filter) + stmt = stmt.where(events_entity_filter) if states_entity_filter is not None: - stmt += lambda s: s.union_all( + stmt = stmt.union_all( _states_query_for_all(start_day, end_day).where(states_entity_filter) ) else: - stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) + stmt = stmt.union_all(_states_query_for_all(start_day, end_day)) - stmt += lambda s: s.order_by(Events.time_fired) - return stmt + return stmt.order_by(Events.time_fired) def _states_query_for_all(start_day: dt, end_day: dt) -> Query: diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index 88e9f50a42c..4c09720348f 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -4,11 +4,10 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt -from sqlalchemy import lambda_stmt, select +from sqlalchemy import select from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import CTE, CompoundSelect +from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select from homeassistant.components.recorder.models import ( DEVICE_ID_IN_EVENT, @@ -31,11 +30,11 @@ def _select_device_id_context_ids_sub_query( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quotable_device_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple devices.""" inner = select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quotable_device_ids) + apply_event_device_id_matchers(json_quoted_device_ids) ) return select(inner.c.context_id).group_by(inner.c.context_id) @@ -45,14 +44,14 @@ def _apply_devices_context_union( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quotable_device_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: """Generate a CTE to find the device context ids and a query to find linked row.""" devices_cte: CTE = _select_device_id_context_ids_sub_query( start_day, end_day, event_types, - json_quotable_device_ids, + json_quoted_device_ids, ).cte() return query.union_all( apply_events_context_hints( @@ -72,25 +71,22 @@ def devices_stmt( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quotable_device_ids: list[str], -) -> StatementLambdaElement: + json_quoted_device_ids: list[str], +) -> Select: """Generate a logbook query for multiple devices.""" - stmt = lambda_stmt( - lambda: _apply_devices_context_union( - select_events_without_states(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quotable_device_ids) - ), - start_day, - end_day, - event_types, - json_quotable_device_ids, - ).order_by(Events.time_fired) - ) - return stmt + return _apply_devices_context_union( + select_events_without_states(start_day, end_day, event_types).where( + apply_event_device_id_matchers(json_quoted_device_ids) + ), + start_day, + end_day, + event_types, + json_quoted_device_ids, + ).order_by(Events.time_fired) def apply_event_device_id_matchers( - json_quotable_device_ids: Iterable[str], + json_quoted_device_ids: Iterable[str], ) -> ClauseList: """Create matchers for the device_ids in the event_data.""" - return DEVICE_ID_IN_EVENT.in_(json_quotable_device_ids) + return DEVICE_ID_IN_EVENT.in_(json_quoted_device_ids) diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 8de4a5eaf64..a13a0f154e6 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -5,10 +5,9 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy import select, union_all from sqlalchemy.orm import Query -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import CTE, CompoundSelect +from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select from homeassistant.components.recorder.models import ( ENTITY_ID_IN_EVENT, @@ -36,12 +35,12 @@ def _select_entities_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], + json_quoted_entity_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities.""" union = union_all( select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) + apply_event_entity_id_matchers(json_quoted_entity_ids) ), apply_entities_hints(select(States.context_id)) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) @@ -56,7 +55,7 @@ def _apply_entities_context_union( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], + json_quoted_entity_ids: list[str], ) -> CompoundSelect: """Generate a CTE to find the entity and device context ids and a query to find linked row.""" entities_cte: CTE = _select_entities_context_ids_sub_query( @@ -64,7 +63,7 @@ def _apply_entities_context_union( end_day, event_types, entity_ids, - json_quotable_entity_ids, + json_quoted_entity_ids, ).cte() # We used to optimize this to exclude rows we already in the union with # a States.entity_id.not_in(entity_ids) but that made the @@ -91,21 +90,19 @@ def entities_stmt( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], -) -> StatementLambdaElement: + json_quoted_entity_ids: list[str], +) -> Select: """Generate a logbook query for multiple entities.""" - return lambda_stmt( - lambda: _apply_entities_context_union( - select_events_without_states(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) - ), - start_day, - end_day, - event_types, - entity_ids, - json_quotable_entity_ids, - ).order_by(Events.time_fired) - ) + return _apply_entities_context_union( + select_events_without_states(start_day, end_day, event_types).where( + apply_event_entity_id_matchers(json_quoted_entity_ids) + ), + start_day, + end_day, + event_types, + entity_ids, + json_quoted_entity_ids, + ).order_by(Events.time_fired) def states_query_for_entity_ids( @@ -118,12 +115,12 @@ def states_query_for_entity_ids( def apply_event_entity_id_matchers( - json_quotable_entity_ids: Iterable[str], + json_quoted_entity_ids: Iterable[str], ) -> sqlalchemy.or_: """Create matchers for the entity_id in the event_data.""" - return ENTITY_ID_IN_EVENT.in_( - json_quotable_entity_ids - ) | OLD_ENTITY_ID_IN_EVENT.in_(json_quotable_entity_ids) + return ENTITY_ID_IN_EVENT.in_(json_quoted_entity_ids) | OLD_ENTITY_ID_IN_EVENT.in_( + json_quoted_entity_ids + ) def apply_entities_hints(query: Query) -> Query: diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index 1c4271422b7..7514074cc85 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -5,10 +5,9 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy import select, union_all from sqlalchemy.orm import Query -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import CTE, CompoundSelect +from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select from homeassistant.components.recorder.models import EventData, Events, States @@ -33,14 +32,14 @@ def _select_entities_device_id_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities and multiple devices.""" union = union_all( select_events_context_id_subquery(start_day, end_day, event_types).where( _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids + json_quoted_entity_ids, json_quoted_device_ids ) ), apply_entities_hints(select(States.context_id)) @@ -56,16 +55,16 @@ def _apply_entities_devices_context_union( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: devices_entities_cte: CTE = _select_entities_device_id_context_ids_sub_query( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, + json_quoted_entity_ids, + json_quoted_device_ids, ).cte() # We used to optimize this to exclude rows we already in the union with # a States.entity_id.not_in(entity_ids) but that made the @@ -92,32 +91,30 @@ def entities_devices_stmt( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], -) -> StatementLambdaElement: + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], +) -> Select: """Generate a logbook query for multiple entities.""" - stmt = lambda_stmt( - lambda: _apply_entities_devices_context_union( - select_events_without_states(start_day, end_day, event_types).where( - _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids - ) - ), - start_day, - end_day, - event_types, - entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, - ).order_by(Events.time_fired) - ) + stmt = _apply_entities_devices_context_union( + select_events_without_states(start_day, end_day, event_types).where( + _apply_event_entity_id_device_id_matchers( + json_quoted_entity_ids, json_quoted_device_ids + ) + ), + start_day, + end_day, + event_types, + entity_ids, + json_quoted_entity_ids, + json_quoted_device_ids, + ).order_by(Events.time_fired) return stmt def _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids: Iterable[str], json_quotable_device_ids: Iterable[str] + json_quoted_entity_ids: Iterable[str], json_quoted_device_ids: Iterable[str] ) -> sqlalchemy.or_: """Create matchers for the device_id and entity_id in the event_data.""" return apply_event_entity_id_matchers( - json_quotable_entity_ids - ) | apply_event_device_id_matchers(json_quotable_device_ids) + json_quoted_entity_ids + ) | apply_event_device_id_matchers(json_quoted_device_ids) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 37285f66d1d..b3fff62ae03 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -9,13 +9,11 @@ import logging import time from typing import Any, cast -from sqlalchemy import Column, Text, and_, func, lambda_stmt, or_, select +from sqlalchemy import Column, Text, and_, func, or_, select from sqlalchemy.engine.row import Row -from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Subquery +from sqlalchemy.sql.selectable import Select, Subquery from homeassistant.components import recorder from homeassistant.components.websocket_api.const import ( @@ -36,7 +34,7 @@ from .models import ( process_timestamp_to_utc_isoformat, row_to_compressed_state, ) -from .util import execute_stmt_lambda_element, session_scope +from .util import execute_stmt, session_scope # mypy: allow-untyped-defs, no-check-untyped-defs @@ -116,22 +114,18 @@ def _schema_version(hass: HomeAssistant) -> int: return recorder.get_instance(hass).schema_version -def lambda_stmt_and_join_attributes( +def stmt_and_join_attributes( schema_version: int, no_attributes: bool, include_last_changed: bool = True -) -> tuple[StatementLambdaElement, bool]: - """Return the lambda_stmt and if StateAttributes should be joined. - - Because these are lambda_stmt the values inside the lambdas need - to be explicitly written out to avoid caching the wrong values. - """ +) -> tuple[Select, bool]: + """Return the stmt and if StateAttributes should be joined.""" # If no_attributes was requested we do the query # without the attributes fields and do not join the # state_attributes table if no_attributes: if include_last_changed: - return lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR)), False + return select(*QUERY_STATE_NO_ATTR), False return ( - lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), + select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED), False, ) # If we in the process of migrating schema we do @@ -140,19 +134,19 @@ def lambda_stmt_and_join_attributes( if schema_version < 25: if include_last_changed: return ( - lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25)), + select(*QUERY_STATES_PRE_SCHEMA_25), False, ) return ( - lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), + select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED), False, ) # Finally if no migration is in progress and no_attributes # was not requested, we query both attributes columns and # join state_attributes if include_last_changed: - return lambda_stmt(lambda: select(*QUERY_STATES)), True - return lambda_stmt(lambda: select(*QUERY_STATES_NO_LAST_CHANGED)), True + return select(*QUERY_STATES), True + return select(*QUERY_STATES_NO_LAST_CHANGED), True def get_significant_states( @@ -184,7 +178,7 @@ def get_significant_states( ) -def _ignore_domains_filter(query: Query) -> Query: +def _ignore_domains_filter(query: Select) -> Select: """Add a filter to ignore domains we do not fetch history for.""" return query.filter( and_( @@ -204,9 +198,9 @@ def _significant_states_stmt( filters: Filters | None, significant_changes_only: bool, no_attributes: bool, -) -> StatementLambdaElement: +) -> Select: """Query the database for significant state changes.""" - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=not significant_changes_only ) if ( @@ -215,11 +209,11 @@ def _significant_states_stmt( and significant_changes_only and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS ): - stmt += lambda q: q.filter( + stmt = stmt.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) ) elif significant_changes_only: - stmt += lambda q: q.filter( + stmt = stmt.filter( or_( *[ States.entity_id.like(entity_domain) @@ -233,25 +227,22 @@ def _significant_states_stmt( ) if entity_ids: - stmt += lambda q: q.filter(States.entity_id.in_(entity_ids)) + stmt = stmt.filter(States.entity_id.in_(entity_ids)) else: - stmt += _ignore_domains_filter + stmt = _ignore_domains_filter(stmt) if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt = stmt.add_criteria( - lambda q: q.filter(entity_filter), track_on=[filters] - ) + stmt = stmt.filter(entity_filter) - stmt += lambda q: q.filter(States.last_updated > start_time) + stmt = stmt.filter(States.last_updated > start_time) if end_time: - stmt += lambda q: q.filter(States.last_updated < end_time) + stmt = stmt.filter(States.last_updated < end_time) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - stmt += lambda q: q.order_by(States.entity_id, States.last_updated) - return stmt + return stmt.order_by(States.entity_id, States.last_updated) def get_significant_states_with_session( @@ -288,9 +279,7 @@ def get_significant_states_with_session( significant_changes_only, no_attributes, ) - states = execute_stmt_lambda_element( - session, stmt, None if entity_ids else start_time, end_time - ) + states = execute_stmt(session, stmt, None if entity_ids else start_time, end_time) return _sorted_states_to_dict( hass, session, @@ -342,28 +331,28 @@ def _state_changed_during_period_stmt( no_attributes: bool, descending: bool, limit: int | None, -) -> StatementLambdaElement: - stmt, join_attributes = lambda_stmt_and_join_attributes( +) -> Select: + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=False ) - stmt += lambda q: q.filter( + stmt = stmt.filter( ((States.last_changed == States.last_updated) | States.last_changed.is_(None)) & (States.last_updated > start_time) ) if end_time: - stmt += lambda q: q.filter(States.last_updated < end_time) + stmt = stmt.filter(States.last_updated < end_time) if entity_id: - stmt += lambda q: q.filter(States.entity_id == entity_id) + stmt = stmt.filter(States.entity_id == entity_id) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) if descending: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()) + stmt = stmt.order_by(States.entity_id, States.last_updated.desc()) else: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated) + stmt = stmt.order_by(States.entity_id, States.last_updated) if limit: - stmt += lambda q: q.limit(limit) + stmt = stmt.limit(limit) return stmt @@ -391,7 +380,7 @@ def state_changes_during_period( descending, limit, ) - states = execute_stmt_lambda_element( + states = execute_stmt( session, stmt, None if entity_id else start_time, end_time ) return cast( @@ -409,23 +398,22 @@ def state_changes_during_period( def _get_last_state_changes_stmt( schema_version: int, number_of_states: int, entity_id: str | None -) -> StatementLambdaElement: - stmt, join_attributes = lambda_stmt_and_join_attributes( +) -> Select: + stmt, join_attributes = stmt_and_join_attributes( schema_version, False, include_last_changed=False ) - stmt += lambda q: q.filter( + stmt = stmt.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) ) if entity_id: - stmt += lambda q: q.filter(States.entity_id == entity_id) + stmt = stmt.filter(States.entity_id == entity_id) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()).limit( + return stmt.order_by(States.entity_id, States.last_updated.desc()).limit( number_of_states ) - return stmt def get_last_state_changes( @@ -440,7 +428,7 @@ def get_last_state_changes( stmt = _get_last_state_changes_stmt( _schema_version(hass), number_of_states, entity_id ) - states = list(execute_stmt_lambda_element(session, stmt)) + states = list(execute_stmt(session, stmt)) return cast( MutableMapping[str, list[State]], _sorted_states_to_dict( @@ -460,14 +448,14 @@ def _get_states_for_entites_stmt( utc_point_in_time: datetime, entity_ids: list[str], no_attributes: bool, -) -> StatementLambdaElement: +) -> Select: """Baked query to get states for specific entities.""" - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) # We got an include-list of entities, accelerate the query by filtering already # in the inner query. - stmt += lambda q: q.where( + stmt = stmt.where( States.state_id == ( select(func.max(States.state_id).label("max_state_id")) @@ -481,7 +469,7 @@ def _get_states_for_entites_stmt( ).c.max_state_id ) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) return stmt @@ -512,9 +500,9 @@ def _get_states_for_all_stmt( utc_point_in_time: datetime, filters: Filters | None, no_attributes: bool, -) -> StatementLambdaElement: +) -> Select: """Baked query to get states for all entities.""" - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) # We did not get an include-list of entities, query all states in the inner @@ -524,7 +512,7 @@ def _get_states_for_all_stmt( most_recent_states_by_date = _generate_most_recent_states_by_date( run_start, utc_point_in_time ) - stmt += lambda q: q.where( + stmt = stmt.where( States.state_id == ( select(func.max(States.state_id).label("max_state_id")) @@ -540,12 +528,12 @@ def _get_states_for_all_stmt( .subquery() ).c.max_state_id, ) - stmt += _ignore_domains_filter + stmt = _ignore_domains_filter(stmt) if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt = stmt.add_criteria(lambda q: q.filter(entity_filter), track_on=[filters]) + stmt = stmt.filter(entity_filter) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) return stmt @@ -563,7 +551,7 @@ def _get_rows_with_session( """Return the states at a specific point in time.""" schema_version = _schema_version(hass) if entity_ids and len(entity_ids) == 1: - return execute_stmt_lambda_element( + return execute_stmt( session, _get_single_entity_states_stmt( schema_version, utc_point_in_time, entity_ids[0], no_attributes @@ -588,7 +576,7 @@ def _get_rows_with_session( schema_version, run.start, utc_point_in_time, filters, no_attributes ) - return execute_stmt_lambda_element(session, stmt) + return execute_stmt(session, stmt) def _get_single_entity_states_stmt( @@ -596,14 +584,14 @@ def _get_single_entity_states_stmt( utc_point_in_time: datetime, entity_id: str, no_attributes: bool = False, -) -> StatementLambdaElement: +) -> Select: # Use an entirely different (and extremely fast) query if we only # have a single entity id - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) - stmt += ( - lambda q: q.filter( + stmt = ( + stmt.filter( States.last_updated < utc_point_in_time, States.entity_id == entity_id, ) @@ -611,7 +599,7 @@ def _get_single_entity_states_stmt( .limit(1) ) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) return stmt diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 012b34ec0ef..9a52a36ab5f 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -14,13 +14,12 @@ import re from statistics import mean from typing import TYPE_CHECKING, Any, Literal, overload -from sqlalchemy import bindparam, func, lambda_stmt, select +from sqlalchemy import bindparam, func, select from sqlalchemy.engine.row import Row from sqlalchemy.exc import SQLAlchemyError, StatementError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal_column, true -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Subquery +from sqlalchemy.sql.selectable import Select, Subquery import voluptuous as vol from homeassistant.const import ( @@ -53,12 +52,7 @@ from .models import ( process_timestamp, process_timestamp_to_utc_isoformat, ) -from .util import ( - execute, - execute_stmt_lambda_element, - retryable_database_job, - session_scope, -) +from .util import execute, execute_stmt, retryable_database_job, session_scope if TYPE_CHECKING: from . import Recorder @@ -483,10 +477,10 @@ def delete_statistics_meta_duplicates(session: Session) -> None: def _compile_hourly_statistics_summary_mean_stmt( start_time: datetime, end_time: datetime -) -> StatementLambdaElement: +) -> Select: """Generate the summary mean statement for hourly statistics.""" - return lambda_stmt( - lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN) + return ( + select(*QUERY_STATISTICS_SUMMARY_MEAN) .filter(StatisticsShortTerm.start >= start_time) .filter(StatisticsShortTerm.start < end_time) .group_by(StatisticsShortTerm.metadata_id) @@ -509,7 +503,7 @@ def compile_hourly_statistics( # Compute last hour's average, min, max summary: dict[str, StatisticData] = {} stmt = _compile_hourly_statistics_summary_mean_stmt(start_time, end_time) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if stats: for stat in stats: @@ -691,17 +685,17 @@ def _generate_get_metadata_stmt( statistic_ids: list[str] | tuple[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, statistic_source: str | None = None, -) -> StatementLambdaElement: +) -> Select: """Generate a statement to fetch metadata.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTIC_META)) + stmt = select(*QUERY_STATISTIC_META) if statistic_ids is not None: - stmt += lambda q: q.where(StatisticsMeta.statistic_id.in_(statistic_ids)) + stmt = stmt.where(StatisticsMeta.statistic_id.in_(statistic_ids)) if statistic_source is not None: - stmt += lambda q: q.where(StatisticsMeta.source == statistic_source) + stmt = stmt.where(StatisticsMeta.source == statistic_source) if statistic_type == "mean": - stmt += lambda q: q.where(StatisticsMeta.has_mean == true()) + stmt = stmt.where(StatisticsMeta.has_mean == true()) elif statistic_type == "sum": - stmt += lambda q: q.where(StatisticsMeta.has_sum == true()) + stmt = stmt.where(StatisticsMeta.has_sum == true()) return stmt @@ -723,7 +717,7 @@ def get_metadata_with_session( # Fetch metatadata from the database stmt = _generate_get_metadata_stmt(statistic_ids, statistic_type, statistic_source) - result = execute_stmt_lambda_element(session, stmt) + result = execute_stmt(session, stmt) if not result: return {} @@ -985,44 +979,30 @@ def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, -) -> StatementLambdaElement: - """Prepare a database query for statistics during a given period. - - This prepares a lambda_stmt query, so we don't insert the parameters yet. - """ - stmt = lambda_stmt( - lambda: select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) - ) +) -> Select: + """Prepare a database query for statistics during a given period.""" + stmt = select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) if end_time is not None: - stmt += lambda q: q.filter(Statistics.start < end_time) + stmt = stmt.filter(Statistics.start < end_time) if metadata_ids: - stmt += lambda q: q.filter(Statistics.metadata_id.in_(metadata_ids)) - stmt += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) - return stmt + stmt = stmt.filter(Statistics.metadata_id.in_(metadata_ids)) + return stmt.order_by(Statistics.metadata_id, Statistics.start) def _statistics_during_period_stmt_short_term( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, -) -> StatementLambdaElement: - """Prepare a database query for short term statistics during a given period. - - This prepares a lambda_stmt query, so we don't insert the parameters yet. - """ - stmt = lambda_stmt( - lambda: select(*QUERY_STATISTICS_SHORT_TERM).filter( - StatisticsShortTerm.start >= start_time - ) +) -> Select: + """Prepare a database query for short term statistics during a given period.""" + stmt = select(*QUERY_STATISTICS_SHORT_TERM).filter( + StatisticsShortTerm.start >= start_time ) if end_time is not None: - stmt += lambda q: q.filter(StatisticsShortTerm.start < end_time) + stmt = stmt.filter(StatisticsShortTerm.start < end_time) if metadata_ids: - stmt += lambda q: q.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) - stmt += lambda q: q.order_by( - StatisticsShortTerm.metadata_id, StatisticsShortTerm.start - ) - return stmt + stmt = stmt.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) + return stmt.order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start) def statistics_during_period( @@ -1057,7 +1037,7 @@ def statistics_during_period( else: table = Statistics stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if not stats: return {} @@ -1088,10 +1068,10 @@ def statistics_during_period( def _get_last_statistics_stmt( metadata_id: int, number_of_stats: int, -) -> StatementLambdaElement: +) -> Select: """Generate a statement for number_of_stats statistics for a given statistic_id.""" - return lambda_stmt( - lambda: select(*QUERY_STATISTICS) + return ( + select(*QUERY_STATISTICS) .filter_by(metadata_id=metadata_id) .order_by(Statistics.metadata_id, Statistics.start.desc()) .limit(number_of_stats) @@ -1101,10 +1081,10 @@ def _get_last_statistics_stmt( def _get_last_statistics_short_term_stmt( metadata_id: int, number_of_stats: int, -) -> StatementLambdaElement: +) -> Select: """Generate a statement for number_of_stats short term statistics for a given statistic_id.""" - return lambda_stmt( - lambda: select(*QUERY_STATISTICS_SHORT_TERM) + return ( + select(*QUERY_STATISTICS_SHORT_TERM) .filter_by(metadata_id=metadata_id) .order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc()) .limit(number_of_stats) @@ -1130,7 +1110,7 @@ def _get_last_statistics( stmt = _get_last_statistics_stmt(metadata_id, number_of_stats) else: stmt = _get_last_statistics_short_term_stmt(metadata_id, number_of_stats) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if not stats: return {} @@ -1180,11 +1160,11 @@ def _generate_most_recent_statistic_row(metadata_ids: list[int]) -> Subquery: def _latest_short_term_statistics_stmt( metadata_ids: list[int], -) -> StatementLambdaElement: +) -> Select: """Create the statement for finding the latest short term stat rows.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) + stmt = select(*QUERY_STATISTICS_SHORT_TERM) most_recent_statistic_row = _generate_most_recent_statistic_row(metadata_ids) - stmt += lambda s: s.join( + return stmt.join( most_recent_statistic_row, ( StatisticsShortTerm.metadata_id # pylint: disable=comparison-with-callable @@ -1192,7 +1172,6 @@ def _latest_short_term_statistics_stmt( ) & (StatisticsShortTerm.start == most_recent_statistic_row.c.start_max), ) - return stmt def get_latest_short_term_statistics( @@ -1215,7 +1194,7 @@ def get_latest_short_term_statistics( if statistic_id in metadata ] stmt = _latest_short_term_statistics_stmt(metadata_ids) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if not stats: return {} diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 843f0e4b185..9c8a0c1eae3 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -22,7 +22,6 @@ from sqlalchemy.engine.row import Row from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session -from sqlalchemy.sql.lambdas import StatementLambdaElement from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant @@ -167,9 +166,9 @@ def execute( assert False # unreachable # pragma: no cover -def execute_stmt_lambda_element( +def execute_stmt( session: Session, - stmt: StatementLambdaElement, + query: Query, start_time: datetime | None = None, end_time: datetime | None = None, yield_per: int | None = DEFAULT_YIELD_STATES_ROWS, @@ -185,11 +184,12 @@ def execute_stmt_lambda_element( specific entities) since they are usually faster with .all(). """ - executed = session.execute(stmt) use_all = not start_time or ((end_time or dt_util.utcnow()) - start_time).days <= 1 for tryno in range(0, RETRIES): try: - return executed.all() if use_all else executed.yield_per(yield_per) # type: ignore[no-any-return] + if use_all: + return session.execute(query).all() # type: ignore[no-any-return] + return session.execute(query).yield_per(yield_per) # type: ignore[no-any-return] except SQLAlchemyError as err: _LOGGER.error("Error executing query: %s", err) if tryno == RETRIES - 1: diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 343c57045cf..6ac6aabd023 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -9,7 +9,6 @@ from sqlalchemy import text from sqlalchemy.engine.result import ChunkedIteratorResult from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.sql.elements import TextClause -from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util @@ -712,8 +711,8 @@ def test_build_mysqldb_conv(): @patch("homeassistant.components.recorder.util.QUERY_RETRY_WAIT", 0) -def test_execute_stmt_lambda_element(hass_recorder): - """Test executing with execute_stmt_lambda_element.""" +def test_execute_stmt(hass_recorder): + """Test executing with execute_stmt.""" hass = hass_recorder() instance = recorder.get_instance(hass) hass.states.set("sensor.on", "on") @@ -724,13 +723,15 @@ def test_execute_stmt_lambda_element(hass_recorder): one_week_from_now = now + timedelta(days=7) class MockExecutor: + + _calls = 0 + def __init__(self, stmt): - assert isinstance(stmt, StatementLambdaElement) - self.calls = 0 + """Init the mock.""" def all(self): - self.calls += 1 - if self.calls == 2: + MockExecutor._calls += 1 + if MockExecutor._calls == 2: return ["mock_row"] raise SQLAlchemyError @@ -739,24 +740,24 @@ def test_execute_stmt_lambda_element(hass_recorder): stmt = history._get_single_entity_states_stmt( instance.schema_version, dt_util.utcnow(), "sensor.on", False ) - rows = util.execute_stmt_lambda_element(session, stmt) + rows = util.execute_stmt(session, stmt) assert isinstance(rows, list) assert rows[0].state == new_state.state assert rows[0].entity_id == new_state.entity_id # Time window >= 2 days, we get a ChunkedIteratorResult - rows = util.execute_stmt_lambda_element(session, stmt, now, one_week_from_now) + rows = util.execute_stmt(session, stmt, now, one_week_from_now) assert isinstance(rows, ChunkedIteratorResult) row = next(rows) assert row.state == new_state.state assert row.entity_id == new_state.entity_id # Time window < 2 days, we get a list - rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) + rows = util.execute_stmt(session, stmt, now, tomorrow) assert isinstance(rows, list) assert rows[0].state == new_state.state assert rows[0].entity_id == new_state.entity_id with patch.object(session, "execute", MockExecutor): - rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) + rows = util.execute_stmt(session, stmt, now, tomorrow) assert rows == ["mock_row"] From 2ee4cd02c7fe0c3cfaa8e0c2fc7b4ea5a7c3c384 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Jun 2022 16:16:30 -0700 Subject: [PATCH 058/107] Bumped version to 2022.6.4 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4287c7e96f8..7d887d1a4d7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index a16b51c03d8..ed1fdb27e4f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.3 +version = 2022.6.4 url = https://www.home-assistant.io/ [options] From 8cf6f501931640cd8c69143f81a2323ab2d60c24 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 8 Jun 2022 10:52:01 +0200 Subject: [PATCH 059/107] Ensure netgear devices are tracked with one enabled config entry (#72969) Co-authored-by: Martin Hjelmare --- homeassistant/components/netgear/__init__.py | 7 +++---- homeassistant/components/netgear/router.py | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 7a4d0e7a8cd..679a93f8da1 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -18,7 +18,6 @@ from .const import ( KEY_COORDINATOR_SPEED, KEY_COORDINATOR_TRAFFIC, KEY_ROUTER, - MODE_ROUTER, PLATFORMS, ) from .errors import CannotLoginException @@ -72,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_devices() -> bool: """Fetch data from the router.""" - if router.mode == MODE_ROUTER: + if router.track_devices: return await router.async_update_device_trackers() return False @@ -107,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=SPEED_TEST_INTERVAL, ) - if router.mode == MODE_ROUTER: + if router.track_devices: await coordinator.async_config_entry_first_refresh() await coordinator_traffic_meter.async_config_entry_first_refresh() @@ -134,7 +133,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) - if router.mode != MODE_ROUTER: + if not router.track_devices: router_id = None # Remove devices that are no longer tracked device_registry = dr.async_get(hass) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 301906f22b6..67e573d0e92 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -80,6 +80,7 @@ class NetgearRouter: self.hardware_version = "" self.serial_number = "" + self.track_devices = True self.method_version = 1 consider_home_int = entry.options.get( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() @@ -112,11 +113,23 @@ class NetgearRouter: self.serial_number = self._info["SerialNumber"] self.mode = self._info.get("DeviceMode", MODE_ROUTER) + enabled_entries = [ + entry + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.disabled_by is None + ] + self.track_devices = self.mode == MODE_ROUTER or len(enabled_entries) == 1 + _LOGGER.debug( + "Netgear track_devices = '%s', device mode '%s'", + self.track_devices, + self.mode, + ) + for model in MODELS_V2: if self.model.startswith(model): self.method_version = 2 - if self.method_version == 2 and self.mode == MODE_ROUTER: + if self.method_version == 2 and self.track_devices: if not self._api.get_attached_devices_2(): _LOGGER.error( "Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2", @@ -133,7 +146,7 @@ class NetgearRouter: return False # set already known devices to away instead of unavailable - if self.mode == MODE_ROUTER: + if self.track_devices: device_registry = dr.async_get(self.hass) devices = dr.async_entries_for_config_entry(device_registry, self.entry_id) for device_entry in devices: From 498da3bba3da12c3d2291583d648c722e51ff865 Mon Sep 17 00:00:00 2001 From: Matrix Date: Wed, 8 Jun 2022 14:11:41 +0800 Subject: [PATCH 060/107] Bump yolink-api to 0.0.8 (#73173) * update api libray fix hearbeat message valiation * update yolink-api ignore invalidate message --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 7fb78a4974b..d2a02a44c42 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.6"], + "requirements": ["yolink-api==0.0.8"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index d0ac6af8c74..63212e94b4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.6 +yolink-api==0.0.8 # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24916a4ac0d..1d4479f9d77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1638,7 +1638,7 @@ yalexs==1.1.25 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.6 +yolink-api==0.0.8 # homeassistant.components.youless youless-api==0.16 From a24463f7ea2414672d47f20b35ca6ca6f1c15cd5 Mon Sep 17 00:00:00 2001 From: d0nni3q84 <62199227+d0nni3q84@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:32:01 -0500 Subject: [PATCH 061/107] Fix Feedreader Atom feeds using `updated` date (#73208) * Feedreader: Properly support Atom feeds that use only the `updated` date format and resolve #73207. * Revert "Feedreader: Properly support Atom feeds that use only the `updated` date format and resolve #73207." This reverts commit 4dbd11ee04b4e8f935a22dfb51405b7bdaaba676. * Properly support Atom feeds that use only the `updated` date format and resolve #73207. * Revert "Properly support Atom feeds that use only the `updated` date format and resolve #73207." This reverts commit 14366c6a2491584282b8bb96fe3779fd41849897. * Properly support Atom feeds that use only the `updated` date format and resolve #73207. --- .../components/feedreader/__init__.py | 31 ++++++++++++++---- tests/components/feedreader/test_init.py | 32 ++++++++++++++++++- tests/fixtures/feedreader5.xml | 18 +++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/feedreader5.xml diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index a4cd546aa16..d06aeec4932 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -70,6 +70,7 @@ class FeedManager: self._last_entry_timestamp = None self._last_update_successful = False self._has_published_parsed = False + self._has_updated_parsed = False self._event_type = EVENT_FEEDREADER self._feed_id = url hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: self._update()) @@ -122,7 +123,7 @@ class FeedManager: ) self._filter_entries() self._publish_new_entries() - if self._has_published_parsed: + if self._has_published_parsed or self._has_updated_parsed: self._storage.put_timestamp( self._feed_id, self._last_entry_timestamp ) @@ -143,7 +144,7 @@ class FeedManager: def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" - # Check if the entry has a published date. + # Check if the entry has a published or updated date. if "published_parsed" in entry and entry.published_parsed: # We are lucky, `published_parsed` data available, let's make use of # it to publish only new available entries since the last run @@ -151,9 +152,20 @@ class FeedManager: self._last_entry_timestamp = max( entry.published_parsed, self._last_entry_timestamp ) + elif "updated_parsed" in entry and entry.updated_parsed: + # We are lucky, `updated_parsed` data available, let's make use of + # it to publish only new available entries since the last run + self._has_updated_parsed = True + self._last_entry_timestamp = max( + entry.updated_parsed, self._last_entry_timestamp + ) else: self._has_published_parsed = False - _LOGGER.debug("No published_parsed info available for entry %s", entry) + self._has_updated_parsed = False + _LOGGER.debug( + "No published_parsed or updated_parsed info available for entry %s", + entry, + ) entry.update({"feed_url": self._url}) self._hass.bus.fire(self._event_type, entry) @@ -167,9 +179,16 @@ class FeedManager: # Set last entry timestamp as epoch time if not available self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple() for entry in self._feed.entries: - if self._firstrun or ( - "published_parsed" in entry - and entry.published_parsed > self._last_entry_timestamp + if ( + self._firstrun + or ( + "published_parsed" in entry + and entry.published_parsed > self._last_entry_timestamp + ) + or ( + "updated_parsed" in entry + and entry.updated_parsed > self._last_entry_timestamp + ) ): self._update_and_fire_entry(entry) new_entries = True diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 34f27c36a6c..be5ebb42a6d 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -23,6 +23,7 @@ VALID_CONFIG_1 = {feedreader.DOMAIN: {CONF_URLS: [URL]}} VALID_CONFIG_2 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_SCAN_INTERVAL: 60}} VALID_CONFIG_3 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 100}} VALID_CONFIG_4 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 5}} +VALID_CONFIG_5 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 1}} def load_fixture_bytes(src): @@ -56,6 +57,12 @@ def fixture_feed_three_events(hass): return load_fixture_bytes("feedreader3.xml") +@pytest.fixture(name="feed_atom_event") +def fixture_feed_atom_event(hass): + """Load test feed data for atom event.""" + return load_fixture_bytes("feedreader5.xml") + + @pytest.fixture(name="events") async def fixture_events(hass): """Fixture that catches alexa events.""" @@ -98,7 +105,7 @@ async def test_setup_max_entries(hass): async def test_feed(hass, events, feed_one_event): - """Test simple feed with valid data.""" + """Test simple rss feed with valid data.""" with patch( "feedparser.http.get", return_value=feed_one_event, @@ -120,6 +127,29 @@ async def test_feed(hass, events, feed_one_event): assert events[0].data.published_parsed.tm_min == 10 +async def test_atom_feed(hass, events, feed_atom_event): + """Test simple atom feed with valid data.""" + with patch( + "feedparser.http.get", + return_value=feed_atom_event, + ): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_5) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data.title == "Atom-Powered Robots Run Amok" + assert events[0].data.description == "Some text." + assert events[0].data.link == "http://example.org/2003/12/13/atom03" + assert events[0].data.id == "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a" + assert events[0].data.updated_parsed.tm_year == 2003 + assert events[0].data.updated_parsed.tm_mon == 12 + assert events[0].data.updated_parsed.tm_mday == 13 + assert events[0].data.updated_parsed.tm_hour == 18 + assert events[0].data.updated_parsed.tm_min == 30 + + async def test_feed_updates(hass, events, feed_one_event, feed_two_event): """Test feed updates.""" side_effect = [ diff --git a/tests/fixtures/feedreader5.xml b/tests/fixtures/feedreader5.xml new file mode 100644 index 00000000000..d9b1dda1ad2 --- /dev/null +++ b/tests/fixtures/feedreader5.xml @@ -0,0 +1,18 @@ + + + Example Feed + + 2003-12-13T18:30:02Z + + John Doe + + urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 + + Atom-Powered Robots Run Amok + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2003-12-13T18:30:02Z + Some text. + + From 89dfd4b1624d8dd34d0f740d4696cfd4241a3fd6 Mon Sep 17 00:00:00 2001 From: Khole Date: Fri, 10 Jun 2022 04:54:24 +0100 Subject: [PATCH 062/107] Hive auth fix for users (#73247) --- homeassistant/components/hive/config_flow.py | 7 +++-- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hive/test_config_flow.py | 30 -------------------- 5 files changed, 8 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index c713a3011f4..90c78aefcbd 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -27,6 +27,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.data = {} self.tokens = {} self.entry = None + self.device_registration = False async def async_step_user(self, user_input=None): """Prompt user input. Create or edit entry.""" @@ -88,6 +89,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not errors: try: + self.device_registration = True return await self.async_setup_hive_entry() except UnknownHiveError: errors["base"] = "unknown" @@ -102,9 +104,10 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): raise UnknownHiveError # Setup the config entry - await self.hive_auth.device_registration("Home Assistant") + if self.device_registration: + await self.hive_auth.device_registration("Home Assistant") + self.data["device_data"] = await self.hive_auth.getDeviceData() self.data["tokens"] = self.tokens - self.data["device_data"] = await self.hive_auth.getDeviceData() if self.context["source"] == config_entries.SOURCE_REAUTH: self.hass.config_entries.async_update_entry( self.entry, title=self.data["username"], data=self.data diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index d8cd56abe0b..e0faa9e0f20 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.5"], + "requirements": ["pyhiveapi==0.5.9"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 63212e94b4c..7cb70b9e8f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.5 +pyhiveapi==0.5.9 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d4479f9d77..af361e4a6b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.5 +pyhiveapi==0.5.9 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index 51ceec43ad2..35e20e8eee3 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -33,16 +33,6 @@ async def test_import_flow(hass): "AccessToken": "mock-access-token", }, }, - ), patch( - "homeassistant.components.hive.config_flow.Auth.device_registration", - return_value=True, - ), patch( - "homeassistant.components.hive.config_flow.Auth.getDeviceData", - return_value=[ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -67,11 +57,6 @@ async def test_import_flow(hass): }, "ChallengeName": "SUCCESS", }, - "device_data": [ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], } assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -96,16 +81,6 @@ async def test_user_flow(hass): "AccessToken": "mock-access-token", }, }, - ), patch( - "homeassistant.components.hive.config_flow.Auth.device_registration", - return_value=True, - ), patch( - "homeassistant.components.hive.config_flow.Auth.getDeviceData", - return_value=[ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -130,11 +105,6 @@ async def test_user_flow(hass): }, "ChallengeName": "SUCCESS", }, - "device_data": [ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], } assert len(mock_setup.mock_calls) == 1 From db148b65e539bb87aba5d016fcf638f21b187219 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 9 Jun 2022 03:30:13 +0200 Subject: [PATCH 063/107] Fix handling of connection error during Synology DSM setup (#73248) * dont reload on conection error during setup * also fetch API errors during update --- .../components/synology_dsm/common.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index e27c7475251..2ca9cbf3ccf 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -from datetime import timedelta import logging from synology_dsm import SynologyDSM @@ -98,7 +97,7 @@ class SynoApi: self._async_setup_api_requests() await self._hass.async_add_executor_job(self._fetch_device_configuration) - await self.async_update() + await self.async_update(first_setup=True) @callback def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: @@ -251,7 +250,7 @@ class SynoApi: # ignore API errors during logout pass - async def async_update(self, now: timedelta | None = None) -> None: + async def async_update(self, first_setup: bool = False) -> None: """Update function for updating API information.""" LOGGER.debug("Start data update for '%s'", self._entry.unique_id) self._async_setup_api_requests() @@ -259,14 +258,22 @@ class SynoApi: await self._hass.async_add_executor_job( self.dsm.update, self._with_information ) - except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - LOGGER.warning( - "Connection error during update, fallback by reloading the entry" - ) + except ( + SynologyDSMLoginFailedException, + SynologyDSMRequestException, + SynologyDSMAPIErrorException, + ) as err: LOGGER.debug( "Connection error during update of '%s' with exception: %s", self._entry.unique_id, err, ) + + if first_setup: + raise err + + LOGGER.warning( + "Connection error during update, fallback by reloading the entry" + ) await self._hass.config_entries.async_reload(self._entry.entry_id) return From 44a4f4115f03f4bd4171047659699a1faba99a9d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 8 Jun 2022 16:31:39 -0600 Subject: [PATCH 064/107] Bump regenmaschine to 2022.06.1 (#73250) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index a61283ea298..e9df60e4697 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.06.0"], + "requirements": ["regenmaschine==2022.06.1"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 7cb70b9e8f6..81c66275128 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.06.0 +regenmaschine==2022.06.1 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af361e4a6b9..a6a1d9b0f81 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ rachiopy==1.0.3 radios==0.1.1 # homeassistant.components.rainmachine -regenmaschine==2022.06.0 +regenmaschine==2022.06.1 # homeassistant.components.renault renault-api==0.1.11 From e41cb1e02066e905600611e565d920a072e54a61 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 9 Jun 2022 13:48:39 +0200 Subject: [PATCH 065/107] Improve Netgear logging (#73274) * improve logging * fix black * invert checks --- homeassistant/components/netgear/sensor.py | 44 ++++++++++++++-------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 9dec1ab3390..a1cf134beda 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -5,6 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import date, datetime from decimal import Decimal +import logging from homeassistant.components.sensor import ( RestoreSensor, @@ -34,6 +35,8 @@ from .const import ( ) from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity +_LOGGER = logging.getLogger(__name__) + SENSOR_TYPES = { "type": SensorEntityDescription( key="type", @@ -114,7 +117,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewWeekUpload", @@ -123,7 +126,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewWeekDownload", @@ -132,7 +135,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewWeekDownload", @@ -141,7 +144,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewMonthUpload", @@ -150,7 +153,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewMonthUpload", @@ -159,7 +162,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewMonthDownload", @@ -168,7 +171,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewMonthDownload", @@ -177,7 +180,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewLastMonthUpload", @@ -186,7 +189,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewLastMonthUpload", @@ -195,7 +198,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewLastMonthDownload", @@ -204,7 +207,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewLastMonthDownload", @@ -213,7 +216,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), ] @@ -372,6 +375,17 @@ class NetgearRouterSensorEntity(NetgearRouterEntity, RestoreSensor): @callback def async_update_device(self) -> None: """Update the Netgear device.""" - if self.coordinator.data is not None: - data = self.coordinator.data.get(self.entity_description.key) - self._value = self.entity_description.value(data) + if self.coordinator.data is None: + return + + data = self.coordinator.data.get(self.entity_description.key) + if data is None: + self._value = None + _LOGGER.debug( + "key '%s' not in Netgear router response '%s'", + self.entity_description.key, + data, + ) + return + + self._value = self.entity_description.value(data) From 6dbe5942ed93d1b2a000154fc2444cd255de9ed3 Mon Sep 17 00:00:00 2001 From: Adam Dullage Date: Fri, 10 Jun 2022 05:37:36 +0100 Subject: [PATCH 066/107] Fix polling frequency for Starling integration (#73282) --- homeassistant/components/starlingbank/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index 40f1e0ff3fd..0069fe7a65f 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -1,6 +1,7 @@ """Support for balance data via the Starling Bank API.""" from __future__ import annotations +from datetime import timedelta import logging import requests @@ -26,6 +27,7 @@ DEFAULT_SANDBOX = False DEFAULT_ACCOUNT_NAME = "Starling" ICON = "mdi:currency-gbp" +SCAN_INTERVAL = timedelta(seconds=180) ACCOUNT_SCHEMA = vol.Schema( { From 1253f7f85d522a6250ed0b408f8bd738310a7e2a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Jun 2022 12:46:13 -0700 Subject: [PATCH 067/107] Fix reloading themes crashing if no themes configured (#73287) --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c1deb02fc6a..b3907143eb9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -460,7 +460,7 @@ async def _async_setup_themes( async def reload_themes(_: ServiceCall) -> None: """Reload themes.""" config = await async_hass_config_yaml(hass) - new_themes = config[DOMAIN].get(CONF_THEMES, {}) + new_themes = config.get(DOMAIN, {}).get(CONF_THEMES, {}) hass.data[DATA_THEMES] = new_themes if hass.data[DATA_DEFAULT_THEME] not in new_themes: hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME From 300d2a08818e571d343360b7ceb14096613dd1e8 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 7 Jun 2022 14:19:39 -0400 Subject: [PATCH 068/107] Bump version of pyunifiprotect to 3.9.0 (#73168) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/test_binary_sensor.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 898abd73a6f..52f69abea00 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.6.0", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.0", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 81c66275128..19bcba89a9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.6.0 +pyunifiprotect==3.9.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6a1d9b0f81..d26f67d8267 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1322,7 +1322,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.6.0 +pyunifiprotect==3.9.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 60a3d5a8126..88b42d36994 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -168,7 +168,7 @@ async def sensor_fixture( sensor_obj.motion_detected_at = now - timedelta(hours=1) sensor_obj.open_status_changed_at = now - timedelta(hours=1) sensor_obj.alarm_triggered_at = now - timedelta(hours=1) - sensor_obj.tampering_detected_at = now - timedelta(hours=1) + sensor_obj.tampering_detected_at = None mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] @@ -204,7 +204,7 @@ async def sensor_none_fixture( sensor_obj.mount_type = MountType.LEAK sensor_obj.battery_status.is_low = False sensor_obj.alarm_settings.is_enabled = False - sensor_obj.tampering_detected_at = now - timedelta(hours=1) + sensor_obj.tampering_detected_at = None mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] From 31a4c649ffd73063e35e1e6cd44ac8988da55005 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 8 Jun 2022 19:58:06 -0400 Subject: [PATCH 069/107] Bumps version of pyunifiprotect to 3.9.1 (#73252) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 52f69abea00..a27c0125da3 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.0", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.1", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 19bcba89a9e..9242102e78b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.0 +pyunifiprotect==3.9.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d26f67d8267..af615ce4e7e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1322,7 +1322,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.0 +pyunifiprotect==3.9.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 343c2672bbbe367248b133fa6cb7daf7a0fa5c9c Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 9 Jun 2022 23:32:16 -0400 Subject: [PATCH 070/107] Bumps version of pyunifiprotect to 3.9.2 to fix compat with protect 2.1.1 (#73299) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index a27c0125da3..199298d76ca 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.1", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 9242102e78b..86e7ea1fc44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.1 +pyunifiprotect==3.9.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af615ce4e7e..d656be540fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1322,7 +1322,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.1 +pyunifiprotect==3.9.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From c6b68ed916739da03d217fba986a9d9f33daf180 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Jun 2022 11:11:40 +0200 Subject: [PATCH 071/107] Fix initial tilt value of MQTT cover (#73308) --- homeassistant/components/mqtt/cover.py | 2 -- tests/components/mqtt/test_cover.py | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 325433817c0..8d4df0c301d 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -25,7 +25,6 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, - STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -470,7 +469,6 @@ class MqttCover(MqttEntity, CoverEntity): } if self._config.get(CONF_TILT_STATUS_TOPIC) is not None: - self._tilt_value = STATE_UNKNOWN topics["tilt_status_topic"] = { "topic": self._config.get(CONF_TILT_STATUS_TOPIC), "msg_callback": tilt_message_received, diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 5796c12f3cf..31e30ebf11a 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1256,12 +1256,8 @@ async def test_tilt_defaults(hass, mqtt_mock_entry_with_yaml_config): await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes - assert ATTR_CURRENT_TILT_POSITION in state_attributes_dict - - current_cover_position = hass.states.get("cover.test").attributes[ - ATTR_CURRENT_TILT_POSITION - ] - assert current_cover_position == STATE_UNKNOWN + # Tilt position is not yet known + assert ATTR_CURRENT_TILT_POSITION not in state_attributes_dict async def test_tilt_via_invocation_defaults(hass, mqtt_mock_entry_with_yaml_config): From f98f7f202261ae60fba528dfe76cc8eb3eda52c0 Mon Sep 17 00:00:00 2001 From: hesselonline Date: Fri, 10 Jun 2022 20:55:55 +0200 Subject: [PATCH 072/107] Fix wallbox sensor rounding (#73310) --- homeassistant/components/wallbox/manifest.json | 4 ---- homeassistant/components/wallbox/sensor.py | 8 ++++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index 914adda980a..5c195b8bfce 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -4,10 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wallbox", "requirements": ["wallbox==0.4.9"], - "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], "codeowners": ["@hesselonline"], "iot_class": "cloud_polling", "loggers": ["wallbox"] diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index c8ad3cb3a67..e3598ca7e07 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -167,8 +167,12 @@ class WallboxSensor(WallboxEntity, SensorEntity): @property def native_value(self) -> StateType: - """Return the state of the sensor.""" - if (sensor_round := self.entity_description.precision) is not None: + """Return the state of the sensor. Round the value when it, and the precision property are not None.""" + if ( + sensor_round := self.entity_description.precision + ) is not None and self.coordinator.data[ + self.entity_description.key + ] is not None: return cast( StateType, round(self.coordinator.data[self.entity_description.key], sensor_round), From c5adee6821aa4c672b6ec2a0372be40d23b8322f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Jun 2022 14:23:08 +0200 Subject: [PATCH 073/107] Improve MQTT reload performance (#73313) * Improve MQTT reload performance * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/mqtt/mixins.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/mqtt/__init__.py | 30 +++++++++++++++++++++-- homeassistant/components/mqtt/const.py | 2 ++ homeassistant/components/mqtt/mixins.py | 21 ++++++---------- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e21885d2585..f9a6ebd025f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -28,7 +28,14 @@ from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.reload import ( + async_integration_yaml_config, + async_setup_reload_service, +) from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow @@ -60,12 +67,14 @@ from .const import ( # noqa: F401 DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_QOS, DEFAULT_RETAIN, DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, + MQTT_RELOADED, PLATFORMS, ) from .models import ( # noqa: F401 @@ -227,7 +236,9 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) @@ -364,6 +375,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() + # Setup reload service. Once support for legacy config is removed in 2022.9, we + # should no longer call async_setup_reload_service but instead implement a custom + # service + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + + async def _async_reload_platforms(_: Event | None) -> None: + """Discover entities for a platform.""" + config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} + hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) + async_dispatcher_send(hass, MQTT_RELOADED) + async def async_forward_entry_setup(): """Forward the config entry setup to the platforms.""" async with hass.data[DATA_CONFIG_ENTRY_LOCK]: @@ -374,6 +396,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setup( entry, component ) + # Setup reload service after all platforms have loaded + entry.async_on_unload( + hass.bus.async_listen("event_mqtt_reloaded", _async_reload_platforms) + ) hass.async_create_task(async_forward_entry_setup()) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 2f7e27e7252..b05fd867eeb 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -35,6 +35,7 @@ DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_MQTT = "mqtt" DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" +DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config" DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" @@ -63,6 +64,7 @@ DOMAIN = "mqtt" MQTT_CONNECTED = "mqtt_connected" MQTT_DISCONNECTED = "mqtt_disconnected" +MQTT_RELOADED = "mqtt_reloaded" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index b0f17cc335b..e768c2ff409 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -48,10 +48,6 @@ from homeassistant.helpers.entity import ( async_generate_entity_id, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import ( - async_integration_yaml_config, - async_setup_reload_service, -) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription @@ -67,13 +63,14 @@ from .const import ( DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - PLATFORMS, + MQTT_RELOADED, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -270,14 +267,11 @@ async def async_setup_platform_discovery( ) -> CALLBACK_TYPE: """Set up platform discovery for manual config.""" - async def _async_discover_entities(event: Event | None) -> None: + async def _async_discover_entities() -> None: """Discover entities for a platform.""" - if event: + if DATA_MQTT_UPDATED_CONFIG in hass.data: # The platform has been reloaded - config_yaml = await async_integration_yaml_config(hass, DOMAIN) - if not config_yaml: - return - config_yaml = config_yaml.get(DOMAIN, {}) + config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] else: config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) if not config_yaml: @@ -293,8 +287,8 @@ async def async_setup_platform_discovery( ) ) - unsub = hass.bus.async_listen("event_mqtt_reloaded", _async_discover_entities) - await _async_discover_entities(None) + unsub = async_dispatcher_connect(hass, MQTT_RELOADED, _async_discover_entities) + await _async_discover_entities() return unsub @@ -359,7 +353,6 @@ async def async_setup_platform_helper( async_setup_entities: SetupEntity, ) -> None: """Return true if platform setup should be aborted.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) if not bool(hass.config_entries.async_entries(DOMAIN)): hass.data[DATA_MQTT_RELOAD_NEEDED] = None _LOGGER.warning( From 972aab3c26f41756b9750028479db295bed991ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Jun 2022 12:49:58 -0700 Subject: [PATCH 074/107] Guard MySQL size calculation returning None (#73331) --- .../recorder/system_health/mysql.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/system_health/mysql.py b/homeassistant/components/recorder/system_health/mysql.py index 52ea06f61c3..747a806c227 100644 --- a/homeassistant/components/recorder/system_health/mysql.py +++ b/homeassistant/components/recorder/system_health/mysql.py @@ -5,15 +5,18 @@ from sqlalchemy import text from sqlalchemy.orm.session import Session -def db_size_bytes(session: Session, database_name: str) -> float: +def db_size_bytes(session: Session, database_name: str) -> float | None: """Get the mysql database size.""" - return float( - session.execute( - text( - "SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH), 2) " - "FROM information_schema.TABLES WHERE " - "TABLE_SCHEMA=:database_name" - ), - {"database_name": database_name}, - ).first()[0] - ) + size = session.execute( + text( + "SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH), 2) " + "FROM information_schema.TABLES WHERE " + "TABLE_SCHEMA=:database_name" + ), + {"database_name": database_name}, + ).first()[0] + + if size is None: + return None + + return float(size) From 17fd03d8fd691c3f72484fa4d04fc2bce2f21415 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Jun 2022 12:51:48 -0700 Subject: [PATCH 075/107] Bumped version to 2022.6.5 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d887d1a4d7..eeba0f6698c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index ed1fdb27e4f..fd13e6f7d09 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.4 +version = 2022.6.5 url = https://www.home-assistant.io/ [options] From 28bf9db5a298e58bf3ab08148d647e41b0c0df07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Jun 2022 11:04:43 -1000 Subject: [PATCH 076/107] Filter out forced updates in live logbook when the state has not changed (#73335) --- homeassistant/components/logbook/helpers.py | 20 +-- .../components/logbook/test_websocket_api.py | 114 ++++++++++++++++++ 2 files changed, 126 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index ef322c44e05..221612e1e97 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -189,9 +189,10 @@ def async_subscribe_events( def _forward_state_events_filtered(event: Event) -> None: if event.data.get("old_state") is None or event.data.get("new_state") is None: return - state: State = event.data["new_state"] - if _is_state_filtered(ent_reg, state) or ( - entities_filter and not entities_filter(state.entity_id) + new_state: State = event.data["new_state"] + old_state: State = event.data["old_state"] + if _is_state_filtered(ent_reg, new_state, old_state) or ( + entities_filter and not entities_filter(new_state.entity_id) ): return target(event) @@ -229,17 +230,20 @@ def is_sensor_continuous(ent_reg: er.EntityRegistry, entity_id: str) -> bool: ) -def _is_state_filtered(ent_reg: er.EntityRegistry, state: State) -> bool: +def _is_state_filtered( + ent_reg: er.EntityRegistry, new_state: State, old_state: State +) -> bool: """Check if the logbook should filter a state. Used when we are in live mode to ensure we only get significant changes (state.last_changed != state.last_updated) """ return bool( - split_entity_id(state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS - or state.last_changed != state.last_updated - or ATTR_UNIT_OF_MEASUREMENT in state.attributes - or is_sensor_continuous(ent_reg, state.entity_id) + new_state.state == old_state.state + or split_entity_id(new_state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or new_state.last_changed != new_state.last_updated + or ATTR_UNIT_OF_MEASUREMENT in new_state.attributes + or is_sensor_continuous(ent_reg, new_state.entity_id) ) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 4df2f456eb6..ac6a31202e7 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2404,3 +2404,117 @@ async def test_subscribe_entities_some_have_uom_multiple( # Check our listener got unsubscribed assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_logbook_stream_ignores_forced_updates( + hass, recorder_mock, hass_ws_client +): + """Test logbook live stream ignores forced updates.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + assert msg["event"]["start_time"] == now.timestamp() + assert msg["event"]["end_time"] > msg["event"]["start_time"] + assert msg["event"]["partial"] is True + + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": STATE_ON, + "when": ANY, + }, + { + "entity_id": "binary_sensor.is_light", + "state": STATE_OFF, + "when": ANY, + }, + ] + + # Now we force an update to make sure we ignore + # forced updates when the state has not actually changed + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + for _ in range(3): + hass.states.async_set("binary_sensor.is_light", STATE_OFF, force_update=True) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": STATE_ON, + "when": ANY, + }, + # We should only get the first one and ignore + # the other forced updates since the state + # has not actually changed + { + "entity_id": "binary_sensor.is_light", + "state": STATE_OFF, + "when": ANY, + }, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count From 003de09c48e78ebd6d002e97898b79db7c6d1720 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 11 Jun 2022 02:13:50 -0400 Subject: [PATCH 077/107] Fix zwave_js add node schemas (#73343) * Fix zwave_js add node schemas * Code cleanup * Add test --- homeassistant/components/zwave_js/api.py | 32 +++++++++-- tests/components/zwave_js/test_api.py | 70 ++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5b8b95e951c..27a0c065c0b 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -14,6 +14,7 @@ from zwave_js_server.const import ( InclusionStrategy, LogLevel, Protocols, + ProvisioningEntryStatus, QRCodeVersion, SecurityClass, ZwaveFeature, @@ -148,6 +149,8 @@ MAX_INCLUSION_REQUEST_INTERVAL = "max_inclusion_request_interval" UUID = "uuid" SUPPORTED_PROTOCOLS = "supported_protocols" ADDITIONAL_PROPERTIES = "additional_properties" +STATUS = "status" +REQUESTED_SECURITY_CLASSES = "requested_security_classes" FEATURE = "feature" UNPROVISION = "unprovision" @@ -160,19 +163,22 @@ def convert_planned_provisioning_entry(info: dict) -> ProvisioningEntry: """Handle provisioning entry dict to ProvisioningEntry.""" return ProvisioningEntry( dsk=info[DSK], - security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]], + security_classes=info[SECURITY_CLASSES], + status=info[STATUS], + requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES), additional_properties={ - k: v for k, v in info.items() if k not in (DSK, SECURITY_CLASSES) + k: v + for k, v in info.items() + if k not in (DSK, SECURITY_CLASSES, STATUS, REQUESTED_SECURITY_CLASSES) }, ) def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation: """Convert QR provisioning information dict to QRProvisioningInformation.""" - protocols = [Protocols(proto) for proto in info.get(SUPPORTED_PROTOCOLS, [])] return QRProvisioningInformation( - version=QRCodeVersion(info[VERSION]), - security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]], + version=info[VERSION], + security_classes=info[SECURITY_CLASSES], dsk=info[DSK], generic_device_class=info[GENERIC_DEVICE_CLASS], specific_device_class=info[SPECIFIC_DEVICE_CLASS], @@ -183,7 +189,9 @@ def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation application_version=info[APPLICATION_VERSION], max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL), uuid=info.get(UUID), - supported_protocols=protocols if protocols else None, + supported_protocols=info.get(SUPPORTED_PROTOCOLS), + status=info[STATUS], + requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES), additional_properties=info.get(ADDITIONAL_PROPERTIES, {}), ) @@ -197,6 +205,12 @@ PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( cv.ensure_list, [vol.Coerce(SecurityClass)], ), + vol.Optional(STATUS, default=ProvisioningEntryStatus.ACTIVE): vol.Coerce( + ProvisioningEntryStatus + ), + vol.Optional(REQUESTED_SECURITY_CLASSES): vol.All( + cv.ensure_list, [vol.Coerce(SecurityClass)] + ), }, # Provisioning entries can have extra keys for SmartStart extra=vol.ALLOW_EXTRA, @@ -226,6 +240,12 @@ QR_PROVISIONING_INFORMATION_SCHEMA = vol.All( cv.ensure_list, [vol.Coerce(Protocols)], ), + vol.Optional(STATUS, default=ProvisioningEntryStatus.ACTIVE): vol.Coerce( + ProvisioningEntryStatus + ), + vol.Optional(REQUESTED_SECURITY_CLASSES): vol.All( + cv.ensure_list, [vol.Coerce(SecurityClass)] + ), vol.Optional(ADDITIONAL_PROPERTIES): dict, } ), diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index e59a923ff44..e2ab9cc9503 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -10,6 +10,7 @@ from zwave_js_server.const import ( InclusionStrategy, LogLevel, Protocols, + ProvisioningEntryStatus, QRCodeVersion, SecurityClass, ZwaveFeature, @@ -63,8 +64,10 @@ from homeassistant.components.zwave_js.api import ( PROPERTY_KEY, QR_CODE_STRING, QR_PROVISIONING_INFORMATION, + REQUESTED_SECURITY_CLASSES, SECURITY_CLASSES, SPECIFIC_DEVICE_CLASS, + STATUS, TYPE, UNPROVISION, VALUE, @@ -619,13 +622,68 @@ async def test_add_node( client.async_send_command.reset_mock() client.async_send_command.return_value = {"success": True} - # Test S2 QR code string + # Test S2 QR provisioning information await ws_client.send_json( { ID: 4, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, + QR_PROVISIONING_INFORMATION: { + VERSION: 0, + SECURITY_CLASSES: [0], + DSK: "test", + GENERIC_DEVICE_CLASS: 1, + SPECIFIC_DEVICE_CLASS: 1, + INSTALLER_ICON_TYPE: 1, + MANUFACTURER_ID: 1, + PRODUCT_TYPE: 1, + PRODUCT_ID: 1, + APPLICATION_VERSION: "test", + STATUS: 1, + REQUESTED_SECURITY_CLASSES: [0], + }, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.begin_inclusion", + "options": { + "strategy": InclusionStrategy.SECURITY_S2, + "provisioning": QRProvisioningInformation( + version=QRCodeVersion.S2, + security_classes=[SecurityClass.S2_UNAUTHENTICATED], + dsk="test", + generic_device_class=1, + specific_device_class=1, + installer_icon_type=1, + manufacturer_id=1, + product_type=1, + product_id=1, + application_version="test", + max_inclusion_request_interval=None, + uuid=None, + supported_protocols=None, + status=ProvisioningEntryStatus.INACTIVE, + requested_security_classes=[SecurityClass.S2_UNAUTHENTICATED], + ).to_dict(), + }, + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test S2 QR code string + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", } ) @@ -648,7 +706,7 @@ async def test_add_node( # Test Smart Start QR provisioning information with S2 inclusion strategy fails await ws_client.send_json( { - ID: 5, + ID: 6, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, @@ -678,7 +736,7 @@ async def test_add_node( # Test QR provisioning information with S0 inclusion strategy fails await ws_client.send_json( { - ID: 5, + ID: 7, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S0, @@ -708,7 +766,7 @@ async def test_add_node( # Test ValueError is caught as failure await ws_client.send_json( { - ID: 6, + ID: 8, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.DEFAULT.value, @@ -728,7 +786,7 @@ async def test_add_node( ): await ws_client.send_json( { - ID: 7, + ID: 9, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, } @@ -744,7 +802,7 @@ async def test_add_node( await hass.async_block_till_done() await ws_client.send_json( - {ID: 8, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} + {ID: 10, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() From 8dc61a8c2e650a6e4cca2c1f26b2b38153636698 Mon Sep 17 00:00:00 2001 From: Khole Date: Sat, 11 Jun 2022 20:43:57 +0100 Subject: [PATCH 078/107] Hive Bump pyhiveapi to 0.5.10 for credentials fix (#73365) --- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index e0faa9e0f20..bc07a251779 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.9"], + "requirements": ["pyhiveapi==0.5.10"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 86e7ea1fc44..828e0882df9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.9 +pyhiveapi==0.5.10 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d656be540fb..2b5b0ac2bd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.9 +pyhiveapi==0.5.10 # homeassistant.components.homematic pyhomematic==0.1.77 From 7b7fc125132557406e057b1ac072344cda7c4cc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 17:29:44 -1000 Subject: [PATCH 079/107] Fix reload race in yeelight when updating the ip address (#73390) --- .../components/yeelight/config_flow.py | 5 ++- tests/components/yeelight/test_config_flow.py | 31 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 8a3a5b41320..440b717fd8c 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -96,7 +96,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_HOST: self._discovered_ip} ) - reload = True + reload = entry.state in ( + ConfigEntryState.SETUP_RETRY, + ConfigEntryState.LOADED, + ) if reload: self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 80acaa6f10e..1c19a5e7dfd 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -739,7 +739,7 @@ async def test_discovered_zeroconf(hass): async def test_discovery_updates_ip(hass: HomeAssistant): - """Test discovery updtes ip.""" + """Test discovery updates ip.""" config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "1.2.2.3"}, unique_id=ID ) @@ -761,6 +761,35 @@ async def test_discovery_updates_ip(hass: HomeAssistant): assert config_entry.data[CONF_HOST] == IP_ADDRESS +async def test_discovery_updates_ip_no_reload_setup_in_progress(hass: HomeAssistant): + """Test discovery updates ip does not reload if setup is an an error state.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.2.3"}, + unique_id=ID, + state=config_entries.ConfigEntryState.SETUP_ERROR, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + with patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_setup_entry, _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZEROCONF_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == IP_ADDRESS + assert len(mock_setup_entry.mock_calls) == 0 + + async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant): """Test discovery adds missing ip.""" config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_ID: ID}) From c59b03e3dc7ed1abb55c117f52ecf9a9cae23bfe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 17:04:17 -1000 Subject: [PATCH 080/107] Only update unifiprotect ips from discovery when the console is offline (#73411) --- .../components/unifiprotect/config_flow.py | 32 ++++++++++++++- homeassistant/components/unifiprotect/data.py | 10 +++++ .../components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../unifiprotect/test_config_flow.py | 40 ++++++++++++++++++- 6 files changed, 83 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index daaae214df9..d9fd3a87cb0 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -7,6 +7,7 @@ from typing import Any from aiohttp import CookieJar from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect.data import NVR +from unifi_discovery import async_console_is_alive import voluptuous as vol from homeassistant import config_entries @@ -21,7 +22,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.aiohttp_client import ( + async_create_clientsession, + async_get_clientsession, +) from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.loader import async_get_integration from homeassistant.util.network import is_ip_address @@ -36,11 +40,17 @@ from .const import ( MIN_REQUIRED_PROTECT_V, OUTDATED_LOG_MESSAGE, ) +from .data import async_last_update_was_successful from .discovery import async_start_discovery from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass _LOGGER = logging.getLogger(__name__) +ENTRY_FAILURE_STATES = ( + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, +) + async def async_local_user_documentation_url(hass: HomeAssistant) -> str: """Get the documentation url for creating a local user.""" @@ -53,6 +63,25 @@ def _host_is_direct_connect(host: str) -> bool: return host.endswith(".ui.direct") +async def _async_console_is_offline( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, +) -> bool: + """Check if a console is offline. + + We define offline by the config entry + is in a failure/retry state or the updates + are failing and the console is unreachable + since protect may be updating. + """ + return bool( + entry.state in ENTRY_FAILURE_STATES + or not async_last_update_was_successful(hass, entry) + ) and not await async_console_is_alive( + async_get_clientsession(hass, verify_ssl=False), entry.data[CONF_HOST] + ) + + class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a UniFi Protect config flow.""" @@ -110,6 +139,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): not entry_has_direct_connect and is_ip_address(entry_host) and entry_host != source_ip + and await _async_console_is_offline(self.hass, entry) ): new_host = source_ip if new_host: diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 371c1c7831b..02cfa6c16ff 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -25,6 +25,16 @@ from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES _LOGGER = logging.getLogger(__name__) +@callback +def async_last_update_was_successful(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Check if the last update was successful for a config entry.""" + return bool( + DOMAIN in hass.data + and entry.entry_id in hass.data[DOMAIN] + and hass.data[DOMAIN][entry.entry_id].last_update_success + ) + + class ProtectData: """Coordinate updates.""" diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 199298d76ca..2554d12c866 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 828e0882df9..c129fb2cb54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2358,7 +2358,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.3 +unifi-discovery==1.1.4 # homeassistant.components.unifiled unifiled==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b5b0ac2bd2..ea6cd6a144a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1546,7 +1546,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.3 +unifi-discovery==1.1.4 # homeassistant.components.upb upb_lib==0.4.12 diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 80e845591b1..75f08acb37c 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -402,7 +402,10 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) mock_config.add_to_hass(hass) - with _patch_discovery(): + with _patch_discovery(), patch( + "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", + return_value=False, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, @@ -415,6 +418,41 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin assert mock_config.data[CONF_HOST] == "127.0.0.1" +async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_still_online( + hass: HomeAssistant, mock_nvr: NVR +) -> None: + """Test a discovery from unifi-discovery does not update the ip unless the console at the old ip is offline.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.2.2.2", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": False, + }, + version=2, + unique_id=DEVICE_MAC_ADDRESS.replace(":", "").upper(), + ) + mock_config.add_to_hass(hass) + + with _patch_discovery(), patch( + "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=UNIFI_DISCOVERY_DICT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert mock_config.data[CONF_HOST] == "1.2.2.2" + + async def test_discovered_host_not_updated_if_existing_is_a_hostname( hass: HomeAssistant, mock_nvr: NVR ) -> None: From 8822feaf854c75605bdf85176e6976c8841e7f87 Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Sun, 12 Jun 2022 22:27:18 -0300 Subject: [PATCH 081/107] Fix smart by bond detection with v3 firmware (#73414) --- homeassistant/components/bond/manifest.json | 2 +- homeassistant/components/bond/utils.py | 5 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bond/common.py | 11 +++++- tests/components/bond/test_config_flow.py | 24 ++++++------ tests/components/bond/test_diagnostics.py | 2 +- tests/components/bond/test_init.py | 41 ++++++++++++++------- tests/components/bond/test_light.py | 2 +- 9 files changed, 58 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 52e9dd1763f..a5625d7b642 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,7 +3,7 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-async==0.1.20"], + "requirements": ["bond-async==0.1.22"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"], "quality_scale": "platinum", diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index cba213d9450..c426bf64577 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -5,7 +5,7 @@ import logging from typing import Any, cast from aiohttp import ClientResponseError -from bond_async import Action, Bond +from bond_async import Action, Bond, BondType from homeassistant.util.async_ import gather_with_concurrency @@ -224,4 +224,5 @@ class BondHub: @property def is_bridge(self) -> bool: """Return if the Bond is a Bond Bridge.""" - return bool(self._bridge) + bondid = self._version["bondid"] + return bool(BondType.is_bridge_from_serial(bondid)) diff --git a/requirements_all.txt b/requirements_all.txt index c129fb2cb54..a580a474b68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bond -bond-async==0.1.20 +bond-async==0.1.22 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea6cd6a144a..bdbe9ba1e7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -318,7 +318,7 @@ blebox_uniapi==1.3.3 blinkpy==0.19.0 # homeassistant.components.bond -bond-async==0.1.20 +bond-async==0.1.22 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 4b45a4016c0..909fb35a1e2 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -113,7 +113,7 @@ def patch_bond_version( return nullcontext() if return_value is None: - return_value = {"bondid": "test-bond-id"} + return_value = {"bondid": "ZXXX12345"} return patch( "homeassistant.components.bond.Bond.version", @@ -246,3 +246,12 @@ async def help_test_entity_available( async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + +def ceiling_fan(name: str): + """Create a ceiling fan with given name.""" + return { + "name": name, + "type": DeviceType.CEILING_FAN, + "actions": ["SetSpeed", "SetDirection"], + } diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 5d3b357b9f7..519fa9dec9d 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -35,7 +35,7 @@ async def test_user_form(hass: core.HomeAssistant): assert result["errors"] == {} with patch_bond_version( - return_value={"bondid": "test-bond-id"} + return_value={"bondid": "ZXXX12345"} ), patch_bond_device_ids( return_value=["f6776c11", "f6776c12"] ), patch_bond_bridge(), patch_bond_device_properties(), patch_bond_device(), _patch_async_setup_entry() as mock_setup_entry: @@ -64,7 +64,7 @@ async def test_user_form_with_non_bridge(hass: core.HomeAssistant): assert result["errors"] == {} with patch_bond_version( - return_value={"bondid": "test-bond-id"} + return_value={"bondid": "KXXX12345"} ), patch_bond_device_ids( return_value=["f6776c11"] ), patch_bond_device_properties(), patch_bond_device( @@ -96,7 +96,7 @@ async def test_user_form_invalid_auth(hass: core.HomeAssistant): ) with patch_bond_version( - return_value={"bond_id": "test-bond-id"} + return_value={"bond_id": "ZXXX12345"} ), patch_bond_bridge(), patch_bond_device_ids( side_effect=ClientResponseError(Mock(), Mock(), status=401), ): @@ -203,7 +203,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -213,7 +213,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): assert result["errors"] == {} with patch_bond_version( - return_value={"bondid": "test-bond-id"} + return_value={"bondid": "ZXXX12345"} ), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -241,7 +241,7 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -270,7 +270,7 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): """Test we get the discovery form when we can get the token.""" - with patch_bond_version(return_value={"bondid": "test-bond-id"}), patch_bond_token( + with patch_bond_version(return_value={"bondid": "ZXXX12345"}), patch_bond_token( return_value={"token": "discovered-token"} ), patch_bond_bridge( return_value={"name": "discovered-name"} @@ -282,7 +282,7 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -323,7 +323,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -341,7 +341,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["title"] == "test-bond-id" + assert result2["title"] == "ZXXX12345" assert result2["data"] == { CONF_HOST: "test-host", CONF_ACCESS_TOKEN: "discovered-token", @@ -472,7 +472,7 @@ async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -497,7 +497,7 @@ async def _help_test_form_unexpected_error( ) with patch_bond_version( - return_value={"bond_id": "test-bond-id"} + return_value={"bond_id": "ZXXX12345"} ), patch_bond_device_ids(side_effect=error): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input diff --git a/tests/components/bond/test_diagnostics.py b/tests/components/bond/test_diagnostics.py index 88d33ff2cc0..b738c72ee8c 100644 --- a/tests/components/bond/test_diagnostics.py +++ b/tests/components/bond/test_diagnostics.py @@ -39,5 +39,5 @@ async def test_diagnostics(hass, hass_client): "data": {"access_token": "**REDACTED**", "host": "some host"}, "title": "Mock Title", }, - "hub": {"version": {"bondid": "test-bond-id"}}, + "hub": {"version": {"bondid": "ZXXX12345"}}, } diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 03eb490b65e..56087d4bf11 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -7,13 +7,15 @@ from bond_async import DeviceType import pytest from homeassistant.components.bond.const import DOMAIN +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST +from homeassistant.const import ATTR_ASSUMED_STATE, CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from .common import ( + ceiling_fan, patch_bond_bridge, patch_bond_device, patch_bond_device_ids, @@ -23,6 +25,7 @@ from .common import ( patch_setup_entry, patch_start_bpup, setup_bond_entity, + setup_platform, ) from tests.common import MockConfigEntry @@ -81,7 +84,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss with patch_bond_bridge(), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "ZXXX12345", "target": "test-model", "fw_ver": "test-version", "mcu_ver": "test-hw-version", @@ -99,11 +102,11 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "ZXXX12345" # verify hub device is registered correctly device_registry = dr.async_get(hass) - hub = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + hub = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")}) assert hub.name == "bond-name" assert hub.manufacturer == "Olibra" assert hub.model == "test-model" @@ -151,7 +154,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): ) old_identifers = (DOMAIN, "device_id") - new_identifiers = (DOMAIN, "test-bond-id", "device_id") + new_identifiers = (DOMAIN, "ZXXX12345", "device_id") device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -164,7 +167,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): with patch_bond_bridge(), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "ZXXX12345", "target": "test-model", "fw_ver": "test-version", } @@ -185,7 +188,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "ZXXX12345" # verify the device info is cleaned up assert device_registry.async_get_device(identifiers={old_identifers}) is None @@ -205,7 +208,7 @@ async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant): side_effect=ClientResponseError(Mock(), Mock(), status=404) ), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "KXXX12345", "target": "test-model", "fw_ver": "test-version", } @@ -227,10 +230,10 @@ async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant): assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "KXXX12345" device_registry = dr.async_get(hass) - device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "KXXX12345")}) assert device is not None assert device.suggested_area == "Den" @@ -251,7 +254,7 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant): } ), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "ZXXX12345", "target": "test-model", "fw_ver": "test-version", } @@ -273,9 +276,21 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant): assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "ZXXX12345" device_registry = dr.async_get(hass) - device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")}) assert device is not None assert device.suggested_area == "Office" + + +async def test_smart_by_bond_v3_firmware(hass: HomeAssistant) -> None: + """Test we can detect smart by bond with the v3 firmware.""" + await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan("name-1"), + bond_version={"bondid": "KXXXX12345", "target": "breck-northstar"}, + bond_device_id="test-device-id", + ) + assert ATTR_ASSUMED_STATE not in hass.states.get("fan.name_1").attributes diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index c7d8f195423..7577b1d70ab 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -249,7 +249,7 @@ async def test_sbb_trust_state(hass: core.HomeAssistant): """Assumed state should be False if device is a Smart by Bond.""" version = { "model": "MR123A", - "bondid": "test-bond-id", + "bondid": "KXXX12345", } await setup_platform( hass, LIGHT_DOMAIN, ceiling_fan("name-1"), bond_version=version, bridge={} From 1ab91bcf0f440d0f91c6ded3d631f69812d6bc13 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 12 Jun 2022 22:33:45 -0700 Subject: [PATCH 082/107] Bump aiohue to 4.4.2 (#73420) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index d3b492f3b9e..b3dbe4df50a 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.4.1"], + "requirements": ["aiohue==4.4.2"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index a580a474b68..c90229fdb76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiohomekit==0.7.17 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.1 +aiohue==4.4.2 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdbe9ba1e7c..881aa1cef55 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -153,7 +153,7 @@ aiohomekit==0.7.17 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.1 +aiohue==4.4.2 # homeassistant.components.apache_kafka aiokafka==0.6.0 From d6bfb86da266b8ac2c167178be4b6d403610b5d3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 14 Jun 2022 06:19:22 -0700 Subject: [PATCH 083/107] Fix fan support in nest, removing FAN_ONLY which isn't supported (#73422) * Fix fan support in nest, removing FAN_ONLY which isn't supported * Revert change to make supported features dynamic --- homeassistant/components/nest/climate_sdm.py | 37 ++-- tests/components/nest/test_climate_sdm.py | 195 ++++++++++--------- 2 files changed, 119 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 8a56f78028b..6ee988b714f 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -70,6 +70,7 @@ FAN_MODE_MAP = { "OFF": FAN_OFF, } FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()} +FAN_INV_MODES = list(FAN_INV_MODE_MAP) MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API MIN_TEMP = 10 @@ -99,7 +100,7 @@ class ThermostatEntity(ClimateEntity): """Initialize ThermostatEntity.""" self._device = device self._device_info = NestDeviceInfo(device) - self._supported_features = 0 + self._attr_supported_features = 0 @property def should_poll(self) -> bool: @@ -124,7 +125,7 @@ class ThermostatEntity(ClimateEntity): async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" - self._supported_features = self._get_supported_features() + self._attr_supported_features = self._get_supported_features() self.async_on_remove( self._device.add_update_listener(self.async_write_ha_state) ) @@ -198,8 +199,6 @@ class ThermostatEntity(ClimateEntity): trait = self._device.traits[ThermostatModeTrait.NAME] if trait.mode in THERMOSTAT_MODE_MAP: hvac_mode = THERMOSTAT_MODE_MAP[trait.mode] - if hvac_mode == HVACMode.OFF and self.fan_mode == FAN_ON: - hvac_mode = HVACMode.FAN_ONLY return hvac_mode @property @@ -209,8 +208,6 @@ class ThermostatEntity(ClimateEntity): for mode in self._get_device_hvac_modes: if mode in THERMOSTAT_MODE_MAP: supported_modes.append(THERMOSTAT_MODE_MAP[mode]) - if self.supported_features & ClimateEntityFeature.FAN_MODE: - supported_modes.append(HVACMode.FAN_ONLY) return supported_modes @property @@ -252,7 +249,10 @@ class ThermostatEntity(ClimateEntity): @property def fan_mode(self) -> str: """Return the current fan mode.""" - if FanTrait.NAME in self._device.traits: + if ( + self.supported_features & ClimateEntityFeature.FAN_MODE + and FanTrait.NAME in self._device.traits + ): trait = self._device.traits[FanTrait.NAME] return FAN_MODE_MAP.get(trait.timer_mode, FAN_OFF) return FAN_OFF @@ -260,15 +260,12 @@ class ThermostatEntity(ClimateEntity): @property def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" - modes = [] - if FanTrait.NAME in self._device.traits: - modes = list(FAN_INV_MODE_MAP) - return modes - - @property - def supported_features(self) -> int: - """Bitmap of supported features.""" - return self._supported_features + if ( + self.supported_features & ClimateEntityFeature.FAN_MODE + and FanTrait.NAME in self._device.traits + ): + return FAN_INV_MODES + return [] def _get_supported_features(self) -> int: """Compute the bitmap of supported features from the current state.""" @@ -290,10 +287,6 @@ class ThermostatEntity(ClimateEntity): """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") - if hvac_mode == HVACMode.FAN_ONLY: - # Turn the fan on but also turn off the hvac if it is on - await self.async_set_fan_mode(FAN_ON) - hvac_mode = HVACMode.OFF api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] trait = self._device.traits[ThermostatModeTrait.NAME] try: @@ -338,6 +331,10 @@ class ThermostatEntity(ClimateEntity): """Set new target fan mode.""" if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan_mode '{fan_mode}'") + if fan_mode == FAN_ON and self.hvac_mode == HVACMode.OFF: + raise ValueError( + "Cannot turn on fan, please set an HVAC mode (e.g. heat/cool) first" + ) trait = self._device.traits[FanTrait.NAME] duration = None if fan_mode != FAN_OFF: diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index 123742607ad..c271687a348 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -33,15 +33,15 @@ from homeassistant.components.climate.const import ( FAN_ON, HVAC_MODE_COOL, HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_ECO, PRESET_NONE, PRESET_SLEEP, + ClimateEntityFeature, ) -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.const import ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -794,7 +794,7 @@ async def test_thermostat_fan_off( "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, @@ -806,18 +806,22 @@ async def test_thermostat_fan_off( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVAC_MODE_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) async def test_thermostat_fan_on( @@ -837,7 +841,7 @@ async def test_thermostat_fan_on( }, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, @@ -849,18 +853,22 @@ async def test_thermostat_fan_on( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_FAN_ONLY + assert thermostat.state == HVAC_MODE_COOL assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) async def test_thermostat_cool_with_fan( @@ -895,11 +903,15 @@ async def test_thermostat_cool_with_fan( HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) async def test_thermostat_set_fan( @@ -907,6 +919,68 @@ async def test_thermostat_set_fan( setup_platform: PlatformSetup, auth: FakeAuth, create_device: CreateDevice, +) -> None: + """Test a thermostat enabling the fan.""" + create_device.create( + { + "sdm.devices.traits.Fan": { + "timerMode": "ON", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "HEAT", + }, + } + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_HEAT + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) + + # Turn off fan mode + await common.async_set_fan_mode(hass, FAN_OFF) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": {"timerMode": "OFF"}, + } + + # Turn on fan mode + await common.async_set_fan_mode(hass, FAN_ON) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": { + "duration": "43200s", + "timerMode": "ON", + }, + } + + +async def test_thermostat_set_fan_when_off( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + create_device: CreateDevice, ) -> None: """Test a thermostat enabling the fan.""" create_device.create( @@ -929,34 +1003,18 @@ async def test_thermostat_set_fan( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_FAN_ONLY + assert thermostat.state == HVAC_MODE_OFF assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) - # Turn off fan mode - await common.async_set_fan_mode(hass, FAN_OFF) - await hass.async_block_till_done() - - assert auth.method == "post" - assert auth.url == DEVICE_COMMAND - assert auth.json == { - "command": "sdm.devices.commands.Fan.SetTimer", - "params": {"timerMode": "OFF"}, - } - - # Turn on fan mode - await common.async_set_fan_mode(hass, FAN_ON) - await hass.async_block_till_done() - - assert auth.method == "post" - assert auth.url == DEVICE_COMMAND - assert auth.json == { - "command": "sdm.devices.commands.Fan.SetTimer", - "params": { - "duration": "43200s", - "timerMode": "ON", - }, - } + # Fan cannot be turned on when HVAC is off + with pytest.raises(ValueError): + await common.async_set_fan_mode(hass, FAN_ON, entity_id="climate.my_thermostat") async def test_thermostat_fan_empty( @@ -994,6 +1052,10 @@ async def test_thermostat_fan_empty( } assert ATTR_FAN_MODE not in thermostat.attributes assert ATTR_FAN_MODES not in thermostat.attributes + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) # Ignores set_fan_mode since it is lacking SUPPORT_FAN_MODE await common.async_set_fan_mode(hass, FAN_ON) @@ -1018,7 +1080,7 @@ async def test_thermostat_invalid_fan_mode( "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, @@ -1030,14 +1092,13 @@ async def test_thermostat_invalid_fan_mode( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_FAN_ONLY + assert thermostat.state == HVAC_MODE_COOL assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON @@ -1048,58 +1109,6 @@ async def test_thermostat_invalid_fan_mode( await hass.async_block_till_done() -async def test_thermostat_set_hvac_fan_only( - hass: HomeAssistant, - setup_platform: PlatformSetup, - auth: FakeAuth, - create_device: CreateDevice, -) -> None: - """Test a thermostat enabling the fan via hvac_mode.""" - create_device.create( - { - "sdm.devices.traits.Fan": { - "timerMode": "OFF", - "timerTimeout": "2019-05-10T03:22:54Z", - }, - "sdm.devices.traits.ThermostatHvac": { - "status": "OFF", - }, - "sdm.devices.traits.ThermostatMode": { - "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", - }, - } - ) - await setup_platform() - - assert len(hass.states.async_all()) == 1 - thermostat = hass.states.get("climate.my_thermostat") - assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF - assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] - - await common.async_set_hvac_mode(hass, HVAC_MODE_FAN_ONLY) - await hass.async_block_till_done() - - assert len(auth.captured_requests) == 2 - - (method, url, json, headers) = auth.captured_requests.pop(0) - assert method == "post" - assert url == DEVICE_COMMAND - assert json == { - "command": "sdm.devices.commands.Fan.SetTimer", - "params": {"duration": "43200s", "timerMode": "ON"}, - } - (method, url, json, headers) = auth.captured_requests.pop(0) - assert method == "post" - assert url == DEVICE_COMMAND - assert json == { - "command": "sdm.devices.commands.ThermostatMode.SetMode", - "params": {"mode": "OFF"}, - } - - async def test_thermostat_target_temp( hass: HomeAssistant, setup_platform: PlatformSetup, @@ -1397,7 +1406,7 @@ async def test_thermostat_hvac_mode_failure( "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Fan": { "timerMode": "OFF", @@ -1416,8 +1425,8 @@ async def test_thermostat_hvac_mode_failure( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVAC_MODE_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] with pytest.raises(HomeAssistantError): From d8f2afb7727ffa66e3a5c10ceca18201f36620d5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jun 2022 10:55:58 -0700 Subject: [PATCH 084/107] Guard withings accessing hass.data without it being set (#73454) Co-authored-by: Martin Hjelmare --- homeassistant/components/withings/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 47702090cc0..6e8dee9a774 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -72,11 +72,12 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Withings component.""" - conf = config.get(DOMAIN, {}) - if not (conf := config.get(DOMAIN, {})): + if not (conf := config.get(DOMAIN)): + # Apply the defaults. + conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] + hass.data[DOMAIN] = {const.CONFIG: conf} return True - # Make the config available to the oauth2 config flow. hass.data[DOMAIN] = {const.CONFIG: conf} # Setup the oauth2 config flow. From 0b22e47c53b63bd071deaf3a3d24d97ebc92f1a0 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Tue, 14 Jun 2022 15:17:40 +0200 Subject: [PATCH 085/107] =?UTF-8?q?Fix=20max=5Fvalue=20access=20for=20numb?= =?UTF-8?q?er=20platform=20in=E2=80=AFOverkiz=20(#73479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix wrong property name --- homeassistant/components/overkiz/number.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index 741c666a42a..167065e9015 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -134,7 +134,7 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): """Return the entity value to represent the entity state.""" if state := self.device.states.get(self.entity_description.key): if self.entity_description.inverted: - return self._attr_max_value - cast(float, state.value) + return self.max_value - cast(float, state.value) return cast(float, state.value) @@ -143,7 +143,7 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): async def async_set_value(self, value: float) -> None: """Set new value.""" if self.entity_description.inverted: - value = self._attr_max_value - value + value = self.max_value - value await self.executor.async_execute_command( self.entity_description.command, value From a4a511b6db6f706ce9d42b951da2fd76975b04ec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jun 2022 12:31:51 -0700 Subject: [PATCH 086/107] Bumped version to 2022.6.6 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index eeba0f6698c..8f8a8008457 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index fd13e6f7d09..17e4901b2ff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.5 +version = 2022.6.6 url = https://www.home-assistant.io/ [options] From 063e680589ca956b9534c63841fb7fea943aa28f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jun 2022 13:23:27 -0700 Subject: [PATCH 087/107] Fix unifiprotect import --- homeassistant/components/unifiprotect/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 02cfa6c16ff..b902409595e 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -20,7 +20,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval -from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES +from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES, DOMAIN _LOGGER = logging.getLogger(__name__) From 574ec5a507fd073553c02a09ec5a96eba27505ef Mon Sep 17 00:00:00 2001 From: Gordon Allott Date: Mon, 20 Jun 2022 19:27:39 +0100 Subject: [PATCH 088/107] Ensure metoffice daily are returned once daily (#72440) * ensure metoffice daily are returned once daily * Fixes metoffice tests for MODE_DAILY --- homeassistant/components/metoffice/helpers.py | 4 ++ tests/components/metoffice/test_weather.py | 45 ++++++++++--------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 31ac4c141a9..00d5e73501d 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -7,6 +7,7 @@ import datapoint from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.util.dt import utcnow +from .const import MODE_3HOURLY from .data import MetOfficeData _LOGGER = logging.getLogger(__name__) @@ -39,6 +40,9 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: for day in forecast.days for timestep in day.timesteps if timestep.date > time_now + and ( + mode == MODE_3HOURLY or timestep.date.hour > 6 + ) # ensures only one result per day in MODE_DAILY ], site, ) diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index fb00203661b..bf279ff3cf7 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -163,16 +163,17 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check - assert len(weather.attributes.get("forecast")) == 8 + # ensures that daily filters out multiple results per day + assert len(weather.attributes.get("forecast")) == 4 assert ( - weather.attributes.get("forecast")[7]["datetime"] == "2020-04-29T12:00:00+00:00" + weather.attributes.get("forecast")[3]["datetime"] == "2020-04-29T12:00:00+00:00" ) - assert weather.attributes.get("forecast")[7]["condition"] == "rainy" - assert weather.attributes.get("forecast")[7]["precipitation_probability"] == 59 - assert weather.attributes.get("forecast")[7]["temperature"] == 13 - assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 - assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[3]["condition"] == "rainy" + assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 + assert weather.attributes.get("forecast")[3]["temperature"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" @freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc)) @@ -258,16 +259,17 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check - assert len(weather.attributes.get("forecast")) == 8 + # ensures that daily filters out multiple results per day + assert len(weather.attributes.get("forecast")) == 4 assert ( - weather.attributes.get("forecast")[7]["datetime"] == "2020-04-29T12:00:00+00:00" + weather.attributes.get("forecast")[3]["datetime"] == "2020-04-29T12:00:00+00:00" ) - assert weather.attributes.get("forecast")[7]["condition"] == "rainy" - assert weather.attributes.get("forecast")[7]["precipitation_probability"] == 59 - assert weather.attributes.get("forecast")[7]["temperature"] == 13 - assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 - assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[3]["condition"] == "rainy" + assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 + assert weather.attributes.get("forecast")[3]["temperature"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" # King's Lynn 3-hourly weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_3_hourly") @@ -305,13 +307,14 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("humidity") == 75 # All should have Forecast added - again, just picking out 1 entry to check - assert len(weather.attributes.get("forecast")) == 8 + # ensures daily filters out multiple results per day + assert len(weather.attributes.get("forecast")) == 4 assert ( - weather.attributes.get("forecast")[5]["datetime"] == "2020-04-28T12:00:00+00:00" + weather.attributes.get("forecast")[2]["datetime"] == "2020-04-28T12:00:00+00:00" ) - assert weather.attributes.get("forecast")[5]["condition"] == "cloudy" - assert weather.attributes.get("forecast")[5]["precipitation_probability"] == 14 - assert weather.attributes.get("forecast")[5]["temperature"] == 11 - assert weather.attributes.get("forecast")[5]["wind_speed"] == 7 - assert weather.attributes.get("forecast")[5]["wind_bearing"] == "ESE" + assert weather.attributes.get("forecast")[2]["condition"] == "cloudy" + assert weather.attributes.get("forecast")[2]["precipitation_probability"] == 14 + assert weather.attributes.get("forecast")[2]["temperature"] == 11 + assert weather.attributes.get("forecast")[2]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[2]["wind_bearing"] == "ESE" From 8dbc6b10855b1317a736c08ece0cb8c3f58ef69b Mon Sep 17 00:00:00 2001 From: Jonny Bergdahl Date: Wed, 22 Jun 2022 10:22:09 +0200 Subject: [PATCH 089/107] Fix thumbnail issues in Twitch integration (#72564) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/twitch/sensor.py | 9 ++++++--- tests/components/twitch/test_twitch.py | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 95006a4cab7..dcc2dc9bbf6 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -108,10 +108,8 @@ class TwitchSensor(SensorEntity): def update(self) -> None: """Update device state.""" - followers = self._client.get_users_follows(to_id=self.unique_id)["total"] channel = self._client.get_users(user_ids=[self.unique_id])["data"][0] - self._attr_extra_state_attributes = { ATTR_FOLLOWING: followers, ATTR_VIEWS: channel["view_count"], @@ -151,8 +149,13 @@ class TwitchSensor(SensorEntity): self._attr_extra_state_attributes[ATTR_GAME] = stream["game_name"] self._attr_extra_state_attributes[ATTR_TITLE] = stream["title"] self._attr_entity_picture = stream["thumbnail_url"] + if self._attr_entity_picture is not None: + self._attr_entity_picture = self._attr_entity_picture.format( + height=24, + width=24, + ) else: self._attr_native_value = STATE_OFFLINE self._attr_extra_state_attributes[ATTR_GAME] = None self._attr_extra_state_attributes[ATTR_TITLE] = None - self._attr_entity_picture = channel["offline_image_url"] + self._attr_entity_picture = channel["profile_image_url"] diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index bfffeb4ae7f..0e086fe6f7a 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -28,6 +28,7 @@ USER_OBJECT = { "id": 123, "display_name": "channel123", "offline_image_url": "logo.png", + "profile_image_url": "logo.png", "view_count": 42, } STREAM_OBJECT_ONLINE = { From 7b5258bc575767e578218a24f50d7a4f3c5e4ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Tue, 14 Jun 2022 20:11:37 -0700 Subject: [PATCH 090/107] Bump aiobafi6 to 0.6.0 to fix logging performance (#73517) --- homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 8143c35410e..821ad1a21cb 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.5.0"], + "requirements": ["aiobafi6==0.6.0"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index c90229fdb76..608d0d5bbdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -122,7 +122,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.5.0 +aiobafi6==0.6.0 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 881aa1cef55..19a9bdfc684 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.5.0 +aiobafi6==0.6.0 # homeassistant.components.aws aiobotocore==2.1.0 From a310b28170f608a3632b03979e47bb51d42f9531 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 16 Jun 2022 11:43:36 +0200 Subject: [PATCH 091/107] Use IP address instead of hostname in Brother integration (#73556) --- .../components/brother/config_flow.py | 5 +-- tests/components/brother/test_config_flow.py | 38 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 39a196aa6cb..a136c98bf91 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -83,8 +83,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - # Hostname is format: brother.local. - self.host = discovery_info.hostname.rstrip(".") + self.host = discovery_info.host # Do not probe the device if the host is already configured self._async_abort_entries_match({CONF_HOST: self.host}) @@ -102,7 +101,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Check if already configured await self.async_set_unique_id(self.brother.serial.lower()) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured({CONF_HOST: self.host}) self.context.update( { diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 6dbaebdfa7b..00493011500 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_TYPE from tests.common import MockConfigEntry, load_fixture -CONFIG = {CONF_HOST: "localhost", CONF_TYPE: "laser"} +CONFIG = {CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"} async def test_show_form(hass): @@ -32,13 +32,15 @@ async def test_create_entry_with_hostname(hass): return_value=json.loads(load_fixture("printer_data.json", "brother")), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" - assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] - assert result["data"][CONF_TYPE] == CONFIG[CONF_TYPE] + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_TYPE] == "laser" async def test_create_entry_with_ipv4_address(hass): @@ -48,9 +50,7 @@ async def test_create_entry_with_ipv4_address(hass): return_value=json.loads(load_fixture("printer_data.json", "brother")), ): result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"}, + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -145,7 +145,7 @@ async def test_zeroconf_snmp_error(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -166,7 +166,7 @@ async def test_zeroconf_unsupported_model(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -187,15 +187,18 @@ async def test_zeroconf_device_exists_abort(hass): "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): - MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG).add_to_hass( - hass + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) + entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -208,6 +211,9 @@ async def test_zeroconf_device_exists_abort(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + # Test config entry got updated with latest IP + assert entry.data["host"] == "127.0.0.1" + async def test_zeroconf_no_probe_existing_device(hass): """Test we do not probe the device is the host is already configured.""" @@ -218,9 +224,9 @@ async def test_zeroconf_no_probe_existing_device(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], - hostname="localhost", + hostname="example.local.", name="Brother Printer", port=None, properties={}, @@ -245,7 +251,7 @@ async def test_zeroconf_confirm_create_entry(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -266,5 +272,5 @@ async def test_zeroconf_confirm_create_entry(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" - assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" From 66bcebbab75155b129cf996b434fe09d003a0f5b Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Thu, 16 Jun 2022 09:15:19 +0100 Subject: [PATCH 092/107] Bump growattServer to 1.2.2 (#73561) Fix #71577 - Updating growattServer dependency --- homeassistant/components/growatt_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index c8a71d426e7..4127b48ae64 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==1.1.0"], + "requirements": ["growattServer==1.2.2"], "codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"], "iot_class": "cloud_polling", "loggers": ["growattServer"] diff --git a/requirements_all.txt b/requirements_all.txt index 608d0d5bbdf..20dd9ae64ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -768,7 +768,7 @@ greenwavereality==0.5.1 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.1.0 +growattServer==1.2.2 # homeassistant.components.gstreamer gstreamer-player==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19a9bdfc684..fe7002d422a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -550,7 +550,7 @@ greeneye_monitor==3.0.3 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.1.0 +growattServer==1.2.2 # homeassistant.components.profiler guppy3==3.1.2 From cfbc2ed4375d071dd2f9a828633e3b49866936bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Jun 2022 19:51:55 -1000 Subject: [PATCH 093/107] Handle offline generators in oncue (#73568) Fixes #73565 --- homeassistant/components/oncue/entity.py | 10 +- tests/components/oncue/__init__.py | 277 +++++++++++++++++++++++ tests/components/oncue/test_sensor.py | 22 +- 3 files changed, 304 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/oncue/entity.py b/homeassistant/components/oncue/entity.py index 40ca21edf96..d1942c532e7 100644 --- a/homeassistant/components/oncue/entity.py +++ b/homeassistant/components/oncue/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from aiooncue import OncueDevice, OncueSensor +from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -30,16 +31,21 @@ class OncueEntity(CoordinatorEntity, Entity): self._device_id = device_id self._attr_unique_id = f"{device_id}_{description.key}" self._attr_name = f"{device.name} {sensor.display_name}" - mac_address_hex = hex(int(device.sensors["MacAddress"].value))[2:] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device_id)}, - connections={(dr.CONNECTION_NETWORK_MAC, mac_address_hex)}, name=device.name, hw_version=device.hardware_version, sw_version=device.sensors["FirmwareVersion"].display_value, model=device.sensors["GensetModelNumberSelect"].display_value, manufacturer="Kohler", ) + try: + mac_address_hex = hex(int(device.sensors["MacAddress"].value))[2:] + except ValueError: # MacAddress may be invalid if the gateway is offline + return + self._attr_device_info[ATTR_CONNECTIONS] = { + (dr.CONNECTION_NETWORK_MAC, mac_address_hex) + } @property def _oncue_value(self) -> str: diff --git a/tests/components/oncue/__init__.py b/tests/components/oncue/__init__.py index 48492a19933..32845aa8d26 100644 --- a/tests/components/oncue/__init__.py +++ b/tests/components/oncue/__init__.py @@ -269,6 +269,271 @@ MOCK_ASYNC_FETCH_ALL = { } +MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE = { + "456789": OncueDevice( + name="My Generator", + state="Off", + product_name="RDC 2.4", + hardware_version="319", + serial_number="SERIAL", + sensors={ + "Product": OncueSensor( + name="Product", + display_name="Controller Type", + value="RDC 2.4", + display_value="RDC 2.4", + unit=None, + ), + "FirmwareVersion": OncueSensor( + name="FirmwareVersion", + display_name="Current Firmware", + value="2.0.6", + display_value="2.0.6", + unit=None, + ), + "LatestFirmware": OncueSensor( + name="LatestFirmware", + display_name="Latest Firmware", + value="2.0.6", + display_value="2.0.6", + unit=None, + ), + "EngineSpeed": OncueSensor( + name="EngineSpeed", + display_name="Engine Speed", + value="0", + display_value="0 R/min", + unit="R/min", + ), + "EngineTargetSpeed": OncueSensor( + name="EngineTargetSpeed", + display_name="Engine Target Speed", + value="0", + display_value="0 R/min", + unit="R/min", + ), + "EngineOilPressure": OncueSensor( + name="EngineOilPressure", + display_name="Engine Oil Pressure", + value=0, + display_value="0 Psi", + unit="Psi", + ), + "EngineCoolantTemperature": OncueSensor( + name="EngineCoolantTemperature", + display_name="Engine Coolant Temperature", + value=32, + display_value="32 F", + unit="F", + ), + "BatteryVoltage": OncueSensor( + name="BatteryVoltage", + display_name="Battery Voltage", + value="13.4", + display_value="13.4 V", + unit="V", + ), + "LubeOilTemperature": OncueSensor( + name="LubeOilTemperature", + display_name="Lube Oil Temperature", + value=32, + display_value="32 F", + unit="F", + ), + "GensetControllerTemperature": OncueSensor( + name="GensetControllerTemperature", + display_name="Generator Controller Temperature", + value=84.2, + display_value="84.2 F", + unit="F", + ), + "EngineCompartmentTemperature": OncueSensor( + name="EngineCompartmentTemperature", + display_name="Engine Compartment Temperature", + value=62.6, + display_value="62.6 F", + unit="F", + ), + "GeneratorTrueTotalPower": OncueSensor( + name="GeneratorTrueTotalPower", + display_name="Generator True Total Power", + value="0.0", + display_value="0.0 W", + unit="W", + ), + "GeneratorTruePercentOfRatedPower": OncueSensor( + name="GeneratorTruePercentOfRatedPower", + display_name="Generator True Percent Of Rated Power", + value="0", + display_value="0 %", + unit="%", + ), + "GeneratorVoltageAB": OncueSensor( + name="GeneratorVoltageAB", + display_name="Generator Voltage AB", + value="0.0", + display_value="0.0 V", + unit="V", + ), + "GeneratorVoltageAverageLineToLine": OncueSensor( + name="GeneratorVoltageAverageLineToLine", + display_name="Generator Voltage Average Line To Line", + value="0.0", + display_value="0.0 V", + unit="V", + ), + "GeneratorCurrentAverage": OncueSensor( + name="GeneratorCurrentAverage", + display_name="Generator Current Average", + value="0.0", + display_value="0.0 A", + unit="A", + ), + "GeneratorFrequency": OncueSensor( + name="GeneratorFrequency", + display_name="Generator Frequency", + value="0.0", + display_value="0.0 Hz", + unit="Hz", + ), + "GensetSerialNumber": OncueSensor( + name="GensetSerialNumber", + display_name="Generator Serial Number", + value="33FDGMFR0026", + display_value="33FDGMFR0026", + unit=None, + ), + "GensetState": OncueSensor( + name="GensetState", + display_name="Generator State", + value="Off", + display_value="Off", + unit=None, + ), + "GensetControllerSerialNumber": OncueSensor( + name="GensetControllerSerialNumber", + display_name="Generator Controller Serial Number", + value="-1", + display_value="-1", + unit=None, + ), + "GensetModelNumberSelect": OncueSensor( + name="GensetModelNumberSelect", + display_name="Genset Model Number Select", + value="38 RCLB", + display_value="38 RCLB", + unit=None, + ), + "GensetControllerClockTime": OncueSensor( + name="GensetControllerClockTime", + display_name="Generator Controller Clock Time", + value="2022-01-13 18:08:13", + display_value="2022-01-13 18:08:13", + unit=None, + ), + "GensetControllerTotalOperationTime": OncueSensor( + name="GensetControllerTotalOperationTime", + display_name="Generator Controller Total Operation Time", + value="16770.8", + display_value="16770.8 h", + unit="h", + ), + "EngineTotalRunTime": OncueSensor( + name="EngineTotalRunTime", + display_name="Engine Total Run Time", + value="28.1", + display_value="28.1 h", + unit="h", + ), + "EngineTotalRunTimeLoaded": OncueSensor( + name="EngineTotalRunTimeLoaded", + display_name="Engine Total Run Time Loaded", + value="5.5", + display_value="5.5 h", + unit="h", + ), + "EngineTotalNumberOfStarts": OncueSensor( + name="EngineTotalNumberOfStarts", + display_name="Engine Total Number Of Starts", + value="101", + display_value="101", + unit=None, + ), + "GensetTotalEnergy": OncueSensor( + name="GensetTotalEnergy", + display_name="Genset Total Energy", + value="1.2022309E7", + display_value="1.2022309E7 kWh", + unit="kWh", + ), + "AtsContactorPosition": OncueSensor( + name="AtsContactorPosition", + display_name="Ats Contactor Position", + value="Source1", + display_value="Source1", + unit=None, + ), + "AtsSourcesAvailable": OncueSensor( + name="AtsSourcesAvailable", + display_name="Ats Sources Available", + value="Source1", + display_value="Source1", + unit=None, + ), + "Source1VoltageAverageLineToLine": OncueSensor( + name="Source1VoltageAverageLineToLine", + display_name="Source1 Voltage Average Line To Line", + value="253.5", + display_value="253.5 V", + unit="V", + ), + "Source2VoltageAverageLineToLine": OncueSensor( + name="Source2VoltageAverageLineToLine", + display_name="Source2 Voltage Average Line To Line", + value="0.0", + display_value="0.0 V", + unit="V", + ), + "IPAddress": OncueSensor( + name="IPAddress", + display_name="IP Address", + value="1.2.3.4:1026", + display_value="1.2.3.4:1026", + unit=None, + ), + "MacAddress": OncueSensor( + name="MacAddress", + display_name="Mac Address", + value="--", + display_value="--", + unit=None, + ), + "ConnectedServerIPAddress": OncueSensor( + name="ConnectedServerIPAddress", + display_name="Connected Server IP Address", + value="40.117.195.28", + display_value="40.117.195.28", + unit=None, + ), + "NetworkConnectionEstablished": OncueSensor( + name="NetworkConnectionEstablished", + display_name="Network Connection Established", + value="true", + display_value="True", + unit=None, + ), + "SerialNumber": OncueSensor( + name="SerialNumber", + display_name="Serial Number", + value="1073879692", + display_value="1073879692", + unit=None, + ), + }, + ) +} + + def _patch_login_and_data(): @contextmanager def _patcher(): @@ -279,3 +544,15 @@ def _patch_login_and_data(): yield return _patcher() + + +def _patch_login_and_data_offline_device(): + @contextmanager + def _patcher(): + with patch("homeassistant.components.oncue.Oncue.async_login",), patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + return_value=MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE, + ): + yield + + return _patcher() diff --git a/tests/components/oncue/test_sensor.py b/tests/components/oncue/test_sensor.py index 5fe8b807c1b..60c9f68f81b 100644 --- a/tests/components/oncue/test_sensor.py +++ b/tests/components/oncue/test_sensor.py @@ -1,19 +1,29 @@ """Tests for the oncue sensor.""" from __future__ import annotations +import pytest + from homeassistant.components import oncue from homeassistant.components.oncue.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from . import _patch_login_and_data +from . import _patch_login_and_data, _patch_login_and_data_offline_device from tests.common import MockConfigEntry -async def test_sensors(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "patcher, connections", + [ + [_patch_login_and_data, {("mac", "c9:24:22:6f:14:00")}], + [_patch_login_and_data_offline_device, set()], + ], +) +async def test_sensors(hass: HomeAssistant, patcher, connections) -> None: """Test that the sensors are setup with the expected values.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -21,11 +31,17 @@ async def test_sensors(hass: HomeAssistant) -> None: unique_id="any", ) config_entry.add_to_hass(hass) - with _patch_login_and_data(): + with patcher(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.LOADED + entity_registry = er.async_get(hass) + ent = entity_registry.async_get("sensor.my_generator_latest_firmware") + device_registry = dr.async_get(hass) + dev = device_registry.async_get(ent.device_id) + assert dev.connections == connections + assert len(hass.states.async_all("sensor")) == 25 assert hass.states.get("sensor.my_generator_latest_firmware").state == "2.0.6" From 08382204a348453cec7630fe144e947ecf8f1b26 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jun 2022 10:26:50 +0200 Subject: [PATCH 094/107] Don't attempt to reload MQTT device tracker (#73577) --- homeassistant/components/mqtt/__init__.py | 3 ++- homeassistant/components/mqtt/const.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f9a6ebd025f..95cf621f456 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -76,6 +76,7 @@ from .const import ( # noqa: F401 MQTT_DISCONNECTED, MQTT_RELOADED, PLATFORMS, + RELOADABLE_PLATFORMS, ) from .models import ( # noqa: F401 MqttCommandTemplate, @@ -378,7 +379,7 @@ async def async_setup_entry( # noqa: C901 # Setup reload service. Once support for legacy config is removed in 2022.9, we # should no longer call async_setup_reload_service but instead implement a custom # service - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + await async_setup_reload_service(hass, DOMAIN, RELOADABLE_PLATFORMS) async def _async_reload_platforms(_: Event | None) -> None: """Discover entities for a platform.""" diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index b05fd867eeb..67a9208faba 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -92,3 +92,23 @@ PLATFORMS = [ Platform.SWITCH, Platform.VACUUM, ] + +RELOADABLE_PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.LIGHT, + Platform.LOCK, + Platform.NUMBER, + Platform.SELECT, + Platform.SCENE, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, + Platform.VACUUM, +] From 8d41f8bbc24ce2eaf35339262795775e9a989d40 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jun 2022 08:52:37 +0200 Subject: [PATCH 095/107] Fix handling of illegal dates in onvif sensor (#73600) * Fix handling of illegal dates in onvif sensor * Address review comment * Address review comment --- homeassistant/components/onvif/parsers.py | 38 ++++++++++++++--------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index 5141f25cbef..87b901d2c52 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -1,4 +1,6 @@ """ONVIF event parsers.""" +from __future__ import annotations + from collections.abc import Callable, Coroutine import datetime from typing import Any @@ -12,16 +14,16 @@ from .models import Event PARSERS: Registry[str, Callable[[str, Any], Coroutine[Any, Any, Event]]] = Registry() -def datetime_or_zero(value: str) -> datetime: - """Convert strings to datetimes, if invalid, return datetime.min.""" +def local_datetime_or_none(value: str) -> datetime.datetime | None: + """Convert strings to datetimes, if invalid, return None.""" # To handle cameras that return times like '0000-00-00T00:00:00Z' (e.g. hikvision) try: ret = dt_util.parse_datetime(value) except ValueError: - return datetime.datetime.min - if ret is None: - return datetime.datetime.min - return ret + return None + if ret is not None: + return dt_util.as_local(ret) + return None @PARSERS.register("tns1:VideoSource/MotionAlarm") @@ -394,14 +396,16 @@ async def async_parse_last_reboot(uid: str, msg) -> Event: Topic: tns1:Monitoring/OperatingTime/LastReboot """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Reboot", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): @@ -416,14 +420,16 @@ async def async_parse_last_reset(uid: str, msg) -> Event: Topic: tns1:Monitoring/OperatingTime/LastReset """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Reset", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, entity_enabled=False, ) @@ -440,14 +446,16 @@ async def async_parse_backup_last(uid: str, msg) -> Event: """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Backup", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, entity_enabled=False, ) @@ -463,14 +471,16 @@ async def async_parse_last_clock_sync(uid: str, msg) -> Event: Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Clock Synchronization", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, entity_enabled=False, ) From 01053418f6d267af7918eb71674198a29a6ea8ec Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 17 Jun 2022 07:40:02 +0200 Subject: [PATCH 096/107] Fix voltage and current values for Fritz!DECT smart plugs (#73608) fix voltage and current values --- homeassistant/components/fritzbox/sensor.py | 4 +- tests/components/fritzbox/test_switch.py | 65 +++++++++++++++------ 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 2ae7f9dccc8..161dfc196d2 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -96,7 +96,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.voltage / 1000 + native_value=lambda device: device.voltage if getattr(device, "voltage", None) else 0.0, ), @@ -107,7 +107,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.power / device.voltage + native_value=lambda device: device.power / device.voltage / 1000 if device.power and getattr(device, "voltage", None) else 0.0, ), diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index a3135dd61f3..75799a08d48 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -16,6 +16,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, POWER_WATT, SERVICE_TURN_OFF, @@ -48,29 +50,54 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME assert ATTR_STATE_CLASS not in state.attributes - state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature") - assert state - assert state.state == "1.23" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT - state = hass.states.get(f"{ENTITY_ID}_humidity") assert state is None - state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_power_consumption") - assert state - assert state.state == "5.678" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Power Consumption" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + sensors = ( + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature", + "1.23", + f"{CONF_FAKE_NAME} Temperature", + TEMP_CELSIUS, + SensorStateClass.MEASUREMENT, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_power_consumption", + "5.678", + f"{CONF_FAKE_NAME} Power Consumption", + POWER_WATT, + SensorStateClass.MEASUREMENT, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy", + "1.234", + f"{CONF_FAKE_NAME} Total Energy", + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_voltage", + "230", + f"{CONF_FAKE_NAME} Voltage", + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_electric_current", + "0.0246869565217391", + f"{CONF_FAKE_NAME} Electric Current", + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + ], + ) - state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy") - assert state - assert state.state == "1.234" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Total Energy" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING + for sensor in sensors: + state = hass.states.get(sensor[0]) + assert state + assert state.state == sensor[1] + assert state.attributes[ATTR_FRIENDLY_NAME] == sensor[2] + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == sensor[3] + assert state.attributes[ATTR_STATE_CLASS] == sensor[4] async def test_turn_on(hass: HomeAssistant, fritz: Mock): From 155117758191ff75eef80d09a4a5e6d67dba16ab Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 20 Jun 2022 08:51:12 +0200 Subject: [PATCH 097/107] Fix MQTT config schema to ensure correct validation (#73619) * Ensure config schema validation * Use correct schema for device_tracker * Remove schema validation from the platform setup * Remove loop to build schema --- homeassistant/components/mqtt/__init__.py | 6 +- .../components/mqtt/alarm_control_panel.py | 2 +- .../components/mqtt/binary_sensor.py | 4 +- homeassistant/components/mqtt/button.py | 4 +- homeassistant/components/mqtt/camera.py | 4 +- homeassistant/components/mqtt/climate.py | 4 +- homeassistant/components/mqtt/config.py | 107 +--------- .../components/mqtt/config_integration.py | 193 ++++++++++++++++++ homeassistant/components/mqtt/cover.py | 2 +- .../mqtt/device_tracker/__init__.py | 1 + .../mqtt/device_tracker/schema_discovery.py | 2 +- homeassistant/components/mqtt/fan.py | 4 +- homeassistant/components/mqtt/humidifier.py | 4 +- .../components/mqtt/light/__init__.py | 2 +- homeassistant/components/mqtt/lock.py | 2 +- homeassistant/components/mqtt/mixins.py | 22 +- homeassistant/components/mqtt/number.py | 4 +- homeassistant/components/mqtt/scene.py | 2 +- homeassistant/components/mqtt/select.py | 4 +- homeassistant/components/mqtt/sensor.py | 4 +- homeassistant/components/mqtt/siren.py | 2 +- homeassistant/components/mqtt/switch.py | 4 +- .../components/mqtt/vacuum/__init__.py | 4 +- tests/components/mqtt/test_humidifier.py | 13 ++ tests/components/mqtt/test_init.py | 58 +++--- 25 files changed, 262 insertions(+), 196 deletions(-) create mode 100644 homeassistant/components/mqtt/config_integration.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 95cf621f456..fc4e15f3237 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -47,7 +47,11 @@ from .client import ( # noqa: F401 publish, subscribe, ) -from .config import CONFIG_SCHEMA_BASE, DEFAULT_VALUES, DEPRECATED_CONFIG_KEYS +from .config_integration import ( + CONFIG_SCHEMA_BASE, + DEFAULT_VALUES, + DEPRECATED_CONFIG_KEYS, +) from .const import ( # noqa: F401 ATTR_PAYLOAD, ATTR_QOS, diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c0c6f9732d7..6bb7d9cd0d1 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -148,7 +148,7 @@ async def async_setup_entry( """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, alarm.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index cec065e20f2..39fd87c8b02 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -103,9 +103,7 @@ async def async_setup_entry( """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, binary_sensor.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index afa9900db35..370243c3579 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -83,9 +83,7 @@ async def async_setup_entry( """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, button.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 86db828b111..5c8d3bc48b2 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -81,9 +81,7 @@ async def async_setup_entry( """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, camera.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index bdcc82f2c39..a26e9cba8df 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -392,9 +392,7 @@ async def async_setup_entry( """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, climate.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py index 4f84d911418..8cfc3490f0c 100644 --- a/homeassistant/components/mqtt/config.py +++ b/homeassistant/components/mqtt/config.py @@ -3,126 +3,21 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_PORT, - CONF_PROTOCOL, - CONF_USERNAME, - CONF_VALUE_TEMPLATE, -) +from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.helpers import config_validation as cv from .const import ( - ATTR_PAYLOAD, - ATTR_QOS, - ATTR_RETAIN, - ATTR_TOPIC, - CONF_BIRTH_MESSAGE, - CONF_BROKER, - CONF_CERTIFICATE, - CONF_CLIENT_CERT, - CONF_CLIENT_KEY, CONF_COMMAND_TOPIC, - CONF_DISCOVERY_PREFIX, CONF_ENCODING, - CONF_KEEPALIVE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_TLS_INSECURE, - CONF_TLS_VERSION, - CONF_WILL_MESSAGE, - DEFAULT_BIRTH, - DEFAULT_DISCOVERY, DEFAULT_ENCODING, - DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, - DEFAULT_WILL, - PLATFORMS, - PROTOCOL_31, - PROTOCOL_311, ) from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic -DEFAULT_PORT = 1883 -DEFAULT_KEEPALIVE = 60 -DEFAULT_PROTOCOL = PROTOCOL_311 -DEFAULT_TLS_PROTOCOL = "auto" - -DEFAULT_VALUES = { - CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, - CONF_DISCOVERY: DEFAULT_DISCOVERY, - CONF_PORT: DEFAULT_PORT, - CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, - CONF_WILL_MESSAGE: DEFAULT_WILL, -} - -CLIENT_KEY_AUTH_MSG = ( - "client_key and client_cert must both be present in " - "the MQTT broker configuration" -) - -MQTT_WILL_BIRTH_SCHEMA = vol.Schema( - { - vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, - vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - }, - required=True, -) - -PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( - {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} -) - -CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( - { - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( - vol.Coerce(int), vol.Range(min=15) - ), - vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), - vol.Inclusive( - CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Inclusive( - CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) - ), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY): cv.boolean, - # discovery_prefix must be a valid publish topic because if no - # state topic is specified, it will be created with the given prefix. - vol.Optional( - CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX - ): valid_publish_topic, - } -) - -DEPRECATED_CONFIG_KEYS = [ - CONF_BIRTH_MESSAGE, - CONF_BROKER, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_PORT, - CONF_TLS_VERSION, - CONF_USERNAME, - CONF_WILL_MESSAGE, -] - SCHEMA_BASE = { vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py new file mode 100644 index 00000000000..ab685a63802 --- /dev/null +++ b/homeassistant/components/mqtt/config_integration.py @@ -0,0 +1,193 @@ +"""Support for MQTT platform config setup.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, + Platform, +) +from homeassistant.helpers import config_validation as cv + +from . import ( + alarm_control_panel as alarm_control_panel_platform, + binary_sensor as binary_sensor_platform, + button as button_platform, + camera as camera_platform, + climate as climate_platform, + cover as cover_platform, + device_tracker as device_tracker_platform, + fan as fan_platform, + humidifier as humidifier_platform, + light as light_platform, + lock as lock_platform, + number as number_platform, + scene as scene_platform, + select as select_platform, + sensor as sensor_platform, + siren as siren_platform, + switch as switch_platform, + vacuum as vacuum_platform, +) +from .const import ( + ATTR_PAYLOAD, + ATTR_QOS, + ATTR_RETAIN, + ATTR_TOPIC, + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, + CONF_DISCOVERY_PREFIX, + CONF_KEEPALIVE, + CONF_TLS_INSECURE, + CONF_TLS_VERSION, + CONF_WILL_MESSAGE, + DEFAULT_BIRTH, + DEFAULT_DISCOVERY, + DEFAULT_PREFIX, + DEFAULT_QOS, + DEFAULT_RETAIN, + DEFAULT_WILL, + PROTOCOL_31, + PROTOCOL_311, +) +from .util import _VALID_QOS_SCHEMA, valid_publish_topic + +DEFAULT_PORT = 1883 +DEFAULT_KEEPALIVE = 60 +DEFAULT_PROTOCOL = PROTOCOL_311 +DEFAULT_TLS_PROTOCOL = "auto" + +DEFAULT_VALUES = { + CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, + CONF_DISCOVERY: DEFAULT_DISCOVERY, + CONF_PORT: DEFAULT_PORT, + CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, + CONF_WILL_MESSAGE: DEFAULT_WILL, +} + +PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( + { + Platform.ALARM_CONTROL_PANEL.value: vol.All( + cv.ensure_list, [alarm_control_panel_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.BINARY_SENSOR.value: vol.All( + cv.ensure_list, [binary_sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.BUTTON.value: vol.All( + cv.ensure_list, [button_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.CAMERA.value: vol.All( + cv.ensure_list, [camera_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.CLIMATE.value: vol.All( + cv.ensure_list, [climate_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.COVER.value: vol.All( + cv.ensure_list, [cover_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.DEVICE_TRACKER.value: vol.All( + cv.ensure_list, [device_tracker_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.FAN.value: vol.All( + cv.ensure_list, [fan_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.HUMIDIFIER.value: vol.All( + cv.ensure_list, [humidifier_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.LOCK.value: vol.All( + cv.ensure_list, [lock_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.LIGHT.value: vol.All( + cv.ensure_list, [light_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.NUMBER.value: vol.All( + cv.ensure_list, [number_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SCENE.value: vol.All( + cv.ensure_list, [scene_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SELECT.value: vol.All( + cv.ensure_list, [select_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SENSOR.value: vol.All( + cv.ensure_list, [sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SIREN.value: vol.All( + cv.ensure_list, [siren_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SWITCH.value: vol.All( + cv.ensure_list, [switch_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.VACUUM.value: vol.All( + cv.ensure_list, [vacuum_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + } +) + + +CLIENT_KEY_AUTH_MSG = ( + "client_key and client_cert must both be present in " + "the MQTT broker configuration" +) + +MQTT_WILL_BIRTH_SCHEMA = vol.Schema( + { + vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, + vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + }, + required=True, +) + +CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( + { + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( + vol.Coerce(int), vol.Range(min=15) + ), + vol.Optional(CONF_BROKER): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), + vol.Inclusive( + CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Inclusive( + CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Optional(CONF_TLS_INSECURE): cv.boolean, + vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( + cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) + ), + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. + vol.Optional( + CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX + ): valid_publish_topic, + } +) + +DEPRECATED_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_TLS_VERSION, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 8d4df0c301d..0901a4f63a6 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -241,7 +241,7 @@ async def async_setup_entry( """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, cover.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py index bcd5bbd4ee1..1b6c2b25ff3 100644 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -4,6 +4,7 @@ import voluptuous as vol from homeassistant.components import device_tracker from ..mixins import warn_for_legacy_schema +from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401 from .schema_discovery import async_setup_entry_from_discovery from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 1b48e15b80e..1ba540c8243 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -54,7 +54,7 @@ async def async_setup_entry_from_discovery(hass, config_entry, async_add_entitie *( _async_setup_entity(hass, async_add_entities, config, config_entry) for config in await async_get_platform_config_from_yaml( - hass, device_tracker.DOMAIN, PLATFORM_SCHEMA_MODERN + hass, device_tracker.DOMAIN ) ) ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index d0b4ff10692..4e1d1465ac2 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -231,9 +231,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN) - ) + config_entry.async_on_unload(await async_setup_platform_discovery(hass, fan.DOMAIN)) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 1c9ec5dc201..d2856767cf0 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -188,9 +188,7 @@ async def async_setup_entry( """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, humidifier.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 158ea6ffa0d..d4914cb9506 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry( """Set up MQTT lights configured under the light platform key (deprecated).""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, light.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 862e76635f7..ab9eca9aafa 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -103,7 +103,7 @@ async def async_setup_entry( """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, lock.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index e768c2ff409..dcf387eb360 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -10,7 +10,6 @@ from typing import Any, Protocol, cast, final import voluptuous as vol -from homeassistant.config import async_log_exception from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CONFIGURATION_URL, @@ -263,7 +262,7 @@ class SetupEntity(Protocol): async def async_setup_platform_discovery( - hass: HomeAssistant, platform_domain: str, schema: vol.Schema + hass: HomeAssistant, platform_domain: str ) -> CALLBACK_TYPE: """Set up platform discovery for manual config.""" @@ -282,7 +281,7 @@ async def async_setup_platform_discovery( *( discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) for config in await async_get_platform_config_from_yaml( - hass, platform_domain, schema, config_yaml + hass, platform_domain, config_yaml ) ) ) @@ -295,32 +294,17 @@ async def async_setup_platform_discovery( async def async_get_platform_config_from_yaml( hass: HomeAssistant, platform_domain: str, - schema: vol.Schema, config_yaml: ConfigType = None, ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" - def async_validate_config( - hass: HomeAssistant, - config: list[ConfigType], - ) -> list[ConfigType]: - """Validate config.""" - validated_config = [] - for config_item in config: - try: - validated_config.append(schema(config_item)) - except vol.MultipleInvalid as err: - async_log_exception(err, platform_domain, config_item, hass) - - return validated_config - if config_yaml is None: config_yaml = hass.data.get(DATA_MQTT_CONFIG) if not config_yaml: return [] if not (platform_configs := config_yaml.get(platform_domain)): return [] - return async_validate_config(hass, platform_configs) + return platform_configs async def async_setup_entry_helper(hass, domain, async_setup, schema): diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 1404dc86a3c..7ec07394aa5 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -134,9 +134,7 @@ async def async_setup_entry( """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, number.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 9c4a212bd8e..cc911cc3431 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -80,7 +80,7 @@ async def async_setup_entry( """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, scene.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 994c11653b7..0d9f1411fd1 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -95,9 +95,7 @@ async def async_setup_entry( """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, select.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index f9e0b5151bb..672e22f632f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -148,9 +148,7 @@ async def async_setup_entry( """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, sensor.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index fef2a4fb3dd..e7b91274f4f 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -144,7 +144,7 @@ async def async_setup_entry( """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, siren.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index be7fc655e1e..dadd5f86f20 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -98,9 +98,7 @@ async def async_setup_entry( """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, switch.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 206a15a024a..694e9530939 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -92,9 +92,7 @@ async def async_setup_entry( """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, vacuum.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index ea9a6edf0e3..cea5bfa1787 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -13,6 +13,7 @@ from homeassistant.components.humidifier import ( SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, ) +from homeassistant.components.mqtt import CONFIG_SCHEMA from homeassistant.components.mqtt.humidifier import ( CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, @@ -1277,3 +1278,15 @@ async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): hass, caplog, tmp_path, platform, config ) assert hass.states.get(f"{platform}.test") is not None + + +async def test_config_schema_validation(hass): + """Test invalid platform options in the config schema do pass the config validation.""" + platform = humidifier.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + CONFIG_SCHEMA({DOMAIN: {platform: config}}) + CONFIG_SCHEMA({DOMAIN: {platform: [config]}}) + with pytest.raises(MultipleInvalid): + CONFIG_SCHEMA({"mqtt": {"humidifier": [{"bla": "bla"}]}}) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 861b50d2f71..e9625e0fdb2 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -14,7 +14,7 @@ import yaml from homeassistant import config as hass_config from homeassistant.components import mqtt -from homeassistant.components.mqtt import debug_info +from homeassistant.components.mqtt import CONFIG_SCHEMA, debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.const import ( @@ -1362,56 +1362,47 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): assert calls_username_password_set[0][1] == "somepassword" -async def test_setup_manual_mqtt_with_platform_key(hass, caplog, tmp_path): +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_setup_manual_mqtt_with_platform_key(hass, caplog): """Test set up a manual MQTT item with a platform key.""" config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} - await help_test_setup_manual_entity_from_yaml( - hass, - caplog, - tmp_path, - "light", - config, - ) + with pytest.raises(AssertionError): + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert ( - "Invalid config for [light]: [platform] is an invalid option for [light]. " - "Check: light->platform. (See ?, line ?)" in caplog.text + "Invalid config for [mqtt]: [platform] is an invalid option for [mqtt]" + in caplog.text ) -async def test_setup_manual_mqtt_with_invalid_config(hass, caplog, tmp_path): +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): """Test set up a manual MQTT item with an invalid config.""" config = {"name": "test"} - await help_test_setup_manual_entity_from_yaml( - hass, - caplog, - tmp_path, - "light", - config, - ) + with pytest.raises(AssertionError): + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert ( - "Invalid config for [light]: required key not provided @ data['command_topic']." + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']." " Got None. (See ?, line ?)" in caplog.text ) -async def test_setup_manual_mqtt_empty_platform(hass, caplog, tmp_path): +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_setup_manual_mqtt_empty_platform(hass, caplog): """Test set up a manual MQTT platform without items.""" - config = None - await help_test_setup_manual_entity_from_yaml( - hass, - caplog, - tmp_path, - "light", - config, - ) + config = [] + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert "voluptuous.error.MultipleInvalid" not in caplog.text +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_mqtt_client_protocol(hass): """Test MQTT client protocol setup.""" entry = MockConfigEntry( domain=mqtt.DOMAIN, - data={mqtt.CONF_BROKER: "test-broker", mqtt.config.CONF_PROTOCOL: "3.1"}, + data={ + mqtt.CONF_BROKER: "test-broker", + mqtt.config_integration.CONF_PROTOCOL: "3.1", + }, ) with patch("paho.mqtt.client.Client") as mock_client: mock_client.on_connect(return_value=0) @@ -2617,3 +2608,10 @@ async def test_one_deprecation_warning_per_platform( ): count += 1 assert count == 1 + + +async def test_config_schema_validation(hass): + """Test invalid platform options in the config schema do not pass the config validation.""" + config = {"mqtt": {"sensor": [{"some_illegal_topic": "mystate/topic/path"}]}} + with pytest.raises(vol.MultipleInvalid): + CONFIG_SCHEMA(config) From 65c1d4812a48e876a8f63fcb3d76e679c419bb27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Jun 2022 21:57:44 -0500 Subject: [PATCH 098/107] Fix calling permanent off with nexia (#73623) * Fix calling permanent off with nexia Changelog: https://github.com/bdraco/nexia/compare/1.0.1...1.0.2 Fixes #73610 * one more --- homeassistant/components/nexia/climate.py | 4 +++- homeassistant/components/nexia/manifest.json | 2 +- homeassistant/components/nexia/switch.py | 6 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 20fcf5c6b85..33ad91e1561 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -391,7 +391,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc).""" - if hvac_mode == HVACMode.AUTO: + if hvac_mode == HVACMode.OFF: + await self._zone.call_permanent_off() + elif hvac_mode == HVACMode.AUTO: await self._zone.call_return_to_schedule() await self._zone.set_mode(mode=OPERATION_MODE_AUTO) else: diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index f9ca21d9e0b..4bae2d9a15d 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==1.0.1"], + "requirements": ["nexia==1.0.2"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index 380fea8c4a0..e242032c947 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from nexia.const import OPERATION_MODE_OFF from nexia.home import NexiaHome from nexia.thermostat import NexiaThermostat from nexia.zone import NexiaThermostatZone @@ -58,7 +59,10 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Enable permanent hold.""" - await self._zone.call_permanent_hold() + if self._zone.get_current_mode() == OPERATION_MODE_OFF: + await self._zone.call_permanent_off() + else: + await self._zone.call_permanent_hold() self._signal_zone_update() async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 20dd9ae64ff..dbffe63f9fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1080,7 +1080,7 @@ nettigo-air-monitor==1.2.4 neurio==0.3.1 # homeassistant.components.nexia -nexia==1.0.1 +nexia==1.0.2 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe7002d422a..73d4d24ca01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,7 +745,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.2.4 # homeassistant.components.nexia -nexia==1.0.1 +nexia==1.0.2 # homeassistant.components.discord nextcord==2.0.0a8 From 9e4ee0d36d11a8b4ad58710cca9d6f3c48eaf1e5 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 17 Jun 2022 18:26:25 +0200 Subject: [PATCH 099/107] Don't verify ssl certificates for ssdp/upnp devices (#73647) --- homeassistant/components/ssdp/__init__.py | 2 +- homeassistant/components/upnp/device.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index e854472f21f..d221cb162f4 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -351,7 +351,7 @@ class Scanner: async def async_start(self) -> None: """Start the scanners.""" - session = async_get_clientsession(self.hass) + session = async_get_clientsession(self.hass, verify_ssl=False) requester = AiohttpSessionRequester(session, True, 10) self._description_cache = DescriptionCache(requester) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 334d870939f..3a688b8571d 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -48,7 +48,7 @@ async def async_get_mac_address_from_host(hass: HomeAssistant, host: str) -> str async def async_create_device(hass: HomeAssistant, ssdp_location: str) -> Device: """Create UPnP/IGD device.""" - session = async_get_clientsession(hass) + session = async_get_clientsession(hass, verify_ssl=False) requester = AiohttpSessionRequester(session, with_sleep=True, timeout=20) factory = UpnpFactory(requester, disable_state_variable_validation=True) From eda2c8cb8f9f39817b3dceaaca8549b6d5e299d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Jun 2022 14:03:42 -0500 Subject: [PATCH 100/107] Retry on SenseAPIException during sense config entry setup (#73651) --- homeassistant/components/sense/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 2e0ed4622e8..e938f7132e0 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -5,6 +5,7 @@ import logging from sense_energy import ( ASyncSenseable, + SenseAPIException, SenseAuthenticationException, SenseMFARequiredException, ) @@ -84,6 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( str(err) or "Timed out during authentication" ) from err + except SenseAPIException as err: + raise ConfigEntryNotReady(str(err)) from err sense_devices_data = SenseDevicesData() try: From 14c11cd13a96d310a0fe799dafb22920ae7362b0 Mon Sep 17 00:00:00 2001 From: Max Gashkov Date: Mon, 20 Jun 2022 16:05:28 +0900 Subject: [PATCH 101/107] Fix AmbiClimate services definition (#73668) --- homeassistant/components/ambiclimate/services.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ambiclimate/services.yaml b/homeassistant/components/ambiclimate/services.yaml index f75857e4d2e..e5532ae82f9 100644 --- a/homeassistant/components/ambiclimate/services.yaml +++ b/homeassistant/components/ambiclimate/services.yaml @@ -5,7 +5,7 @@ set_comfort_mode: description: > Enable comfort mode on your AC. fields: - Name: + name: description: > String with device name. required: true @@ -18,14 +18,14 @@ send_comfort_feedback: description: > Send feedback for comfort mode. fields: - Name: + name: description: > String with device name. required: true example: Bedroom selector: text: - Value: + value: description: > Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing required: true @@ -38,14 +38,14 @@ set_temperature_mode: description: > Enable temperature mode on your AC. fields: - Name: + name: description: > String with device name. required: true example: Bedroom selector: text: - Value: + value: description: > Target value in celsius required: true From d211399056ea21c29b9170cfed71fe66e3110dcc Mon Sep 17 00:00:00 2001 From: micha91 Date: Mon, 20 Jun 2022 10:36:04 +0200 Subject: [PATCH 102/107] Update aiomusiccast (#73694) --- homeassistant/components/yamaha_musiccast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 86115e77988..8c0b55def69 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -3,7 +3,7 @@ "name": "MusicCast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", - "requirements": ["aiomusiccast==0.14.3"], + "requirements": ["aiomusiccast==0.14.4"], "ssdp": [ { "manufacturer": "Yamaha Corporation" diff --git a/requirements_all.txt b/requirements_all.txt index dbffe63f9fa..47be523361f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -196,7 +196,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.3 +aiomusiccast==0.14.4 # homeassistant.components.nanoleaf aionanoleaf==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73d4d24ca01..aa7eaf87853 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.3 +aiomusiccast==0.14.4 # homeassistant.components.nanoleaf aionanoleaf==0.2.0 From 5c71de8055f0516ac0fc2656f3b72989e230bc3f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 20 Jun 2022 10:31:19 +0200 Subject: [PATCH 103/107] Fix CSRF token for UniFi (#73716) Bump aiounifi to v32 --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index e779dca22f0..d481f0d0fc4 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==31"], + "requirements": ["aiounifi==32"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 47be523361f..c42bbe6c076 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -256,7 +256,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==31 +aiounifi==32 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa7eaf87853..a1efb0f1941 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -225,7 +225,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==31 +aiounifi==32 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 29f8493a0654f2279bdd21fe547bebc9a4be569f Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 22 Jun 2022 04:04:11 -0400 Subject: [PATCH 104/107] Insteon bug fixes (#73791) --- homeassistant/components/insteon/api/properties.py | 9 ++++----- homeassistant/components/insteon/manifest.json | 4 ++-- homeassistant/components/insteon/utils.py | 5 ++--- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/insteon/test_api_properties.py | 4 ++-- 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py index 47def71c1ab..8e697a88459 100644 --- a/homeassistant/components/insteon/api/properties.py +++ b/homeassistant/components/insteon/api/properties.py @@ -99,8 +99,6 @@ def get_properties(device: Device, show_advanced=False): continue prop_schema = get_schema(prop, name, device.groups) - if name == "momentary_delay": - print(prop_schema) if prop_schema is None: continue schema[name] = prop_schema @@ -216,7 +214,7 @@ async def websocket_write_properties( result = await device.async_write_config() await devices.async_save(workdir=hass.config.config_dir) - if result != ResponseStatus.SUCCESS: + if result not in [ResponseStatus.SUCCESS, ResponseStatus.RUN_ON_WAKE]: connection.send_message( websocket_api.error_message( msg[ID], "write_failed", "properties not written to device" @@ -244,9 +242,10 @@ async def websocket_load_properties( notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return - result, _ = await device.async_read_config(read_aldb=False) + result = await device.async_read_config(read_aldb=False) await devices.async_save(workdir=hass.config.config_dir) - if result != ResponseStatus.SUCCESS: + + if result not in [ResponseStatus.SUCCESS, ResponseStatus.RUN_ON_WAKE]: connection.send_message( websocket_api.error_message( msg[ID], "load_failed", "properties not loaded from device" diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index c69f4f2cdf5..1be077a6b38 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,8 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.0", - "insteon-frontend-home-assistant==0.1.0" + "pyinsteon==1.1.1", + "insteon-frontend-home-assistant==0.1.1" ], "codeowners": ["@teharris1"], "dhcp": [ diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index e8e34ee8e62..09375e7827a 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -196,9 +196,8 @@ def async_register_services(hass): for address in devices: device = devices[address] if device != devices.modem and device.cat != 0x03: - await device.aldb.async_load( - refresh=reload, callback=async_srv_save_devices - ) + await device.aldb.async_load(refresh=reload) + await async_srv_save_devices() async def async_srv_save_devices(): """Write the Insteon device configuration to file.""" diff --git a/requirements_all.txt b/requirements_all.txt index c42bbe6c076..b52f43924d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -885,7 +885,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.0 +insteon-frontend-home-assistant==0.1.1 # homeassistant.components.intellifire intellifire4py==1.0.2 @@ -1556,7 +1556,7 @@ pyialarmxr-homeassistant==1.0.18 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0 +pyinsteon==1.1.1 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1efb0f1941..1f70484f554 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.0 +insteon-frontend-home-assistant==0.1.1 # homeassistant.components.intellifire intellifire4py==1.0.2 @@ -1044,7 +1044,7 @@ pyialarmxr-homeassistant==1.0.18 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0 +pyinsteon==1.1.1 # homeassistant.components.ipma pyipma==2.0.5 diff --git a/tests/components/insteon/test_api_properties.py b/tests/components/insteon/test_api_properties.py index 7211402e343..9088b23f42a 100644 --- a/tests/components/insteon/test_api_properties.py +++ b/tests/components/insteon/test_api_properties.py @@ -401,7 +401,7 @@ async def test_load_properties(hass, hass_ws_client, kpl_properties_data): ) device = devices["33.33.33"] - device.async_read_config = AsyncMock(return_value=(1, 1)) + device.async_read_config = AsyncMock(return_value=1) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"} @@ -418,7 +418,7 @@ async def test_load_properties_failure(hass, hass_ws_client, kpl_properties_data ) device = devices["33.33.33"] - device.async_read_config = AsyncMock(return_value=(0, 0)) + device.async_read_config = AsyncMock(return_value=0) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"} From 30383e0102fb10d744830f767bf8b74faba6bb1b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 09:54:35 +0200 Subject: [PATCH 105/107] Fix Plugwise migration error (#73812) --- homeassistant/components/plugwise/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 7eb2b1371d5..afa7451021e 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -113,7 +113,7 @@ def migrate_sensor_entities( # Migrating opentherm_outdoor_temperature to opentherm_outdoor_air_temperature sensor for device_id, device in coordinator.data.devices.items(): - if device["dev_class"] != "heater_central": + if device.get("dev_class") != "heater_central": continue old_unique_id = f"{device_id}-outdoor_temperature" From 505c4b0f770f60825ca7b2fce424bda0e37e55db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 11:50:34 +0200 Subject: [PATCH 106/107] Fix MQTT tests for RC --- tests/components/mqtt/test_init.py | 35 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index e9625e0fdb2..eb392f8c4f8 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1362,35 +1362,50 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): assert calls_username_password_set[0][1] == "somepassword" -@patch("homeassistant.components.mqtt.PLATFORMS", []) -async def test_setup_manual_mqtt_with_platform_key(hass, caplog): +async def test_setup_manual_mqtt_with_platform_key(hass, caplog, tmp_path): """Test set up a manual MQTT item with a platform key.""" config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} with pytest.raises(AssertionError): - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) assert ( "Invalid config for [mqtt]: [platform] is an invalid option for [mqtt]" in caplog.text ) -@patch("homeassistant.components.mqtt.PLATFORMS", []) -async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): +async def test_setup_manual_mqtt_with_invalid_config(hass, caplog, tmp_path): """Test set up a manual MQTT item with an invalid config.""" config = {"name": "test"} with pytest.raises(AssertionError): - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) assert ( "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']." " Got None. (See ?, line ?)" in caplog.text ) -@patch("homeassistant.components.mqtt.PLATFORMS", []) -async def test_setup_manual_mqtt_empty_platform(hass, caplog): +async def test_setup_manual_mqtt_empty_platform(hass, caplog, tmp_path): """Test set up a manual MQTT platform without items.""" - config = [] - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + config = None + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) assert "voluptuous.error.MultipleInvalid" not in caplog.text From 08ff99a1e87e444a1dd70bebadb06d2a66d6caec Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 11:50:42 +0200 Subject: [PATCH 107/107] Bumped version to 2022.6.7 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8f8a8008457..3bcf2a603b4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 17e4901b2ff..ab5ab807491 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.6 +version = 2022.6.7 url = https://www.home-assistant.io/ [options]