mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 18:27:09 +00:00
Support in blueprint schema for input sections (#110513)
* initial commit for sections * updates * add description * fix test * rename collapsed key * New schema * update snapshots * Testing for sections * Validate no duplicate input keys across sections * rename all_inputs * Update homeassistant/components/blueprint/models.py --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
d83ab7bb04
commit
6e5dcd8b8d
@ -9,5 +9,6 @@ CONF_SOURCE_URL = "source_url"
|
||||
CONF_HOMEASSISTANT = "homeassistant"
|
||||
CONF_MIN_VERSION = "min_version"
|
||||
CONF_AUTHOR = "author"
|
||||
CONF_COLLAPSED = "collapsed"
|
||||
|
||||
DOMAIN = "blueprint"
|
||||
|
@ -78,7 +78,7 @@ class Blueprint:
|
||||
|
||||
self.domain = data_domain
|
||||
|
||||
missing = yaml.extract_inputs(data) - set(data[CONF_BLUEPRINT][CONF_INPUT])
|
||||
missing = yaml.extract_inputs(data) - set(self.inputs)
|
||||
|
||||
if missing:
|
||||
raise InvalidBlueprint(
|
||||
@ -95,8 +95,15 @@ class Blueprint:
|
||||
|
||||
@property
|
||||
def inputs(self) -> dict[str, Any]:
|
||||
"""Return blueprint inputs."""
|
||||
return self.data[CONF_BLUEPRINT][CONF_INPUT] # type: ignore[no-any-return]
|
||||
"""Return flattened blueprint inputs."""
|
||||
inputs = {}
|
||||
for key, value in self.data[CONF_BLUEPRINT][CONF_INPUT].items():
|
||||
if value and CONF_INPUT in value:
|
||||
for key, value in value[CONF_INPUT].items():
|
||||
inputs[key] = value
|
||||
else:
|
||||
inputs[key] = value
|
||||
return inputs
|
||||
|
||||
@property
|
||||
def metadata(self) -> dict[str, Any]:
|
||||
|
@ -8,6 +8,7 @@ from homeassistant.const import (
|
||||
CONF_DEFAULT,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_DOMAIN,
|
||||
CONF_ICON,
|
||||
CONF_NAME,
|
||||
CONF_PATH,
|
||||
CONF_SELECTOR,
|
||||
@ -18,6 +19,7 @@ from homeassistant.helpers import config_validation as cv, selector
|
||||
from .const import (
|
||||
CONF_AUTHOR,
|
||||
CONF_BLUEPRINT,
|
||||
CONF_COLLAPSED,
|
||||
CONF_HOMEASSISTANT,
|
||||
CONF_INPUT,
|
||||
CONF_MIN_VERSION,
|
||||
@ -46,6 +48,23 @@ def version_validator(value: Any) -> str:
|
||||
return value
|
||||
|
||||
|
||||
def unique_input_validator(inputs: Any) -> Any:
|
||||
"""Validate the inputs don't have duplicate keys under different sections."""
|
||||
all_inputs = set()
|
||||
for key, value in inputs.items():
|
||||
if value and CONF_INPUT in value:
|
||||
for key in value[CONF_INPUT]:
|
||||
if key in all_inputs:
|
||||
raise vol.Invalid(f"Duplicate use of input key {key} in blueprint.")
|
||||
all_inputs.add(key)
|
||||
else:
|
||||
if key in all_inputs:
|
||||
raise vol.Invalid(f"Duplicate use of input key {key} in blueprint.")
|
||||
all_inputs.add(key)
|
||||
|
||||
return inputs
|
||||
|
||||
|
||||
@callback
|
||||
def is_blueprint_config(config: Any) -> bool:
|
||||
"""Return if it is a blueprint config."""
|
||||
@ -67,6 +86,21 @@ BLUEPRINT_INPUT_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
BLUEPRINT_INPUT_SECTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME): str,
|
||||
vol.Optional(CONF_ICON): str,
|
||||
vol.Optional(CONF_DESCRIPTION): str,
|
||||
vol.Optional(CONF_COLLAPSED): bool,
|
||||
vol.Required(CONF_INPUT, default=dict): {
|
||||
str: vol.Any(
|
||||
None,
|
||||
BLUEPRINT_INPUT_SCHEMA,
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
BLUEPRINT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BLUEPRINT): vol.Schema(
|
||||
@ -79,12 +113,16 @@ BLUEPRINT_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_HOMEASSISTANT): {
|
||||
vol.Optional(CONF_MIN_VERSION): version_validator
|
||||
},
|
||||
vol.Optional(CONF_INPUT, default=dict): {
|
||||
str: vol.Any(
|
||||
None,
|
||||
BLUEPRINT_INPUT_SCHEMA,
|
||||
)
|
||||
},
|
||||
vol.Optional(CONF_INPUT, default=dict): vol.All(
|
||||
{
|
||||
str: vol.Any(
|
||||
None,
|
||||
BLUEPRINT_INPUT_SCHEMA,
|
||||
BLUEPRINT_INPUT_SECTION_SCHEMA,
|
||||
)
|
||||
},
|
||||
unique_input_validator,
|
||||
),
|
||||
}
|
||||
),
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
# serializer version: 1
|
||||
# name: test_extract_blueprint_from_community_topic
|
||||
NodeDictClass({
|
||||
dict({
|
||||
'brightness': NodeDictClass({
|
||||
'default': 50,
|
||||
'description': 'Brightness of the light(s) when turning on',
|
||||
@ -97,7 +97,7 @@
|
||||
})
|
||||
# ---
|
||||
# name: test_fetch_blueprint_from_community_url
|
||||
NodeDictClass({
|
||||
dict({
|
||||
'brightness': NodeDictClass({
|
||||
'default': 50,
|
||||
'description': 'Brightness of the light(s) when turning on',
|
||||
@ -194,7 +194,7 @@
|
||||
})
|
||||
# ---
|
||||
# name: test_fetch_blueprint_from_github_gist_url
|
||||
NodeDictClass({
|
||||
dict({
|
||||
'light_entity': NodeDictClass({
|
||||
'name': 'Light',
|
||||
'selector': dict({
|
||||
|
@ -26,24 +26,38 @@ def blueprint_1():
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def blueprint_2():
|
||||
@pytest.fixture(params=[False, True])
|
||||
def blueprint_2(request):
|
||||
"""Blueprint fixture with default inputs."""
|
||||
return models.Blueprint(
|
||||
{
|
||||
"blueprint": {
|
||||
"name": "Hello",
|
||||
"domain": "automation",
|
||||
"source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
|
||||
blueprint = {
|
||||
"blueprint": {
|
||||
"name": "Hello",
|
||||
"domain": "automation",
|
||||
"source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
|
||||
"input": {
|
||||
"test-input": {"name": "Name", "description": "Description"},
|
||||
"test-input-default": {"default": "test"},
|
||||
},
|
||||
},
|
||||
"example": Input("test-input"),
|
||||
"example-default": Input("test-input-default"),
|
||||
}
|
||||
if request.param:
|
||||
# Replace the inputs with inputs in sections. Test should otherwise behave the same.
|
||||
blueprint["blueprint"]["input"] = {
|
||||
"section-1": {
|
||||
"name": "Section 1",
|
||||
"input": {
|
||||
"test-input": {"name": "Name", "description": "Description"},
|
||||
"test-input-default": {"default": "test"},
|
||||
},
|
||||
},
|
||||
"example": Input("test-input"),
|
||||
"example-default": Input("test-input-default"),
|
||||
"section-2": {
|
||||
"input": {
|
||||
"test-input-default": {"default": "test"},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
return models.Blueprint(blueprint)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -52,6 +52,24 @@ _LOGGER = logging.getLogger(__name__)
|
||||
},
|
||||
}
|
||||
},
|
||||
# With input sections
|
||||
{
|
||||
"blueprint": {
|
||||
"name": "Test Name",
|
||||
"domain": "automation",
|
||||
"input": {
|
||||
"section_a": {
|
||||
"input": {"some_placeholder": None},
|
||||
},
|
||||
"section_b": {
|
||||
"name": "Section",
|
||||
"description": "A section with no inputs",
|
||||
"input": {},
|
||||
},
|
||||
"some_placeholder_2": None,
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_blueprint_schema(blueprint) -> None:
|
||||
@ -94,6 +112,34 @@ def test_blueprint_schema(blueprint) -> None:
|
||||
},
|
||||
}
|
||||
},
|
||||
# Duplicate inputs in sections (1 of 2)
|
||||
{
|
||||
"blueprint": {
|
||||
"name": "Test Name",
|
||||
"domain": "automation",
|
||||
"input": {
|
||||
"section_a": {
|
||||
"input": {"some_placeholder": None},
|
||||
},
|
||||
"section_b": {
|
||||
"input": {"some_placeholder": None},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
# Duplicate inputs in sections (2 of 2)
|
||||
{
|
||||
"blueprint": {
|
||||
"name": "Test Name",
|
||||
"domain": "automation",
|
||||
"input": {
|
||||
"section_a": {
|
||||
"input": {"some_placeholder": None},
|
||||
},
|
||||
"some_placeholder": None,
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_blueprint_schema_invalid(blueprint) -> None:
|
||||
|
Loading…
x
Reference in New Issue
Block a user