core/tests/components/mqtt_statestream/test_init.py

658 lines
23 KiB
Python

"""The tests for the MQTT statestream component."""
from unittest.mock import ANY, call
import pytest
import homeassistant.components.mqtt_statestream as statestream
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CoreState, HomeAssistant, State
from homeassistant.setup import async_setup_component
from tests.common import MockEntity, MockEntityPlatform, mock_state_change_event
from tests.typing import MqttMockHAClient
async def add_statestream(
hass: HomeAssistant,
base_topic=None,
publish_attributes=None,
publish_timestamps=None,
publish_include=None,
publish_exclude=None,
) -> bool:
"""Add a mqtt_statestream component."""
config = {}
if base_topic:
config["base_topic"] = base_topic
if publish_attributes:
config["publish_attributes"] = publish_attributes
if publish_timestamps:
config["publish_timestamps"] = publish_timestamps
if publish_include:
config["include"] = publish_include
if publish_exclude:
config["exclude"] = publish_exclude
return await async_setup_component(
hass, statestream.DOMAIN, {statestream.DOMAIN: config}
)
async def test_fails_with_no_base(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Setup should fail if no base_topic is set."""
assert await add_statestream(hass) is False
async def test_setup_succeeds_without_attributes(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test the success of the setup with a valid base_topic."""
assert await add_statestream(hass, base_topic="pub")
async def test_setup_and_stop_waits_for_ha(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test the success of the setup with a valid base_topic."""
e_id = "fake.entity"
# HA is not running
hass.set_state(CoreState.not_running)
assert await add_statestream(hass, base_topic="pub")
await hass.async_block_till_done()
# Set a state of an entity
mock_state_change_event(hass, State(e_id, "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was not published to pub/fake/entity/state
mqtt_mock.async_publish.assert_not_called()
# HA is starting up
await hass.async_start()
await hass.async_block_till_done()
# Change a state of an entity
mock_state_change_event(hass, State(e_id, "off"))
await hass.async_block_till_done()
await hass.async_block_till_done()
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "off", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.reset_mock()
# HA is shutting down
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
# Change a state of an entity
mock_state_change_event(hass, State(e_id, "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was not published to pub/fake/entity/state
mqtt_mock.async_publish.assert_not_called()
# We use xfail with this test because there is an unhandled exception
# in a background task in this test.
# The exception is raised by mqtt.async_publish.
@pytest.mark.xfail
async def test_startup_no_mqtt(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test startup without MQTT support."""
e_id = "fake.entity"
assert await add_statestream(hass, base_topic="pub")
# Set a state of an entity
mock_state_change_event(hass, State(e_id, "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert "MQTT is not enabled" in caplog.text
async def test_setup_succeeds_with_attributes(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test setup with a valid base_topic and publish_attributes."""
assert await add_statestream(hass, base_topic="pub", publish_attributes=True)
async def test_state_changed_event_sends_message(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test the sending of a new message if event changed."""
e_id = "fake.entity"
base_topic = "pub"
# Add the statestream component for publishing state updates
assert await add_statestream(hass, base_topic=base_topic)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity
mock_state_change_event(hass, State(e_id, "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "on", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Create a test entity and add it to hass
platform = MockEntityPlatform(hass)
entity = MockEntity(unique_id="1234")
await platform.async_add_entities([entity])
await hass.async_block_till_done()
mqtt_mock.async_publish.assert_called_with(
"pub/test_domain/test_platform_1234/state", "unknown", 1, True
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("test_domain.test_platform_1234")
assert state is not None
# Now remove it, nothing should be published
hass.states.async_remove("test_domain.test_platform_1234")
await hass.async_block_till_done()
await hass.async_block_till_done()
mqtt_mock.async_publish.assert_not_called()
async def test_state_changed_event_sends_message_and_timestamp(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test the sending of a message and timestamps if event changed."""
e_id = "another.entity"
base_topic = "pub"
# Add the statestream component for publishing state updates
assert await add_statestream(
hass, base_topic=base_topic, publish_attributes=None, publish_timestamps=True
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity
mock_state_change_event(hass, State(e_id, "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
calls = [
call.async_publish("pub/another/entity/state", "on", 1, True),
call.async_publish("pub/another/entity/last_changed", ANY, 1, True),
call.async_publish("pub/another/entity/last_updated", ANY, 1, True),
]
mqtt_mock.async_publish.assert_has_calls(calls, any_order=True)
assert mqtt_mock.async_publish.called
async def test_state_changed_attr_sends_message(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test the sending of a new message if attribute changed."""
e_id = "fake.entity"
base_topic = "pub"
# Add the statestream component for publishing state updates
assert await add_statestream(hass, base_topic=base_topic, publish_attributes=True)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
test_attributes = {"testing": "YES", "list": ["a", "b", "c"], "bool": False}
# Set a state of an entity
mock_state_change_event(hass, State(e_id, "off", attributes=test_attributes))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
calls = [
call.async_publish("pub/fake/entity/state", "off", 1, True),
call.async_publish("pub/fake/entity/testing", '"YES"', 1, True),
call.async_publish("pub/fake/entity/list", '["a", "b", "c"]', 1, True),
call.async_publish("pub/fake/entity/bool", "false", 1, True),
]
mqtt_mock.async_publish.assert_has_calls(calls, any_order=True)
assert mqtt_mock.async_publish.called
async def test_state_changed_event_include_domain(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test that filtering on included domain works as expected."""
base_topic = "pub"
incl = {"domains": ["fake"]}
excl = {}
# Add the statestream component for publishing state updates
# Set the filter to allow fake.* items
assert await add_statestream(
hass, base_topic=base_topic, publish_include=incl, publish_exclude=excl
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity
mock_state_change_event(hass, State("fake.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "on", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included
mock_state_change_event(hass, State("fake2.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
async def test_state_changed_event_include_entity(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test that filtering on included entity works as expected."""
base_topic = "pub"
incl = {"entities": ["fake.entity"]}
excl = {}
# Add the statestream component for publishing state updates
# Set the filter to allow fake.* items
assert await add_statestream(
hass, base_topic=base_topic, publish_include=incl, publish_exclude=excl
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity
mock_state_change_event(hass, State("fake.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "on", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included
mock_state_change_event(hass, State("fake.entity2", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
async def test_state_changed_event_exclude_domain(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test that filtering on excluded domain works as expected."""
base_topic = "pub"
incl = {}
excl = {"domains": ["fake2"]}
# Add the statestream component for publishing state updates
# Set the filter to allow fake.* items
assert await add_statestream(
hass, base_topic=base_topic, publish_include=incl, publish_exclude=excl
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity
mock_state_change_event(hass, State("fake.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "on", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included
mock_state_change_event(hass, State("fake2.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
async def test_state_changed_event_exclude_entity(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test that filtering on excluded entity works as expected."""
base_topic = "pub"
incl = {}
excl = {"entities": ["fake.entity2"]}
# Add the statestream component for publishing state updates
# Set the filter to allow fake.* items
assert await add_statestream(
hass, base_topic=base_topic, publish_include=incl, publish_exclude=excl
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity
mock_state_change_event(hass, State("fake.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "on", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included
mock_state_change_event(hass, State("fake.entity2", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
async def test_state_changed_event_exclude_domain_include_entity(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test filtering with excluded domain and included entity."""
base_topic = "pub"
incl = {"entities": ["fake.entity"]}
excl = {"domains": ["fake"]}
# Add the statestream component for publishing state updates
# Set the filter to allow fake.* items
assert await add_statestream(
hass, base_topic=base_topic, publish_include=incl, publish_exclude=excl
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity
mock_state_change_event(hass, State("fake.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "on", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included
mock_state_change_event(hass, State("fake.entity2", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
async def test_state_changed_event_include_domain_exclude_entity(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test filtering with included domain and excluded entity."""
base_topic = "pub"
incl = {"domains": ["fake"]}
excl = {"entities": ["fake.entity2"]}
# Add the statestream component for publishing state updates
# Set the filter to allow fake.* items
assert await add_statestream(
hass, base_topic=base_topic, publish_include=incl, publish_exclude=excl
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity
mock_state_change_event(hass, State("fake.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "on", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included
mock_state_change_event(hass, State("fake.entity2", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
async def test_state_changed_event_include_globs(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test that filtering on included glob works as expected."""
base_topic = "pub"
incl = {"entity_globs": ["*.included_*"]}
excl = {}
# Add the statestream component for publishing state updates
# Set the filter to allow *.included_* items
assert await add_statestream(
hass, base_topic=base_topic, publish_include=incl, publish_exclude=excl
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity with included glob
mock_state_change_event(hass, State("fake2.included_entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake2/included_entity/state
mqtt_mock.async_publish.assert_called_with(
"pub/fake2/included_entity/state", "on", 1, True
)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included
mock_state_change_event(hass, State("fake2.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
async def test_state_changed_event_exclude_globs(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test that filtering on excluded globs works as expected."""
base_topic = "pub"
incl = {}
excl = {"entity_globs": ["*.excluded_*"]}
# Add the statestream component for publishing state updates
# Set the filter to allow *.excluded_* items
assert await add_statestream(
hass, base_topic=base_topic, publish_include=incl, publish_exclude=excl
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity
mock_state_change_event(hass, State("fake.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "on", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included by glob
mock_state_change_event(hass, State("fake.excluded_entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
async def test_state_changed_event_exclude_domain_globs_include_entity(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test filtering with excluded domain and glob and included entity."""
base_topic = "pub"
incl = {"entities": ["fake.entity"]}
excl = {"domains": ["fake"], "entity_globs": ["*.excluded_*"]}
# Add the statestream component for publishing state updates
# Set the filter to exclude with include filter
assert await add_statestream(
hass, base_topic=base_topic, publish_include=incl, publish_exclude=excl
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity
mock_state_change_event(hass, State("fake.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "on", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that doesn't match any filters
mock_state_change_event(hass, State("fake2.included_entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with(
"pub/fake2/included_entity/state", "on", 1, True
)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included by domain
mock_state_change_event(hass, State("fake.entity2", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included by glob
mock_state_change_event(hass, State("fake.excluded_entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
async def test_state_changed_event_include_domain_globs_exclude_entity(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
) -> None:
"""Test filtering with included domain and glob and excluded entity."""
base_topic = "pub"
incl = {"domains": ["fake"], "entity_globs": ["*.included_*"]}
excl = {"entities": ["fake.entity2"]}
# Add the statestream component for publishing state updates
# Set the filter to include with exclude filter
assert await add_statestream(
hass, base_topic=base_topic, publish_include=incl, publish_exclude=excl
)
await hass.async_block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_statestream state change on initialization, etc.
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity included by domain
mock_state_change_event(hass, State("fake.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with("pub/fake/entity/state", "on", 1, True)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity included by glob
mock_state_change_event(hass, State("fake.included_entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
# Make sure 'on' was published to pub/fake/entity/state
mqtt_mock.async_publish.assert_called_with(
"pub/fake/included_entity/state", "on", 1, True
)
assert mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that shouldn't be included
mock_state_change_event(hass, State("fake.entity2", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called
mqtt_mock.async_publish.reset_mock()
# Set a state of an entity that doesn't match any filters
mock_state_change_event(hass, State("fake2.entity", "on"))
await hass.async_block_till_done()
await hass.async_block_till_done()
assert not mqtt_mock.async_publish.called