mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	fixes
This commit is contained in:
		| @@ -17,7 +17,7 @@ from esphome.core import CORE, ID, coroutine | |||||||
| from esphome.coroutine import FakeAwaitable | from esphome.coroutine import FakeAwaitable | ||||||
| from esphome.cpp_generator import MockObj, add, get_variable | from esphome.cpp_generator import MockObj, add, get_variable | ||||||
| from esphome.cpp_types import App | from esphome.cpp_types import App | ||||||
| from esphome.helpers import sanitize, snake_case | from esphome.entity import get_base_entity_object_id | ||||||
| from esphome.types import ConfigFragmentType, ConfigType | from esphome.types import ConfigFragmentType, ConfigType | ||||||
| from esphome.util import Registry, RegistryEntry | from esphome.util import Registry, RegistryEntry | ||||||
|  |  | ||||||
| @@ -122,19 +122,14 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: | |||||||
|  |  | ||||||
|     add(var.set_name(config[CONF_NAME])) |     add(var.set_name(config[CONF_NAME])) | ||||||
|  |  | ||||||
|     # Calculate base object_id |     # Calculate base object_id using the same logic as C++ | ||||||
|     base_object_id: str |     # This must match the C++ behavior in esphome/core/entity_base.cpp | ||||||
|  |     base_object_id = get_base_entity_object_id(config[CONF_NAME], CORE.friendly_name) | ||||||
|  |  | ||||||
|     if not config[CONF_NAME]: |     if not config[CONF_NAME]: | ||||||
|         # Use the friendly name if available, otherwise use the device name |  | ||||||
|         if CORE.friendly_name: |  | ||||||
|             base_object_id = sanitize(snake_case(CORE.friendly_name)) |  | ||||||
|         else: |  | ||||||
|             base_object_id = sanitize(snake_case(CORE.name)) |  | ||||||
|         _LOGGER.debug( |         _LOGGER.debug( | ||||||
|             "Entity has empty name, using '%s' as object_id base", base_object_id |             "Entity has empty name, using '%s' as object_id base", base_object_id | ||||||
|         ) |         ) | ||||||
|     else: |  | ||||||
|         base_object_id = sanitize(snake_case(config[CONF_NAME])) |  | ||||||
|  |  | ||||||
|     # Handle duplicates |     # Handle duplicates | ||||||
|     # Check for duplicates |     # Check for duplicates | ||||||
| @@ -156,6 +151,12 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: | |||||||
|         object_id = base_object_id |         object_id = base_object_id | ||||||
|  |  | ||||||
|     add(var.set_object_id(object_id)) |     add(var.set_object_id(object_id)) | ||||||
|  |     _LOGGER.debug( | ||||||
|  |         "Setting object_id '%s' for entity '%s' on platform '%s'", | ||||||
|  |         object_id, | ||||||
|  |         config[CONF_NAME], | ||||||
|  |         platform, | ||||||
|  |     ) | ||||||
|     add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) |     add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) | ||||||
|     if CONF_INTERNAL in config: |     if CONF_INTERNAL in config: | ||||||
|         add(var.set_internal(config[CONF_INTERNAL])) |         add(var.set_internal(config[CONF_INTERNAL])) | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								esphome/entity.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/entity.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | """Entity-related helper functions.""" | ||||||
|  |  | ||||||
|  | from esphome.core import CORE | ||||||
|  | from esphome.helpers import sanitize, snake_case | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_base_entity_object_id(name: str, friendly_name: str | None) -> str: | ||||||
|  |     """Calculate the base object ID for an entity that will be set via set_object_id(). | ||||||
|  |  | ||||||
|  |     This function calculates what object_id_c_str_ should be set to in C++. | ||||||
|  |  | ||||||
|  |     The C++ EntityBase::get_object_id() (entity_base.cpp lines 38-49) works as: | ||||||
|  |     - If !has_own_name && is_name_add_mac_suffix_enabled(): | ||||||
|  |         return str_sanitize(str_snake_case(App.get_friendly_name()))  // Dynamic | ||||||
|  |     - Else: | ||||||
|  |         return object_id_c_str_ ?? ""  // What we set via set_object_id() | ||||||
|  |  | ||||||
|  |     Since we're calculating what to pass to set_object_id(), we always need to | ||||||
|  |     generate the object_id the same way, regardless of name_add_mac_suffix setting. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         name: The entity name (empty string if no name) | ||||||
|  |         friendly_name: The friendly name from CORE.friendly_name | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         The base object ID to use for duplicate checking and to pass to set_object_id() | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     if name: | ||||||
|  |         # Entity has its own name (has_own_name will be true) | ||||||
|  |         base_str = name | ||||||
|  |     elif friendly_name: | ||||||
|  |         # Entity has empty name (has_own_name will be false) | ||||||
|  |         # Calculate what the object_id should be | ||||||
|  |         # C++ uses App.get_friendly_name() which returns friendly_name or device name | ||||||
|  |         base_str = friendly_name | ||||||
|  |     else: | ||||||
|  |         # Fallback to device name | ||||||
|  |         base_str = CORE.name | ||||||
|  |  | ||||||
|  |     return sanitize(snake_case(base_str)) | ||||||
							
								
								
									
										118
									
								
								tests/integration/fixtures/duplicate_entities.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								tests/integration/fixtures/duplicate_entities.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | |||||||
|  | esphome: | ||||||
|  |   name: duplicate-entities-test | ||||||
|  |   # Define devices to test multi-device duplicate handling | ||||||
|  |   devices: | ||||||
|  |     - id: controller_1 | ||||||
|  |       name: Controller 1 | ||||||
|  |     - id: controller_2 | ||||||
|  |       name: Controller 2 | ||||||
|  |  | ||||||
|  | host: | ||||||
|  | api:  # Port will be automatically injected | ||||||
|  | logger: | ||||||
|  |  | ||||||
|  | # Create duplicate entities across different scenarios | ||||||
|  |  | ||||||
|  | # Scenario 1: Multiple sensors with same name on same device (should get _2, _3, _4) | ||||||
|  | sensor: | ||||||
|  |   - platform: template | ||||||
|  |     name: Temperature | ||||||
|  |     lambda: return 1.0; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Temperature | ||||||
|  |     lambda: return 2.0; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Temperature | ||||||
|  |     lambda: return 3.0; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Temperature | ||||||
|  |     lambda: return 4.0; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  |   # Scenario 2: Device-specific duplicates using device_id configuration | ||||||
|  |   - platform: template | ||||||
|  |     name: Device Temperature | ||||||
|  |     device_id: controller_1 | ||||||
|  |     lambda: return 10.0; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Device Temperature | ||||||
|  |     device_id: controller_1 | ||||||
|  |     lambda: return 11.0; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Device Temperature | ||||||
|  |     device_id: controller_1 | ||||||
|  |     lambda: return 12.0; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  |   # Different device, same name - should not conflict | ||||||
|  |   - platform: template | ||||||
|  |     name: Device Temperature | ||||||
|  |     device_id: controller_2 | ||||||
|  |     lambda: return 20.0; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  | # Scenario 3: Binary sensors (different platform, same name) | ||||||
|  | binary_sensor: | ||||||
|  |   - platform: template | ||||||
|  |     name: Temperature | ||||||
|  |     lambda: return true; | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Temperature | ||||||
|  |     lambda: return false; | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Temperature | ||||||
|  |     lambda: return true; | ||||||
|  |  | ||||||
|  |   # Scenario 5: Binary sensors on devices | ||||||
|  |   - platform: template | ||||||
|  |     name: Device Temperature | ||||||
|  |     device_id: controller_1 | ||||||
|  |     lambda: return true; | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Device Temperature | ||||||
|  |     device_id: controller_2 | ||||||
|  |     lambda: return false; | ||||||
|  |  | ||||||
|  | # Scenario 6: Test with special characters that need sanitization | ||||||
|  | text_sensor: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Status Message!" | ||||||
|  |     lambda: return {"status1"}; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: "Status Message!" | ||||||
|  |     lambda: return {"status2"}; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: "Status Message!" | ||||||
|  |     lambda: return {"status3"}; | ||||||
|  |     update_interval: 0.1s | ||||||
|  |  | ||||||
|  | # Scenario 7: More switch duplicates | ||||||
|  | switch: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Power Switch" | ||||||
|  |     lambda: return false; | ||||||
|  |     turn_on_action: [] | ||||||
|  |     turn_off_action: [] | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: "Power Switch" | ||||||
|  |     lambda: return true; | ||||||
|  |     turn_on_action: [] | ||||||
|  |     turn_off_action: [] | ||||||
							
								
								
									
										187
									
								
								tests/integration/test_duplicate_entities.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								tests/integration/test_duplicate_entities.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | |||||||
|  | """Integration test for duplicate entity handling.""" | ||||||
|  |  | ||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
|  | from aioesphomeapi import EntityInfo | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_duplicate_entities( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that duplicate entity names are automatically suffixed with _2, _3, _4.""" | ||||||
|  |     async with run_compiled(yaml_config), api_client_connected() as client: | ||||||
|  |         # Get device info | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |  | ||||||
|  |         # Get devices | ||||||
|  |         devices = device_info.devices | ||||||
|  |         assert len(devices) >= 2, f"Expected at least 2 devices, got {len(devices)}" | ||||||
|  |  | ||||||
|  |         # Find our test devices | ||||||
|  |         controller_1 = next((d for d in devices if d.name == "Controller 1"), None) | ||||||
|  |         controller_2 = next((d for d in devices if d.name == "Controller 2"), None) | ||||||
|  |  | ||||||
|  |         assert controller_1 is not None, "Controller 1 device not found" | ||||||
|  |         assert controller_2 is not None, "Controller 2 device not found" | ||||||
|  |  | ||||||
|  |         # Get entity list | ||||||
|  |         entities = await client.list_entities_services() | ||||||
|  |         all_entities: list[EntityInfo] = [] | ||||||
|  |         for entity_list in entities[0]: | ||||||
|  |             if hasattr(entity_list, "object_id"): | ||||||
|  |                 all_entities.append(entity_list) | ||||||
|  |  | ||||||
|  |         # Group entities by type for easier testing | ||||||
|  |         sensors = [e for e in all_entities if e.__class__.__name__ == "SensorInfo"] | ||||||
|  |         binary_sensors = [ | ||||||
|  |             e for e in all_entities if e.__class__.__name__ == "BinarySensorInfo" | ||||||
|  |         ] | ||||||
|  |         text_sensors = [ | ||||||
|  |             e for e in all_entities if e.__class__.__name__ == "TextSensorInfo" | ||||||
|  |         ] | ||||||
|  |         switches = [e for e in all_entities if e.__class__.__name__ == "SwitchInfo"] | ||||||
|  |  | ||||||
|  |         # Scenario 1: Check sensors with duplicate "Temperature" names | ||||||
|  |         temp_sensors = [s for s in sensors if s.name == "Temperature"] | ||||||
|  |         temp_object_ids = sorted([s.object_id for s in temp_sensors]) | ||||||
|  |  | ||||||
|  |         # Should have temperature, temperature_2, temperature_3, temperature_4 | ||||||
|  |         assert len(temp_object_ids) >= 4, ( | ||||||
|  |             f"Expected at least 4 temperature sensors, got {len(temp_object_ids)}" | ||||||
|  |         ) | ||||||
|  |         assert "temperature" in temp_object_ids, ( | ||||||
|  |             "First temperature sensor should not have suffix" | ||||||
|  |         ) | ||||||
|  |         assert "temperature_2" in temp_object_ids, ( | ||||||
|  |             "Second temperature sensor should be temperature_2" | ||||||
|  |         ) | ||||||
|  |         assert "temperature_3" in temp_object_ids, ( | ||||||
|  |             "Third temperature sensor should be temperature_3" | ||||||
|  |         ) | ||||||
|  |         assert "temperature_4" in temp_object_ids, ( | ||||||
|  |             "Fourth temperature sensor should be temperature_4" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Scenario 2: Check device-specific sensors don't conflict | ||||||
|  |         device_temp_sensors = [s for s in sensors if s.name == "Device Temperature"] | ||||||
|  |  | ||||||
|  |         # Group by device | ||||||
|  |         controller_1_temps = [ | ||||||
|  |             s | ||||||
|  |             for s in device_temp_sensors | ||||||
|  |             if getattr(s, "device_id", None) == controller_1.device_id | ||||||
|  |         ] | ||||||
|  |         controller_2_temps = [ | ||||||
|  |             s | ||||||
|  |             for s in device_temp_sensors | ||||||
|  |             if getattr(s, "device_id", None) == controller_2.device_id | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         # Controller 1 should have device_temperature, device_temperature_2, device_temperature_3 | ||||||
|  |         c1_object_ids = sorted([s.object_id for s in controller_1_temps]) | ||||||
|  |         assert len(c1_object_ids) >= 3, ( | ||||||
|  |             f"Expected at least 3 sensors on controller_1, got {len(c1_object_ids)}" | ||||||
|  |         ) | ||||||
|  |         assert "device_temperature" in c1_object_ids, ( | ||||||
|  |             "First device sensor should not have suffix" | ||||||
|  |         ) | ||||||
|  |         assert "device_temperature_2" in c1_object_ids, ( | ||||||
|  |             "Second device sensor should be device_temperature_2" | ||||||
|  |         ) | ||||||
|  |         assert "device_temperature_3" in c1_object_ids, ( | ||||||
|  |             "Third device sensor should be device_temperature_3" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Controller 2 should have only device_temperature (no suffix) | ||||||
|  |         c2_object_ids = [s.object_id for s in controller_2_temps] | ||||||
|  |         assert len(c2_object_ids) >= 1, ( | ||||||
|  |             f"Expected at least 1 sensor on controller_2, got {len(c2_object_ids)}" | ||||||
|  |         ) | ||||||
|  |         assert "device_temperature" in c2_object_ids, ( | ||||||
|  |             "Controller 2 sensor should not have suffix" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Scenario 3: Check binary sensors (different platform, same name) | ||||||
|  |         temp_binary = [b for b in binary_sensors if b.name == "Temperature"] | ||||||
|  |         binary_object_ids = sorted([b.object_id for b in temp_binary]) | ||||||
|  |  | ||||||
|  |         # Should have temperature, temperature_2, temperature_3 (no conflict with sensor platform) | ||||||
|  |         assert len(binary_object_ids) >= 3, ( | ||||||
|  |             f"Expected at least 3 binary sensors, got {len(binary_object_ids)}" | ||||||
|  |         ) | ||||||
|  |         assert "temperature" in binary_object_ids, ( | ||||||
|  |             "First binary sensor should not have suffix" | ||||||
|  |         ) | ||||||
|  |         assert "temperature_2" in binary_object_ids, ( | ||||||
|  |             "Second binary sensor should be temperature_2" | ||||||
|  |         ) | ||||||
|  |         assert "temperature_3" in binary_object_ids, ( | ||||||
|  |             "Third binary sensor should be temperature_3" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Scenario 4: Check text sensors with special characters | ||||||
|  |         status_sensors = [t for t in text_sensors if t.name == "Status Message!"] | ||||||
|  |         status_object_ids = sorted([t.object_id for t in status_sensors]) | ||||||
|  |  | ||||||
|  |         # Special characters should be sanitized to _ | ||||||
|  |         assert len(status_object_ids) >= 3, ( | ||||||
|  |             f"Expected at least 3 status sensors, got {len(status_object_ids)}" | ||||||
|  |         ) | ||||||
|  |         assert "status_message_" in status_object_ids, ( | ||||||
|  |             "First status sensor should be status_message_" | ||||||
|  |         ) | ||||||
|  |         assert "status_message__2" in status_object_ids, ( | ||||||
|  |             "Second status sensor should be status_message__2" | ||||||
|  |         ) | ||||||
|  |         assert "status_message__3" in status_object_ids, ( | ||||||
|  |             "Third status sensor should be status_message__3" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Scenario 5: Check switches with duplicate names | ||||||
|  |         power_switches = [s for s in switches if s.name == "Power Switch"] | ||||||
|  |         power_object_ids = sorted([s.object_id for s in power_switches]) | ||||||
|  |  | ||||||
|  |         # Should have power_switch, power_switch_2 | ||||||
|  |         assert len(power_object_ids) >= 2, ( | ||||||
|  |             f"Expected at least 2 power switches, got {len(power_object_ids)}" | ||||||
|  |         ) | ||||||
|  |         assert "power_switch" in power_object_ids, ( | ||||||
|  |             "First power switch should be power_switch" | ||||||
|  |         ) | ||||||
|  |         assert "power_switch_2" in power_object_ids, ( | ||||||
|  |             "Second power switch should be power_switch_2" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify we can get states for all entities (ensures they're functional) | ||||||
|  |         loop = asyncio.get_running_loop() | ||||||
|  |         states_future: asyncio.Future[bool] = loop.create_future() | ||||||
|  |         state_count = 0 | ||||||
|  |         expected_count = ( | ||||||
|  |             len(sensors) + len(binary_sensors) + len(text_sensors) + len(switches) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         def on_state(state) -> None: | ||||||
|  |             nonlocal state_count | ||||||
|  |             state_count += 1 | ||||||
|  |             if state_count >= expected_count and not states_future.done(): | ||||||
|  |                 states_future.set_result(True) | ||||||
|  |  | ||||||
|  |         client.subscribe_states(on_state) | ||||||
|  |  | ||||||
|  |         # Wait for all entity states | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(states_future, timeout=10.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Did not receive all entity states within 10 seconds. " | ||||||
|  |                 f"Expected {expected_count}, received {state_count}" | ||||||
|  |             ) | ||||||
							
								
								
									
										140
									
								
								tests/unit_tests/test_get_base_entity_object_id.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								tests/unit_tests/test_get_base_entity_object_id.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | """Test get_base_entity_object_id function matches C++ behavior.""" | ||||||
|  |  | ||||||
|  | from esphome.core import CORE | ||||||
|  | from esphome.entity import get_base_entity_object_id | ||||||
|  | from esphome.helpers import sanitize, snake_case | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestGetBaseEntityObjectId: | ||||||
|  |     """Test that get_base_entity_object_id matches C++ EntityBase::get_object_id behavior.""" | ||||||
|  |  | ||||||
|  |     def test_with_entity_name(self) -> None: | ||||||
|  |         """Test when entity has its own name - should use entity name.""" | ||||||
|  |         # Simple name | ||||||
|  |         assert ( | ||||||
|  |             get_base_entity_object_id("Temperature Sensor", None) | ||||||
|  |             == "temperature_sensor" | ||||||
|  |         ) | ||||||
|  |         assert ( | ||||||
|  |             get_base_entity_object_id("Temperature Sensor", "Device Name") | ||||||
|  |             == "temperature_sensor" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Name with special characters | ||||||
|  |         assert ( | ||||||
|  |             get_base_entity_object_id("Temp!@#$%^&*()Sensor", None) | ||||||
|  |             == "temp__________sensor" | ||||||
|  |         ) | ||||||
|  |         assert get_base_entity_object_id("Temp-Sensor_123", None) == "temp-sensor_123" | ||||||
|  |  | ||||||
|  |         # Already snake_case | ||||||
|  |         assert ( | ||||||
|  |             get_base_entity_object_id("temperature_sensor", None) | ||||||
|  |             == "temperature_sensor" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Mixed case | ||||||
|  |         assert ( | ||||||
|  |             get_base_entity_object_id("TemperatureSensor", None) == "temperaturesensor" | ||||||
|  |         ) | ||||||
|  |         assert ( | ||||||
|  |             get_base_entity_object_id("TEMPERATURE SENSOR", None) | ||||||
|  |             == "temperature_sensor" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_empty_name_with_friendly_name(self) -> None: | ||||||
|  |         """Test when entity has empty name - should use friendly name.""" | ||||||
|  |         # C++ behavior: when has_own_name is false, uses App.get_friendly_name() | ||||||
|  |         assert get_base_entity_object_id("", "Friendly Device") == "friendly_device" | ||||||
|  |         assert ( | ||||||
|  |             get_base_entity_object_id("", "Kitchen Controller") == "kitchen_controller" | ||||||
|  |         ) | ||||||
|  |         assert get_base_entity_object_id("", "Test-Device_123") == "test-device_123" | ||||||
|  |  | ||||||
|  |         # Special characters in friendly name | ||||||
|  |         assert get_base_entity_object_id("", "Device!@#$%") == "device_____" | ||||||
|  |  | ||||||
|  |     def test_empty_name_no_friendly_name(self) -> None: | ||||||
|  |         """Test when entity has empty name and no friendly name - should use device name.""" | ||||||
|  |         # Save original values | ||||||
|  |         original_name = getattr(CORE, "name", None) | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             # Test with CORE.name set | ||||||
|  |             CORE.name = "device-name" | ||||||
|  |             assert get_base_entity_object_id("", None) == "device-name" | ||||||
|  |  | ||||||
|  |             CORE.name = "Test Device" | ||||||
|  |             assert get_base_entity_object_id("", None) == "test_device" | ||||||
|  |  | ||||||
|  |         finally: | ||||||
|  |             # Restore original value | ||||||
|  |             if original_name is not None: | ||||||
|  |                 CORE.name = original_name | ||||||
|  |  | ||||||
|  |     def test_edge_cases(self) -> None: | ||||||
|  |         """Test edge cases.""" | ||||||
|  |         # Only spaces | ||||||
|  |         assert get_base_entity_object_id("   ", None) == "___" | ||||||
|  |  | ||||||
|  |         # Unicode characters (should be replaced) | ||||||
|  |         assert get_base_entity_object_id("Température", None) == "temp_rature" | ||||||
|  |         assert get_base_entity_object_id("测试", None) == "__" | ||||||
|  |  | ||||||
|  |         # Empty string with empty friendly name (empty friendly name is treated as None) | ||||||
|  |         # Falls back to CORE.name | ||||||
|  |         original_name = getattr(CORE, "name", None) | ||||||
|  |         try: | ||||||
|  |             CORE.name = "device" | ||||||
|  |             assert get_base_entity_object_id("", "") == "device" | ||||||
|  |         finally: | ||||||
|  |             if original_name is not None: | ||||||
|  |                 CORE.name = original_name | ||||||
|  |  | ||||||
|  |         # Very long name (should work fine) | ||||||
|  |         long_name = "a" * 100 + " " + "b" * 100 | ||||||
|  |         expected = "a" * 100 + "_" + "b" * 100 | ||||||
|  |         assert get_base_entity_object_id(long_name, None) == expected | ||||||
|  |  | ||||||
|  |     def test_matches_cpp_helpers(self) -> None: | ||||||
|  |         """Test that the logic matches using snake_case and sanitize directly.""" | ||||||
|  |         test_cases = [ | ||||||
|  |             ("Temperature Sensor", "temperature_sensor"), | ||||||
|  |             ("Living Room Light", "living_room_light"), | ||||||
|  |             ("Test-Device_123", "test-device_123"), | ||||||
|  |             ("Special!@#Chars", "special___chars"), | ||||||
|  |             ("UPPERCASE NAME", "uppercase_name"), | ||||||
|  |             ("lowercase name", "lowercase_name"), | ||||||
|  |             ("Mixed Case Name", "mixed_case_name"), | ||||||
|  |             ("   Spaces   ", "___spaces___"), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         for name, expected in test_cases: | ||||||
|  |             # For non-empty names, verify our function produces same result as direct snake_case + sanitize | ||||||
|  |             assert get_base_entity_object_id(name, None) == sanitize(snake_case(name)) | ||||||
|  |             assert get_base_entity_object_id(name, None) == expected | ||||||
|  |  | ||||||
|  |         # Empty name is handled specially - it doesn't just use sanitize(snake_case("")) | ||||||
|  |         # Instead it falls back to friendly_name or CORE.name | ||||||
|  |         assert sanitize(snake_case("")) == ""  # Direct conversion gives empty string | ||||||
|  |         # But our function returns a fallback | ||||||
|  |         original_name = getattr(CORE, "name", None) | ||||||
|  |         try: | ||||||
|  |             CORE.name = "device" | ||||||
|  |             assert get_base_entity_object_id("", None) == "device"  # Uses device name | ||||||
|  |         finally: | ||||||
|  |             if original_name is not None: | ||||||
|  |                 CORE.name = original_name | ||||||
|  |  | ||||||
|  |     def test_name_add_mac_suffix_behavior(self) -> None: | ||||||
|  |         """Test behavior related to name_add_mac_suffix. | ||||||
|  |  | ||||||
|  |         In C++, when name_add_mac_suffix is enabled and entity has no name, | ||||||
|  |         get_object_id() returns str_sanitize(str_snake_case(App.get_friendly_name())) | ||||||
|  |         dynamically. Our function always returns the same result since we're | ||||||
|  |         calculating the base for duplicate tracking. | ||||||
|  |         """ | ||||||
|  |         # The function should always return the same result regardless of | ||||||
|  |         # name_add_mac_suffix setting, as we're calculating the base object_id | ||||||
|  |         assert get_base_entity_object_id("", "Test Device") == "test_device" | ||||||
|  |         assert get_base_entity_object_id("Entity Name", "Test Device") == "entity_name" | ||||||
		Reference in New Issue
	
	Block a user