Add support for checking minimum HA version (#43350)
This commit is contained in:
parent
e98f36e357
commit
43ba053030
8 changed files with 130 additions and 5 deletions
|
@ -6,5 +6,7 @@ CONF_USE_BLUEPRINT = "use_blueprint"
|
|||
CONF_INPUT = "input"
|
||||
CONF_SOURCE_URL = "source_url"
|
||||
CONF_DESCRIPTION = "description"
|
||||
CONF_HOMEASSISTANT = "homeassistant"
|
||||
CONF_MIN_VERSION = "min_version"
|
||||
|
||||
DOMAIN = "blueprint"
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import pathlib
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from pkg_resources import parse_version
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH
|
||||
from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH, __version__
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import placeholder
|
||||
|
@ -16,7 +17,9 @@ from homeassistant.util import yaml
|
|||
from .const import (
|
||||
BLUEPRINT_FOLDER,
|
||||
CONF_BLUEPRINT,
|
||||
CONF_HOMEASSISTANT,
|
||||
CONF_INPUT,
|
||||
CONF_MIN_VERSION,
|
||||
CONF_SOURCE_URL,
|
||||
CONF_USE_BLUEPRINT,
|
||||
DOMAIN,
|
||||
|
@ -62,7 +65,7 @@ class Blueprint:
|
|||
|
||||
self.domain = data_domain
|
||||
|
||||
missing = self.placeholders - set(data[CONF_BLUEPRINT].get(CONF_INPUT, {}))
|
||||
missing = self.placeholders - set(data[CONF_BLUEPRINT][CONF_INPUT])
|
||||
|
||||
if missing:
|
||||
raise InvalidBlueprint(
|
||||
|
@ -91,6 +94,23 @@ class Blueprint:
|
|||
"""Dump blueprint as YAML."""
|
||||
return yaml.dump(self.data)
|
||||
|
||||
@callback
|
||||
def validate(self) -> Optional[List[str]]:
|
||||
"""Test if the Home Assistant installation supports this blueprint.
|
||||
|
||||
Return list of errors if not valid.
|
||||
"""
|
||||
errors = []
|
||||
metadata = self.metadata
|
||||
min_version = metadata.get(CONF_HOMEASSISTANT, {}).get(CONF_MIN_VERSION)
|
||||
|
||||
if min_version is not None and parse_version(__version__) < parse_version(
|
||||
min_version
|
||||
):
|
||||
errors.append(f"Requires at least Home Assistant {min_version}")
|
||||
|
||||
return errors or None
|
||||
|
||||
|
||||
class BlueprintInputs:
|
||||
"""Inputs for a blueprint."""
|
||||
|
|
|
@ -10,12 +10,34 @@ from homeassistant.helpers import config_validation as cv, selector
|
|||
from .const import (
|
||||
CONF_BLUEPRINT,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_HOMEASSISTANT,
|
||||
CONF_INPUT,
|
||||
CONF_MIN_VERSION,
|
||||
CONF_SOURCE_URL,
|
||||
CONF_USE_BLUEPRINT,
|
||||
)
|
||||
|
||||
|
||||
def version_validator(value):
|
||||
"""Validate a Home Assistant version."""
|
||||
if not isinstance(value, str):
|
||||
raise vol.Invalid("Version needs to be a string")
|
||||
|
||||
parts = value.split(".")
|
||||
|
||||
if len(parts) != 3:
|
||||
raise vol.Invalid("Version needs to be formatted as {major}.{minor}.{patch}")
|
||||
|
||||
try:
|
||||
parts = [int(p) for p in parts]
|
||||
except ValueError:
|
||||
raise vol.Invalid(
|
||||
"Major, minor and patch version needs to be an integer"
|
||||
) from None
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@callback
|
||||
def is_blueprint_config(config: Any) -> bool:
|
||||
"""Return if it is a blueprint config."""
|
||||
|
@ -43,6 +65,9 @@ BLUEPRINT_SCHEMA = vol.Schema(
|
|||
vol.Required(CONF_NAME): str,
|
||||
vol.Required(CONF_DOMAIN): str,
|
||||
vol.Optional(CONF_SOURCE_URL): cv.url,
|
||||
vol.Optional(CONF_HOMEASSISTANT): {
|
||||
vol.Optional(CONF_MIN_VERSION): version_validator
|
||||
},
|
||||
vol.Optional(CONF_INPUT, default=dict): {
|
||||
str: vol.Any(
|
||||
None,
|
||||
|
@ -68,7 +93,7 @@ BLUEPRINT_INSTANCE_FIELDS = vol.Schema(
|
|||
vol.Required(CONF_USE_BLUEPRINT): vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PATH): vol.All(cv.path, validate_yaml_suffix),
|
||||
vol.Required(CONF_INPUT): {str: cv.match_all},
|
||||
vol.Required(CONF_INPUT, default=dict): {str: cv.match_all},
|
||||
}
|
||||
)
|
||||
},
|
||||
|
|
|
@ -85,6 +85,7 @@ async def ws_import_blueprint(hass, connection, msg):
|
|||
"blueprint": {
|
||||
"metadata": imported_blueprint.blueprint.metadata,
|
||||
},
|
||||
"validation_errors": imported_blueprint.blueprint.validate(),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -40,7 +40,9 @@ class EntitySelector(Selector):
|
|||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
# Integration that provided the entity
|
||||
vol.Optional("integration"): str,
|
||||
# Domain the entity belongs to
|
||||
vol.Optional("domain"): str,
|
||||
}
|
||||
)
|
||||
|
@ -52,8 +54,11 @@ class DeviceSelector(Selector):
|
|||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
# Integration linked to it with a config entry
|
||||
vol.Optional("integration"): str,
|
||||
# Manufacturer of device
|
||||
vol.Optional("manufacturer"): str,
|
||||
# Model of device
|
||||
vol.Optional("model"): str,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -71,7 +71,7 @@ def test_blueprint_properties(blueprint_1):
|
|||
|
||||
|
||||
def test_blueprint_update_metadata():
|
||||
"""Test properties."""
|
||||
"""Test update metadata."""
|
||||
bp = models.Blueprint(
|
||||
{
|
||||
"blueprint": {
|
||||
|
@ -85,6 +85,34 @@ def test_blueprint_update_metadata():
|
|||
assert bp.metadata["source_url"] == "http://bla.com"
|
||||
|
||||
|
||||
def test_blueprint_validate():
|
||||
"""Test validate blueprint."""
|
||||
assert (
|
||||
models.Blueprint(
|
||||
{
|
||||
"blueprint": {
|
||||
"name": "Hello",
|
||||
"domain": "automation",
|
||||
},
|
||||
}
|
||||
).validate()
|
||||
is None
|
||||
)
|
||||
|
||||
assert (
|
||||
models.Blueprint(
|
||||
{
|
||||
"blueprint": {
|
||||
"name": "Hello",
|
||||
"domain": "automation",
|
||||
"homeassistant": {"min_version": "100000.0.0"},
|
||||
},
|
||||
}
|
||||
).validate()
|
||||
== ["Requires at least Home Assistant 100000.0.0"]
|
||||
)
|
||||
|
||||
|
||||
def test_blueprint_inputs(blueprint_1):
|
||||
"""Test blueprint inputs."""
|
||||
inputs = models.BlueprintInputs(
|
||||
|
|
|
@ -31,6 +31,26 @@ _LOGGER = logging.getLogger(__name__)
|
|||
},
|
||||
}
|
||||
},
|
||||
# With selector
|
||||
{
|
||||
"blueprint": {
|
||||
"name": "Test Name",
|
||||
"domain": "automation",
|
||||
"input": {
|
||||
"some_placeholder": {"selector": {"entity": {}}},
|
||||
},
|
||||
}
|
||||
},
|
||||
# With min version
|
||||
{
|
||||
"blueprint": {
|
||||
"name": "Test Name",
|
||||
"domain": "automation",
|
||||
"homeassistant": {
|
||||
"min_version": "1000000.0.0",
|
||||
},
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
def test_blueprint_schema(blueprint):
|
||||
|
@ -63,9 +83,32 @@ def test_blueprint_schema(blueprint):
|
|||
"input": {"some_placeholder": {"non_existing": "bla"}},
|
||||
}
|
||||
},
|
||||
# Invalid version
|
||||
{
|
||||
"blueprint": {
|
||||
"name": "Test Name",
|
||||
"domain": "automation",
|
||||
"homeassistant": {
|
||||
"min_version": "1000000.invalid.0",
|
||||
},
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
def test_blueprint_schema_invalid(blueprint):
|
||||
"""Test different schemas."""
|
||||
with pytest.raises(vol.Invalid):
|
||||
schemas.BLUEPRINT_SCHEMA(blueprint)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bp_instance",
|
||||
(
|
||||
{"path": "hello.yaml"},
|
||||
{"path": "hello.yaml", "input": {}},
|
||||
{"path": "hello.yaml", "input": {"hello": None}},
|
||||
),
|
||||
)
|
||||
def test_blueprint_instance_fields(bp_instance):
|
||||
"""Test blueprint instance fields."""
|
||||
schemas.BLUEPRINT_INSTANCE_FIELDS({"use_blueprint": bp_instance})
|
||||
|
|
|
@ -96,6 +96,7 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client):
|
|||
"name": "Call service based on event",
|
||||
},
|
||||
},
|
||||
"validation_errors": None,
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue