diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index e19439133ee..85c87b5baa7 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -10,7 +10,13 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant import loader -from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH, __version__ +from homeassistant.const import ( + CONF_DEFAULT, + CONF_DOMAIN, + CONF_NAME, + CONF_PATH, + __version__, +) from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import placeholder @@ -82,6 +88,11 @@ class Blueprint: """Return blueprint name.""" return self.data[CONF_BLUEPRINT][CONF_NAME] + @property + def inputs(self) -> dict: + """Return blueprint inputs.""" + return self.data[CONF_BLUEPRINT][CONF_INPUT] + @property def metadata(self) -> dict: """Return blueprint metadata.""" @@ -129,9 +140,23 @@ class BlueprintInputs: """Return the inputs.""" return self.config_with_inputs[CONF_USE_BLUEPRINT][CONF_INPUT] + @property + def inputs_with_default(self): + """Return the inputs and fallback to defaults.""" + no_input = self.blueprint.placeholders - set(self.inputs) + + inputs_with_default = dict(self.inputs) + + for inp in no_input: + blueprint_input = self.blueprint.inputs[inp] + if isinstance(blueprint_input, dict) and CONF_DEFAULT in blueprint_input: + inputs_with_default[inp] = blueprint_input[CONF_DEFAULT] + + return inputs_with_default + def validate(self) -> None: """Validate the inputs.""" - missing = self.blueprint.placeholders - set(self.inputs) + missing = self.blueprint.placeholders - set(self.inputs_with_default) if missing: raise MissingPlaceholder( @@ -144,7 +169,9 @@ class BlueprintInputs: @callback def async_substitute(self) -> dict: """Get the blueprint value with the inputs substituted.""" - processed = placeholder.substitute(self.blueprint.data, self.inputs) + processed = placeholder.substitute( + self.blueprint.data, self.inputs_with_default + ) combined = {**self.config_with_inputs, **processed} # From config_with_inputs combined.pop(CONF_USE_BLUEPRINT) diff --git a/homeassistant/components/blueprint/schemas.py b/homeassistant/components/blueprint/schemas.py index 900497c3f71..07d8e8b0128 100644 --- a/homeassistant/components/blueprint/schemas.py +++ b/homeassistant/components/blueprint/schemas.py @@ -3,7 +3,13 @@ from typing import Any import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH, CONF_SELECTOR +from homeassistant.const import ( + CONF_DEFAULT, + CONF_DOMAIN, + CONF_NAME, + CONF_PATH, + CONF_SELECTOR, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, selector @@ -54,6 +60,7 @@ BLUEPRINT_INPUT_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): str, vol.Optional(CONF_DESCRIPTION): str, + vol.Optional(CONF_DEFAULT): cv.match_all, vol.Optional(CONF_SELECTOR): selector.validate_selector, } ) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 48fbf617fa1..5c2d5f965ff 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -27,6 +27,26 @@ def blueprint_1(): ) +@pytest.fixture +def blueprint_2(): + """Blueprint fixture with default placeholder.""" + return models.Blueprint( + { + "blueprint": { + "name": "Hello", + "domain": "automation", + "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", + "input": { + "test-placeholder": {"name": "Name", "description": "Description"}, + "test-placeholder-default": {"default": "test"}, + }, + }, + "example": Placeholder("test-placeholder"), + "example-default": Placeholder("test-placeholder-default"), + } + ) + + @pytest.fixture def domain_bps(hass): """Domain blueprints fixture.""" @@ -134,6 +154,44 @@ def test_blueprint_inputs_validation(blueprint_1): inputs.validate() +def test_blueprint_inputs_default(blueprint_2): + """Test blueprint inputs.""" + inputs = models.BlueprintInputs( + blueprint_2, + {"use_blueprint": {"path": "bla", "input": {"test-placeholder": 1}}}, + ) + inputs.validate() + assert inputs.inputs == {"test-placeholder": 1} + assert inputs.inputs_with_default == { + "test-placeholder": 1, + "test-placeholder-default": "test", + } + assert inputs.async_substitute() == {"example": 1, "example-default": "test"} + + +def test_blueprint_inputs_override_default(blueprint_2): + """Test blueprint inputs.""" + inputs = models.BlueprintInputs( + blueprint_2, + { + "use_blueprint": { + "path": "bla", + "input": {"test-placeholder": 1, "test-placeholder-default": "custom"}, + } + }, + ) + inputs.validate() + assert inputs.inputs == { + "test-placeholder": 1, + "test-placeholder-default": "custom", + } + assert inputs.inputs_with_default == { + "test-placeholder": 1, + "test-placeholder-default": "custom", + } + assert inputs.async_substitute() == {"example": 1, "example-default": "custom"} + + async def test_domain_blueprints_get_blueprint_errors(hass, domain_bps): """Test domain blueprints.""" assert hass.data["blueprint"]["automation"] is domain_bps