1 """Blueprint models."""
3 from __future__
import annotations
6 from collections.abc
import Awaitable, Callable
10 from typing
import Any
12 from awesomeversion
import AwesomeVersion
13 import voluptuous
as vol
14 from voluptuous.humanize
import humanize_error
16 from homeassistant
import loader
44 InvalidBlueprintInputs,
47 from .schemas
import BLUEPRINT_INSTANCE_FIELDS
51 """Blueprint of a configuration structure."""
57 path: str |
None =
None,
58 expected_domain: str |
None =
None,
59 schema: Callable[[Any], Any],
61 """Initialize a blueprint."""
63 data = self.
datadata = schema(data)
64 except vol.Invalid
as err:
68 data_domain = data[CONF_BLUEPRINT][CONF_DOMAIN]
69 if expected_domain
is not None and data_domain != expected_domain:
72 path
or self.
namename,
75 f
"Found incorrect blueprint type {data_domain}, expected"
82 missing = yaml.extract_inputs(data) - set(self.
inputsinputs)
87 path
or self.
namename,
89 f
"Missing input definition for {', '.join(missing)}",
94 """Return blueprint name."""
95 return self.
datadata[CONF_BLUEPRINT][CONF_NAME]
99 """Return flattened blueprint inputs."""
101 for key, value
in self.
datadata[CONF_BLUEPRINT][CONF_INPUT].items():
102 if value
and CONF_INPUT
in value:
103 inputs.update(
dict(value[CONF_INPUT]))
110 """Return blueprint metadata."""
111 return self.
datadata[CONF_BLUEPRINT]
114 """Update metadata."""
115 if source_url
is not None:
116 self.
datadata[CONF_BLUEPRINT][CONF_SOURCE_URL] = source_url
119 """Dump blueprint as YAML."""
120 return yaml.dump(self.
datadata)
124 """Test if the Home Assistant installation supports this blueprint.
126 Return list of errors if not valid.
130 min_version = metadata.get(CONF_HOMEASSISTANT, {}).
get(CONF_MIN_VERSION)
132 if min_version
is not None and AwesomeVersion(__version__) < AwesomeVersion(
135 errors.append(f
"Requires at least Home Assistant {min_version}")
137 return errors
or None
141 """Inputs for a blueprint."""
144 self, blueprint: Blueprint, config_with_inputs: dict[str, Any]
146 """Instantiate a blueprint inputs object."""
152 """Return the inputs."""
157 """Return the inputs and fallback to defaults."""
158 no_input = set(self.
blueprintblueprint.inputs) - set(self.
inputsinputs)
163 blueprint_input = self.
blueprintblueprint.inputs[inp]
164 if isinstance(blueprint_input, dict)
and CONF_DEFAULT
in blueprint_input:
165 inputs_with_default[inp] = blueprint_input[CONF_DEFAULT]
167 return inputs_with_default
170 """Validate the inputs."""
181 """Get the blueprint value with the inputs substituted."""
185 combined.pop(CONF_USE_BLUEPRINT)
187 combined.pop(CONF_BLUEPRINT)
192 """Blueprints for a specific domain."""
198 logger: logging.Logger,
199 blueprint_in_use: Callable[[HomeAssistant, str], bool],
200 reload_blueprint_consumers: Callable[[HomeAssistant, str], Awaitable[
None]],
201 blueprint_schema: Callable[[Any], Any],
203 """Initialize a domain blueprints instance."""
209 self.
_blueprints_blueprints: dict[str, Blueprint |
None] = {}
213 hass.data.setdefault(DOMAIN, {})[domain] = self
217 """Return the blueprint folder."""
218 return pathlib.Path(self.
hasshass.config.path(BLUEPRINT_FOLDER, self.
domaindomain))
221 """Reset the blueprint cache."""
226 """Load a blueprint."""
228 blueprint_data = yaml.load_yaml_dict(self.
blueprint_folderblueprint_folder / blueprint_path)
229 except FileNotFoundError
as err:
233 FileNotFoundError(f
"Unable to find {blueprint_path}"),
235 except HomeAssistantError
as err:
240 expected_domain=self.
domaindomain,
246 """Load all the blueprints."""
247 blueprint_folder = pathlib.Path(
248 self.
hasshass.config.path(BLUEPRINT_FOLDER, self.
domaindomain)
250 results: dict[str, Blueprint | BlueprintException |
None] = {}
252 for path
in blueprint_folder.glob(
"**/*.yaml"):
253 blueprint_path =
str(path.relative_to(blueprint_folder))
259 except BlueprintException
as err:
261 results[blueprint_path] = err
264 results[blueprint_path] = self.
_blueprints_blueprints[blueprint_path]
270 ) -> dict[str, Blueprint | BlueprintException | None]:
271 """Get all the blueprints."""
276 """Get a blueprint."""
278 def load_from_cache() -> Blueprint:
279 """Load blueprint from cache."""
280 if (blueprint := self.
_blueprints_blueprints[blueprint_path])
is None:
284 FileNotFoundError(f
"Unable to find {blueprint_path}"),
289 return load_from_cache()
294 return load_from_cache()
297 blueprint = await self.
hasshass.async_add_executor_job(
304 self.
_blueprints_blueprints[blueprint_path] = blueprint
308 self, config_with_blueprint: dict
309 ) -> BlueprintInputs:
310 """Process a blueprint config."""
313 except vol.Invalid
as err:
318 bp_conf = config_with_blueprint[CONF_USE_BLUEPRINT]
325 """Remove a blueprint file."""
329 await self.
hasshass.async_add_executor_job(path.unlink)
333 self, blueprint: Blueprint, blueprint_path: str, allow_override: bool
335 """Create blueprint file.
337 Returns true if the action overrides an existing blueprint.
341 self.
hasshass.config.path(BLUEPRINT_FOLDER, self.
domaindomain, blueprint_path)
343 exists = path.exists()
345 if not allow_override
and exists:
348 path.parent.mkdir(parents=
True, exist_ok=
True)
349 path.write_text(blueprint.yaml(), encoding=
"utf-8")
353 self, blueprint: Blueprint, blueprint_path: str, allow_override: bool =
False
355 """Add a blueprint."""
356 overrides_existing = await self.
hasshass.async_add_executor_job(
357 self.
_create_file_create_file, blueprint, blueprint_path, allow_override
360 self.
_blueprints_blueprints[blueprint_path] = blueprint
362 if overrides_existing:
365 return overrides_existing
368 """Create folder if it doesn't exist and populate with examples."""
373 integration = await loader.async_get_integration(self.
hasshass, self.
domaindomain)
375 def populate() -> None:
380 integration.file_path / BLUEPRINT_FOLDER,
384 await self.
hasshass.async_add_executor_job(populate)
None __init__(self, dict[str, Any] data, *str|None path=None, str|None expected_domain=None, Callable[[Any], Any] schema)
None update_metadata(self, *str|None source_url=None)
list[str]|None validate(self)
dict[str, Any] inputs(self)
dict[str, Any] metadata(self)
Blueprint async_get_blueprint(self, str blueprint_path)
_reload_blueprint_consumers
None __init__(self, HomeAssistant hass, str domain, logging.Logger logger, Callable[[HomeAssistant, str], bool] blueprint_in_use, Callable[[HomeAssistant, str], Awaitable[None]] reload_blueprint_consumers, Callable[[Any], Any] blueprint_schema)
bool async_add_blueprint(self, Blueprint blueprint, str blueprint_path, bool allow_override=False)
pathlib.Path blueprint_folder(self)
None async_reset_cache(self)
dict[str, Blueprint|BlueprintException|None] async_get_blueprints(self)
BlueprintInputs async_inputs_from_config(self, dict config_with_blueprint)
None async_remove_blueprint(self, str blueprint_path)
Blueprint _load_blueprint(self, str blueprint_path)
dict[str, Blueprint|BlueprintException|None] _load_blueprints(self)
bool _create_file(self, Blueprint blueprint, str blueprint_path, bool allow_override)
None async_populate(self)
BLUEPRINT_INSTANCE_FIELDS
web.Response get(self, web.Request request, str config_key)
str humanize_error(HomeAssistant hass, vol.Invalid validation_error, str domain, dict config, str|None link, int max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH)