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:
karwosts 2024-05-29 04:13:01 -07:00 committed by GitHub
parent d83ab7bb04
commit 6e5dcd8b8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 130 additions and 24 deletions

View File

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

View File

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

View File

@ -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,
),
}
),
},

View File

@ -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({

View File

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

View File

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