124 files

This commit is contained in:
Home Assistant Version Control
2026-08-31 15:02:40 +00:00
parent 33de328a33
commit 9742f385a3
124 changed files with 13907 additions and 1376 deletions
+94 -27
View File
@@ -10,7 +10,7 @@ import importlib
from pathlib import Path
from typing import TYPE_CHECKING, Any, final
from homeassistant.components import blueprint
from homeassistant.components import blueprint, lovelace as lovelace_const
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.homeassistant import SERVICE_HOMEASSISTANT_RESTART
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
@@ -25,6 +25,7 @@ from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
area_registry as ar,
collection,
device_registry as dr,
entity_registry as er,
floor_registry as fr,
@@ -38,6 +39,7 @@ from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util.async_ import create_eager_task
from .const import DOMAIN, LOGGER
from .dashboard_resources import redundant_item_ids
from .entity_filtering import async_filter_known_entity_ids, async_get_all_entity_ids
from .entity_suggestions import async_describe_unknown_entities
@@ -687,31 +689,6 @@ class UnusedLabelFixFlow(_RemoveOrIgnoreFixFlow):
registry.async_delete(thing_id)
class StaleAccessTokenFixFlow(_RemoveOrIgnoreFixFlow):
"""Handler for a stale access token: revoke it, or keep it and stop nagging.
A fixable issue cannot be dismissed from the repairs UI, so keeping a
token you still want is offered explicitly as the "keep it" option.
"""
_key = "token"
_id_key = "stale_access_token_id"
def _menu_placeholders(self) -> dict[str, str]:
"""Name the token, owner, and last-used date in the menu step."""
data = self.data or {}
return {
key: str(data.get(key, "")) for key in ("token", "owner", "last_active")
}
@callback
def _remove(self, thing_id: str) -> None:
"""Revoke the token, if it still exists."""
# The token may already be gone if revoked elsewhere meanwhile.
if token := self.hass.auth.async_get_refresh_token(thing_id):
self.hass.auth.async_remove_refresh_token(token)
class UnusedBlueprintFixFlow(_RemoveOrIgnoreFixFlow):
"""Handler for an unused blueprint: remove it, or keep it and stop nagging.
@@ -748,6 +725,96 @@ class UnusedBlueprintFixFlow(_RemoveOrIgnoreFixFlow):
return self.async_create_entry(data={})
class DuplicateResourceFixFlow(_RemoveOrIgnoreFixFlow):
"""Handler for a dashboard resource listed more than once.
Clearing the copies is async and needs the resource collection, so this
overrides ``async_step_remove`` rather than the synchronous ``_remove``
hook. One copy is kept: the most recently added, which for a card updated
by adding a resource instead of editing one is the version wanted.
"""
_key = "resource"
_id_key = "duplicate_resource_url"
def _menu_placeholders(self) -> dict[str, str]:
"""Name the resource, how many copies, and which URLs.
The inherited version supplies only the name, and this dialog's text
interpolates all three.
"""
data = self.data or {}
return {
"resource": str(data.get("resource", "")),
"resources": str(data.get("resources", "")),
"count": str(data.get("count", "")),
}
async def async_step_init(
self,
_: dict[str, str] | None = None,
) -> FlowResult:
"""Offer the usual menu, unless there is nothing here that can fix it.
Resources listed in YAML are static: nothing in Home Assistant can
delete one, so offering to would be a button that quietly does
nothing. Those get told where the file is instead.
"""
lovelace = self.hass.data.get(lovelace_const.DOMAIN)
resources = lovelace.resources if lovelace is not None else None
if resources is not None and not hasattr(resources, "async_delete_item"):
return self.async_abort(
reason="yaml",
description_placeholders=self._menu_placeholders(),
)
return await super().async_step_init()
async def async_step_remove(
self,
_: dict[str, str] | None = None,
) -> FlowResult:
"""Clear the redundant copies, keeping the most recent."""
key = str((self.data or {}).get(self._id_key, ""))
if (lovelace := self.hass.data.get(lovelace_const.DOMAIN)) is None or (
resources := lovelace.resources
) is None:
return self.async_create_entry(data={})
# A storage collection hands out nothing until it has been loaded, and
# this flow cannot assume the inspection that raised the issue is what
# loaded it. Reading it cold would clear nothing and still say it did.
await resources.async_get_info()
# Worked out again after every deletion rather than once up front.
# Each delete awaits, and somebody editing the same resource in that
# window can take away the copy this was going to keep. Against a
# stale list that ends with every copy gone, which is not what anybody
# asked for.
tried: set[str] = set()
while True:
redundant = [
item_id
for item_id in redundant_item_ids(resources.async_items() or [], key)
if item_id not in tried
]
if not redundant:
break
item_id = redundant[0]
# Remembered whatever happens, so a copy that cannot be deleted
# cannot spin this loop forever either.
tried.add(item_id)
# Somebody clearing it themselves first is a fine ending too.
with suppress(collection.ItemNotFound):
await resources.async_delete_item(item_id)
return self.async_create_entry(data={})
class PersonUnknownDeviceTrackerFixFlow(_RemoveOrIgnoreFixFlow):
"""Handler for a person's unknown device trackers.
@@ -959,11 +1026,11 @@ class HelperUnknownSourcesFixFlow(_RemoveOrIgnoreFixFlow):
# Remove-or-ignore fix flows, keyed by the data field that identifies their
# leftover registry thing.
_REMOVE_OR_IGNORE_FLOWS: dict[str, type[_RemoveOrIgnoreFixFlow]] = {
"stale_access_token_id": StaleAccessTokenFixFlow,
"empty_area_id": EmptyAreaFixFlow,
"empty_floor_id": EmptyFloorFixFlow,
"unused_label_id": UnusedLabelFixFlow,
"unused_blueprint_path": UnusedBlueprintFixFlow,
"duplicate_resource_url": DuplicateResourceFixFlow,
"person_entity_id": PersonUnknownDeviceTrackerFixFlow,
"group_entity_id": GroupUnknownMembersFixFlow,
"min_max_config_entry_id": MinMaxUnknownSourcesFixFlow,