mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 08:41:59 +00:00
bug for bug compat
This commit is contained in:
@@ -19,9 +19,17 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
// Use friendly_name if available, otherwise fall back to device name
|
||||
// Bug-for-bug compatibility with OLD behavior:
|
||||
// - With MAC suffix: OLD code used App.get_friendly_name() directly (no fallback)
|
||||
// - Without MAC suffix: OLD code used pre-computed object_id with fallback to device name
|
||||
const std::string &friendly = App.get_friendly_name();
|
||||
this->name_ = StringRef(!friendly.empty() ? friendly : App.get_name());
|
||||
if (App.is_name_add_mac_suffix_enabled()) {
|
||||
// MAC suffix enabled - use friendly_name directly (even if empty) for compatibility
|
||||
this->name_ = StringRef(friendly);
|
||||
} else {
|
||||
// No MAC suffix - fallback to device name if friendly_name is empty
|
||||
this->name_ = StringRef(!friendly.empty() ? friendly : App.get_name());
|
||||
}
|
||||
}
|
||||
this->flags_.has_own_name = false;
|
||||
// Dynamic name - must calculate hash at runtime
|
||||
|
||||
@@ -84,19 +84,27 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
device_name = device_id_obj.id
|
||||
|
||||
# Set the entity name with pre-computed object_id hash
|
||||
# We always pre-compute the hash using the same fallback logic as get_base_entity_object_id
|
||||
# to ensure hash matches the object_id that would be generated
|
||||
# Must match OLD behavior for bug-for-bug compatibility:
|
||||
# - With MAC suffix: OLD code used App.get_friendly_name() directly (no fallback)
|
||||
# - Without MAC suffix: OLD code used pre-computed object_id with fallback to device name
|
||||
entity_name = config[CONF_NAME]
|
||||
if entity_name:
|
||||
# Named entity - hash from entity name
|
||||
object_id_hash = fnv1_hash_object_id(entity_name)
|
||||
else:
|
||||
# Empty name - use fallback logic: device_name -> friendly_name -> CORE.name
|
||||
# Empty name - behavior depends on MAC suffix setting
|
||||
if device_name:
|
||||
# Entity on sub-device - use device name
|
||||
base_name = device_name
|
||||
elif CORE.config.get("name_add_mac_suffix", False):
|
||||
# MAC suffix enabled - OLD behavior used friendly_name directly (even if empty)
|
||||
# This is bug-for-bug compatibility
|
||||
base_name = CORE.friendly_name or ""
|
||||
elif CORE.friendly_name:
|
||||
# No MAC suffix, friendly_name set - use it
|
||||
base_name = CORE.friendly_name
|
||||
else:
|
||||
# No MAC suffix, no friendly_name - fallback to device name
|
||||
base_name = CORE.name
|
||||
object_id_hash = fnv1_hash_object_id(base_name)
|
||||
add(var.set_name(entity_name, object_id_hash))
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
esphome:
|
||||
name: test-device
|
||||
# No friendly_name set, no MAC suffix
|
||||
# OLD behavior: object_id = device name because Python pre-computed with fallback
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
|
||||
sensor:
|
||||
# Empty name entity - OLD behavior used device name as fallback
|
||||
- platform: template
|
||||
name: ""
|
||||
id: sensor_empty_name
|
||||
lambda: return 42.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Named entity for comparison
|
||||
- platform: template
|
||||
name: "Temperature"
|
||||
id: sensor_named
|
||||
lambda: return 43.0;
|
||||
update_interval: 60s
|
||||
@@ -0,0 +1,26 @@
|
||||
esphome:
|
||||
name: test-device
|
||||
# No friendly_name set, MAC suffix enabled
|
||||
# OLD behavior: object_id = "" (empty) because is_object_id_dynamic_() used App.get_friendly_name() directly
|
||||
name_add_mac_suffix: true
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
|
||||
sensor:
|
||||
# Empty name entity - OLD behavior produced empty object_id when MAC suffix enabled
|
||||
- platform: template
|
||||
name: ""
|
||||
id: sensor_empty_name
|
||||
lambda: return 42.0;
|
||||
update_interval: 60s
|
||||
|
||||
# Named entity for comparison
|
||||
- platform: template
|
||||
name: "Temperature"
|
||||
id: sensor_named
|
||||
lambda: return 43.0;
|
||||
update_interval: 60s
|
||||
138
tests/integration/test_object_id_no_friendly_name.py
Normal file
138
tests/integration/test_object_id_no_friendly_name.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Integration tests for object_id when friendly_name is not set.
|
||||
|
||||
These tests verify bug-for-bug compatibility with the old behavior:
|
||||
|
||||
1. With MAC suffix enabled + no friendly_name:
|
||||
- OLD: is_object_id_dynamic_() was true, used App.get_friendly_name() directly
|
||||
- OLD: object_id = "" (empty) because friendly_name was empty
|
||||
- NEW: Must maintain same behavior for compatibility
|
||||
|
||||
2. Without MAC suffix + no friendly_name:
|
||||
- OLD: is_object_id_dynamic_() was false, used pre-computed object_id_c_str_
|
||||
- OLD: Python computed object_id with fallback to device name
|
||||
- NEW: Must maintain same behavior (object_id = device name)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679"
|
||||
MAC_SUFFIX = "abf679"
|
||||
|
||||
# FNV1 offset basis - hash of empty string
|
||||
FNV1_OFFSET_BASIS = 2166136261
|
||||
|
||||
|
||||
def compute_expected_object_id(name: str) -> str:
|
||||
"""Compute expected object_id from name using Python helpers."""
|
||||
return sanitize(snake_case(name))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_id_no_friendly_name_with_mac_suffix(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test object_id when friendly_name not set but MAC suffix enabled.
|
||||
|
||||
OLD behavior (bug-for-bug compatibility):
|
||||
- is_object_id_dynamic_() returned true (no own name AND mac suffix enabled)
|
||||
- format_dynamic_object_id() used App.get_friendly_name() directly
|
||||
- Since friendly_name was empty, object_id was empty
|
||||
|
||||
This was arguably a bug, but we maintain it for compatibility.
|
||||
"""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
|
||||
# Device name should include MAC suffix
|
||||
expected_device_name = f"test-device-{MAC_SUFFIX}"
|
||||
assert device_info.name == expected_device_name
|
||||
|
||||
# Friendly name should be empty (not set in config)
|
||||
assert device_info.friendly_name == ""
|
||||
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find the empty-name entity
|
||||
empty_name_entities = [e for e in entities if e.name == ""]
|
||||
assert len(empty_name_entities) == 1
|
||||
|
||||
entity = empty_name_entities[0]
|
||||
|
||||
# OLD behavior: object_id was empty because App.get_friendly_name() was empty
|
||||
# This is bug-for-bug compatibility
|
||||
assert entity.object_id == "", (
|
||||
f"Expected empty object_id for bug-for-bug compatibility, "
|
||||
f"got '{entity.object_id}'"
|
||||
)
|
||||
|
||||
# Hash should be FNV1_OFFSET_BASIS (hash of empty string)
|
||||
assert entity.key == FNV1_OFFSET_BASIS, (
|
||||
f"Expected hash of empty string ({FNV1_OFFSET_BASIS:#x}), "
|
||||
f"got {entity.key:#x}"
|
||||
)
|
||||
|
||||
# Named entity should work normally
|
||||
named_entities = [e for e in entities if e.name == "Temperature"]
|
||||
assert len(named_entities) == 1
|
||||
assert named_entities[0].object_id == "temperature"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_object_id_no_friendly_name_no_mac_suffix(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test object_id when friendly_name not set and no MAC suffix.
|
||||
|
||||
OLD behavior:
|
||||
- is_object_id_dynamic_() returned false (mac suffix not enabled)
|
||||
- Used object_id_c_str_ which was pre-computed in Python
|
||||
- Python used get_base_entity_object_id() with fallback to CORE.name
|
||||
|
||||
Result: object_id = sanitize(snake_case(device_name))
|
||||
"""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
|
||||
# Device name should NOT include MAC suffix
|
||||
assert device_info.name == "test-device"
|
||||
|
||||
# Friendly name should be empty (not set in config)
|
||||
assert device_info.friendly_name == ""
|
||||
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find the empty-name entity
|
||||
empty_name_entities = [e for e in entities if e.name == ""]
|
||||
assert len(empty_name_entities) == 1
|
||||
|
||||
entity = empty_name_entities[0]
|
||||
|
||||
# OLD behavior: object_id was computed from device name
|
||||
expected_object_id = compute_expected_object_id("test-device")
|
||||
assert entity.object_id == expected_object_id, (
|
||||
f"Expected object_id '{expected_object_id}' from device name, "
|
||||
f"got '{entity.object_id}'"
|
||||
)
|
||||
|
||||
# Hash should match device name
|
||||
expected_hash = fnv1_hash_object_id("test-device")
|
||||
assert entity.key == expected_hash, (
|
||||
f"Expected hash {expected_hash:#x}, got {entity.key:#x}"
|
||||
)
|
||||
|
||||
# Named entity should work normally
|
||||
named_entities = [e for e in entities if e.name == "Temperature"]
|
||||
assert len(named_entities) == 1
|
||||
assert named_entities[0].object_id == "temperature"
|
||||
Reference in New Issue
Block a user