Add support for checking minimum HA version (#43350)

This commit is contained in:
Paulus Schoutsen 2020-11-20 15:24:42 +01:00 committed by GitHub
parent e98f36e357
commit 43ba053030
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 130 additions and 5 deletions

View file

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

View file

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

View file

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

View file

@ -85,6 +85,7 @@ async def ws_import_blueprint(hass, connection, msg):
"blueprint": {
"metadata": imported_blueprint.blueprint.metadata,
},
"validation_errors": imported_blueprint.blueprint.validate(),
},
)

View file

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

View file

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

View file

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

View file

@ -96,6 +96,7 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client):
"name": "Call service based on event",
},
},
"validation_errors": None,
}