mirror of
https://github.com/home-assistant/core.git
synced 2026-02-03 22:05:35 +01:00
Fix KNX fan unique_id for switch-only fans (#162002)
This commit is contained in:
@@ -2,16 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from propcache.api import cached_property
|
||||
from xknx.devices import Fan as XknxFan
|
||||
from xknx.telegram.address import parse_device_group_address
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
@@ -37,6 +40,58 @@ from .storage.const import (
|
||||
)
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_yaml_uids(
|
||||
hass: HomeAssistant, platform_config: list[ConfigType]
|
||||
) -> None:
|
||||
"""Migrate entities unique_id for YAML switch-only fan entities."""
|
||||
# issue was introduced in 2026.1 - this migration in 2026.2
|
||||
ent_reg = er.async_get(hass)
|
||||
invalid_uid = str(None)
|
||||
if (
|
||||
none_entity_id := ent_reg.async_get_entity_id(Platform.FAN, DOMAIN, invalid_uid)
|
||||
) is None:
|
||||
return
|
||||
for config in platform_config:
|
||||
if not config.get(KNX_ADDRESS) and (
|
||||
new_uid_base := config.get(FanSchema.CONF_SWITCH_ADDRESS)
|
||||
):
|
||||
break
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"No YAML entry found to migrate fan entity '%s' unique_id from '%s'. Removing entry",
|
||||
none_entity_id,
|
||||
invalid_uid,
|
||||
)
|
||||
ent_reg.async_remove(none_entity_id)
|
||||
return
|
||||
new_uid = str(
|
||||
parse_device_group_address(
|
||||
new_uid_base[0], # list of group addresses - first item is sending address
|
||||
)
|
||||
)
|
||||
try:
|
||||
ent_reg.async_update_entity(none_entity_id, new_unique_id=str(new_uid))
|
||||
_LOGGER.info(
|
||||
"Migrating fan entity '%s' unique_id from '%s' to %s",
|
||||
none_entity_id,
|
||||
invalid_uid,
|
||||
new_uid,
|
||||
)
|
||||
except ValueError:
|
||||
# New unique_id already exists - remove invalid entry. User might have changed YAML
|
||||
_LOGGER.info(
|
||||
"Failed to migrate fan entity '%s' unique_id from '%s' to '%s'. "
|
||||
"Removing the invalid entry",
|
||||
none_entity_id,
|
||||
invalid_uid,
|
||||
new_uid,
|
||||
)
|
||||
ent_reg.async_remove(none_entity_id)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -57,6 +112,7 @@ async def async_setup_entry(
|
||||
|
||||
entities: list[_KnxFan] = []
|
||||
if yaml_platform_config := knx_module.config_yaml.get(Platform.FAN):
|
||||
async_migrate_yaml_uids(hass, yaml_platform_config)
|
||||
entities.extend(
|
||||
KnxYamlFan(knx_module, entity_config)
|
||||
for entity_config in yaml_platform_config
|
||||
@@ -177,7 +233,10 @@ class KnxYamlFan(_KnxFan, KnxYamlEntity):
|
||||
self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
|
||||
self._attr_unique_id = str(self._device.speed.group_address)
|
||||
if self._device.speed.group_address:
|
||||
self._attr_unique_id = str(self._device.speed.group_address)
|
||||
else:
|
||||
self._attr_unique_id = str(self._device.switch.group_address)
|
||||
|
||||
|
||||
class KnxUiFan(_KnxFan, KnxUiEntity):
|
||||
|
||||
@@ -4,16 +4,19 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.knx.const import KNX_ADDRESS, FanConf
|
||||
from homeassistant.components.knx.const import DOMAIN, KNX_ADDRESS, FanConf
|
||||
from homeassistant.components.knx.schema import FanSchema
|
||||
from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import KnxEntityGenerator
|
||||
from .conftest import KNXTestKit
|
||||
|
||||
|
||||
async def test_fan_percent(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
async def test_fan_percent(
|
||||
hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test KNX fan with percentage speed."""
|
||||
await knx.setup_integration(
|
||||
{
|
||||
@@ -23,6 +26,9 @@ async def test_fan_percent(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
}
|
||||
}
|
||||
)
|
||||
entry = entity_registry.async_get("fan.test")
|
||||
assert entry
|
||||
assert entry.unique_id == "1/2/3"
|
||||
|
||||
# turn on fan with default speed (50%)
|
||||
await hass.services.async_call(
|
||||
@@ -109,7 +115,9 @@ async def test_fan_step(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
await knx.assert_telegram_count(0)
|
||||
|
||||
|
||||
async def test_fan_switch(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
async def test_fan_switch(
|
||||
hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test KNX fan with switch only."""
|
||||
await knx.setup_integration(
|
||||
{
|
||||
@@ -119,6 +127,9 @@ async def test_fan_switch(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
}
|
||||
}
|
||||
)
|
||||
entry = entity_registry.async_get("fan.test")
|
||||
assert entry
|
||||
assert entry.unique_id == "1/2/3"
|
||||
|
||||
# turn on fan
|
||||
await hass.services.async_call(
|
||||
@@ -133,7 +144,9 @@ async def test_fan_switch(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
await knx.assert_write("1/2/3", False)
|
||||
|
||||
|
||||
async def test_fan_switch_step(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
async def test_fan_switch_step(
|
||||
hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test KNX fan with speed steps and switch address."""
|
||||
await knx.setup_integration(
|
||||
{
|
||||
@@ -145,6 +158,9 @@ async def test_fan_switch_step(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
}
|
||||
}
|
||||
)
|
||||
entry = entity_registry.async_get("fan.test")
|
||||
assert entry
|
||||
assert entry.unique_id == "1/1/1"
|
||||
|
||||
# turn on fan without percentage - actuator sets default speed
|
||||
await hass.services.async_call(
|
||||
@@ -216,6 +232,53 @@ async def test_fan_oscillation(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
assert state.attributes.get("oscillating") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fan_config",
|
||||
[
|
||||
{
|
||||
# before fix: unique_id is 'None', after fix: from switch address
|
||||
CONF_NAME: "test",
|
||||
FanSchema.CONF_SWITCH_ADDRESS: "1/2/3",
|
||||
},
|
||||
{
|
||||
# no change in unique_id here, but since no YAML to update from, the
|
||||
# invalid registry entry will be removed - it wouldn't be loaded anyway
|
||||
# this YAML will create a new entry (same unique_id as above for tests)
|
||||
CONF_NAME: "test",
|
||||
KNX_ADDRESS: "1/2/3",
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_fan_unique_id_fix(
|
||||
hass: HomeAssistant,
|
||||
knx: KNXTestKit,
|
||||
entity_registry: er.EntityRegistry,
|
||||
fan_config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test KNX fan unique_id migration fix."""
|
||||
invalid_unique_id = "None"
|
||||
knx.mock_config_entry.add_to_hass(hass)
|
||||
entity_registry.async_get_or_create(
|
||||
object_id_base="test",
|
||||
disabled_by=None,
|
||||
domain=Platform.FAN,
|
||||
platform=DOMAIN,
|
||||
unique_id=invalid_unique_id,
|
||||
config_entry=knx.mock_config_entry,
|
||||
)
|
||||
await knx.setup_integration(
|
||||
{FanSchema.PLATFORM: fan_config},
|
||||
add_entry_to_hass=False,
|
||||
)
|
||||
entry = entity_registry.async_get("fan.test")
|
||||
assert entry
|
||||
assert entry.unique_id == "1/2/3"
|
||||
# Verify the old entity with invalid unique_id has been updated or removed
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.FAN, DOMAIN, invalid_unique_id
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("knx_data", "expected_read_response", "expected_state"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user