mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Add config endpoint for scene (#28429)
* Add config endpoint for scene * Add scenes to default config * Fix test and add context to websocket service call * Update scene.py * Add tests
This commit is contained in:
parent
bbe0cf3a0c
commit
e419689229
@ -25,6 +25,7 @@ SECTIONS = (
|
||||
"entity_registry",
|
||||
"group",
|
||||
"script",
|
||||
"scene",
|
||||
)
|
||||
ON_DEMAND = ("zwave",)
|
||||
|
||||
|
65
homeassistant/components/config/scene.py
Normal file
65
homeassistant/components/config/scene.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Provide configuration end points for Scenes."""
|
||||
from collections import OrderedDict
|
||||
import uuid
|
||||
|
||||
from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_ID, SERVICE_RELOAD
|
||||
from homeassistant.config import SCENE_CONFIG_PATH
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import EditIdBasedConfigView
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Set up the Scene config API."""
|
||||
|
||||
async def hook(hass):
|
||||
"""post_write_hook for Config View that reloads scenes."""
|
||||
await hass.services.async_call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
hass.http.register_view(
|
||||
EditSceneConfigView(
|
||||
DOMAIN,
|
||||
"config",
|
||||
SCENE_CONFIG_PATH,
|
||||
cv.string,
|
||||
PLATFORM_SCHEMA,
|
||||
post_write_hook=hook,
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class EditSceneConfigView(EditIdBasedConfigView):
|
||||
"""Edit scene config."""
|
||||
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
index = None
|
||||
for index, cur_value in enumerate(data):
|
||||
# When people copy paste their scenes to the config file,
|
||||
# they sometimes forget to add IDs. Fix it here.
|
||||
if CONF_ID not in cur_value:
|
||||
cur_value[CONF_ID] = uuid.uuid4().hex
|
||||
|
||||
elif cur_value[CONF_ID] == config_key:
|
||||
break
|
||||
else:
|
||||
cur_value = dict()
|
||||
cur_value[CONF_ID] = config_key
|
||||
index = len(data)
|
||||
data.append(cur_value)
|
||||
|
||||
# Iterate through some keys that we want to have ordered in the output
|
||||
updated_value = OrderedDict()
|
||||
for key in ("id", "name", "entities"):
|
||||
if key in cur_value:
|
||||
updated_value[key] = cur_value[key]
|
||||
if key in new_value:
|
||||
updated_value[key] = new_value[key]
|
||||
|
||||
# We cover all current fields above, but just in case we start
|
||||
# supporting more fields in the future.
|
||||
updated_value.update(cur_value)
|
||||
updated_value.update(new_value)
|
||||
data[index] = updated_value
|
@ -8,6 +8,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_STATE,
|
||||
CONF_ENTITIES,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
STATE_OFF,
|
||||
@ -160,7 +161,11 @@ def _process_scenes_config(hass, async_add_entities, config):
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
HomeAssistantScene(hass, SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES]))
|
||||
HomeAssistantScene(
|
||||
hass,
|
||||
SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES]),
|
||||
scene.get(CONF_ID),
|
||||
)
|
||||
for scene in scene_config
|
||||
)
|
||||
|
||||
@ -168,8 +173,9 @@ def _process_scenes_config(hass, async_add_entities, config):
|
||||
class HomeAssistantScene(Scene):
|
||||
"""A scene is a group of entities and the states we want them to be."""
|
||||
|
||||
def __init__(self, hass, scene_config):
|
||||
def __init__(self, hass, scene_config, scene_id=None):
|
||||
"""Initialize the scene."""
|
||||
self._id = scene_id
|
||||
self.hass = hass
|
||||
self.scene_config = scene_config
|
||||
|
||||
@ -181,7 +187,10 @@ class HomeAssistantScene(Scene):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the scene state attributes."""
|
||||
return {ATTR_ENTITY_ID: list(self.scene_config.states)}
|
||||
attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)}
|
||||
if self._id is not None:
|
||||
attributes[CONF_ID] = self._id
|
||||
return attributes
|
||||
|
||||
async def async_activate(self):
|
||||
"""Activate scene. Try to get entities into requested state."""
|
||||
|
@ -132,7 +132,9 @@ async def handle_call_service(hass, connection, msg):
|
||||
blocking,
|
||||
connection.context(msg),
|
||||
)
|
||||
connection.send_message(messages.result_message(msg["id"]))
|
||||
connection.send_message(
|
||||
messages.result_message(msg["id"], {"context": connection.context(msg)})
|
||||
)
|
||||
except ServiceNotFound as err:
|
||||
if err.domain == msg["domain"] and err.service == msg["service"]:
|
||||
connection.send_message(
|
||||
|
@ -69,6 +69,7 @@ DATA_CUSTOMIZE = "hass_customize"
|
||||
GROUP_CONFIG_PATH = "groups.yaml"
|
||||
AUTOMATION_CONFIG_PATH = "automations.yaml"
|
||||
SCRIPT_CONFIG_PATH = "scripts.yaml"
|
||||
SCENE_CONFIG_PATH = "scenes.yaml"
|
||||
|
||||
DEFAULT_CONFIG = f"""
|
||||
# Configure a default setup of Home Assistant (frontend, api, etc)
|
||||
@ -85,6 +86,7 @@ tts:
|
||||
group: !include {GROUP_CONFIG_PATH}
|
||||
automation: !include {AUTOMATION_CONFIG_PATH}
|
||||
script: !include {SCRIPT_CONFIG_PATH}
|
||||
scene: !include {SCENE_CONFIG_PATH}
|
||||
"""
|
||||
DEFAULT_SECRETS = """
|
||||
# Use this file to store secrets like usernames and passwords.
|
||||
@ -261,6 +263,7 @@ def _write_default_config(config_dir: str) -> Optional[str]:
|
||||
group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH)
|
||||
automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH)
|
||||
script_yaml_path = os.path.join(config_dir, SCRIPT_CONFIG_PATH)
|
||||
scene_yaml_path = os.path.join(config_dir, SCENE_CONFIG_PATH)
|
||||
|
||||
# Writing files with YAML does not create the most human readable results
|
||||
# So we're hard coding a YAML template.
|
||||
@ -283,6 +286,9 @@ def _write_default_config(config_dir: str) -> Optional[str]:
|
||||
with open(script_yaml_path, "wt"):
|
||||
pass
|
||||
|
||||
with open(scene_yaml_path, "wt"):
|
||||
pass
|
||||
|
||||
return config_path
|
||||
|
||||
except OSError:
|
||||
|
144
tests/components/config/test_scene.py
Normal file
144
tests/components/config/test_scene.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""Test Automation config panel."""
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
from homeassistant.components import config
|
||||
from homeassistant.util.yaml import dump
|
||||
|
||||
|
||||
async def test_update_scene(hass, hass_client):
|
||||
"""Test updating a scene."""
|
||||
with patch.object(config, "SECTIONS", ["scene"]):
|
||||
await async_setup_component(hass, "config", {})
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
orig_data = [{"id": "light_on"}, {"id": "light_off"}]
|
||||
|
||||
def mock_read(path):
|
||||
"""Mock reading data."""
|
||||
return orig_data
|
||||
|
||||
written = []
|
||||
|
||||
def mock_write(path, data):
|
||||
"""Mock writing data."""
|
||||
data = dump(data)
|
||||
written.append(data)
|
||||
|
||||
with patch("homeassistant.components.config._read", mock_read), patch(
|
||||
"homeassistant.components.config._write", mock_write
|
||||
):
|
||||
resp = await client.post(
|
||||
"/api/config/scene/config/light_off",
|
||||
data=json.dumps(
|
||||
{
|
||||
"id": "light_off",
|
||||
"name": "Lights off",
|
||||
"entities": {"light.bedroom": {"state": "off"}},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert result == {"result": "ok"}
|
||||
|
||||
assert len(written) == 1
|
||||
written_yaml = written[0]
|
||||
assert (
|
||||
written_yaml
|
||||
== """- id: light_on
|
||||
- id: light_off
|
||||
name: Lights off
|
||||
entities:
|
||||
light.bedroom:
|
||||
state: 'off'
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def test_bad_formatted_scene(hass, hass_client):
|
||||
"""Test that we handle scene without ID."""
|
||||
with patch.object(config, "SECTIONS", ["scene"]):
|
||||
await async_setup_component(hass, "config", {})
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
orig_data = [
|
||||
{
|
||||
# No ID
|
||||
"entities": {"light.bedroom": "on"}
|
||||
},
|
||||
{"id": "light_off"},
|
||||
]
|
||||
|
||||
def mock_read(path):
|
||||
"""Mock reading data."""
|
||||
return orig_data
|
||||
|
||||
written = []
|
||||
|
||||
def mock_write(path, data):
|
||||
"""Mock writing data."""
|
||||
written.append(data)
|
||||
|
||||
with patch("homeassistant.components.config._read", mock_read), patch(
|
||||
"homeassistant.components.config._write", mock_write
|
||||
):
|
||||
resp = await client.post(
|
||||
"/api/config/scene/config/light_off",
|
||||
data=json.dumps(
|
||||
{
|
||||
"id": "light_off",
|
||||
"name": "Lights off",
|
||||
"entities": {"light.bedroom": {"state": "off"}},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert result == {"result": "ok"}
|
||||
|
||||
# Verify ID added to orig_data
|
||||
assert "id" in orig_data[0]
|
||||
|
||||
assert orig_data[1] == {
|
||||
"id": "light_off",
|
||||
"name": "Lights off",
|
||||
"entities": {"light.bedroom": {"state": "off"}},
|
||||
}
|
||||
|
||||
|
||||
async def test_delete_scene(hass, hass_client):
|
||||
"""Test deleting a scene."""
|
||||
with patch.object(config, "SECTIONS", ["scene"]):
|
||||
await async_setup_component(hass, "config", {})
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
orig_data = [{"id": "light_on"}, {"id": "light_off"}]
|
||||
|
||||
def mock_read(path):
|
||||
"""Mock reading data."""
|
||||
return orig_data
|
||||
|
||||
written = []
|
||||
|
||||
def mock_write(path, data):
|
||||
"""Mock writing data."""
|
||||
written.append(data)
|
||||
|
||||
with patch("homeassistant.components.config._read", mock_read), patch(
|
||||
"homeassistant.components.config._write", mock_write
|
||||
):
|
||||
resp = await client.delete("/api/config/scene/config/light_on")
|
||||
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert result == {"result": "ok"}
|
||||
|
||||
assert len(written) == 1
|
||||
assert written[0][0]["id"] == "light_off"
|
@ -44,6 +44,7 @@ VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE)
|
||||
GROUP_PATH = os.path.join(CONFIG_DIR, config_util.GROUP_CONFIG_PATH)
|
||||
AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, config_util.AUTOMATION_CONFIG_PATH)
|
||||
SCRIPTS_PATH = os.path.join(CONFIG_DIR, config_util.SCRIPT_CONFIG_PATH)
|
||||
SCENES_PATH = os.path.join(CONFIG_DIR, config_util.SCENE_CONFIG_PATH)
|
||||
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
|
||||
|
||||
|
||||
@ -75,6 +76,9 @@ def teardown():
|
||||
if os.path.isfile(SCRIPTS_PATH):
|
||||
os.remove(SCRIPTS_PATH)
|
||||
|
||||
if os.path.isfile(SCENES_PATH):
|
||||
os.remove(SCENES_PATH)
|
||||
|
||||
|
||||
async def test_create_default_config(hass):
|
||||
"""Test creation of default config."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user