mirror of
https://github.com/home-assistant/core.git
synced 2025-11-11 12:00:52 +00:00
Compare commits
3 Commits
eve_shutte
...
cursor/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2fe77b7f5 | ||
|
|
d984e4398e | ||
|
|
75bd1a0310 |
@@ -1,7 +1,9 @@
|
||||
"""The blueprint integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import websocket_api
|
||||
@@ -28,4 +30,7 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the blueprint integration."""
|
||||
websocket_api.async_setup(hass)
|
||||
hass.async_create_task(
|
||||
async_load_platform(hass, Platform.UPDATE, DOMAIN, None, config)
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -204,8 +204,8 @@ class DomainBlueprints:
|
||||
self.hass = hass
|
||||
self.domain = domain
|
||||
self.logger = logger
|
||||
self._blueprint_in_use = blueprint_in_use
|
||||
self._reload_blueprint_consumers = reload_blueprint_consumers
|
||||
self.blueprint_in_use = blueprint_in_use
|
||||
self.reload_blueprint_consumers = reload_blueprint_consumers
|
||||
self._blueprints: dict[str, Blueprint | None] = {}
|
||||
self._load_lock = asyncio.Lock()
|
||||
self._blueprint_schema = blueprint_schema
|
||||
@@ -325,7 +325,7 @@ class DomainBlueprints:
|
||||
|
||||
async def async_remove_blueprint(self, blueprint_path: str) -> None:
|
||||
"""Remove a blueprint file."""
|
||||
if self._blueprint_in_use(self.hass, blueprint_path):
|
||||
if self.blueprint_in_use(self.hass, blueprint_path):
|
||||
raise BlueprintInUse(self.domain, blueprint_path)
|
||||
path = self.blueprint_folder / blueprint_path
|
||||
await self.hass.async_add_executor_job(path.unlink)
|
||||
@@ -362,7 +362,7 @@ class DomainBlueprints:
|
||||
self._blueprints[blueprint_path] = blueprint
|
||||
|
||||
if overrides_existing:
|
||||
await self._reload_blueprint_consumers(self.hass, blueprint_path)
|
||||
await self.reload_blueprint_consumers(self.hass, blueprint_path)
|
||||
|
||||
return overrides_existing
|
||||
|
||||
|
||||
293
homeassistant/components/blueprint/update.py
Normal file
293
homeassistant/components/blueprint/update.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""Update entities for blueprints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Any, Final
|
||||
|
||||
from homeassistant.components import automation, script
|
||||
from . import importer, models
|
||||
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
|
||||
from homeassistant.const import CONF_SOURCE_URL
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import event as event_helper
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN as BLUEPRINT_DOMAIN
|
||||
from .errors import BlueprintException
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_LATEST_VERSION_PLACEHOLDER: Final = "remote"
|
||||
DATA_UPDATE_MANAGER: Final = "update_manager"
|
||||
REFRESH_INTERVAL: Final = timedelta(days=1)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BlueprintUsage:
|
||||
"""Details about a blueprint currently in use."""
|
||||
|
||||
domain: str
|
||||
path: str
|
||||
domain_blueprints: models.DomainBlueprints
|
||||
blueprint: models.Blueprint
|
||||
entities: list[str]
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the blueprint update platform."""
|
||||
data = hass.data.setdefault(BLUEPRINT_DOMAIN, {})
|
||||
|
||||
if (manager := data.get(DATA_UPDATE_MANAGER)) is None:
|
||||
manager = BlueprintUpdateManager(hass, async_add_entities)
|
||||
data[DATA_UPDATE_MANAGER] = manager
|
||||
await manager.async_start()
|
||||
return
|
||||
|
||||
manager.replace_add_entities(async_add_entities)
|
||||
await manager.async_recreate_entities()
|
||||
|
||||
|
||||
class BlueprintUpdateManager:
|
||||
"""Manage blueprint update entities based on blueprint usage."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Initialize the manager."""
|
||||
self.hass = hass
|
||||
self._async_add_entities = async_add_entities
|
||||
self._entities: dict[tuple[str, str], BlueprintUpdateEntity] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
self._refresh_cancel: CALLBACK_TYPE | None = None
|
||||
self._started = False
|
||||
self._interval_unsub: CALLBACK_TYPE | None = None
|
||||
|
||||
async def async_start(self) -> None:
|
||||
"""Start tracking blueprint usage."""
|
||||
if self._started:
|
||||
return
|
||||
self._started = True
|
||||
|
||||
self._interval_unsub = event_helper.async_track_time_interval(
|
||||
self.hass, self._handle_time_interval, REFRESH_INTERVAL
|
||||
)
|
||||
await self.async_refresh_entities()
|
||||
|
||||
def replace_add_entities(self, async_add_entities: AddEntitiesCallback) -> None:
|
||||
"""Update the callback used to register entities."""
|
||||
self._async_add_entities = async_add_entities
|
||||
|
||||
async def async_recreate_entities(self) -> None:
|
||||
"""Recreate entities after the platform has been reloaded."""
|
||||
async with self._lock:
|
||||
entities = list(self._entities.values())
|
||||
self._entities.clear()
|
||||
|
||||
for entity in entities:
|
||||
await entity.async_remove()
|
||||
|
||||
await self.async_refresh_entities()
|
||||
|
||||
async def async_refresh_entities(self) -> None:
|
||||
"""Refresh update entities based on current blueprint usage."""
|
||||
async with self._lock:
|
||||
usage_map = await self._async_collect_in_use_blueprints()
|
||||
|
||||
current_keys = set(self._entities)
|
||||
new_keys = set(usage_map)
|
||||
|
||||
for key in current_keys - new_keys:
|
||||
entity = self._entities.pop(key)
|
||||
await entity.async_remove()
|
||||
|
||||
new_entities: list[BlueprintUpdateEntity] = []
|
||||
|
||||
for key in new_keys - current_keys:
|
||||
usage = usage_map[key]
|
||||
entity = BlueprintUpdateEntity(self, usage)
|
||||
self._entities[key] = entity
|
||||
new_entities.append(entity)
|
||||
|
||||
for key in new_keys & current_keys:
|
||||
self._entities[key].update_usage(usage_map[key])
|
||||
self._entities[key].async_write_ha_state()
|
||||
|
||||
if new_entities:
|
||||
self._async_add_entities(new_entities)
|
||||
|
||||
def async_schedule_refresh(self) -> None:
|
||||
"""Schedule an asynchronous refresh."""
|
||||
if self._refresh_cancel is not None:
|
||||
return
|
||||
|
||||
self._refresh_cancel = event_helper.async_call_later(
|
||||
self.hass, 0, self._handle_scheduled_refresh
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_scheduled_refresh(self, _now: Any) -> None:
|
||||
"""Run a scheduled refresh task."""
|
||||
self._refresh_cancel = None
|
||||
self.hass.async_create_task(self.async_refresh_entities())
|
||||
|
||||
@callback
|
||||
def _handle_time_interval(self, _now: Any) -> None:
|
||||
"""Handle scheduled interval refresh."""
|
||||
self.async_schedule_refresh()
|
||||
|
||||
async def _async_collect_in_use_blueprints(self) -> dict[tuple[str, str], BlueprintUsage]:
|
||||
"""Collect blueprint usage information for automations and scripts."""
|
||||
|
||||
usage_keys: set[tuple[str, str]] = set()
|
||||
|
||||
if automation.DATA_COMPONENT in self.hass.data:
|
||||
component = self.hass.data[automation.DATA_COMPONENT]
|
||||
for automation_entity in list(component.entities):
|
||||
if (path := getattr(automation_entity, "referenced_blueprint", None)):
|
||||
usage_keys.add((automation.DOMAIN, path))
|
||||
|
||||
if script.DOMAIN in self.hass.data:
|
||||
component = self.hass.data[script.DOMAIN]
|
||||
for script_entity in list(component.entities):
|
||||
if (path := getattr(script_entity, "referenced_blueprint", None)):
|
||||
usage_keys.add((script.DOMAIN, path))
|
||||
|
||||
domain_blueprints_map = self.hass.data.get(BLUEPRINT_DOMAIN, {})
|
||||
usage_map: dict[tuple[str, str], BlueprintUsage] = {}
|
||||
|
||||
for domain, path in usage_keys:
|
||||
domain_blueprints: models.DomainBlueprints | None = domain_blueprints_map.get(
|
||||
domain
|
||||
)
|
||||
|
||||
if domain_blueprints is None:
|
||||
continue
|
||||
|
||||
if not domain_blueprints.blueprint_in_use(self.hass, path):
|
||||
continue
|
||||
|
||||
try:
|
||||
blueprint = await domain_blueprints.async_get_blueprint(path)
|
||||
except BlueprintException:
|
||||
continue
|
||||
|
||||
source_url = blueprint.metadata.get(CONF_SOURCE_URL)
|
||||
if not source_url:
|
||||
continue
|
||||
|
||||
if domain == automation.DOMAIN:
|
||||
entities = automation.automations_with_blueprint(self.hass, path)
|
||||
elif domain == script.DOMAIN:
|
||||
entities = script.scripts_with_blueprint(self.hass, path)
|
||||
else:
|
||||
entities = []
|
||||
|
||||
usage_map[(domain, path)] = BlueprintUsage(
|
||||
domain=domain,
|
||||
path=path,
|
||||
domain_blueprints=domain_blueprints,
|
||||
blueprint=blueprint,
|
||||
entities=entities,
|
||||
)
|
||||
|
||||
return usage_map
|
||||
|
||||
|
||||
class BlueprintUpdateEntity(UpdateEntity):
|
||||
"""Define a blueprint update entity."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_attr_supported_features = UpdateEntityFeature.INSTALL
|
||||
|
||||
def __init__(self, manager: BlueprintUpdateManager, usage: BlueprintUsage) -> None:
|
||||
"""Initialize the update entity."""
|
||||
self._manager = manager
|
||||
self._domain = usage.domain
|
||||
self._path = usage.path
|
||||
self._domain_blueprints = usage.domain_blueprints
|
||||
self._blueprint = usage.blueprint
|
||||
self._entities_in_use = usage.entities
|
||||
self._source_url = usage.blueprint.metadata.get(CONF_SOURCE_URL)
|
||||
self._attr_unique_id = f"{self._domain}:{self._path}"
|
||||
self._attr_in_progress = False
|
||||
|
||||
self.update_usage(usage)
|
||||
|
||||
@callback
|
||||
def update_usage(self, usage: BlueprintUsage) -> None:
|
||||
"""Update the entity with latest usage information."""
|
||||
self._domain_blueprints = usage.domain_blueprints
|
||||
self._blueprint = usage.blueprint
|
||||
self._entities_in_use = usage.entities
|
||||
self._source_url = usage.blueprint.metadata.get(CONF_SOURCE_URL)
|
||||
|
||||
self._attr_name = usage.blueprint.name
|
||||
self._attr_release_summary = usage.blueprint.metadata.get("description")
|
||||
self._attr_installed_version = usage.blueprint.metadata.get("version")
|
||||
self._attr_release_url = self._source_url
|
||||
self._attr_available = self._source_url is not None
|
||||
self._attr_latest_version = (
|
||||
_LATEST_VERSION_PLACEHOLDER
|
||||
if self._source_url is not None
|
||||
else self._attr_installed_version
|
||||
)
|
||||
|
||||
async def async_install(self, version: str | None, backup: bool) -> None:
|
||||
"""Install (refresh) the blueprint from its source."""
|
||||
if self._source_url is None:
|
||||
raise HomeAssistantError("Blueprint does not define a source URL")
|
||||
|
||||
self._attr_in_progress = True
|
||||
self.async_write_ha_state()
|
||||
usage: BlueprintUsage | None = None
|
||||
|
||||
try:
|
||||
imported = await importer.fetch_blueprint_from_url(
|
||||
self.hass, self._source_url
|
||||
)
|
||||
blueprint = imported.blueprint
|
||||
|
||||
if blueprint.domain != self._domain:
|
||||
raise HomeAssistantError(
|
||||
"Downloaded blueprint domain does not match the existing blueprint"
|
||||
)
|
||||
|
||||
await self._domain_blueprints.async_add_blueprint(
|
||||
blueprint, self._path, allow_override=True
|
||||
)
|
||||
|
||||
usage = BlueprintUsage(
|
||||
domain=self._domain,
|
||||
path=self._path,
|
||||
domain_blueprints=self._domain_blueprints,
|
||||
blueprint=blueprint,
|
||||
entities=self._entities_in_use,
|
||||
)
|
||||
|
||||
except HomeAssistantError:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001 - Provide context for unexpected errors
|
||||
raise HomeAssistantError("Failed to update blueprint from source") from err
|
||||
finally:
|
||||
self._attr_in_progress = False
|
||||
|
||||
if usage is not None:
|
||||
self.update_usage(usage)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._manager.async_schedule_refresh()
|
||||
@@ -74,6 +74,7 @@ from .const import (
|
||||
CONF_TRACE,
|
||||
DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
EVENT_SCRIPT_RELOADED,
|
||||
EVENT_SCRIPT_STARTED,
|
||||
LOGGER,
|
||||
)
|
||||
@@ -237,6 +238,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if (conf := await component.async_prepare_reload(skip_reset=True)) is None:
|
||||
return
|
||||
await _async_process_config(hass, conf, component)
|
||||
hass.bus.async_fire(EVENT_SCRIPT_RELOADED, context=service.context)
|
||||
|
||||
async def turn_on_service(service: ServiceCall) -> None:
|
||||
"""Call a service to turn script on."""
|
||||
|
||||
@@ -17,5 +17,6 @@ CONF_TRACE = "trace"
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
EVENT_SCRIPT_STARTED = "script_started"
|
||||
EVENT_SCRIPT_RELOADED = "script_reloaded"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
257
tests/components/blueprint/test_update.py
Normal file
257
tests/components/blueprint/test_update.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""Tests for blueprint update entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation, script
|
||||
from homeassistant.components.blueprint import importer, models
|
||||
from homeassistant.components.blueprint.const import DOMAIN as BLUEPRINT_DOMAIN
|
||||
from homeassistant.components.blueprint.schemas import BLUEPRINT_SCHEMA
|
||||
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import yaml as yaml_util
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_blueprint_component(hass):
|
||||
"""Ensure the blueprint integration is set up for each test."""
|
||||
assert await async_setup_component(hass, BLUEPRINT_DOMAIN, {})
|
||||
|
||||
|
||||
async def test_automation_blueprint_update_entity_reloads(hass) -> None:
|
||||
"""Test that updating an automation blueprint refreshes YAML and reloads automations."""
|
||||
source_url = "https://example.com/blueprints/automation/test.yaml"
|
||||
blueprint_rel_path = "test_namespace/test_automation.yaml"
|
||||
blueprint_file = Path(
|
||||
hass.config.path("blueprints", automation.DOMAIN, blueprint_rel_path)
|
||||
)
|
||||
blueprint_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
initial_yaml = (
|
||||
"blueprint:\n"
|
||||
" name: Test automation blueprint\n"
|
||||
" description: Initial version\n"
|
||||
" domain: automation\n"
|
||||
f" source_url: {source_url}\n"
|
||||
" input: {}\n"
|
||||
"trigger:\n"
|
||||
" - platform: event\n"
|
||||
" event_type: test_event\n"
|
||||
"action:\n"
|
||||
" - service: test.initial_service\n"
|
||||
)
|
||||
blueprint_file.write_text(initial_yaml, encoding="utf-8")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.automation.helpers._reload_blueprint_automations",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_reload:
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"use_blueprint": {
|
||||
"path": blueprint_rel_path,
|
||||
"input": {},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
unique_id = f"{automation.DOMAIN}:{blueprint_rel_path}"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
UPDATE_DOMAIN, BLUEPRINT_DOMAIN, unique_id
|
||||
)
|
||||
assert entity_id is not None
|
||||
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "on"
|
||||
assert state.attributes.get("release_url") == source_url
|
||||
|
||||
updated_yaml = (
|
||||
"blueprint:\n"
|
||||
" name: Test automation blueprint\n"
|
||||
" description: Updated version\n"
|
||||
" domain: automation\n"
|
||||
f" source_url: {source_url}\n"
|
||||
" input: {}\n"
|
||||
"trigger:\n"
|
||||
" - platform: event\n"
|
||||
" event_type: test_event\n"
|
||||
"action:\n"
|
||||
" - service: test.new_service\n"
|
||||
)
|
||||
updated_data = yaml_util.parse_yaml(updated_yaml)
|
||||
new_blueprint = models.Blueprint(
|
||||
updated_data,
|
||||
expected_domain=automation.DOMAIN,
|
||||
path=blueprint_rel_path,
|
||||
schema=automation.config.AUTOMATION_BLUEPRINT_SCHEMA,
|
||||
)
|
||||
imported_blueprint = importer.ImportedBlueprint(
|
||||
blueprint_rel_path.removesuffix(".yaml"), updated_yaml, new_blueprint
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.blueprint.importer.fetch_blueprint_from_url",
|
||||
new=AsyncMock(return_value=imported_blueprint),
|
||||
) as mock_fetch:
|
||||
await hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
"install",
|
||||
{"entity_id": entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_fetch.assert_awaited_once_with(hass, source_url)
|
||||
mock_reload.assert_awaited_once_with(hass, blueprint_rel_path)
|
||||
assert "test.new_service" in blueprint_file.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
async def test_script_blueprint_update_entity_reloads(hass) -> None:
|
||||
"""Test that updating a script blueprint refreshes YAML and reloads scripts."""
|
||||
source_url = "https://example.com/blueprints/script/test.yaml"
|
||||
blueprint_rel_path = "test_namespace/test_script.yaml"
|
||||
blueprint_file = Path(
|
||||
hass.config.path("blueprints", script.DOMAIN, blueprint_rel_path)
|
||||
)
|
||||
blueprint_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
initial_yaml = (
|
||||
"blueprint:\n"
|
||||
" name: Test script blueprint\n"
|
||||
" description: Initial version\n"
|
||||
" domain: script\n"
|
||||
f" source_url: {source_url}\n"
|
||||
" input: {}\n"
|
||||
"sequence:\n"
|
||||
" - event: test_event\n"
|
||||
)
|
||||
blueprint_file.write_text(initial_yaml, encoding="utf-8")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.script.helpers._reload_blueprint_scripts",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_reload:
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
script.DOMAIN,
|
||||
{
|
||||
script.DOMAIN: {
|
||||
"test_script": {
|
||||
"use_blueprint": {
|
||||
"path": blueprint_rel_path,
|
||||
"input": {},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
unique_id = f"{script.DOMAIN}:{blueprint_rel_path}"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
UPDATE_DOMAIN, BLUEPRINT_DOMAIN, unique_id
|
||||
)
|
||||
assert entity_id is not None
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "on"
|
||||
assert state.attributes.get("release_url") == source_url
|
||||
|
||||
updated_yaml = (
|
||||
"blueprint:\n"
|
||||
" name: Test script blueprint\n"
|
||||
" description: Updated version\n"
|
||||
" domain: script\n"
|
||||
f" source_url: {source_url}\n"
|
||||
" input: {}\n"
|
||||
"sequence:\n"
|
||||
" - event: updated_event\n"
|
||||
)
|
||||
updated_data = yaml_util.parse_yaml(updated_yaml)
|
||||
new_blueprint = models.Blueprint(
|
||||
updated_data,
|
||||
expected_domain=script.DOMAIN,
|
||||
path=blueprint_rel_path,
|
||||
schema=BLUEPRINT_SCHEMA,
|
||||
)
|
||||
imported_blueprint = importer.ImportedBlueprint(
|
||||
blueprint_rel_path.removesuffix(".yaml"), updated_yaml, new_blueprint
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.blueprint.importer.fetch_blueprint_from_url",
|
||||
new=AsyncMock(return_value=imported_blueprint),
|
||||
) as mock_fetch:
|
||||
await hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
"install",
|
||||
{"entity_id": entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_fetch.assert_awaited_once_with(hass, source_url)
|
||||
mock_reload.assert_awaited_once_with(hass, blueprint_rel_path)
|
||||
assert "updated_event" in blueprint_file.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
async def test_blueprint_without_source_has_no_update_entity(hass) -> None:
|
||||
"""Ensure blueprints without a source URL do not expose update entities."""
|
||||
blueprint_rel_path = "test_namespace/without_source.yaml"
|
||||
blueprint_file = Path(
|
||||
hass.config.path("blueprints", automation.DOMAIN, blueprint_rel_path)
|
||||
)
|
||||
blueprint_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
yaml_without_source = (
|
||||
"blueprint:\n"
|
||||
" name: No source blueprint\n"
|
||||
" description: No source\n"
|
||||
" domain: automation\n"
|
||||
" input: {}\n"
|
||||
"trigger:\n"
|
||||
" - platform: event\n"
|
||||
" event_type: test_event\n"
|
||||
"action:\n"
|
||||
" - service: test.service\n"
|
||||
)
|
||||
blueprint_file.write_text(yaml_without_source, encoding="utf-8")
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"use_blueprint": {
|
||||
"path": blueprint_rel_path,
|
||||
"input": {},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
unique_id = f"{automation.DOMAIN}:{blueprint_rel_path}"
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(UPDATE_DOMAIN, BLUEPRINT_DOMAIN, unique_id)
|
||||
is None
|
||||
)
|
||||
Reference in New Issue
Block a user