fix: use a valid entity_id with the platform domain (#1777)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled

Home Assistant reports the entity_id set by the integration as invalid when
its domain is not the platform domain, or when the object_id contains
characters that are not allowed, e.g. a MIoT-Spec-V2 description in Chinese
or a description that is empty once slugified. Both are deprecated and stop
working in 2027.5.0 and 2027.2.0.

Derive the platform domain from the Home Assistant entity base class that
each entity mixes in, and slugify the object_id.

The unique_id keeps using the integration domain and the raw description, so
it stays byte for byte identical for the entities that are already
registered. The entity_id of an already registered entity is owned by the
entity registry and is not changed either, and a newly registered entity gets
the same entity_id as before, since the registry was already slugifying the
object_id it was given.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: LiShuzhen <SusanPhevos@gmail.com>
This commit is contained in:
Necroneco
2026-09-23 15:48:13 +08:00
committed by GitHub
parent 77c4c09823
commit 48b0248892

View File

@@ -108,6 +108,32 @@ from .miot_spec import (
_LOGGER = logging.getLogger(__name__)
def ha_entity_domain(entity: Entity) -> str:
"""Return the Home Assistant platform domain of an entity.
Every entity mixes in a Home Assistant entity base class, e.g.
Light(MIoTServiceEntity, LightEntity), which is declared in
homeassistant.components.<domain>. Fall back to the integration domain
when no such base class is found.
"""
for cls in type(entity).__mro__:
module = cls.__module__ or ''
if module.startswith('homeassistant.components.'):
return module.split('.')[2]
return DOMAIN
def to_valid_entity_id(entity_id: str) -> str:
"""Return a valid entity_id.
The object_id is built from the MIoT-Spec-V2 instance, which may contain
characters that are not allowed in an entity_id, e.g. a description in
Chinese or a trailing separator.
"""
domain, _, object_id = entity_id.partition('.')
return f'{domain}.{slugify_name(object_id)}'
class MIoTEntityData:
"""MIoT Entity Data."""
platform: str
@@ -933,19 +959,28 @@ class MIoTServiceEntity(Entity):
self._state_sub_id = 0
self._value_sub_ids = {}
# Gen entity id
# The unique_id keeps the integration domain so that it stays the same
# for the entities that are already registered, the entity_id uses the
# Home Assistant platform domain and must be a valid entity_id.
ha_domain = ha_entity_domain(self)
if isinstance(self.entity_data.spec, MIoTSpecInstance):
self.entity_id = miot_device.gen_device_entity_id(DOMAIN)
self._attr_unique_id = miot_device.gen_device_entity_id(DOMAIN)
self.entity_id = to_valid_entity_id(
miot_device.gen_device_entity_id(ha_domain))
self._attr_name = f' {self.entity_data.spec.description_trans}'
elif isinstance(self.entity_data.spec, MIoTSpecService):
self.entity_id = miot_device.gen_service_entity_id(
self._attr_unique_id = miot_device.gen_service_entity_id(
DOMAIN, siid=self.entity_data.spec.iid,
description=self.entity_data.spec.description)
self.entity_id = to_valid_entity_id(
miot_device.gen_service_entity_id(
ha_domain, siid=self.entity_data.spec.iid,
description=self.entity_data.spec.description))
self._attr_name = (
f'{"* "if self.entity_data.spec.proprietary else " "}'
f'{self.entity_data.spec.description_trans}')
self._attr_entity_category = entity_data.spec.entity_category
# Set entity attr
self._attr_unique_id = self.entity_id
self._attr_should_poll = False
self._attr_has_entity_name = True
self._attr_available = miot_device.online
@@ -1240,11 +1275,15 @@ class MIoTPropertyEntity(Entity):
self._value_sub_id = 0
self._pending_write_ha_state_timer = None
# Gen entity_id
self.entity_id = self.miot_device.gen_prop_entity_id(
# The unique_id keeps the integration domain, see MIoTServiceEntity
self._attr_unique_id = self.miot_device.gen_prop_entity_id(
ha_domain=DOMAIN, spec_name=spec.name,
siid=spec.service.iid, piid=spec.iid)
self.entity_id = to_valid_entity_id(
self.miot_device.gen_prop_entity_id(
ha_domain=ha_entity_domain(self), spec_name=spec.name,
siid=spec.service.iid, piid=spec.iid))
# Set entity attr
self._attr_unique_id = self.entity_id
self._attr_should_poll = False
self._attr_has_entity_name = True
self._attr_name = (
@@ -1382,11 +1421,15 @@ class MIoTEventEntity(Entity):
self.service = spec.service
self._main_loop = miot_device.miot_client.main_loop
# Gen entity_id
self.entity_id = self.miot_device.gen_event_entity_id(
# The unique_id keeps the integration domain, see MIoTServiceEntity
self._attr_unique_id = self.miot_device.gen_event_entity_id(
ha_domain=DOMAIN, spec_name=spec.name,
siid=spec.service.iid, eiid=spec.iid)
self.entity_id = to_valid_entity_id(
self.miot_device.gen_event_entity_id(
ha_domain=ha_entity_domain(self), spec_name=spec.name,
siid=spec.service.iid, eiid=spec.iid))
# Set entity attr
self._attr_unique_id = self.entity_id
self._attr_should_poll = False
self._attr_has_entity_name = True
self._attr_name = (
@@ -1493,11 +1536,15 @@ class MIoTActionEntity(Entity):
self._main_loop = miot_device.miot_client.main_loop
self._state_sub_id = 0
# Gen entity_id
self.entity_id = self.miot_device.gen_action_entity_id(
# The unique_id keeps the integration domain, see MIoTServiceEntity
self._attr_unique_id = self.miot_device.gen_action_entity_id(
ha_domain=DOMAIN, spec_name=spec.name,
siid=spec.service.iid, aiid=spec.iid)
self.entity_id = to_valid_entity_id(
self.miot_device.gen_action_entity_id(
ha_domain=ha_entity_domain(self), spec_name=spec.name,
siid=spec.service.iid, aiid=spec.iid))
# Set entity attr
self._attr_unique_id = self.entity_id
self._attr_should_poll = False
self._attr_has_entity_name = True
self._attr_name = (