mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 17:18:40 +00:00
move icons to progmem
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user