fix(review): fix(review): lead repair notice with warning, cap entity list
parent
c510b64260
commit
0407cb049d
|
|
@ -32,6 +32,7 @@ from __future__ import annotations
|
|||
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import ATTR_RESTORED, STATE_UNKNOWN
|
||||
|
|
@ -44,6 +45,9 @@ from .data import build_device_info
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# How many stranded entities the repair notice names before it falls back to the count.
|
||||
ISSUE_ENTITY_SAMPLE: Final = 5
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AdoptionResult:
|
||||
|
|
@ -201,6 +205,18 @@ def _adopt_devices(hass: HomeAssistant, entry: ConfigEntry, old_entry: ConfigEnt
|
|||
`entity_registry.async_device_modified` only deletes entities whose `config_entry_id`
|
||||
is one of the removed device's own, and by this point the adopted ones point at this
|
||||
entry.
|
||||
|
||||
That count is taken before our platforms run, so it catches a device that is already
|
||||
empty when adoption starts, but not one adoption itself empties. A predecessor still
|
||||
on v2.0.0pre1 arrives with its per-channel Ecowitt device still holding entities, so
|
||||
it reads as populated and gets this entry attached; its entities are then adopted and
|
||||
`async_forward_entry_setups` re-homes them onto the canonical device, because
|
||||
`EcowittBridge` and `WeatherSensor` both take their `device_info` from
|
||||
`build_device_info`. The row is left empty, still referenced by a live config entry,
|
||||
and `async_cleanup` therefore never reaps it. The residue is cosmetic only - the
|
||||
`entity_id` values do not change, so no history and no long-term statistics are
|
||||
involved - and it is left alone deliberately, because closing it would mean another
|
||||
pass after platform setup for a configuration that does not exist in the field.
|
||||
"""
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
|
|
@ -354,6 +370,22 @@ def _adoption_issue_id(entry: ConfigEntry) -> str:
|
|||
return f"predecessor_adoption_{entry.entry_id}"
|
||||
|
||||
|
||||
def _sample_skipped(skipped: list[str]) -> str:
|
||||
"""Render the stranded entities short enough to read at a glance.
|
||||
|
||||
A predecessor that refuses to unload strands its whole station at once, and a WSLink
|
||||
station carries on the order of eighty sensor keys. Spelled out in full, the list
|
||||
would push the one sentence that matters - do not delete the old entry - off the
|
||||
bottom of the notice. Cut here rather than in the translations, so no language has to
|
||||
solve it again; the total travels beside it as its own placeholder.
|
||||
"""
|
||||
|
||||
shown = sorted(skipped)[:ISSUE_ENTITY_SAMPLE]
|
||||
if len(skipped) > len(shown):
|
||||
shown.append("...")
|
||||
return ", ".join(shown)
|
||||
|
||||
|
||||
@callback
|
||||
def update_predecessor_adoption_issue(
|
||||
hass: HomeAssistant,
|
||||
|
|
@ -366,8 +398,9 @@ def update_predecessor_adoption_issue(
|
|||
reads as broken ("Integration not found"). The obvious reaction - deleting it under
|
||||
Settings - is the one action that cannot be undone: it calls
|
||||
`entity_registry.async_clear_config_entry`, and the registry entries it drops are
|
||||
what the recorder history and the long-term statistics hang off. Hence a repair
|
||||
notice that names the entities and spells the wrong move out.
|
||||
what the recorder history and the long-term statistics hang off. That warning leads
|
||||
the notice; the list of what is still stranded comes underneath it, sampled rather
|
||||
than spelled out.
|
||||
|
||||
Cleared again as soon as a later pass finishes the job, so it cannot outlive the
|
||||
problem it describes.
|
||||
|
|
@ -384,7 +417,10 @@ def update_predecessor_adoption_issue(
|
|||
is_fixable=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="predecessor_adoption_incomplete",
|
||||
translation_placeholders={"entities": ", ".join(sorted(result.skipped))},
|
||||
translation_placeholders={
|
||||
"entities": _sample_skipped(result.skipped),
|
||||
"count": str(len(result.skipped)),
|
||||
},
|
||||
)
|
||||
else:
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_id=issue_id)
|
||||
|
|
|
|||
|
|
@ -651,7 +651,7 @@
|
|||
},
|
||||
"predecessor_adoption_incomplete": {
|
||||
"title": "Migration from the previous integration is not finished",
|
||||
"description": "These entities could not be taken over from the previous integration yet: {entities}. The old entry is deliberately left in place so nothing is lost, even though it shows up as broken.\n\nDo NOT delete the old entry via Settings -> Devices & Services -> Delete. That removes its entity registry records, and once they are gone there is nothing left for the history and the long-term statistics to attach to - they cannot be relinked afterwards.\n\nRestart Home Assistant instead and let the migration finish. This message disappears on its own once every entity has been moved over."
|
||||
"description": "Do NOT delete the old entry via Settings -> Devices & Services -> Delete. That removes its entity registry records, and once they are gone there is nothing left for the history and the long-term statistics to attach to - they cannot be relinked afterwards.\n\nRestart Home Assistant instead and let the migration finish. The old entry is deliberately left in place so nothing is lost, even though it shows up as broken. This message disappears on its own once every entity has been moved over.\n\nStill waiting to be taken over, {count} in total: {entities}"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
|
|
|||
|
|
@ -651,7 +651,7 @@
|
|||
},
|
||||
"predecessor_adoption_incomplete": {
|
||||
"title": "Migrace z předchozí integrace není dokončená",
|
||||
"description": "Tyto entity se zatím nepodařilo převzít z předchozí integrace: {entities}. Starý záznam je záměrně ponechaný na místě, aby se nic neztratilo, i když se tváří jako nefunkční.\n\nStarý záznam NEMAŽ přes Nastavení -> Zařízení a služby -> Smazat. Tím se odstraní jeho záznamy v registru entit, a jakmile jsou pryč, nemá se historie ani dlouhodobé statistiky na co navázat - zpětně už je připojit nelze.\n\nMísto toho restartuj Home Assistant a nech migraci doběhnout. Tahle zpráva zmizí sama, jakmile se přenesou všechny entity."
|
||||
"description": "Starý záznam NEMAŽ přes Nastavení -> Zařízení a služby -> Smazat. Tím se odstraní jeho záznamy v registru entit, a jakmile jsou pryč, nemá se historie ani dlouhodobé statistiky na co navázat - zpětně už je připojit nelze.\n\nMísto toho restartuj Home Assistant a nech migraci doběhnout. Starý záznam je záměrně ponechaný na místě, aby se nic neztratilo, i když se tváří jako nefunkční. Tahle zpráva zmizí sama, jakmile se přenesou všechny entity.\n\nZatím nepřevzato, celkem {count}: {entities}"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
|
|
|||
|
|
@ -651,7 +651,7 @@
|
|||
},
|
||||
"predecessor_adoption_incomplete": {
|
||||
"title": "Migration from the previous integration is not finished",
|
||||
"description": "These entities could not be taken over from the previous integration yet: {entities}. The old entry is deliberately left in place so nothing is lost, even though it shows up as broken.\n\nDo NOT delete the old entry via Settings -> Devices & Services -> Delete. That removes its entity registry records, and once they are gone there is nothing left for the history and the long-term statistics to attach to - they cannot be relinked afterwards.\n\nRestart Home Assistant instead and let the migration finish. This message disappears on its own once every entity has been moved over."
|
||||
"description": "Do NOT delete the old entry via Settings -> Devices & Services -> Delete. That removes its entity registry records, and once they are gone there is nothing left for the history and the long-term statistics to attach to - they cannot be relinked afterwards.\n\nRestart Home Assistant instead and let the migration finish. The old entry is deliberately left in place so nothing is lost, even though it shows up as broken. This message disappears on its own once every entity has been moved over.\n\nStill waiting to be taken over, {count} in total: {entities}"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
|
|
|||
|
|
@ -490,7 +490,7 @@ async def test_a_stalled_migration_raises_a_repair_issue(hass: HomeAssistant, en
|
|||
issue = ir.async_get(hass).async_get_issue(DOMAIN, f"predecessor_adoption_{new.entry_id}")
|
||||
assert issue is not None
|
||||
assert issue.translation_key == "predecessor_adoption_incomplete"
|
||||
assert issue.translation_placeholders == {"entities": blocked}
|
||||
assert issue.translation_placeholders == {"entities": blocked, "count": "1"}
|
||||
|
||||
|
||||
async def test_a_predecessor_that_will_not_unload_raises_a_repair_issue(
|
||||
|
|
@ -522,7 +522,48 @@ async def test_a_predecessor_that_will_not_unload_raises_a_repair_issue(
|
|||
assert not result.complete
|
||||
issue = ir.async_get(hass).async_get_issue(DOMAIN, f"predecessor_adoption_{new.entry_id}")
|
||||
assert issue is not None
|
||||
assert issue.translation_placeholders == {"entities": ", ".join(sorted(created.values()))}
|
||||
assert issue.translation_placeholders == {
|
||||
"entities": ", ".join(sorted(created.values())),
|
||||
"count": str(len(created)),
|
||||
}
|
||||
|
||||
|
||||
async def test_a_whole_station_is_summarised_not_spelled_out(
|
||||
hass: HomeAssistant, entries, monkeypatch
|
||||
) -> None:
|
||||
"""A refused unload strands every sensor at once, and a WSLink station has dozens.
|
||||
|
||||
Rendered in full, the list buries the one sentence the notice exists for. The sample
|
||||
stays short and the total rides alongside it, so the user can still tell a couple of
|
||||
stuck entities apart from the entire station.
|
||||
"""
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
||||
from custom_components.sws12500.predecessor import ISSUE_ENTITY_SAMPLE
|
||||
|
||||
old, new = entries
|
||||
registry = er.async_get(hass)
|
||||
seeded = [
|
||||
registry.async_get_or_create("sensor", OLD_DOMAIN, f"key_{index:02d}", config_entry=old).entity_id
|
||||
for index in range(ISSUE_ENTITY_SAMPLE * 3)
|
||||
]
|
||||
old.mock_state(hass, ConfigEntryState.LOADED)
|
||||
|
||||
async def _refuse(*_a: Any, **_kw: Any) -> bool:
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(hass.config_entries, "async_unload", _refuse)
|
||||
|
||||
result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN)
|
||||
update_predecessor_adoption_issue(hass, new, result)
|
||||
|
||||
issue = ir.async_get(hass).async_get_issue(DOMAIN, f"predecessor_adoption_{new.entry_id}")
|
||||
assert issue is not None
|
||||
placeholders = issue.translation_placeholders
|
||||
assert placeholders is not None
|
||||
listed = placeholders["entities"].split(", ")
|
||||
assert listed == [*sorted(seeded)[:ISSUE_ENTITY_SAMPLE], "..."]
|
||||
assert placeholders["count"] == str(len(seeded))
|
||||
|
||||
|
||||
async def test_a_finished_migration_raises_no_issue(hass: HomeAssistant, entries) -> None:
|
||||
|
|
|
|||
Loading…
Reference in New Issue