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 <marhje52@gmail.com>

* Fix linting

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
pull/104516/head^2
Tudor Sandu 2023-11-25 11:14:48 -08:00 committed by GitHub
parent 48f8cec84b
commit 837f34c40c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 119 additions and 2 deletions

View File

@ -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]

View File

@ -54,3 +54,9 @@ create:
selector:
entity:
multiple: true
delete:
target:
entity:
- integration: homeassistant
domain: scene

View File

@ -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`."
}
}
}

View File

@ -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: