"""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