diff --git a/homeassistant/core.py b/homeassistant/core.py index ad5fb44a514..1993e657368 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -82,7 +82,7 @@ from .exceptions import ( ) from .helpers.aiohttp_compat import restore_original_aiohttp_cancel_behavior from .helpers.json import json_dumps -from .util import dt as dt_util, location, ulid as ulid_util +from .util import dt as dt_util, location from .util.async_ import ( cancelling, run_callback_threadsafe, @@ -91,6 +91,7 @@ from .util.async_ import ( from .util.json import JsonObjectType from .util.read_only_dict import ReadOnlyDict from .util.timeout import TimeoutManager +from .util.ulid import ulid, ulid_at_time from .util.unit_system import ( _CONF_UNIT_SYSTEM_IMPERIAL, _CONF_UNIT_SYSTEM_US_CUSTOMARY, @@ -874,7 +875,7 @@ class Context: id: str | None = None, # pylint: disable=redefined-builtin ) -> None: """Init the context.""" - self.id = id or ulid_util.ulid() + self.id = id or ulid() self.user_id = user_id self.parent_id = parent_id self.origin_event: Event | None = None @@ -926,10 +927,14 @@ class Event: self.data = data or {} self.origin = origin self.time_fired = time_fired or dt_util.utcnow() - self.context: Context = context or Context( - id=ulid_util.ulid_at_time(dt_util.utc_to_timestamp(self.time_fired)) - ) + if not context: + context = Context( + id=ulid_at_time(dt_util.utc_to_timestamp(self.time_fired)) + ) + self.context = context self._as_dict: ReadOnlyDict[str, Any] | None = None + if not context.origin_event: + context.origin_event = self def as_dict(self) -> ReadOnlyDict[str, Any]: """Create a dict representation of this Event. @@ -973,6 +978,8 @@ class EventBus: def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" self._listeners: dict[str, list[_FilterableJob]] = {} + self._match_all_listeners: list[_FilterableJob] = [] + self._listeners[MATCH_ALL] = self._match_all_listeners self._hass = hass @callback @@ -1019,20 +1026,19 @@ class EventBus: ) listeners = self._listeners.get(event_type, []) + match_all_listeners = self._match_all_listeners - # EVENT_HOMEASSISTANT_CLOSE should go only to this listeners - match_all_listeners = self._listeners.get(MATCH_ALL) - if match_all_listeners is not None and event_type != EVENT_HOMEASSISTANT_CLOSE: + if not listeners and not match_all_listeners: + return + + # EVENT_HOMEASSISTANT_CLOSE should not be sent to MATCH_ALL listeners + if event_type != EVENT_HOMEASSISTANT_CLOSE: listeners = match_all_listeners + listeners event = Event(event_type, event_data, origin, time_fired, context) - if not event.context.origin_event: - event.context.origin_event = event - _LOGGER.debug("Bus:Handling %s", event) - - if not listeners: - return + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Bus:Handling %s", event) for job, event_filter, run_immediately in listeners: if event_filter is not None: @@ -1195,7 +1201,7 @@ class EventBus: self._listeners[event_type].remove(filterable_job) # delete event_type list if empty - if not self._listeners[event_type]: + if not self._listeners[event_type] and event_type != MATCH_ALL: self._listeners.pop(event_type) except (KeyError, ValueError): # KeyError is key event_type listener did not exist @@ -1630,7 +1636,7 @@ class StateMachine: # https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6323 timestamp = time.time() now = dt_util.utc_from_timestamp(timestamp) - context = Context(id=ulid_util.ulid_at_time(timestamp)) + context = Context(id=ulid_at_time(timestamp)) else: now = dt_util.utcnow() diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index 01472144c59..afb49cbffca 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -296,6 +296,7 @@ async def test_service_calls_off_mode( device.set_setpoint_heat.reset_mock() device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError + caplog.clear() await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -308,8 +309,9 @@ async def test_service_calls_off_mode( ) device.set_setpoint_cool.assert_called_with(95) device.set_setpoint_heat.assert_called_with(77) - assert "Invalid temperature" in caplog.messages[-1] + assert "Invalid temperature" in caplog.text + caplog.clear() reset_mock(device) await hass.services.async_call( CLIMATE_DOMAIN, @@ -436,6 +438,7 @@ async def test_service_calls_cool_mode( device.set_setpoint_cool.assert_called_with(95) device.set_setpoint_heat.assert_called_with(77) + caplog.clear() device.set_setpoint_cool.reset_mock() device.set_setpoint_cool.side_effect = aiosomecomfort.SomeComfortError await hass.services.async_call( @@ -450,7 +453,7 @@ async def test_service_calls_cool_mode( ) device.set_setpoint_cool.assert_called_with(95) device.set_setpoint_heat.assert_called_with(77) - assert "Invalid temperature" in caplog.messages[-1] + assert "Invalid temperature" in caplog.text reset_mock(device) await hass.services.async_call( @@ -467,6 +470,7 @@ async def test_service_calls_cool_mode( reset_mock(device) device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError + caplog.clear() await hass.services.async_call( CLIMATE_DOMAIN, @@ -478,7 +482,7 @@ async def test_service_calls_cool_mode( device.set_hold_cool.assert_called_once_with(True, 12) device.set_hold_heat.assert_not_called() device.set_setpoint_heat.assert_not_called() - assert "Temperature out of range" in caplog.messages[-1] + assert "Temperature out of range" in caplog.text reset_mock(device) @@ -512,6 +516,7 @@ async def test_service_calls_cool_mode( device.raw_ui_data["StatusHeat"] = 2 device.raw_ui_data["StatusCool"] = 2 + caplog.clear() await hass.services.async_call( CLIMATE_DOMAIN, @@ -521,7 +526,7 @@ async def test_service_calls_cool_mode( ) device.set_hold_cool.assert_called_once_with(True) device.set_hold_heat.assert_not_called() - assert "Couldn't set permanent hold" in caplog.messages[-1] + assert "Couldn't set permanent hold" in caplog.text reset_mock(device) @@ -536,6 +541,7 @@ async def test_service_calls_cool_mode( device.set_hold_cool.assert_called_once_with(False) reset_mock(device) + caplog.clear() device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError @@ -548,7 +554,7 @@ async def test_service_calls_cool_mode( device.set_hold_heat.assert_not_called() device.set_hold_cool.assert_called_once_with(False) - assert "Can not stop hold mode" in caplog.messages[-1] + assert "Can not stop hold mode" in caplog.text reset_mock(device) @@ -566,6 +572,8 @@ async def test_service_calls_cool_mode( device.set_hold_heat.assert_not_called() reset_mock(device) + caplog.clear() + device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError device.raw_ui_data["StatusHeat"] = 2 @@ -580,9 +588,10 @@ async def test_service_calls_cool_mode( device.set_hold_cool.assert_called_once_with(True) device.set_hold_heat.assert_not_called() - assert "Couldn't set permanent hold" in caplog.messages[-1] + assert "Couldn't set permanent hold" in caplog.text reset_mock(device) + caplog.clear() device.raw_ui_data["StatusHeat"] = 2 device.raw_ui_data["StatusCool"] = 2 @@ -597,7 +606,7 @@ async def test_service_calls_cool_mode( device.set_hold_cool.assert_not_called() device.set_hold_heat.assert_not_called() - assert "Invalid system mode returned" in caplog.messages[-2] + assert "Invalid system mode returned" in caplog.text async def test_service_calls_heat_mode( @@ -638,8 +647,9 @@ async def test_service_calls_heat_mode( ) device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 59) device.set_hold_heat.reset_mock() - assert "Invalid temperature" in caplog.messages[-1] + assert "Invalid temperature" in caplog.text + caplog.clear() await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -667,7 +677,7 @@ async def test_service_calls_heat_mode( ) device.set_setpoint_cool.assert_called_with(95) device.set_setpoint_heat.assert_called_with(77) - assert "Invalid temperature" in caplog.messages[-1] + assert "Invalid temperature" in caplog.text reset_mock(device) device.raw_ui_data["StatusHeat"] = 2 @@ -696,6 +706,7 @@ async def test_service_calls_heat_mode( device.set_setpoint_heat.assert_called_once() reset_mock(device) + caplog.clear() device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError @@ -710,7 +721,7 @@ async def test_service_calls_heat_mode( ) device.set_hold_heat.assert_called_once_with(True) device.set_hold_cool.assert_not_called() - assert "Couldn't set permanent hold" in caplog.messages[-1] + assert "Couldn't set permanent hold" in caplog.text reset_mock(device) @@ -726,6 +737,7 @@ async def test_service_calls_heat_mode( device.set_setpoint_cool.assert_not_called() reset_mock(device) + caplog.clear() device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError @@ -739,9 +751,10 @@ async def test_service_calls_heat_mode( device.set_hold_heat.assert_called_once_with(True, 22) device.set_hold_cool.assert_not_called() device.set_setpoint_cool.assert_not_called() - assert "Temperature out of range" in caplog.messages[-1] + assert "Temperature out of range" in caplog.text reset_mock(device) + caplog.clear() await hass.services.async_call( CLIMATE_DOMAIN, @@ -765,7 +778,7 @@ async def test_service_calls_heat_mode( ) device.set_hold_heat.assert_called_once_with(False) - assert "Can not stop hold mode" in caplog.messages[-1] + assert "Can not stop hold mode" in caplog.text reset_mock(device) device.raw_ui_data["StatusHeat"] = 2 @@ -844,6 +857,7 @@ async def test_service_calls_auto_mode( device.set_setpoint_heat.assert_called_once_with(77) reset_mock(device) + caplog.clear() device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError @@ -855,9 +869,10 @@ async def test_service_calls_auto_mode( blocking=True, ) device.set_setpoint_heat.assert_not_called() - assert "Invalid temperature" in caplog.messages[-1] + assert "Invalid temperature" in caplog.text reset_mock(device) + caplog.clear() device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError device.set_setpoint_cool.side_effect = aiosomecomfort.SomeComfortError @@ -872,9 +887,10 @@ async def test_service_calls_auto_mode( blocking=True, ) device.set_setpoint_heat.assert_not_called() - assert "Invalid temperature" in caplog.messages[-1] + assert "Invalid temperature" in caplog.text reset_mock(device) + caplog.clear() device.set_hold_heat.side_effect = None device.set_hold_cool.side_effect = None @@ -893,6 +909,7 @@ async def test_service_calls_auto_mode( device.set_hold_heat.assert_called_once_with(True) reset_mock(device) + caplog.clear() device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError device.raw_ui_data["StatusHeat"] = 2 @@ -906,7 +923,7 @@ async def test_service_calls_auto_mode( ) device.set_hold_cool.assert_called_once_with(True) device.set_hold_heat.assert_called_once_with(True) - assert "Couldn't set permanent hold" in caplog.messages[-1] + assert "Couldn't set permanent hold" in caplog.text reset_mock(device) device.set_setpoint_heat.side_effect = None @@ -923,6 +940,7 @@ async def test_service_calls_auto_mode( device.set_hold_heat.assert_called_once_with(True, 22) reset_mock(device) + caplog.clear() await hass.services.async_call( CLIMATE_DOMAIN, @@ -946,9 +964,10 @@ async def test_service_calls_auto_mode( device.set_hold_heat.assert_not_called() device.set_hold_cool.assert_called_once_with(False) - assert "Can not stop hold mode" in caplog.messages[-1] + assert "Can not stop hold mode" in caplog.text reset_mock(device) + caplog.clear() device.raw_ui_data["StatusHeat"] = 2 device.raw_ui_data["StatusCool"] = 2 @@ -978,7 +997,7 @@ async def test_service_calls_auto_mode( device.set_hold_cool.assert_called_once_with(True) device.set_hold_heat.assert_not_called() - assert "Couldn't set permanent hold" in caplog.messages[-1] + assert "Couldn't set permanent hold" in caplog.text async def test_async_update_errors( diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index a61ea692bf2..5eabb2202aa 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -49,7 +49,7 @@ async def test_setup_no_mqtt( async def test_setup_with_pub(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None: """Test the setup with subscription.""" # Should start off with no listeners for all events - assert hass.bus.async_listeners().get("*") is None + assert not hass.bus.async_listeners().get("*") assert await add_eventstream(hass, pub_topic="bar") await hass.async_block_till_done() diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index 365cd942ab9..96f384f98b9 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -11,7 +11,11 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import assert_setup_component, async_fire_time_changed +from tests.common import ( + assert_setup_component, + async_capture_events, + async_fire_time_changed, +) _LOGGER = logging.getLogger(__name__) @@ -222,9 +226,9 @@ async def test_start_stop(mock_pilight_error, hass: HomeAssistant) -> None: @patch("pilight.pilight.Client", PilightDaemonSim) -@patch("homeassistant.core._LOGGER.debug") -async def test_receive_code(mock_debug, hass: HomeAssistant) -> None: +async def test_receive_code(hass: HomeAssistant) -> None: """Check if code receiving via pilight daemon works.""" + events = async_capture_events(hass, pilight.EVENT) with assert_setup_component(4): assert await async_setup_component(hass, pilight.DOMAIN, {pilight.DOMAIN: {}}) @@ -239,18 +243,13 @@ async def test_receive_code(mock_debug, hass: HomeAssistant) -> None: }, **PilightDaemonSim.test_message["message"], ) - debug_log_call = mock_debug.call_args_list[-1] - - # Check if all message parts are put on event bus - for key, value in expected_message.items(): - assert str(key) in str(debug_log_call) - assert str(value) in str(debug_log_call) + assert events[0].data == expected_message @patch("pilight.pilight.Client", PilightDaemonSim) -@patch("homeassistant.core._LOGGER.debug") -async def test_whitelist_exact_match(mock_debug, hass: HomeAssistant) -> None: +async def test_whitelist_exact_match(hass: HomeAssistant) -> None: """Check whitelist filter with matched data.""" + events = async_capture_events(hass, pilight.EVENT) with assert_setup_component(4): whitelist = { "protocol": [PilightDaemonSim.test_message["protocol"]], @@ -272,18 +271,14 @@ async def test_whitelist_exact_match(mock_debug, hass: HomeAssistant) -> None: }, **PilightDaemonSim.test_message["message"], ) - debug_log_call = mock_debug.call_args_list[-1] - # Check if all message parts are put on event bus - for key, value in expected_message.items(): - assert str(key) in str(debug_log_call) - assert str(value) in str(debug_log_call) + assert events[0].data == expected_message @patch("pilight.pilight.Client", PilightDaemonSim) -@patch("homeassistant.core._LOGGER.debug") -async def test_whitelist_partial_match(mock_debug, hass: HomeAssistant) -> None: +async def test_whitelist_partial_match(hass: HomeAssistant) -> None: """Check whitelist filter with partially matched data, should work.""" + events = async_capture_events(hass, pilight.EVENT) with assert_setup_component(4): whitelist = { "protocol": [PilightDaemonSim.test_message["protocol"]], @@ -303,18 +298,15 @@ async def test_whitelist_partial_match(mock_debug, hass: HomeAssistant) -> None: }, **PilightDaemonSim.test_message["message"], ) - debug_log_call = mock_debug.call_args_list[-1] - # Check if all message parts are put on event bus - for key, value in expected_message.items(): - assert str(key) in str(debug_log_call) - assert str(value) in str(debug_log_call) + assert events[0].data == expected_message @patch("pilight.pilight.Client", PilightDaemonSim) -@patch("homeassistant.core._LOGGER.debug") -async def test_whitelist_or_match(mock_debug, hass: HomeAssistant) -> None: +async def test_whitelist_or_match(hass: HomeAssistant) -> None: """Check whitelist filter with several subsection, should work.""" + events = async_capture_events(hass, pilight.EVENT) + with assert_setup_component(4): whitelist = { "protocol": [ @@ -337,18 +329,15 @@ async def test_whitelist_or_match(mock_debug, hass: HomeAssistant) -> None: }, **PilightDaemonSim.test_message["message"], ) - debug_log_call = mock_debug.call_args_list[-1] - # Check if all message parts are put on event bus - for key, value in expected_message.items(): - assert str(key) in str(debug_log_call) - assert str(value) in str(debug_log_call) + assert events[0].data == expected_message @patch("pilight.pilight.Client", PilightDaemonSim) -@patch("homeassistant.core._LOGGER.debug") -async def test_whitelist_no_match(mock_debug, hass: HomeAssistant) -> None: +async def test_whitelist_no_match(hass: HomeAssistant) -> None: """Check whitelist filter with unmatched data, should not work.""" + events = async_capture_events(hass, pilight.EVENT) + with assert_setup_component(4): whitelist = { "protocol": ["wrong_protocol"], @@ -360,9 +349,8 @@ async def test_whitelist_no_match(mock_debug, hass: HomeAssistant) -> None: await hass.async_start() await hass.async_block_till_done() - debug_log_call = mock_debug.call_args_list[-1] - assert "Event pilight_received" not in debug_log_call + assert len(events) == 0 async def test_call_rate_delay_throttle_enabled(hass: HomeAssistant) -> None: