Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions custom_components/tinxylocal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.

Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion custom_components/tinxylocal/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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*",
Expand Down
52 changes: 52 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading