Honor failed platform unload; derive labels from the snapshot
A False platform unload now returns False and retains the runtime record so live entities keep their database. Sensor names and device labels re-derive from the current snapshot so child renames propagate; unique_ids stay child-id based.
This commit is contained in:
@@ -578,7 +578,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Platforms unload FIRST (mirroring HA conventions): entity
|
||||
# listeners must not outlive the database and coordinator the
|
||||
# teardown below closes.
|
||||
await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
platforms_unloaded = await hass.config_entries.async_unload_platforms(
|
||||
entry, PLATFORMS
|
||||
)
|
||||
if platforms_unloaded is False and runtime_data is not None:
|
||||
# Home Assistant KEPT one or more platforms loaded — their
|
||||
# entities still live and must keep their database. Restore
|
||||
# the runtime record popped above and report failure so HA
|
||||
# retries the unload later instead of leaving live entities
|
||||
# backed by a closed database.
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = runtime_data
|
||||
return False
|
||||
unload_error: BaseException | None = None
|
||||
if runtime_data is not None:
|
||||
remove_update_listener = getattr(runtime_data, "remove_update_listener", None)
|
||||
|
||||
@@ -101,14 +101,20 @@ def _instance_payload(
|
||||
class _NestQuestChildDaySensor(CoordinatorEntity, SensorEntity):
|
||||
"""Base for the per-child day sensors.
|
||||
|
||||
Registry identity and the child's NestQuest device grouping are
|
||||
built from the child's database id; the day's numbers are read
|
||||
from the child's CURRENT snapshot so every refresh lands
|
||||
coherently. A child absent from the latest snapshot (a
|
||||
Registry identity is built from the child's database id and never
|
||||
changes; the DISPLAYED name and device label are derived from the
|
||||
child's CURRENT snapshot on every read, so a rename through
|
||||
``manage_child`` propagates on the next refresh without orphaning
|
||||
the entity's history. A child absent from the latest snapshot (a
|
||||
deactivation; removal is a later task's scope) yields ``None``
|
||||
states rather than stale counts.
|
||||
"""
|
||||
|
||||
#: Per-subclass label appended to the name (e.g. ``quests due
|
||||
#: today``), set as a class attribute so ``name`` composes from the
|
||||
#: current snapshot.
|
||||
_name_suffix: str = ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NestQuestCoordinator,
|
||||
@@ -116,11 +122,8 @@ class _NestQuestChildDaySensor(CoordinatorEntity, SensorEntity):
|
||||
) -> None:
|
||||
super().__init__(coordinator)
|
||||
self._child_id = child.child_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"child_{child.child_id}")},
|
||||
name=f"NestQuest {child.child_name}",
|
||||
manufacturer="NestQuest",
|
||||
)
|
||||
self._fallback_name = child.child_name
|
||||
self._attr_unique_id = None # set by the subclass
|
||||
|
||||
@property
|
||||
def _child(self) -> ChildDaySnapshot | None:
|
||||
@@ -133,11 +136,28 @@ class _NestQuestChildDaySensor(CoordinatorEntity, SensorEntity):
|
||||
return child
|
||||
return None
|
||||
|
||||
def _displayed_child_name(self) -> str:
|
||||
child = self._child
|
||||
return child.child_name if child is not None else self._fallback_name
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
return f"NestQuest {self._displayed_child_name()} {self._name_suffix}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, f"child_{self._child_id}")},
|
||||
name=f"NestQuest {self._displayed_child_name()}",
|
||||
manufacturer="NestQuest",
|
||||
)
|
||||
|
||||
|
||||
class NestQuestQuestsDueTodaySensor(_NestQuestChildDaySensor):
|
||||
"""How many quests the child has due today, with the day payload."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_name_suffix = "quests due today"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -145,7 +165,6 @@ class NestQuestQuestsDueTodaySensor(_NestQuestChildDaySensor):
|
||||
child: ChildDaySnapshot,
|
||||
) -> None:
|
||||
super().__init__(coordinator, child)
|
||||
self._attr_name = f"NestQuest {child.child_name} quests due today"
|
||||
self._attr_unique_id = (
|
||||
f"{DOMAIN}_child_{child.child_id}_quests_due_today"
|
||||
)
|
||||
@@ -179,6 +198,7 @@ class NestQuestQuestsCompletedTodaySensor(_NestQuestChildDaySensor):
|
||||
"""How many of the child's quests were completed today."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_name_suffix = "quests completed today"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -186,9 +206,6 @@ class NestQuestQuestsCompletedTodaySensor(_NestQuestChildDaySensor):
|
||||
child: ChildDaySnapshot,
|
||||
) -> None:
|
||||
super().__init__(coordinator, child)
|
||||
self._attr_name = (
|
||||
f"NestQuest {child.child_name} quests completed today"
|
||||
)
|
||||
self._attr_unique_id = (
|
||||
f"{DOMAIN}_child_{child.child_id}_quests_completed_today"
|
||||
)
|
||||
@@ -205,6 +222,7 @@ class NestQuestQuestsRemainingTodaySensor(_NestQuestChildDaySensor):
|
||||
"""How many of the child's quests are still owed today."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_name_suffix = "quests remaining today"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -212,9 +230,6 @@ class NestQuestQuestsRemainingTodaySensor(_NestQuestChildDaySensor):
|
||||
child: ChildDaySnapshot,
|
||||
) -> None:
|
||||
super().__init__(coordinator, child)
|
||||
self._attr_name = (
|
||||
f"NestQuest {child.child_name} quests remaining today"
|
||||
)
|
||||
self._attr_unique_id = (
|
||||
f"{DOMAIN}_child_{child.child_id}_quests_remaining_today"
|
||||
)
|
||||
@@ -233,6 +248,7 @@ class NestQuestCompletionPctTodaySensor(_NestQuestChildDaySensor):
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_device_class = SensorDeviceClass.PERCENTAGE
|
||||
_attr_native_unit_of_measurement = "%"
|
||||
_name_suffix = "completion pct today"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -240,9 +256,6 @@ class NestQuestCompletionPctTodaySensor(_NestQuestChildDaySensor):
|
||||
child: ChildDaySnapshot,
|
||||
) -> None:
|
||||
super().__init__(coordinator, child)
|
||||
self._attr_name = (
|
||||
f"NestQuest {child.child_name} completion pct today"
|
||||
)
|
||||
self._attr_unique_id = (
|
||||
f"{DOMAIN}_child_{child.child_id}_completion_pct_today"
|
||||
)
|
||||
|
||||
@@ -339,3 +339,47 @@ async def test_unload_unloads_platforms(hass, make_entry) -> None:
|
||||
assert await async_setup_entry(hass, entry) is True
|
||||
assert await async_unload_entry(hass, entry) is True
|
||||
assert hass.config_entries.unloaded_platforms == ["sensor"]
|
||||
|
||||
|
||||
async def test_rename_propagates_to_name_and_device_label(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
"""A manage_child rename shows on the next refresh: the name and
|
||||
device label re-derive from the snapshot while the unique_id (and
|
||||
therefore history) is untouched."""
|
||||
from custom_components.nestquest.children import edit_child
|
||||
|
||||
entry, coordinator, (ada, _bo, _cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
)
|
||||
database = entry.runtime_data.database
|
||||
due_sensor = _sensor(hass, ada.id, "quests_due_today")
|
||||
original_unique_id = due_sensor.unique_id
|
||||
assert due_sensor.name.startswith("NestQuest Ada ")
|
||||
|
||||
await edit_child(database, ada.id, display_name="Mirren")
|
||||
await coordinator.async_refresh()
|
||||
|
||||
assert due_sensor.name.startswith("NestQuest Mirren ")
|
||||
assert due_sensor.device_info.name == "NestQuest Mirren"
|
||||
assert due_sensor.unique_id == original_unique_id
|
||||
|
||||
|
||||
async def test_failed_platform_unload_returns_false_and_retains_runtime(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
"""When HA reports platforms still loaded, the unload must NOT
|
||||
close the database the live entities read: it returns False and
|
||||
keeps the runtime record for a retry."""
|
||||
entry, _coordinator, _children = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
)
|
||||
database = entry.runtime_data.database
|
||||
|
||||
async def _refuse_unload(entry_arg, platforms):
|
||||
return False
|
||||
|
||||
hass.config_entries.async_unload_platforms = _refuse_unload
|
||||
assert await async_unload_entry(hass, entry) is False
|
||||
assert hass.data[DOMAIN][entry.entry_id] is entry.runtime_data
|
||||
assert database.connected is True
|
||||
Reference in New Issue
Block a user