From 0b2c29fdb98643dcb42f90e7bfb1f4c4ccebba98 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 May 2024 18:48:38 -0500 Subject: [PATCH] Move thread safety in floor_registry sooner (#117044) --- homeassistant/helpers/floor_registry.py | 9 ++++-- tests/helpers/test_floor_registry.py | 43 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/floor_registry.py b/homeassistant/helpers/floor_registry.py index 63d3bb56100..4d2faba41b9 100644 --- a/homeassistant/helpers/floor_registry.py +++ b/homeassistant/helpers/floor_registry.py @@ -121,6 +121,7 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]): level: int | None = None, ) -> FloorEntry: """Create a new floor.""" + self.hass.verify_event_loop_thread("async_create") if floor := self.async_get_floor_by_name(name): raise ValueError( f"The name {name} ({floor.normalized_name}) is already in use" @@ -139,7 +140,7 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]): floor_id = floor.floor_id self.floors[floor_id] = floor self.async_schedule_save() - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_FLOOR_REGISTRY_UPDATED, EventFloorRegistryUpdatedData( action="create", @@ -151,8 +152,9 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]): @callback def async_delete(self, floor_id: str) -> None: """Delete floor.""" + self.hass.verify_event_loop_thread("async_delete") del self.floors[floor_id] - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_FLOOR_REGISTRY_UPDATED, EventFloorRegistryUpdatedData( action="remove", @@ -189,10 +191,11 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]): if not changes: return old + self.hass.verify_event_loop_thread("async_update") new = self.floors[floor_id] = dataclasses.replace(old, **changes) # type: ignore[arg-type] self.async_schedule_save() - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_FLOOR_REGISTRY_UPDATED, EventFloorRegistryUpdatedData( action="update", diff --git a/tests/helpers/test_floor_registry.py b/tests/helpers/test_floor_registry.py index faa9eb131a1..80734d11561 100644 --- a/tests/helpers/test_floor_registry.py +++ b/tests/helpers/test_floor_registry.py @@ -1,5 +1,6 @@ """Tests for the floor registry.""" +from functools import partial import re from typing import Any @@ -357,3 +358,45 @@ async def test_floor_removed_from_areas( entries = ar.async_entries_for_floor(area_registry, floor.floor_id) assert len(entries) == 0 + + +async def test_async_create_thread_safety( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, +) -> None: + """Test async_create raises when called from 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(floor_registry.async_create, "any") + + +async def test_async_delete_thread_safety( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, +) -> None: + """Test async_delete raises when called from wrong thread.""" + any_floor = floor_registry.async_create("any") + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_delete from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(floor_registry.async_delete, any_floor) + + +async def test_async_update_thread_safety( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, +) -> None: + """Test async_update raises when called from wrong thread.""" + any_floor = floor_registry.async_create("any") + + 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(floor_registry.async_update, any_floor.floor_id, name="new name") + )