Correct handling of entities with empty name for ESPHome devices (#143366)

Correct handling of empty name for ESPHome devices

If the name was set to "", ESPHome should treat this as if the
name is empty. Since protobuf treats empty fields as "" we need
to handle this as `None` internally as otherwise it leads to
friendly names like "Friendly Name " with a trailing space and
unexpected entity_id formats

fixes #132532
This commit is contained in:
J. Nick Koston 2025-04-21 03:38:29 -10:00 committed by GitHub
parent bb73ecc1f4
commit 352ef0d009
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 54 additions and 2 deletions

View File

@ -224,7 +224,16 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
)
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
if entity_info.name:
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
else:
# https://github.com/home-assistant/core/issues/132532
# If name is not set, ESPHome will use the sanitized friendly name
# as the name, however we want to use the original object_id
# as the entity_id before it is sanitized since the sanitizer
# is not utf-8 aware. In this case, its always going to be
# an empty string so we drop the object_id.
self.entity_id = f"{domain}.{device_info.name}"
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@ -260,7 +269,12 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
self._static_info = static_info
self._attr_unique_id = build_unique_id(device_info.mac_address, static_info)
self._attr_entity_registry_enabled_default = not static_info.disabled_by_default
self._attr_name = static_info.name
# https://github.com/home-assistant/core/issues/132532
# If the name is "", we need to set it to None since otherwise
# the friendly_name will be "{friendly_name} " with a trailing
# space. ESPHome uses protobuf under the hood, and an empty field
# gets a default value of "".
self._attr_name = static_info.name if static_info.name else None
if entity_category := static_info.entity_category:
self._attr_entity_category = ENTITY_CATEGORIES.from_esphome(entity_category)
else:

View File

@ -17,6 +17,7 @@ from aioesphomeapi import (
)
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
ATTR_RESTORED,
EVENT_HOMEASSISTANT_STOP,
STATE_OFF,
@ -503,3 +504,40 @@ async def test_esphome_device_without_friendly_name(
state = hass.states.get("binary_sensor.test_mybinary_sensor")
assert state is not None
assert state.state == STATE_ON
async def test_entity_without_name_device_with_friendly_name(
hass: HomeAssistant,
mock_client: APIClient,
hass_storage: dict[str, Any],
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
) -> None:
"""Test name and entity_id for a device a friendly name and an entity without a name."""
entity_info = [
BinarySensorInfo(
object_id="mybinary_sensor",
key=1,
name="",
unique_id="my_binary_sensor",
),
]
states = [
BinarySensorState(key=1, state=True, missing_state=False),
]
user_service = []
await mock_esphome_device(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
device_info={"friendly_name": "The Best Mixer", "name": "mixer"},
)
state = hass.states.get("binary_sensor.mixer")
assert state is not None
assert state.state == STATE_ON
# Make sure we have set the name to `None` as otherwise
# the friendly_name will be "The Best Mixer "
assert state.attributes[ATTR_FRIENDLY_NAME] == "The Best Mixer"