diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 741947a2e23..1a726623cd4 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -84,7 +84,7 @@ from .helpers import ( template, translation, ) -from .helpers.dispatcher import async_dispatcher_send +from .helpers.dispatcher import async_dispatcher_send_internal from .helpers.storage import get_internal_store_manager from .helpers.system_info import async_get_system_info from .helpers.typing import ConfigType @@ -700,7 +700,7 @@ class _WatchPendingSetups: def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None: """Dispatch the signal.""" if remaining_with_setup_started or not self._previous_was_empty: - async_dispatcher_send( + async_dispatcher_send_internal( self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started ) self._previous_was_empty = not remaining_with_setup_started diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ba642cc0216..aba7f105040 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -48,7 +48,7 @@ from .exceptions import ( ) from .helpers import device_registry, entity_registry, issue_registry as ir, storage from .helpers.debounce import Debouncer -from .helpers.dispatcher import SignalType, async_dispatcher_send +from .helpers.dispatcher import SignalType, async_dispatcher_send_internal from .helpers.event import ( RANDOM_MICROSECOND_MAX, RANDOM_MICROSECOND_MIN, @@ -841,7 +841,7 @@ class ConfigEntry(Generic[_DataT]): error_reason_translation_placeholders, ) self.clear_cache() - async_dispatcher_send( + async_dispatcher_send_internal( hass, SIGNAL_CONFIG_ENTRY_CHANGED, ConfigEntryChange.UPDATED, self ) @@ -1880,6 +1880,7 @@ class ConfigEntries: if entry.entry_id not in self._entries: raise UnknownEntry(entry.entry_id) + self.hass.verify_event_loop_thread("async_update_entry") changed = False _setter = object.__setattr__ @@ -1928,7 +1929,7 @@ class ConfigEntries: self, change_type: ConfigEntryChange, entry: ConfigEntry ) -> None: """Dispatch a config entry change.""" - async_dispatcher_send( + async_dispatcher_send_internal( self.hass, SIGNAL_CONFIG_ENTRY_CHANGED, change_type, entry ) diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 2e14759b814..9f656dad56c 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -16,7 +16,7 @@ from homeassistant.const import Platform from homeassistant.loader import bind_hass from ..util.signal_type import SignalTypeFormat -from .dispatcher import async_dispatcher_connect, async_dispatcher_send +from .dispatcher import async_dispatcher_connect, async_dispatcher_send_internal from .typing import ConfigType, DiscoveryInfoType SIGNAL_PLATFORM_DISCOVERED: SignalTypeFormat[DiscoveryDict] = SignalTypeFormat( @@ -95,7 +95,9 @@ async def async_discover( "discovered": discovered, } - async_dispatcher_send(hass, SIGNAL_PLATFORM_DISCOVERED.format(service), data) + async_dispatcher_send_internal( + hass, SIGNAL_PLATFORM_DISCOVERED.format(service), data + ) @bind_hass @@ -177,4 +179,6 @@ async def async_load_platform( "discovered": discovered, } - async_dispatcher_send(hass, SIGNAL_PLATFORM_DISCOVERED.format(service), data) + async_dispatcher_send_internal( + hass, SIGNAL_PLATFORM_DISCOVERED.format(service), data + ) diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index aa8176a1b83..9a6cc0eca3a 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -145,7 +145,7 @@ def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: ... @bind_hass # type: ignore[misc] # workaround; exclude typing of 2 overload in func def def dispatcher_send(hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts) -> None: """Send signal and data.""" - hass.loop.call_soon_threadsafe(async_dispatcher_send, hass, signal, *args) + hass.loop.call_soon_threadsafe(async_dispatcher_send_internal, hass, signal, *args) def _format_err( @@ -199,9 +199,33 @@ def async_dispatcher_send( This method must be run in the event loop. """ - if hass.config.debug: - hass.verify_event_loop_thread("async_dispatcher_send") + # We turned on asyncio debug in April 2024 in the dev containers + # in the hope of catching some of the issues that have been + # reported. It will take a while to get all the issues fixed in + # custom components. + # + # In 2025.5 we should guard the `verify_event_loop_thread` + # check with a check for the `hass.config.debug` flag being set as + # long term we don't want to be checking this in production + # environments since it is a performance hit. + hass.verify_event_loop_thread("async_dispatcher_send") + async_dispatcher_send_internal(hass, signal, *args) + +@callback +@bind_hass +def async_dispatcher_send_internal( + hass: HomeAssistant, signal: SignalType[*_Ts] | str, *args: *_Ts +) -> None: + """Send signal and data. + + This method is intended to only be used by core internally + and should not be considered a stable API. We will make + breaking changes to this function in the future and it + should not be used in integrations. + + This method must be run in the event loop. + """ if (maybe_dispatchers := hass.data.get(DATA_DISPATCHER)) is None: return dispatchers: _DispatcherDataType[*_Ts] = maybe_dispatchers diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1bbe7749ff7..4b2146d59bf 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -85,7 +85,7 @@ from homeassistant.util.signal_type import SignalType, SignalTypeFormat from . import condition, config_validation as cv, service, template from .condition import ConditionCheckerType, trace_condition_function -from .dispatcher import async_dispatcher_connect, async_dispatcher_send +from .dispatcher import async_dispatcher_connect, async_dispatcher_send_internal from .event import async_call_later, async_track_template from .script_variables import ScriptVariables from .trace import ( @@ -208,7 +208,9 @@ async def trace_action( ) ) ): - async_dispatcher_send(hass, SCRIPT_BREAKPOINT_HIT, key, run_id, path) + async_dispatcher_send_internal( + hass, SCRIPT_BREAKPOINT_HIT, key, run_id, path + ) done = hass.loop.create_future() @@ -1986,7 +1988,7 @@ def debug_continue(hass: HomeAssistant, key: str, run_id: str) -> None: breakpoint_clear(hass, key, run_id, NODE_ANY) signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id) - async_dispatcher_send(hass, signal, "continue") + async_dispatcher_send_internal(hass, signal, "continue") @callback @@ -1996,11 +1998,11 @@ def debug_step(hass: HomeAssistant, key: str, run_id: str) -> None: breakpoint_set(hass, key, run_id, NODE_ANY) signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id) - async_dispatcher_send(hass, signal, "continue") + async_dispatcher_send_internal(hass, signal, "continue") @callback def debug_stop(hass: HomeAssistant, key: str, run_id: str) -> None: """Stop execution of a running or halted script.""" signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id) - async_dispatcher_send(hass, signal, "stop") + async_dispatcher_send_internal(hass, signal, "stop") diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index d9a79cc6a7a..89d05407fbd 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -243,7 +243,6 @@ async def test_dispatcher_add_dispatcher(hass: HomeAssistant) -> None: async def test_thread_safety_checks(hass: HomeAssistant) -> None: """Test dispatcher thread safety checks.""" - hass.config.debug = True calls = [] @callback