diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index 3bd2e8d7533..34327304888 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -73,31 +73,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: started = False async def _async_handle_labs_update( - event: Event[labs.EventLabsUpdatedData], + event_data: labs.EventLabsUpdatedData, ) -> None: """Handle labs feature toggle.""" - await analytics.save_preferences({ATTR_SNAPSHOTS: event.data["enabled"]}) + await analytics.save_preferences({ATTR_SNAPSHOTS: event_data["enabled"]}) if started: await analytics.async_schedule() - @callback - def _async_labs_event_filter(event_data: labs.EventLabsUpdatedData) -> bool: - """Filter labs events for this integration's snapshot feature.""" - return ( - event_data["domain"] == DOMAIN - and event_data["preview_feature"] == LABS_SNAPSHOT_FEATURE - ) - async def start_schedule(_event: Event) -> None: """Start the send schedule after the started event.""" nonlocal started started = True await analytics.async_schedule() - hass.bus.async_listen( - labs.EVENT_LABS_UPDATED, - _async_handle_labs_update, - event_filter=_async_labs_event_filter, + labs.async_subscribe_preview_feature( + hass, DOMAIN, LABS_SNAPSHOT_FEATURE, _async_handle_labs_update ) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index dbae4c5af89..7643219484a 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components import labs, websocket_api from homeassistant.components.blueprint import CONF_USE_BLUEPRINT -from homeassistant.components.labs import async_listen as async_labs_listen +from homeassistant.components.labs import async_subscribe_preview_feature from homeassistant.const import ( ATTR_AREA_ID, ATTR_ENTITY_ID, @@ -386,14 +386,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=vol.Schema({vol.Optional(CONF_ID): str}), ) - @callback - def new_triggers_conditions_listener() -> None: + async def new_triggers_conditions_listener( + _event_data: labs.EventLabsUpdatedData, + ) -> None: """Handle new_triggers_conditions flag change.""" - hass.async_create_task( - reload_helper.execute_service(ServiceCall(hass, DOMAIN, SERVICE_RELOAD)) - ) + await reload_helper.execute_service(ServiceCall(hass, DOMAIN, SERVICE_RELOAD)) - async_labs_listen( + async_subscribe_preview_feature( hass, DOMAIN, NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG, diff --git a/homeassistant/components/labs/__init__.py b/homeassistant/components/labs/__init__.py index 485d7f8c878..00a9e9c241d 100644 --- a/homeassistant/components/labs/__init__.py +++ b/homeassistant/components/labs/__init__.py @@ -21,6 +21,7 @@ from .const import DOMAIN, LABS_DATA, STORAGE_KEY, STORAGE_VERSION from .helpers import ( async_is_preview_feature_enabled, async_listen, + async_subscribe_preview_feature, async_update_preview_feature, ) from .models import ( @@ -41,6 +42,7 @@ __all__ = [ "EventLabsUpdatedData", "async_is_preview_feature_enabled", "async_listen", + "async_subscribe_preview_feature", "async_update_preview_feature", ] diff --git a/homeassistant/components/labs/helpers.py b/homeassistant/components/labs/helpers.py index 85430724bef..81454cbe811 100644 --- a/homeassistant/components/labs/helpers.py +++ b/homeassistant/components/labs/helpers.py @@ -2,7 +2,8 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine +from typing import Any from homeassistant.const import EVENT_LABS_UPDATED from homeassistant.core import Event, HomeAssistant, callback @@ -32,6 +33,43 @@ def async_is_preview_feature_enabled( return (domain, preview_feature) in labs_data.data.preview_feature_status +@callback +def async_subscribe_preview_feature( + hass: HomeAssistant, + domain: str, + preview_feature: str, + listener: Callable[[EventLabsUpdatedData], Coroutine[Any, Any, None]], +) -> Callable[[], None]: + """Listen for changes to a specific preview feature. + + Args: + hass: HomeAssistant instance + domain: Integration domain + preview_feature: Preview feature name + listener: Coroutine function to invoke when the preview feature + is toggled. Receives the event data as argument. Runs eagerly. + + Returns: + Callable to unsubscribe from the listener + """ + + @callback + def _async_event_filter(event_data: EventLabsUpdatedData) -> bool: + """Filter labs events for this integration's preview feature.""" + return ( + event_data["domain"] == domain + and event_data["preview_feature"] == preview_feature + ) + + async def _handler(event: Event[EventLabsUpdatedData]) -> None: + """Handle labs feature update event.""" + await listener(event.data) + + return hass.bus.async_listen( + EVENT_LABS_UPDATED, _handler, event_filter=_async_event_filter + ) + + @callback def async_listen( hass: HomeAssistant, @@ -51,16 +89,10 @@ def async_listen( Callable to unsubscribe from the listener """ - @callback - def _async_feature_updated(event: Event[EventLabsUpdatedData]) -> None: - """Handle labs feature update event.""" - if ( - event.data["domain"] == domain - and event.data["preview_feature"] == preview_feature - ): - listener() + async def _listener(_event_data: EventLabsUpdatedData) -> None: + listener() - return hass.bus.async_listen(EVENT_LABS_UPDATED, _async_feature_updated) + return async_subscribe_preview_feature(hass, domain, preview_feature, _listener) async def async_update_preview_feature( diff --git a/homeassistant/components/labs/websocket_api.py b/homeassistant/components/labs/websocket_api.py index f1f744f8a47..7b19e9edc52 100644 --- a/homeassistant/components/labs/websocket_api.py +++ b/homeassistant/components/labs/websocket_api.py @@ -13,9 +13,10 @@ from homeassistant.core import HomeAssistant, callback from .const import LABS_DATA from .helpers import ( async_is_preview_feature_enabled, - async_listen, + async_subscribe_preview_feature, async_update_preview_feature, ) +from .models import EventLabsUpdatedData @callback @@ -132,20 +133,27 @@ def websocket_subscribe_feature( preview_feature = labs_data.preview_features[preview_feature_id] - @callback - def send_event() -> None: + async def send_event(event_data: EventLabsUpdatedData) -> None: """Send feature state to client.""" - enabled = async_is_preview_feature_enabled(hass, domain, preview_feature_key) connection.send_message( websocket_api.event_message( msg["id"], - preview_feature.to_dict(enabled=enabled), + preview_feature.to_dict(enabled=event_data["enabled"]), ) ) - connection.subscriptions[msg["id"]] = async_listen( + connection.subscriptions[msg["id"]] = async_subscribe_preview_feature( hass, domain, preview_feature_key, send_event ) connection.send_result(msg["id"]) - send_event() + connection.send_message( + websocket_api.event_message( + msg["id"], + preview_feature.to_dict( + enabled=async_is_preview_feature_enabled( + hass, domain, preview_feature_key + ) + ), + ) + ) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 8b1e2ce8b30..2231c254c78 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -13,7 +13,10 @@ import voluptuous as vol from homeassistant.components import automation, websocket_api from homeassistant.components.blueprint import CONF_USE_BLUEPRINT -from homeassistant.components.labs import async_listen as async_labs_listen +from homeassistant.components.labs import ( + EventLabsUpdatedData, + async_subscribe_preview_feature, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -282,14 +285,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, SERVICE_TOGGLE, toggle_service, schema=SCRIPT_TURN_ONOFF_SCHEMA ) - @callback - def new_triggers_conditions_listener() -> None: + async def new_triggers_conditions_listener( + _event_data: EventLabsUpdatedData, + ) -> None: """Handle new_triggers_conditions flag change.""" - hass.async_create_task( - reload_service(ServiceCall(hass, DOMAIN, SERVICE_RELOAD)) - ) + await reload_service(ServiceCall(hass, DOMAIN, SERVICE_RELOAD)) - async_labs_listen( + async_subscribe_preview_feature( hass, automation.DOMAIN, automation.NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG, diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 865a899bcd7..e614b33287c 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -173,14 +173,15 @@ async def async_setup(hass: HomeAssistant) -> None: hass.data[CONDITION_PLATFORM_SUBSCRIPTIONS] = [] hass.data[CONDITIONS] = {} - @callback - def new_triggers_conditions_listener() -> None: + async def new_triggers_conditions_listener( + _event_data: labs.EventLabsUpdatedData, + ) -> None: """Handle new_triggers_conditions flag change.""" # Invalidate the cache hass.data[CONDITION_DESCRIPTION_CACHE] = {} hass.data[CONDITION_DISABLED_CONDITIONS] = set() - labs.async_listen( + labs.async_subscribe_preview_feature( hass, automation.DOMAIN, automation.NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG, diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index e92522c6879..188859149ba 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -149,14 +149,15 @@ async def async_setup(hass: HomeAssistant) -> None: hass.data[TRIGGER_PLATFORM_SUBSCRIPTIONS] = [] hass.data[TRIGGERS] = {} - @callback - def new_triggers_conditions_listener() -> None: + async def new_triggers_conditions_listener( + _event_data: labs.EventLabsUpdatedData, + ) -> None: """Handle new_triggers_conditions flag change.""" # Invalidate the cache hass.data[TRIGGER_DESCRIPTION_CACHE] = {} hass.data[TRIGGER_DISABLED_TRIGGERS] = set() - labs.async_listen( + labs.async_subscribe_preview_feature( hass, automation.DOMAIN, automation.NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG, diff --git a/tests/components/labs/test_init.py b/tests/components/labs/test_init.py index cc040d11321..46f2ca01a23 100644 --- a/tests/components/labs/test_init.py +++ b/tests/components/labs/test_init.py @@ -9,8 +9,10 @@ import pytest from homeassistant.components.labs import ( EVENT_LABS_UPDATED, + EventLabsUpdatedData, async_is_preview_feature_enabled, async_listen, + async_subscribe_preview_feature, async_update_preview_feature, ) from homeassistant.components.labs.const import DOMAIN, LABS_DATA @@ -441,6 +443,96 @@ async def test_async_listen_helper(hass: HomeAssistant) -> None: assert len(listener_calls) == 1 +async def test_async_subscribe_preview_feature_helper(hass: HomeAssistant) -> None: + """Test async_subscribe_preview_feature helper.""" + hass.config.components.add("kitchen_sink") + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + calls: list[EventLabsUpdatedData] = [] + + async def listener(event_data: EventLabsUpdatedData) -> None: + """Test listener callback.""" + calls.append(event_data) + + unsub = async_subscribe_preview_feature( + hass, + domain="kitchen_sink", + preview_feature="special_repair", + listener=listener, + ) + + # Fire event for the subscribed feature + hass.bus.async_fire( + EVENT_LABS_UPDATED, + { + "domain": "kitchen_sink", + "preview_feature": "special_repair", + "enabled": True, + }, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0]["enabled"] is True + + # Fire event for a different feature - should not trigger listener + hass.bus.async_fire( + EVENT_LABS_UPDATED, + { + "domain": "kitchen_sink", + "preview_feature": "other_feature", + "enabled": True, + }, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + + # Fire event for a different domain - should not trigger listener + hass.bus.async_fire( + EVENT_LABS_UPDATED, + { + "domain": "other_domain", + "preview_feature": "special_repair", + "enabled": True, + }, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + + # Fire event with enabled=False + hass.bus.async_fire( + EVENT_LABS_UPDATED, + { + "domain": "kitchen_sink", + "preview_feature": "special_repair", + "enabled": False, + }, + ) + await hass.async_block_till_done() + + assert len(calls) == 2 + assert calls[1]["enabled"] is False + + # Test unsubscribe + unsub() + + hass.bus.async_fire( + EVENT_LABS_UPDATED, + { + "domain": "kitchen_sink", + "preview_feature": "special_repair", + "enabled": True, + }, + ) + await hass.async_block_till_done() + + assert len(calls) == 2 + + async def test_async_update_preview_feature( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: