mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +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_HOMEASSISTANT = "homeassistant"
|
||||||
CONF_MIN_VERSION = "min_version"
|
CONF_MIN_VERSION = "min_version"
|
||||||
CONF_AUTHOR = "author"
|
CONF_AUTHOR = "author"
|
||||||
|
CONF_COLLAPSED = "collapsed"
|
||||||
|
|
||||||
DOMAIN = "blueprint"
|
DOMAIN = "blueprint"
|
||||||
|
@ -78,7 +78,7 @@ class Blueprint:
|
|||||||
|
|
||||||
self.domain = data_domain
|
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:
|
if missing:
|
||||||
raise InvalidBlueprint(
|
raise InvalidBlueprint(
|
||||||
@ -95,8 +95,15 @@ class Blueprint:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def inputs(self) -> dict[str, Any]:
|
def inputs(self) -> dict[str, Any]:
|
||||||
"""Return blueprint inputs."""
|
"""Return flattened blueprint inputs."""
|
||||||
return self.data[CONF_BLUEPRINT][CONF_INPUT] # type: ignore[no-any-return]
|
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
|
@property
|
||||||
def metadata(self) -> dict[str, Any]:
|
def metadata(self) -> dict[str, Any]:
|
||||||
|
@ -8,6 +8,7 @@ from homeassistant.const import (
|
|||||||
CONF_DEFAULT,
|
CONF_DEFAULT,
|
||||||
CONF_DESCRIPTION,
|
CONF_DESCRIPTION,
|
||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
|
CONF_ICON,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PATH,
|
CONF_PATH,
|
||||||
CONF_SELECTOR,
|
CONF_SELECTOR,
|
||||||
@ -18,6 +19,7 @@ from homeassistant.helpers import config_validation as cv, selector
|
|||||||
from .const import (
|
from .const import (
|
||||||
CONF_AUTHOR,
|
CONF_AUTHOR,
|
||||||
CONF_BLUEPRINT,
|
CONF_BLUEPRINT,
|
||||||
|
CONF_COLLAPSED,
|
||||||
CONF_HOMEASSISTANT,
|
CONF_HOMEASSISTANT,
|
||||||
CONF_INPUT,
|
CONF_INPUT,
|
||||||
CONF_MIN_VERSION,
|
CONF_MIN_VERSION,
|
||||||
@ -46,6 +48,23 @@ def version_validator(value: Any) -> str:
|
|||||||
return value
|
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
|
@callback
|
||||||
def is_blueprint_config(config: Any) -> bool:
|
def is_blueprint_config(config: Any) -> bool:
|
||||||
"""Return if it is a blueprint config."""
|
"""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(
|
BLUEPRINT_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_BLUEPRINT): vol.Schema(
|
vol.Required(CONF_BLUEPRINT): vol.Schema(
|
||||||
@ -79,12 +113,16 @@ BLUEPRINT_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_HOMEASSISTANT): {
|
vol.Optional(CONF_HOMEASSISTANT): {
|
||||||
vol.Optional(CONF_MIN_VERSION): version_validator
|
vol.Optional(CONF_MIN_VERSION): version_validator
|
||||||
},
|
},
|
||||||
vol.Optional(CONF_INPUT, default=dict): {
|
vol.Optional(CONF_INPUT, default=dict): vol.All(
|
||||||
|
{
|
||||||
str: vol.Any(
|
str: vol.Any(
|
||||||
None,
|
None,
|
||||||
BLUEPRINT_INPUT_SCHEMA,
|
BLUEPRINT_INPUT_SCHEMA,
|
||||||
|
BLUEPRINT_INPUT_SECTION_SCHEMA,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
unique_input_validator,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
# name: test_extract_blueprint_from_community_topic
|
# name: test_extract_blueprint_from_community_topic
|
||||||
NodeDictClass({
|
dict({
|
||||||
'brightness': NodeDictClass({
|
'brightness': NodeDictClass({
|
||||||
'default': 50,
|
'default': 50,
|
||||||
'description': 'Brightness of the light(s) when turning on',
|
'description': 'Brightness of the light(s) when turning on',
|
||||||
@ -97,7 +97,7 @@
|
|||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_fetch_blueprint_from_community_url
|
# name: test_fetch_blueprint_from_community_url
|
||||||
NodeDictClass({
|
dict({
|
||||||
'brightness': NodeDictClass({
|
'brightness': NodeDictClass({
|
||||||
'default': 50,
|
'default': 50,
|
||||||
'description': 'Brightness of the light(s) when turning on',
|
'description': 'Brightness of the light(s) when turning on',
|
||||||
@ -194,7 +194,7 @@
|
|||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_fetch_blueprint_from_github_gist_url
|
# name: test_fetch_blueprint_from_github_gist_url
|
||||||
NodeDictClass({
|
dict({
|
||||||
'light_entity': NodeDictClass({
|
'light_entity': NodeDictClass({
|
||||||
'name': 'Light',
|
'name': 'Light',
|
||||||
'selector': dict({
|
'selector': dict({
|
||||||
|
@ -26,11 +26,10 @@ def blueprint_1():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture(params=[False, True])
|
||||||
def blueprint_2():
|
def blueprint_2(request):
|
||||||
"""Blueprint fixture with default inputs."""
|
"""Blueprint fixture with default inputs."""
|
||||||
return models.Blueprint(
|
blueprint = {
|
||||||
{
|
|
||||||
"blueprint": {
|
"blueprint": {
|
||||||
"name": "Hello",
|
"name": "Hello",
|
||||||
"domain": "automation",
|
"domain": "automation",
|
||||||
@ -43,7 +42,22 @@ def blueprint_2():
|
|||||||
"example": Input("test-input"),
|
"example": Input("test-input"),
|
||||||
"example-default": Input("test-input-default"),
|
"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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"section-2": {
|
||||||
|
"input": {
|
||||||
|
"test-input-default": {"default": "test"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return models.Blueprint(blueprint)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@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:
|
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:
|
def test_blueprint_schema_invalid(blueprint) -> None:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user