1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 10:59:24 +00:00

Extract issue template functions into an issues Jinja2 extension (#157116)

This commit is contained in:
Franck Nijhof
2025-11-23 19:14:46 +01:00
committed by GitHub
parent 03ac634e6d
commit 43f40c6f0e
5 changed files with 178 additions and 147 deletions

View File

@@ -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.

View File

@@ -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",

View 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

View 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

View File

@@ -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(