mirror of
https://github.com/home-assistant/core.git
synced 2026-02-14 23:28:42 +00:00
Update template update to new template entity framework (#162561)
This commit is contained in:
@@ -24,16 +24,15 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.template import _SENTINEL
|
||||
from homeassistant.helpers.trigger_template_entity import CONF_PICTURE
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from . import TriggerUpdateCoordinator, validators as template_validators
|
||||
from .const import DOMAIN
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .helpers import (
|
||||
@@ -145,19 +144,49 @@ class AbstractTemplateUpdate(AbstractTemplateEntity, UpdateEntity):
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
|
||||
def __init__(self, name: str, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
|
||||
"""Initialize the features."""
|
||||
|
||||
self._installed_version_template = config[CONF_INSTALLED_VERSION]
|
||||
self._latest_version_template = config[CONF_LATEST_VERSION]
|
||||
|
||||
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
self._in_progress_template = config.get(CONF_IN_PROGRESS)
|
||||
self._release_summary_template = config.get(CONF_RELEASE_SUMMARY)
|
||||
self._release_url_template = config.get(CONF_RELEASE_URL)
|
||||
self._title_template = config.get(CONF_TITLE)
|
||||
self._update_percentage_template = config.get(CONF_UPDATE_PERCENTAGE)
|
||||
# Setup templates.
|
||||
self.setup_template(
|
||||
CONF_INSTALLED_VERSION,
|
||||
"_attr_installed_version",
|
||||
template_validators.string(self, CONF_INSTALLED_VERSION),
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_LATEST_VERSION,
|
||||
"_attr_latest_version",
|
||||
template_validators.string(self, CONF_LATEST_VERSION),
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_IN_PROGRESS,
|
||||
"_attr_in_progress",
|
||||
template_validators.boolean(self, CONF_IN_PROGRESS),
|
||||
self._update_in_progress,
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_RELEASE_SUMMARY,
|
||||
"_attr_release_summary",
|
||||
template_validators.string(self, CONF_RELEASE_SUMMARY),
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_RELEASE_URL,
|
||||
"_attr_release_url",
|
||||
template_validators.url(self, CONF_RELEASE_URL),
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_TITLE,
|
||||
"_attr_title",
|
||||
template_validators.string(self, CONF_TITLE),
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_UPDATE_PERCENTAGE,
|
||||
"_attr_update_percentage",
|
||||
template_validators.number(self, CONF_UPDATE_PERCENTAGE, 0.0, 100.0),
|
||||
self._update_update_percentage,
|
||||
)
|
||||
|
||||
self._attr_supported_features = UpdateEntityFeature(0)
|
||||
if config[CONF_BACKUP]:
|
||||
@@ -165,99 +194,40 @@ class AbstractTemplateUpdate(AbstractTemplateEntity, UpdateEntity):
|
||||
if config[CONF_SPECIFIC_VERSION]:
|
||||
self._attr_supported_features |= UpdateEntityFeature.SPECIFIC_VERSION
|
||||
if (
|
||||
self._in_progress_template is not None
|
||||
or self._update_percentage_template is not None
|
||||
CONF_IN_PROGRESS in self._templates
|
||||
or CONF_UPDATE_PERCENTAGE in self._templates
|
||||
):
|
||||
self._attr_supported_features |= UpdateEntityFeature.PROGRESS
|
||||
|
||||
self._optimistic_in_process = (
|
||||
self._in_progress_template is None
|
||||
and self._update_percentage_template is not None
|
||||
CONF_IN_PROGRESS not in self._templates
|
||||
and CONF_UPDATE_PERCENTAGE in self._templates
|
||||
)
|
||||
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (install_action := config.get(CONF_INSTALL)) is not None:
|
||||
self.add_script(CONF_INSTALL, install_action, name, DOMAIN)
|
||||
self._attr_supported_features |= UpdateEntityFeature.INSTALL
|
||||
|
||||
@callback
|
||||
def _update_installed_version(self, result: Any) -> None:
|
||||
def _update_in_progress(self, result: bool | None) -> None:
|
||||
if result is None:
|
||||
self._attr_installed_version = None
|
||||
return
|
||||
|
||||
self._attr_installed_version = cv.string(result)
|
||||
|
||||
@callback
|
||||
def _update_latest_version(self, result: Any) -> None:
|
||||
if result is None:
|
||||
self._attr_latest_version = None
|
||||
return
|
||||
|
||||
self._attr_latest_version = cv.string(result)
|
||||
|
||||
@callback
|
||||
def _update_in_process(self, result: Any) -> None:
|
||||
try:
|
||||
self._attr_in_progress = cv.boolean(result)
|
||||
except vol.Invalid:
|
||||
_LOGGER.error(
|
||||
"Received invalid in_process value: %s for entity %s. Expected: True, False",
|
||||
result,
|
||||
self.entity_id,
|
||||
template_validators.log_validation_result_error(
|
||||
self, CONF_IN_PROGRESS, result, "expected a boolean"
|
||||
)
|
||||
self._attr_in_progress = False
|
||||
self._attr_in_progress = result or False
|
||||
|
||||
@callback
|
||||
def _update_release_summary(self, result: Any) -> None:
|
||||
if result is None:
|
||||
self._attr_release_summary = None
|
||||
return
|
||||
|
||||
self._attr_release_summary = cv.string(result)
|
||||
|
||||
@callback
|
||||
def _update_release_url(self, result: Any) -> None:
|
||||
if result is None:
|
||||
self._attr_release_url = None
|
||||
return
|
||||
|
||||
try:
|
||||
self._attr_release_url = cv.url(result)
|
||||
except vol.Invalid:
|
||||
_LOGGER.error(
|
||||
"Received invalid release_url: %s for entity %s",
|
||||
result,
|
||||
self.entity_id,
|
||||
)
|
||||
self._attr_release_url = None
|
||||
|
||||
@callback
|
||||
def _update_title(self, result: Any) -> None:
|
||||
if result is None:
|
||||
self._attr_title = None
|
||||
return
|
||||
|
||||
self._attr_title = cv.string(result)
|
||||
|
||||
@callback
|
||||
def _update_update_percentage(self, result: Any) -> None:
|
||||
def _update_update_percentage(self, result: float | None) -> None:
|
||||
if result is None:
|
||||
if self._optimistic_in_process:
|
||||
self._attr_in_progress = False
|
||||
self._attr_update_percentage = None
|
||||
return
|
||||
|
||||
try:
|
||||
percentage = vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(0, 100, min_included=True, max_included=True),
|
||||
)(result)
|
||||
if self._optimistic_in_process:
|
||||
self._attr_in_progress = True
|
||||
self._attr_update_percentage = percentage
|
||||
except vol.Invalid:
|
||||
_LOGGER.error(
|
||||
"Received invalid update_percentage: %s for entity %s",
|
||||
result,
|
||||
self.entity_id,
|
||||
)
|
||||
self._attr_update_percentage = None
|
||||
if self._optimistic_in_process:
|
||||
self._attr_in_progress = True
|
||||
self._attr_update_percentage = result
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
@@ -283,16 +253,10 @@ class StateUpdateEntity(TemplateEntity, AbstractTemplateUpdate):
|
||||
) -> None:
|
||||
"""Initialize the Template update."""
|
||||
TemplateEntity.__init__(self, hass, config, unique_id)
|
||||
AbstractTemplateUpdate.__init__(self, config)
|
||||
|
||||
name = self._attr_name
|
||||
if TYPE_CHECKING:
|
||||
assert name is not None
|
||||
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (install_action := config.get(CONF_INSTALL)) is not None:
|
||||
self.add_script(CONF_INSTALL, install_action, name, DOMAIN)
|
||||
self._attr_supported_features |= UpdateEntityFeature.INSTALL
|
||||
AbstractTemplateUpdate.__init__(self, name, config)
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
@@ -305,65 +269,6 @@ class StateUpdateEntity(TemplateEntity, AbstractTemplateUpdate):
|
||||
return "https://brands.home-assistant.io/_/template/icon.png"
|
||||
return self._attr_entity_picture
|
||||
|
||||
@callback
|
||||
def _async_setup_templates(self) -> None:
|
||||
"""Set up templates."""
|
||||
self.add_template_attribute(
|
||||
"_attr_installed_version",
|
||||
self._installed_version_template,
|
||||
None,
|
||||
self._update_installed_version,
|
||||
none_on_template_error=True,
|
||||
)
|
||||
self.add_template_attribute(
|
||||
"_attr_latest_version",
|
||||
self._latest_version_template,
|
||||
None,
|
||||
self._update_latest_version,
|
||||
none_on_template_error=True,
|
||||
)
|
||||
if self._in_progress_template is not None:
|
||||
self.add_template_attribute(
|
||||
"_attr_in_progress",
|
||||
self._in_progress_template,
|
||||
None,
|
||||
self._update_in_process,
|
||||
none_on_template_error=True,
|
||||
)
|
||||
if self._release_summary_template is not None:
|
||||
self.add_template_attribute(
|
||||
"_attr_release_summary",
|
||||
self._release_summary_template,
|
||||
None,
|
||||
self._update_release_summary,
|
||||
none_on_template_error=True,
|
||||
)
|
||||
if self._release_url_template is not None:
|
||||
self.add_template_attribute(
|
||||
"_attr_release_url",
|
||||
self._release_url_template,
|
||||
None,
|
||||
self._update_release_url,
|
||||
none_on_template_error=True,
|
||||
)
|
||||
if self._title_template is not None:
|
||||
self.add_template_attribute(
|
||||
"_attr_title",
|
||||
self._title_template,
|
||||
None,
|
||||
self._update_title,
|
||||
none_on_template_error=True,
|
||||
)
|
||||
if self._update_percentage_template is not None:
|
||||
self.add_template_attribute(
|
||||
"_attr_update_percentage",
|
||||
self._update_percentage_template,
|
||||
None,
|
||||
self._update_update_percentage,
|
||||
none_on_template_error=True,
|
||||
)
|
||||
super()._async_setup_templates()
|
||||
|
||||
|
||||
class TriggerUpdateEntity(TriggerEntity, AbstractTemplateUpdate):
|
||||
"""Update entity based on trigger data."""
|
||||
@@ -378,35 +283,8 @@ class TriggerUpdateEntity(TriggerEntity, AbstractTemplateUpdate):
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
AbstractTemplateUpdate.__init__(self, config)
|
||||
|
||||
for key in (
|
||||
CONF_INSTALLED_VERSION,
|
||||
CONF_LATEST_VERSION,
|
||||
):
|
||||
self._to_render_simple.append(key)
|
||||
self._parse_result.add(key)
|
||||
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (install_action := config.get(CONF_INSTALL)) is not None:
|
||||
self.add_script(
|
||||
CONF_INSTALL,
|
||||
install_action,
|
||||
self._rendered.get(CONF_NAME, DEFAULT_NAME),
|
||||
DOMAIN,
|
||||
)
|
||||
self._attr_supported_features |= UpdateEntityFeature.INSTALL
|
||||
|
||||
for key in (
|
||||
CONF_IN_PROGRESS,
|
||||
CONF_RELEASE_SUMMARY,
|
||||
CONF_RELEASE_URL,
|
||||
CONF_TITLE,
|
||||
CONF_UPDATE_PERCENTAGE,
|
||||
):
|
||||
if isinstance(config.get(key), template.Template):
|
||||
self._to_render_simple.append(key)
|
||||
self._parse_result.add(key)
|
||||
name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
|
||||
AbstractTemplateUpdate.__init__(self, name, config)
|
||||
|
||||
# Ensure the entity picture can resolve None to produce the default picture.
|
||||
if CONF_PICTURE in config:
|
||||
@@ -431,32 +309,3 @@ class TriggerUpdateEntity(TriggerEntity, AbstractTemplateUpdate):
|
||||
if (picture := self._rendered.get(CONF_PICTURE)) is None:
|
||||
return UpdateEntity.entity_picture.fget(self) # type: ignore[attr-defined]
|
||||
return picture
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle update of the data."""
|
||||
self._process_data()
|
||||
|
||||
if not self.available:
|
||||
return
|
||||
|
||||
write_ha_state = False
|
||||
for key, updater in (
|
||||
(CONF_INSTALLED_VERSION, self._update_installed_version),
|
||||
(CONF_LATEST_VERSION, self._update_latest_version),
|
||||
(CONF_IN_PROGRESS, self._update_in_process),
|
||||
(CONF_RELEASE_SUMMARY, self._update_release_summary),
|
||||
(CONF_RELEASE_URL, self._update_release_url),
|
||||
(CONF_TITLE, self._update_title),
|
||||
(CONF_UPDATE_PERCENTAGE, self._update_update_percentage),
|
||||
):
|
||||
if (rendered := self._rendered.get(key, _SENTINEL)) is not _SENTINEL:
|
||||
updater(rendered)
|
||||
write_ha_state = True
|
||||
|
||||
if len(self._rendered) > 0:
|
||||
# In case any non optimistic template
|
||||
write_ha_state = True
|
||||
|
||||
if write_ha_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -308,3 +308,56 @@ def item_in_list[T](
|
||||
return result
|
||||
|
||||
return convert
|
||||
|
||||
|
||||
def url(
|
||||
entity: Entity,
|
||||
attribute: str,
|
||||
**kwargs: Any,
|
||||
) -> Callable[[Any], str | None]:
|
||||
"""Convert the result to a string url or None."""
|
||||
|
||||
def convert(result: Any) -> str | None:
|
||||
if check_result_for_none(result, **kwargs):
|
||||
return None
|
||||
|
||||
try:
|
||||
return cv.url(result)
|
||||
except vol.Invalid:
|
||||
log_validation_result_error(
|
||||
entity,
|
||||
attribute,
|
||||
result,
|
||||
"expected a url",
|
||||
)
|
||||
return None
|
||||
|
||||
return convert
|
||||
|
||||
|
||||
def string(
|
||||
entity: Entity,
|
||||
attribute: str,
|
||||
**kwargs: Any,
|
||||
) -> Callable[[Any], str | None]:
|
||||
"""Convert the result to a string or None."""
|
||||
|
||||
def convert(result: Any) -> str | None:
|
||||
if check_result_for_none(result, **kwargs):
|
||||
return None
|
||||
|
||||
if isinstance(result, str):
|
||||
return result
|
||||
|
||||
try:
|
||||
return cv.string(result)
|
||||
except vol.Invalid:
|
||||
log_validation_result_error(
|
||||
entity,
|
||||
attribute,
|
||||
result,
|
||||
"expected a string",
|
||||
)
|
||||
return None
|
||||
|
||||
return convert
|
||||
|
||||
@@ -540,11 +540,11 @@ async def test_entity_picture_uses_default(hass: HomeAssistant) -> None:
|
||||
[
|
||||
("{{ True }}", True, None),
|
||||
("{{ False }}", False, None),
|
||||
("{{ None }}", False, "Received invalid in_process value: None"),
|
||||
("{{ None }}", False, "Received invalid update in_progress: None"),
|
||||
(
|
||||
"{{ 'foo' }}",
|
||||
False,
|
||||
"Received invalid in_process value: foo",
|
||||
"Received invalid update in_progress: foo",
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -621,22 +621,22 @@ async def test_release_summary_and_title_templates(
|
||||
(
|
||||
"{{ '/local/thing' }}",
|
||||
None,
|
||||
"Received invalid release_url: /local/thing",
|
||||
"Received invalid update release_url: /local/thing",
|
||||
),
|
||||
(
|
||||
"{{ 'foo' }}",
|
||||
None,
|
||||
"Received invalid release_url: foo",
|
||||
"Received invalid update release_url: foo",
|
||||
),
|
||||
(
|
||||
"{{ 1.0 }}",
|
||||
None,
|
||||
"Received invalid release_url: 1",
|
||||
"Received invalid update release_url: 1",
|
||||
),
|
||||
(
|
||||
"{{ True }}",
|
||||
None,
|
||||
"Received invalid release_url: True",
|
||||
"Received invalid update release_url: True",
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -674,9 +674,9 @@ async def test_release_url_template(
|
||||
("{{ 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"),
|
||||
("{{ -1 }}", None, "Received invalid update update_percentage: -1"),
|
||||
("{{ 101 }}", None, "Received invalid update update_percentage: 101"),
|
||||
("{{ 'foo' }}", None, "Received invalid update update_percentage: foo"),
|
||||
("{{ x - 4 }}", None, "UndefinedError: 'x' is undefined"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -922,3 +922,100 @@ async def test_empty_items_in_list(
|
||||
value = cv.item_in_list(entity, "state", the_list, "bar")(value)
|
||||
assert value == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected a url",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected a url",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("http://foo.bar", "http://foo.bar"),
|
||||
("https://foo.bar", "https://foo.bar"),
|
||||
*expect_none(
|
||||
None,
|
||||
"/local/thing",
|
||||
"beeal;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"7",
|
||||
"-1",
|
||||
True,
|
||||
False,
|
||||
1,
|
||||
1.0,
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_url(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test enum validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
assert cv.url(entity, "state")(value) == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected a string",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected a string",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("a string", "a string"),
|
||||
(True, "True"),
|
||||
(False, "False"),
|
||||
(1, "1"),
|
||||
(1.0, "1.0"),
|
||||
*expect_none(
|
||||
None,
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_string(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test enum validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
assert cv.string(entity, "state")(value) == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
Reference in New Issue
Block a user