From 837f34c40c2ded83220f4b2c5cba78e10d2f150f Mon Sep 17 00:00:00 2001 From: Tudor Sandu Date: Sat, 25 Nov 2023 11:14:48 -0800 Subject: [PATCH] Add scene.delete service for dynamically created scenes (with scene.create) (#89090) * Added scene.delete service Only for scenes created with scene.create * Refactor after #95984 #96390 * Split scene validation in 2 First, check if entity_id is a scene Second, check if it's a scene created with `scene.create` * Address feedback - Move service to `homeassistant` domain - Register with `platform.async_register_entity_service` - Raise validation errors instead of just logging messages * Revert moving the service to the `homeassistant` domain * Remove unneeded validation * Use helpers and fix tests * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Fix linting --------- Co-authored-by: Martin Hjelmare --- .../components/homeassistant/scene.py | 43 ++++++++++++- homeassistant/components/scene/services.yaml | 6 ++ homeassistant/components/scene/strings.json | 12 ++++ tests/components/homeassistant/test_scene.py | 60 +++++++++++++++++++ 4 files changed, 119 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 258970378b2..3308083f22f 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -29,14 +29,17 @@ from homeassistant.core import ( State, callback, ) -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import ( config_per_platform, config_validation as cv, entity_platform, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback, EntityPlatform -from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.service import ( + async_extract_entity_ids, + async_register_admin_service, +) from homeassistant.helpers.state import async_reproduce_state from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import async_get_integration @@ -125,6 +128,7 @@ CREATE_SCENE_SCHEMA = vol.All( SERVICE_APPLY = "apply" SERVICE_CREATE = "create" +SERVICE_DELETE = "delete" _LOGGER = logging.getLogger(__name__) @@ -273,6 +277,41 @@ async def async_setup_platform( SCENE_DOMAIN, SERVICE_CREATE, create_service, CREATE_SCENE_SCHEMA ) + async def delete_service(call: ServiceCall) -> None: + """Delete a dynamically created scene.""" + entity_ids = await async_extract_entity_ids(hass, call) + + for entity_id in entity_ids: + scene = platform.entities.get(entity_id) + if scene is None: + raise ServiceValidationError( + f"{entity_id} is not a valid scene entity_id", + translation_domain=SCENE_DOMAIN, + translation_key="entity_not_scene", + translation_placeholders={ + "entity_id": entity_id, + }, + ) + assert isinstance(scene, HomeAssistantScene) + if not scene.from_service: + raise ServiceValidationError( + f"The scene {entity_id} is not created with service `scene.create`", + translation_domain=SCENE_DOMAIN, + translation_key="entity_not_dynamically_created", + translation_placeholders={ + "entity_id": entity_id, + }, + ) + + await platform.async_remove_entity(entity_id) + + hass.services.async_register( + SCENE_DOMAIN, + SERVICE_DELETE, + delete_service, + cv.make_entity_service_schema({}), + ) + def _process_scenes_config( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: dict[str, Any] diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 543cefd5b9a..a2139529ccf 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -54,3 +54,9 @@ create: selector: entity: multiple: true + +delete: + target: + entity: + - integration: homeassistant + domain: scene diff --git a/homeassistant/components/scene/strings.json b/homeassistant/components/scene/strings.json index 3bfea1b09e7..af91b2e227e 100644 --- a/homeassistant/components/scene/strings.json +++ b/homeassistant/components/scene/strings.json @@ -46,6 +46,18 @@ "description": "List of entities to be included in the snapshot. By taking a snapshot, you record the current state of those entities. If you do not want to use the current state of all your entities for this scene, you can combine the `snapshot_entities` with `entities`." } } + }, + "delete": { + "name": "Delete", + "description": "Deletes a dynamically created scene." + } + }, + "exceptions": { + "entity_not_scene": { + "message": "{entity_id} is not a valid scene entity_id." + }, + "entity_not_dynamically_created": { + "message": "The scene {entity_id} is not created with service `scene.create`." } } } diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 085ed4f0641..d754c67ad49 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -8,6 +8,7 @@ from homeassistant.components.homeassistant import scene as ha_scene from homeassistant.components.homeassistant.scene import EVENT_SCENE_RELOADED from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.setup import async_setup_component from tests.common import async_capture_events, async_mock_service @@ -164,6 +165,65 @@ async def test_create_service( assert scene.attributes.get("entity_id") == ["light.kitchen"] +async def test_delete_service( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the delete service.""" + assert await async_setup_component( + hass, + "scene", + {"scene": {"name": "hallo_2", "entities": {"light.kitchen": "on"}}}, + ) + + await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo", + "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, + }, + blocking=True, + ) + await hass.async_block_till_done() + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + "scene", + "delete", + { + "entity_id": "scene.hallo_3", + }, + blocking=True, + ) + await hass.async_block_till_done() + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + "scene", + "delete", + { + "entity_id": "scene.hallo_2", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("scene.hallo_2") is not None + + assert hass.states.get("scene.hallo") is not None + + await hass.services.async_call( + "scene", + "delete", + { + "entity_id": "scene.hallo", + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("state.hallo") is None + + async def test_snapshot_service( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: