Optimize entity icon memory usage with USE_ENTITY_ICON flag (#9337)

This commit is contained in:
J. Nick Koston 2025-07-07 15:22:40 -05:00 committed by GitHub
parent 31f36df4ba
commit 053feb5e3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 192 additions and 1 deletions

View File

@ -33,6 +33,7 @@
#define USE_DEEP_SLEEP #define USE_DEEP_SLEEP
#define USE_DEVICES #define USE_DEVICES
#define USE_DISPLAY #define USE_DISPLAY
#define USE_ENTITY_ICON
#define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT #define USE_EVENT
#define USE_FAN #define USE_FAN

View File

@ -27,12 +27,22 @@ void EntityBase::set_name(const char *name) {
// Entity Icon // Entity Icon
std::string EntityBase::get_icon() const { std::string EntityBase::get_icon() const {
#ifdef USE_ENTITY_ICON
if (this->icon_c_str_ == nullptr) { if (this->icon_c_str_ == nullptr) {
return ""; return "";
} }
return this->icon_c_str_; return this->icon_c_str_;
#else
return "";
#endif
}
void EntityBase::set_icon(const char *icon) {
#ifdef USE_ENTITY_ICON
this->icon_c_str_ = icon;
#else
// No-op when USE_ENTITY_ICON is not defined
#endif
} }
void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; }
// Entity Object ID // Entity Object ID
std::string EntityBase::get_object_id() const { std::string EntityBase::get_object_id() const {

View File

@ -80,7 +80,9 @@ class EntityBase {
StringRef name_; StringRef name_;
const char *object_id_c_str_{nullptr}; const char *object_id_c_str_{nullptr};
#ifdef USE_ENTITY_ICON
const char *icon_c_str_{nullptr}; const char *icon_c_str_{nullptr};
#endif
uint32_t object_id_hash_{}; uint32_t object_id_hash_{};
#ifdef USE_DEVICES #ifdef USE_DEVICES
Device *device_{}; Device *device_{};

View File

@ -1,6 +1,7 @@
from collections.abc import Callable from collections.abc import Callable
import logging import logging
import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_DEVICE_ID, CONF_DEVICE_ID,
@ -108,6 +109,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
if CONF_INTERNAL in config: if CONF_INTERNAL in config:
add(var.set_internal(config[CONF_INTERNAL])) add(var.set_internal(config[CONF_INTERNAL]))
if CONF_ICON in config: if CONF_ICON in config:
# Add USE_ENTITY_ICON define when icons are used
cg.add_define("USE_ENTITY_ICON")
add(var.set_icon(config[CONF_ICON])) add(var.set_icon(config[CONF_ICON]))
if CONF_ENTITY_CATEGORY in config: if CONF_ENTITY_CATEGORY in config:
add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))

View File

@ -0,0 +1,78 @@
esphome:
name: icon-test
host:
api:
logger:
# Test entities with custom icons
sensor:
- platform: template
name: "Sensor With Icon"
icon: "mdi:temperature-celsius"
unit_of_measurement: "°C"
update_interval: 1s
lambda: |-
return 25.5;
- platform: template
name: "Sensor Without Icon"
unit_of_measurement: "%"
update_interval: 1s
lambda: |-
return 50.0;
binary_sensor:
- platform: template
name: "Binary Sensor With Icon"
icon: "mdi:motion-sensor"
lambda: |-
return true;
- platform: template
name: "Binary Sensor Without Icon"
lambda: |-
return false;
text_sensor:
- platform: template
name: "Text Sensor With Icon"
icon: "mdi:text-box"
lambda: |-
return {"Hello Icons"};
switch:
- platform: template
name: "Switch With Icon"
icon: "mdi:toggle-switch"
optimistic: true
button:
- platform: template
name: "Button With Icon"
icon: "mdi:gesture-tap-button"
on_press:
- logger.log: "Button with icon pressed"
number:
- platform: template
name: "Number With Icon"
icon: "mdi:numeric"
initial_value: 42
min_value: 0
max_value: 100
step: 1
optimistic: true
select:
- platform: template
name: "Select With Icon"
icon: "mdi:format-list-bulleted"
options:
- "Option A"
- "Option B"
- "Option C"
initial_option: "Option A"
optimistic: true

View File

@ -0,0 +1,97 @@
"""Integration test for entity icons with USE_ENTITY_ICON feature."""
from __future__ import annotations
import asyncio
from aioesphomeapi import EntityState
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_entity_icon(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that entities with custom icons work correctly with USE_ENTITY_ICON."""
# Write, compile and run the ESPHome device, then connect to API
async with run_compiled(yaml_config), api_client_connected() as client:
# Get all entities
entities = await client.list_entities_services()
# Create a map of entity names to entity info
entity_map = {entity.name: entity for entity in entities[0]}
# Test entities with icons
icon_test_cases = [
# (entity_name, expected_icon)
("Sensor With Icon", "mdi:temperature-celsius"),
("Binary Sensor With Icon", "mdi:motion-sensor"),
("Text Sensor With Icon", "mdi:text-box"),
("Switch With Icon", "mdi:toggle-switch"),
("Button With Icon", "mdi:gesture-tap-button"),
("Number With Icon", "mdi:numeric"),
("Select With Icon", "mdi:format-list-bulleted"),
]
# Test entities without icons (should have empty string)
no_icon_test_cases = [
"Sensor Without Icon",
"Binary Sensor Without Icon",
]
# Verify entities with icons
for entity_name, expected_icon in icon_test_cases:
assert entity_name in entity_map, (
f"Entity '{entity_name}' not found in API response"
)
entity = entity_map[entity_name]
# Check icon field
assert hasattr(entity, "icon"), (
f"{entity_name}: Entity should have icon attribute"
)
assert entity.icon == expected_icon, (
f"{entity_name}: icon mismatch - "
f"expected '{expected_icon}', got '{entity.icon}'"
)
# Verify entities without icons
for entity_name in no_icon_test_cases:
assert entity_name in entity_map, (
f"Entity '{entity_name}' not found in API response"
)
entity = entity_map[entity_name]
# Check icon field is empty
assert hasattr(entity, "icon"), (
f"{entity_name}: Entity should have icon attribute"
)
assert entity.icon == "", (
f"{entity_name}: icon should be empty string for entities without icons, "
f"got '{entity.icon}'"
)
# Subscribe to states to ensure everything works normally
states: dict[int, EntityState] = {}
state_received = asyncio.Event()
def on_state(state: EntityState) -> None:
states[state.key] = state
state_received.set()
client.subscribe_states(on_state)
# Wait for states
try:
await asyncio.wait_for(state_received.wait(), timeout=5.0)
except asyncio.TimeoutError:
pytest.fail("No states received within 5 seconds")
# Verify we received states
assert len(states) > 0, (
"No states received - entities may not be working correctly"
)