mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 02:48:57 +00:00
Extract issue template functions into an issues Jinja2 extension (#157116)
This commit is contained in:
@@ -55,11 +55,7 @@ from homeassistant.core import (
|
||||
valid_entity_id,
|
||||
)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import (
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
location as loc_helper,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry as er, location as loc_helper
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.translation import async_translate_state
|
||||
from homeassistant.helpers.typing import TemplateVarsType
|
||||
@@ -1223,25 +1219,6 @@ def config_entry_attr(
|
||||
return getattr(config_entry, attr_name)
|
||||
|
||||
|
||||
def issues(hass: HomeAssistant) -> dict[tuple[str, str], dict[str, Any]]:
|
||||
"""Return all open issues."""
|
||||
current_issues = ir.async_get(hass).issues
|
||||
# Use JSON for safe representation
|
||||
return {
|
||||
key: issue_entry.to_json()
|
||||
for (key, issue_entry) in current_issues.items()
|
||||
if issue_entry.active
|
||||
}
|
||||
|
||||
|
||||
def issue(hass: HomeAssistant, domain: str, issue_id: str) -> dict[str, Any] | None:
|
||||
"""Get issue by domain and issue_id."""
|
||||
result = ir.async_get(hass).async_get_issue(domain, issue_id)
|
||||
if result:
|
||||
return result.to_json()
|
||||
return None
|
||||
|
||||
|
||||
def closest(hass: HomeAssistant, *args: Any) -> State | None:
|
||||
"""Find closest entity.
|
||||
|
||||
@@ -1896,6 +1873,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
)
|
||||
self.add_extension("homeassistant.helpers.template.extensions.DeviceExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.FloorExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.IssuesExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.LabelExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.RegexExtension")
|
||||
@@ -1982,12 +1960,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.globals["config_entry_id"] = hassfunction(config_entry_id)
|
||||
self.filters["config_entry_id"] = self.globals["config_entry_id"]
|
||||
|
||||
# Issue extensions
|
||||
|
||||
self.globals["issues"] = hassfunction(issues)
|
||||
self.globals["issue"] = hassfunction(issue)
|
||||
self.filters["issue"] = self.globals["issue"]
|
||||
|
||||
if limited:
|
||||
# Only device_entities is available to limited templates, mark other
|
||||
# functions and filters as unsupported.
|
||||
|
||||
@@ -7,6 +7,7 @@ from .crypto import CryptoExtension
|
||||
from .datetime import DateTimeExtension
|
||||
from .devices import DeviceExtension
|
||||
from .floors import FloorExtension
|
||||
from .issues import IssuesExtension
|
||||
from .labels import LabelExtension
|
||||
from .math import MathExtension
|
||||
from .regex import RegexExtension
|
||||
@@ -20,6 +21,7 @@ __all__ = [
|
||||
"DateTimeExtension",
|
||||
"DeviceExtension",
|
||||
"FloorExtension",
|
||||
"IssuesExtension",
|
||||
"LabelExtension",
|
||||
"MathExtension",
|
||||
"RegexExtension",
|
||||
|
||||
54
homeassistant/helpers/template/extensions/issues.py
Normal file
54
homeassistant/helpers/template/extensions/issues.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Issue functions for Home Assistant templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from .base import BaseTemplateExtension, TemplateFunction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.helpers.template import TemplateEnvironment
|
||||
|
||||
|
||||
class IssuesExtension(BaseTemplateExtension):
|
||||
"""Extension for issue-related template functions."""
|
||||
|
||||
def __init__(self, environment: TemplateEnvironment) -> None:
|
||||
"""Initialize the issues extension."""
|
||||
super().__init__(
|
||||
environment,
|
||||
functions=[
|
||||
TemplateFunction(
|
||||
"issues",
|
||||
self.issues,
|
||||
as_global=True,
|
||||
requires_hass=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"issue",
|
||||
self.issue,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
requires_hass=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def issues(self) -> dict[tuple[str, str], dict[str, Any]]:
|
||||
"""Return all open issues."""
|
||||
current_issues = ir.async_get(self.hass).issues
|
||||
# Use JSON for safe representation
|
||||
return {
|
||||
key: issue_entry.to_json()
|
||||
for (key, issue_entry) in current_issues.items()
|
||||
if issue_entry.active
|
||||
}
|
||||
|
||||
def issue(self, domain: str, issue_id: str) -> dict[str, Any] | None:
|
||||
"""Get issue by domain and issue_id."""
|
||||
result = ir.async_get(self.hass).async_get_issue(domain, issue_id)
|
||||
if result:
|
||||
return result.to_json()
|
||||
return None
|
||||
119
tests/helpers/template/extensions/test_issues.py
Normal file
119
tests/helpers/template/extensions/test_issues.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Test issue template functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.helpers.template.helpers import assert_result_info, render_to_info
|
||||
|
||||
|
||||
async def test_issues(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
|
||||
"""Test issues function."""
|
||||
# Test no issues
|
||||
info = render_to_info(hass, "{{ issues() }}")
|
||||
assert_result_info(info, {})
|
||||
assert info.rate_limit is None
|
||||
|
||||
# Test persistent issue
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"test",
|
||||
"issue 1",
|
||||
breaks_in_ha_version="2023.7",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
learn_more_url="https://theuselessweb.com",
|
||||
severity="error",
|
||||
translation_key="abc_1234",
|
||||
translation_placeholders={"abc": "123"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
created_issue = issue_registry.async_get_issue("test", "issue 1")
|
||||
info = render_to_info(hass, "{{ issues()['test', 'issue 1'] }}")
|
||||
assert_result_info(info, created_issue.to_json())
|
||||
assert info.rate_limit is None
|
||||
|
||||
# Test fixed issue
|
||||
ir.async_delete_issue(hass, "test", "issue 1")
|
||||
await hass.async_block_till_done()
|
||||
info = render_to_info(hass, "{{ issues() }}")
|
||||
assert_result_info(info, {})
|
||||
assert info.rate_limit is None
|
||||
|
||||
issue = ir.IssueEntry(
|
||||
active=False,
|
||||
breaks_in_ha_version="2025.12",
|
||||
created=dt_util.utcnow(),
|
||||
data=None,
|
||||
dismissed_version=None,
|
||||
domain="test",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
issue_domain="test",
|
||||
issue_id="issue 2",
|
||||
learn_more_url=None,
|
||||
severity="warning",
|
||||
translation_key="abc_1234",
|
||||
translation_placeholders={"abc": "123"},
|
||||
)
|
||||
# Add non active issue
|
||||
issue_registry.issues[("test", "issue 2")] = issue
|
||||
# Test non active issue is omitted
|
||||
issue_entry = issue_registry.async_get_issue("test", "issue 2")
|
||||
assert issue_entry
|
||||
issue_2_created = issue_entry.created
|
||||
assert issue_entry and not issue_entry.active
|
||||
info = render_to_info(hass, "{{ issues() }}")
|
||||
assert_result_info(info, {})
|
||||
assert info.rate_limit is None
|
||||
|
||||
# Load and activate the issue
|
||||
ir.async_create_issue(
|
||||
hass=hass,
|
||||
breaks_in_ha_version="2025.12",
|
||||
data=None,
|
||||
domain="test",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
issue_domain="test",
|
||||
issue_id="issue 2",
|
||||
learn_more_url=None,
|
||||
severity="warning",
|
||||
translation_key="abc_1234",
|
||||
translation_placeholders={"abc": "123"},
|
||||
)
|
||||
activated_issue_entry = issue_registry.async_get_issue("test", "issue 2")
|
||||
assert activated_issue_entry and activated_issue_entry.active
|
||||
assert issue_2_created == activated_issue_entry.created
|
||||
info = render_to_info(hass, "{{ issues()['test', 'issue 2'] }}")
|
||||
assert_result_info(info, activated_issue_entry.to_json())
|
||||
assert info.rate_limit is None
|
||||
|
||||
|
||||
async def test_issue(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
|
||||
"""Test issue function."""
|
||||
# Test non existent issue
|
||||
info = render_to_info(hass, "{{ issue('non_existent', 'issue') }}")
|
||||
assert_result_info(info, None)
|
||||
assert info.rate_limit is None
|
||||
|
||||
# Test existing issue
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"test",
|
||||
"issue 1",
|
||||
breaks_in_ha_version="2023.7",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
learn_more_url="https://theuselessweb.com",
|
||||
severity="error",
|
||||
translation_key="abc_1234",
|
||||
translation_placeholders={"abc": "123"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
created_issue = issue_registry.async_get_issue("test", "issue 1")
|
||||
info = render_to_info(hass, "{{ issue('test', 'issue 1') }}")
|
||||
assert_result_info(info, created_issue.to_json())
|
||||
assert info.rate_limit is None
|
||||
@@ -33,13 +33,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import (
|
||||
entity,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
template,
|
||||
translation,
|
||||
)
|
||||
from homeassistant.helpers import entity, entity_registry as er, template, translation
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.helpers.template.render_info import (
|
||||
@@ -1762,116 +1756,6 @@ async def test_config_entry_attr(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_issues(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
|
||||
"""Test issues function."""
|
||||
# Test no issues
|
||||
info = render_to_info(hass, "{{ issues() }}")
|
||||
assert_result_info(info, {})
|
||||
assert info.rate_limit is None
|
||||
|
||||
# Test persistent issue
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"test",
|
||||
"issue 1",
|
||||
breaks_in_ha_version="2023.7",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
learn_more_url="https://theuselessweb.com",
|
||||
severity="error",
|
||||
translation_key="abc_1234",
|
||||
translation_placeholders={"abc": "123"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
created_issue = issue_registry.async_get_issue("test", "issue 1")
|
||||
info = render_to_info(hass, "{{ issues()['test', 'issue 1'] }}")
|
||||
assert_result_info(info, created_issue.to_json())
|
||||
assert info.rate_limit is None
|
||||
|
||||
# Test fixed issue
|
||||
ir.async_delete_issue(hass, "test", "issue 1")
|
||||
await hass.async_block_till_done()
|
||||
info = render_to_info(hass, "{{ issues() }}")
|
||||
assert_result_info(info, {})
|
||||
assert info.rate_limit is None
|
||||
|
||||
issue = ir.IssueEntry(
|
||||
active=False,
|
||||
breaks_in_ha_version="2025.12",
|
||||
created=dt_util.utcnow(),
|
||||
data=None,
|
||||
dismissed_version=None,
|
||||
domain="test",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
issue_domain="test",
|
||||
issue_id="issue 2",
|
||||
learn_more_url=None,
|
||||
severity="warning",
|
||||
translation_key="abc_1234",
|
||||
translation_placeholders={"abc": "123"},
|
||||
)
|
||||
# Add non active issue
|
||||
issue_registry.issues[("test", "issue 2")] = issue
|
||||
# Test non active issue is omitted
|
||||
issue_entry = issue_registry.async_get_issue("test", "issue 2")
|
||||
assert issue_entry
|
||||
issue_2_created = issue_entry.created
|
||||
assert issue_entry and not issue_entry.active
|
||||
info = render_to_info(hass, "{{ issues() }}")
|
||||
assert_result_info(info, {})
|
||||
assert info.rate_limit is None
|
||||
|
||||
# Load and activate the issue
|
||||
ir.async_create_issue(
|
||||
hass=hass,
|
||||
breaks_in_ha_version="2025.12",
|
||||
data=None,
|
||||
domain="test",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
issue_domain="test",
|
||||
issue_id="issue 2",
|
||||
learn_more_url=None,
|
||||
severity="warning",
|
||||
translation_key="abc_1234",
|
||||
translation_placeholders={"abc": "123"},
|
||||
)
|
||||
activated_issue_entry = issue_registry.async_get_issue("test", "issue 2")
|
||||
assert activated_issue_entry and activated_issue_entry.active
|
||||
assert issue_2_created == activated_issue_entry.created
|
||||
info = render_to_info(hass, "{{ issues()['test', 'issue 2'] }}")
|
||||
assert_result_info(info, activated_issue_entry.to_json())
|
||||
assert info.rate_limit is None
|
||||
|
||||
|
||||
async def test_issue(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
|
||||
"""Test issue function."""
|
||||
# Test non existent issue
|
||||
info = render_to_info(hass, "{{ issue('non_existent', 'issue') }}")
|
||||
assert_result_info(info, None)
|
||||
assert info.rate_limit is None
|
||||
|
||||
# Test existing issue
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"test",
|
||||
"issue 1",
|
||||
breaks_in_ha_version="2023.7",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
learn_more_url="https://theuselessweb.com",
|
||||
severity="error",
|
||||
translation_key="abc_1234",
|
||||
translation_placeholders={"abc": "123"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
created_issue = issue_registry.async_get_issue("test", "issue 1")
|
||||
info = render_to_info(hass, "{{ issue('test', 'issue 1') }}")
|
||||
assert_result_info(info, created_issue.to_json())
|
||||
assert info.rate_limit is None
|
||||
|
||||
|
||||
def test_closest_function_to_coord(hass: HomeAssistant) -> None:
|
||||
"""Test closest function to coord."""
|
||||
hass.states.async_set(
|
||||
|
||||
Reference in New Issue
Block a user