mirror of
https://github.com/esphome/esphome.git
synced 2025-07-29 06:36:45 +00:00
Optimize entity icon memory usage with USE_ENTITY_ICON flag (#9337)
This commit is contained in:
parent
31f36df4ba
commit
053feb5e3b
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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_{};
|
||||||
|
@ -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]))
|
||||||
|
78
tests/integration/fixtures/entity_icon.yaml
Normal file
78
tests/integration/fixtures/entity_icon.yaml
Normal 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
|
97
tests/integration/test_entity_icon.py
Normal file
97
tests/integration/test_entity_icon.py
Normal 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"
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user