diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index b39fee9c185..4dba510396f 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -202,6 +202,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): picture: str | None = None, ) -> AreaEntry: """Create a new area.""" + self.hass.verify_event_loop_thread("async_create") normalized_name = normalize_name(name) if self.async_get_area_by_name(name): @@ -221,7 +222,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): assert area.id is not None self.areas[area.id] = area self.async_schedule_save() - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_AREA_REGISTRY_UPDATED, EventAreaRegistryUpdatedData(action="create", area_id=area.id), ) @@ -230,6 +231,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): @callback def async_delete(self, area_id: str) -> None: """Delete area.""" + self.hass.verify_event_loop_thread("async_delete") device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) device_registry.async_clear_area_id(area_id) @@ -237,7 +239,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): del self.areas[area_id] - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_AREA_REGISTRY_UPDATED, EventAreaRegistryUpdatedData(action="remove", area_id=area_id), ) @@ -266,6 +268,10 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): name=name, picture=picture, ) + # Since updated may be the old or the new and we always fire + # an event even if nothing has changed we cannot use async_fire_internal + # here because we do not know if the thread safety check already + # happened or not in _async_update. self.hass.bus.async_fire( EVENT_AREA_REGISTRY_UPDATED, EventAreaRegistryUpdatedData(action="update", area_id=area_id), @@ -306,6 +312,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): if not new_values: return old + self.hass.verify_event_loop_thread("_async_update") new = self.areas[area_id] = dataclasses.replace(old, **new_values) # type: ignore[arg-type] self.async_schedule_save() diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 1ee8a42b6b9..22f1dc8e534 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -1,5 +1,6 @@ """Tests for the Area Registry.""" +from functools import partial from typing import Any import pytest @@ -491,3 +492,40 @@ async def test_entries_for_label( assert not ar.async_entries_for_label(area_registry, "unknown") assert not ar.async_entries_for_label(area_registry, "") + + +async def test_async_get_or_create_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to create in the wrong thread.""" + with pytest.raises( + RuntimeError, + match="Detected code that calls async_create from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(area_registry.async_create, "Mock1") + + +async def test_async_update_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to update in the wrong thread.""" + area = area_registry.async_create("Mock1") + with pytest.raises( + RuntimeError, + match="Detected code that calls _async_update from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial(area_registry.async_update, area.id, name="Mock2") + ) + + +async def test_async_delete_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to delete in the wrong thread.""" + area = area_registry.async_create("Mock1") + with pytest.raises( + RuntimeError, + match="Detected code that calls async_delete from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(area_registry.async_delete, area.id)