From e46de0c40a8de4c3b7f4d11023d88b219f1104a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:05:01 -0600 Subject: [PATCH 01/16] [core] Add PROGMEM_STRING_TABLE macro for flash-optimized string lookups --- .../components/light/light_json_schema.cpp | 37 +++------- esphome/components/logger/logger.cpp | 30 ++------ esphome/core/progmem.h | 68 +++++++++++++++++++ 3 files changed, 85 insertions(+), 50 deletions(-) diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 631f59221f..874d3d547d 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -9,32 +9,17 @@ namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema -// Get JSON string for color mode. -// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would -// generate a large jump table. Converting to bit index (0-9) allows a compact switch. -static ProgmemStr get_color_mode_json_str(ColorMode mode) { - switch (ColorModeBitPolicy::to_bit(mode)) { - case 1: - return ESPHOME_F("onoff"); - case 2: - return ESPHOME_F("brightness"); - case 3: - return ESPHOME_F("white"); - case 4: - return ESPHOME_F("color_temp"); - case 5: - return ESPHOME_F("cwww"); - case 6: - return ESPHOME_F("rgb"); - case 7: - return ESPHOME_F("rgbw"); - case 8: - return ESPHOME_F("rgbct"); - case 9: - return ESPHOME_F("rgbww"); - default: - return nullptr; - } +// Color mode JSON strings - packed into flash with compile-time generated offsets. +// Indexed by ColorModeBitPolicy bit index (1-9), so index 0 maps to bit 1 ("onoff"). +PROGMEM_STRING_TABLE(ColorModeStrings, "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", + "rgbww"); + +// Get JSON string for color mode. Returns nullptr for UNKNOWN (bit 0). +static const char *get_color_mode_json_str(ColorMode mode) { + unsigned bit = ColorModeBitPolicy::to_bit(mode); + if (bit == 0) + return nullptr; + return ColorModeStrings::get(bit - 1); } void LightJSONSchema::dump_json(LightState &state, JsonObject root) { diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 3a726d4046..c70520677a 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -4,6 +4,7 @@ #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::logger { @@ -291,34 +292,15 @@ UARTSelection Logger::get_uart() const { return this->uart_; } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } -#ifdef USE_STORE_LOG_STR_IN_FLASH -// ESP8266: PSTR() cannot be used in array initializers, so we need to declare -// each string separately as a global constant first -static const char LOG_LEVEL_NONE[] PROGMEM = "NONE"; -static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR"; -static const char LOG_LEVEL_WARN[] PROGMEM = "WARN"; -static const char LOG_LEVEL_INFO[] PROGMEM = "INFO"; -static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG"; -static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG"; -static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE"; -static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE"; - -static const LogString *const LOG_LEVELS[] = { - reinterpret_cast(LOG_LEVEL_NONE), reinterpret_cast(LOG_LEVEL_ERROR), - reinterpret_cast(LOG_LEVEL_WARN), reinterpret_cast(LOG_LEVEL_INFO), - reinterpret_cast(LOG_LEVEL_CONFIG), reinterpret_cast(LOG_LEVEL_DEBUG), - reinterpret_cast(LOG_LEVEL_VERBOSE), reinterpret_cast(LOG_LEVEL_VERY_VERBOSE), -}; -#else -static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; -#endif +// Log level strings - packed into flash on ESP8266, indexed by log level (0-7) +PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"); void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:\n" " Max Level: %s\n" " Initial Level: %s", - LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_])); + LogLevelStrings::get(ESPHOME_LOG_LEVEL), LogLevelStrings::get(this->current_level_)); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" @@ -337,7 +319,7 @@ void Logger::dump_config() { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second])); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LogLevelStrings::get(it.second)); } #endif } @@ -345,7 +327,7 @@ void Logger::dump_config() { void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; - ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); + ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LogLevelStrings::get(ESPHOME_LOG_LEVEL)); } this->current_level_ = level; #ifdef USE_LOGGER_LEVEL_LISTENERS diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 4b897fb2de..581b063be9 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -1,5 +1,9 @@ #pragma once +#include +#include +#include + // Platform-agnostic macros for PROGMEM string handling // On ESP8266/Arduino: Use Arduino's F() macro for PROGMEM strings // On other platforms: Use plain strings (no PROGMEM) @@ -16,6 +20,7 @@ #define ESPHOME_strcasecmp_P strcasecmp_P #define ESPHOME_strncmp_P strncmp_P #define ESPHOME_strncasecmp_P strncasecmp_P +#define progmem_read_byte(addr) pgm_read_byte(addr) // Type for pointers to PROGMEM strings (for use with ESPHOME_F return values) using ProgmemStr = const __FlashStringHelper *; #else @@ -29,6 +34,69 @@ using ProgmemStr = const __FlashStringHelper *; #define ESPHOME_strcasecmp_P strcasecmp #define ESPHOME_strncmp_P strncmp #define ESPHOME_strncasecmp_P strncasecmp +#define progmem_read_byte(addr) (*(addr)) // Type for pointers to strings (no PROGMEM on non-ESP8266 platforms) using ProgmemStr = const char *; #endif + +namespace esphome { + +/// Helper for C++20 string literal template arguments +template struct FixedString { + char data[N]{}; + constexpr FixedString(const char (&str)[N]) { + for (size_t i = 0; i < N; ++i) + data[i] = str[i]; + } + constexpr size_t size() const { return N - 1; } // exclude null terminator +}; + +/// Compile-time string table that packs strings into a single blob with offset lookup. +/// Use PROGMEM_STRING_TABLE macro to instantiate with proper flash placement on ESP8266. +/// +/// Example: +/// PROGMEM_STRING_TABLE(MyStrings, "foo", "bar", "baz"); +/// const char *str = MyStrings::get(index); // 0-based index +/// +template struct ProgmemStringTable { + static constexpr size_t COUNT = sizeof...(Strs); + static constexpr size_t BLOB_SIZE = (... + (Strs.size() + 1)); + + /// Generate packed string blob at compile time + static constexpr auto make_blob() { + std::array result{}; + size_t pos = 0; + auto copy = [&](const auto &str) { + for (size_t i = 0; i <= str.size(); ++i) + result[pos++] = str.data[i]; + }; + (copy(Strs), ...); + return result; + } + + /// Generate offset table at compile time + static constexpr auto make_offsets() { + std::array result{}; + size_t pos = 0, idx = 0; + ((result[idx++] = pos, pos += Strs.size() + 1), ...); + return result; + } +}; + +/// Instantiate a ProgmemStringTable with PROGMEM storage. +/// Creates: Name::get(index), Name::COUNT, Name::BLOB_SIZE +#define PROGMEM_STRING_TABLE(Name, ...) \ + struct Name { \ + using Table = ProgmemStringTable<__VA_ARGS__>; \ + static constexpr size_t COUNT = Table::COUNT; \ + static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \ + static constexpr auto BLOB PROGMEM = Table::make_blob(); \ + static constexpr auto OFFSETS PROGMEM = Table::make_offsets(); \ + static const char *get(uint8_t index) { \ + if (index >= COUNT) \ + return nullptr; \ + return &BLOB[progmem_read_byte(&OFFSETS[index])]; \ + } \ + } + +} // namespace esphome From 67febb13c0719ca7eda6f52cceee0a06a7a3b45a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:20:26 -0600 Subject: [PATCH 02/16] tweaks --- .../components/light/light_json_schema.cpp | 5 +++-- esphome/components/logger/logger.cpp | 8 +++++--- esphome/core/progmem.h | 19 +++++++++++++------ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 874d3d547d..b44609fc02 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -15,11 +15,12 @@ PROGMEM_STRING_TABLE(ColorModeStrings, "onoff", "brightness", "white", "color_te "rgbww"); // Get JSON string for color mode. Returns nullptr for UNKNOWN (bit 0). -static const char *get_color_mode_json_str(ColorMode mode) { +// Returns ProgmemStr so ArduinoJson knows to handle PROGMEM strings on ESP8266. +static ProgmemStr get_color_mode_json_str(ColorMode mode) { unsigned bit = ColorModeBitPolicy::to_bit(mode); if (bit == 0) return nullptr; - return ColorModeStrings::get(bit - 1); + return ColorModeStrings::get_progmem_str(bit - 1); } void LightJSONSchema::dump_json(LightState &state, JsonObject root) { diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index c70520677a..fa732c2771 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -300,7 +300,8 @@ void Logger::dump_config() { "Logger:\n" " Max Level: %s\n" " Initial Level: %s", - LogLevelStrings::get(ESPHOME_LOG_LEVEL), LogLevelStrings::get(this->current_level_)); + LOG_STR_ARG(LogLevelStrings::get_log_str(ESPHOME_LOG_LEVEL)), + LOG_STR_ARG(LogLevelStrings::get_log_str(this->current_level_))); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" @@ -319,7 +320,7 @@ void Logger::dump_config() { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LogLevelStrings::get(it.second)); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LogLevelStrings::get_log_str(it.second))); } #endif } @@ -327,7 +328,8 @@ void Logger::dump_config() { void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; - ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LogLevelStrings::get(ESPHOME_LOG_LEVEL)); + ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", + LOG_STR_ARG(LogLevelStrings::get_log_str(ESPHOME_LOG_LEVEL))); } this->current_level_ = level; #ifdef USE_LOGGER_LEVEL_LISTENERS diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 581b063be9..7cf172dafc 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -20,7 +20,6 @@ #define ESPHOME_strcasecmp_P strcasecmp_P #define ESPHOME_strncmp_P strncmp_P #define ESPHOME_strncasecmp_P strncasecmp_P -#define progmem_read_byte(addr) pgm_read_byte(addr) // Type for pointers to PROGMEM strings (for use with ESPHOME_F return values) using ProgmemStr = const __FlashStringHelper *; #else @@ -34,7 +33,6 @@ using ProgmemStr = const __FlashStringHelper *; #define ESPHOME_strcasecmp_P strcasecmp #define ESPHOME_strncmp_P strncmp #define ESPHOME_strncasecmp_P strncasecmp -#define progmem_read_byte(addr) (*(addr)) // Type for pointers to strings (no PROGMEM on non-ESP8266 platforms) using ProgmemStr = const char *; #endif @@ -56,7 +54,8 @@ template struct FixedString { /// /// Example: /// PROGMEM_STRING_TABLE(MyStrings, "foo", "bar", "baz"); -/// const char *str = MyStrings::get(index); // 0-based index +/// ProgmemStr str = MyStrings::get_progmem_str(index); // For ArduinoJson +/// const LogString *log_str = MyStrings::get_log_str(index); // For ESP_LOG* /// template struct ProgmemStringTable { static constexpr size_t COUNT = sizeof...(Strs); @@ -83,8 +82,11 @@ template struct ProgmemStringTable { } }; +// Forward declaration for LogString (defined in log.h) +struct LogString; + /// Instantiate a ProgmemStringTable with PROGMEM storage. -/// Creates: Name::get(index), Name::COUNT, Name::BLOB_SIZE +/// Creates: Name::get_progmem_str(index), Name::get_log_str(index) #define PROGMEM_STRING_TABLE(Name, ...) \ struct Name { \ using Table = ProgmemStringTable<__VA_ARGS__>; \ @@ -92,10 +94,15 @@ template struct ProgmemStringTable { static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \ static constexpr auto BLOB PROGMEM = Table::make_blob(); \ static constexpr auto OFFSETS PROGMEM = Table::make_offsets(); \ - static const char *get(uint8_t index) { \ + static ProgmemStr get_progmem_str(uint8_t index) { \ if (index >= COUNT) \ return nullptr; \ - return &BLOB[progmem_read_byte(&OFFSETS[index])]; \ + return reinterpret_cast(&BLOB[progmem_read_byte(&OFFSETS[index])]); \ + } \ + static const LogString *get_log_str(uint8_t index) { \ + if (index >= COUNT) \ + return nullptr; \ + return reinterpret_cast(&BLOB[progmem_read_byte(&OFFSETS[index])]); \ } \ } From 1b6ae4348b389bb14515ebcef3b2d8426d36b085 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:22:04 -0600 Subject: [PATCH 03/16] tweaks --- esphome/core/progmem.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 7cf172dafc..a7b4a65adc 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -55,7 +55,7 @@ template struct FixedString { /// Example: /// PROGMEM_STRING_TABLE(MyStrings, "foo", "bar", "baz"); /// ProgmemStr str = MyStrings::get_progmem_str(index); // For ArduinoJson -/// const LogString *log_str = MyStrings::get_log_str(index); // For ESP_LOG* +/// const LogString *log_str = MyStrings::get_log_str(index); // For logging /// template struct ProgmemStringTable { static constexpr size_t COUNT = sizeof...(Strs); From 2d1fbe0736b43bb71645f7c774453ee2ce749217 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:23:28 -0600 Subject: [PATCH 04/16] bot comments --- esphome/core/progmem.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index a7b4a65adc..08949751a5 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -73,8 +73,9 @@ template struct ProgmemStringTable { return result; } - /// Generate offset table at compile time + /// Generate offset table at compile time (uint8_t limits blob to 255 bytes) static constexpr auto make_offsets() { + static_assert(BLOB_SIZE <= 255, "PROGMEM_STRING_TABLE blob exceeds 255 bytes; use fewer/shorter strings"); std::array result{}; size_t pos = 0, idx = 0; ((result[idx++] = pos, pos += Strs.size() + 1), ...); From 3b5c4c2416dfe20344a156c6bf00d7a87b5a7138 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:29:28 -0600 Subject: [PATCH 05/16] reduce --- .../wifi/wifi_component_esp8266.cpp | 45 ++++++++----------- esphome/core/progmem.h | 2 + 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c714afaad3..d938dadfef 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -36,6 +36,7 @@ extern "C" { #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/util.h" namespace esphome::wifi { @@ -398,35 +399,27 @@ class WiFiMockClass : public ESP8266WiFiGenericClass { static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT }; +// Auth mode strings indexed by AUTH_* constants (0-4), with UNKNOWN at index 5 +// Static asserts verify the SDK constants are contiguous as expected +static_assert(AUTH_OPEN == 0 && AUTH_WEP == 1 && AUTH_WPA_PSK == 2 && AUTH_WPA2_PSK == 3 && AUTH_WPA_WPA2_PSK == 4, + "AUTH_* constants are not contiguous"); +PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN"); + const LogString *get_auth_mode_str(uint8_t mode) { - switch (mode) { - case AUTH_OPEN: - return LOG_STR("OPEN"); - case AUTH_WEP: - return LOG_STR("WEP"); - case AUTH_WPA_PSK: - return LOG_STR("WPA PSK"); - case AUTH_WPA2_PSK: - return LOG_STR("WPA2 PSK"); - case AUTH_WPA_WPA2_PSK: - return LOG_STR("WPA/WPA2 PSK"); - default: - return LOG_STR("UNKNOWN"); - } + if (mode > 4) + mode = 5; // UNKNOWN + return AuthModeStrings::get_log_str(mode); } + +// WiFi op mode strings indexed by WIFI_* constants (0-3), with UNKNOWN at index 4 +static_assert(WIFI_OFF == 0 && WIFI_STA == 1 && WIFI_AP == 2 && WIFI_AP_STA == 3, + "WIFI_* op mode constants are not contiguous"); +PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN"); + const LogString *get_op_mode_str(uint8_t mode) { - switch (mode) { - case WIFI_OFF: - return LOG_STR("OFF"); - case WIFI_STA: - return LOG_STR("STA"); - case WIFI_AP: - return LOG_STR("AP"); - case WIFI_AP_STA: - return LOG_STR("AP+STA"); - default: - return LOG_STR("UNKNOWN"); - } + if (mode > 3) + mode = 4; // UNKNOWN + return OpModeStrings::get_log_str(mode); } const LogString *get_disconnect_reason_str(uint8_t reason) { diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 08949751a5..e95bed8130 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -4,6 +4,8 @@ #include #include +#include "esphome/core/hal.h" // For PROGMEM definition + // Platform-agnostic macros for PROGMEM string handling // On ESP8266/Arduino: Use Arduino's F() macro for PROGMEM strings // On other platforms: Use plain strings (no PROGMEM) From e23d295e8b90b61bb7e00c6c3108340e37a18c65 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:30:33 -0600 Subject: [PATCH 06/16] convert sensor to make sure it works --- esphome/components/sensor/sensor.cpp | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 3f2be02af2..11015d53ec 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::sensor { @@ -30,20 +31,14 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o } } +// State class strings indexed by StateClass enum (0-4): NONE, MEASUREMENT, TOTAL_INCREASING, TOTAL, MEASUREMENT_ANGLE +PROGMEM_STRING_TABLE(StateClassStrings, "", "measurement", "total_increasing", "total", "measurement_angle"); + const LogString *state_class_to_string(StateClass state_class) { - switch (state_class) { - case STATE_CLASS_MEASUREMENT: - return LOG_STR("measurement"); - case STATE_CLASS_TOTAL_INCREASING: - return LOG_STR("total_increasing"); - case STATE_CLASS_TOTAL: - return LOG_STR("total"); - case STATE_CLASS_MEASUREMENT_ANGLE: - return LOG_STR("measurement_angle"); - case STATE_CLASS_NONE: - default: - return LOG_STR(""); - } + uint8_t index = static_cast(state_class); + if (index > 4) + index = 0; // Default to empty string (STATE_CLASS_NONE) + return StateClassStrings::get_log_str(index); } Sensor::Sensor() : state(NAN), raw_state(NAN) {} From 69be5813464130ea22e451f90dcf779c4eeaa312 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:31:17 -0600 Subject: [PATCH 07/16] convert sensor to make sure it works --- esphome/components/sensor/sensor.cpp | 2 +- esphome/components/sensor/sensor.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 11015d53ec..52533cf6f6 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -36,7 +36,7 @@ PROGMEM_STRING_TABLE(StateClassStrings, "", "measurement", "total_increasing", " const LogString *state_class_to_string(StateClass state_class) { uint8_t index = static_cast(state_class); - if (index > 4) + if (index > STATE_CLASS_LAST) index = 0; // Default to empty string (STATE_CLASS_NONE) return StateClassStrings::get_log_str(index); } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index d9046020f6..d57f072d40 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -32,6 +32,7 @@ enum StateClass : uint8_t { STATE_CLASS_TOTAL = 3, STATE_CLASS_MEASUREMENT_ANGLE = 4 }; +constexpr uint8_t STATE_CLASS_LAST = STATE_CLASS_MEASUREMENT_ANGLE; const LogString *state_class_to_string(StateClass state_class); From 8556ae7209dae88ffa42fe48a2b3f786c0abee7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:33:02 -0600 Subject: [PATCH 08/16] convert sensor to make sure it works --- esphome/components/wifi/wifi_component_esp8266.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index d938dadfef..f9ad5e1da9 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -403,22 +403,26 @@ class WiFiMockClass : public ESP8266WiFiGenericClass { // Static asserts verify the SDK constants are contiguous as expected static_assert(AUTH_OPEN == 0 && AUTH_WEP == 1 && AUTH_WPA_PSK == 2 && AUTH_WPA2_PSK == 3 && AUTH_WPA_WPA2_PSK == 4, "AUTH_* constants are not contiguous"); +constexpr uint8_t AUTH_MODE_LAST = AUTH_WPA_WPA2_PSK; +constexpr uint8_t AUTH_MODE_UNKNOWN_INDEX = AUTH_MODE_LAST + 1; PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN"); const LogString *get_auth_mode_str(uint8_t mode) { - if (mode > 4) - mode = 5; // UNKNOWN + if (mode > AUTH_MODE_LAST) + mode = AUTH_MODE_UNKNOWN_INDEX; return AuthModeStrings::get_log_str(mode); } // WiFi op mode strings indexed by WIFI_* constants (0-3), with UNKNOWN at index 4 static_assert(WIFI_OFF == 0 && WIFI_STA == 1 && WIFI_AP == 2 && WIFI_AP_STA == 3, "WIFI_* op mode constants are not contiguous"); +constexpr uint8_t OP_MODE_LAST = WIFI_AP_STA; +constexpr uint8_t OP_MODE_UNKNOWN_INDEX = OP_MODE_LAST + 1; PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN"); const LogString *get_op_mode_str(uint8_t mode) { - if (mode > 3) - mode = 4; // UNKNOWN + if (mode > OP_MODE_LAST) + mode = OP_MODE_UNKNOWN_INDEX; return OpModeStrings::get_log_str(mode); } From a60dea1d83c5fbb676ece51154a8a60582919238 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:35:51 -0600 Subject: [PATCH 09/16] clamp --- esphome/components/logger/logger.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index fa732c2771..1e0dd2f0d9 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -294,14 +294,21 @@ float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; // Log level strings - packed into flash on ESP8266, indexed by log level (0-7) PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"); +constexpr uint8_t LOG_LEVEL_LAST = ESPHOME_LOG_LEVEL_VERY_VERBOSE; + +static const LogString *get_log_level_str_(uint8_t level) { + if (level > LOG_LEVEL_LAST) + level = LOG_LEVEL_LAST; + return LogLevelStrings::get_log_str(level); +} void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:\n" " Max Level: %s\n" " Initial Level: %s", - LOG_STR_ARG(LogLevelStrings::get_log_str(ESPHOME_LOG_LEVEL)), - LOG_STR_ARG(LogLevelStrings::get_log_str(this->current_level_))); + LOG_STR_ARG(get_log_level_str_(ESPHOME_LOG_LEVEL)), + LOG_STR_ARG(get_log_level_str_(this->current_level_))); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" @@ -320,7 +327,7 @@ void Logger::dump_config() { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LogLevelStrings::get_log_str(it.second))); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str_(it.second))); } #endif } @@ -329,7 +336,7 @@ void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", - LOG_STR_ARG(LogLevelStrings::get_log_str(ESPHOME_LOG_LEVEL))); + LOG_STR_ARG(get_log_level_str_(ESPHOME_LOG_LEVEL))); } this->current_level_ = level; #ifdef USE_LOGGER_LEVEL_LISTENERS From 49b652ed895f4e993b066d6a7e57ca21cb068344 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:48:12 -0600 Subject: [PATCH 10/16] Update esphome/components/sensor/sensor.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/sensor/sensor.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index d57f072d40..f9a45cb1d0 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -32,7 +32,7 @@ enum StateClass : uint8_t { STATE_CLASS_TOTAL = 3, STATE_CLASS_MEASUREMENT_ANGLE = 4 }; -constexpr uint8_t STATE_CLASS_LAST = STATE_CLASS_MEASUREMENT_ANGLE; +constexpr uint8_t STATE_CLASS_LAST = static_cast(STATE_CLASS_MEASUREMENT_ANGLE); const LogString *state_class_to_string(StateClass state_class); From 8b3f020dbadd8eb31e1b4d9d0f0aec605dad6409 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 21:48:21 -0600 Subject: [PATCH 11/16] Update esphome/core/progmem.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/progmem.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index e95bed8130..0867268205 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -80,7 +80,7 @@ template struct ProgmemStringTable { static_assert(BLOB_SIZE <= 255, "PROGMEM_STRING_TABLE blob exceeds 255 bytes; use fewer/shorter strings"); std::array result{}; size_t pos = 0, idx = 0; - ((result[idx++] = pos, pos += Strs.size() + 1), ...); + ((result[idx++] = static_cast(pos), pos += Strs.size() + 1), ...); return result; } }; From 726c5daa74167fc28c4be251219fcc06e14bd7f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 22:00:47 -0600 Subject: [PATCH 12/16] bot review --- esphome/core/progmem.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 0867268205..3cd0999345 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -77,6 +77,8 @@ template struct ProgmemStringTable { /// Generate offset table at compile time (uint8_t limits blob to 255 bytes) static constexpr auto make_offsets() { + static_assert(COUNT > 0, "PROGMEM_STRING_TABLE must contain at least one string"); + static_assert(COUNT <= 255, "PROGMEM_STRING_TABLE supports at most 255 strings with uint8_t indices"); static_assert(BLOB_SIZE <= 255, "PROGMEM_STRING_TABLE blob exceeds 255 bytes; use fewer/shorter strings"); std::array result{}; size_t pos = 0, idx = 0; @@ -92,20 +94,20 @@ struct LogString; /// Creates: Name::get_progmem_str(index), Name::get_log_str(index) #define PROGMEM_STRING_TABLE(Name, ...) \ struct Name { \ - using Table = ProgmemStringTable<__VA_ARGS__>; \ + using Table = ::esphome::ProgmemStringTable<__VA_ARGS__>; \ static constexpr size_t COUNT = Table::COUNT; \ static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \ static constexpr auto BLOB PROGMEM = Table::make_blob(); \ static constexpr auto OFFSETS PROGMEM = Table::make_offsets(); \ - static ProgmemStr get_progmem_str(uint8_t index) { \ + static ::esphome::ProgmemStr get_progmem_str(uint8_t index) { \ if (index >= COUNT) \ return nullptr; \ - return reinterpret_cast(&BLOB[progmem_read_byte(&OFFSETS[index])]); \ + return reinterpret_cast<::esphome::ProgmemStr>(&BLOB[::esphome::progmem_read_byte(&OFFSETS[index])]); \ } \ - static const LogString *get_log_str(uint8_t index) { \ + static const ::esphome::LogString *get_log_str(uint8_t index) { \ if (index >= COUNT) \ return nullptr; \ - return reinterpret_cast(&BLOB[progmem_read_byte(&OFFSETS[index])]); \ + return reinterpret_cast(&BLOB[::esphome::progmem_read_byte(&OFFSETS[index])]); \ } \ } From b298837276780b3732e86e6a0a1cb19b0440472c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 22:02:57 -0600 Subject: [PATCH 13/16] make sure cover works --- esphome/components/cover/cover.cpp | 17 +++++++---------- esphome/components/cover/cover.h | 2 ++ esphome/components/valve/valve.h | 2 ++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 0d9e7e8ffb..186db73da5 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -22,17 +22,14 @@ const LogString *cover_command_to_str(float pos) { return LOG_STR("UNKNOWN"); } } +// Cover operation strings indexed by CoverOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN +PROGMEM_STRING_TABLE(CoverOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN"); + const LogString *cover_operation_to_str(CoverOperation op) { - switch (op) { - case COVER_OPERATION_IDLE: - return LOG_STR("IDLE"); - case COVER_OPERATION_OPENING: - return LOG_STR("OPENING"); - case COVER_OPERATION_CLOSING: - return LOG_STR("CLOSING"); - default: - return LOG_STR("UNKNOWN"); - } + uint8_t index = static_cast(op); + if (index > COVER_OPERATION_LAST) + index = COVER_OPERATION_UNKNOWN_INDEX; + return CoverOperationStrings::get_log_str(index); } Cover::Cover() : position{COVER_OPEN} {} diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index e5427ceaa8..a94df7f771 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -84,6 +84,8 @@ enum CoverOperation : uint8_t { /// The cover is currently closing. COVER_OPERATION_CLOSING, }; +constexpr uint8_t COVER_OPERATION_LAST = static_cast(COVER_OPERATION_CLOSING); +constexpr uint8_t COVER_OPERATION_UNKNOWN_INDEX = COVER_OPERATION_LAST + 1; const LogString *cover_operation_to_str(CoverOperation op); diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index cd46144372..9b904c45a8 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -79,6 +79,8 @@ enum ValveOperation : uint8_t { /// The valve is currently closing. VALVE_OPERATION_CLOSING, }; +constexpr uint8_t VALVE_OPERATION_LAST = static_cast(VALVE_OPERATION_CLOSING); +constexpr uint8_t VALVE_OPERATION_UNKNOWN_INDEX = VALVE_OPERATION_LAST + 1; const LogString *valve_operation_to_str(ValveOperation op); From dcc8f50750e3bb52283f0adaa1c986e574285aed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 22:03:14 -0600 Subject: [PATCH 14/16] make sure valve works --- esphome/components/valve/valve.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 607f614ef7..9d07ddf8fc 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -23,17 +23,14 @@ const LogString *valve_command_to_str(float pos) { return LOG_STR("UNKNOWN"); } } +// Valve operation strings indexed by ValveOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN +PROGMEM_STRING_TABLE(ValveOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN"); + const LogString *valve_operation_to_str(ValveOperation op) { - switch (op) { - case VALVE_OPERATION_IDLE: - return LOG_STR("IDLE"); - case VALVE_OPERATION_OPENING: - return LOG_STR("OPENING"); - case VALVE_OPERATION_CLOSING: - return LOG_STR("CLOSING"); - default: - return LOG_STR("UNKNOWN"); - } + uint8_t index = static_cast(op); + if (index > VALVE_OPERATION_LAST) + index = VALVE_OPERATION_UNKNOWN_INDEX; + return ValveOperationStrings::get_log_str(index); } Valve::Valve() : position{VALVE_OPEN} {} From 1f4be6512f99898f8a4132690ba29316cb96a472 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 22:05:15 -0600 Subject: [PATCH 15/16] more fixes --- esphome/core/progmem.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 3cd0999345..5755338cf2 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -99,10 +99,10 @@ struct LogString; static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \ static constexpr auto BLOB PROGMEM = Table::make_blob(); \ static constexpr auto OFFSETS PROGMEM = Table::make_offsets(); \ - static ::esphome::ProgmemStr get_progmem_str(uint8_t index) { \ + static ::ProgmemStr get_progmem_str(uint8_t index) { \ if (index >= COUNT) \ return nullptr; \ - return reinterpret_cast<::esphome::ProgmemStr>(&BLOB[::esphome::progmem_read_byte(&OFFSETS[index])]); \ + return reinterpret_cast<::ProgmemStr>(&BLOB[::esphome::progmem_read_byte(&OFFSETS[index])]); \ } \ static const ::esphome::LogString *get_log_str(uint8_t index) { \ if (index >= COUNT) \ From c69e6e43639634d37817304949067c78e96eb75f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jan 2026 22:18:20 -0600 Subject: [PATCH 16/16] tweak --- esphome/components/logger/logger.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 1e0dd2f0d9..5aebe459ba 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -296,7 +296,7 @@ float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"); constexpr uint8_t LOG_LEVEL_LAST = ESPHOME_LOG_LEVEL_VERY_VERBOSE; -static const LogString *get_log_level_str_(uint8_t level) { +static const LogString *get_log_level_str(uint8_t level) { if (level > LOG_LEVEL_LAST) level = LOG_LEVEL_LAST; return LogLevelStrings::get_log_str(level); @@ -307,8 +307,8 @@ void Logger::dump_config() { "Logger:\n" " Max Level: %s\n" " Initial Level: %s", - LOG_STR_ARG(get_log_level_str_(ESPHOME_LOG_LEVEL)), - LOG_STR_ARG(get_log_level_str_(this->current_level_))); + LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)), + LOG_STR_ARG(get_log_level_str(this->current_level_))); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" @@ -327,7 +327,7 @@ void Logger::dump_config() { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str_(it.second))); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second))); } #endif } @@ -336,7 +336,7 @@ void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", - LOG_STR_ARG(get_log_level_str_(ESPHOME_LOG_LEVEL))); + LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL))); } this->current_level_ = level; #ifdef USE_LOGGER_LEVEL_LISTENERS