mirror of
https://github.com/home-assistant/core.git
synced 2025-12-22 11:59:34 +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,
|
valid_entity_id,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import entity_registry as er, location as loc_helper
|
||||||
entity_registry as er,
|
|
||||||
issue_registry as ir,
|
|
||||||
location as loc_helper,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.singleton import singleton
|
from homeassistant.helpers.singleton import singleton
|
||||||
from homeassistant.helpers.translation import async_translate_state
|
from homeassistant.helpers.translation import async_translate_state
|
||||||
from homeassistant.helpers.typing import TemplateVarsType
|
from homeassistant.helpers.typing import TemplateVarsType
|
||||||
@@ -1223,25 +1219,6 @@ def config_entry_attr(
|
|||||||
return getattr(config_entry, attr_name)
|
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:
|
def closest(hass: HomeAssistant, *args: Any) -> State | None:
|
||||||
"""Find closest entity.
|
"""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.DeviceExtension")
|
||||||
self.add_extension("homeassistant.helpers.template.extensions.FloorExtension")
|
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.LabelExtension")
|
||||||
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
|
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
|
||||||
self.add_extension("homeassistant.helpers.template.extensions.RegexExtension")
|
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.globals["config_entry_id"] = hassfunction(config_entry_id)
|
||||||
self.filters["config_entry_id"] = self.globals["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:
|
if limited:
|
||||||
# Only device_entities is available to limited templates, mark other
|
# Only device_entities is available to limited templates, mark other
|
||||||
# functions and filters as unsupported.
|
# functions and filters as unsupported.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from .crypto import CryptoExtension
|
|||||||
from .datetime import DateTimeExtension
|
from .datetime import DateTimeExtension
|
||||||
from .devices import DeviceExtension
|
from .devices import DeviceExtension
|
||||||
from .floors import FloorExtension
|
from .floors import FloorExtension
|
||||||
|
from .issues import IssuesExtension
|
||||||
from .labels import LabelExtension
|
from .labels import LabelExtension
|
||||||
from .math import MathExtension
|
from .math import MathExtension
|
||||||
from .regex import RegexExtension
|
from .regex import RegexExtension
|
||||||
@@ -20,6 +21,7 @@ __all__ = [
|
|||||||
"DateTimeExtension",
|
"DateTimeExtension",
|
||||||
"DeviceExtension",
|
"DeviceExtension",
|
||||||
"FloorExtension",
|
"FloorExtension",
|
||||||
|
"IssuesExtension",
|
||||||
"LabelExtension",
|
"LabelExtension",
|
||||||
"MathExtension",
|
"MathExtension",
|
||||||
"RegexExtension",
|
"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.core import HomeAssistant
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import entity, entity_registry as er, template, translation
|
||||||
entity,
|
|
||||||
entity_registry as er,
|
|
||||||
issue_registry as ir,
|
|
||||||
template,
|
|
||||||
translation,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||||
from homeassistant.helpers.json import json_dumps
|
from homeassistant.helpers.json import json_dumps
|
||||||
from homeassistant.helpers.template.render_info import (
|
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:
|
def test_closest_function_to_coord(hass: HomeAssistant) -> None:
|
||||||
"""Test closest function to coord."""
|
"""Test closest function to coord."""
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
|||||||
Reference in New Issue
Block a user