Skip to content

Commit 43f40c6

Browse files
authored
Extract issue template functions into an issues Jinja2 extension (#157116)
1 parent 03ac634 commit 43f40c6

File tree

5 files changed

+178
-147
lines changed

5 files changed

+178
-147
lines changed

homeassistant/helpers/template/__init__.py

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,7 @@
5555
valid_entity_id,
5656
)
5757
from homeassistant.exceptions import TemplateError
58-
from homeassistant.helpers import (
59-
entity_registry as er,
60-
issue_registry as ir,
61-
location as loc_helper,
62-
)
58+
from homeassistant.helpers import entity_registry as er, location as loc_helper
6359
from homeassistant.helpers.singleton import singleton
6460
from homeassistant.helpers.translation import async_translate_state
6561
from homeassistant.helpers.typing import TemplateVarsType
@@ -1223,25 +1219,6 @@ def config_entry_attr(
12231219
return getattr(config_entry, attr_name)
12241220

12251221

1226-
def issues(hass: HomeAssistant) -> dict[tuple[str, str], dict[str, Any]]:
1227-
"""Return all open issues."""
1228-
current_issues = ir.async_get(hass).issues
1229-
# Use JSON for safe representation
1230-
return {
1231-
key: issue_entry.to_json()
1232-
for (key, issue_entry) in current_issues.items()
1233-
if issue_entry.active
1234-
}
1235-
1236-
1237-
def issue(hass: HomeAssistant, domain: str, issue_id: str) -> dict[str, Any] | None:
1238-
"""Get issue by domain and issue_id."""
1239-
result = ir.async_get(hass).async_get_issue(domain, issue_id)
1240-
if result:
1241-
return result.to_json()
1242-
return None
1243-
1244-
12451222
def closest(hass: HomeAssistant, *args: Any) -> State | None:
12461223
"""Find closest entity.
12471224
@@ -1896,6 +1873,7 @@ def __init__(
18961873
)
18971874
self.add_extension("homeassistant.helpers.template.extensions.DeviceExtension")
18981875
self.add_extension("homeassistant.helpers.template.extensions.FloorExtension")
1876+
self.add_extension("homeassistant.helpers.template.extensions.IssuesExtension")
18991877
self.add_extension("homeassistant.helpers.template.extensions.LabelExtension")
19001878
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
19011879
self.add_extension("homeassistant.helpers.template.extensions.RegexExtension")
@@ -1982,12 +1960,6 @@ def wrapper(_: Any, *args: _P.args, **kwargs: _P.kwargs) -> _R:
19821960
self.globals["config_entry_id"] = hassfunction(config_entry_id)
19831961
self.filters["config_entry_id"] = self.globals["config_entry_id"]
19841962

1985-
# Issue extensions
1986-
1987-
self.globals["issues"] = hassfunction(issues)
1988-
self.globals["issue"] = hassfunction(issue)
1989-
self.filters["issue"] = self.globals["issue"]
1990-
19911963
if limited:
19921964
# Only device_entities is available to limited templates, mark other
19931965
# functions and filters as unsupported.

homeassistant/helpers/template/extensions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .datetime import DateTimeExtension
88
from .devices import DeviceExtension
99
from .floors import FloorExtension
10+
from .issues import IssuesExtension
1011
from .labels import LabelExtension
1112
from .math import MathExtension
1213
from .regex import RegexExtension
@@ -20,6 +21,7 @@
2021
"DateTimeExtension",
2122
"DeviceExtension",
2223
"FloorExtension",
24+
"IssuesExtension",
2325
"LabelExtension",
2426
"MathExtension",
2527
"RegexExtension",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Issue functions for Home Assistant templates."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, Any
6+
7+
from homeassistant.helpers import issue_registry as ir
8+
9+
from .base import BaseTemplateExtension, TemplateFunction
10+
11+
if TYPE_CHECKING:
12+
from homeassistant.helpers.template import TemplateEnvironment
13+
14+
15+
class IssuesExtension(BaseTemplateExtension):
16+
"""Extension for issue-related template functions."""
17+
18+
def __init__(self, environment: TemplateEnvironment) -> None:
19+
"""Initialize the issues extension."""
20+
super().__init__(
21+
environment,
22+
functions=[
23+
TemplateFunction(
24+
"issues",
25+
self.issues,
26+
as_global=True,
27+
requires_hass=True,
28+
),
29+
TemplateFunction(
30+
"issue",
31+
self.issue,
32+
as_global=True,
33+
as_filter=True,
34+
requires_hass=True,
35+
),
36+
],
37+
)
38+
39+
def issues(self) -> dict[tuple[str, str], dict[str, Any]]:
40+
"""Return all open issues."""
41+
current_issues = ir.async_get(self.hass).issues
42+
# Use JSON for safe representation
43+
return {
44+
key: issue_entry.to_json()
45+
for (key, issue_entry) in current_issues.items()
46+
if issue_entry.active
47+
}
48+
49+
def issue(self, domain: str, issue_id: str) -> dict[str, Any] | None:
50+
"""Get issue by domain and issue_id."""
51+
result = ir.async_get(self.hass).async_get_issue(domain, issue_id)
52+
if result:
53+
return result.to_json()
54+
return None
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Test issue template functions."""
2+
3+
from __future__ import annotations
4+
5+
from homeassistant.core import HomeAssistant
6+
from homeassistant.helpers import issue_registry as ir
7+
from homeassistant.util import dt as dt_util
8+
9+
from tests.helpers.template.helpers import assert_result_info, render_to_info
10+
11+
12+
async def test_issues(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
13+
"""Test issues function."""
14+
# Test no issues
15+
info = render_to_info(hass, "{{ issues() }}")
16+
assert_result_info(info, {})
17+
assert info.rate_limit is None
18+
19+
# Test persistent issue
20+
ir.async_create_issue(
21+
hass,
22+
"test",
23+
"issue 1",
24+
breaks_in_ha_version="2023.7",
25+
is_fixable=True,
26+
is_persistent=True,
27+
learn_more_url="https://theuselessweb.com",
28+
severity="error",
29+
translation_key="abc_1234",
30+
translation_placeholders={"abc": "123"},
31+
)
32+
await hass.async_block_till_done()
33+
created_issue = issue_registry.async_get_issue("test", "issue 1")
34+
info = render_to_info(hass, "{{ issues()['test', 'issue 1'] }}")
35+
assert_result_info(info, created_issue.to_json())
36+
assert info.rate_limit is None
37+
38+
# Test fixed issue
39+
ir.async_delete_issue(hass, "test", "issue 1")
40+
await hass.async_block_till_done()
41+
info = render_to_info(hass, "{{ issues() }}")
42+
assert_result_info(info, {})
43+
assert info.rate_limit is None
44+
45+
issue = ir.IssueEntry(
46+
active=False,
47+
breaks_in_ha_version="2025.12",
48+
created=dt_util.utcnow(),
49+
data=None,
50+
dismissed_version=None,
51+
domain="test",
52+
is_fixable=False,
53+
is_persistent=False,
54+
issue_domain="test",
55+
issue_id="issue 2",
56+
learn_more_url=None,
57+
severity="warning",
58+
translation_key="abc_1234",
59+
translation_placeholders={"abc": "123"},
60+
)
61+
# Add non active issue
62+
issue_registry.issues[("test", "issue 2")] = issue
63+
# Test non active issue is omitted
64+
issue_entry = issue_registry.async_get_issue("test", "issue 2")
65+
assert issue_entry
66+
issue_2_created = issue_entry.created
67+
assert issue_entry and not issue_entry.active
68+
info = render_to_info(hass, "{{ issues() }}")
69+
assert_result_info(info, {})
70+
assert info.rate_limit is None
71+
72+
# Load and activate the issue
73+
ir.async_create_issue(
74+
hass=hass,
75+
breaks_in_ha_version="2025.12",
76+
data=None,
77+
domain="test",
78+
is_fixable=False,
79+
is_persistent=False,
80+
issue_domain="test",
81+
issue_id="issue 2",
82+
learn_more_url=None,
83+
severity="warning",
84+
translation_key="abc_1234",
85+
translation_placeholders={"abc": "123"},
86+
)
87+
activated_issue_entry = issue_registry.async_get_issue("test", "issue 2")
88+
assert activated_issue_entry and activated_issue_entry.active
89+
assert issue_2_created == activated_issue_entry.created
90+
info = render_to_info(hass, "{{ issues()['test', 'issue 2'] }}")
91+
assert_result_info(info, activated_issue_entry.to_json())
92+
assert info.rate_limit is None
93+
94+
95+
async def test_issue(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
96+
"""Test issue function."""
97+
# Test non existent issue
98+
info = render_to_info(hass, "{{ issue('non_existent', 'issue') }}")
99+
assert_result_info(info, None)
100+
assert info.rate_limit is None
101+
102+
# Test existing issue
103+
ir.async_create_issue(
104+
hass,
105+
"test",
106+
"issue 1",
107+
breaks_in_ha_version="2023.7",
108+
is_fixable=True,
109+
is_persistent=True,
110+
learn_more_url="https://theuselessweb.com",
111+
severity="error",
112+
translation_key="abc_1234",
113+
translation_placeholders={"abc": "123"},
114+
)
115+
await hass.async_block_till_done()
116+
created_issue = issue_registry.async_get_issue("test", "issue 1")
117+
info = render_to_info(hass, "{{ issue('test', 'issue 1') }}")
118+
assert_result_info(info, created_issue.to_json())
119+
assert info.rate_limit is None

tests/helpers/template/test_init.py

Lines changed: 1 addition & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,7 @@
3333
)
3434
from homeassistant.core import HomeAssistant
3535
from homeassistant.exceptions import TemplateError
36-
from homeassistant.helpers import (
37-
entity,
38-
entity_registry as er,
39-
issue_registry as ir,
40-
template,
41-
translation,
42-
)
36+
from homeassistant.helpers import entity, entity_registry as er, template, translation
4337
from homeassistant.helpers.entity_platform import EntityPlatform
4438
from homeassistant.helpers.json import json_dumps
4539
from homeassistant.helpers.template.render_info import (
@@ -1762,116 +1756,6 @@ async def test_config_entry_attr(hass: HomeAssistant) -> None:
17621756
)
17631757

17641758

1765-
async def test_issues(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
1766-
"""Test issues function."""
1767-
# Test no issues
1768-
info = render_to_info(hass, "{{ issues() }}")
1769-
assert_result_info(info, {})
1770-
assert info.rate_limit is None
1771-
1772-
# Test persistent issue
1773-
ir.async_create_issue(
1774-
hass,
1775-
"test",
1776-
"issue 1",
1777-
breaks_in_ha_version="2023.7",
1778-
is_fixable=True,
1779-
is_persistent=True,
1780-
learn_more_url="https://theuselessweb.com",
1781-
severity="error",
1782-
translation_key="abc_1234",
1783-
translation_placeholders={"abc": "123"},
1784-
)
1785-
await hass.async_block_till_done()
1786-
created_issue = issue_registry.async_get_issue("test", "issue 1")
1787-
info = render_to_info(hass, "{{ issues()['test', 'issue 1'] }}")
1788-
assert_result_info(info, created_issue.to_json())
1789-
assert info.rate_limit is None
1790-
1791-
# Test fixed issue
1792-
ir.async_delete_issue(hass, "test", "issue 1")
1793-
await hass.async_block_till_done()
1794-
info = render_to_info(hass, "{{ issues() }}")
1795-
assert_result_info(info, {})
1796-
assert info.rate_limit is None
1797-
1798-
issue = ir.IssueEntry(
1799-
active=False,
1800-
breaks_in_ha_version="2025.12",
1801-
created=dt_util.utcnow(),
1802-
data=None,
1803-
dismissed_version=None,
1804-
domain="test",
1805-
is_fixable=False,
1806-
is_persistent=False,
1807-
issue_domain="test",
1808-
issue_id="issue 2",
1809-
learn_more_url=None,
1810-
severity="warning",
1811-
translation_key="abc_1234",
1812-
translation_placeholders={"abc": "123"},
1813-
)
1814-
# Add non active issue
1815-
issue_registry.issues[("test", "issue 2")] = issue
1816-
# Test non active issue is omitted
1817-
issue_entry = issue_registry.async_get_issue("test", "issue 2")
1818-
assert issue_entry
1819-
issue_2_created = issue_entry.created
1820-
assert issue_entry and not issue_entry.active
1821-
info = render_to_info(hass, "{{ issues() }}")
1822-
assert_result_info(info, {})
1823-
assert info.rate_limit is None
1824-
1825-
# Load and activate the issue
1826-
ir.async_create_issue(
1827-
hass=hass,
1828-
breaks_in_ha_version="2025.12",
1829-
data=None,
1830-
domain="test",
1831-
is_fixable=False,
1832-
is_persistent=False,
1833-
issue_domain="test",
1834-
issue_id="issue 2",
1835-
learn_more_url=None,
1836-
severity="warning",
1837-
translation_key="abc_1234",
1838-
translation_placeholders={"abc": "123"},
1839-
)
1840-
activated_issue_entry = issue_registry.async_get_issue("test", "issue 2")
1841-
assert activated_issue_entry and activated_issue_entry.active
1842-
assert issue_2_created == activated_issue_entry.created
1843-
info = render_to_info(hass, "{{ issues()['test', 'issue 2'] }}")
1844-
assert_result_info(info, activated_issue_entry.to_json())
1845-
assert info.rate_limit is None
1846-
1847-
1848-
async def test_issue(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
1849-
"""Test issue function."""
1850-
# Test non existent issue
1851-
info = render_to_info(hass, "{{ issue('non_existent', 'issue') }}")
1852-
assert_result_info(info, None)
1853-
assert info.rate_limit is None
1854-
1855-
# Test existing issue
1856-
ir.async_create_issue(
1857-
hass,
1858-
"test",
1859-
"issue 1",
1860-
breaks_in_ha_version="2023.7",
1861-
is_fixable=True,
1862-
is_persistent=True,
1863-
learn_more_url="https://theuselessweb.com",
1864-
severity="error",
1865-
translation_key="abc_1234",
1866-
translation_placeholders={"abc": "123"},
1867-
)
1868-
await hass.async_block_till_done()
1869-
created_issue = issue_registry.async_get_issue("test", "issue 1")
1870-
info = render_to_info(hass, "{{ issue('test', 'issue 1') }}")
1871-
assert_result_info(info, created_issue.to_json())
1872-
assert info.rate_limit is None
1873-
1874-
18751759
def test_closest_function_to_coord(hass: HomeAssistant) -> None:
18761760
"""Test closest function to coord."""
18771761
hass.states.async_set(

0 commit comments

Comments
 (0)