From cd9b89aa8a522fa4e99403dfb5382a215ee4bbcc Mon Sep 17 00:00:00 2001 From: arevindh <693151+arevindh@users.noreply.github.com> Date: Mon, 14 Sep 2026 04:51:35 +0530 Subject: [PATCH] Repair entity ids that carry the device name twice (3.0.0b2) Upgrading from 2.x produced ids like sensor.hall_hall_ip_address. Fresh installs are unaffected: on one instance, same version, a device added fresh got sensor.hub_ip_address while three upgraded ones doubled. So it is specific to that upgrade, not to the sensor code. Not reproduced outside a real upgraded instance. A clean registry, a registry pre-populated with 2.6.0-style entities, and a check for colliding ids all generate correctly, so there is no root cause to fix with confidence. This repairs the result instead, which is deterministic and testable. The rename keeps the unique_id, so recorder history follows the entity to its corrected id. Only entities this integration created are touched, only where the doubled prefix is present, and only when the target id is free. Cosmetic throughout: the displayed name, state and history were always correct. --- custom_components/tinxylocal/__init__.py | 37 +++++++++++++++ custom_components/tinxylocal/manifest.json | 2 +- tests/test_init.py | 52 ++++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/custom_components/tinxylocal/__init__.py b/custom_components/tinxylocal/__init__.py index 2aeeb7c..7789d6e 100644 --- a/custom_components/tinxylocal/__init__.py +++ b/custom_components/tinxylocal/__init__.py @@ -7,7 +7,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import slugify from .const import ( CONF_DEVICE, @@ -34,6 +36,40 @@ ] +def _async_repair_doubled_entity_ids(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Strip a duplicated device name from generated entity ids. + + Upgrading from 2.x produced ids like `sensor.hall_hall_ip_address`: the + device name appears twice. Fresh installs are unaffected. On one instance, + same version, a device added fresh got `sensor.hub_ip_address` while three + upgraded ones doubled, so it is specific to that upgrade rather than to the + sensor code. + + It has not been reproduced outside a real upgraded instance, so this repairs + the result rather than the cause. The rename keeps the unique_id, so recorder + history follows the entity. Only entities this integration created are + touched, only where the doubled prefix is present, and only when the + corrected id is free. + """ + registry = er.async_get(hass) + for item in er.async_entries_for_config_entry(registry, entry.entry_id): + if not item.has_entity_name: + continue + + domain, _, object_id = item.entity_id.partition(".") + prefix = slugify(entry.title) + doubled = f"{prefix}_{prefix}_" + if not object_id.startswith(doubled): + continue + + fixed = f"{domain}.{object_id.replace(doubled, f'{prefix}_', 1)}" + if registry.async_get(fixed): + continue + + _LOGGER.info("Renaming %s to %s", item.entity_id, fixed) + registry.async_update_entity(item.entity_id, new_entity_id=fixed) + + def _async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Bring entries created before 3.0.0 up to date, in place. @@ -61,6 +97,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TinxyConfigEntry) -> boo """Set up Tinxy from a config entry.""" _async_migrate_entry(hass, entry) + _async_repair_doubled_entity_ids(hass, entry) web_session = async_get_clientsession(hass) diff --git a/custom_components/tinxylocal/manifest.json b/custom_components/tinxylocal/manifest.json index e2fc3e2..12d6132 100644 --- a/custom_components/tinxylocal/manifest.json +++ b/custom_components/tinxylocal/manifest.json @@ -11,7 +11,7 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/arevindh/tinxylocal/issues", "requirements": [], - "version": "3.0.0b1", + "version": "3.0.0b2", "zeroconf": [ { "name": "tinxy*", diff --git a/tests/test_init.py b/tests/test_init.py index eb3bec0..0b93c52 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -91,3 +91,55 @@ async def test_options_are_applied_to_the_hub( # the coordinator must poll through the same hubs the platforms command assert coordinator.hubs[0].request_timeout == 9 assert coordinator.hubs[0].rate_limit_delay == 3 + + +async def test_doubled_entity_ids_are_repaired( + hass: HomeAssistant, device_online: AiohttpClientMocker +) -> None: + """Upgrading from 2.x produced ids with the device name twice. + + The entity keeps its unique_id, so recorder history follows it across. + """ + from homeassistant.helpers import entity_registry as er + + from .const import DEVICE_ID + + entry = MockConfigEntry( + domain=DOMAIN, data=ENTRY_DATA, unique_id=CHIP_ID, title="Hall" + ) + entry.add_to_hass(hass) + + registry = er.async_get(hass) + broken = registry.async_get_or_create( + "sensor", + DOMAIN, + f"{DEVICE_ID}_ip", + config_entry=entry, + original_name="IP address", + has_entity_name=True, + suggested_object_id="hall_hall_ip_address", + ) + assert broken.entity_id == "sensor.hall_hall_ip_address" + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert registry.async_get("sensor.hall_hall_ip_address") is None + fixed = registry.async_get("sensor.hall_ip_address") + assert fixed is not None + assert fixed.unique_id == f"{DEVICE_ID}_ip" + + +async def test_repair_leaves_correct_ids_alone( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """A fresh install must not be touched by the repair.""" + from homeassistant.helpers import entity_registry as er + + registry = er.async_get(hass) + ids = { + e.entity_id + for e in er.async_entries_for_config_entry(registry, loaded_entry.entry_id) + } + assert "sensor.hall_ip_address" in ids + assert not any(".hall_hall_" in i for i in ids)