From 352ef0d009ea2a786e3f6ca66f09a5f2db66b590 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Apr 2025 03:38:29 -1000 Subject: [PATCH] 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 --- homeassistant/components/esphome/entity.py | 18 ++++++++-- tests/components/esphome/test_entity.py | 38 ++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 313785fd2df..b442eaebb65 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -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: diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 5c82337e71b..290b1871cd7 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -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"