1086 lines
32 KiB
Python
1086 lines
32 KiB
Python
"""The tests for the Template update platform."""
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from syrupy.assertion import SnapshotAssertion
|
|
|
|
from homeassistant.components import template, update
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_PICTURE,
|
|
ATTR_ICON,
|
|
STATE_OFF,
|
|
STATE_ON,
|
|
STATE_UNAVAILABLE,
|
|
STATE_UNKNOWN,
|
|
)
|
|
from homeassistant.core import HomeAssistant, ServiceCall, State
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from .conftest import (
|
|
ConfigurationStyle,
|
|
async_get_flow_preview_state,
|
|
async_setup_modern_state_format,
|
|
async_setup_modern_trigger_format,
|
|
make_test_trigger,
|
|
)
|
|
|
|
from tests.common import (
|
|
MockConfigEntry,
|
|
assert_setup_component,
|
|
mock_restore_cache_with_extra_data,
|
|
)
|
|
from tests.conftest import WebSocketGenerator
|
|
|
|
TEST_OBJECT_ID = "template_update"
|
|
TEST_ENTITY_ID = f"update.{TEST_OBJECT_ID}"
|
|
TEST_INSTALLED_SENSOR = "sensor.installed_update"
|
|
TEST_LATEST_SENSOR = "sensor.latest_update"
|
|
TEST_SENSOR_ID = "sensor.test_update"
|
|
TEST_STATE_TRIGGER = make_test_trigger(
|
|
TEST_INSTALLED_SENSOR, TEST_LATEST_SENSOR, TEST_SENSOR_ID
|
|
)
|
|
TEST_INSTALLED_TEMPLATE = "{{ '1.0' }}"
|
|
TEST_LATEST_TEMPLATE = "{{ '2.0' }}"
|
|
|
|
TEST_UPDATE_CONFIG = {
|
|
"installed_version": TEST_INSTALLED_TEMPLATE,
|
|
"latest_version": TEST_LATEST_TEMPLATE,
|
|
}
|
|
TEST_UNIQUE_ID_CONFIG = {
|
|
**TEST_UPDATE_CONFIG,
|
|
"unique_id": "not-so-unique-anymore",
|
|
}
|
|
|
|
INSTALL_ACTION = {
|
|
"install": {
|
|
"action": "test.automation",
|
|
"data": {
|
|
"caller": "{{ this.entity_id }}",
|
|
"action": "install",
|
|
"backup": "{{ backup }}",
|
|
"specific_version": "{{ specific_version }}",
|
|
},
|
|
}
|
|
}
|
|
|
|
|
|
async def async_setup_config(
|
|
hass: HomeAssistant,
|
|
count: int,
|
|
style: ConfigurationStyle,
|
|
config: dict[str, Any],
|
|
extra_config: dict[str, Any] | None,
|
|
) -> None:
|
|
"""Do setup of update integration."""
|
|
config = {**config, **extra_config} if extra_config else config
|
|
if style == ConfigurationStyle.MODERN:
|
|
await async_setup_modern_state_format(hass, update.DOMAIN, count, config)
|
|
elif style == ConfigurationStyle.TRIGGER:
|
|
await async_setup_modern_trigger_format(
|
|
hass, update.DOMAIN, TEST_STATE_TRIGGER, count, config
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def setup_base(
|
|
hass: HomeAssistant,
|
|
count: int,
|
|
style: ConfigurationStyle,
|
|
config: dict[str, Any],
|
|
) -> None:
|
|
"""Do setup of update integration."""
|
|
await async_setup_config(
|
|
hass,
|
|
count,
|
|
style,
|
|
config,
|
|
None,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def setup_update(
|
|
hass: HomeAssistant,
|
|
count: int,
|
|
style: ConfigurationStyle,
|
|
installed_template: str,
|
|
latest_template: str,
|
|
extra_config: dict[str, Any] | None,
|
|
) -> None:
|
|
"""Do setup of update integration."""
|
|
await async_setup_config(
|
|
hass,
|
|
count,
|
|
style,
|
|
{
|
|
"name": TEST_OBJECT_ID,
|
|
"installed_version": installed_template,
|
|
"latest_version": latest_template,
|
|
},
|
|
extra_config,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def setup_single_attribute_update(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
installed_template: str,
|
|
latest_template: str,
|
|
attribute: str,
|
|
attribute_template: str,
|
|
) -> None:
|
|
"""Do setup of update platform testing a single attribute."""
|
|
await async_setup_config(
|
|
hass,
|
|
1,
|
|
style,
|
|
{
|
|
"name": TEST_OBJECT_ID,
|
|
"installed_version": installed_template,
|
|
"latest_version": latest_template,
|
|
},
|
|
{attribute: attribute_template} if attribute and attribute_template else {},
|
|
)
|
|
|
|
|
|
async def test_legacy_platform_config(hass: HomeAssistant) -> None:
|
|
"""Test a legacy platform does not create update entities."""
|
|
with assert_setup_component(1, update.DOMAIN):
|
|
assert await async_setup_component(
|
|
hass,
|
|
update.DOMAIN,
|
|
{"update": {"platform": "template", "updates": {TEST_OBJECT_ID: {}}}},
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
await hass.async_start()
|
|
await hass.async_block_till_done()
|
|
assert hass.states.async_all("update") == []
|
|
|
|
|
|
async def test_setup_config_entry(
|
|
hass: HomeAssistant,
|
|
snapshot: SnapshotAssertion,
|
|
) -> None:
|
|
"""Test the config flow."""
|
|
|
|
template_config_entry = MockConfigEntry(
|
|
data={},
|
|
domain=template.DOMAIN,
|
|
options={
|
|
"name": TEST_OBJECT_ID,
|
|
"template_type": update.DOMAIN,
|
|
**TEST_UPDATE_CONFIG,
|
|
},
|
|
title="My template",
|
|
)
|
|
template_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(template_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state is not None
|
|
assert state == snapshot
|
|
|
|
|
|
async def test_device_id(
|
|
hass: HomeAssistant,
|
|
device_registry: dr.DeviceRegistry,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test for device for Template."""
|
|
|
|
device_config_entry = MockConfigEntry()
|
|
device_config_entry.add_to_hass(hass)
|
|
device_entry = device_registry.async_get_or_create(
|
|
config_entry_id=device_config_entry.entry_id,
|
|
identifiers={("test", "identifier_test")},
|
|
connections={("mac", "30:31:32:33:34:35")},
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert device_entry is not None
|
|
assert device_entry.id is not None
|
|
|
|
template_config_entry = MockConfigEntry(
|
|
data={},
|
|
domain=template.DOMAIN,
|
|
options={
|
|
"name": TEST_OBJECT_ID,
|
|
"template_type": update.DOMAIN,
|
|
**TEST_UPDATE_CONFIG,
|
|
"device_id": device_entry.id,
|
|
},
|
|
title="My template",
|
|
)
|
|
template_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(template_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
template_entity = entity_registry.async_get(TEST_ENTITY_ID)
|
|
assert template_entity is not None
|
|
assert template_entity.device_id == device_entry.id
|
|
|
|
|
|
@pytest.mark.parametrize(("count", "extra_config"), [(1, None)])
|
|
@pytest.mark.parametrize(
|
|
("style", "expected_state"),
|
|
[
|
|
(ConfigurationStyle.MODERN, STATE_UNKNOWN),
|
|
(ConfigurationStyle.TRIGGER, STATE_UNKNOWN),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("installed_template", "latest_template"),
|
|
[
|
|
("{{states.test['big.fat...']}}", TEST_LATEST_TEMPLATE),
|
|
(TEST_INSTALLED_TEMPLATE, "{{states.test['big.fat...']}}"),
|
|
("{{states.test['big.fat...']}}", "{{states.test['big.fat...']}}"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_update")
|
|
async def test_syntax_error(
|
|
hass: HomeAssistant,
|
|
expected_state: str,
|
|
) -> None:
|
|
"""Test template update with render error."""
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.state == expected_state
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("count", "extra_config", "installed_template", "latest_template"),
|
|
[
|
|
(
|
|
1,
|
|
None,
|
|
"{{ states('sensor.installed_update') }}",
|
|
"{{ states('sensor.latest_update') }}",
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("installed", "latest", "expected"),
|
|
[
|
|
("1.0", "2.0", STATE_ON),
|
|
("2.0", "2.0", STATE_OFF),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_update")
|
|
async def test_update_templates(
|
|
hass: HomeAssistant, installed: str, latest: str, expected: str
|
|
) -> None:
|
|
"""Test update template."""
|
|
hass.states.async_set(TEST_INSTALLED_SENSOR, installed)
|
|
hass.states.async_set(TEST_LATEST_SENSOR, latest)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state is not None
|
|
assert state.state == expected
|
|
assert state.attributes["installed_version"] == installed
|
|
assert state.attributes["latest_version"] == latest
|
|
|
|
# ensure that the entity picture exists when not provided.
|
|
assert (
|
|
state.attributes["entity_picture"]
|
|
== "https://brands.home-assistant.io/_/template/icon.png"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("count", "extra_config", "installed_template", "latest_template"),
|
|
[
|
|
(
|
|
1,
|
|
None,
|
|
"{{ states('sensor.installed_update') }}",
|
|
"{{ states('sensor.latest_update') }}",
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.usefixtures("setup_update")
|
|
async def test_installed_and_latest_template_updates_from_entity(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test template installed and latest version templates updates from entities."""
|
|
hass.states.async_set(TEST_INSTALLED_SENSOR, "1.0")
|
|
hass.states.async_set(TEST_LATEST_SENSOR, "2.0")
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state is not None
|
|
assert state.state == STATE_ON
|
|
assert state.attributes["installed_version"] == "1.0"
|
|
assert state.attributes["latest_version"] == "2.0"
|
|
|
|
hass.states.async_set(TEST_INSTALLED_SENSOR, "2.0")
|
|
hass.states.async_set(TEST_LATEST_SENSOR, "2.0")
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state is not None
|
|
assert state.state == STATE_OFF
|
|
assert state.attributes["installed_version"] == "2.0"
|
|
assert state.attributes["latest_version"] == "2.0"
|
|
|
|
hass.states.async_set(TEST_INSTALLED_SENSOR, "2.0")
|
|
hass.states.async_set(TEST_LATEST_SENSOR, "3.0")
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state is not None
|
|
assert state.state == STATE_ON
|
|
assert state.attributes["installed_version"] == "2.0"
|
|
assert state.attributes["latest_version"] == "3.0"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("count", "extra_config", "latest_template"),
|
|
[(1, None, TEST_LATEST_TEMPLATE)],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("installed_template", "expected", "expected_attr"),
|
|
[
|
|
("{{ '1.0' }}", STATE_ON, "1.0"),
|
|
("{{ 1.0 }}", STATE_ON, "1.0"),
|
|
("{{ '2.0' }}", STATE_OFF, "2.0"),
|
|
("{{ 2.0 }}", STATE_OFF, "2.0"),
|
|
("{{ None }}", STATE_UNKNOWN, None),
|
|
("{{ 'foo' }}", STATE_ON, "foo"),
|
|
("{{ x + 2 }}", STATE_UNKNOWN, None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_update")
|
|
async def test_installed_version_template(
|
|
hass: HomeAssistant, expected: str, expected_attr: Any
|
|
) -> None:
|
|
"""Test installed_version template results."""
|
|
# Ensure trigger based template entities update
|
|
hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state is not None
|
|
assert state.state == expected
|
|
assert state.attributes["installed_version"] == expected_attr
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("count", "extra_config", "installed_template"),
|
|
[(1, None, TEST_INSTALLED_TEMPLATE)],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("latest_template", "expected", "expected_attr"),
|
|
[
|
|
("{{ '1.0' }}", STATE_OFF, "1.0"),
|
|
("{{ 1.0 }}", STATE_OFF, "1.0"),
|
|
("{{ '2.0' }}", STATE_ON, "2.0"),
|
|
("{{ 2.0 }}", STATE_ON, "2.0"),
|
|
("{{ None }}", STATE_UNKNOWN, None),
|
|
("{{ 'foo' }}", STATE_ON, "foo"),
|
|
("{{ x + 2 }}", STATE_UNKNOWN, None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_update")
|
|
async def test_latest_version_template(
|
|
hass: HomeAssistant, expected: str, expected_attr: Any
|
|
) -> None:
|
|
"""Test latest_version template results."""
|
|
# Ensure trigger based template entities update
|
|
hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state is not None
|
|
assert state.state == expected
|
|
assert state.attributes["latest_version"] == expected_attr
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("count", "extra_config", "installed_template", "latest_template"),
|
|
[
|
|
(
|
|
1,
|
|
INSTALL_ACTION,
|
|
"{{ states('sensor.installed_update') }}",
|
|
"{{ states('sensor.latest_update') }}",
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.usefixtures("setup_update")
|
|
async def test_install_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None:
|
|
"""Test install action."""
|
|
|
|
hass.states.async_set(TEST_INSTALLED_SENSOR, "1.0")
|
|
hass.states.async_set(TEST_LATEST_SENSOR, "2.0")
|
|
await hass.async_block_till_done()
|
|
|
|
await hass.services.async_call(
|
|
update.DOMAIN,
|
|
update.SERVICE_INSTALL,
|
|
{"entity_id": TEST_ENTITY_ID},
|
|
blocking=True,
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
# verify
|
|
assert len(calls) == 1
|
|
assert calls[-1].data["action"] == "install"
|
|
assert calls[-1].data["caller"] == TEST_ENTITY_ID
|
|
|
|
hass.states.async_set(TEST_INSTALLED_SENSOR, "2.0")
|
|
hass.states.async_set(TEST_LATEST_SENSOR, "2.0")
|
|
await hass.async_block_till_done()
|
|
|
|
# Ensure an error is raised when there's no update.
|
|
with pytest.raises(HomeAssistantError):
|
|
await hass.services.async_call(
|
|
update.DOMAIN,
|
|
update.SERVICE_INSTALL,
|
|
{"entity_id": TEST_ENTITY_ID},
|
|
blocking=True,
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
# verify
|
|
assert len(calls) == 1
|
|
assert calls[-1].data["action"] == "install"
|
|
assert calls[-1].data["caller"] == TEST_ENTITY_ID
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("installed_template", "latest_template"),
|
|
[(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute", "attribute_template", "key", "expected"),
|
|
[
|
|
(
|
|
"picture",
|
|
"{% if is_state('sensor.installed_update', 'on') %}something{% endif %}",
|
|
ATTR_ENTITY_PICTURE,
|
|
"something",
|
|
),
|
|
(
|
|
"icon",
|
|
"{% if is_state('sensor.installed_update', 'on') %}mdi:something{% endif %}",
|
|
ATTR_ICON,
|
|
"mdi:something",
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_update")
|
|
async def test_entity_picture_and_icon_templates(
|
|
hass: HomeAssistant, key: str, expected: str
|
|
) -> None:
|
|
"""Test picture and icon template."""
|
|
state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.attributes.get(key) in ("", None)
|
|
|
|
state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
|
|
assert state.attributes[key] == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("installed_template", "latest_template"),
|
|
[(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute", "attribute_template"),
|
|
[
|
|
(
|
|
"picture",
|
|
"{{ 'foo.png' if is_state('sensor.installed_update', 'on') else None }}",
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_update")
|
|
async def test_entity_picture_uses_default(hass: HomeAssistant) -> None:
|
|
"""Test entity picture when template resolves None."""
|
|
state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.attributes[ATTR_ENTITY_PICTURE] == "foo.png"
|
|
|
|
state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
|
|
assert (
|
|
state.attributes[ATTR_ENTITY_PICTURE]
|
|
== "https://brands.home-assistant.io/_/template/icon.png"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("installed_template", "latest_template", "attribute"),
|
|
[(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE, "in_progress")],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected", "error"),
|
|
[
|
|
("{{ True }}", True, None),
|
|
("{{ False }}", False, None),
|
|
("{{ None }}", False, "Received invalid in_process value: None"),
|
|
(
|
|
"{{ 'foo' }}",
|
|
False,
|
|
"Received invalid in_process value: foo",
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_update")
|
|
async def test_in_process_template(
|
|
hass: HomeAssistant,
|
|
attribute: str,
|
|
expected: Any,
|
|
error: str | None,
|
|
caplog: pytest.LogCaptureFixture,
|
|
caplog_setup_text: str,
|
|
) -> None:
|
|
"""Test in process templates."""
|
|
# Ensure trigger entities trigger.
|
|
state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.attributes.get(attribute) == expected
|
|
|
|
assert error is None or error in caplog_setup_text or error in caplog.text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"installed_template",
|
|
"latest_template",
|
|
),
|
|
[(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize("attribute", ["release_summary", "title"])
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ True }}", "True"),
|
|
("{{ False }}", "False"),
|
|
("{{ None }}", None),
|
|
("{{ 'foo' }}", "foo"),
|
|
("{{ 1.0 }}", "1.0"),
|
|
("{{ x + 2 }}", None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_update")
|
|
async def test_release_summary_and_title_templates(
|
|
hass: HomeAssistant,
|
|
attribute: str,
|
|
expected: Any,
|
|
) -> None:
|
|
"""Test release summary and title templates."""
|
|
# Ensure trigger entities trigger.
|
|
state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.attributes.get(attribute) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("installed_template", "latest_template", "attribute"),
|
|
[(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE, "release_url")],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected", "error"),
|
|
[
|
|
("{{ 'http://foo.bar' }}", "http://foo.bar", None),
|
|
("{{ 'https://foo.bar' }}", "https://foo.bar", None),
|
|
("{{ None }}", None, None),
|
|
(
|
|
"{{ '/local/thing' }}",
|
|
None,
|
|
"Received invalid release_url: /local/thing",
|
|
),
|
|
(
|
|
"{{ 'foo' }}",
|
|
None,
|
|
"Received invalid release_url: foo",
|
|
),
|
|
(
|
|
"{{ 1.0 }}",
|
|
None,
|
|
"Received invalid release_url: 1",
|
|
),
|
|
(
|
|
"{{ True }}",
|
|
None,
|
|
"Received invalid release_url: True",
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_update")
|
|
async def test_release_url_template(
|
|
hass: HomeAssistant,
|
|
attribute: str,
|
|
expected: Any,
|
|
error: str | None,
|
|
caplog: pytest.LogCaptureFixture,
|
|
caplog_setup_text: str,
|
|
) -> None:
|
|
"""Test release url templates."""
|
|
# Ensure trigger entities trigger.
|
|
state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.attributes.get(attribute) == expected
|
|
|
|
assert error is None or error in caplog_setup_text or error in caplog.text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("installed_template", "latest_template", "attribute"),
|
|
[(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE, "update_percentage")],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected", "error"),
|
|
[
|
|
("{{ 100 }}", 100, None),
|
|
("{{ 0 }}", 0, None),
|
|
("{{ 45 }}", 45, None),
|
|
("{{ None }}", None, None),
|
|
("{{ -1 }}", None, "Received invalid update_percentage: -1"),
|
|
("{{ 101 }}", None, "Received invalid update_percentage: 101"),
|
|
("{{ 'foo' }}", None, "Received invalid update_percentage: foo"),
|
|
("{{ x - 4 }}", None, "UndefinedError: 'x' is undefined"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_update")
|
|
async def test_update_percent_template(
|
|
hass: HomeAssistant,
|
|
attribute: str,
|
|
expected: Any,
|
|
error: str | None,
|
|
caplog: pytest.LogCaptureFixture,
|
|
caplog_setup_text: str,
|
|
) -> None:
|
|
"""Test update percent templates."""
|
|
# Ensure trigger entities trigger.
|
|
state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.attributes.get(attribute) == expected
|
|
|
|
assert error is None or error in caplog_setup_text or error in caplog.text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("installed_template", "latest_template", "attribute", "attribute_template"),
|
|
[
|
|
(
|
|
TEST_INSTALLED_TEMPLATE,
|
|
TEST_LATEST_TEMPLATE,
|
|
"update_percentage",
|
|
"{% set e = 'sensor.test_update' %}{{ states(e) if e | has_value else None }}",
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_update")
|
|
async def test_optimistic_in_progress_with_update_percent_template(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test optimistic in_progress attribute with update percent templates."""
|
|
# Ensure trigger entities trigger.
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.attributes["in_progress"] is False
|
|
assert state.attributes["update_percentage"] is None
|
|
|
|
for i in range(101):
|
|
state = hass.states.async_set(TEST_SENSOR_ID, i)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.attributes["in_progress"] is True
|
|
assert state.attributes["update_percentage"] == i
|
|
|
|
state = hass.states.async_set(TEST_SENSOR_ID, STATE_UNAVAILABLE)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.attributes["in_progress"] is False
|
|
assert state.attributes["update_percentage"] is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"count",
|
|
"installed_template",
|
|
"latest_template",
|
|
),
|
|
[(1, TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"extra_config",
|
|
"supported_feature",
|
|
"action_data",
|
|
"expected_backup",
|
|
"expected_version",
|
|
),
|
|
[
|
|
(
|
|
{"backup": True, **INSTALL_ACTION},
|
|
update.UpdateEntityFeature.BACKUP | update.UpdateEntityFeature.INSTALL,
|
|
{"backup": True},
|
|
True,
|
|
None,
|
|
),
|
|
(
|
|
{"specific_version": True, **INSTALL_ACTION},
|
|
update.UpdateEntityFeature.SPECIFIC_VERSION
|
|
| update.UpdateEntityFeature.INSTALL,
|
|
{"version": "v2.0"},
|
|
False,
|
|
"v2.0",
|
|
),
|
|
(
|
|
{"backup": True, "specific_version": True, **INSTALL_ACTION},
|
|
update.UpdateEntityFeature.SPECIFIC_VERSION
|
|
| update.UpdateEntityFeature.BACKUP
|
|
| update.UpdateEntityFeature.INSTALL,
|
|
{"backup": True, "version": "v2.0"},
|
|
True,
|
|
"v2.0",
|
|
),
|
|
(INSTALL_ACTION, update.UpdateEntityFeature.INSTALL, {}, False, None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_update")
|
|
async def test_supported_features(
|
|
hass: HomeAssistant,
|
|
supported_feature: update.UpdateEntityFeature,
|
|
action_data: dict,
|
|
calls: list[ServiceCall],
|
|
expected_backup: bool,
|
|
expected_version: str | None,
|
|
) -> None:
|
|
"""Test release summary and title templates."""
|
|
# Ensure trigger entities trigger.
|
|
state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.attributes["supported_features"] == supported_feature
|
|
|
|
await hass.services.async_call(
|
|
update.DOMAIN,
|
|
update.SERVICE_INSTALL,
|
|
{"entity_id": TEST_ENTITY_ID, **action_data},
|
|
blocking=True,
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
# verify
|
|
assert len(calls) == 1
|
|
data = calls[-1].data
|
|
assert data["action"] == "install"
|
|
assert data["caller"] == TEST_ENTITY_ID
|
|
assert data["backup"] == expected_backup
|
|
assert data["specific_version"] == expected_version
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("installed_template", "latest_template", "attribute", "attribute_template"),
|
|
[
|
|
(
|
|
TEST_INSTALLED_TEMPLATE,
|
|
TEST_LATEST_TEMPLATE,
|
|
"availability",
|
|
"{{ 'sensor.test_update' | has_value }}",
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_update")
|
|
async def test_available_template_with_entities(hass: HomeAssistant) -> None:
|
|
"""Test availability templates with values from other entities."""
|
|
hass.states.async_set(TEST_SENSOR_ID, STATE_ON)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.state != STATE_UNAVAILABLE
|
|
|
|
hass.states.async_set(TEST_SENSOR_ID, STATE_UNAVAILABLE)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.state == STATE_UNAVAILABLE
|
|
|
|
hass.states.async_set(TEST_SENSOR_ID, STATE_OFF)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.state != STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("installed_template", "latest_template", "attribute", "attribute_template"),
|
|
[
|
|
(
|
|
TEST_INSTALLED_TEMPLATE,
|
|
TEST_LATEST_TEMPLATE,
|
|
"availability",
|
|
"{{ x - 12 }}",
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_update")
|
|
async def test_invalid_availability_template_keeps_component_available(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
caplog_setup_text,
|
|
) -> None:
|
|
"""Test that an invalid availability keeps the device available."""
|
|
# Ensure entity triggers
|
|
hass.states.async_set(TEST_SENSOR_ID, "anything")
|
|
await hass.async_block_till_done()
|
|
|
|
assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE
|
|
|
|
error = "UndefinedError: 'x' is undefined"
|
|
assert error in caplog_setup_text or error in caplog.text
|
|
|
|
|
|
@pytest.mark.parametrize(("count", "domain"), [(1, "template")])
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[
|
|
{
|
|
"template": {
|
|
"trigger": {"platform": "event", "event_type": "test_event"},
|
|
"update": {
|
|
"name": TEST_OBJECT_ID,
|
|
"installed_version": "{{ trigger.event.data.action }}",
|
|
"latest_version": "{{ '1.0.2' }}",
|
|
"picture": "{{ '/local/dogs.png' }}",
|
|
"icon": "{{ 'mdi:pirate' }}",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
)
|
|
async def test_trigger_entity_restore_state(
|
|
hass: HomeAssistant,
|
|
count: int,
|
|
domain: str,
|
|
config: dict,
|
|
) -> None:
|
|
"""Test restoring trigger entities."""
|
|
restored_attributes = {
|
|
"installed_version": "1.0.0",
|
|
"latest_version": "1.0.1",
|
|
"entity_picture": "/local/cats.png",
|
|
"icon": "mdi:ship",
|
|
"skipped_version": "1.0.1",
|
|
}
|
|
fake_state = State(
|
|
TEST_ENTITY_ID,
|
|
STATE_OFF,
|
|
restored_attributes,
|
|
)
|
|
mock_restore_cache_with_extra_data(hass, ((fake_state, {}),))
|
|
with assert_setup_component(count, domain):
|
|
assert await async_setup_component(
|
|
hass,
|
|
domain,
|
|
config,
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
await hass.async_start()
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.state == STATE_OFF
|
|
for attr, value in restored_attributes.items():
|
|
assert state.attributes[attr] == value
|
|
|
|
hass.bus.async_fire("test_event", {"action": "1.0.0"})
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY_ID)
|
|
assert state.state == STATE_ON
|
|
assert state.attributes["icon"] == "mdi:pirate"
|
|
assert state.attributes["entity_picture"] == "/local/dogs.png"
|
|
|
|
|
|
@pytest.mark.parametrize("count", [1])
|
|
@pytest.mark.parametrize(
|
|
("updates", "style"),
|
|
[
|
|
(
|
|
[
|
|
{
|
|
"name": "test_template_event_01",
|
|
**TEST_UNIQUE_ID_CONFIG,
|
|
},
|
|
{
|
|
"name": "test_template_event_02",
|
|
**TEST_UNIQUE_ID_CONFIG,
|
|
},
|
|
],
|
|
ConfigurationStyle.MODERN,
|
|
),
|
|
(
|
|
[
|
|
{
|
|
"name": "test_template_event_01",
|
|
**TEST_UNIQUE_ID_CONFIG,
|
|
},
|
|
{
|
|
"name": "test_template_event_02",
|
|
**TEST_UNIQUE_ID_CONFIG,
|
|
},
|
|
],
|
|
ConfigurationStyle.TRIGGER,
|
|
),
|
|
],
|
|
)
|
|
async def test_unique_id(
|
|
hass: HomeAssistant, count: int, updates: list[dict], style: ConfigurationStyle
|
|
) -> None:
|
|
"""Test unique_id option only creates one update entity per id."""
|
|
config = {"update": updates}
|
|
if style == ConfigurationStyle.TRIGGER:
|
|
config = {**config, **TEST_STATE_TRIGGER}
|
|
with assert_setup_component(count, template.DOMAIN):
|
|
assert await async_setup_component(
|
|
hass,
|
|
template.DOMAIN,
|
|
{"template": config},
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
await hass.async_start()
|
|
await hass.async_block_till_done()
|
|
|
|
assert len(hass.states.async_all("update")) == 1
|
|
|
|
|
|
async def test_nested_unique_id(
|
|
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
|
) -> None:
|
|
"""Test unique_id option creates one update entity per nested id."""
|
|
|
|
with assert_setup_component(1, template.DOMAIN):
|
|
assert await async_setup_component(
|
|
hass,
|
|
template.DOMAIN,
|
|
{
|
|
"template": {
|
|
"unique_id": "x",
|
|
"update": [
|
|
{
|
|
"name": "test_a",
|
|
**TEST_UPDATE_CONFIG,
|
|
"unique_id": "a",
|
|
},
|
|
{
|
|
"name": "test_b",
|
|
**TEST_UPDATE_CONFIG,
|
|
"unique_id": "b",
|
|
},
|
|
],
|
|
}
|
|
},
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
await hass.async_start()
|
|
await hass.async_block_till_done()
|
|
|
|
assert len(hass.states.async_all("update")) == 2
|
|
|
|
entry = entity_registry.async_get("update.test_a")
|
|
assert entry
|
|
assert entry.unique_id == "x-a"
|
|
|
|
entry = entity_registry.async_get("update.test_b")
|
|
assert entry
|
|
assert entry.unique_id == "x-b"
|
|
|
|
|
|
async def test_flow_preview(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
) -> None:
|
|
"""Test the config flow preview."""
|
|
|
|
state = await async_get_flow_preview_state(
|
|
hass,
|
|
hass_ws_client,
|
|
update.DOMAIN,
|
|
{"name": "My template", **TEST_UPDATE_CONFIG},
|
|
)
|
|
|
|
assert state["state"] == STATE_ON
|
|
assert state["attributes"]["installed_version"] == "1.0"
|
|
assert state["attributes"]["latest_version"] == "2.0"
|