mirror of
https://github.com/esphome/esphome.git
synced 2026-09-26 06:20:21 +00:00
dry up tests
This commit is contained in:
@@ -51,6 +51,9 @@ if platform.system() == "Windows":
|
||||
|
||||
import pty # not available on Windows
|
||||
|
||||
# Register assert rewrite for entity_utils so assertions have proper error messages
|
||||
pytest.register_assert_rewrite("tests.integration.entity_utils")
|
||||
|
||||
|
||||
def _get_platformio_env(cache_dir: Path) -> dict[str, str]:
|
||||
"""Get environment variables for PlatformIO with shared cache."""
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Utilities for computing entity object_id in integration tests.
|
||||
|
||||
This module contains the algorithm that aioesphomeapi will use to compute
|
||||
object_id client-side from API data.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aioesphomeapi import DeviceInfo, EntityInfo
|
||||
|
||||
|
||||
def compute_object_id(name: str) -> str:
|
||||
"""Compute object_id from name using snake_case + sanitize."""
|
||||
return sanitize(snake_case(name))
|
||||
|
||||
|
||||
def infer_name_add_mac_suffix(device_info: DeviceInfo) -> bool:
|
||||
"""Infer name_add_mac_suffix from device name ending with MAC suffix."""
|
||||
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
|
||||
return device_info.name.endswith(f"-{mac_suffix}")
|
||||
|
||||
|
||||
def compute_entity_object_id(
|
||||
entity: EntityInfo,
|
||||
device_info: DeviceInfo,
|
||||
device_id_to_name: dict[int, str],
|
||||
) -> str:
|
||||
"""Compute expected object_id for an entity using the algorithm from PR summary.
|
||||
|
||||
This is the algorithm that aioesphomeapi will use to compute object_id
|
||||
client-side from API data.
|
||||
|
||||
Args:
|
||||
entity: The entity to compute object_id for
|
||||
device_info: Device info from the API
|
||||
device_id_to_name: Mapping of device_id to device name for sub-devices
|
||||
|
||||
Returns:
|
||||
The computed object_id string
|
||||
"""
|
||||
name_add_mac_suffix = infer_name_add_mac_suffix(device_info)
|
||||
|
||||
if entity.name:
|
||||
# Named entity: use entity name
|
||||
name_for_id = entity.name
|
||||
elif entity.device_id != 0:
|
||||
# Empty name on sub-device: use sub-device name
|
||||
name_for_id = device_id_to_name[entity.device_id]
|
||||
elif name_add_mac_suffix:
|
||||
# Empty name on main device with MAC suffix: use friendly_name directly
|
||||
# (even if empty - this is bug-for-bug compatibility)
|
||||
name_for_id = device_info.friendly_name
|
||||
elif device_info.friendly_name:
|
||||
# Empty name on main device with friendly_name set: use it
|
||||
name_for_id = device_info.friendly_name
|
||||
else:
|
||||
# Empty name on main device, no friendly_name: use device name
|
||||
name_for_id = device_info.name
|
||||
|
||||
return compute_object_id(name_for_id)
|
||||
|
||||
|
||||
def compute_entity_hash(
|
||||
entity: EntityInfo,
|
||||
device_info: DeviceInfo,
|
||||
device_id_to_name: dict[int, str],
|
||||
) -> int:
|
||||
"""Compute expected object_id hash for an entity.
|
||||
|
||||
Args:
|
||||
entity: The entity to compute hash for
|
||||
device_info: Device info from the API
|
||||
device_id_to_name: Mapping of device_id to device name for sub-devices
|
||||
|
||||
Returns:
|
||||
The computed FNV-1 hash
|
||||
"""
|
||||
name_add_mac_suffix = infer_name_add_mac_suffix(device_info)
|
||||
|
||||
if entity.name:
|
||||
name_for_id = entity.name
|
||||
elif entity.device_id != 0:
|
||||
name_for_id = device_id_to_name[entity.device_id]
|
||||
elif name_add_mac_suffix or device_info.friendly_name:
|
||||
name_for_id = device_info.friendly_name
|
||||
else:
|
||||
name_for_id = device_info.name
|
||||
|
||||
return fnv1_hash_object_id(name_for_id)
|
||||
|
||||
|
||||
def verify_entity_object_id(
|
||||
entity: EntityInfo,
|
||||
device_info: DeviceInfo,
|
||||
device_id_to_name: dict[int, str],
|
||||
) -> None:
|
||||
"""Verify an entity's object_id and hash match the expected values.
|
||||
|
||||
Args:
|
||||
entity: The entity to verify
|
||||
device_info: Device info from the API
|
||||
device_id_to_name: Mapping of device_id to device name for sub-devices
|
||||
|
||||
Raises:
|
||||
AssertionError: If object_id or hash doesn't match expected value
|
||||
"""
|
||||
expected_object_id = compute_entity_object_id(
|
||||
entity, device_info, device_id_to_name
|
||||
)
|
||||
assert entity.object_id == expected_object_id, (
|
||||
f"object_id mismatch for entity '{entity.name}': "
|
||||
f"expected '{expected_object_id}', got '{entity.object_id}'"
|
||||
)
|
||||
|
||||
expected_hash = compute_entity_hash(entity, device_info, device_id_to_name)
|
||||
assert entity.key == expected_hash, (
|
||||
f"hash mismatch for entity '{entity.name}': "
|
||||
f"expected {expected_hash:#x}, got {entity.key:#x}"
|
||||
)
|
||||
|
||||
|
||||
def verify_all_entities(
|
||||
entities: list[EntityInfo],
|
||||
device_info: DeviceInfo,
|
||||
) -> None:
|
||||
"""Verify all entities have correct object_id and hash values.
|
||||
|
||||
Args:
|
||||
entities: List of entities to verify
|
||||
device_info: Device info from the API
|
||||
|
||||
Raises:
|
||||
AssertionError: If any entity's object_id or hash doesn't match
|
||||
"""
|
||||
# Build device_id -> name lookup from sub-devices
|
||||
device_id_to_name = {d.device_id: d.name for d in device_info.devices}
|
||||
|
||||
for entity in entities:
|
||||
verify_entity_object_id(entity, device_info, device_id_to_name)
|
||||
@@ -25,8 +25,9 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
|
||||
from esphome.helpers import fnv1_hash_object_id
|
||||
|
||||
from .entity_utils import compute_object_id, verify_all_entities
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679"
|
||||
@@ -62,11 +63,6 @@ SUB_DEVICE_EMPTY_NAME_ENTITIES = [
|
||||
]
|
||||
|
||||
|
||||
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_api_verification(
|
||||
yaml_config: str,
|
||||
@@ -120,7 +116,7 @@ async def test_object_id_api_verification(
|
||||
)
|
||||
|
||||
# Verify Python computation matches
|
||||
computed = compute_expected_object_id(entity_name)
|
||||
computed = compute_object_id(entity_name)
|
||||
assert computed == expected_object_id, (
|
||||
f"Entity '{entity_name}': Python computation mismatch. "
|
||||
f"Computed '{computed}', expected '{expected_object_id}'"
|
||||
@@ -160,7 +156,7 @@ async def test_object_id_api_verification(
|
||||
)
|
||||
expected_name = device_id_to_name[entity.device_id]
|
||||
|
||||
expected_object_id = compute_expected_object_id(expected_name)
|
||||
expected_object_id = compute_object_id(expected_name)
|
||||
assert entity.object_id == expected_object_id, (
|
||||
f"Empty-name entity (device_id={entity.device_id}): object_id mismatch. "
|
||||
f"API: '{entity.object_id}', expected: '{expected_object_id}' "
|
||||
@@ -174,44 +170,7 @@ async def test_object_id_api_verification(
|
||||
f"API key: {entity.key:#x}, expected: {expected_hash:#x}"
|
||||
)
|
||||
|
||||
# === Test 3: Verify ALL entities can have object_id computed from API data ===
|
||||
# This uses the algorithm from the PR summary that aioesphomeapi will use.
|
||||
# Infer name_add_mac_suffix from device name ending with MAC suffix.
|
||||
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
|
||||
name_add_mac_suffix = device_info.name.endswith(f"-{mac_suffix}")
|
||||
|
||||
for entity in entities:
|
||||
if entity.name:
|
||||
# Named entity: use entity name
|
||||
name_for_id = entity.name
|
||||
elif entity.device_id != 0:
|
||||
# Empty name on sub-device: use sub-device name
|
||||
name_for_id = device_id_to_name[entity.device_id]
|
||||
elif name_add_mac_suffix:
|
||||
# Empty name on main device with MAC suffix: use friendly_name directly
|
||||
# (even if empty - this is bug-for-bug compatibility)
|
||||
name_for_id = device_info.friendly_name
|
||||
elif device_info.friendly_name:
|
||||
# Empty name on main device with friendly_name set: use it
|
||||
name_for_id = device_info.friendly_name
|
||||
else:
|
||||
# Empty name on main device, no friendly_name: use device name
|
||||
name_for_id = device_info.name
|
||||
|
||||
# Compute object_id from the appropriate name
|
||||
computed_object_id = compute_expected_object_id(name_for_id)
|
||||
|
||||
# Verify it matches what the API returned
|
||||
assert entity.object_id == computed_object_id, (
|
||||
f"Entity (name='{entity.name}', device_id={entity.device_id}): "
|
||||
f"object_id cannot be computed. "
|
||||
f"API: '{entity.object_id}', Computed from '{name_for_id}': '{computed_object_id}'"
|
||||
)
|
||||
|
||||
# Verify hash can also be computed
|
||||
computed_hash = fnv1_hash_object_id(name_for_id)
|
||||
assert entity.key == computed_hash, (
|
||||
f"Entity (name='{entity.name}', device_id={entity.device_id}): "
|
||||
f"hash cannot be computed. "
|
||||
f"API key: {entity.key:#x}, Computed: {computed_hash:#x}"
|
||||
)
|
||||
# === Test 3: Verify ALL entities using the algorithm from entity_utils ===
|
||||
# This uses the algorithm that aioesphomeapi will use to compute object_id
|
||||
# client-side from API data.
|
||||
verify_all_entities(entities, device_info)
|
||||
|
||||
@@ -11,16 +11,16 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
|
||||
from esphome.helpers import fnv1_hash_object_id
|
||||
|
||||
from .entity_utils import (
|
||||
compute_object_id,
|
||||
infer_name_add_mac_suffix,
|
||||
verify_all_entities,
|
||||
)
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
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_friendly_name_no_mac_suffix(
|
||||
yaml_config: str,
|
||||
@@ -54,7 +54,7 @@ async def test_object_id_friendly_name_no_mac_suffix(
|
||||
entity = empty_name_entities[0]
|
||||
|
||||
# Should use friendly_name for object_id (Branch 4)
|
||||
expected_object_id = compute_expected_object_id("My Friendly Device")
|
||||
expected_object_id = compute_object_id("My Friendly Device")
|
||||
assert expected_object_id == "my_friendly_device" # Verify our expectation
|
||||
assert entity.object_id == expected_object_id, (
|
||||
f"Expected object_id '{expected_object_id}' from friendly_name, "
|
||||
@@ -72,35 +72,10 @@ async def test_object_id_friendly_name_no_mac_suffix(
|
||||
assert len(named_entities) == 1
|
||||
assert named_entities[0].object_id == "temperature"
|
||||
|
||||
# Verify the full algorithm from PR summary works for ALL entities
|
||||
# Infer name_add_mac_suffix from device name ending with MAC suffix.
|
||||
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
|
||||
name_add_mac_suffix = device_info.name.endswith(f"-{mac_suffix}")
|
||||
|
||||
# Verify our inference: no MAC suffix in this test
|
||||
assert not name_add_mac_suffix, "Device name should NOT have MAC suffix"
|
||||
assert not infer_name_add_mac_suffix(device_info), (
|
||||
"Device name should NOT have MAC suffix"
|
||||
)
|
||||
|
||||
for entity in entities:
|
||||
if entity.name:
|
||||
name_for_id = entity.name
|
||||
elif name_add_mac_suffix:
|
||||
# MAC suffix enabled: use friendly_name directly (even if empty)
|
||||
name_for_id = device_info.friendly_name
|
||||
elif device_info.friendly_name:
|
||||
# Branch 4: No MAC suffix, but friendly_name is set
|
||||
name_for_id = device_info.friendly_name
|
||||
else:
|
||||
# No MAC suffix, no friendly_name: use device name
|
||||
name_for_id = device_info.name
|
||||
|
||||
computed_object_id = compute_expected_object_id(name_for_id)
|
||||
assert entity.object_id == computed_object_id, (
|
||||
f"Algorithm failed for entity '{entity.name}': "
|
||||
f"expected '{computed_object_id}', got '{entity.object_id}'"
|
||||
)
|
||||
|
||||
computed_hash = fnv1_hash_object_id(name_for_id)
|
||||
assert entity.key == computed_hash, (
|
||||
f"Algorithm hash failed for entity '{entity.name}': "
|
||||
f"expected {computed_hash:#x}, got {entity.key:#x}"
|
||||
)
|
||||
# Verify the full algorithm from entity_utils works for ALL entities
|
||||
verify_all_entities(entities, device_info)
|
||||
|
||||
@@ -17,8 +17,9 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
|
||||
from esphome.helpers import fnv1_hash_object_id
|
||||
|
||||
from .entity_utils import compute_object_id, verify_all_entities
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679"
|
||||
@@ -28,11 +29,6 @@ MAC_SUFFIX = "abf679"
|
||||
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,
|
||||
@@ -85,33 +81,8 @@ async def test_object_id_no_friendly_name_with_mac_suffix(
|
||||
assert len(named_entities) == 1
|
||||
assert named_entities[0].object_id == "temperature"
|
||||
|
||||
# Verify the full algorithm from PR summary works for ALL entities
|
||||
# Infer name_add_mac_suffix from device name ending with MAC suffix.
|
||||
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
|
||||
name_add_mac_suffix = device_info.name.endswith(f"-{mac_suffix}")
|
||||
|
||||
for entity in entities:
|
||||
if entity.name:
|
||||
name_for_id = entity.name
|
||||
elif name_add_mac_suffix:
|
||||
# MAC suffix enabled: use friendly_name directly (even if empty)
|
||||
name_for_id = device_info.friendly_name
|
||||
elif device_info.friendly_name:
|
||||
name_for_id = device_info.friendly_name
|
||||
else:
|
||||
name_for_id = device_info.name
|
||||
|
||||
computed_object_id = compute_expected_object_id(name_for_id)
|
||||
assert entity.object_id == computed_object_id, (
|
||||
f"Algorithm failed for entity '{entity.name}': "
|
||||
f"expected '{computed_object_id}', got '{entity.object_id}'"
|
||||
)
|
||||
|
||||
computed_hash = fnv1_hash_object_id(name_for_id)
|
||||
assert entity.key == computed_hash, (
|
||||
f"Algorithm hash failed for entity '{entity.name}': "
|
||||
f"expected {computed_hash:#x}, got {entity.key:#x}"
|
||||
)
|
||||
# Verify the full algorithm from entity_utils works for ALL entities
|
||||
verify_all_entities(entities, device_info)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -148,7 +119,7 @@ async def test_object_id_no_friendly_name_no_mac_suffix(
|
||||
entity = empty_name_entities[0]
|
||||
|
||||
# OLD behavior: object_id was computed from device name
|
||||
expected_object_id = compute_expected_object_id("test-device")
|
||||
expected_object_id = compute_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}'"
|
||||
@@ -165,31 +136,5 @@ async def test_object_id_no_friendly_name_no_mac_suffix(
|
||||
assert len(named_entities) == 1
|
||||
assert named_entities[0].object_id == "temperature"
|
||||
|
||||
# Verify the full algorithm from PR summary works for ALL entities
|
||||
# Infer name_add_mac_suffix from device name ending with MAC suffix.
|
||||
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
|
||||
name_add_mac_suffix = device_info.name.endswith(f"-{mac_suffix}")
|
||||
|
||||
for entity in entities:
|
||||
if entity.name:
|
||||
name_for_id = entity.name
|
||||
elif name_add_mac_suffix:
|
||||
# MAC suffix enabled: use friendly_name directly (even if empty)
|
||||
name_for_id = device_info.friendly_name
|
||||
elif device_info.friendly_name:
|
||||
name_for_id = device_info.friendly_name
|
||||
else:
|
||||
# No MAC suffix, no friendly_name: use device name
|
||||
name_for_id = device_info.name
|
||||
|
||||
computed_object_id = compute_expected_object_id(name_for_id)
|
||||
assert entity.object_id == computed_object_id, (
|
||||
f"Algorithm failed for entity '{entity.name}': "
|
||||
f"expected '{computed_object_id}', got '{entity.object_id}'"
|
||||
)
|
||||
|
||||
computed_hash = fnv1_hash_object_id(name_for_id)
|
||||
assert entity.key == computed_hash, (
|
||||
f"Algorithm hash failed for entity '{entity.name}': "
|
||||
f"expected {computed_hash:#x}, got {entity.key:#x}"
|
||||
)
|
||||
# Verify the full algorithm from entity_utils works for ALL entities
|
||||
verify_all_entities(entities, device_info)
|
||||
|
||||
Reference in New Issue
Block a user