move icons to progmem

This commit is contained in:
J. Nick Koston
2026-03-03 08:04:09 -10:00
parent 95544dddf8
commit 807e3f9efc
8 changed files with 112 additions and 29 deletions
+2 -1
View File
@@ -348,7 +348,8 @@ class APIConnection final : public APIServerConnectionBase {
// Set common EntityBase properties
#ifdef USE_ENTITY_ICON
msg.icon = entity->get_icon_ref();
char icon_buf[MAX_ICON_LENGTH];
msg.icon = entity->get_icon_to(icon_buf);
#endif
msg.disabled_by_default = entity->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
+4 -6
View File
@@ -209,12 +209,11 @@ bool MQTTComponent::send_discovery_() {
if (this->is_disabled_by_default_())
root[MQTT_ENABLED_BY_DEFAULT] = false;
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
const auto icon_ref = this->get_icon_ref_();
if (!icon_ref.empty()) {
root[MQTT_ICON] = icon_ref;
char icon_buf[MAX_ICON_LENGTH];
const char *icon = this->get_icon_to_(icon_buf);
if (icon[0] != '\0') {
root[MQTT_ICON] = icon;
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
const auto entity_category = this->get_entity()->get_entity_category();
if (entity_category != ENTITY_CATEGORY_NONE) {
@@ -413,7 +412,6 @@ const StringRef &MQTTComponent::friendly_name_() const { return this->get_entity
StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const {
return this->get_entity()->get_object_id_to(buf);
}
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
bool MQTTComponent::compute_is_internal_() {
if (this->custom_state_topic_.has_value()) {
+2 -2
View File
@@ -298,8 +298,8 @@ class MQTTComponent : public Component {
/// Get the friendly name of this MQTT component.
const StringRef &friendly_name_() const;
/// Get the icon field of this component as StringRef
StringRef get_icon_ref_() const;
/// Get the icon field of this component into a stack buffer
const char *get_icon_to_(std::span<char, MAX_ICON_LENGTH> buf) const { return this->get_entity()->get_icon_to(buf); }
/// Get whether the underlying Entity is disabled by default
bool is_disabled_by_default_() const;
+2 -1
View File
@@ -568,7 +568,8 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
}
#endif
#ifdef USE_ENTITY_ICON
root[ESPHOME_F("icon")] = obj->get_icon_ref().c_str();
char icon_buf[MAX_ICON_LENGTH];
root[ESPHOME_F("icon")] = obj->get_icon_to(icon_buf);
#endif
root[ESPHOME_F("entity_category")] = obj->get_entity_category();
bool is_disabled = obj->is_disabled_by_default();
+10 -5
View File
@@ -403,11 +403,16 @@ def icon(value):
value = string_strict(value)
if not value:
return value
if re.match("^[\\w\\-]+:[\\w\\-]+$", value):
return value
raise Invalid(
'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"'
)
if not re.match("^[\\w\\-]+:[\\w\\-]+$", value):
raise Invalid(
'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"'
)
if len(value) > 63:
raise Invalid(
f"Icon string is too long ({len(value)} chars, max 63). "
"Icons are stored in PROGMEM with a 64-byte buffer limit."
)
return value
def sub_device_id(value: str | None) -> core.ID | None:
+24 -3
View File
@@ -1,6 +1,7 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome {
@@ -72,7 +73,24 @@ std::string EntityBase::get_unit_of_measurement() const {
return std::string(this->get_unit_of_measurement_ref().c_str());
}
// Entity icon (from index)
// Entity icon — buffer-based API for PROGMEM safety on ESP8266
const char *EntityBase::get_icon_to(std::span<char, MAX_ICON_LENGTH> buffer) const {
#ifdef USE_ENTITY_ICON
const char *icon = entity_icon_lookup(this->icon_idx_);
#else
const char *icon = entity_icon_lookup(0);
#endif
#ifdef USE_ESP8266
ESPHOME_strncpy_P(buffer.data(), icon, buffer.size() - 1);
buffer[buffer.size() - 1] = '\0';
return buffer.data();
#else
return icon;
#endif
}
#ifndef USE_ESP8266
// Deprecated icon accessors — not available on ESP8266 (rodata is RAM)
StringRef EntityBase::get_icon_ref() const {
#ifdef USE_ENTITY_ICON
return StringRef(entity_icon_lookup(this->icon_idx_));
@@ -81,6 +99,7 @@ StringRef EntityBase::get_icon_ref() const {
#endif
}
std::string EntityBase::get_icon() const { return std::string(this->get_icon_ref().c_str()); }
#endif // !USE_ESP8266
// Entity Object ID - computed on-demand from name
std::string EntityBase::get_object_id() const {
@@ -154,8 +173,10 @@ ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t ve
#ifdef USE_ENTITY_ICON
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) {
if (!obj.get_icon_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str());
char icon_buf[MAX_ICON_LENGTH];
const char *icon = obj.get_icon_to(icon_buf);
if (icon[0] != '\0') {
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, icon);
}
}
#endif
+28 -5
View File
@@ -36,6 +36,10 @@ static constexpr size_t OBJECT_ID_MAX_LEN = 128;
// Maximum state length that Home Assistant will accept without raising ValueError
static constexpr size_t MAX_STATE_LEN = 255;
// Maximum icon string buffer size (63 chars + null terminator)
// Icons are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer.
static constexpr size_t MAX_ICON_LENGTH = 64;
enum EntityCategory : uint8_t {
ENTITY_CATEGORY_NONE = 0,
ENTITY_CATEGORY_CONFIG = 1,
@@ -124,12 +128,31 @@ class EntityBase {
"2026.3.0")
std::string get_unit_of_measurement() const;
// Get/set this entity's icon
ESPDEPRECATED(
"Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0",
"2025.11.0")
std::string get_icon() const;
// Get this entity's icon into a stack buffer.
// On ESP32: returns pointer to PROGMEM string directly (buffer unused).
// On ESP8266: copies from PROGMEM to buffer, returns buffer pointer.
const char *get_icon_to(std::span<char, MAX_ICON_LENGTH> buffer) const;
#ifdef USE_ESP8266
// On ESP8266, rodata is RAM. Icons are in PROGMEM and cannot be accessed
// directly as const char*. Use get_icon_to() with a stack buffer instead.
template<typename T = void> StringRef get_icon_ref() const {
static_assert(!sizeof(T),
"get_icon_ref() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer.");
return StringRef("");
}
template<typename T = void> std::string get_icon() const {
static_assert(!sizeof(T),
"get_icon() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer.");
return "";
}
#else
// Deprecated: use get_icon_to() instead. Icons are in PROGMEM.
ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0")
StringRef get_icon_ref() const;
ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0")
std::string get_icon() const;
#endif
#ifdef USE_DEVICES
// Get/set this entity's device id
+40 -6
View File
@@ -78,6 +78,8 @@ def _generate_category_code(
table_var: str,
lookup_fn: str,
strings: dict[str, int],
*,
progmem_strings: bool = False,
) -> str:
"""Generate C++ code for one string category (PROGMEM pointer table + lookup).
@@ -85,14 +87,37 @@ def _generate_category_code(
in flash (via PROGMEM) and read with progmem_read_ptr(). String literals
themselves remain in RAM but benefit from linker string deduplication.
Index 0 means "not set" and returns empty string.
When progmem_strings=True, each string is declared as a separate PROGMEM
char array. This ensures the string data itself is in flash on ESP8266
(where .rodata is RAM). On other platforms PROGMEM is a no-op.
"""
if not strings:
return ""
sorted_strings = sorted(strings.items(), key=lambda x: x[1])
entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings)
count = len(sorted_strings)
if progmem_strings:
# Emit individual PROGMEM char arrays so string data lives in flash
lines: list[str] = []
var_names: list[str] = []
for i, (s, _) in enumerate(sorted_strings):
var_name = f"{table_var}_STR_{i}"
var_names.append(var_name)
lines.append(
f"static const char {var_name}[] PROGMEM = {cpp_string_escape(s)};"
)
entries = ", ".join(var_names)
lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};")
lines.append(f"const char *{lookup_fn}(uint8_t index) {{")
lines.append(f' if (index == 0 || index > {count}) return "";')
lines.append(f" return progmem_read_ptr(&{table_var}[index - 1]);")
lines.append("}")
return "\n".join(lines) + "\n"
entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings)
return (
f"static const char *const {table_var}[] PROGMEM = {{{entries}}};\n"
f"const char *{lookup_fn}(uint8_t index) {{\n"
@@ -103,9 +128,9 @@ def _generate_category_code(
_CATEGORY_CONFIGS = (
("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes"),
("ENTITY_UOM_TABLE", "entity_uom_lookup", "units"),
("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons"),
("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", False),
("ENTITY_UOM_TABLE", "entity_uom_lookup", "units", False),
("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons", True),
)
@@ -117,8 +142,10 @@ async def _generate_tables_job() -> None:
"""
pool = _get_pool()
parts = ["namespace esphome {"]
for table_var, lookup_fn, attr in _CATEGORY_CONFIGS:
code = _generate_category_code(table_var, lookup_fn, getattr(pool, attr))
for table_var, lookup_fn, attr, progmem_strs in _CATEGORY_CONFIGS:
code = _generate_category_code(
table_var, lookup_fn, getattr(pool, attr), progmem_strings=progmem_strs
)
if code:
parts.append(code)
parts.append("} // namespace esphome")
@@ -158,8 +185,15 @@ def register_unit_of_measurement(value: str) -> int:
return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement")
_MAX_ICON_LENGTH = 63 # Max icon string length (64-byte buffer with null terminator)
def register_icon(value: str) -> int:
"""Register an icon string and return its 1-based index."""
if value and len(value) > _MAX_ICON_LENGTH:
raise ValueError(
f"Icon string too long ({len(value)} chars, max {_MAX_ICON_LENGTH}): '{value}'"
)
return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon")