mirror of
https://github.com/home-assistant/core.git
synced 2026-10-06 14:29:21 -04:00
Don't forget command line ignored shell repair issues (#183765)
This commit is contained in:
@@ -67,7 +67,7 @@ from .const import (
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
from .utils import async_clear_shell_template_issues
|
||||
from .utils import async_prune_shell_template_issues, build_shell_template_issue_id
|
||||
|
||||
BINARY_SENSOR_DEFAULT_NAME = "Binary Command Sensor"
|
||||
DEFAULT_PAYLOAD_ON = "ON"
|
||||
@@ -210,9 +210,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
for reset_platform in reset_platforms:
|
||||
_LOGGER.debug("Reload resetting platform: %s", reset_platform.domain)
|
||||
await reset_platform.async_reset()
|
||||
# Prune stale template deprecation issues; entities still needing one
|
||||
# recreate it on their next update after reload.
|
||||
async_clear_shell_template_issues(hass)
|
||||
# Prune template deprecation issues for entities that no longer exist,
|
||||
# keeping issues for still-configured entities so an ignored issue is not
|
||||
# reset by a delete-and-recreate. Each entity refreshes or clears its own
|
||||
# issue on its next update after reload.
|
||||
valid_issue_ids = _shell_template_issue_ids(
|
||||
reload_config.get(DOMAIN, []) if reload_config else []
|
||||
)
|
||||
async_prune_shell_template_issues(hass, valid_issue_ids)
|
||||
if not reload_config:
|
||||
return
|
||||
await async_load_platforms(hass, reload_config.get(DOMAIN, []), reload_config)
|
||||
@@ -224,6 +229,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _shell_template_issue_ids(
|
||||
command_line_config: list[dict[str, dict[str, Any]]],
|
||||
) -> set[str]:
|
||||
"""Return the shell template deprecation issue ids for the given config.
|
||||
|
||||
Only sensor, binary_sensor and notify run templated commands and can raise
|
||||
the issue. The name mirrors each platform's setup: sensor and binary_sensor
|
||||
always have a name (schema default), while notify falls back to the
|
||||
integration domain when no name is configured.
|
||||
"""
|
||||
issue_ids: set[str] = set()
|
||||
for platform_config in command_line_config:
|
||||
for platform, platform_conf in platform_config.items():
|
||||
if platform == NOTIFY_DOMAIN:
|
||||
name = platform_conf.get(CONF_NAME) or DOMAIN
|
||||
elif platform in (SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN):
|
||||
name = platform_conf[CONF_NAME]
|
||||
else:
|
||||
continue
|
||||
issue_ids.add(build_shell_template_issue_id(platform, name))
|
||||
return issue_ids
|
||||
|
||||
|
||||
async def async_load_platforms(
|
||||
hass: HomeAssistant,
|
||||
command_line_config: list[dict[str, dict[str, Any]]],
|
||||
|
||||
@@ -73,16 +73,38 @@ def _update_issue(
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_clear_shell_template_issues(hass: HomeAssistant) -> None:
|
||||
"""Delete all shell command template deprecation issues.
|
||||
def build_shell_template_issue_id(platform: str, name: str) -> str:
|
||||
"""Build the shell command template deprecation issue id for an entity.
|
||||
|
||||
Called on reload so issues for removed or renamed entities are not left
|
||||
stale. Entities that still need one recreate it on their next update.
|
||||
A hash of the raw name is appended because slugify is not injective (e.g.
|
||||
"Test More" and "Test_(More)" both slugify to "test_more"), which would
|
||||
otherwise let one entity clear another's issue. Hashing the name rather than
|
||||
the command keeps the id stable across command edits so following the repair
|
||||
instructions clears it.
|
||||
"""
|
||||
name_hash = hashlib.sha256(name.encode()).hexdigest()[:8]
|
||||
return f"{_ISSUE_ID_PREFIX}{platform}_{slugify(name)}_{name_hash}"
|
||||
|
||||
|
||||
@callback
|
||||
def async_prune_shell_template_issues(
|
||||
hass: HomeAssistant, valid_issue_ids: set[str]
|
||||
) -> None:
|
||||
"""Delete deprecation issues for entities that no longer exist.
|
||||
|
||||
Called on reload to remove issues left behind by removed or renamed
|
||||
entities. Issues for still-configured entities are kept so a user's decision
|
||||
to ignore an issue survives the reload; each entity refreshes or clears its
|
||||
own issue on its next update. Deleting and later recreating an issue would
|
||||
reset the ignore state, so we never delete an issue we cannot prove is stale.
|
||||
"""
|
||||
registry = ir.async_get(hass)
|
||||
for domain, issue_id in list(registry.issues):
|
||||
if domain == DOMAIN and issue_id.startswith(_ISSUE_ID_PREFIX):
|
||||
if (
|
||||
domain == DOMAIN
|
||||
and issue_id.startswith(_ISSUE_ID_PREFIX)
|
||||
and issue_id not in valid_issue_ids
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_id)
|
||||
|
||||
|
||||
@@ -240,13 +262,8 @@ def render_template_args(
|
||||
|
||||
# Template substitution occurred. Determine the safe execution path.
|
||||
# The name makes the issue id unique per entity so two entities that happen
|
||||
# to share a command string get their own issue. A hash of the raw name is
|
||||
# appended because slugify is not injective (e.g. "Test More" and
|
||||
# "Test_(More)" both slugify to "test_more"), which would otherwise let one
|
||||
# entity clear another's issue. Hashing the name rather than the command
|
||||
# keeps the id stable across command edits so the repair instructions clear it.
|
||||
name_hash = hashlib.sha256(name.encode()).hexdigest()[:8]
|
||||
issue_id = f"{_ISSUE_ID_PREFIX}{platform}_{slugify(name)}_{name_hash}"
|
||||
# to share a command string get their own issue.
|
||||
issue_id = build_shell_template_issue_id(platform, name)
|
||||
|
||||
# Classify and parse the whole command, not just the rendered args, so shell
|
||||
# features and quoting in the executable token are handled too.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
command_line:
|
||||
- "sensor":
|
||||
"name": "Test"
|
||||
"command": "echo {{ states.sensor.input_sensor.state }} | cat"
|
||||
@@ -156,3 +156,52 @@ async def test_reload_prunes_stale_template_issue(
|
||||
issue.translation_key == "shell_command_template_deprecation"
|
||||
for issue in issue_registry.issues.values()
|
||||
)
|
||||
|
||||
|
||||
async def test_reload_keeps_valid_template_issue_and_ignore_state(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""A still-valid deprecation issue and its ignore state survive reload."""
|
||||
hass.states.async_set("sensor.input_sensor", "safe_value")
|
||||
await setup.async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
"command_line": [
|
||||
{
|
||||
"sensor": {
|
||||
"name": "Test",
|
||||
"command": "echo {{ states.sensor.input_sensor.state }} | cat",
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with mock_asyncio_subprocess_run(b"safe_value\n"):
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1))
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
issue = next(
|
||||
entry
|
||||
for entry in issue_registry.issues.values()
|
||||
if entry.translation_key == "shell_command_template_deprecation"
|
||||
)
|
||||
# The user ignores the issue; a delete-and-recreate would reset this.
|
||||
ir.async_ignore_issue(hass, DOMAIN, issue.issue_id, True)
|
||||
assert issue_registry.issues[(DOMAIN, issue.issue_id)].dismissed_version
|
||||
|
||||
# Reload with the same entity still configured.
|
||||
yaml_path = get_fixture_path("configuration_shell_template.yaml", "command_line")
|
||||
with (
|
||||
patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path),
|
||||
mock_asyncio_subprocess_run(b"safe_value\n"),
|
||||
):
|
||||
await hass.services.async_call(DOMAIN, SERVICE_RELOAD, {}, blocking=True)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
kept_issue = issue_registry.issues.get((DOMAIN, issue.issue_id))
|
||||
assert kept_issue is not None
|
||||
assert kept_issue.dismissed_version
|
||||
|
||||
Reference in New Issue
Block a user