3322 lines
112 KiB
Python
3322 lines
112 KiB
Python
"""The tests for the MQTT component."""
|
|
import asyncio
|
|
from collections.abc import Generator
|
|
import copy
|
|
from datetime import datetime, timedelta
|
|
from functools import partial
|
|
import json
|
|
from pathlib import Path
|
|
import ssl
|
|
from typing import Any, TypedDict
|
|
from unittest.mock import ANY, MagicMock, call, mock_open, patch
|
|
|
|
import pytest
|
|
import voluptuous as vol
|
|
import yaml
|
|
|
|
from homeassistant import config as hass_config
|
|
from homeassistant.components import mqtt
|
|
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 MessageCallbackType, ReceiveMessage
|
|
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
|
|
from homeassistant.const import (
|
|
ATTR_ASSUMED_STATE,
|
|
EVENT_HOMEASSISTANT_STARTED,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
Platform,
|
|
UnitOfTemperature,
|
|
)
|
|
import homeassistant.core as ha
|
|
from homeassistant.core import CoreState, HomeAssistant, callback
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers import device_registry as dr, entity_registry as er, template
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.entity_platform import async_get_platforms
|
|
from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
|
|
from homeassistant.helpers.typing import ConfigType
|
|
from homeassistant.setup import async_setup_component
|
|
from homeassistant.util.dt import utcnow
|
|
|
|
from .test_common import (
|
|
help_test_entry_reload_with_new_config,
|
|
help_test_reload_with_config,
|
|
help_test_setup_manual_entity_from_yaml,
|
|
)
|
|
|
|
from tests.common import (
|
|
MockConfigEntry,
|
|
async_fire_mqtt_message,
|
|
async_fire_time_changed,
|
|
mock_restore_cache,
|
|
)
|
|
from tests.testing_config.custom_components.test.sensor import ( # type: ignore[attr-defined]
|
|
DEVICE_CLASSES,
|
|
)
|
|
from tests.typing import (
|
|
MqttMockHAClient,
|
|
MqttMockHAClientGenerator,
|
|
MqttMockPahoClient,
|
|
WebSocketGenerator,
|
|
)
|
|
|
|
|
|
class _DebugDeviceInfo(TypedDict, total=False):
|
|
"""Debug device info test data type."""
|
|
|
|
device: dict[str, Any]
|
|
platform: str
|
|
state_topic: str
|
|
unique_id: str
|
|
type: str
|
|
subtype: str
|
|
automation_type: str
|
|
topic: str
|
|
|
|
|
|
class _DebugInfo(TypedDict):
|
|
"""Debug info test data type."""
|
|
|
|
domain: str
|
|
config: _DebugDeviceInfo
|
|
|
|
|
|
class RecordCallsPartial(partial[Any]):
|
|
"""Wrapper class for partial."""
|
|
|
|
__name__ = "RecordCallPartialTest"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def sensor_platforms_only() -> Generator[None, None, None]:
|
|
"""Only setup the sensor platforms to speed up tests."""
|
|
with patch(
|
|
"homeassistant.components.mqtt.PLATFORMS",
|
|
[Platform.SENSOR, Platform.BINARY_SENSOR],
|
|
):
|
|
yield
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_storage(hass_storage: dict[str, Any]) -> None:
|
|
"""Autouse hass_storage for the TestCase tests."""
|
|
|
|
|
|
@pytest.fixture
|
|
def calls() -> list[ReceiveMessage]:
|
|
"""Fixture to hold recorded calls."""
|
|
return []
|
|
|
|
|
|
@pytest.fixture
|
|
def record_calls(calls: list[ReceiveMessage]) -> MessageCallbackType:
|
|
"""Fixture to record calls."""
|
|
|
|
@callback
|
|
def record_calls(msg: ReceiveMessage) -> None:
|
|
"""Record calls."""
|
|
calls.append(msg)
|
|
|
|
return record_calls
|
|
|
|
|
|
@pytest.fixture
|
|
def empty_mqtt_config(
|
|
hass: HomeAssistant, tmp_path: Path
|
|
) -> Generator[Path, None, None]:
|
|
"""Fixture to provide an empty config from yaml."""
|
|
new_yaml_config_file = tmp_path / "configuration.yaml"
|
|
new_yaml_config_file.write_text("")
|
|
|
|
with patch.object(
|
|
hass_config, "YAML_CONFIG_FILE", new_yaml_config_file
|
|
) as empty_config:
|
|
yield empty_config
|
|
|
|
|
|
async def test_mqtt_connects_on_home_assistant_mqtt_setup(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""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: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
) -> None:
|
|
"""Test if client stops on HA stop."""
|
|
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_client_mock.loop_stop.call_count == 1
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [])
|
|
async def test_mqtt_await_ack_at_disconnect(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test if ACK is awaited correctly when disconnecting."""
|
|
|
|
class FakeInfo:
|
|
"""Returns a simulated client publish response."""
|
|
|
|
mid = 100
|
|
rc = 0
|
|
|
|
with patch("paho.mqtt.client.Client") as mock_client:
|
|
mock_client().connect = MagicMock(return_value=0)
|
|
mock_client().publish = MagicMock(return_value=FakeInfo())
|
|
entry = MockConfigEntry(
|
|
domain=mqtt.DOMAIN,
|
|
data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
assert await mqtt.async_setup_entry(hass, entry)
|
|
mqtt_client = mock_client.return_value
|
|
|
|
# publish from MQTT client without awaiting
|
|
hass.async_create_task(
|
|
mqtt.async_publish(hass, "test-topic", "some-payload", 0, False)
|
|
)
|
|
await asyncio.sleep(0)
|
|
# Simulate late ACK callback from client with mid 100
|
|
mqtt_client.on_publish(0, 0, 100)
|
|
# disconnect the MQTT client
|
|
await hass.async_stop()
|
|
await hass.async_block_till_done()
|
|
# assert the payload was sent through the client
|
|
assert mqtt_client.publish.called
|
|
assert mqtt_client.publish.call_args[0] == (
|
|
"test-topic",
|
|
"some-payload",
|
|
0,
|
|
False,
|
|
)
|
|
|
|
|
|
async def test_publish(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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
|
|
assert mqtt_mock.async_publish.call_args[0] == (
|
|
"test-topic",
|
|
"test-payload",
|
|
0,
|
|
False,
|
|
)
|
|
mqtt_mock.reset_mock()
|
|
|
|
await mqtt.async_publish(hass, "test-topic", "test-payload", 2, True)
|
|
await hass.async_block_till_done()
|
|
assert mqtt_mock.async_publish.called
|
|
assert mqtt_mock.async_publish.call_args[0] == (
|
|
"test-topic",
|
|
"test-payload",
|
|
2,
|
|
True,
|
|
)
|
|
mqtt_mock.reset_mock()
|
|
|
|
mqtt.publish(hass, "test-topic2", "test-payload2")
|
|
await hass.async_block_till_done()
|
|
assert mqtt_mock.async_publish.called
|
|
assert mqtt_mock.async_publish.call_args[0] == (
|
|
"test-topic2",
|
|
"test-payload2",
|
|
0,
|
|
False,
|
|
)
|
|
mqtt_mock.reset_mock()
|
|
|
|
mqtt.publish(hass, "test-topic2", "test-payload2", 2, True)
|
|
await hass.async_block_till_done()
|
|
assert mqtt_mock.async_publish.called
|
|
assert mqtt_mock.async_publish.call_args[0] == (
|
|
"test-topic2",
|
|
"test-payload2",
|
|
2,
|
|
True,
|
|
)
|
|
mqtt_mock.reset_mock()
|
|
|
|
# test binary pass-through
|
|
mqtt.publish(
|
|
hass,
|
|
"test-topic3",
|
|
b"\xde\xad\xbe\xef",
|
|
0,
|
|
False,
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert mqtt_mock.async_publish.called
|
|
assert mqtt_mock.async_publish.call_args[0] == (
|
|
"test-topic3",
|
|
b"\xde\xad\xbe\xef",
|
|
0,
|
|
False,
|
|
)
|
|
mqtt_mock.reset_mock()
|
|
|
|
|
|
async def test_convert_outgoing_payload(hass: HomeAssistant) -> None:
|
|
"""Test the converting of outgoing MQTT payloads without template."""
|
|
command_template = mqtt.MqttCommandTemplate(None, hass=hass)
|
|
assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef"
|
|
|
|
assert (
|
|
command_template.async_render("b'\\xde\\xad\\xbe\\xef'")
|
|
== "b'\\xde\\xad\\xbe\\xef'"
|
|
)
|
|
|
|
assert command_template.async_render(1234) == 1234
|
|
|
|
assert command_template.async_render(1234.56) == 1234.56
|
|
|
|
assert command_template.async_render(None) is None
|
|
|
|
|
|
async def test_command_template_value(hass: HomeAssistant) -> None:
|
|
"""Test the rendering of MQTT command template."""
|
|
|
|
variables = {"id": 1234, "some_var": "beer"}
|
|
|
|
# test rendering value
|
|
tpl = template.Template("{{ value + 1 }}", hass=hass)
|
|
cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass=hass)
|
|
assert cmd_tpl.async_render(4321) == "4322"
|
|
|
|
# test variables at rendering
|
|
tpl = template.Template("{{ some_var }}", hass=hass)
|
|
cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass=hass)
|
|
assert cmd_tpl.async_render(None, variables=variables) == "beer"
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SELECT])
|
|
async def test_command_template_variables(
|
|
hass: HomeAssistant, mqtt_mock_entry_with_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test the rendering of entity variables."""
|
|
topic = "test/select"
|
|
|
|
fake_state = ha.State("select.test_select", "milk")
|
|
mock_restore_cache(hass, (fake_state,))
|
|
|
|
assert await async_setup_component(
|
|
hass,
|
|
mqtt.DOMAIN,
|
|
{
|
|
mqtt.DOMAIN: {
|
|
"select": {
|
|
"command_topic": topic,
|
|
"name": "Test Select",
|
|
"options": ["milk", "beer"],
|
|
"command_template": '{"option": "{{ value }}", "entity_id": "{{ entity_id }}", "name": "{{ name }}", "this_object_state": "{{ this.state }}"}',
|
|
}
|
|
}
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
mqtt_mock = await mqtt_mock_entry_with_yaml_config()
|
|
|
|
state = hass.states.get("select.test_select")
|
|
assert state and state.state == "milk"
|
|
assert state.attributes.get(ATTR_ASSUMED_STATE)
|
|
|
|
await hass.services.async_call(
|
|
"select",
|
|
"select_option",
|
|
{"entity_id": "select.test_select", "option": "beer"},
|
|
blocking=True,
|
|
)
|
|
|
|
mqtt_mock.async_publish.assert_called_once_with(
|
|
topic,
|
|
'{"option": "beer", "entity_id": "select.test_select", "name": "Test Select", "this_object_state": "milk"}',
|
|
0,
|
|
False,
|
|
)
|
|
mqtt_mock.async_publish.reset_mock()
|
|
state = hass.states.get("select.test_select")
|
|
assert state and state.state == "beer"
|
|
|
|
# Test that TemplateStateFromEntityId is not called again
|
|
with patch(
|
|
"homeassistant.helpers.template.TemplateStateFromEntityId", MagicMock()
|
|
) as template_state_calls:
|
|
await hass.services.async_call(
|
|
"select",
|
|
"select_option",
|
|
{"entity_id": "select.test_select", "option": "milk"},
|
|
blocking=True,
|
|
)
|
|
assert template_state_calls.call_count == 0
|
|
state = hass.states.get("select.test_select")
|
|
assert state and state.state == "milk"
|
|
|
|
|
|
async def test_value_template_value(hass: HomeAssistant) -> None:
|
|
"""Test the rendering of MQTT value template."""
|
|
|
|
variables = {"id": 1234, "some_var": "beer"}
|
|
|
|
# test rendering value
|
|
tpl = template.Template("{{ value_json.id }}")
|
|
val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass)
|
|
assert val_tpl.async_render_with_possible_json_value('{"id": 4321}') == "4321"
|
|
|
|
# test variables at rendering
|
|
tpl = template.Template("{{ value_json.id }} {{ some_var }} {{ code }}")
|
|
val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass, config_attributes={"code": 1234})
|
|
assert (
|
|
val_tpl.async_render_with_possible_json_value(
|
|
'{"id": 4321}', variables=variables
|
|
)
|
|
== "4321 beer 1234"
|
|
)
|
|
|
|
# test with default value if an error occurs due to an invalid template
|
|
tpl = template.Template("{{ value_json.id | as_datetime }}")
|
|
val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass)
|
|
assert (
|
|
val_tpl.async_render_with_possible_json_value('{"otherid": 4321}', "my default")
|
|
== "my default"
|
|
)
|
|
|
|
# test value template with entity
|
|
entity = Entity()
|
|
entity.hass = hass
|
|
entity.entity_id = "select.test"
|
|
tpl = template.Template("{{ value_json.id }}")
|
|
val_tpl = mqtt.MqttValueTemplate(tpl, entity=entity)
|
|
assert val_tpl.async_render_with_possible_json_value('{"id": 4321}') == "4321"
|
|
|
|
# test this object in a template
|
|
tpl2 = template.Template("{{ this.entity_id }}")
|
|
val_tpl2 = mqtt.MqttValueTemplate(tpl2, entity=entity)
|
|
assert val_tpl2.async_render_with_possible_json_value("bla") == "select.test"
|
|
|
|
with patch(
|
|
"homeassistant.helpers.template.TemplateStateFromEntityId", MagicMock()
|
|
) as template_state_calls:
|
|
tpl3 = template.Template("{{ this.entity_id }}")
|
|
val_tpl3 = mqtt.MqttValueTemplate(tpl3, entity=entity)
|
|
val_tpl3.async_render_with_possible_json_value("call1")
|
|
val_tpl3.async_render_with_possible_json_value("call2")
|
|
assert template_state_calls.call_count == 1
|
|
|
|
|
|
async def test_service_call_without_topic_does_not_publish(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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,
|
|
mqtt.SERVICE_PUBLISH,
|
|
{},
|
|
blocking=True,
|
|
)
|
|
assert not mqtt_mock.async_publish.called
|
|
|
|
|
|
async def test_service_call_with_topic_and_topic_template_does_not_publish(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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):
|
|
await hass.services.async_call(
|
|
mqtt.DOMAIN,
|
|
mqtt.SERVICE_PUBLISH,
|
|
{
|
|
mqtt.ATTR_TOPIC: topic,
|
|
mqtt.ATTR_TOPIC_TEMPLATE: topic_template,
|
|
mqtt.ATTR_PAYLOAD: "payload",
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert not mqtt_mock.async_publish.called
|
|
|
|
|
|
async def test_service_call_with_invalid_topic_template_does_not_publish(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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,
|
|
{
|
|
mqtt.ATTR_TOPIC_TEMPLATE: "test/{{ 1 | no_such_filter }}",
|
|
mqtt.ATTR_PAYLOAD: "payload",
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert not mqtt_mock.async_publish.called
|
|
|
|
|
|
async def test_service_call_with_template_topic_renders_template(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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,
|
|
{
|
|
mqtt.ATTR_TOPIC_TEMPLATE: "test/{{ 1+1 }}",
|
|
mqtt.ATTR_PAYLOAD: "payload",
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mqtt_mock.async_publish.called
|
|
assert mqtt_mock.async_publish.call_args[0][0] == "test/2"
|
|
|
|
|
|
async def test_service_call_with_template_topic_renders_invalid_topic(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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,
|
|
{
|
|
mqtt.ATTR_TOPIC_TEMPLATE: "test/{{ '+' if True else 'topic' }}/topic",
|
|
mqtt.ATTR_PAYLOAD: "payload",
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert not mqtt_mock.async_publish.called
|
|
|
|
|
|
async def test_service_call_with_invalid_rendered_template_topic_doesnt_render_template(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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):
|
|
await hass.services.async_call(
|
|
mqtt.DOMAIN,
|
|
mqtt.SERVICE_PUBLISH,
|
|
{
|
|
mqtt.ATTR_TOPIC: "test/topic",
|
|
mqtt.ATTR_PAYLOAD: payload,
|
|
mqtt.ATTR_PAYLOAD_TEMPLATE: payload_template,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert not mqtt_mock.async_publish.called
|
|
|
|
|
|
async def test_service_call_with_template_payload_renders_template(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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,
|
|
{mqtt.ATTR_TOPIC: "test/topic", mqtt.ATTR_PAYLOAD_TEMPLATE: "{{ 4+4 }}"},
|
|
blocking=True,
|
|
)
|
|
assert mqtt_mock.async_publish.called
|
|
assert mqtt_mock.async_publish.call_args[0][1] == "8"
|
|
mqtt_mock.reset_mock()
|
|
|
|
await hass.services.async_call(
|
|
mqtt.DOMAIN,
|
|
mqtt.SERVICE_PUBLISH,
|
|
{
|
|
mqtt.ATTR_TOPIC: "test/topic",
|
|
mqtt.ATTR_PAYLOAD_TEMPLATE: "{{ (4+4) | pack('B') }}",
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mqtt_mock.async_publish.called
|
|
assert mqtt_mock.async_publish.call_args[0][1] == b"\x08"
|
|
mqtt_mock.reset_mock()
|
|
|
|
|
|
async def test_service_call_with_bad_template(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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,
|
|
{mqtt.ATTR_TOPIC: "test/topic", mqtt.ATTR_PAYLOAD_TEMPLATE: "{{ 1 | bad }}"},
|
|
blocking=True,
|
|
)
|
|
assert not mqtt_mock.async_publish.called
|
|
|
|
|
|
async def test_service_call_with_payload_doesnt_render_template(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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):
|
|
await hass.services.async_call(
|
|
mqtt.DOMAIN,
|
|
mqtt.SERVICE_PUBLISH,
|
|
{
|
|
mqtt.ATTR_TOPIC: "test/topic",
|
|
mqtt.ATTR_PAYLOAD: payload,
|
|
mqtt.ATTR_PAYLOAD_TEMPLATE: payload_template,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert not mqtt_mock.async_publish.called
|
|
|
|
|
|
async def test_service_call_with_ascii_qos_retain_flags(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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,
|
|
{
|
|
mqtt.ATTR_TOPIC: "test/topic",
|
|
mqtt.ATTR_PAYLOAD: "",
|
|
mqtt.ATTR_QOS: "2",
|
|
mqtt.ATTR_RETAIN: "no",
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mqtt_mock.async_publish.called
|
|
assert mqtt_mock.async_publish.call_args[0][2] == 2
|
|
assert not mqtt_mock.async_publish.call_args[0][3]
|
|
|
|
|
|
async def test_publish_function_with_bad_encoding_conditions(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""Test internal publish function with basic use cases."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
await mqtt.async_publish(
|
|
hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None
|
|
)
|
|
assert (
|
|
"Can't pass-through payload for publishing test-payload on some-topic with no encoding set, need 'bytes' got <class 'str'>"
|
|
in caplog.text
|
|
)
|
|
caplog.clear()
|
|
await mqtt.async_publish(
|
|
hass,
|
|
"some-topic",
|
|
"test-payload",
|
|
qos=0,
|
|
retain=False,
|
|
encoding="invalid_encoding",
|
|
)
|
|
assert (
|
|
"Can't encode payload for publishing test-payload on some-topic with encoding invalid_encoding"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
def test_validate_topic() -> None:
|
|
"""Test topic name/filter validation."""
|
|
# Invalid UTF-8, must not contain U+D800 to U+DFFF.
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\ud800")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\udfff")
|
|
# Topic MUST NOT be empty
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("")
|
|
# Topic MUST NOT be longer than 65535 encoded bytes.
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("ü" * 32768)
|
|
# UTF-8 MUST NOT include null character
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("bad\0one")
|
|
|
|
# Topics "SHOULD NOT" include these special characters
|
|
# (not MUST NOT, RFC2119). The receiver MAY close the connection.
|
|
# We enforce this because mosquitto does: https://github.com/eclipse/mosquitto/commit/94fdc9cb44c829ff79c74e1daa6f7d04283dfffd
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\u0001")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\u001F")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\u007F")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\u009F")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\ufdd0")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\ufdef")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\ufffe")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\ufffe")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\uffff")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\U0001fffe")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.util.valid_topic("\U0001ffff")
|
|
|
|
|
|
def test_validate_subscribe_topic() -> None:
|
|
"""Test invalid subscribe topics."""
|
|
mqtt.valid_subscribe_topic("#")
|
|
mqtt.valid_subscribe_topic("sport/#")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_subscribe_topic("sport/#/")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_subscribe_topic("foo/bar#")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_subscribe_topic("foo/#/bar")
|
|
|
|
mqtt.valid_subscribe_topic("+")
|
|
mqtt.valid_subscribe_topic("+/tennis/#")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_subscribe_topic("sport+")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_subscribe_topic("sport+/")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_subscribe_topic("sport/+1")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_subscribe_topic("sport/+#")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_subscribe_topic("bad+topic")
|
|
mqtt.valid_subscribe_topic("sport/+/player1")
|
|
mqtt.valid_subscribe_topic("/finance")
|
|
mqtt.valid_subscribe_topic("+/+")
|
|
mqtt.valid_subscribe_topic("$SYS/#")
|
|
|
|
|
|
def test_validate_publish_topic() -> None:
|
|
"""Test invalid publish topics."""
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_publish_topic("pub+")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_publish_topic("pub/+")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_publish_topic("1#")
|
|
with pytest.raises(vol.Invalid):
|
|
mqtt.valid_publish_topic("bad+topic")
|
|
mqtt.valid_publish_topic("//")
|
|
|
|
# Topic names beginning with $ SHOULD NOT be used, but can
|
|
mqtt.valid_publish_topic("$SYS/")
|
|
|
|
|
|
def test_entity_device_info_schema() -> None:
|
|
"""Test MQTT entity device info validation."""
|
|
# just identifier
|
|
MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": ["abcd"]})
|
|
MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": "abcd"})
|
|
# just connection
|
|
MQTT_ENTITY_DEVICE_INFO_SCHEMA(
|
|
{"connections": [[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"]]}
|
|
)
|
|
# full device info
|
|
MQTT_ENTITY_DEVICE_INFO_SCHEMA(
|
|
{
|
|
"identifiers": ["helloworld", "hello"],
|
|
"connections": [
|
|
[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"],
|
|
[dr.CONNECTION_ZIGBEE, "zigbee_id"],
|
|
],
|
|
"manufacturer": "Whatever",
|
|
"name": "Beer",
|
|
"model": "Glass",
|
|
"sw_version": "0.1-beta",
|
|
"configuration_url": "http://example.com",
|
|
}
|
|
)
|
|
# full device info with via_device
|
|
MQTT_ENTITY_DEVICE_INFO_SCHEMA(
|
|
{
|
|
"identifiers": ["helloworld", "hello"],
|
|
"connections": [
|
|
[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"],
|
|
[dr.CONNECTION_ZIGBEE, "zigbee_id"],
|
|
],
|
|
"manufacturer": "Whatever",
|
|
"name": "Beer",
|
|
"model": "Glass",
|
|
"sw_version": "0.1-beta",
|
|
"via_device": "test-hub",
|
|
"configuration_url": "http://example.com",
|
|
}
|
|
)
|
|
# no identifiers
|
|
with pytest.raises(vol.Invalid):
|
|
MQTT_ENTITY_DEVICE_INFO_SCHEMA(
|
|
{
|
|
"manufacturer": "Whatever",
|
|
"name": "Beer",
|
|
"model": "Glass",
|
|
"sw_version": "0.1-beta",
|
|
}
|
|
)
|
|
# empty identifiers
|
|
with pytest.raises(vol.Invalid):
|
|
MQTT_ENTITY_DEVICE_INFO_SCHEMA(
|
|
{"identifiers": [], "connections": [], "name": "Beer"}
|
|
)
|
|
|
|
# not an valid URL
|
|
with pytest.raises(vol.Invalid):
|
|
MQTT_ENTITY_DEVICE_INFO_SCHEMA(
|
|
{
|
|
"manufacturer": "Whatever",
|
|
"name": "Beer",
|
|
"model": "Glass",
|
|
"sw_version": "0.1-beta",
|
|
"configuration_url": "fake://link",
|
|
}
|
|
)
|
|
|
|
|
|
async def test_receiving_non_utf8_message_gets_logged(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
record_calls: MessageCallbackType,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert (
|
|
"Can't decode payload b'\\x9a' on test-topic with encoding utf-8" in caplog.text
|
|
)
|
|
|
|
|
|
async def test_all_subscriptions_run_when_decode_fails(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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)
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS)
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
|
|
|
|
async def test_subscribe_topic(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == "test-topic"
|
|
assert calls[0].payload == "test-payload"
|
|
|
|
unsub()
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
|
|
# Cannot unsubscribe twice
|
|
with pytest.raises(HomeAssistantError):
|
|
unsub()
|
|
|
|
|
|
async def test_subscribe_topic_non_async(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == "test-topic"
|
|
assert calls[0].payload == "test-payload"
|
|
|
|
await hass.async_add_executor_job(unsub)
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
|
|
|
|
async def test_subscribe_bad_topic(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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) # type: ignore[arg-type]
|
|
|
|
|
|
# Support for a deprecated callback type will be removed from HA core 2023.2.0
|
|
async def test_subscribe_deprecated(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test the subscription of a topic using deprecated callback signature."""
|
|
calls: list[tuple[str, ReceivePayloadType, int]]
|
|
|
|
mqtt_mock = await mqtt_mock_entry_no_yaml_config()
|
|
|
|
async def record_calls(topic: str, payload: ReceivePayloadType, qos: int) -> None:
|
|
"""Record calls."""
|
|
calls.append((topic, payload, qos))
|
|
|
|
calls = []
|
|
unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls)
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0][0] == "test-topic"
|
|
assert calls[0][1] == "test-payload"
|
|
|
|
unsub()
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
mqtt_mock.async_publish.reset_mock()
|
|
|
|
# Test with partial wrapper
|
|
calls = []
|
|
unsub = await mqtt.async_subscribe(
|
|
hass, "test-topic", RecordCallsPartial(record_calls)
|
|
)
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0][0] == "test-topic"
|
|
assert calls[0][1] == "test-payload"
|
|
|
|
unsub()
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
|
|
|
|
# Support for a deprecated callback type will be removed from HA core 2023.2.0
|
|
async def test_subscribe_deprecated_async(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test the subscription of a topic using deprecated coroutine signature."""
|
|
calls: list[tuple[str, ReceivePayloadType, int]]
|
|
|
|
mqtt_mock = await mqtt_mock_entry_no_yaml_config()
|
|
|
|
@callback
|
|
def async_record_calls(topic: str, payload: ReceivePayloadType, qos: int) -> None:
|
|
"""Record calls."""
|
|
calls.append((topic, payload, qos))
|
|
|
|
calls = []
|
|
unsub = await mqtt.async_subscribe(hass, "test-topic", async_record_calls)
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0][0] == "test-topic"
|
|
assert calls[0][1] == "test-payload"
|
|
|
|
unsub()
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
mqtt_mock.async_publish.reset_mock()
|
|
|
|
# Test with partial wrapper
|
|
calls = []
|
|
unsub = await mqtt.async_subscribe(
|
|
hass, "test-topic", RecordCallsPartial(async_record_calls)
|
|
)
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0][0] == "test-topic"
|
|
assert calls[0][1] == "test-payload"
|
|
|
|
unsub()
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test-payload")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
|
|
|
|
async def test_subscribe_topic_not_match(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 0
|
|
|
|
|
|
async def test_subscribe_topic_level_wildcard(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == "test-topic/bier/on"
|
|
assert calls[0].payload == "test-payload"
|
|
|
|
|
|
async def test_subscribe_topic_level_wildcard_no_subtree_match(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 0
|
|
|
|
|
|
async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 0
|
|
|
|
|
|
async def test_subscribe_topic_subtree_wildcard_subtree_topic(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == "test-topic/bier/on"
|
|
assert calls[0].payload == "test-payload"
|
|
|
|
|
|
async def test_subscribe_topic_subtree_wildcard_root_topic(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == "test-topic"
|
|
assert calls[0].payload == "test-payload"
|
|
|
|
|
|
async def test_subscribe_topic_subtree_wildcard_no_match(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 0
|
|
|
|
|
|
async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == "hi/test-topic"
|
|
assert calls[0].payload == "test-payload"
|
|
|
|
|
|
async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == "hi/test-topic/here-iam"
|
|
assert calls[0].payload == "test-payload"
|
|
|
|
|
|
async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 0
|
|
|
|
|
|
async def test_subscribe_topic_level_wildcard_and_wildcard_no_match(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 0
|
|
|
|
|
|
async def test_subscribe_topic_sys_root(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == "$test-topic/subtree/on"
|
|
assert calls[0].payload == "test-payload"
|
|
|
|
|
|
async def test_subscribe_topic_sys_root_and_wildcard_topic(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == "$test-topic/some-topic"
|
|
assert calls[0].payload == "test-payload"
|
|
|
|
|
|
async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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")
|
|
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == "$test-topic/subtree/some-topic"
|
|
assert calls[0].payload == "test-payload"
|
|
|
|
|
|
async def test_subscribe_special_characters(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
calls: list[ReceiveMessage],
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""Test the subscription to topics with special characters."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
topic = "/test-topic/$(.)[^]{-}"
|
|
payload = "p4y.l[]a|> ?"
|
|
|
|
await mqtt.async_subscribe(hass, topic, record_calls)
|
|
|
|
async_fire_mqtt_message(hass, topic, payload)
|
|
await hass.async_block_till_done()
|
|
assert len(calls) == 1
|
|
assert calls[0].topic == topic
|
|
assert calls[0].payload == payload
|
|
|
|
|
|
async def test_subscribe_same_topic(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""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
|
|
|
|
calls_a = MagicMock()
|
|
await mqtt.async_subscribe(hass, "test/state", calls_a)
|
|
async_fire_mqtt_message(
|
|
hass, "test/state", "online"
|
|
) # Simulate a (retained) message
|
|
await hass.async_block_till_done()
|
|
assert calls_a.called
|
|
mqtt_client_mock.subscribe.assert_called()
|
|
calls_a.reset_mock()
|
|
mqtt_client_mock.reset_mock()
|
|
|
|
calls_b = MagicMock()
|
|
await mqtt.async_subscribe(hass, "test/state", calls_b)
|
|
async_fire_mqtt_message(
|
|
hass, "test/state", "online"
|
|
) # Simulate a (retained) message
|
|
await hass.async_block_till_done()
|
|
assert calls_a.called
|
|
assert calls_b.called
|
|
mqtt_client_mock.subscribe.assert_called()
|
|
|
|
|
|
async def test_not_calling_unsubscribe_with_active_subscribers(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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
|
|
|
|
unsub = await mqtt.async_subscribe(hass, "test/state", record_calls)
|
|
await mqtt.async_subscribe(hass, "test/state", record_calls)
|
|
await hass.async_block_till_done()
|
|
assert mqtt_client_mock.subscribe.called
|
|
|
|
unsub()
|
|
await hass.async_block_till_done()
|
|
assert not mqtt_client_mock.unsubscribe.called
|
|
|
|
|
|
async def test_unsubscribe_race(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""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
|
|
|
|
calls_a = MagicMock()
|
|
calls_b = MagicMock()
|
|
|
|
mqtt_client_mock.reset_mock()
|
|
unsub = await mqtt.async_subscribe(hass, "test/state", calls_a)
|
|
unsub()
|
|
await mqtt.async_subscribe(hass, "test/state", calls_b)
|
|
await hass.async_block_till_done()
|
|
|
|
async_fire_mqtt_message(hass, "test/state", "online")
|
|
await hass.async_block_till_done()
|
|
assert not calls_a.called
|
|
assert calls_b.called
|
|
|
|
# We allow either calls [subscribe, unsubscribe, subscribe] or [subscribe, subscribe]
|
|
expected_calls_1 = [
|
|
call.subscribe("test/state", 0),
|
|
call.unsubscribe("test/state"),
|
|
call.subscribe("test/state", 0),
|
|
]
|
|
expected_calls_2 = [
|
|
call.subscribe("test/state", 0),
|
|
call.subscribe("test/state", 0),
|
|
]
|
|
assert mqtt_client_mock.mock_calls in (expected_calls_1, expected_calls_2)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mqtt_config_entry_data",
|
|
[{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}],
|
|
)
|
|
async def test_restore_subscriptions_on_reconnect(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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
|
|
|
|
await mqtt.async_subscribe(hass, "test/state", record_calls)
|
|
await hass.async_block_till_done()
|
|
assert mqtt_client_mock.subscribe.call_count == 1
|
|
|
|
mqtt_client_mock.on_disconnect(None, None, 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
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mqtt_config_entry_data",
|
|
[{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}],
|
|
)
|
|
async def test_restore_all_active_subscriptions_on_reconnect(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""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
|
|
|
|
unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2)
|
|
await mqtt.async_subscribe(hass, "test/state", record_calls)
|
|
await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1)
|
|
await hass.async_block_till_done()
|
|
|
|
expected = [
|
|
call("test/state", 2),
|
|
call("test/state", 0),
|
|
call("test/state", 1),
|
|
]
|
|
assert mqtt_client_mock.subscribe.mock_calls == expected
|
|
|
|
unsub()
|
|
await hass.async_block_till_done()
|
|
assert mqtt_client_mock.unsubscribe.call_count == 0
|
|
|
|
mqtt_client_mock.on_disconnect(None, None, 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()
|
|
|
|
expected.append(call("test/state", 1))
|
|
assert mqtt_client_mock.subscribe.mock_calls == expected
|
|
|
|
|
|
async def test_initial_setup_logs_error(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
empty_mqtt_config: Path,
|
|
) -> None:
|
|
"""Test for setup failure if initial client connection fails."""
|
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
|
|
entry.add_to_hass(hass)
|
|
mqtt_client_mock.connect.return_value = 1
|
|
try:
|
|
assert await mqtt.async_setup_entry(hass, entry)
|
|
await hass.async_block_till_done()
|
|
except HomeAssistantError:
|
|
assert True
|
|
assert "Failed to connect to MQTT server:" in caplog.text
|
|
|
|
|
|
async def test_logs_error_if_no_connect_broker(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
) -> None:
|
|
"""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()
|
|
assert (
|
|
"Unable to connect to the MQTT broker: Connection Refused: broker unavailable."
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3)
|
|
async def test_handle_mqtt_on_callback(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
) -> None:
|
|
"""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()
|
|
# Make sure the ACK has been received
|
|
await hass.async_block_till_done()
|
|
# Now call publish without call back, this will call _wait_for_mid(msg_info.mid)
|
|
await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload")
|
|
# Since the mid event was already set, we should not see any timeout warning in the log
|
|
await hass.async_block_till_done()
|
|
assert "No ACK from MQTT server" not in caplog.text
|
|
|
|
|
|
async def test_publish_error(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test publish error."""
|
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
|
|
entry.add_to_hass(hass)
|
|
|
|
# simulate an Out of memory error
|
|
with patch("paho.mqtt.client.Client") as mock_client:
|
|
mock_client().connect = lambda *args: 1
|
|
mock_client().publish().rc = 1
|
|
assert await mqtt.async_setup_entry(hass, entry)
|
|
await hass.async_block_till_done()
|
|
with pytest.raises(HomeAssistantError):
|
|
await mqtt.async_publish(
|
|
hass, "some-topic", b"test-payload", qos=0, retain=False, encoding=None
|
|
)
|
|
assert "Failed to connect to MQTT server: Out of memory." in caplog.text
|
|
|
|
|
|
async def test_subscribe_error(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""Test publish error."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0)
|
|
await hass.async_block_till_done()
|
|
with pytest.raises(HomeAssistantError):
|
|
# simulate client is not connected error before subscribing
|
|
mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None)
|
|
await mqtt.async_subscribe(hass, "some-topic", record_calls)
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
async def test_handle_message_callback(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
) -> None:
|
|
"""Test for handling an incoming message callback."""
|
|
callbacks = []
|
|
|
|
@callback
|
|
def _callback(args) -> None:
|
|
callbacks.append(args)
|
|
|
|
mock_mqtt = await mqtt_mock_entry_no_yaml_config()
|
|
msg = ReceiveMessage("some-topic", b"test-payload", 1, False)
|
|
mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0)
|
|
await mqtt.async_subscribe(hass, "some-topic", _callback)
|
|
mqtt_client_mock.on_message(mock_mqtt, None, msg)
|
|
|
|
await hass.async_block_till_done()
|
|
await hass.async_block_till_done()
|
|
assert len(callbacks) == 1
|
|
assert callbacks[0].topic == "some-topic"
|
|
assert callbacks[0].qos == 1
|
|
assert callbacks[0].payload == "test-payload"
|
|
|
|
|
|
async def test_setup_override_configuration(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path
|
|
) -> None:
|
|
"""Test override setup from configuration entry."""
|
|
calls_username_password_set = []
|
|
|
|
def mock_usename_password_set(username: str, password: str) -> None:
|
|
calls_username_password_set.append((username, password))
|
|
|
|
# Mock password setup from config
|
|
config = {
|
|
"username": "someuser",
|
|
"password": "someyamlconfiguredpassword",
|
|
"protocol": "3.1",
|
|
}
|
|
new_yaml_config_file = tmp_path / "configuration.yaml"
|
|
new_yaml_config = yaml.dump({mqtt.DOMAIN: config})
|
|
new_yaml_config_file.write_text(new_yaml_config)
|
|
assert new_yaml_config_file.read_text() == new_yaml_config
|
|
|
|
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
|
|
# Mock config entry
|
|
entry = MockConfigEntry(
|
|
domain=mqtt.DOMAIN,
|
|
data={mqtt.CONF_BROKER: "test-broker", "password": "somepassword"},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
|
|
with patch("paho.mqtt.client.Client") as mock_client:
|
|
mock_client().username_pw_set = mock_usename_password_set
|
|
mock_client.on_connect(return_value=0)
|
|
await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
|
|
await entry.async_setup(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
assert (
|
|
"Deprecated configuration settings found in configuration.yaml. "
|
|
"These settings from your configuration entry will override:"
|
|
in caplog.text
|
|
)
|
|
|
|
# Check if the protocol was set to 3.1 from configuration.yaml
|
|
assert mock_client.call_args[1]["protocol"] == 3
|
|
|
|
# Check if the password override worked
|
|
assert calls_username_password_set[0][0] == "someuser"
|
|
assert calls_username_password_set[0][1] == "somepassword"
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [])
|
|
async def test_setup_manual_mqtt_with_platform_key(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test set up a manual MQTT item with a platform key."""
|
|
config = {
|
|
mqtt.DOMAIN: {
|
|
"light": {
|
|
"platform": "mqtt",
|
|
"name": "test",
|
|
"command_topic": "test-topic",
|
|
}
|
|
}
|
|
}
|
|
with pytest.raises(AssertionError):
|
|
await help_test_setup_manual_entity_from_yaml(hass, 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: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test set up a manual MQTT item with an invalid config."""
|
|
config = {mqtt.DOMAIN: {"light": {"name": "test"}}}
|
|
with pytest.raises(AssertionError):
|
|
await help_test_setup_manual_entity_from_yaml(hass, 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: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test set up a manual MQTT platform without items."""
|
|
config: ConfigType = {mqtt.DOMAIN: {"light": []}}
|
|
await help_test_setup_manual_entity_from_yaml(hass, config)
|
|
assert "voluptuous.error.MultipleInvalid" not in caplog.text
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [])
|
|
async def test_setup_mqtt_client_protocol(
|
|
hass: HomeAssistant, mqtt_mock_entry_with_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test MQTT client protocol setup."""
|
|
with patch("paho.mqtt.client.Client") as mock_client:
|
|
assert await async_setup_component(
|
|
hass,
|
|
mqtt.DOMAIN,
|
|
{
|
|
mqtt.DOMAIN: {
|
|
mqtt.config_integration.CONF_PROTOCOL: "3.1",
|
|
}
|
|
},
|
|
)
|
|
mock_client.on_connect(return_value=0)
|
|
await hass.async_block_till_done()
|
|
await mqtt_mock_entry_with_yaml_config()
|
|
|
|
# check if protocol setup was correctly
|
|
assert mock_client.call_args[1]["protocol"] == 3
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2)
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [])
|
|
async def test_handle_mqtt_timeout_on_callback(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test publish without receiving an ACK callback."""
|
|
mid = 0
|
|
|
|
class FakeInfo:
|
|
"""Returns a simulated client publish response."""
|
|
|
|
mid = 100
|
|
rc = 0
|
|
|
|
with patch("paho.mqtt.client.Client") as mock_client:
|
|
|
|
def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]:
|
|
# Handle ACK for subscribe normally
|
|
nonlocal mid
|
|
mid += 1
|
|
mock_client.on_subscribe(0, 0, mid)
|
|
return (0, mid)
|
|
|
|
# We want to simulate the publish behaviour MQTT client
|
|
mock_client = mock_client.return_value
|
|
mock_client.publish.return_value = FakeInfo()
|
|
mock_client.subscribe.side_effect = _mock_ack
|
|
mock_client.connect.return_value = 0
|
|
|
|
entry = MockConfigEntry(
|
|
domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}
|
|
)
|
|
entry.add_to_hass(hass)
|
|
|
|
# Make sure we are connected correctly
|
|
mock_client.on_connect(mock_client, None, None, 0)
|
|
# Set up the integration
|
|
assert await mqtt.async_setup_entry(hass, entry)
|
|
await hass.async_block_till_done()
|
|
|
|
# Now call we publish without simulating and ACK callback
|
|
await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload")
|
|
await hass.async_block_till_done()
|
|
# There is no ACK so we should see a timeout in the log after publishing
|
|
assert len(mock_client.publish.mock_calls) == 1
|
|
assert "No ACK from MQTT server" in caplog.text
|
|
|
|
|
|
async def test_setup_raises_config_entry_not_ready_if_no_connect_broker(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test for setup failure if connection to broker is missing."""
|
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
|
|
entry.add_to_hass(hass)
|
|
|
|
with patch("paho.mqtt.client.Client") as mock_client:
|
|
mock_client().connect = MagicMock(side_effect=OSError("Connection error"))
|
|
assert await mqtt.async_setup_entry(hass, entry)
|
|
await hass.async_block_till_done()
|
|
assert "Failed to connect to MQTT server due to exception:" in caplog.text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"config, insecure_param",
|
|
[
|
|
({"certificate": "auto"}, "not set"),
|
|
({"certificate": "auto", "tls_insecure": False}, False),
|
|
({"certificate": "auto", "tls_insecure": True}, True),
|
|
],
|
|
)
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [])
|
|
async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_with_yaml_config: MqttMockHAClientGenerator,
|
|
config: ConfigType,
|
|
insecure_param: bool | str,
|
|
) -> None:
|
|
"""Test setup uses bundled certs when certificate is set to auto and insecure."""
|
|
calls = []
|
|
insecure_check = {"insecure": "not set"}
|
|
|
|
def mock_tls_set(
|
|
certificate, certfile=None, keyfile=None, tls_version=None
|
|
) -> None:
|
|
calls.append((certificate, certfile, keyfile, tls_version))
|
|
|
|
def mock_tls_insecure_set(insecure_param) -> None:
|
|
insecure_check["insecure"] = insecure_param
|
|
|
|
with patch("paho.mqtt.client.Client") as mock_client:
|
|
mock_client().tls_set = mock_tls_set
|
|
mock_client().tls_insecure_set = mock_tls_insecure_set
|
|
assert await async_setup_component(
|
|
hass,
|
|
mqtt.DOMAIN,
|
|
{mqtt.DOMAIN: config},
|
|
)
|
|
await hass.async_block_till_done()
|
|
await mqtt_mock_entry_with_yaml_config()
|
|
|
|
assert calls
|
|
|
|
import certifi
|
|
|
|
expected_certificate = certifi.where()
|
|
assert calls[0][0] == expected_certificate
|
|
|
|
# test if insecure is set
|
|
assert insecure_check["insecure"] == insecure_param
|
|
|
|
|
|
async def test_tls_version(
|
|
hass: HomeAssistant, mqtt_mock_entry_with_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test setup defaults for tls."""
|
|
calls = []
|
|
|
|
def mock_tls_set(
|
|
certificate, certfile=None, keyfile=None, tls_version=None
|
|
) -> None:
|
|
calls.append((certificate, certfile, keyfile, tls_version))
|
|
|
|
with patch("paho.mqtt.client.Client") as mock_client:
|
|
mock_client().tls_set = mock_tls_set
|
|
assert await async_setup_component(
|
|
hass,
|
|
mqtt.DOMAIN,
|
|
{mqtt.DOMAIN: {"certificate": "auto"}},
|
|
)
|
|
await hass.async_block_till_done()
|
|
await mqtt_mock_entry_with_yaml_config()
|
|
|
|
assert calls
|
|
assert calls[0][3] == ssl.PROTOCOL_TLS
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mqtt_config_entry_data",
|
|
[
|
|
{
|
|
mqtt.CONF_BROKER: "mock-broker",
|
|
mqtt.CONF_BIRTH_MESSAGE: {
|
|
mqtt.ATTR_TOPIC: "birth",
|
|
mqtt.ATTR_PAYLOAD: "birth",
|
|
mqtt.ATTR_QOS: 0,
|
|
mqtt.ATTR_RETAIN: False,
|
|
},
|
|
}
|
|
],
|
|
)
|
|
async def test_custom_birth_message(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""Test sending birth message."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
birth = asyncio.Event()
|
|
|
|
async def wait_birth(topic, payload, qos) -> None:
|
|
"""Handle birth message."""
|
|
birth.set()
|
|
|
|
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()
|
|
await birth.wait()
|
|
mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mqtt_config_entry_data",
|
|
[
|
|
{
|
|
mqtt.CONF_BROKER: "mock-broker",
|
|
mqtt.CONF_BIRTH_MESSAGE: {
|
|
mqtt.ATTR_TOPIC: "homeassistant/status",
|
|
mqtt.ATTR_PAYLOAD: "online",
|
|
mqtt.ATTR_QOS: 0,
|
|
mqtt.ATTR_RETAIN: False,
|
|
},
|
|
}
|
|
],
|
|
)
|
|
async def test_default_birth_message(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""Test sending birth message."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
birth = asyncio.Event()
|
|
|
|
async def wait_birth(topic, payload, qos) -> None:
|
|
"""Handle birth message."""
|
|
birth.set()
|
|
|
|
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()
|
|
await birth.wait()
|
|
mqtt_client_mock.publish.assert_called_with(
|
|
"homeassistant/status", "online", 0, False
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mqtt_config_entry_data",
|
|
[{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}],
|
|
)
|
|
async def test_no_birth_message(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""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()
|
|
await asyncio.sleep(0.2)
|
|
mqtt_client_mock.publish.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mqtt_config_entry_data",
|
|
[
|
|
{
|
|
mqtt.CONF_BROKER: "mock-broker",
|
|
mqtt.CONF_BIRTH_MESSAGE: {
|
|
mqtt.ATTR_TOPIC: "homeassistant/status",
|
|
mqtt.ATTR_PAYLOAD: "online",
|
|
mqtt.ATTR_QOS: 0,
|
|
mqtt.ATTR_RETAIN: False,
|
|
},
|
|
}
|
|
],
|
|
)
|
|
async def test_delayed_birth_message(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_config_entry_data,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""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()
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data)
|
|
entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
mqtt_component_mock = MagicMock(
|
|
return_value=hass.data["mqtt"].client,
|
|
spec_set=hass.data["mqtt"].client,
|
|
wraps=hass.data["mqtt"].client,
|
|
)
|
|
mqtt_component_mock._mqttc = mqtt_client_mock
|
|
|
|
hass.data["mqtt"].client = mqtt_component_mock
|
|
mqtt_mock = hass.data["mqtt"].client
|
|
mqtt_mock.reset_mock()
|
|
|
|
async def wait_birth(topic, payload, qos) -> None:
|
|
"""Handle birth message."""
|
|
birth.set()
|
|
|
|
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()
|
|
with pytest.raises(asyncio.TimeoutError):
|
|
await asyncio.wait_for(birth.wait(), 0.2)
|
|
assert not mqtt_client_mock.publish.called
|
|
assert not birth.is_set()
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
await birth.wait()
|
|
mqtt_client_mock.publish.assert_called_with(
|
|
"homeassistant/status", "online", 0, False
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mqtt_config_entry_data",
|
|
[
|
|
{
|
|
mqtt.CONF_BROKER: "mock-broker",
|
|
mqtt.CONF_WILL_MESSAGE: {
|
|
mqtt.ATTR_TOPIC: "death",
|
|
mqtt.ATTR_PAYLOAD: "death",
|
|
mqtt.ATTR_QOS: 0,
|
|
mqtt.ATTR_RETAIN: False,
|
|
},
|
|
}
|
|
],
|
|
)
|
|
async def test_custom_will_message(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""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: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""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
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mqtt_config_entry_data",
|
|
[{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}],
|
|
)
|
|
async def test_no_will_message(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""Test will message."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
|
|
mqtt_client_mock.will_set.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mqtt_config_entry_data",
|
|
[
|
|
{
|
|
mqtt.CONF_BROKER: "mock-broker",
|
|
mqtt.CONF_BIRTH_MESSAGE: {},
|
|
mqtt.CONF_DISCOVERY: False,
|
|
}
|
|
],
|
|
)
|
|
async def test_mqtt_subscribes_topics_on_connect(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
record_calls: MessageCallbackType,
|
|
) -> None:
|
|
"""Test subscription to topic on connect."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
|
|
await mqtt.async_subscribe(hass, "topic/test", record_calls)
|
|
await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2)
|
|
await mqtt.async_subscribe(hass, "still/pending", record_calls)
|
|
await mqtt.async_subscribe(hass, "still/pending", record_calls, 1)
|
|
|
|
with patch.object(hass, "add_job") as hass_jobs:
|
|
mqtt_client_mock.on_connect(None, None, 0, 0)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
assert mqtt_client_mock.disconnect.call_count == 0
|
|
|
|
assert len(hass_jobs.mock_calls) == 1
|
|
assert set(hass_jobs.mock_calls[0][1][1]) == {
|
|
("home/sensor", 2),
|
|
("still/pending", 1),
|
|
("topic/test", 0),
|
|
}
|
|
|
|
|
|
async def test_setup_entry_with_config_override(
|
|
hass: HomeAssistant,
|
|
device_registry: dr.DeviceRegistry,
|
|
mqtt_mock_entry_with_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""Test if the MQTT component loads with no config and config entry can be setup."""
|
|
data = (
|
|
'{ "device":{"identifiers":["0AFFD2"]},'
|
|
' "state_topic": "foobar/sensor",'
|
|
' "unique_id": "unique" }'
|
|
)
|
|
|
|
# mqtt present in yaml config
|
|
assert await async_setup_component(hass, mqtt.DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
# User sets up a config entry
|
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
|
|
entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
# Discover a device to verify the entry was setup correctly
|
|
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")})
|
|
assert device_entry is not None
|
|
|
|
|
|
async def test_update_incomplete_entry(
|
|
hass: HomeAssistant,
|
|
device_registry: dr.DeviceRegistry,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test if the MQTT component loads when config entry data is incomplete."""
|
|
data = (
|
|
'{ "device":{"identifiers":["0AFFD2"]},'
|
|
' "state_topic": "foobar/sensor",'
|
|
' "unique_id": "unique" }'
|
|
)
|
|
|
|
# Config entry data is incomplete
|
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={"port": 1234})
|
|
entry.add_to_hass(hass)
|
|
# Mqtt present in yaml config
|
|
config = {"broker": "yaml_broker"}
|
|
await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
|
|
await hass.async_block_till_done()
|
|
|
|
# Config entry data should now be updated
|
|
assert dict(entry.data) == {
|
|
"port": 1234,
|
|
"discovery_prefix": "homeassistant",
|
|
"broker": "yaml_broker",
|
|
}
|
|
# Warnings about broker deprecated, but not about other keys with default values
|
|
assert (
|
|
"The 'broker' option is deprecated, please remove it from your configuration"
|
|
in caplog.text
|
|
)
|
|
|
|
# Discover a device to verify the entry was setup correctly
|
|
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")})
|
|
assert device_entry is not None
|
|
|
|
|
|
async def test_fail_no_broker(
|
|
hass: HomeAssistant,
|
|
device_registry: dr.DeviceRegistry,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test if the MQTT component loads when broker configuration is missing."""
|
|
# Config entry data is incomplete
|
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={})
|
|
entry.add_to_hass(hass)
|
|
assert not await hass.config_entries.async_setup(entry.entry_id)
|
|
assert "MQTT broker is not configured, please configure it" in caplog.text
|
|
|
|
|
|
@pytest.mark.no_fail_on_log_exception
|
|
async def test_message_callback_exception_gets_logged(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""Test exception raised by message handler."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
|
|
@callback
|
|
def bad_handler(*args) -> None:
|
|
"""Record calls."""
|
|
raise ValueError("This is a bad message callback")
|
|
|
|
await mqtt.async_subscribe(hass, "test-topic", bad_handler)
|
|
async_fire_mqtt_message(hass, "test-topic", "test")
|
|
await hass.async_block_till_done()
|
|
|
|
assert (
|
|
"Exception in bad_handler when handling msg on 'test-topic':"
|
|
" 'test'" in caplog.text
|
|
)
|
|
|
|
|
|
async def test_mqtt_ws_subscription(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""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()
|
|
assert response["success"]
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test1")
|
|
async_fire_mqtt_message(hass, "test-topic", "test2")
|
|
async_fire_mqtt_message(hass, "test-topic", b"\xDE\xAD\xBE\xEF")
|
|
|
|
response = await client.receive_json()
|
|
assert response["event"]["topic"] == "test-topic"
|
|
assert response["event"]["payload"] == "test1"
|
|
|
|
response = await client.receive_json()
|
|
assert response["event"]["topic"] == "test-topic"
|
|
assert response["event"]["payload"] == "test2"
|
|
|
|
response = await client.receive_json()
|
|
assert response["event"]["topic"] == "test-topic"
|
|
assert response["event"]["payload"] == "b'\\xde\\xad\\xbe\\xef'"
|
|
|
|
# Unsubscribe
|
|
await client.send_json({"id": 8, "type": "unsubscribe_events", "subscription": 5})
|
|
response = await client.receive_json()
|
|
assert response["success"]
|
|
|
|
# Subscribe with QoS 2
|
|
await client.send_json(
|
|
{"id": 9, "type": "mqtt/subscribe", "topic": "test-topic", "qos": 2}
|
|
)
|
|
response = await client.receive_json()
|
|
assert response["success"]
|
|
|
|
async_fire_mqtt_message(hass, "test-topic", "test1", 2)
|
|
async_fire_mqtt_message(hass, "test-topic", "test2", 2)
|
|
async_fire_mqtt_message(hass, "test-topic", b"\xDE\xAD\xBE\xEF", 2)
|
|
|
|
response = await client.receive_json()
|
|
assert response["event"]["topic"] == "test-topic"
|
|
assert response["event"]["payload"] == "test1"
|
|
assert response["event"]["qos"] == 2
|
|
|
|
response = await client.receive_json()
|
|
assert response["event"]["topic"] == "test-topic"
|
|
assert response["event"]["payload"] == "test2"
|
|
assert response["event"]["qos"] == 2
|
|
|
|
response = await client.receive_json()
|
|
assert response["event"]["topic"] == "test-topic"
|
|
assert response["event"]["payload"] == "b'\\xde\\xad\\xbe\\xef'"
|
|
assert response["event"]["qos"] == 2
|
|
|
|
# Unsubscribe
|
|
await client.send_json({"id": 15, "type": "unsubscribe_events", "subscription": 9})
|
|
response = await client.receive_json()
|
|
assert response["success"]
|
|
|
|
|
|
async def test_mqtt_ws_subscription_not_admin(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
hass_read_only_access_token: str,
|
|
) -> None:
|
|
"""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()
|
|
assert response["success"] is False
|
|
assert response["error"]["code"] == "unauthorized"
|
|
assert response["error"]["message"] == "Unauthorized"
|
|
|
|
|
|
async def test_dump_service(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test that we can dump a topic."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
mopen = mock_open()
|
|
|
|
await hass.services.async_call(
|
|
"mqtt", "dump", {"topic": "bla/#", "duration": 3}, blocking=True
|
|
)
|
|
async_fire_mqtt_message(hass, "bla/1", "test1")
|
|
async_fire_mqtt_message(hass, "bla/2", "test2")
|
|
|
|
with patch("homeassistant.components.mqtt.open", mopen):
|
|
async_fire_time_changed(hass, utcnow() + timedelta(seconds=3))
|
|
await hass.async_block_till_done()
|
|
|
|
writes = mopen.return_value.write.mock_calls
|
|
assert len(writes) == 2
|
|
assert writes[0][1][0] == "bla/1,test1\n"
|
|
assert writes[1][1][0] == "bla/2,test2\n"
|
|
|
|
|
|
async def test_mqtt_ws_remove_discovered_device(
|
|
hass: HomeAssistant,
|
|
device_registry: dr.DeviceRegistry,
|
|
hass_ws_client: WebSocketGenerator,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""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"]},'
|
|
' "state_topic": "foobar/sensor",'
|
|
' "unique_id": "unique" }'
|
|
)
|
|
|
|
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
# Verify device entry is created
|
|
device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")})
|
|
assert device_entry is not None
|
|
|
|
client = await hass_ws_client(hass)
|
|
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
|
await client.send_json(
|
|
{
|
|
"id": 5,
|
|
"type": "config/device_registry/remove_config_entry",
|
|
"config_entry_id": mqtt_config_entry.entry_id,
|
|
"device_id": device_entry.id,
|
|
}
|
|
)
|
|
response = await client.receive_json()
|
|
assert response["success"]
|
|
|
|
# Verify device entry is cleared
|
|
device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")})
|
|
assert device_entry is None
|
|
|
|
|
|
async def test_mqtt_ws_get_device_debug_info(
|
|
hass: HomeAssistant,
|
|
device_registry: dr.DeviceRegistry,
|
|
hass_ws_client: WebSocketGenerator,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""Test MQTT websocket device debug info."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
config_sensor = {
|
|
"device": {"identifiers": ["0AFFD2"]},
|
|
"state_topic": "foobar/sensor",
|
|
"unique_id": "unique",
|
|
}
|
|
config_trigger = {
|
|
"automation_type": "trigger",
|
|
"device": {"identifiers": ["0AFFD2"]},
|
|
"topic": "test-topic1",
|
|
"type": "foo",
|
|
"subtype": "bar",
|
|
}
|
|
data_sensor = json.dumps(config_sensor)
|
|
data_trigger = json.dumps(config_trigger)
|
|
config_sensor["platform"] = config_trigger["platform"] = mqtt.DOMAIN
|
|
|
|
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data_sensor)
|
|
async_fire_mqtt_message(
|
|
hass, "homeassistant/device_automation/bla/config", data_trigger
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
# Verify device entry is created
|
|
device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")})
|
|
assert device_entry is not None
|
|
|
|
client = await hass_ws_client(hass)
|
|
await client.send_json(
|
|
{"id": 5, "type": "mqtt/device/debug_info", "device_id": device_entry.id}
|
|
)
|
|
response = await client.receive_json()
|
|
assert response["success"]
|
|
expected_result = {
|
|
"entities": [
|
|
{
|
|
"entity_id": "sensor.mqtt_sensor",
|
|
"subscriptions": [{"topic": "foobar/sensor", "messages": []}],
|
|
"discovery_data": {
|
|
"payload": config_sensor,
|
|
"topic": "homeassistant/sensor/bla/config",
|
|
},
|
|
"transmitted": [],
|
|
}
|
|
],
|
|
"triggers": [
|
|
{
|
|
"discovery_data": {
|
|
"payload": config_trigger,
|
|
"topic": "homeassistant/device_automation/bla/config",
|
|
},
|
|
"trigger_key": ["device_automation", "bla"],
|
|
}
|
|
],
|
|
}
|
|
assert response["result"] == expected_result
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.CAMERA])
|
|
async def test_mqtt_ws_get_device_debug_info_binary(
|
|
hass: HomeAssistant,
|
|
device_registry: dr.DeviceRegistry,
|
|
hass_ws_client: WebSocketGenerator,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""Test MQTT websocket device debug info."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
config = {
|
|
"device": {"identifiers": ["0AFFD2"]},
|
|
"topic": "foobar/image",
|
|
"unique_id": "unique",
|
|
}
|
|
data = json.dumps(config)
|
|
config["platform"] = mqtt.DOMAIN
|
|
|
|
async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
# Verify device entry is created
|
|
device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")})
|
|
assert device_entry is not None
|
|
|
|
small_png = (
|
|
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x04\x00\x00\x00\x04\x08\x06"
|
|
b"\x00\x00\x00\xa9\xf1\x9e~\x00\x00\x00\x13IDATx\xdac\xfc\xcf\xc0P\xcf\x80\x04"
|
|
b"\x18I\x17\x00\x00\xf2\xae\x05\xfdR\x01\xc2\xde\x00\x00\x00\x00IEND\xaeB`\x82"
|
|
)
|
|
async_fire_mqtt_message(hass, "foobar/image", small_png)
|
|
await hass.async_block_till_done()
|
|
|
|
client = await hass_ws_client(hass)
|
|
await client.send_json(
|
|
{"id": 5, "type": "mqtt/device/debug_info", "device_id": device_entry.id}
|
|
)
|
|
response = await client.receive_json()
|
|
assert response["success"]
|
|
expected_result = {
|
|
"entities": [
|
|
{
|
|
"entity_id": "camera.mqtt_camera",
|
|
"subscriptions": [
|
|
{
|
|
"topic": "foobar/image",
|
|
"messages": [
|
|
{
|
|
"payload": str(small_png),
|
|
"qos": 0,
|
|
"retain": False,
|
|
"time": ANY,
|
|
"topic": "foobar/image",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
"discovery_data": {
|
|
"payload": config,
|
|
"topic": "homeassistant/camera/bla/config",
|
|
},
|
|
"transmitted": [],
|
|
}
|
|
],
|
|
"triggers": [],
|
|
}
|
|
assert response["result"] == expected_result
|
|
|
|
|
|
async def test_debug_info_multiple_devices(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test we get correct debug_info when multiple devices are present."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
devices: list[_DebugInfo] = [
|
|
{
|
|
"domain": "sensor",
|
|
"config": {
|
|
"device": {"identifiers": ["0AFFD0"]},
|
|
"platform": "mqtt",
|
|
"state_topic": "test-topic-sensor",
|
|
"unique_id": "unique",
|
|
},
|
|
},
|
|
{
|
|
"domain": "binary_sensor",
|
|
"config": {
|
|
"device": {"identifiers": ["0AFFD1"]},
|
|
"platform": "mqtt",
|
|
"state_topic": "test-topic-binary-sensor",
|
|
"unique_id": "unique",
|
|
},
|
|
},
|
|
{
|
|
"domain": "device_automation",
|
|
"config": {
|
|
"automation_type": "trigger",
|
|
"device": {"identifiers": ["0AFFD2"]},
|
|
"platform": "mqtt",
|
|
"topic": "test-topic1",
|
|
"type": "foo",
|
|
"subtype": "bar",
|
|
},
|
|
},
|
|
{
|
|
"domain": "device_automation",
|
|
"config": {
|
|
"automation_type": "trigger",
|
|
"device": {"identifiers": ["0AFFD3"]},
|
|
"platform": "mqtt",
|
|
"topic": "test-topic2",
|
|
"type": "ikk",
|
|
"subtype": "baz",
|
|
},
|
|
},
|
|
]
|
|
|
|
registry = dr.async_get(hass)
|
|
|
|
for dev in devices:
|
|
data = json.dumps(dev["config"])
|
|
domain = dev["domain"]
|
|
id = dev["config"]["device"]["identifiers"][0]
|
|
async_fire_mqtt_message(hass, f"homeassistant/{domain}/{id}/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
for dev in devices:
|
|
domain = dev["domain"]
|
|
id = dev["config"]["device"]["identifiers"][0]
|
|
device = registry.async_get_device({("mqtt", id)})
|
|
assert device is not None
|
|
|
|
debug_info_data = debug_info.info_for_device(hass, device.id)
|
|
if dev["domain"] != "device_automation":
|
|
assert len(debug_info_data["entities"]) == 1
|
|
assert len(debug_info_data["triggers"]) == 0
|
|
discovery_data = debug_info_data["entities"][0]["discovery_data"]
|
|
assert len(debug_info_data["entities"][0]["subscriptions"]) == 1
|
|
topic = dev["config"]["state_topic"]
|
|
assert {"topic": topic, "messages": []} in debug_info_data["entities"][0][
|
|
"subscriptions"
|
|
]
|
|
else:
|
|
assert len(debug_info_data["entities"]) == 0
|
|
assert len(debug_info_data["triggers"]) == 1
|
|
discovery_data = debug_info_data["triggers"][0]["discovery_data"]
|
|
|
|
assert discovery_data["topic"] == f"homeassistant/{domain}/{id}/config"
|
|
assert discovery_data["payload"] == dev["config"]
|
|
|
|
|
|
async def test_debug_info_multiple_entities_triggers(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test we get correct debug_info for a device with multiple entities and triggers."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
config: list[_DebugInfo] = [
|
|
{
|
|
"domain": "sensor",
|
|
"config": {
|
|
"device": {"identifiers": ["0AFFD0"]},
|
|
"platform": "mqtt",
|
|
"state_topic": "test-topic-sensor",
|
|
"unique_id": "unique",
|
|
},
|
|
},
|
|
{
|
|
"domain": "binary_sensor",
|
|
"config": {
|
|
"device": {"identifiers": ["0AFFD0"]},
|
|
"platform": "mqtt",
|
|
"state_topic": "test-topic-binary-sensor",
|
|
"unique_id": "unique",
|
|
},
|
|
},
|
|
{
|
|
"domain": "device_automation",
|
|
"config": {
|
|
"automation_type": "trigger",
|
|
"device": {"identifiers": ["0AFFD0"]},
|
|
"platform": "mqtt",
|
|
"topic": "test-topic1",
|
|
"type": "foo",
|
|
"subtype": "bar",
|
|
},
|
|
},
|
|
{
|
|
"domain": "device_automation",
|
|
"config": {
|
|
"automation_type": "trigger",
|
|
"device": {"identifiers": ["0AFFD0"]},
|
|
"platform": "mqtt",
|
|
"topic": "test-topic2",
|
|
"type": "ikk",
|
|
"subtype": "baz",
|
|
},
|
|
},
|
|
]
|
|
|
|
registry = dr.async_get(hass)
|
|
|
|
for c in config:
|
|
data = json.dumps(c["config"])
|
|
domain = c["domain"]
|
|
# Use topic as discovery_id
|
|
id = c["config"].get("topic", c["config"].get("state_topic"))
|
|
async_fire_mqtt_message(hass, f"homeassistant/{domain}/{id}/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
device_id = config[0]["config"]["device"]["identifiers"][0]
|
|
device = registry.async_get_device({("mqtt", device_id)})
|
|
assert device is not None
|
|
debug_info_data = debug_info.info_for_device(hass, device.id)
|
|
assert len(debug_info_data["entities"]) == 2
|
|
assert len(debug_info_data["triggers"]) == 2
|
|
|
|
for c in config:
|
|
# Test we get debug info for each entity and trigger
|
|
domain = c["domain"]
|
|
# Use topic as discovery_id
|
|
id = c["config"].get("topic", c["config"].get("state_topic"))
|
|
|
|
if c["domain"] != "device_automation":
|
|
discovery_data = [e["discovery_data"] for e in debug_info_data["entities"]]
|
|
topic = c["config"]["state_topic"]
|
|
assert {"topic": topic, "messages": []} in [
|
|
t for e in debug_info_data["entities"] for t in e["subscriptions"]
|
|
]
|
|
else:
|
|
discovery_data = [e["discovery_data"] for e in debug_info_data["triggers"]]
|
|
|
|
assert {
|
|
"topic": f"homeassistant/{domain}/{id}/config",
|
|
"payload": c["config"],
|
|
} in discovery_data
|
|
|
|
|
|
async def test_debug_info_non_mqtt(
|
|
hass: HomeAssistant,
|
|
device_registry: dr.DeviceRegistry,
|
|
entity_registry: er.EntityRegistry,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
) -> None:
|
|
"""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()
|
|
|
|
config_entry = MockConfigEntry(domain="test", data={})
|
|
config_entry.add_to_hass(hass)
|
|
device_entry = device_registry.async_get_or_create(
|
|
config_entry_id=config_entry.entry_id,
|
|
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
|
)
|
|
for device_class in DEVICE_CLASSES:
|
|
entity_registry.async_get_or_create(
|
|
domain,
|
|
"test",
|
|
platform.ENTITIES[device_class].unique_id,
|
|
device_id=device_entry.id,
|
|
)
|
|
|
|
assert await async_setup_component(
|
|
hass, mqtt.DOMAIN, {mqtt.DOMAIN: {domain: {"platform": "test"}}}
|
|
)
|
|
|
|
debug_info_data = debug_info.info_for_device(hass, device_entry.id)
|
|
assert len(debug_info_data["entities"]) == 0
|
|
assert len(debug_info_data["triggers"]) == 0
|
|
|
|
|
|
async def test_debug_info_wildcard(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test debug info."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
config = {
|
|
"device": {"identifiers": ["helloworld"]},
|
|
"name": "test",
|
|
"state_topic": "sensor/#",
|
|
"unique_id": "veryunique",
|
|
}
|
|
|
|
registry = dr.async_get(hass)
|
|
|
|
data = json.dumps(config)
|
|
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
device = registry.async_get_device({("mqtt", "helloworld")})
|
|
assert device is not None
|
|
|
|
debug_info_data = debug_info.info_for_device(hass, device.id)
|
|
assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1
|
|
assert {"topic": "sensor/#", "messages": []} in debug_info_data["entities"][0][
|
|
"subscriptions"
|
|
]
|
|
|
|
start_dt = datetime(2019, 1, 1, 0, 0, 0)
|
|
with patch("homeassistant.util.dt.utcnow") as dt_utcnow:
|
|
dt_utcnow.return_value = start_dt
|
|
async_fire_mqtt_message(hass, "sensor/abc", "123")
|
|
|
|
debug_info_data = debug_info.info_for_device(hass, device.id)
|
|
assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1
|
|
assert {
|
|
"topic": "sensor/#",
|
|
"messages": [
|
|
{
|
|
"payload": "123",
|
|
"qos": 0,
|
|
"retain": False,
|
|
"time": start_dt,
|
|
"topic": "sensor/abc",
|
|
}
|
|
],
|
|
} in debug_info_data["entities"][0]["subscriptions"]
|
|
|
|
|
|
async def test_debug_info_filter_same(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test debug info removes messages with same timestamp."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
config = {
|
|
"device": {"identifiers": ["helloworld"]},
|
|
"name": "test",
|
|
"state_topic": "sensor/#",
|
|
"unique_id": "veryunique",
|
|
}
|
|
|
|
registry = dr.async_get(hass)
|
|
|
|
data = json.dumps(config)
|
|
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
device = registry.async_get_device({("mqtt", "helloworld")})
|
|
assert device is not None
|
|
|
|
debug_info_data = debug_info.info_for_device(hass, device.id)
|
|
assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1
|
|
assert {"topic": "sensor/#", "messages": []} in debug_info_data["entities"][0][
|
|
"subscriptions"
|
|
]
|
|
|
|
dt1 = datetime(2019, 1, 1, 0, 0, 0)
|
|
dt2 = datetime(2019, 1, 1, 0, 0, 1)
|
|
with patch("homeassistant.util.dt.utcnow") as dt_utcnow:
|
|
dt_utcnow.return_value = dt1
|
|
async_fire_mqtt_message(hass, "sensor/abc", "123")
|
|
async_fire_mqtt_message(hass, "sensor/abc", "123")
|
|
dt_utcnow.return_value = dt2
|
|
async_fire_mqtt_message(hass, "sensor/abc", "123")
|
|
|
|
debug_info_data = debug_info.info_for_device(hass, device.id)
|
|
assert len(debug_info_data["entities"][0]["subscriptions"]) == 1
|
|
assert len(debug_info_data["entities"][0]["subscriptions"][0]["messages"]) == 2
|
|
assert {
|
|
"topic": "sensor/#",
|
|
"messages": [
|
|
{
|
|
"payload": "123",
|
|
"qos": 0,
|
|
"retain": False,
|
|
"time": dt1,
|
|
"topic": "sensor/abc",
|
|
},
|
|
{
|
|
"payload": "123",
|
|
"qos": 0,
|
|
"retain": False,
|
|
"time": dt2,
|
|
"topic": "sensor/abc",
|
|
},
|
|
],
|
|
} == debug_info_data["entities"][0]["subscriptions"][0]
|
|
|
|
|
|
async def test_debug_info_same_topic(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test debug info."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
config = {
|
|
"device": {"identifiers": ["helloworld"]},
|
|
"name": "test",
|
|
"state_topic": "sensor/status",
|
|
"availability_topic": "sensor/status",
|
|
"unique_id": "veryunique",
|
|
}
|
|
|
|
registry = dr.async_get(hass)
|
|
|
|
data = json.dumps(config)
|
|
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
device = registry.async_get_device({("mqtt", "helloworld")})
|
|
assert device is not None
|
|
|
|
debug_info_data = debug_info.info_for_device(hass, device.id)
|
|
assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1
|
|
assert {"topic": "sensor/status", "messages": []} in debug_info_data["entities"][0][
|
|
"subscriptions"
|
|
]
|
|
|
|
start_dt = datetime(2019, 1, 1, 0, 0, 0)
|
|
with patch("homeassistant.util.dt.utcnow") as dt_utcnow:
|
|
dt_utcnow.return_value = start_dt
|
|
async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False)
|
|
|
|
debug_info_data = debug_info.info_for_device(hass, device.id)
|
|
assert len(debug_info_data["entities"][0]["subscriptions"]) == 1
|
|
assert {
|
|
"payload": "123",
|
|
"qos": 0,
|
|
"retain": False,
|
|
"time": start_dt,
|
|
"topic": "sensor/status",
|
|
} in debug_info_data["entities"][0]["subscriptions"][0]["messages"]
|
|
|
|
config["availability_topic"] = "sensor/availability"
|
|
data = json.dumps(config)
|
|
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
start_dt = datetime(2019, 1, 1, 0, 0, 0)
|
|
with patch("homeassistant.util.dt.utcnow") as dt_utcnow:
|
|
dt_utcnow.return_value = start_dt
|
|
async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False)
|
|
|
|
|
|
async def test_debug_info_qos_retain(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""Test debug info."""
|
|
await mqtt_mock_entry_no_yaml_config()
|
|
config = {
|
|
"device": {"identifiers": ["helloworld"]},
|
|
"name": "test",
|
|
"state_topic": "sensor/#",
|
|
"unique_id": "veryunique",
|
|
}
|
|
|
|
registry = dr.async_get(hass)
|
|
|
|
data = json.dumps(config)
|
|
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
|
|
await hass.async_block_till_done()
|
|
|
|
device = registry.async_get_device({("mqtt", "helloworld")})
|
|
assert device is not None
|
|
|
|
debug_info_data = debug_info.info_for_device(hass, device.id)
|
|
assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1
|
|
assert {"topic": "sensor/#", "messages": []} in debug_info_data["entities"][0][
|
|
"subscriptions"
|
|
]
|
|
|
|
start_dt = datetime(2019, 1, 1, 0, 0, 0)
|
|
with patch("homeassistant.util.dt.utcnow") as dt_utcnow:
|
|
dt_utcnow.return_value = start_dt
|
|
async_fire_mqtt_message(hass, "sensor/abc", "123", qos=0, retain=False)
|
|
async_fire_mqtt_message(hass, "sensor/abc", "123", qos=1, retain=True)
|
|
async_fire_mqtt_message(hass, "sensor/abc", "123", qos=2, retain=False)
|
|
|
|
debug_info_data = debug_info.info_for_device(hass, device.id)
|
|
assert len(debug_info_data["entities"][0]["subscriptions"]) == 1
|
|
assert {
|
|
"payload": "123",
|
|
"qos": 0,
|
|
"retain": False,
|
|
"time": start_dt,
|
|
"topic": "sensor/abc",
|
|
} in debug_info_data["entities"][0]["subscriptions"][0]["messages"]
|
|
assert {
|
|
"payload": "123",
|
|
"qos": 1,
|
|
"retain": True,
|
|
"time": start_dt,
|
|
"topic": "sensor/abc",
|
|
} in debug_info_data["entities"][0]["subscriptions"][0]["messages"]
|
|
assert {
|
|
"payload": "123",
|
|
"qos": 2,
|
|
"retain": False,
|
|
"time": start_dt,
|
|
"topic": "sensor/abc",
|
|
} in debug_info_data["entities"][0]["subscriptions"][0]["messages"]
|
|
|
|
|
|
async def test_publish_json_from_template(
|
|
hass: HomeAssistant, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator
|
|
) -> None:
|
|
"""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'}"
|
|
|
|
await async_setup_component(
|
|
hass,
|
|
"script",
|
|
{
|
|
"script": {
|
|
"test_script_payload": {
|
|
"sequence": {
|
|
"service": "mqtt.publish",
|
|
"data": {"topic": "test-topic", "payload": test_str_tpl},
|
|
}
|
|
},
|
|
"test_script_payload_template": {
|
|
"sequence": {
|
|
"service": "mqtt.publish",
|
|
"data": {
|
|
"topic": "test-topic",
|
|
"payload_template": test_str_tpl,
|
|
},
|
|
}
|
|
},
|
|
}
|
|
},
|
|
)
|
|
|
|
await hass.services.async_call("script", "test_script_payload", blocking=True)
|
|
await hass.async_block_till_done()
|
|
|
|
assert mqtt_mock.async_publish.called
|
|
assert mqtt_mock.async_publish.call_args[0][1] == test_str
|
|
|
|
mqtt_mock.async_publish.reset_mock()
|
|
assert not mqtt_mock.async_publish.called
|
|
|
|
await hass.services.async_call(
|
|
"script", "test_script_payload_template", blocking=True
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
assert mqtt_mock.async_publish.called
|
|
assert mqtt_mock.async_publish.call_args[0][1] == test_str
|
|
|
|
|
|
async def test_subscribe_connection_status(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
) -> None:
|
|
"""Test connextion status subscription."""
|
|
mqtt_mock = await mqtt_mock_entry_no_yaml_config()
|
|
mqtt_connected_calls = []
|
|
|
|
@callback
|
|
def async_mqtt_connected(status: bool) -> None:
|
|
"""Update state on connection/disconnection to MQTT broker."""
|
|
mqtt_connected_calls.append(status)
|
|
|
|
mqtt_mock.connected = True
|
|
|
|
unsub = mqtt.async_subscribe_connection_status(hass, async_mqtt_connected)
|
|
await hass.async_block_till_done()
|
|
|
|
# Mock connection status
|
|
mqtt_client_mock.on_connect(None, None, 0, 0)
|
|
await hass.async_block_till_done()
|
|
assert mqtt.is_connected(hass) is True
|
|
|
|
# Mock disconnect status
|
|
mqtt_client_mock.on_disconnect(None, None, 0)
|
|
await hass.async_block_till_done()
|
|
|
|
# Unsubscribe
|
|
unsub()
|
|
|
|
mqtt_client_mock.on_connect(None, None, 0, 0)
|
|
await hass.async_block_till_done()
|
|
|
|
# Check calls
|
|
assert len(mqtt_connected_calls) == 2
|
|
assert mqtt_connected_calls[0] is True
|
|
assert mqtt_connected_calls[1] is False
|
|
|
|
|
|
# Test existence of removed YAML configuration under the platform key
|
|
# This warning and test is to be removed from HA core 2023.6
|
|
async def test_one_deprecation_warning_per_platform(
|
|
hass: HomeAssistant,
|
|
mqtt_mock_entry_with_yaml_config: MqttMockHAClientGenerator,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a deprecation warning is is logged once per platform."""
|
|
platform = "light"
|
|
config = {"platform": "mqtt", "command_topic": "test-topic"}
|
|
config1 = copy.deepcopy(config)
|
|
config1["name"] = "test1"
|
|
config2 = copy.deepcopy(config)
|
|
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 == "ERROR" and (
|
|
f"Manually configured MQTT {platform}(s) found under platform key '{platform}'"
|
|
in record.message
|
|
):
|
|
count += 1
|
|
assert count == 1
|
|
|
|
|
|
async def test_config_schema_validation(hass: HomeAssistant) -> None:
|
|
"""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)
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
|
|
async def test_unload_config_entry(
|
|
hass: HomeAssistant,
|
|
mqtt_mock: MqttMockHAClient,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
tmp_path: Path,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test unloading the MQTT entry."""
|
|
assert hass.services.has_service(mqtt.DOMAIN, "dump")
|
|
assert hass.services.has_service(mqtt.DOMAIN, "publish")
|
|
|
|
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
|
assert mqtt_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
# Publish just before unloading to test await cleanup
|
|
mqtt_client_mock.reset_mock()
|
|
mqtt.publish(hass, "just_in_time", "published", qos=0, retain=False)
|
|
|
|
new_yaml_config_file = tmp_path / "configuration.yaml"
|
|
new_yaml_config = yaml.dump({})
|
|
new_yaml_config_file.write_text(new_yaml_config)
|
|
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
|
|
assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id)
|
|
new_mqtt_config_entry = mqtt_config_entry
|
|
mqtt_client_mock.publish.assert_any_call("just_in_time", "published", 0, False)
|
|
assert new_mqtt_config_entry.state is ConfigEntryState.NOT_LOADED
|
|
await hass.async_block_till_done()
|
|
assert not hass.services.has_service(mqtt.DOMAIN, "dump")
|
|
assert not hass.services.has_service(mqtt.DOMAIN, "publish")
|
|
assert "No ACK from MQTT server" not in caplog.text
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [])
|
|
async def test_setup_with_disabled_entry(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test setting up the platform with a disabled config entry."""
|
|
# Try to setup the platform with a disabled config entry
|
|
config_entry = MockConfigEntry(
|
|
domain=mqtt.DOMAIN, data={}, disabled_by=ConfigEntryDisabler.USER
|
|
)
|
|
config_entry.add_to_hass(hass)
|
|
|
|
config: ConfigType = {mqtt.DOMAIN: {}}
|
|
await async_setup_component(hass, mqtt.DOMAIN, config)
|
|
await hass.async_block_till_done()
|
|
|
|
assert "MQTT will be not available until the config entry is enabled" in caplog.text
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [])
|
|
async def test_publish_or_subscribe_without_valid_config_entry(
|
|
hass: HomeAssistant, record_calls: MessageCallbackType
|
|
) -> None:
|
|
"""Test internal publish function with bas use cases."""
|
|
with pytest.raises(HomeAssistantError):
|
|
await mqtt.async_publish(
|
|
hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None
|
|
)
|
|
with pytest.raises(HomeAssistantError):
|
|
await mqtt.async_subscribe(hass, "some-topic", record_calls, qos=0)
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
|
|
async def test_reload_entry_with_new_config(
|
|
hass: HomeAssistant, tmp_path: Path
|
|
) -> None:
|
|
"""Test reloading the config entry with a new yaml config."""
|
|
config_old = {
|
|
"mqtt": {"light": [{"name": "test_old1", "command_topic": "test-topic_old"}]}
|
|
}
|
|
config_yaml_new = {
|
|
"mqtt": {
|
|
"light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}]
|
|
},
|
|
}
|
|
await help_test_setup_manual_entity_from_yaml(hass, config_old)
|
|
assert hass.states.get("light.test_old1") is not None
|
|
|
|
await help_test_entry_reload_with_new_config(hass, tmp_path, config_yaml_new)
|
|
assert hass.states.get("light.test_old1") is None
|
|
assert hass.states.get("light.test_new_modern") is not None
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
|
|
async def test_disabling_and_enabling_entry(
|
|
hass: HomeAssistant, tmp_path: Path
|
|
) -> None:
|
|
"""Test disabling and enabling the config entry."""
|
|
config_old = {
|
|
"mqtt": {"light": [{"name": "test_old1", "command_topic": "test-topic_old"}]}
|
|
}
|
|
config_yaml_new = {
|
|
"mqtt": {
|
|
"light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}]
|
|
},
|
|
}
|
|
await help_test_setup_manual_entity_from_yaml(hass, config_old)
|
|
assert hass.states.get("light.test_old1") is not None
|
|
|
|
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
|
|
|
assert mqtt_config_entry.state is ConfigEntryState.LOADED
|
|
new_yaml_config_file = tmp_path / "configuration.yaml"
|
|
new_yaml_config = yaml.dump(config_yaml_new)
|
|
new_yaml_config_file.write_text(new_yaml_config)
|
|
assert new_yaml_config_file.read_text() == new_yaml_config
|
|
|
|
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file), patch(
|
|
"paho.mqtt.client.Client"
|
|
) as mock_client:
|
|
mock_client().connect = lambda *args: 0
|
|
|
|
# Late discovery of a light
|
|
config = '{"name": "abc", "command_topic": "test-topic"}'
|
|
async_fire_mqtt_message(hass, "homeassistant/light/abc/config", config)
|
|
|
|
# Disable MQTT config entry
|
|
await hass.config_entries.async_set_disabled_by(
|
|
mqtt_config_entry.entry_id, ConfigEntryDisabler.USER
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
await hass.async_block_till_done()
|
|
|
|
new_mqtt_config_entry = mqtt_config_entry
|
|
assert new_mqtt_config_entry.state is ConfigEntryState.NOT_LOADED
|
|
assert hass.states.get("light.test_old1") is None
|
|
|
|
# Enable the entry again
|
|
await hass.config_entries.async_set_disabled_by(
|
|
mqtt_config_entry.entry_id, None
|
|
)
|
|
await hass.async_block_till_done()
|
|
await hass.async_block_till_done()
|
|
new_mqtt_config_entry = mqtt_config_entry
|
|
assert new_mqtt_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
assert hass.states.get("light.test_old1") is None
|
|
assert hass.states.get("light.test_new_modern") is not None
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
|
|
@pytest.mark.parametrize(
|
|
"config, unique",
|
|
[
|
|
(
|
|
[
|
|
{
|
|
"name": "test1",
|
|
"unique_id": "very_not_unique_deadbeef",
|
|
"command_topic": "test-topic_unique",
|
|
},
|
|
{
|
|
"name": "test2",
|
|
"unique_id": "very_not_unique_deadbeef",
|
|
"command_topic": "test-topic_unique",
|
|
},
|
|
],
|
|
False,
|
|
),
|
|
(
|
|
[
|
|
{
|
|
"name": "test1",
|
|
"unique_id": "very_unique_deadbeef1",
|
|
"command_topic": "test-topic_unique",
|
|
},
|
|
{
|
|
"name": "test2",
|
|
"unique_id": "very_unique_deadbeef2",
|
|
"command_topic": "test-topic_unique",
|
|
},
|
|
],
|
|
True,
|
|
),
|
|
],
|
|
)
|
|
async def test_setup_manual_items_with_unique_ids(
|
|
hass: HomeAssistant,
|
|
tmp_path: Path,
|
|
caplog: pytest.LogCaptureFixture,
|
|
config: ConfigType,
|
|
unique: bool,
|
|
) -> None:
|
|
"""Test setup manual items is generating unique id's."""
|
|
await help_test_setup_manual_entity_from_yaml(
|
|
hass, {mqtt.DOMAIN: {"light": config}}
|
|
)
|
|
|
|
assert hass.states.get("light.test1") is not None
|
|
assert (hass.states.get("light.test2") is not None) == unique
|
|
assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique
|
|
|
|
# reload and assert again
|
|
caplog.clear()
|
|
await help_test_entry_reload_with_new_config(
|
|
hass, tmp_path, {"mqtt": {"light": config}}
|
|
)
|
|
|
|
assert hass.states.get("light.test1") is not None
|
|
assert (hass.states.get("light.test2") is not None) == unique
|
|
assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique
|
|
|
|
|
|
async def test_remove_unknown_conf_entry_options(
|
|
hass: HomeAssistant,
|
|
mqtt_client_mock: MqttMockPahoClient,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test unknown keys in config entry data is removed."""
|
|
mqtt_config_entry_data = {
|
|
mqtt.CONF_BROKER: "mock-broker",
|
|
mqtt.CONF_BIRTH_MESSAGE: {},
|
|
"old_option": "old_value",
|
|
}
|
|
|
|
entry = MockConfigEntry(
|
|
data=mqtt_config_entry_data,
|
|
domain=mqtt.DOMAIN,
|
|
title="MQTT",
|
|
)
|
|
|
|
entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert mqtt.client.CONF_PROTOCOL not in entry.data
|
|
assert (
|
|
"The following unsupported configuration options were removed from the "
|
|
"MQTT config entry: {'old_option'}"
|
|
) in caplog.text
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
|
|
async def test_link_config_entry(
|
|
hass: HomeAssistant, tmp_path: Path, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test manual and dynamically setup entities are linked to the config entry."""
|
|
config_manual = {
|
|
"mqtt": {
|
|
"light": [
|
|
{
|
|
"name": "test_manual",
|
|
"unique_id": "test_manual_unique_id123",
|
|
"command_topic": "test-topic_manual",
|
|
}
|
|
]
|
|
}
|
|
}
|
|
config_discovery = {
|
|
"name": "test_discovery",
|
|
"unique_id": "test_discovery_unique456",
|
|
"command_topic": "test-topic_discovery",
|
|
}
|
|
|
|
# set up manual item
|
|
await help_test_setup_manual_entity_from_yaml(hass, config_manual)
|
|
|
|
# set up item through discovery
|
|
async_fire_mqtt_message(
|
|
hass, "homeassistant/light/bla/config", json.dumps(config_discovery)
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
assert hass.states.get("light.test_manual") is not None
|
|
assert hass.states.get("light.test_discovery") is not None
|
|
entity_names = ["test_manual", "test_discovery"]
|
|
|
|
# Check if both entities were linked to the MQTT config entry
|
|
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
|
mqtt_platforms = async_get_platforms(hass, mqtt.DOMAIN)
|
|
|
|
def _check_entities() -> int:
|
|
entities: list[Entity] = []
|
|
for mqtt_platform in mqtt_platforms:
|
|
assert mqtt_platform.config_entry is mqtt_config_entry
|
|
entities += (entity for entity in mqtt_platform.entities.values())
|
|
|
|
for entity in entities:
|
|
assert entity.name in entity_names
|
|
return len(entities)
|
|
|
|
assert _check_entities() == 2
|
|
|
|
# reload entry and assert again
|
|
await help_test_entry_reload_with_new_config(hass, tmp_path, config_manual)
|
|
# manual set up item should remain
|
|
assert _check_entities() == 1
|
|
# set up item through discovery
|
|
async_fire_mqtt_message(
|
|
hass, "homeassistant/light/bla/config", json.dumps(config_discovery)
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert _check_entities() == 2
|
|
|
|
# reload manual configured items and assert again
|
|
await help_test_reload_with_config(hass, caplog, tmp_path, config_manual)
|
|
assert _check_entities() == 2
|
|
|
|
|
|
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR])
|
|
@pytest.mark.parametrize(
|
|
"config_manual",
|
|
[
|
|
{"mqtt": {"sensor": []}},
|
|
{"mqtt": {"broker": "test"}},
|
|
],
|
|
)
|
|
async def test_setup_manual_entity_from_yaml(
|
|
hass: HomeAssistant, config_manual: ConfigType
|
|
) -> None:
|
|
"""Test setup with empty platform keys."""
|
|
await help_test_setup_manual_entity_from_yaml(hass, config_manual)
|