mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 17:57:55 +00:00
Creating a scene by snapshotting entities (#28939)
* Initial commit * Add tests * service.yaml * typo * snapshooted -> snapshot * snapshot_entities instead of snapshot * Edit validator * Fix tests * Remove keys() * Improve coverage * Activate scenes * Use pytest.raise * snapshot -> snapshotted
This commit is contained in:
parent
a977f398ae
commit
c35f9ee35f
@ -55,7 +55,22 @@ def _convert_states(states):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_no_intersection(value):
|
||||||
|
"""Validate that entities and snapshot_entities do not overlap."""
|
||||||
|
if (
|
||||||
|
CONF_SNAPSHOT not in value
|
||||||
|
or CONF_ENTITIES not in value
|
||||||
|
or not any(
|
||||||
|
entity_id in value[CONF_SNAPSHOT] for entity_id in value[CONF_ENTITIES]
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return value
|
||||||
|
|
||||||
|
raise vol.Invalid("entities and snapshot_entities must not overlap")
|
||||||
|
|
||||||
|
|
||||||
CONF_SCENE_ID = "scene_id"
|
CONF_SCENE_ID = "scene_id"
|
||||||
|
CONF_SNAPSHOT = "snapshot_entities"
|
||||||
|
|
||||||
STATES_SCHEMA = vol.All(dict, _convert_states)
|
STATES_SCHEMA = vol.All(dict, _convert_states)
|
||||||
|
|
||||||
@ -75,8 +90,16 @@ PLATFORM_SCHEMA = vol.Schema(
|
|||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
CREATE_SCENE_SCHEMA = vol.Schema(
|
CREATE_SCENE_SCHEMA = vol.All(
|
||||||
{vol.Required(CONF_SCENE_ID): cv.slug, vol.Required(CONF_ENTITIES): STATES_SCHEMA}
|
cv.has_at_least_one_key(CONF_ENTITIES, CONF_SNAPSHOT),
|
||||||
|
_ensure_no_intersection,
|
||||||
|
vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_SCENE_ID): cv.slug,
|
||||||
|
vol.Optional(CONF_ENTITIES, default={}): STATES_SCHEMA,
|
||||||
|
vol.Optional(CONF_SNAPSHOT, default=[]): cv.entity_ids,
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
SERVICE_APPLY = "apply"
|
SERVICE_APPLY = "apply"
|
||||||
@ -139,7 +162,24 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
|
|
||||||
async def create_service(call):
|
async def create_service(call):
|
||||||
"""Create a scene."""
|
"""Create a scene."""
|
||||||
scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], call.data[CONF_ENTITIES])
|
snapshot = call.data[CONF_SNAPSHOT]
|
||||||
|
entities = call.data[CONF_ENTITIES]
|
||||||
|
|
||||||
|
for entity_id in snapshot:
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
if state is None:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Entity %s does not exist and therefore cannot be snapshotted",
|
||||||
|
entity_id,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
entities[entity_id] = State(entity_id, state.state, state.attributes)
|
||||||
|
|
||||||
|
if not entities:
|
||||||
|
_LOGGER.warning("Empty scenes are not allowed")
|
||||||
|
return
|
||||||
|
|
||||||
|
scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], entities)
|
||||||
entity_id = f"{SCENE_DOMAIN}.{scene_config.name}"
|
entity_id = f"{SCENE_DOMAIN}.{scene_config.name}"
|
||||||
old = platform.entities.get(entity_id)
|
old = platform.entities.get(entity_id)
|
||||||
if old is not None:
|
if old is not None:
|
||||||
|
@ -34,3 +34,8 @@ create:
|
|||||||
light.ceiling:
|
light.ceiling:
|
||||||
state: "on"
|
state: "on"
|
||||||
brightness: 200
|
brightness: 200
|
||||||
|
snapshot_entities:
|
||||||
|
description: The entities of which a snapshot is to be taken
|
||||||
|
example:
|
||||||
|
- light.ceiling
|
||||||
|
- light.kitchen
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
"""Test Home Assistant scenes."""
|
"""Test Home Assistant scenes."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import async_mock_service
|
||||||
|
|
||||||
|
|
||||||
async def test_reload_config_service(hass):
|
async def test_reload_config_service(hass):
|
||||||
"""Test the reload config service."""
|
"""Test the reload config service."""
|
||||||
@ -63,6 +68,16 @@ async def test_create_service(hass, caplog):
|
|||||||
assert hass.states.get("scene.hallo") is None
|
assert hass.states.get("scene.hallo") is None
|
||||||
assert hass.states.get("scene.hallo_2") is not None
|
assert hass.states.get("scene.hallo_2") is not None
|
||||||
|
|
||||||
|
assert await hass.services.async_call(
|
||||||
|
"scene",
|
||||||
|
"create",
|
||||||
|
{"scene_id": "hallo", "entities": {}, "snapshot_entities": []},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert "Empty scenes are not allowed" in caplog.text
|
||||||
|
assert hass.states.get("scene.hallo") is None
|
||||||
|
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
"scene",
|
"scene",
|
||||||
"create",
|
"create",
|
||||||
@ -117,3 +132,80 @@ async def test_create_service(hass, caplog):
|
|||||||
assert scene.name == "hallo_2"
|
assert scene.name == "hallo_2"
|
||||||
assert scene.state == "scening"
|
assert scene.state == "scening"
|
||||||
assert scene.attributes.get("entity_id") == ["light.kitchen"]
|
assert scene.attributes.get("entity_id") == ["light.kitchen"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_snapshot_service(hass, caplog):
|
||||||
|
"""Test the snapshot option."""
|
||||||
|
assert await async_setup_component(hass, "scene", {"scene": {}})
|
||||||
|
hass.states.async_set("light.my_light", "on", {"hs_color": (345, 75)})
|
||||||
|
assert hass.states.get("scene.hallo") is None
|
||||||
|
|
||||||
|
assert await hass.services.async_call(
|
||||||
|
"scene",
|
||||||
|
"create",
|
||||||
|
{"scene_id": "hallo", "snapshot_entities": ["light.my_light"]},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
scene = hass.states.get("scene.hallo")
|
||||||
|
assert scene is not None
|
||||||
|
assert scene.attributes.get("entity_id") == ["light.my_light"]
|
||||||
|
|
||||||
|
hass.states.async_set("light.my_light", "off", {"hs_color": (123, 45)})
|
||||||
|
turn_on_calls = async_mock_service(hass, "light", "turn_on")
|
||||||
|
assert await hass.services.async_call(
|
||||||
|
"scene", "turn_on", {"entity_id": "scene.hallo"}, blocking=True
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(turn_on_calls) == 1
|
||||||
|
assert turn_on_calls[0].data.get("entity_id") == "light.my_light"
|
||||||
|
assert turn_on_calls[0].data.get("hs_color") == (345, 75)
|
||||||
|
|
||||||
|
assert await hass.services.async_call(
|
||||||
|
"scene",
|
||||||
|
"create",
|
||||||
|
{"scene_id": "hallo_2", "snapshot_entities": ["light.not_existent"]},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get("scene.hallo_2") is None
|
||||||
|
assert (
|
||||||
|
"Entity light.not_existent does not exist and therefore cannot be snapshotted"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await hass.services.async_call(
|
||||||
|
"scene",
|
||||||
|
"create",
|
||||||
|
{
|
||||||
|
"scene_id": "hallo_3",
|
||||||
|
"entities": {"light.bed_light": {"state": "on", "brightness": 50}},
|
||||||
|
"snapshot_entities": ["light.my_light"],
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
scene = hass.states.get("scene.hallo_3")
|
||||||
|
assert scene is not None
|
||||||
|
assert "light.my_light" in scene.attributes.get("entity_id")
|
||||||
|
assert "light.bed_light" in scene.attributes.get("entity_id")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ensure_no_intersection(hass):
|
||||||
|
"""Test that entities and snapshot_entities do not overlap."""
|
||||||
|
assert await async_setup_component(hass, "scene", {"scene": {}})
|
||||||
|
|
||||||
|
with pytest.raises(vol.MultipleInvalid) as ex:
|
||||||
|
assert await hass.services.async_call(
|
||||||
|
"scene",
|
||||||
|
"create",
|
||||||
|
{
|
||||||
|
"scene_id": "hallo",
|
||||||
|
"entities": {"light.my_light": {"state": "on", "brightness": 50}},
|
||||||
|
"snapshot_entities": ["light.my_light"],
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert "entities and snapshot_entities must not overlap" in str(ex.value)
|
||||||
|
assert hass.states.get("scene.hallo") is None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user