Skip to content

Samsung TV integration overwrites config entry unique_id when TV regenerates UPnP UUID, creating duplicate entities #169628

@JohnTKelly

Description

@JohnTKelly

Upstream Issue & PR Draft for home-assistant/core


GitHub Issue

Title: Samsung TV integration overwrites config entry unique_id when TV regenerates UPnP UUID, creating duplicate entities

The problem

Some Samsung TVs (particularly older models like the 2017 UN43MU6300) regenerate their UPnP UUID on every reboot or power cycle. When Home Assistant rediscovers the TV via SSDP, the samsungtv config flow's _async_update_existing_matching_entry() method overwrites the config entry's unique_id with the new UUID.

Since entity unique_id values are derived from config_entry.unique_id (in SamsungTVEntity.__init__), this causes:

  1. All existing entities become orphaned — their unique_id no longer matches the config entry
  2. New duplicate entities are created with the new UUID as their unique_id
  3. Automations and dashboard tiles break because they reference the old entity registry entries
  4. The device registry accumulates multiple UUID identifiers over time

This cycle repeats on every TV reboot, creating an ever-growing list of orphaned entities.

Root cause

In config_flow.py, _async_get_existing_matching_entry() returns is_unique_match=True for both MAC matches and UPnP UUID matches:

# Line 376-381
for entry in self._async_current_entries(include_ignore=False):
    if (self._mac and self._mac == entry.data.get(CONF_MAC)) or (
        self._upnp_udn and self._upnp_udn == entry.unique_id
    ):
        return entry, True  # <-- MAC match returns is_unique_match=True

Then in _async_update_existing_matching_entry(), the unique_id is overwritten when is_unique_match=True and the IDs differ:

# Line 401-405
if self.unique_id and (
    entry.unique_id is None
    or (is_unique_match and self.unique_id != entry.unique_id)
):
    entry_kw_args["unique_id"] = self.unique_id

When the TV reboots with a new UUID, the flow discovers it via SSDP, matches the existing entry by MAC address (is_unique_match=True), sees a different unique_id, and overwrites it.

What version of Home Assistant Core has the issue?

core-2026.4.1

What was the last working version of Home Assistant Core?

(unknown — this has likely been present for a long time)

What type of installation are you running?

Home Assistant Container

Integration causing the issue

Samsung Smart TV

Link to integration documentation on our website

https://www.home-assistant.io/integrations/samsungtv

Additional information

Affected TV: Samsung UN43MU6300 (2017), websocket method on port 8002.

Over the course of a few weeks, the device registry accumulated 4 different UPnP UUIDs as identifiers, and 6 duplicate entities (3 pairs of media_player + remote with _2, _3 suffixes).

Workaround: A custom integration that monkey-patches _async_update_existing_matching_entry to skip the unique_id update when the existing entry already has one set.


Proposed Code Change

File: homeassistant/components/samsungtv/config_flow.py

Option A: Don't overwrite unique_id when entry already has one (minimal fix)

# In _async_update_existing_matching_entry(), change lines 401-405 from:

        if self.unique_id and (
            entry.unique_id is None
            or (is_unique_match and self.unique_id != entry.unique_id)
        ):
            entry_kw_args["unique_id"] = self.unique_id

# To:

        if self.unique_id and entry.unique_id is None:
            entry_kw_args["unique_id"] = self.unique_id

Rationale: If the config entry already has a unique_id, it should be treated as the stable identity. The UUID rotation on older TVs is a firmware quirk, not a legitimate identity change. Overwriting the unique_id breaks entity identity across the board.

Option B: Distinguish MAC match from UUID match (more precise fix)

# In _async_get_existing_matching_entry(), change the return to distinguish
# MAC-only matches from UUID matches:

    @callback
    def _async_get_existing_matching_entry(
        self,
    ) -> tuple[ConfigEntry | None, bool]:
        """Get first existing matching entry (prefer unique id)."""
        matching_host_entry: ConfigEntry | None = None
        for entry in self._async_current_entries(include_ignore=False):
            # UUID match — this IS a unique_id match
            if self._upnp_udn and self._upnp_udn == entry.unique_id:
                LOGGER.debug("Found entry matching unique_id for %s", self._host)
                return entry, True

            # MAC match — same device, but NOT a unique_id match
            if self._mac and self._mac == entry.data.get(CONF_MAC):
                LOGGER.debug("Found entry matching MAC for %s", self._host)
                return entry, False

            if entry.data[CONF_HOST] == self._host:
                LOGGER.debug("Found entry matching host for %s", self._host)
                matching_host_entry = entry

        return matching_host_entry, False

Rationale: is_unique_match should only be True when the match was actually on the unique_id (UUID), not when it was on the MAC address. A MAC match means "same physical device" but says nothing about whether the UUID should be updated. With this change, _async_update_existing_matching_entry will only overwrite the unique_id when entry.unique_id is None (for MAC-only matches), which is the correct behavior.

Recommended approach

Option B is the better fix because:

  • It preserves the original intent: is_unique_match should mean "matched by unique_id"
  • It correctly handles the case where entry.unique_id is None (first-time setup — UUID gets populated)
  • It doesn't overwrite the UUID when the match was by MAC, which is the root cause
  • Option A would prevent legitimate UUID updates even when the match was on UUID (though it's unclear when that would be needed)

Test case to add

async def test_ssdp_update_does_not_overwrite_unique_id_on_mac_match(
    hass: HomeAssistant,
) -> None:
    """Test that SSDP rediscovery with a new UUID does not overwrite an existing unique_id when matched by MAC."""
    entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_HOST: "192.168.16.71",
            CONF_MAC: "9c:8c:6e:6b:ae:07",
            CONF_METHOD: "websocket",
        },
        unique_id="original-uuid-1234",
    )
    entry.add_to_hass(hass)

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": config_entries.SOURCE_SSDP},
        data=ssdp.SsdpServiceInfo(
            ssdp_usn="uuid:new-uuid-5678::...",
            ssdp_st="urn:...",
            upnp={
                ATTR_UPNP_UDN: "uuid:new-uuid-5678",
                ATTR_UPNP_FRIENDLY_NAME: "Samsung TV",
                ATTR_UPNP_MANUFACTURER: "Samsung",
            },
            ssdp_location="http://192.168.16.71:9197/dmr",
        ),
    )

    # Should abort (entry already exists), and unique_id should NOT be changed
    assert result["type"] is FlowResultType.ABORT
    assert entry.unique_id == "original-uuid-1234"  # NOT "new-uuid-5678"

Filing Checklist

  1. File the issue at https://github.com/home-assistant/core/issues/new?template=bug_report.yml
  2. Reference the issue number in the PR
  3. Fork home-assistant/core, create a branch
  4. Apply Option B fix to homeassistant/components/samsungtv/config_flow.py
  5. Add the test case to tests/components/samsungtv/test_config_flow.py
  6. Run existing samsungtv tests to ensure no regressions
  7. Submit PR referencing the issue

Metadata

Metadata

Type

No type

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions