Raise a LIFX repair issue for an entry that cannot be migrated (#184191)

Signed-off-by: Avi Miller <me@dje.li>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Avi Miller
2026-10-04 09:47:12 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 71a74f6d8a
commit 6f8a8cefc3
4 changed files with 170 additions and 1 deletions
+24 -1
View File
@@ -7,7 +7,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.typing import ConfigType
from .const import CONF_SERIAL, DATA_LIFX_MANAGER, DOMAIN, LOGGER
@@ -55,6 +55,11 @@ PLATFORMS = [
]
def invalid_serial_issue_id(entry_id: str) -> str:
"""Return the repair issue ID for an entry whose serial cannot be migrated."""
return f"invalid_serial_{entry_id}"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LIFX component."""
async_setup_services(hass)
@@ -80,6 +85,19 @@ async def async_migrate_entry(hass: HomeAssistant, entry: LIFXConfigEntry) -> bo
except ValueError as err:
# Nothing can make the entry usable, so it is left for the user to
# remove rather than retried
ir.async_create_issue(
hass,
DOMAIN,
invalid_serial_issue_id(entry.entry_id),
data={"entry_id": entry.entry_id},
is_fixable=True,
severity=ir.IssueSeverity.ERROR,
translation_key="invalid_serial",
translation_placeholders={
"title": entry.title,
"unique_id": entry.unique_id,
},
)
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="invalid_serial",
@@ -137,3 +155,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: LIFXConfigEntry) -> boo
# is the usual reason an entry is being unloaded in the first place
LOGGER.debug("Could not stop the effects running on %s: %s", entry.title, err)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_remove_entry(hass: HomeAssistant, entry: LIFXConfigEntry) -> None:
"""Remove the repair issue raised for an entry that could not be migrated."""
ir.async_delete_issue(hass, DOMAIN, invalid_serial_issue_id(entry.entry_id))
+37
View File
@@ -0,0 +1,37 @@
"""Repairs for LIFX."""
from typing import override
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlowResult
from homeassistant.core import HomeAssistant
class InvalidSerialRepairFlow(ConfirmRepairFlow):
"""Remove an entry whose serial cannot be migrated."""
def __init__(self, entry_id: str) -> None:
"""Initialize the flow."""
self.entry_id = entry_id
@override
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> RepairsFlowResult:
"""Remove the entry once the user confirms."""
# The entry may have been deleted by hand while this flow was open
if (
user_input is not None
and self.hass.config_entries.async_get_entry(self.entry_id) is not None
):
await self.hass.config_entries.async_remove(self.entry_id)
return await super().async_step_confirm(user_input)
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
data: dict[str, str | int | float | None] | None,
) -> InvalidSerialRepairFlow:
"""Create a fix flow for an invalid serial issue."""
assert data is not None
return InvalidSerialRepairFlow(str(data["entry_id"]))
@@ -104,6 +104,19 @@
"message": "Error fetching the state of the LIFX device: {error}"
}
},
"issues": {
"invalid_serial": {
"fix_flow": {
"step": {
"confirm": {
"description": "Home Assistant cannot migrate the LIFX entry \"{title}\" because its stored identifier \"{unique_id}\" is not a valid LIFX serial number.\n\nClick **Submit** to delete the entry then either wait for it to be rediscovered automatically or add it manually.",
"title": "[%key:component::lifx::issues::invalid_serial::title%]"
}
}
},
"title": "LIFX entry {title} cannot be migrated"
}
},
"services": {
"effect_colorloop": {
"description": "Runs an effect with looping colors.",
+96
View File
@@ -0,0 +1,96 @@
"""Tests for LIFX repairs."""
from homeassistant.components.lifx import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
from . import IP_ADDRESS
from tests.common import MockConfigEntry
from tests.components.repairs import process_repair_fix_flow, start_repair_fix_flow
from tests.typing import ClientSessionGenerator
MALFORMED_UNIQUE_ID = "gg:bb:cc:dd:ee:cc"
async def _setup_malformed_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Set up an entry whose stored serial cannot be migrated."""
entry = MockConfigEntry(
domain=DOMAIN,
title="Kitchen",
version=1,
unique_id=MALFORMED_UNIQUE_ID,
data={CONF_HOST: IP_ADDRESS},
)
entry.add_to_hass(hass)
assert not await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.MIGRATION_ERROR
return entry
async def test_invalid_serial_fix_flow_removes_entry(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
hass_client: ClientSessionGenerator,
) -> None:
"""Test the invalid serial repair removes the entry it was raised for."""
assert await async_setup_component(hass, "repairs", {})
entry = await _setup_malformed_entry(hass)
issue_id = f"invalid_serial_{entry.entry_id}"
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
assert issue is not None
assert issue.is_fixable
assert issue.severity is ir.IssueSeverity.ERROR
assert issue.translation_placeholders == {
"title": "Kitchen",
"unique_id": MALFORMED_UNIQUE_ID,
}
client = await hass_client()
data = await start_repair_fix_flow(client, DOMAIN, issue_id)
assert data["step_id"] == "confirm"
data = await process_repair_fix_flow(client, data["flow_id"])
await hass.async_block_till_done()
assert data["type"] == "create_entry"
assert hass.config_entries.async_get_entry(entry.entry_id) is None
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
async def test_removing_entry_deletes_invalid_serial_issue(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Test deleting the entry by hand also clears its repair issue."""
entry = await _setup_malformed_entry(hass)
issue_id = f"invalid_serial_{entry.entry_id}"
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
async def test_invalid_serial_fix_flow_entry_already_removed(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test confirming the repair after the entry was deleted by hand."""
assert await async_setup_component(hass, "repairs", {})
entry = await _setup_malformed_entry(hass)
issue_id = f"invalid_serial_{entry.entry_id}"
client = await hass_client()
data = await start_repair_fix_flow(client, DOMAIN, issue_id)
assert data["step_id"] == "confirm"
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
data = await process_repair_fix_flow(client, data["flow_id"])
assert data["type"] == "create_entry"