"""Adopting the predecessor integration must not cost a single measurement. Recorder history and long-term statistics hang off `entity_id`. Every test here exists to defend one sentence: after adoption the `entity_id` is byte-for-byte what it was. Everything else - the platform, the config entry, the device - may change freely. The tests drive the real entity and device registries rather than stubs, because the failure modes being guarded against (a placeholder `unavailable` state blocking the migration, a device deleted with its entities in tow, a duplicate unique id landing in the registry index) all live inside Home Assistant's own bookkeeping. """ from __future__ import annotations from typing import Any import pytest from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.sws12500.const import ( CHANNEL_TYPES, DOMAIN, POCASI_CZ_ENABLED, POCASI_CZ_ENABLED_LEGACY, SENSORS_TO_LOAD, ) from custom_components.sws12500.data import build_device_info from custom_components.sws12500.predecessor import ( async_adopt_predecessor, update_predecessor_adoption_issue, ) from homeassistant.const import ATTR_RESTORED, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er, issue_registry as ir OLD_DOMAIN = "sws12500_legacy" # A representative slice: a plain sensor, a binary sensor, and the suffixed unique id # the battery entities use. SEEDED = ( ("sensor", "outside_temp"), ("sensor", "lightning_distance"), ("binary_sensor", "leak_ch1_binary"), ) @pytest.fixture def entries(hass: HomeAssistant) -> tuple[MockConfigEntry, MockConfigEntry]: """A predecessor entry with entities, plus the new entry adopting them.""" old = MockConfigEntry(domain=OLD_DOMAIN, title="Old", options={"api_id": "x"}) old.add_to_hass(hass) new = MockConfigEntry(domain=DOMAIN, title="New") new.add_to_hass(hass) return old, new def _seed(hass: HomeAssistant, old: MockConfigEntry, **kwargs: Any) -> dict[str, str]: """Register the predecessor's entities; returns unique_id -> entity_id.""" registry = er.async_get(hass) created: dict[str, str] = {} for domain, unique_id in SEEDED: entry = registry.async_get_or_create( domain, OLD_DOMAIN, unique_id, config_entry=old, **kwargs ) created[unique_id] = entry.entity_id return created # --------------------------------------------------------------------------- # The one that matters # --------------------------------------------------------------------------- async def test_entity_ids_are_untouched(hass: HomeAssistant, entries) -> None: """History follows entity_id. If this test fails, users lose their measurements.""" old, new = entries before = _seed(hass, old) result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) registry = er.async_get(hass) for unique_id, entity_id in before.items(): adopted = registry.async_get_entity_id(entity_id.split(".")[0], DOMAIN, unique_id) assert adopted == entity_id, f"{unique_id} changed entity_id: {entity_id} -> {adopted}" assert sorted(result.adopted) == sorted(before.values()) assert result.complete async def test_unique_ids_are_untouched(hass: HomeAssistant, entries) -> None: """A rewritten unique id would break the bind between platform and registry entry.""" old, new = entries _seed(hass, old) await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) registry = er.async_get(hass) adopted = {e.unique_id for e in er.async_entries_for_config_entry(registry, new.entry_id)} assert adopted == {unique_id for _, unique_id in SEEDED} async def test_platform_and_config_entry_are_repointed(hass: HomeAssistant, entries) -> None: """The registry entry has to end up owned by this integration.""" old, new = entries _seed(hass, old) await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) registry = er.async_get(hass) assert not er.async_entries_for_config_entry(registry, old.entry_id) for entry in er.async_entries_for_config_entry(registry, new.entry_id): assert entry.platform == DOMAIN async def test_user_customisations_survive(hass: HomeAssistant, entries) -> None: """Renames, icons and placement are part of what the user would lose on a recreate.""" old, new = entries created = _seed(hass, old) registry = er.async_get(hass) entity_id = created["outside_temp"] registry.async_update_entity( entity_id, name="Teplota na zahradÄ›", icon="mdi:thermometer", area_id="garden", ) await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) entry = registry.async_get(entity_id) assert entry is not None assert entry.name == "Teplota na zahradÄ›" assert entry.icon == "mdi:thermometer" assert entry.area_id == "garden" # --------------------------------------------------------------------------- # The trap: the placeholder state Home Assistant writes for unloaded entities # --------------------------------------------------------------------------- async def test_restored_placeholder_does_not_block_adoption(hass: HomeAssistant, entries) -> None: """The exact production state: old files gone, entities restored as `unavailable`. `async_update_entity_platform` accepts only a missing state or `unknown`, and on startup HA writes `unavailable` + ATTR_RESTORED for every registered but unloaded entity. Without clearing that placeholder first, every adoption raises ValueError - which is to say, nobody's history would migrate at all. """ old, new = entries created = _seed(hass, old) for entity_id in created.values(): hass.states.async_set(entity_id, STATE_UNAVAILABLE, {ATTR_RESTORED: True}) result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert result.complete assert sorted(result.adopted) == sorted(created.values()) async def test_unknown_state_is_accepted(hass: HomeAssistant, entries) -> None: """`unknown` is what the registry API itself tolerates.""" old, new = entries created = _seed(hass, old) hass.states.async_set(created["outside_temp"], STATE_UNKNOWN) result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert result.complete async def test_live_entity_is_refused(hass: HomeAssistant, entries) -> None: """A real state means someone owns this entity; stealing it would give it two owners.""" old, new = entries created = _seed(hass, old) live = created["outside_temp"] hass.states.async_set(live, "21.5") # no ATTR_RESTORED: a genuine reading result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert result.live == [live] assert not result.complete registry = er.async_get(hass) still_old = registry.async_get(live) assert still_old is not None assert still_old.platform == OLD_DOMAIN, "a live entity must be left exactly as it was" # --------------------------------------------------------------------------- # Registry integrity # --------------------------------------------------------------------------- async def test_unique_id_conflict_is_refused(hass: HomeAssistant, entries) -> None: """HA does not guard this itself when only the platform changes. `_async_update_entity` checks for a duplicate unique id only when `new_unique_id` is passed. Changing just the platform onto a key this integration already uses would put two registry entries on one index slot, and the loser becomes unreachable. """ old, new = entries created = _seed(hass, old) registry = er.async_get(hass) ours = registry.async_get_or_create("sensor", DOMAIN, "outside_temp", config_entry=new) result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert result.conflicting == [created["outside_temp"]] # Both entries still exist and still resolve to different entity_ids. assert registry.async_get_entity_id("sensor", DOMAIN, "outside_temp") == ours.entity_id assert registry.async_get_entity_id("sensor", OLD_DOMAIN, "outside_temp") == created["outside_temp"] async def test_predecessor_is_kept_when_anything_was_skipped(hass: HomeAssistant, entries) -> None: """Removing the old entry deletes whatever is still attached to it.""" old, new = entries created = _seed(hass, old) hass.states.async_set(created["outside_temp"], "21.5") result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert not result.predecessor_removed assert hass.config_entries.async_get_entry(old.entry_id) is not None registry = er.async_get(hass) assert registry.async_get(created["outside_temp"]) is not None async def test_predecessor_is_removed_when_complete(hass: HomeAssistant, entries) -> None: """A finished migration must not leave a broken entry behind in the UI.""" old, new = entries _seed(hass, old) result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert result.predecessor_removed assert hass.config_entries.async_get_entry(old.entry_id) is None # --------------------------------------------------------------------------- # Devices - the second way to lose entities # --------------------------------------------------------------------------- async def test_device_is_repointed_and_keeps_its_entities(hass: HomeAssistant, entries) -> None: """Deleting a device deletes its entities, so the device has to move, not be replaced.""" old, new = entries device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=old.entry_id, identifiers={(OLD_DOMAIN,)}, # type: ignore[arg-type] name="Weather Station", ) created = _seed(hass, old, device_id=device.id) await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) survived = device_registry.async_get(device.id) assert survived is not None, "the device was deleted, which takes its entities with it" assert survived.identifiers == set(build_device_info(new)["identifiers"]) assert new.entry_id in survived.config_entries assert old.entry_id not in survived.config_entries registry = er.async_get(hass) for entity_id in created.values(): entry = registry.async_get(entity_id) assert entry is not None assert entry.device_id == device.id async def test_device_identifiers_match_what_the_platform_will_look_up( hass: HomeAssistant, entries ) -> None: """If adoption and `build_device_info` drift, setup mints a second device. The adopted device would then be stranded and every entity would be moved onto the new one. History survives that, but the device page does not. """ old, new = entries device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=old.entry_id, identifiers={(OLD_DOMAIN,)}, # type: ignore[arg-type] name="Weather Station", ) _seed(hass, old, device_id=device.id) await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) looked_up = device_registry.async_get_device(identifiers=set(build_device_info(new)["identifiers"])) assert looked_up is not None assert looked_up.id == device.id async def test_a_refused_entity_survives_its_device_being_repointed(hass: HomeAssistant, entries) -> None: """The quietest way to lose history, and the reason a device is never detached here. `entity_registry.async_device_modified` reacts to a device losing a config entry by deleting every entity of that device still pointing at it. On a partial adoption those are precisely the entities we refused to move - so detaching the predecessor from the device would delete them, before `AdoptionResult.complete` ever gets to keep the old entry alive for a retry. The entity_id is freed, the recorder history and the long-term statistics are orphaned, and nothing is logged. """ old, new = entries device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=old.entry_id, identifiers={(OLD_DOMAIN,)}, # type: ignore[arg-type] name="Weather Station", ) created = _seed(hass, old, device_id=device.id) refused = created["outside_temp"] hass.states.async_set(refused, "21.5") # live: adoption must leave this one alone result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert result.live == [refused] registry = er.async_get(hass) survived = registry.async_get(refused) assert survived is not None, "the refused entity was deleted along with the device link" assert survived.entity_id == refused assert survived.platform == OLD_DOMAIN assert hass.config_entries.async_get_entry(old.entry_id) is not None async def test_a_second_device_does_not_collide(hass: HomeAssistant, entries) -> None: """Two devices, one identifier set. Rewriting both would raise and never recover. `device_registry._validate_identifiers` refuses an identifier another device already holds, and the exception would escape `async_setup_entry` - after the entities were repointed and before the old entry was removed, so every restart reproduces it and the integration never sets up again. """ old, new = entries device_registry = dr.async_get(hass) primary = device_registry.async_get_or_create( config_entry_id=old.entry_id, identifiers={(OLD_DOMAIN,)}, # type: ignore[arg-type] name="Weather Station", ) secondary = device_registry.async_get_or_create( config_entry_id=old.entry_id, identifiers={(OLD_DOMAIN, "ecowitt_ch1")}, name="Channel 1", ) created = _seed(hass, old, device_id=primary.id) registry = er.async_get(hass) extra = registry.async_get_or_create( "sensor", OLD_DOMAIN, "ch1_temp", config_entry=old, device_id=secondary.id ) result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert result.complete for entity_id in (*created.values(), extra.entity_id): assert registry.async_get(entity_id) is not None, f"{entity_id} disappeared" # One device holds the shared identifiers, the other keeps its own and is repointed. holder = device_registry.async_get_device(identifiers=set(build_device_info(new)["identifiers"])) assert holder is not None assert holder.id == primary.id kept = device_registry.async_get(secondary.id) assert kept is not None assert kept.identifiers == {(OLD_DOMAIN, "ecowitt_ch1")} assert new.entry_id in kept.config_entries # --------------------------------------------------------------------------- # More than one predecessor entry # --------------------------------------------------------------------------- async def test_a_blocked_entry_does_not_hold_back_a_clean_one(hass: HomeAssistant, entries) -> None: """Completeness is per predecessor entry; the predecessor never had `single_config_entry`. Decided on the running total, one blocked entity under the first entry would condemn every later one to be kept forever - broken "Integration not found" rows that no retry can ever clear, because the same blocker reproduces the state every time. """ old, new = entries created = _seed(hass, old) blocked = created["outside_temp"] hass.states.async_set(blocked, "21.5") second = MockConfigEntry(domain=OLD_DOMAIN, title="Old 2") second.add_to_hass(hass) registry = er.async_get(hass) clean = registry.async_get_or_create("sensor", OLD_DOMAIN, "second_station_temp", config_entry=second) result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert hass.config_entries.async_get_entry(second.entry_id) is None, ( "a fully adopted entry must be removed even when a sibling was blocked" ) assert hass.config_entries.async_get_entry(old.entry_id) is not None assert result.predecessor_removed assert not result.complete assert registry.async_get_entity_id("sensor", DOMAIN, "second_station_temp") == clean.entity_id # --------------------------------------------------------------------------- # Reporting - the user has to be told, and told what not to do # --------------------------------------------------------------------------- async def test_a_stalled_migration_raises_a_repair_issue(hass: HomeAssistant, entries) -> None: """A log line is not enough: the obvious reaction to the leftover entry destroys history.""" old, new = entries created = _seed(hass, old) blocked = created["outside_temp"] hass.states.async_set(blocked, "21.5") 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 assert issue.translation_key == "predecessor_adoption_incomplete" assert issue.translation_placeholders == {"entities": blocked} async def test_a_finished_migration_raises_no_issue(hass: HomeAssistant, entries) -> None: """Nothing was left behind, so there is nothing to warn about.""" old, new = entries _seed(hass, old) result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) update_predecessor_adoption_issue(hass, new, result) assert ir.async_get(hass).async_get_issue(DOMAIN, f"predecessor_adoption_{new.entry_id}") is None async def test_the_issue_is_cleared_once_the_migration_finishes(hass: HomeAssistant, entries) -> None: """Otherwise the notice outlives the problem and the user learns to ignore it.""" old, new = entries created = _seed(hass, old) blocked = created["outside_temp"] hass.states.async_set(blocked, "21.5") first = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) update_predecessor_adoption_issue(hass, new, first) issue_id = f"predecessor_adoption_{new.entry_id}" assert ir.async_get(hass).async_get_issue(DOMAIN, issue_id) is not None hass.states.async_remove(blocked) second = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) update_predecessor_adoption_issue(hass, new, second) assert second.complete assert ir.async_get(hass).async_get_issue(DOMAIN, issue_id) is None # --------------------------------------------------------------------------- # Repeatability # --------------------------------------------------------------------------- async def test_running_twice_changes_nothing(hass: HomeAssistant, entries) -> None: """Adoption runs on every setup, so the second pass must be inert.""" old, new = entries before = _seed(hass, old) await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) second = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert not second.attempted registry = er.async_get(hass) for unique_id, entity_id in before.items(): assert registry.async_get_entity_id(entity_id.split(".")[0], DOMAIN, unique_id) == entity_id async def test_a_partial_run_can_be_finished(hass: HomeAssistant, entries) -> None: """Interrupted migration: the retry has to pick up what was left behind.""" old, new = entries created = _seed(hass, old) blocked = created["outside_temp"] hass.states.async_set(blocked, "21.5") first = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert first.live == [blocked] # The blocker goes away (old integration finally unloaded / HA restarted). hass.states.async_remove(blocked) second = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert second.adopted == [blocked] assert second.complete assert second.predecessor_removed registry = er.async_get(hass) assert registry.async_get_entity_id("sensor", DOMAIN, "outside_temp") == blocked async def test_no_predecessor_is_a_noop(hass: HomeAssistant) -> None: """The overwhelmingly common case: a fresh install, on every single restart.""" new = MockConfigEntry(domain=DOMAIN) new.add_to_hass(hass) result = await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert not result.attempted assert not result.predecessor_removed # --------------------------------------------------------------------------- # Settings - without them the adopted entities have nothing asking for them # --------------------------------------------------------------------------- async def test_settings_are_inherited(hass: HomeAssistant, entries) -> None: """`SENSORS_TO_LOAD` is what makes the platform create an entity at all. Adopting the registry but not this leaves every entity showing "no longer provided" until auto-discovery has seen a payload - and derived sensors, which are never in a payload, only reappear on the next restart. The forwarding credentials would have to be typed in again on top of that. """ old, new = entries hass.config_entries.async_update_entry( old, options={ SENSORS_TO_LOAD: ["outside_temp", "wind_dir"], "WINDY_API_KEY": "windy-secret", CHANNEL_TYPES: {"t234c1tp": "4"}, }, ) await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert new.options[SENSORS_TO_LOAD] == ["outside_temp", "wind_dir"] assert new.options["WINDY_API_KEY"] == "windy-secret" assert new.options[CHANNEL_TYPES] == {"t234c1tp": "4"} async def test_fresh_input_wins_over_inherited(hass: HomeAssistant, entries) -> None: """What the user just typed into this integration's flow is newer by definition.""" old, new = entries hass.config_entries.async_update_entry(old, options={"API_ID": "old-id", "API_KEY": "old-key"}) hass.config_entries.async_update_entry(new, options={"API_ID": "new-id"}) await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert new.options["API_ID"] == "new-id", "the freshly entered value must not be overwritten" assert new.options["API_KEY"] == "old-key", "gaps are still filled from the predecessor" async def test_the_misspelled_pocasi_flag_is_translated(hass: HomeAssistant, entries) -> None: """A version 1 entry carries the old spelling, and `async_migrate_entry` cannot help. Migration runs against this entry's own version, which is already current, so a key copied in afterwards would never be looked at again - and Pocasi forwarding would silently come up disabled. """ old, new = entries hass.config_entries.async_update_entry(old, options={POCASI_CZ_ENABLED_LEGACY: True}) await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert new.options[POCASI_CZ_ENABLED] is True assert POCASI_CZ_ENABLED_LEGACY not in new.options async def test_a_correct_flag_is_not_clobbered_by_the_misspelled_one(hass: HomeAssistant, entries) -> None: """A partially upgraded entry can carry both; the correct spelling has to win.""" old, new = entries hass.config_entries.async_update_entry( old, options={POCASI_CZ_ENABLED_LEGACY: True, POCASI_CZ_ENABLED: False} ) await async_adopt_predecessor(hass, new, predecessor_domain=OLD_DOMAIN) assert new.options[POCASI_CZ_ENABLED] is False async def test_a_failed_setup_leaves_the_predecessor_untouched(hass, entries, monkeypatch) -> None: """The destructive step must come after setup is known to work. Removing the predecessor's config entry is the one thing adoption cannot undo. If it ran before the webhook routes were up, a user who still has the previous version installed - which is exactly what causes the route clash - would get their old entry deleted and then a failed setup, with nothing to fall back to. """ from types import SimpleNamespace from unittest.mock import AsyncMock from custom_components.sws12500 import async_setup_entry from custom_components.sws12500.const import API_ID, API_KEY from homeassistant.exceptions import ConfigEntryNotReady old, new = entries before = _seed(hass, old) hass.config_entries.async_update_entry(old, options={API_ID: "id", API_KEY: "key"}) class _BrokenRouter: def add_get(self, *_a: Any, **_kw: Any) -> Any: raise ValueError("Duplicate '_default_route', already handled by ...") def add_post(self, *_a: Any, **_kw: Any) -> Any: raise ValueError("Duplicate '_wslink_post_route', already handled by ...") hass.http = SimpleNamespace(app=SimpleNamespace(router=_BrokenRouter())) monkeypatch.setattr( "custom_components.sws12500.HealthCoordinator.async_config_entry_first_refresh", AsyncMock(return_value=None), ) monkeypatch.setattr("custom_components.sws12500.PREDECESSOR_DOMAIN", OLD_DOMAIN, raising=False) monkeypatch.setattr("custom_components.sws12500.predecessor.PREDECESSOR_DOMAIN", OLD_DOMAIN) with pytest.raises(ConfigEntryNotReady): await async_setup_entry(hass, new) assert hass.config_entries.async_get_entry(old.entry_id) is not None, ( "the predecessor was removed even though setup failed" ) registry = er.async_get(hass) for entity_id in before.values(): entry = registry.async_get(entity_id) assert entry is not None assert entry.platform == OLD_DOMAIN # Settings are additive and safe to inherit early, so those may already be across. assert new.options[API_ID] == "id" async def test_adoption_runs_before_any_entity_is_created(hass, monkeypatch) -> None: """Ordering inside `async_setup_entry`, and it fails silently if broken. Adoption has to finish before `async_forward_entry_setups`. Run the other way round, our platforms register first, take the free entity_ids for themselves, and the predecessor's entries are then left holding ids nobody reads - no error, no warning, just every user's history detached. """ from types import SimpleNamespace from unittest.mock import AsyncMock from custom_components.sws12500 import async_setup_entry from custom_components.sws12500.const import API_ID, API_KEY, WSLINK class _Router: def add_get(self, *_a: Any, **_kw: Any) -> Any: return SimpleNamespace(method="GET") def add_post(self, *_a: Any, **_kw: Any) -> Any: return SimpleNamespace(method="POST") hass.http = SimpleNamespace(app=SimpleNamespace(router=_Router())) entry = MockConfigEntry(domain=DOMAIN, options={API_ID: "id", API_KEY: "key", WSLINK: False}) entry.add_to_hass(hass) calls: list[str] = [] monkeypatch.setattr( "custom_components.sws12500.HealthCoordinator.async_config_entry_first_refresh", AsyncMock(return_value=None), ) async def _adopt(*_a: Any, **_kw: Any) -> Any: calls.append("adopt") return SimpleNamespace(adopted=[], live=[], conflicting=[], complete=True, attempted=False) async def _forward(*_a: Any, **_kw: Any) -> bool: calls.append("forward") return True monkeypatch.setattr("custom_components.sws12500.async_adopt_predecessor", _adopt) monkeypatch.setattr(hass.config_entries, "async_forward_entry_setups", _forward) assert await async_setup_entry(hass, entry) is True assert calls == ["adopt", "forward"], f"adoption must precede entity creation, got {calls}" async def test_same_domain_short_circuits(hass: HomeAssistant) -> None: """Until the rename actually happens, this must never touch anything.""" entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) registry = er.async_get(hass) ours = registry.async_get_or_create("sensor", DOMAIN, "outside_temp", config_entry=entry) result = await async_adopt_predecessor(hass, entry, predecessor_domain=DOMAIN) assert not result.attempted assert registry.async_get(ours.entity_id) is not None assert hass.config_entries.async_get_entry(entry.entry_id) is not None