Skip to content

Duplicate devices on HA 2025.7 #49

@mik-laj

Description

@mik-laj

Hi there,

I’ve just upgraded to Home Assistant 2025.7 and thanks to new integration UI, I noticed that inpost-air 1.5.0 now registers two devices for every parcel locker:

  • Old device – created by versions ≤ 1.4.x
    Identifiers: ('inpost_air', locker_code, locker_id) (single 3-tuple)

  • New device – created by 1.5.0
    Identifiers: {('inpost_air', locker_code), ('inpost_air', locker_id)} (two 2-tuples)

Here is related code snippet:

Image

1e4db35#diff-def8c2bbe017571a9f34d2df0740883182a7d0af8a59d9cd00b16a061f635429L2-L46

Because HA compares devices by individual tuples, none of the new tuples match the old one, so core thinks it’s a brand-new device. All entities migrate to the new device, leaving the original entry orphaned and visible in the UI.

Image

Suggested fix: one-off migration

I don't think, we need to keep the legacy 3-tuple identifier, so the simplest migration is to delete any empty devices that still carry it and (optionally) copy user metadata to the correct device.:

# custom_components/inpost_air/__init__.py
# Untested and generated by ChatGPT
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr

DOMAIN = "inpost_air"

async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Remove legacy 3-tuple devices left by ≤ 1.4.x."""
    if entry.version == 1:            # 1.5.0 → 1.5.1
        dev_reg = dr.async_get(hass)

        for device in list(dev_reg.devices.values()):
            if entry.entry_id not in device.config_entries:
                continue

            # Detect an old device with a single 3-element identifier
            old_id = next(
                (i for i in device.identifiers
                 if len(i) == 3 and i[0] == DOMAIN),
                None,
            )
            if not old_id:
                continue

            # Skip if it still has entities (unexpected edge-case)
            if device.entities:
                hass.logger.warning(
                    "Legacy device %s still has entities; skipping cleanup", device.id
                )
                continue

            # Optionally carry over area/name/disabled flags to the new device
            _, code, _ = old_id
            new_dev = next(
                (d for d in dev_reg.devices.values()
                 if (DOMAIN, code) in d.identifiers
                 and entry.entry_id in d.config_entries
                 and d.id != device.id),
                None,
            )
            if new_dev:
                dev_reg.async_update_device(
                    new_dev.id,
                    area_id=device.area_id or new_dev.area_id,
                    name_by_user=device.name_by_user or new_dev.name_by_user,
                    disabled_by=device.disabled_by or new_dev.disabled_by,
                )

            # Remove the orphaned 3-tuple device
            dev_reg.async_remove_device(device.id)

        entry.version = 2             # mark migration complete
    return True

This runs once on upgrade, leaves users with a single clean device per locker, and preserves their area or custom name settings.

I’m not planning a PR myself, but feel free to copy the code snippet.

Thanks for maintaining the integration!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions