Merge branch 'dev' into esp32-millis-euclidean-decomposition

This commit is contained in:
J. Nick Koston
2026-03-02 09:28:12 -10:00
committed by GitHub
73 changed files with 2219 additions and 1333 deletions
+1
View File
@@ -11,6 +11,7 @@
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
Expression,
FlashStringLiteral,
LineComment,
LogStringLiteral,
MockObj,
@@ -12,7 +12,14 @@ AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
if (code != nullptr) {
this->code_ = std::string(code);
return this->set_code(code, strlen(code));
}
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code, size_t len) {
if (code != nullptr) {
this->code_ = std::string(code, len);
}
return *this;
}
@@ -15,7 +15,8 @@ class AlarmControlPanelCall {
AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const char *code);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
AlarmControlPanelCall &set_code(const char *code, size_t len);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str(), code.size()); }
AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night();
+24 -10
View File
@@ -535,24 +535,31 @@ async def homeassistant_service_to_code(
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, None)
templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
# output_type=None because lambdas can return non-string types (int,
# float, char*) that TemplatableStringValue converts via to_string.
# Static strings are manually wrapped for PROGMEM on ESP8266.
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
@@ -621,24 +628,31 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, None)
templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
# output_type=None because lambdas can return non-string types (int,
# float, char*) that TemplatableStringValue converts via to_string.
# Static strings are manually wrapped for PROGMEM on ESP8266.
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
return var
@@ -662,11 +676,11 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned"))
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
# Initialize FixedVector with exact size (1 data field)
cg.add(var.init_data(1))
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
cg.add(var.add_data("tag_id", templ))
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
return var
+2 -2
View File
@@ -889,7 +889,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
}
void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
call.set_value(msg.state);
call.set_value(msg.state.c_str(), msg.state.size());
call.perform();
}
#endif
@@ -1360,7 +1360,7 @@ void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPan
call.pending();
break;
}
call.set_code(msg.code);
call.set_code(msg.code.c_str(), msg.code.size());
call.perform();
}
#endif
+1 -1
View File
@@ -257,7 +257,7 @@ class APIServer : public Component,
}
void socket_failed_(const LogString *msg);
// Pointers and pointer-like types first (4 bytes each)
socket::Socket *socket_{nullptr};
socket::ListenSocket *socket_{nullptr};
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> client_connected_trigger_;
#endif
+43 -3
View File
@@ -130,6 +130,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->add_kv_(this->variables_, key, std::forward<V>(value));
}
#ifdef USE_ESP8266
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
@@ -221,7 +235,32 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Ts... x) {
dest.init(source.size());
// Count non-static strings to allocate exact storage needed
#ifdef USE_ESP8266
// On ESP8266, all static strings from codegen are FLASH_STRING (PROGMEM),
// so is_static_string() is always false — the zero-copy STATIC_STRING fast
// path from the non-ESP8266 branch cannot trigger. We copy all keys and
// values unconditionally: keys via _P functions (may be in PROGMEM), values
// via value() which handles FLASH_STRING internally.
value_storage.init(source.size() * 2);
for (auto &it : source) {
auto &kv = dest.emplace_back();
// Key: copy from possible PROGMEM
{
size_t key_len = strlen_P(it.key);
value_storage.push_back(std::string(key_len, '\0'));
memcpy_P(value_storage.back().data(), it.key, key_len);
kv.key = StringRef(value_storage.back());
}
// Value: value() handles FLASH_STRING via _P functions internally
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
#else
// On non-ESP8266, strings are directly readable from flash-mapped memory.
// Count non-static strings to allocate exact storage needed.
size_t lambda_count = 0;
for (const auto &it : source) {
if (!it.value.is_static_string()) {
@@ -235,14 +274,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.key = StringRef(it.key);
if (it.value.is_static_string()) {
// Static string from YAML - zero allocation
// Static string — pointer directly readable, zero allocation
kv.value = StringRef(it.value.get_static_string());
} else {
// Lambda evaluation - store result, reference it
// Lambda evaluate and store result
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
}
#endif
}
APIServer *parent_;
@@ -22,7 +22,7 @@ class DNSServer {
}
static constexpr size_t DNS_BUFFER_SIZE = 192;
socket::Socket *socket_{nullptr};
socket::ListenSocket *socket_{nullptr};
network::IPAddress server_ip_;
uint8_t buffer_[DNS_BUFFER_SIZE];
};
+13 -5
View File
@@ -173,14 +173,17 @@ ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
return *this;
}
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
ClimateCall &ClimateCall::set_mode(const std::string &mode) { return this->set_mode(mode.c_str(), mode.size()); }
ClimateCall &ClimateCall::set_mode(const char *mode, size_t len) {
StringRef mode_ref(mode, len);
for (const auto &mode_entry : CLIMATE_MODES_BY_STR) {
if (str_equals_case_insensitive(mode, mode_entry.str)) {
if (str_equals_case_insensitive(mode_ref, mode_entry.str)) {
this->set_mode(static_cast<ClimateMode>(mode_entry.value));
return *this;
}
}
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode);
return *this;
}
@@ -266,13 +269,18 @@ ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
}
ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) {
return this->set_swing_mode(swing_mode.c_str(), swing_mode.size());
}
ClimateCall &ClimateCall::set_swing_mode(const char *swing_mode, size_t len) {
StringRef mode_ref(swing_mode, len);
for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) {
if (str_equals_case_insensitive(swing_mode, mode_entry.str)) {
if (str_equals_case_insensitive(mode_ref, mode_entry.str)) {
this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value));
return *this;
}
}
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %.*s", this->parent_->get_name().c_str(), (int) len, swing_mode);
return *this;
}
+4
View File
@@ -41,6 +41,8 @@ class ClimateCall {
ClimateCall &set_mode(optional<ClimateMode> mode);
/// Set the mode of the climate device based on a string.
ClimateCall &set_mode(const std::string &mode);
/// Set the mode of the climate device based on a C string.
ClimateCall &set_mode(const char *mode, size_t len);
/// Set the target temperature of the climate device.
ClimateCall &set_target_temperature(float target_temperature);
/// Set the target temperature of the climate device.
@@ -87,6 +89,8 @@ class ClimateCall {
ClimateCall &set_swing_mode(optional<ClimateSwingMode> swing_mode);
/// Set the swing mode of the climate device based on a string.
ClimateCall &set_swing_mode(const std::string &swing_mode);
/// Set the swing mode of the climate device based on a C string.
ClimateCall &set_swing_mode(const char *swing_mode, size_t len);
/// Set the preset of the climate device.
ClimateCall &set_preset(ClimatePreset preset);
/// Set the preset of the climate device.
+1
View File
@@ -1438,6 +1438,7 @@ async def to_code(config):
cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.add_build_flag("-Wl,-z,noexecstack")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
variant = config[CONF_VARIANT]
+1
View File
@@ -48,6 +48,7 @@ void arch_init() {
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
+5 -2
View File
@@ -3,7 +3,7 @@
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "esphome/core/time_64.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <Arduino.h>
@@ -17,7 +17,7 @@ namespace esphome {
void HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); }
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
void HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
@@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { system_soft_wdt_feed(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
uint16_t progmem_read_uint16(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
+1 -1
View File
@@ -84,7 +84,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
std::unique_ptr<uint8_t[]> auth_buf_;
#endif // USE_OTA_PASSWORD
socket::Socket *server_{nullptr};
socket::ListenSocket *server_{nullptr};
std::unique_ptr<socket::Socket> client_;
std::unique_ptr<ota::OTABackend> backend_;
+1
View File
@@ -41,6 +41,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
cg.add_build_flag("-DUSE_HOST")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
cg.add_build_flag("-std=gnu++20")
cg.add_define("ESPHOME_BOARD", "host")
+1
View File
@@ -59,6 +59,7 @@ void HOT arch_feed_wdt() {
}
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
+3 -2
View File
@@ -3,7 +3,7 @@
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "esphome/core/time_64.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
@@ -14,7 +14,7 @@ namespace esphome {
void HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); }
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void HOT delay(uint32_t ms) { ::delay(ms); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
@@ -36,6 +36,7 @@ void HOT arch_feed_wdt() { lt_wdt_feed(); }
uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
} // namespace esphome
+39 -1
View File
@@ -1,3 +1,4 @@
from dataclasses import dataclass, field
import enum
import esphome.automation as auto
@@ -37,7 +38,7 @@ from esphome.const import (
CONF_WEB_SERVER,
CONF_WHITE,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
@@ -66,6 +67,40 @@ from .types import ( # noqa
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
DOMAIN = "light"
@dataclass
class LightData:
gamma_tables: dict = field(default_factory=dict) # gamma_value -> fwd_arr
def _get_data() -> LightData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = LightData()
return CORE.data[DOMAIN]
def _get_or_create_gamma_table(gamma_correct):
data = _get_data()
if gamma_correct in data.gamma_tables:
return data.gamma_tables[gamma_correct]
if gamma_correct > 0:
forward = [
HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535))))
for i in range(256)
]
else:
forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
gamma_str = f"{gamma_correct}".replace(".", "_")
fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16)
fwd_arr = cg.progmem_array(fwd_id, forward)
data.gamma_tables[gamma_correct] = fwd_arr
return fwd_arr
LightRestoreMode = light_ns.enum("LightRestoreMode")
RESTORE_MODES = {
"RESTORE_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF,
@@ -239,6 +274,9 @@ async def setup_light_core_(light_var, output_var, config):
cg.add(light_var.set_flash_transition_length(flash_transition_length))
if (gamma_correct := config.get(CONF_GAMMA_CORRECT)) is not None:
cg.add(light_var.set_gamma_correct(gamma_correct))
fwd_arr = _get_or_create_gamma_table(gamma_correct)
cg.add(light_var.set_gamma_table(fwd_arr))
cg.add_define("USE_LIGHT_GAMMA_LUT")
effects = await cg.build_registry_list(
EFFECTS_REGISTRY, config.get(CONF_EFFECTS, [])
)
+3 -1
View File
@@ -66,7 +66,9 @@ class AddressableLight : public LightOutput, public Component {
Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white)));
}
void setup_state(LightState *state) override {
this->correction_.calculate_gamma_table(state->get_gamma_correct());
#ifdef USE_LIGHT_GAMMA_LUT
this->correction_.set_gamma_table(state->get_gamma_table());
#endif
this->state_parent_ = state;
}
void update_state(LightState *state) override;
@@ -74,11 +74,10 @@ class AddressableLightWrapper : public light::AddressableLight {
return;
}
float gamma = this->light_state_->get_gamma_correct();
float r = gamma_uncorrect(this->wrapper_state_[0] / 255.0f, gamma);
float g = gamma_uncorrect(this->wrapper_state_[1] / 255.0f, gamma);
float b = gamma_uncorrect(this->wrapper_state_[2] / 255.0f, gamma);
float w = gamma_uncorrect(this->wrapper_state_[3] / 255.0f, gamma);
float r = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[0] / 255.0f);
float g = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[1] / 255.0f);
float b = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[2] / 255.0f);
float w = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[3] / 255.0f);
auto call = this->light_state_->make_call();
+1 -1
View File
@@ -41,7 +41,7 @@ template<typename... Ts> class LightControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, color_temperature)
TEMPLATABLE_VALUE(float, cold_white)
TEMPLATABLE_VALUE(float, warm_white)
TEMPLATABLE_VALUE(std::string, effect)
TEMPLATABLE_VALUE(uint32_t, effect)
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
+48 -2
View File
@@ -10,12 +10,14 @@ from esphome.const import (
CONF_COLOR_MODE,
CONF_COLOR_TEMPERATURE,
CONF_EFFECT,
CONF_EFFECTS,
CONF_FLASH_LENGTH,
CONF_GREEN,
CONF_ID,
CONF_LIMIT_MODE,
CONF_MAX_BRIGHTNESS,
CONF_MIN_BRIGHTNESS,
CONF_NAME,
CONF_RANGE_FROM,
CONF_RANGE_TO,
CONF_RED,
@@ -24,6 +26,9 @@ from esphome.const import (
CONF_WARM_WHITE,
CONF_WHITE,
)
from esphome.core import CORE, Lambda
from esphome.cpp_generator import LambdaExpression
from esphome.types import ConfigType
from .types import (
COLOR_MODES,
@@ -111,6 +116,26 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id(
)
def _resolve_effect_index(config: ConfigType) -> int:
"""Resolve a static effect name to its 1-based index at codegen time.
Effect index 0 means "None" (no effect). Effects are 1-indexed matching
the C++ convention in LightState.
"""
original_name = config[CONF_EFFECT]
effect_name = original_name.lower()
if effect_name == "none":
return 0
light_id = config[CONF_ID]
light_path = CORE.config.get_path_for_id(light_id)[:-1]
light_config = CORE.config.get_config_for_path(light_path)
for i, effect_conf in enumerate(light_config.get(CONF_EFFECTS, [])):
key = next(iter(effect_conf))
if effect_conf[key][CONF_NAME].lower() == effect_name:
return i + 1
raise ValueError(f"Effect '{original_name}' not found in light '{light_id}'")
@automation.register_action(
"light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA, synchronous=True
)
@@ -165,8 +190,29 @@ async def light_control_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float)
cg.add(var.set_warm_white(template_))
if CONF_EFFECT in config:
template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string)
cg.add(var.set_effect(template_))
if isinstance(config[CONF_EFFECT], Lambda):
# Lambda returns a string — wrap in a C++ lambda that resolves
# the effect name to its uint32_t index at runtime
inner_lambda = await cg.process_lambda(
config[CONF_EFFECT], args, return_type=cg.std_string
)
fwd_args = ", ".join(n for _, n in args)
# capture="" is correct: paren is a global variable name
# string-interpolated into the body at codegen time, not a
# C++ runtime capture.
wrapper = LambdaExpression(
f"auto __effect_s = ({inner_lambda})({fwd_args});\n"
f"return {paren}->get_effect_index("
f"__effect_s.c_str(), __effect_s.size());",
args,
capture="",
return_type=cg.uint32,
)
cg.add(var.set_effect(wrapper))
else:
# Static string — resolve effect name to index at codegen time
effect_index = _resolve_effect_index(config)
cg.add(var.set_effect(effect_index))
return var
@@ -1,25 +1,25 @@
#include "esp_color_correction.h"
#include "light_color_values.h"
#include "esphome/core/log.h"
namespace esphome::light {
void ESPColorCorrection::calculate_gamma_table(float gamma) {
for (uint16_t i = 0; i < 256; i++) {
// corrected = val ^ gamma
auto corrected = to_uint8_scale(gamma_correct(i / 255.0f, gamma));
this->gamma_table_[i] = corrected;
}
if (gamma == 0.0f) {
for (uint16_t i = 0; i < 256; i++)
this->gamma_reverse_table_[i] = i;
return;
}
for (uint16_t i = 0; i < 256; i++) {
// val = corrected ^ (1/gamma)
auto uncorrected = to_uint8_scale(powf(i / 255.0f, 1.0f / gamma));
this->gamma_reverse_table_[i] = uncorrected;
}
uint8_t ESPColorCorrection::gamma_correct_(uint8_t value) const {
if (this->gamma_table_ == nullptr)
return value;
return static_cast<uint8_t>((progmem_read_uint16(&this->gamma_table_[value]) + 128) / 257);
}
uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const {
if (this->gamma_table_ == nullptr)
return value;
if (value == 0)
return 0;
uint16_t target = value * 257; // Scale 0-255 to 0-65535
uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target);
if (lo >= 255)
return 255;
uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]);
uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]);
return (target - a <= b - target) ? lo : lo + 1;
}
} // namespace esphome::light
+32 -13
View File
@@ -1,15 +1,30 @@
#pragma once
#include "esphome/core/color.h"
#include "esphome/core/hal.h"
namespace esphome::light {
/// Binary search a monotonically increasing uint16[256] PROGMEM table.
/// Returns the largest index where table[index] <= target.
inline uint8_t gamma_table_reverse_search(const uint16_t *table, uint16_t target) {
uint8_t lo = 0, hi = 255;
while (lo < hi) {
uint8_t mid = (lo + hi + 1) / 2;
if (progmem_read_uint16(&table[mid]) <= target) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
class ESPColorCorrection {
public:
ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {}
void set_max_brightness(const Color &max_brightness) { this->max_brightness_ = max_brightness; }
void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; }
void calculate_gamma_table(float gamma);
void set_gamma_table(const uint16_t *table) { this->gamma_table_ = table; }
inline Color color_correct(Color color) const ESPHOME_ALWAYS_INLINE {
// corrected = (uncorrected * max_brightness * local_brightness) ^ gamma
return Color(this->color_correct_red(color.red), this->color_correct_green(color.green),
@@ -17,19 +32,19 @@ class ESPColorCorrection {
}
inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE {
// uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness)
@@ -39,36 +54,40 @@ class ESPColorCorrection {
inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.red == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(red) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.green == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(green) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(blue) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.white == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(white) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
protected:
uint8_t gamma_table_[256];
uint8_t gamma_reverse_table_[256];
Color max_brightness_;
/// Forward gamma: read uint16 PROGMEM table, convert to uint8
uint8_t gamma_correct_(uint8_t value) const;
/// Reverse gamma: binary search the forward PROGMEM table
uint8_t gamma_uncorrect_(uint8_t value) const;
const uint16_t *gamma_table_{nullptr};
Color max_brightness_{255, 255, 255, 255};
uint8_t local_brightness_{255};
};
+2 -3
View File
@@ -389,9 +389,8 @@ void LightCall::transform_parameters_() {
const float ww_fraction = (color_temp - min_mireds) / range;
const float cw_fraction = 1.0f - ww_fraction;
const float max_cw_ww = std::max(ww_fraction, cw_fraction);
const float gamma = this->parent_->get_gamma_correct();
this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma);
this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma);
this->cold_white_ = this->parent_->gamma_uncorrect_lut(cw_fraction / max_cw_ww);
this->warm_white_ = this->parent_->gamma_uncorrect_lut(ww_fraction / max_cw_ww);
this->set_flag_(FLAG_HAS_COLD_WHITE);
this->set_flag_(FLAG_HAS_WARM_WHITE);
}
+21 -27
View File
@@ -111,60 +111,54 @@ class LightColorValues {
}
}
// Note that method signature of as_* methods is kept as-is for compatibility reasons, so not all parameters
// are always used or necessary. Methods will be deprecated later.
/// Convert these light color values to a binary representation and write them to binary.
void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; }
/// Convert these light color values to a brightness-only representation and write them to brightness.
void as_brightness(float *brightness, float gamma = 0) const {
*brightness = gamma_correct(this->state_ * this->brightness_, gamma);
}
void as_brightness(float *brightness) const { *brightness = this->state_ * this->brightness_; }
/// Convert these light color values to an RGB representation and write them to red, green, blue.
void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const {
void as_rgb(float *red, float *green, float *blue) const {
if (this->color_mode_ & ColorCapability::RGB) {
float brightness = this->state_ * this->brightness_ * this->color_brightness_;
*red = gamma_correct(brightness * this->red_, gamma);
*green = gamma_correct(brightness * this->green_, gamma);
*blue = gamma_correct(brightness * this->blue_, gamma);
*red = brightness * this->red_;
*green = brightness * this->green_;
*blue = brightness * this->blue_;
} else {
*red = *green = *blue = 0;
}
}
/// Convert these light color values to an RGBW representation and write them to red, green, blue, white.
void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0,
bool color_interlock = false) const {
this->as_rgb(red, green, blue, gamma);
void as_rgbw(float *red, float *green, float *blue, float *white) const {
this->as_rgb(red, green, blue);
if (this->color_mode_ & ColorCapability::WHITE) {
*white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma);
*white = this->state_ * this->brightness_ * this->white_;
} else {
*white = 0;
}
}
/// Convert these light color values to an RGBWW representation with the given parameters.
void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, float gamma = 0,
void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
bool constant_brightness = false) const {
this->as_rgb(red, green, blue, gamma);
this->as_cwww(cold_white, warm_white, gamma, constant_brightness);
this->as_rgb(red, green, blue);
this->as_cwww(cold_white, warm_white, constant_brightness);
}
/// Convert these light color values to an RGB+CT+BR representation with the given parameters.
void as_rgbct(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue,
float *color_temperature, float *white_brightness, float gamma = 0) const {
this->as_rgb(red, green, blue, gamma);
this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness, gamma);
float *color_temperature, float *white_brightness) const {
this->as_rgb(red, green, blue);
this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness);
}
/// Convert these light color values to an CWWW representation with the given parameters.
void as_cwww(float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const {
void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const {
if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) {
const float cw_level = gamma_correct(this->cold_white_, gamma);
const float ww_level = gamma_correct(this->warm_white_, gamma);
const float white_level = gamma_correct(this->state_ * this->brightness_, gamma);
const float cw_level = this->cold_white_;
const float ww_level = this->warm_white_;
const float white_level = this->state_ * this->brightness_;
if (!constant_brightness) {
*cold_white = white_level * cw_level;
*warm_white = white_level * ww_level;
@@ -184,13 +178,13 @@ class LightColorValues {
}
/// Convert these light color values to a CT+BR representation with the given parameters.
void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, float *white_brightness,
float gamma = 0) const {
void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature,
float *white_brightness) const {
const float white_level = this->color_mode_ & ColorCapability::RGB ? this->white_ : 1;
if (this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) {
*color_temperature =
(this->color_temperature_ - color_temperature_cw) / (color_temperature_ww - color_temperature_cw);
*white_brightness = gamma_correct(this->state_ * this->brightness_ * white_level, gamma);
*white_brightness = this->state_ * this->brightness_ * white_level;
} else { // Probably won't get here but put this here anyway.
*white_brightness = 0;
}
+68 -10
View File
@@ -1,4 +1,5 @@
#include "light_state.h"
#include "esp_color_correction.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
@@ -204,33 +205,90 @@ void LightState::add_effects(const std::initializer_list<LightEffect *> &effects
void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
void LightState::current_values_as_brightness(float *brightness) {
this->current_values.as_brightness(brightness, this->gamma_correct_);
this->current_values.as_brightness(brightness);
*brightness = this->gamma_correct_lut(*brightness);
}
void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) {
this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false);
void LightState::current_values_as_rgb(float *red, float *green, float *blue) {
this->current_values.as_rgb(red, green, blue);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
}
void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) {
this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false);
void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white) {
this->current_values.as_rgbw(red, green, blue, white);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
*white = this->gamma_correct_lut(*white);
}
void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
bool constant_brightness) {
this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, this->gamma_correct_, constant_brightness);
this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
*cold_white = this->gamma_correct_lut(*cold_white);
*warm_white = this->gamma_correct_lut(*warm_white);
}
void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature,
float *white_brightness) {
auto traits = this->get_traits();
this->current_values.as_rgbct(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, color_temperature,
white_brightness, this->gamma_correct_);
white_brightness);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
*white_brightness = this->gamma_correct_lut(*white_brightness);
}
void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) {
this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness);
this->current_values.as_cwww(cold_white, warm_white, constant_brightness);
*cold_white = this->gamma_correct_lut(*cold_white);
*warm_white = this->gamma_correct_lut(*warm_white);
}
void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) {
auto traits = this->get_traits();
this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness,
this->gamma_correct_);
this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness);
*white_brightness = this->gamma_correct_lut(*white_brightness);
}
#ifdef USE_LIGHT_GAMMA_LUT
float LightState::gamma_correct_lut(float value) const {
if (value <= 0.0f)
return 0.0f;
if (value >= 1.0f)
return 1.0f;
if (this->gamma_table_ == nullptr)
return value;
float scaled = value * 255.0f;
auto idx = static_cast<uint8_t>(scaled);
if (idx >= 255)
return progmem_read_uint16(&this->gamma_table_[255]) / 65535.0f;
float frac = scaled - idx;
float a = progmem_read_uint16(&this->gamma_table_[idx]);
float b = progmem_read_uint16(&this->gamma_table_[idx + 1]);
return (a + frac * (b - a)) / 65535.0f;
}
float LightState::gamma_uncorrect_lut(float value) const {
if (value <= 0.0f)
return 0.0f;
if (value >= 1.0f)
return 1.0f;
if (this->gamma_table_ == nullptr)
return value;
uint16_t target = static_cast<uint16_t>(value * 65535.0f);
uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target);
if (lo >= 255)
return 1.0f;
// Interpolate between lo and lo+1
uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]);
uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]);
if (b == a)
return lo / 255.0f;
float frac = static_cast<float>(target - a) / static_cast<float>(b - a);
return (lo + frac) / 255.0f;
}
#endif // USE_LIGHT_GAMMA_LUT
bool LightState::is_transformer_active() { return this->is_transformer_active_; }
void LightState::start_effect_(uint32_t effect_index) {
+39 -2
View File
@@ -11,7 +11,9 @@
#include "light_traits.h"
#include "light_transformer.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include <strings.h>
#include <vector>
@@ -166,6 +168,23 @@ class LightState : public EntityBase, public Component {
void set_gamma_correct(float gamma_correct);
float get_gamma_correct() const { return this->gamma_correct_; }
#ifdef USE_LIGHT_GAMMA_LUT
/// Set pre-computed gamma forward lookup table (256-entry uint16 PROGMEM array)
void set_gamma_table(const uint16_t *forward) { this->gamma_table_ = forward; }
/// Get the forward gamma lookup table
const uint16_t *get_gamma_table() const { return this->gamma_table_; }
/// Apply gamma correction using the pre-computed forward LUT
float gamma_correct_lut(float value) const;
/// Reverse gamma correction by binary-searching the forward LUT
float gamma_uncorrect_lut(float value) const;
#else
/// No gamma LUT — passthrough
float gamma_correct_lut(float value) const { return value; }
float gamma_uncorrect_lut(float value) const { return value; }
#endif // USE_LIGHT_GAMMA_LUT
/// Set the restore mode of this light
void set_restore_mode(LightRestoreMode restore_mode);
@@ -200,6 +219,20 @@ class LightState : public EntityBase, public Component {
return 0; // Effect not found
}
/// Get effect index by name (const char* overload, avoids std::string construction).
uint32_t get_effect_index(const char *name, size_t len) const {
if (len == 4 && ESPHOME_strncasecmp_P(name, ESPHOME_PSTR("none"), 4) == 0) {
return 0;
}
StringRef ref(name, len);
for (size_t i = 0; i < this->effects_.size(); i++) {
if (str_equals_case_insensitive(ref, this->effects_[i]->get_name())) {
return i + 1;
}
}
return 0;
}
/// Get effect by index. Returns nullptr if index is invalid.
LightEffect *get_effect_by_index(uint32_t index) const {
if (index == 0 || index > this->effects_.size()) {
@@ -224,9 +257,9 @@ class LightState : public EntityBase, public Component {
void current_values_as_brightness(float *brightness);
void current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock = false);
void current_values_as_rgb(float *red, float *green, float *blue);
void current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock = false);
void current_values_as_rgbw(float *red, float *green, float *blue, float *white);
void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
bool constant_brightness = false);
@@ -297,6 +330,10 @@ class LightState : public EntityBase, public Component {
uint32_t flash_transition_length_{};
/// Gamma correction factor for the light.
float gamma_correct_{};
#ifdef USE_LIGHT_GAMMA_LUT
const uint16_t *gamma_table_{nullptr};
#endif // USE_LIGHT_GAMMA_LUT
/// Whether the light value should be written in the next cycle.
bool next_write_{true};
// for effects, true if a transformer (transition) is active.
+1 -1
View File
@@ -16,7 +16,7 @@ class LVLight : public light::LightOutput {
}
void write_state(light::LightState *state) override {
float red, green, blue;
state->current_values_as_rgb(&red, &green, &blue, false);
state->current_values_as_rgb(&red, &green, &blue);
auto color = lv_color_make(red * 255, green * 255, blue * 255);
if (this->obj_ != nullptr) {
this->set_value_(color);
+26 -45
View File
@@ -8,90 +8,71 @@ namespace mcp23016 {
static const char *const TAG = "mcp23016";
void MCP23016::setup() {
uint8_t iocon;
if (!this->read_reg_(MCP23016_IOCON0, &iocon)) {
uint16_t iocon;
// MCP23016 registers operate as paired 16-bit registers. Addressing the
// odd register (e.g. IOCON1) reads/writes that register first, then wraps
// to the even register (IOCON0) in the same pair. Starting from the odd
// address gives the correct byte order for 1 << pin mapping:
// high byte = port 1 (pins 8-15), low byte = port 0 (pins 0-7).
if (!this->read_reg_(MCP23016_IOCON1, &iocon)) {
this->mark_failed();
return;
}
// Read current output register state
this->read_reg_(MCP23016_OLAT0, &this->olat_0_);
this->read_reg_(MCP23016_OLAT1, &this->olat_1_);
this->read_reg_(MCP23016_OLAT1, &this->olat_);
// all pins input
this->write_reg_(MCP23016_IODIR0, 0xFF);
this->write_reg_(MCP23016_IODIR1, 0xFF);
this->write_reg_(MCP23016_IODIR1, 0xFFFF);
}
void MCP23016::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
}
bool MCP23016::digital_read_hw(uint8_t pin) {
uint8_t reg_addr = pin < 8 ? MCP23016_GP0 : MCP23016_GP1;
uint8_t value = 0;
if (!this->read_reg_(reg_addr, &value)) {
return false;
}
// Update the appropriate part of input_mask_
if (pin < 8) {
this->input_mask_ = (this->input_mask_ & 0xFF00) | value;
} else {
this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8);
}
return true;
}
bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); }
bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void MCP23016::digital_write_hw(uint8_t pin, bool value) {
uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1;
this->update_reg_(pin, value, reg_addr);
}
void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin, value, MCP23016_OLAT1); }
void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
uint8_t iodir = pin < 8 ? MCP23016_IODIR0 : MCP23016_IODIR1;
if (flags == gpio::FLAG_INPUT) {
this->update_reg_(pin, true, iodir);
this->update_reg_(pin, true, MCP23016_IODIR1);
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_reg_(pin, false, iodir);
this->update_reg_(pin, false, MCP23016_IODIR1);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
bool MCP23016::read_reg_(uint8_t reg, uint16_t *value) {
if (this->is_failed())
return false;
return this->read_byte(reg, value);
return this->read_byte_16(reg, value);
}
bool MCP23016::write_reg_(uint8_t reg, uint8_t value) {
bool MCP23016::write_reg_(uint8_t reg, uint16_t value) {
if (this->is_failed())
return false;
return this->write_byte(reg, value);
return this->write_byte_16(reg, value);
}
void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) {
uint8_t bit = pin % 8;
uint8_t reg_value = 0;
if (reg_addr == MCP23016_OLAT0) {
reg_value = this->olat_0_;
} else if (reg_addr == MCP23016_OLAT1) {
reg_value = this->olat_1_;
uint16_t reg_value = 0;
if (reg_addr == MCP23016_OLAT1) {
reg_value = this->olat_;
} else {
this->read_reg_(reg_addr, &reg_value);
}
if (pin_value) {
reg_value |= 1 << bit;
reg_value |= 1 << pin;
} else {
reg_value &= ~(1 << bit);
reg_value &= ~(1 << pin);
}
this->write_reg_(reg_addr, reg_value);
if (reg_addr == MCP23016_OLAT0) {
this->olat_0_ = reg_value;
} else if (reg_addr == MCP23016_OLAT1) {
this->olat_1_ = reg_value;
if (reg_addr == MCP23016_OLAT1) {
this->olat_ = reg_value;
}
}
+7 -8
View File
@@ -19,13 +19,13 @@ enum MCP23016GPIORegisters {
// 1 side
MCP23016_GP1 = 0x01,
MCP23016_OLAT1 = 0x03,
MCP23016_IPOL1 = 0x04,
MCP23016_IPOL1 = 0x05,
MCP23016_IODIR1 = 0x07,
MCP23016_INTCAP1 = 0x08,
MCP23016_INTCAP1 = 0x09,
MCP23016_IOCON1 = 0x0B,
};
class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander<uint8_t, 16> {
class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander<uint16_t, 16> {
public:
MCP23016() = default;
@@ -42,16 +42,15 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::
void digital_write_hw(uint8_t pin, bool value) override;
// read a given register
bool read_reg_(uint8_t reg, uint8_t *value);
bool read_reg_(uint8_t reg, uint16_t *value);
// write a value to a given register
bool write_reg_(uint8_t reg, uint8_t value);
bool write_reg_(uint8_t reg, uint16_t value);
// update registers with given pin value.
void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a);
uint8_t olat_0_{0x00};
uint8_t olat_1_{0x00};
uint16_t olat_{0x0000};
// Cache for input values (16-bit combined for both banks)
uint16_t input_mask_{0x00};
uint16_t input_mask_{0x0000};
};
class MCP23016GPIOPin : public GPIOPin {
+1 -1
View File
@@ -20,7 +20,7 @@ class RGBLightOutput : public light::LightOutput {
}
void write_state(light::LightState *state) override {
float red, green, blue;
state->current_values_as_rgb(&red, &green, &blue, false);
state->current_values_as_rgb(&red, &green, &blue);
this->red_->set_level(red);
this->green_->set_level(green);
this->blue_->set_level(blue);
+1 -1
View File
@@ -25,7 +25,7 @@ class RGBWLightOutput : public light::LightOutput {
}
void write_state(light::LightState *state) override {
float red, green, blue, white;
state->current_values_as_rgbw(&red, &green, &blue, &white, this->color_interlock_);
state->current_values_as_rgbw(&red, &green, &blue, &white);
this->red_->set_level(red);
this->green_->set_level(green);
this->blue_->set_level(blue);
+1
View File
@@ -169,6 +169,7 @@ async def to_code(config):
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_RP2040")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.set_cpp_standard("gnu++20")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "RP2040")
+1
View File
@@ -34,6 +34,7 @@ void HOT arch_feed_wdt() { watchdog_update(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }
+63 -109
View File
@@ -1,135 +1,81 @@
#include "socket.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "socket.h"
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <cstring>
#include "esphome/core/application.h"
#ifdef USE_ESP32
#include <esp_idf_version.h>
#include <lwip/sockets.h>
#endif
namespace esphome::socket {
class BSDSocketImpl final : public Socket {
public:
BSDSocketImpl(int fd, bool monitor_loop = false) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
}
}
~BSDSocketImpl() override {
if (!this->closed_) {
this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
}
}
int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(this->fd_, addr, addrlen); }
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
int fd = ::accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<BSDSocketImpl>(fd, false);
}
std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override {
int fd = ::accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<BSDSocketImpl>(fd, true);
BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
}
}
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
int close() override {
if (!this->closed_) {
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
int ret = ::close(this->fd_);
this->closed_ = true;
return ret;
BSDSocketImpl::~BSDSocketImpl() {
if (!this->closed_) {
this->close();
}
}
int BSDSocketImpl::close() {
if (!this->closed_) {
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
int ret = ::close(this->fd_);
this->closed_ = true;
return ret;
}
return 0;
}
int BSDSocketImpl::setblocking(bool blocking) {
int fl = ::fcntl(this->fd_, F_GETFL, 0);
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
::fcntl(this->fd_, F_SETFL, fl);
return 0;
}
bool BSDSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); }
size_t BSDSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getpeername(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
int shutdown(int how) override { return ::shutdown(this->fd_, how); }
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return ::getpeername(this->fd_, addr, addrlen);
}
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override {
return ::getsockname(this->fd_, addr, addrlen);
}
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
return ::getsockopt(this->fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
return ::setsockopt(this->fd_, level, optname, optval, optlen);
}
int listen(int backlog) override { return ::listen(this->fd_, backlog); }
ssize_t read(void *buf, size_t len) override {
#ifdef USE_ESP32
return ::lwip_read(this->fd_, buf, len);
#else
return ::read(this->fd_, buf, len);
#endif
}
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override {
#if defined(USE_ESP32) || defined(USE_HOST)
return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len);
#else
return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
#endif
}
ssize_t readv(const struct iovec *iov, int iovcnt) override {
#if defined(USE_ESP32)
return ::lwip_readv(this->fd_, iov, iovcnt);
#else
return ::readv(this->fd_, iov, iovcnt);
#endif
}
ssize_t write(const void *buf, size_t len) override {
#ifdef USE_ESP32
return ::lwip_write(this->fd_, buf, len);
#else
return ::write(this->fd_, buf, len);
#endif
}
ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); }
ssize_t writev(const struct iovec *iov, int iovcnt) override {
#if defined(USE_ESP32)
return ::lwip_writev(this->fd_, iov, iovcnt);
#else
return ::writev(this->fd_, iov, iovcnt);
#endif
}
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument)
}
int setblocking(bool blocking) override {
int fl = ::fcntl(this->fd_, F_GETFL, 0);
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
::fcntl(this->fd_, F_SETFL, fl);
size_t BSDSocketImpl::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getsockname(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
};
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
// Helper to create a socket with optional monitoring
static std::unique_ptr<Socket> create_socket(int domain, int type, int protocol, bool loop_monitored = false) {
static std::unique_ptr<BSDSocketImpl> create_socket(int domain, int type, int protocol, bool loop_monitored = false) {
int ret = ::socket(domain, type, protocol);
if (ret == -1)
return nullptr;
return std::unique_ptr<Socket>{new BSDSocketImpl(ret, loop_monitored)};
return make_unique<BSDSocketImpl>(ret, loop_monitored);
}
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
@@ -140,6 +86,14 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
return create_socket(domain, type, protocol, true);
}
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, false);
}
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, true);
}
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_BSD_SOCKETS
@@ -0,0 +1,114 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <memory>
#include <span>
#include "esphome/core/helpers.h"
#include "headers.h"
#ifdef USE_ESP32
#include <lwip/sockets.h>
#endif
namespace esphome::socket {
class BSDSocketImpl {
public:
BSDSocketImpl(int fd, bool monitor_loop = false);
~BSDSocketImpl();
BSDSocketImpl(const BSDSocketImpl &) = delete;
BSDSocketImpl &operator=(const BSDSocketImpl &) = delete;
int connect(const struct sockaddr *addr, socklen_t addrlen) { return ::connect(this->fd_, addr, addrlen); }
std::unique_ptr<BSDSocketImpl> accept(struct sockaddr *addr, socklen_t *addrlen) {
int fd = ::accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<BSDSocketImpl>(fd, false);
}
std::unique_ptr<BSDSocketImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
int fd = ::accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<BSDSocketImpl>(fd, true);
}
int bind(const struct sockaddr *addr, socklen_t addrlen) { return ::bind(this->fd_, addr, addrlen); }
int close();
int shutdown(int how) { return ::shutdown(this->fd_, how); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return ::getpeername(this->fd_, addr, addrlen); }
int getsockname(struct sockaddr *addr, socklen_t *addrlen) { return ::getsockname(this->fd_, addr, addrlen); }
/// Format peer address into a fixed-size buffer (no heap allocation)
size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
/// Format local address into a fixed-size buffer (no heap allocation)
size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) {
return ::getsockopt(this->fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
return ::setsockopt(this->fd_, level, optname, optval, optlen);
}
int listen(int backlog) { return ::listen(this->fd_, backlog); }
ssize_t read(void *buf, size_t len) {
#ifdef USE_ESP32
return ::lwip_read(this->fd_, buf, len);
#else
return ::read(this->fd_, buf, len);
#endif
}
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) {
#if defined(USE_ESP32) || defined(USE_HOST)
return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len);
#else
return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
#endif
}
ssize_t readv(const struct iovec *iov, int iovcnt) {
#if defined(USE_ESP32)
return ::lwip_readv(this->fd_, iov, iovcnt);
#else
return ::readv(this->fd_, iov, iovcnt);
#endif
}
ssize_t write(const void *buf, size_t len) {
#ifdef USE_ESP32
return ::lwip_write(this->fd_, buf, len);
#else
return ::write(this->fd_, buf, len);
#endif
}
ssize_t send(const void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); }
ssize_t writev(const struct iovec *iov, int iovcnt) {
#if defined(USE_ESP32)
return ::lwip_writev(this->fd_, iov, iovcnt);
#else
return ::writev(this->fd_, iov, iovcnt);
#endif
}
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) {
return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument)
}
int setblocking(bool blocking);
int loop() { return 0; }
bool ready() const;
int get_fd() const { return this->fd_; }
protected:
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
};
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_BSD_SOCKETS
+17
View File
@@ -183,3 +183,20 @@ using socklen_t = uint32_t;
#endif
#endif // USE_SOCKET_IMPL_BSD_SOCKETS
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
namespace esphome::socket {
// Maximum length for formatted socket address string (IP address without port)
// IPv4: "255.255.255.255" = 15 chars + null = 16
// IPv6: full address = 45 chars + null = 46
#if USE_NETWORK_IPV6
static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN
#else
static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN
#endif
} // namespace esphome::socket
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,200 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_SOCKET_IMPL_LWIP_TCP
#include <array>
#include <cerrno>
#include <cstring>
#include <memory>
#include <span>
#include "esphome/core/helpers.h"
#include "headers.h"
#include "lwip/ip.h"
#include "lwip/netif.h"
#include "lwip/opt.h"
#include "lwip/tcp.h"
namespace esphome::socket {
// Forward declaration
class LWIPRawImpl;
/// Non-virtual common base for LWIP raw TCP sockets.
/// Provides shared fields and methods for both connected and listening sockets.
/// No virtual methods — pure code sharing.
class LWIPRawCommon {
public:
LWIPRawCommon(sa_family_t family, struct tcp_pcb *pcb) : pcb_(pcb), family_(family) {}
~LWIPRawCommon();
LWIPRawCommon(const LWIPRawCommon &) = delete;
LWIPRawCommon &operator=(const LWIPRawCommon &) = delete;
int bind(const struct sockaddr *name, socklen_t addrlen);
int close();
int shutdown(int how);
int getpeername(struct sockaddr *name, socklen_t *addrlen);
int getsockname(struct sockaddr *name, socklen_t *addrlen);
/// Format peer address into a fixed-size buffer (no heap allocation)
size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
/// Format local address into a fixed-size buffer (no heap allocation)
size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);
int getsockopt(int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int level, int optname, const void *optval, socklen_t optlen);
int get_fd() const { return -1; }
protected:
int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen);
// Member ordering optimized to minimize padding on 32-bit systems
struct tcp_pcb *pcb_;
// don't use lwip nodelay flag, it sometimes causes reconnect
// instead use it for determining whether to call lwip_output
bool nodelay_ = false;
sa_family_t family_ = 0;
};
/// Connected socket implementation for LWIP raw TCP.
/// No virtual methods — callers always use the concrete type.
class LWIPRawImpl : public LWIPRawCommon {
public:
using LWIPRawCommon::LWIPRawCommon;
~LWIPRawImpl();
void init();
// Non-listening sockets return error
std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *, socklen_t *) {
errno = EINVAL;
return nullptr;
}
std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
return this->accept(addr, addrlen);
}
// Regular sockets can't be converted to listening - this shouldn't happen
// as listen() should only be called on sockets created for listening
int listen(int) {
errno = EOPNOTSUPP;
return -1;
}
ssize_t read(void *buf, size_t len);
ssize_t readv(const struct iovec *iov, int iovcnt);
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
errno = ENOTSUP;
return -1;
}
ssize_t write(const void *buf, size_t len);
ssize_t writev(const struct iovec *iov, int iovcnt);
ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
// return ::sendto(fd_, buf, len, flags, to, tolen);
errno = ENOSYS;
return -1;
}
bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
int setblocking(bool blocking) {
if (this->pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
if (blocking) {
// blocking operation not supported
errno = EINVAL;
return -1;
}
return 0;
}
int loop() { return 0; }
err_t recv_fn(struct pbuf *pb, err_t err);
static void s_err_fn(void *arg, err_t err);
static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
protected:
ssize_t internal_write_(const void *buf, size_t len);
int internal_output_();
pbuf *rx_buf_ = nullptr;
size_t rx_buf_offset_ = 0;
bool rx_closed_ = false;
};
/// Listening socket implementation for LWIP raw TCP.
/// Separate from LWIPRawImpl — no virtual dispatch needed.
class LWIPRawListenImpl : public LWIPRawCommon {
public:
using LWIPRawCommon::LWIPRawCommon;
~LWIPRawListenImpl();
void init();
bool ready() const { return this->accepted_socket_count_ > 0; }
std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *addr, socklen_t *addrlen);
std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
return this->accept(addr, addrlen);
}
int listen(int backlog);
// Listening sockets don't do I/O
ssize_t read(void *, size_t) {
errno = ENOTSUP;
return -1;
}
ssize_t write(const void *, size_t) {
errno = ENOTSUP;
return -1;
}
ssize_t readv(const struct iovec *, int) {
errno = ENOTSUP;
return -1;
}
ssize_t writev(const struct iovec *, int) {
errno = ENOTSUP;
return -1;
}
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
errno = ENOTSUP;
return -1;
}
ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
errno = ENOTSUP;
return -1;
}
int setblocking(bool) { return 0; }
int loop() { return 0; }
static void s_err_fn(void *arg, err_t err);
private:
err_t accept_fn_(struct tcp_pcb *newpcb, err_t err);
static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err);
// Accept queue - holds incoming connections briefly until the event loop calls accept()
// This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop
// 3 slots is plenty since connections are pulled out quickly by the event loop
//
// Memory analysis: std::array<3> vs original std::queue implementation:
// - std::queue uses std::deque internally which on 32-bit systems needs:
// 24 bytes (deque object) + 32+ bytes (map array) + heap allocations
// Total: ~56+ bytes minimum, plus heap fragmentation
// - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes)
// Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations
// Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation)
//
// By using a separate listening socket class, regular connected sockets save
// 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) of memory overhead on 32-bit systems
static constexpr size_t MAX_ACCEPTED_SOCKETS = 3;
std::array<std::unique_ptr<LWIPRawImpl>, MAX_ACCEPTED_SOCKETS> accepted_sockets_;
uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue
};
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_LWIP_TCP
+63 -76
View File
@@ -1,6 +1,6 @@
#include "socket.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "socket.h"
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
@@ -9,94 +9,73 @@
namespace esphome::socket {
class LwIPSocketImpl final : public Socket {
public:
LwIPSocketImpl(int fd, bool monitor_loop = false) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
}
}
~LwIPSocketImpl() override {
if (!this->closed_) {
this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
}
}
int connect(const struct sockaddr *addr, socklen_t addrlen) override {
return lwip_connect(this->fd_, addr, addrlen);
}
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
int fd = lwip_accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<LwIPSocketImpl>(fd, false);
}
std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override {
int fd = lwip_accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<LwIPSocketImpl>(fd, true);
LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
}
}
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
int close() override {
if (!this->closed_) {
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
int ret = lwip_close(this->fd_);
this->closed_ = true;
return ret;
LwIPSocketImpl::~LwIPSocketImpl() {
if (!this->closed_) {
this->close();
}
}
int LwIPSocketImpl::close() {
if (!this->closed_) {
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
int ret = lwip_close(this->fd_);
this->closed_ = true;
return ret;
}
return 0;
}
int LwIPSocketImpl::setblocking(bool blocking) {
int fl = lwip_fcntl(this->fd_, F_GETFL, 0);
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
lwip_fcntl(this->fd_, F_SETFL, fl);
return 0;
}
bool LwIPSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); }
size_t LwIPSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getpeername(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
int shutdown(int how) override { return lwip_shutdown(this->fd_, how); }
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return lwip_getpeername(this->fd_, addr, addrlen);
}
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override {
return lwip_getsockname(this->fd_, addr, addrlen);
}
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
return lwip_getsockopt(this->fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
return lwip_setsockopt(this->fd_, level, optname, optval, optlen);
}
int listen(int backlog) override { return lwip_listen(this->fd_, backlog); }
ssize_t read(void *buf, size_t len) override { return lwip_read(this->fd_, buf, len); }
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override {
return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
}
ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(this->fd_, iov, iovcnt); }
ssize_t write(const void *buf, size_t len) override { return lwip_write(this->fd_, buf, len); }
ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); }
ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(this->fd_, iov, iovcnt); }
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
return lwip_sendto(this->fd_, buf, len, flags, to, tolen);
}
int setblocking(bool blocking) override {
int fl = lwip_fcntl(this->fd_, F_GETFL, 0);
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
lwip_fcntl(this->fd_, F_SETFL, fl);
size_t LwIPSocketImpl::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getsockname(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
};
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
// Helper to create a socket with optional monitoring
static std::unique_ptr<Socket> create_socket(int domain, int type, int protocol, bool loop_monitored = false) {
static std::unique_ptr<LwIPSocketImpl> create_socket(int domain, int type, int protocol, bool loop_monitored = false) {
int ret = lwip_socket(domain, type, protocol);
if (ret == -1)
return nullptr;
return std::unique_ptr<Socket>{new LwIPSocketImpl(ret, loop_monitored)};
return make_unique<LwIPSocketImpl>(ret, loop_monitored);
}
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
@@ -107,6 +86,14 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
return create_socket(domain, type, protocol, true);
}
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, false);
}
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, true);
}
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_LWIP_SOCKETS
@@ -0,0 +1,80 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
#include <memory>
#include <span>
#include "esphome/core/helpers.h"
#include "headers.h"
namespace esphome::socket {
class LwIPSocketImpl {
public:
LwIPSocketImpl(int fd, bool monitor_loop = false);
~LwIPSocketImpl();
LwIPSocketImpl(const LwIPSocketImpl &) = delete;
LwIPSocketImpl &operator=(const LwIPSocketImpl &) = delete;
int connect(const struct sockaddr *addr, socklen_t addrlen) { return lwip_connect(this->fd_, addr, addrlen); }
std::unique_ptr<LwIPSocketImpl> accept(struct sockaddr *addr, socklen_t *addrlen) {
int fd = lwip_accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<LwIPSocketImpl>(fd, false);
}
std::unique_ptr<LwIPSocketImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
int fd = lwip_accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<LwIPSocketImpl>(fd, true);
}
int bind(const struct sockaddr *addr, socklen_t addrlen) { return lwip_bind(this->fd_, addr, addrlen); }
int close();
int shutdown(int how) { return lwip_shutdown(this->fd_, how); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return lwip_getpeername(this->fd_, addr, addrlen); }
int getsockname(struct sockaddr *addr, socklen_t *addrlen) { return lwip_getsockname(this->fd_, addr, addrlen); }
/// Format peer address into a fixed-size buffer (no heap allocation)
size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
/// Format local address into a fixed-size buffer (no heap allocation)
size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) {
return lwip_getsockopt(this->fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
return lwip_setsockopt(this->fd_, level, optname, optval, optlen);
}
int listen(int backlog) { return lwip_listen(this->fd_, backlog); }
ssize_t read(void *buf, size_t len) { return lwip_read(this->fd_, buf, len); }
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) {
return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
}
ssize_t readv(const struct iovec *iov, int iovcnt) { return lwip_readv(this->fd_, iov, iovcnt); }
ssize_t write(const void *buf, size_t len) { return lwip_write(this->fd_, buf, len); }
ssize_t send(const void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); }
ssize_t writev(const struct iovec *iov, int iovcnt) { return lwip_writev(this->fd_, iov, iovcnt); }
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) {
return lwip_sendto(this->fd_, buf, len, flags, to, tolen);
}
int setblocking(bool blocking);
int loop() { return 0; }
bool ready() const;
int get_fd() const { return this->fd_; }
protected:
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
};
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_LWIP_SOCKETS
+6 -26
View File
@@ -8,10 +8,10 @@
namespace esphome::socket {
Socket::~Socket() {}
#ifdef USE_SOCKET_SELECT_SUPPORT
bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); }
// Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets).
// Checks if the Application's select() loop has marked this fd as ready.
bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); }
#endif
// Platform-specific inet_ntop wrappers
@@ -81,26 +81,6 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s
return 0;
}
size_t Socket::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getpeername(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
size_t Socket::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getsockname(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
std::unique_ptr<Socket> socket_ip(int type, int protocol) {
#if USE_NETWORK_IPV6
return socket(AF_INET6, type, protocol);
@@ -109,11 +89,11 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol) {
#endif /* USE_NETWORK_IPV6 */
}
std::unique_ptr<Socket> socket_ip_loop_monitored(int type, int protocol) {
std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol) {
#if USE_NETWORK_IPV6
return socket_loop_monitored(AF_INET6, type, protocol);
return socket_listen_loop_monitored(AF_INET6, type, protocol);
#else
return socket_loop_monitored(AF_INET, type, protocol);
return socket_listen_loop_monitored(AF_INET, type, protocol);
#endif /* USE_NETWORK_IPV6 */
}
+35 -75
View File
@@ -7,87 +7,41 @@
#include "headers.h"
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
// Include only the active implementation's header.
// SOCKADDR_STR_LEN is defined in headers.h.
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include "bsd_sockets_impl.h"
#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
#include "lwip_sockets_impl.h"
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
#include "lwip_raw_tcp_impl.h"
#endif
namespace esphome::socket {
// Maximum length for formatted socket address string (IP address without port)
// IPv4: "255.255.255.255" = 15 chars + null = 16
// IPv6: full address = 45 chars + null = 46
#if USE_NETWORK_IPV6
static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN
#else
static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN
// Type aliases — only one implementation is active per build.
// Socket is the concrete type for connected sockets.
// ListenSocket is the concrete type for listening/server sockets.
// On BSD and LWIP_SOCKETS, both aliases resolve to the same type.
// On LWIP_TCP, they are different types (no virtual dispatch between them).
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
using Socket = BSDSocketImpl;
using ListenSocket = BSDSocketImpl;
#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
using Socket = LwIPSocketImpl;
using ListenSocket = LwIPSocketImpl;
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
using Socket = LWIPRawImpl;
using ListenSocket = LWIPRawListenImpl;
#endif
class Socket {
public:
Socket() = default;
virtual ~Socket();
Socket(const Socket &) = delete;
Socket &operator=(const Socket &) = delete;
virtual std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) = 0;
/// Accept a connection and monitor it in the main loop
/// NOTE: This function is NOT thread-safe and must only be called from the main loop
virtual std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
return accept(addr, addrlen); // Default implementation for backward compatibility
}
virtual int bind(const struct sockaddr *addr, socklen_t addrlen) = 0;
virtual int close() = 0;
// not supported yet:
// virtual int connect(const std::string &address) = 0;
#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0;
#endif
virtual int shutdown(int how) = 0;
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0;
/// Format peer address into a fixed-size buffer (no heap allocation)
/// Non-virtual wrapper around getpeername() - can be optimized away if unused
/// Returns number of characters written (excluding null terminator), or 0 on error
size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
/// Format local address into a fixed-size buffer (no heap allocation)
/// Non-virtual wrapper around getsockname() - can be optimized away if unused
size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);
virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0;
virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0;
virtual int listen(int backlog) = 0;
virtual ssize_t read(void *buf, size_t len) = 0;
virtual ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) = 0;
virtual ssize_t readv(const struct iovec *iov, int iovcnt) = 0;
virtual ssize_t write(const void *buf, size_t len) = 0;
virtual ssize_t writev(const struct iovec *iov, int iovcnt) = 0;
virtual ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) = 0;
virtual int setblocking(bool blocking) = 0;
virtual int loop() { return 0; };
/// Get the underlying file descriptor (returns -1 if not supported)
/// Non-virtual: only one socket implementation is active per build.
#ifdef USE_SOCKET_SELECT_SUPPORT
int get_fd() const { return this->fd_; }
#else
int get_fd() const { return -1; }
/// Shared ready() helper for fd-based socket implementations.
/// Checks if the Application's select() loop has marked this fd as ready.
bool socket_ready_fd(int fd, bool loop_monitored);
#endif
/// Check if socket has data ready to read. Must only be called from the main loop thread.
/// For select()-based sockets: non-virtual, checks Application's select() results
/// For LWIP raw TCP sockets: virtual, checks internal buffer state
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const;
#else
virtual bool ready() const { return true; }
#endif
protected:
#ifdef USE_SOCKET_SELECT_SUPPORT
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
#endif
};
/// Create a socket of the given domain, type and protocol.
std::unique_ptr<Socket> socket(int domain, int type, int protocol);
/// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol.
@@ -100,7 +54,13 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol);
/// NOTE: On ESP platforms, FD_SETSIZE is typically 10, limiting the number of monitored sockets.
/// File descriptors >= FD_SETSIZE will not be monitored and will log an error.
std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol);
std::unique_ptr<Socket> socket_ip_loop_monitored(int type, int protocol);
/// Create a listening socket of the given domain, type and protocol.
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol);
/// Create a listening socket and monitor it for data in the main loop.
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol);
/// Create a listening socket in the newest available IP domain and monitor it.
std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol);
/// Set a sockaddr to the specified address and port for the IP version used by socket_ip().
/// @param addr Destination sockaddr structure
+5
View File
@@ -11,6 +11,11 @@ TextCall &TextCall::set_value(const std::string &value) {
return *this;
}
TextCall &TextCall::set_value(const char *value, size_t len) {
this->value_ = std::string(value, len);
return *this;
}
void TextCall::validate_() {
const auto *name = this->parent_->get_name().c_str();
+1
View File
@@ -13,6 +13,7 @@ class TextCall {
void perform();
TextCall &set_value(const std::string &value);
TextCall &set_value(const char *value, size_t len);
protected:
Text *const parent_;
@@ -5,6 +5,7 @@
#include "esphome/core/progmem.h"
#include <cmath>
#include <cstring>
namespace esphome::water_heater {
@@ -23,23 +24,25 @@ WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) {
return *this;
}
WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) {
if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("OFF")) == 0) {
WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) { return this->set_mode(mode, strlen(mode)); }
WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode, size_t len) {
if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("OFF"), 3) == 0) {
this->set_mode(WATER_HEATER_MODE_OFF);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ECO")) == 0) {
} else if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("ECO"), 3) == 0) {
this->set_mode(WATER_HEATER_MODE_ECO);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ELECTRIC")) == 0) {
} else if (len == 8 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("ELECTRIC"), 8) == 0) {
this->set_mode(WATER_HEATER_MODE_ELECTRIC);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE")) == 0) {
} else if (len == 11 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE"), 11) == 0) {
this->set_mode(WATER_HEATER_MODE_PERFORMANCE);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND")) == 0) {
} else if (len == 11 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND"), 11) == 0) {
this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP")) == 0) {
} else if (len == 9 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP"), 9) == 0) {
this->set_mode(WATER_HEATER_MODE_HEAT_PUMP);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("GAS")) == 0) {
} else if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("GAS"), 3) == 0) {
this->set_mode(WATER_HEATER_MODE_GAS);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode);
ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode);
}
return *this;
}
@@ -76,7 +76,8 @@ class WaterHeaterCall {
WaterHeaterCall &set_mode(WaterHeaterMode mode);
WaterHeaterCall &set_mode(const char *mode);
WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str()); }
WaterHeaterCall &set_mode(const char *mode, size_t len);
WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str(), mode.size()); }
WaterHeaterCall &set_target_temperature(float temperature);
WaterHeaterCall &set_target_temperature_low(float temperature);
WaterHeaterCall &set_target_temperature_high(float temperature);
+29 -9
View File
@@ -969,7 +969,9 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
parse_light_param_uint_(request, ESPHOME_F("transition"), call, &decltype(call)::set_transition_length, 1000);
if (is_on) {
parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect);
parse_cstr_param_(
request, ESPHOME_F("effect"), call,
static_cast<light::LightCall &(light::LightCall::*) (const char *, size_t)>(&decltype(call)::set_effect));
}
DEFER_ACTION(call, call.perform());
@@ -1368,7 +1370,9 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
}
auto call = obj->make_call();
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
parse_cstr_param_(
request, ESPHOME_F("value"), call,
static_cast<text::TextCall &(text::TextCall::*) (const char *, size_t)>(&decltype(call)::set_value));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1426,7 +1430,9 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
}
auto call = obj->make_call();
parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option);
parse_cstr_param_(
request, ESPHOME_F("option"), call,
static_cast<select::SelectCall &(select::SelectCall::*) (const char *, size_t)>(&decltype(call)::set_option));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1487,10 +1493,18 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
auto call = obj->make_call();
// Parse string mode parameters
parse_string_param_(request, ESPHOME_F("mode"), call, &decltype(call)::set_mode);
parse_string_param_(request, ESPHOME_F("fan_mode"), call, &decltype(call)::set_fan_mode);
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
parse_string_param_(request, ESPHOME_F("preset"), call, &decltype(call)::set_preset);
parse_cstr_param_(
request, ESPHOME_F("mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(&decltype(call)::set_mode));
parse_cstr_param_(request, ESPHOME_F("fan_mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
&decltype(call)::set_fan_mode));
parse_cstr_param_(request, ESPHOME_F("swing_mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
&decltype(call)::set_swing_mode));
parse_cstr_param_(request, ESPHOME_F("preset"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
&decltype(call)::set_preset));
// Parse temperature parameters
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
@@ -1804,7 +1818,10 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
}
auto call = obj->make_call();
parse_string_param_(request, ESPHOME_F("code"), call, &decltype(call)::set_code);
parse_cstr_param_(
request, ESPHOME_F("code"), call,
static_cast<alarm_control_panel::AlarmControlPanelCall &(
alarm_control_panel::AlarmControlPanelCall::*) (const char *, size_t)>(&decltype(call)::set_code));
// Lookup table for alarm control panel methods
static const struct {
@@ -1892,7 +1909,10 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
water_heater::WaterHeaterCall &base_call = call;
// Parse mode parameter
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
parse_cstr_param_(
request, ESPHOME_F("mode"), base_call,
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*) (const char *, size_t)>(
&water_heater::WaterHeaterCall::set_mode));
// Parse temperature parameters
parse_num_param_(request, ESPHOME_F("target_temperature"), base_call,
+4 -4
View File
@@ -533,13 +533,13 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
}
}
// Generic helper to parse and apply a string parameter
// Generic helper to parse and apply a string parameter using const char* setter (avoids std::string allocation)
template<typename T, typename Ret>
void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
Ret (T::*setter)(const std::string &)) {
void parse_cstr_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
Ret (T::*setter)(const char *, size_t)) {
if (request->hasArg(param_name)) {
const auto &value = request->arg(param_name);
(call.*setter)(std::string(value.c_str(), value.length()));
(call.*setter)(value.c_str(), value.length());
}
}
+1
View File
@@ -112,6 +112,7 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None:
def zephyr_to_code(config):
cg.add_build_flag("-DUSE_ZEPHYR")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.set_cpp_standard("gnu++20")
# build is done by west so bypass board checking in platformio
cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards"))
+1
View File
@@ -60,6 +60,7 @@ void arch_restart() { sys_reboot(SYS_REBOOT_COLD); }
uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); }
uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
Mutex::Mutex() {
auto *mutex = new k_mutex();
+8 -11
View File
@@ -79,7 +79,12 @@ static void insertion_sort_by_priority(Iterator first, Iterator last) {
}
}
void Application::register_component_(Component *comp) { this->components_.push_back(comp); }
void Application::register_component_impl_(Component *comp, bool has_loop) {
if (has_loop) {
comp->component_state_ |= COMPONENT_HAS_LOOP;
}
this->components_.push_back(comp);
}
void Application::setup() {
ESP_LOGI(TAG, "Running through setup()");
ESP_LOGV(TAG, "Sorting components by setup priority");
@@ -382,16 +387,8 @@ void Application::teardown_components(uint32_t timeout_ms) {
}
void Application::calculate_looping_components_() {
// Count total components that need looping
size_t total_looping = 0;
for (auto *obj : this->components_) {
if (obj->has_overridden_loop()) {
total_looping++;
}
}
// Initialize FixedVector with exact size - no reallocation possible
this->looping_components_.init(total_looping);
// FixedVector capacity was pre-initialized by codegen with the exact count
// of components that override loop(), computed at C++ compile time.
// Add all components with loop override that aren't already LOOP_DONE
// Some components (like logger) may call disable_loop() during initialization
+23 -3
View File
@@ -5,6 +5,7 @@
#include <limits>
#include <span>
#include <string>
#include <type_traits>
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
@@ -105,7 +106,10 @@
#endif
namespace esphome::socket {
class Socket;
#ifdef USE_SOCKET_SELECT_SUPPORT
/// Shared ready() helper for fd-based socket implementations.
bool socket_ready_fd(int fd, bool loop_monitored); // NOLINT(readability-redundant-declaration)
#endif
} // namespace esphome::socket
// Forward declarations for friend access from codegen-generated setup()
@@ -114,6 +118,14 @@ void original_setup(); // NOLINT(readability-redundant-declaration) - used by c
namespace esphome {
/// SFINAE helper: detects whether T overrides Component::loop().
/// When &T::loop is ambiguous (multiple inheritance with separate loop() methods),
/// the ambiguity itself proves an override exists, so the true_type default is correct.
template<typename T, typename = void> struct HasLoopOverride : std::true_type {};
template<typename T>
struct HasLoopOverride<T, std::void_t<decltype(&T::loop)>>
: std::bool_constant<!std::is_same_v<decltype(&T::loop), decltype(&Component::loop)>> {};
// Teardown timeout constant (in milliseconds)
// For reboots, it's more important to shut down quickly than disconnect cleanly
// since we're not entering deep sleep. The only consequence of not shutting down
@@ -520,7 +532,9 @@ class Application {
protected:
friend Component;
friend class socket::Socket;
#ifdef USE_SOCKET_SELECT_SUPPORT
friend bool socket::socket_ready_fd(int fd, bool loop_monitored);
#endif
friend void ::setup();
friend void ::original_setup();
@@ -537,7 +551,13 @@ class Application {
#endif
#endif
void register_component_(Component *comp);
/// Register a component, detecting loop() override at compile time.
/// Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance.
template<typename T> void register_component_(T *comp) {
this->register_component_impl_(comp, HasLoopOverride<T>::value);
}
void register_component_impl_(Component *comp, bool has_loop);
void calculate_looping_components_();
void add_looping_components_by_state_(bool match_loop_done);
+51 -5
View File
@@ -4,6 +4,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
#include <concepts>
#include <functional>
@@ -56,6 +57,16 @@ template<typename T, typename... X> class TemplatableValue {
this->static_str_ = str;
}
#ifdef USE_ESP8266
// On ESP8266, __FlashStringHelper* is a distinct type from const char*.
// ESPHOME_F(s) expands to F(s) which returns __FlashStringHelper* pointing to PROGMEM.
// Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions
// to access the PROGMEM pointer safely.
TemplatableValue(const __FlashStringHelper *str) requires std::same_as<T, std::string> : type_(FLASH_STRING) {
this->static_str_ = reinterpret_cast<const char *>(str);
}
#endif
template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) {
if constexpr (USE_HEAP_STORAGE) {
this->value_ = new T(std::move(value));
@@ -89,7 +100,7 @@ template<typename T, typename... X> class TemplatableValue {
this->f_ = new std::function<T(X...)>(*other.f_);
} else if (this->type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
} else if (this->type_ == STATIC_STRING) {
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
this->static_str_ = other.static_str_;
}
}
@@ -108,7 +119,7 @@ template<typename T, typename... X> class TemplatableValue {
other.f_ = nullptr;
} else if (this->type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
} else if (this->type_ == STATIC_STRING) {
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
this->static_str_ = other.static_str_;
}
other.type_ = NONE;
@@ -141,7 +152,7 @@ template<typename T, typename... X> class TemplatableValue {
} else if (this->type_ == LAMBDA) {
delete this->f_;
}
// STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
// STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
}
bool has_value() const { return this->type_ != NONE; }
@@ -165,6 +176,17 @@ template<typename T, typename... X> class TemplatableValue {
return std::string(this->static_str_);
}
__builtin_unreachable();
#ifdef USE_ESP8266
case FLASH_STRING:
// PROGMEM pointer — must use _P functions to access on ESP8266
if constexpr (std::same_as<T, std::string>) {
size_t len = strlen_P(this->static_str_);
std::string result(len, '\0');
memcpy_P(result.data(), this->static_str_, len);
return result;
}
__builtin_unreachable();
#endif
case NONE:
default:
return T{};
@@ -186,9 +208,12 @@ template<typename T, typename... X> class TemplatableValue {
}
/// Check if this holds a static string (const char* stored without allocation)
/// The pointer is always directly readable (RAM or flash-mapped).
/// Returns false for FLASH_STRING (PROGMEM on ESP8266, requires _P functions).
bool is_static_string() const { return this->type_ == STATIC_STRING; }
/// Get the static string pointer (only valid if is_static_string() returns true)
/// The pointer is always directly readable — FLASH_STRING uses a separate type.
const char *get_static_string() const { return this->static_str_; }
/// Check if the string value is empty without allocating (for std::string specialization).
@@ -200,6 +225,12 @@ template<typename T, typename... X> class TemplatableValue {
return true;
case STATIC_STRING:
return this->static_str_ == nullptr || this->static_str_[0] == '\0';
#ifdef USE_ESP8266
case FLASH_STRING:
// PROGMEM pointer — must use progmem_read_byte on ESP8266
return this->static_str_ == nullptr ||
progmem_read_byte(reinterpret_cast<const uint8_t *>(this->static_str_)) == '\0';
#endif
case VALUE:
return this->value_->empty();
default: // LAMBDA/STATELESS_LAMBDA - must call value()
@@ -209,8 +240,9 @@ template<typename T, typename... X> class TemplatableValue {
/// Get a StringRef to the string value without heap allocation when possible.
/// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
/// For FLASH_STRING (ESP8266 PROGMEM), copies to provided buffer via _P functions.
/// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
/// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used).
/// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used).
/// @param lambda_buf_size Size of the buffer.
/// @return StringRef pointing to the string data.
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
@@ -221,6 +253,19 @@ template<typename T, typename... X> class TemplatableValue {
if (this->static_str_ == nullptr)
return StringRef();
return StringRef(this->static_str_, strlen(this->static_str_));
#ifdef USE_ESP8266
case FLASH_STRING:
if (this->static_str_ == nullptr)
return StringRef();
{
// PROGMEM pointer — copy to buffer via _P functions
size_t len = strlen_P(this->static_str_);
size_t copy_len = std::min(len, lambda_buf_size - 1);
memcpy_P(lambda_buf, this->static_str_, copy_len);
lambda_buf[copy_len] = '\0';
return StringRef(lambda_buf, copy_len);
}
#endif
case VALUE:
return StringRef(this->value_->data(), this->value_->size());
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
@@ -239,6 +284,7 @@ template<typename T, typename... X> class TemplatableValue {
LAMBDA,
STATELESS_LAMBDA,
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms
} type_;
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
// For other types, store value inline as before.
@@ -247,7 +293,7 @@ template<typename T, typename... X> class TemplatableValue {
ValueStorage value_; // T for inline storage, T* for heap storage
std::function<T(X...)> *f_;
T (*stateless_f_)(X...);
const char *static_str_; // For STATIC_STRING type
const char *static_str_; // For STATIC_STRING and FLASH_STRING types
};
};
-12
View File
@@ -496,18 +496,6 @@ void Component::set_setup_priority(float priority) {
}
#endif
bool Component::has_overridden_loop() const {
#if defined(USE_HOST) || defined(CLANG_TIDY)
return true;
#else
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpmf-conversions"
bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop);
#pragma GCC diagnostic pop
return loop_overridden;
#endif
}
PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {}
void PollingComponent::call_setup() {
+5 -2
View File
@@ -76,6 +76,8 @@ inline constexpr uint8_t STATUS_LED_MASK = 0x18;
inline constexpr uint8_t STATUS_LED_OK = 0x00;
inline constexpr uint8_t STATUS_LED_WARNING = 0x08;
inline constexpr uint8_t STATUS_LED_ERROR = 0x10;
// Component loop override flag uses bit 5 (set at registration time)
inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20;
// Remove before 2026.8.0
enum class RetryResult { DONE, RETRY };
@@ -271,7 +273,7 @@ class Component {
*/
void status_momentary_error(const char *name, uint32_t length = 5000);
bool has_overridden_loop() const;
bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; }
/** Set where this component was loaded from for some debug messages.
*
@@ -510,7 +512,8 @@ class Component {
/// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE)
/// Bit 3: STATUS_LED_WARNING
/// Bit 4: STATUS_LED_ERROR
/// Bits 5-7: Unused - reserved for future expansion
/// Bit 5: Has overridden loop() (set at registration time)
/// Bits 6-7: Unused - reserved for future expansion
uint8_t component_state_{0x00};
volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context
};
+43
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections import Counter
import logging
import os
from pathlib import Path
@@ -504,6 +505,41 @@ async def _add_controller_registry_define() -> None:
cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count)
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_looping_components() -> None:
# Emit a constexpr that computes the looping component count at C++ compile time
# and pre-init the FixedVector with the exact capacity. Uses std::is_same_v to
# detect loop() overrides. The constexpr goes in main.cpp's global section where
# all component types are in scope. calculate_looping_components_() then skips
# the counting pass and only does the two population passes.
entries = CORE.data.get("looping_component_entries", [])
if not entries:
return
# Build constexpr sum for the exact count, deduplicating by type
# Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance
type_counts = Counter(entries)
terms = [
f"({count} * HasLoopOverride<{cpp_type}>::value)"
for cpp_type, count in type_counts.items()
]
constexpr_expr = " + \\\n ".join(terms)
cg.add_global(
cg.RawStatement(
f"static constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT = \\\n"
f" {constexpr_expr};"
)
)
# Pre-init FixedVector with exact capacity so calculate_looping_components_()
# can skip the counting pass
cg.add(
cg.RawExpression(
"App.looping_components_.init(ESPHOME_LOOPING_COMPONENT_COUNT)"
)
)
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config: ConfigType) -> None:
cg.add_global(cg.global_ns.namespace("esphome").using)
@@ -527,6 +563,7 @@ async def to_code(config: ConfigType) -> None:
CORE.add_job(_add_platform_defines)
CORE.add_job(_add_controller_registry_define)
CORE.add_job(_add_looping_components)
CORE.add_job(_add_automations, config)
@@ -650,6 +687,12 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"time_64.cpp": {
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
# Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered
# as they are only included when needed by the preprocessor
}
+13 -8
View File
@@ -10,21 +10,26 @@ StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> ControllerRegistry::controll
void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); }
void ControllerRegistry::notify(void *obj, DispatchFunc dispatch) {
for (auto *controller : controllers) {
dispatch(controller, obj);
}
}
// Macro for standard registry notification dispatch - calls on_<entity_name>_update()
// Each wrapper passes a small trampoline lambda that calls the correct virtual method.
// NOLINTBEGIN(bugprone-macro-parentheses)
#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \
void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
for (auto *controller : controllers) { \
controller->on_##entity_name##_update(obj); \
} \
void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \
notify(obj, [](Controller *c, void *o) { c->on_##entity_name##_update(static_cast<entity_type *>(o)); }); \
}
// Macro for entities where controller method has no "_update" suffix (Event, Update)
#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \
void ControllerRegistry::notify_##entity_name(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
for (auto *controller : controllers) { \
controller->on_##entity_name(obj); \
} \
void ControllerRegistry::notify_##entity_name(entity_type *obj) { \
notify(obj, [](Controller *c, void *o) { c->on_##entity_name(static_cast<entity_type *>(o)); }); \
}
// NOLINTEND(bugprone-macro-parentheses)
#ifdef USE_BINARY_SENSOR
CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor)
+15
View File
@@ -247,6 +247,21 @@ class ControllerRegistry {
#endif
protected:
/** Type-erased dispatch function pointer.
*
* Each notify method passes a small trampoline that calls the
* correct virtual method on Controller. The shared notify() loop
* iterates controllers once, calling the trampoline for each.
*/
using DispatchFunc = void (*)(Controller *, void *);
/** Shared dispatch loop - iterates controllers and calls dispatch for each.
*
* Marked noinline to ensure only one copy of the loop exists in flash,
* rather than being duplicated into each notify_*_update wrapper.
*/
static void __attribute__((noinline)) notify(void *obj, DispatchFunc dispatch);
static StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> controllers;
};
+6
View File
@@ -61,6 +61,7 @@
#define USE_IR_RF
#define USE_JSON
#define USE_LIGHT
#define USE_LIGHT_GAMMA_LUT
#define USE_LOCK
#define USE_LOGGER
#define USE_LOGGER_LEVEL_LISTENERS
@@ -178,6 +179,11 @@
#define USE_I2S_LEGACY
#endif
// Platforms with native 64-bit time sources (no rollover tracking needed)
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040)
#define USE_NATIVE_64BIT_TIME
#endif
// ESP32-specific feature flags
#ifdef USE_ESP32
#define USE_MQTT_IDF_ENQUEUE
+1
View File
@@ -42,5 +42,6 @@ void arch_feed_wdt();
uint32_t arch_get_cpu_cycle_count();
uint32_t arch_get_cpu_freq_hz();
uint8_t progmem_read_byte(const uint8_t *addr);
uint16_t progmem_read_uint16(const uint16_t *addr);
} // namespace esphome
+4
View File
@@ -1475,8 +1475,12 @@ bool base64_decode_int32_vector(const std::string &base64, std::vector<int32_t>
///@{
/// Applies gamma correction of \p gamma to \p value.
// Remove before 2026.9.0
ESPDEPRECATED("Use LightState::gamma_correct_lut() instead. Removed in 2026.9.0.", "2026.3.0")
float gamma_correct(float value, float gamma);
/// Reverts gamma correction of \p gamma to \p value.
// Remove before 2026.9.0
ESPDEPRECATED("Use LightState::gamma_uncorrect_lut() instead. Removed in 2026.9.0.", "2026.3.0")
float gamma_uncorrect(float value, float gamma);
/// Convert \p red, \p green and \p blue (all 0-1) values to \p hue (0-360), \p saturation (0-1) and \p value (0-1).
+3 -179
View File
@@ -9,7 +9,6 @@
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <limits>
namespace esphome {
@@ -28,10 +27,6 @@ static constexpr size_t MAX_POOL_SIZE = 5;
// Set to 5 to match the pool size - when we have as many cancelled items as our
// pool can hold, it's time to clean up and recycle them.
static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5;
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
// Half the 32-bit range - used to detect rollovers vs normal time progression
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
#endif
// max delay to start an interval sequence
static constexpr uint32_t MAX_INTERVAL_DELAY = 5000;
@@ -152,9 +147,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
return;
}
// Get fresh 64-bit timestamp BEFORE taking lock
const uint64_t now_64 = millis_64();
// Take lock early to protect scheduler_item_pool_ access
LockGuard guard{this->lock_};
@@ -181,6 +173,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
} else
#endif /* not ESPHOME_THREAD_SINGLE */
{
// Only non-defer items need a timestamp for scheduling
const uint64_t now_64 = millis_64();
// Type-specific setup
if (type == SchedulerItem::INTERVAL) {
item->interval = delay;
@@ -475,19 +470,8 @@ void HOT Scheduler::call(uint32_t now) {
if (now_64 - last_print > 2000) {
last_print = now_64;
std::vector<SchedulerItemPtr> old_items;
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) && \
defined(ESPHOME_THREAD_MULTI_ATOMICS)
const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg);
#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_);
#else
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(),
now_64);
#endif
// Cleanup before debug output
this->cleanup_();
while (!this->items_.empty()) {
@@ -715,166 +699,6 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
return total_cancelled > 0;
}
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
uint64_t Scheduler::millis_64_impl_(uint32_t now) {
// THREAD SAFETY NOTE:
// This function has three implementations, based on the precompiler flags
// - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.)
// - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny BK72xx)
// - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (ESP32, HOST, LibreTiny
// RTL87xx/LN882x, etc.)
//
// Make sure all changes are synchronized if you edit this function.
//
// IMPORTANT: Always pass fresh millis() values to this function. The implementation
// handles out-of-order timestamps between threads, but minimizing time differences
// helps maintain accuracy.
//
#ifdef ESPHOME_THREAD_SINGLE
// This is the single core implementation.
//
// Single-core platforms have no concurrency, so this is a simple implementation
// that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics.
uint16_t major = this->millis_major_;
uint32_t last = this->last_millis_;
// Check for rollover
if (now < last && (last - now) > HALF_MAX_UINT32) {
this->millis_major_++;
major++;
this->last_millis_ = now;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
} else if (now > last) {
// Only update if time moved forward
this->last_millis_ = now;
}
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
// This is the multi core no atomics implementation.
//
// Without atomics, this implementation uses locks more aggressively:
// 1. Always locks when near the rollover boundary (within 10 seconds)
// 2. Always locks when detecting a large backwards jump
// 3. Updates without lock in normal forward progression (accepting minor races)
// This is less efficient but necessary without atomic operations.
uint16_t major = this->millis_major_;
uint32_t last = this->last_millis_;
// Define a safe window around the rollover point (10 seconds)
// This covers any reasonable scheduler delays or thread preemption
static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds
// Check if we're near the rollover boundary (close to std::numeric_limits<uint32_t>::max() or just past 0)
bool near_rollover = (last > (std::numeric_limits<uint32_t>::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW);
if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) {
// Near rollover or detected a rollover - need lock for safety
LockGuard guard{this->lock_};
// Re-read with lock held
last = this->last_millis_;
if (now < last && (last - now) > HALF_MAX_UINT32) {
// True rollover detected (happens every ~49.7 days)
this->millis_major_++;
major++;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
}
// Update last_millis_ while holding lock
this->last_millis_ = now;
} else if (now > last) {
// Normal case: Not near rollover and time moved forward
// Update without lock. While this may cause minor races (microseconds of
// backwards time movement), they're acceptable because:
// 1. The scheduler operates at millisecond resolution, not microsecond
// 2. We've already prevented the critical rollover race condition
// 3. Any backwards movement is orders of magnitude smaller than scheduler delays
this->last_millis_ = now;
}
// If now <= last and we're not near rollover, don't update
// This minimizes backwards time movement
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
// This is the multi core with atomics implementation.
//
// Uses atomic operations with acquire/release semantics to ensure coherent
// reads of millis_major_ and last_millis_ across cores. Features:
// 1. Epoch-coherency retry loop to handle concurrent updates
// 2. Lock only taken for actual rollover detection and update
// 3. Lock-free CAS updates for normal forward time progression
// 4. Memory ordering ensures cores see consistent time values
for (;;) {
uint16_t major = this->millis_major_.load(std::memory_order_acquire);
/*
* Acquire so that if we later decide **not** to take the lock we still
* observe a `millis_major_` value coherent with the loaded `last_millis_`.
* The acquire load ensures any later read of `millis_major_` sees its
* corresponding increment.
*/
uint32_t last = this->last_millis_.load(std::memory_order_acquire);
// If we might be near a rollover (large backwards jump), take the lock for the entire operation
// This ensures rollover detection and last_millis_ update are atomic together
if (now < last && (last - now) > HALF_MAX_UINT32) {
// Potential rollover - need lock for atomic rollover detection + update
LockGuard guard{this->lock_};
// Re-read with lock held; mutex already provides ordering
last = this->last_millis_.load(std::memory_order_relaxed);
if (now < last && (last - now) > HALF_MAX_UINT32) {
// True rollover detected (happens every ~49.7 days)
this->millis_major_.fetch_add(1, std::memory_order_relaxed);
major++;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
}
/*
* Update last_millis_ while holding the lock to prevent races
* Publish the new low-word *after* bumping `millis_major_` (done above)
* so readers never see a mismatched pair.
*/
this->last_millis_.store(now, std::memory_order_release);
} else {
// Normal case: Try lock-free update, but only allow forward movement within same epoch
// This prevents accidentally moving backwards across a rollover boundary
while (now > last && (now - last) < HALF_MAX_UINT32) {
if (this->last_millis_.compare_exchange_weak(last, now,
std::memory_order_release, // success
std::memory_order_relaxed)) { // failure
break;
}
// CAS failure means no data was published; relaxed is fine
// last is automatically updated by compare_exchange_weak if it fails
}
}
uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed);
if (major_end == major)
return now + (static_cast<uint64_t>(major) << 32);
}
// Unreachable - the loop always returns when major_end == major
__builtin_unreachable();
#else
#error \
"No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined."
#endif
}
#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040
bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) {
// High bits are almost always equal (change only on 32-bit rollover ~49 days)
// Optimize for common case: check low bits first when high bits are equal
+4 -43
View File
@@ -12,6 +12,7 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/time_64.h"
namespace esphome {
@@ -284,23 +285,16 @@ class Scheduler {
bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
// Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now.
// On ESP32, Host, Zephyr, and RP2040, ignores now and uses the native 64-bit time source via millis_64().
// On platforms with native 64-bit time, ignores now and uses millis_64() directly.
// On other platforms, extends now to 64-bit using rollover tracking.
uint64_t millis_64_from_(uint32_t now) {
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040)
#ifdef USE_NATIVE_64BIT_TIME
(void) now;
return millis_64();
#else
return this->millis_64_impl_(now);
return Millis64Impl::compute(now);
#endif
}
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
// On platforms without native 64-bit time, millis_64() HAL function delegates to this
// method which tracks 32-bit millis() rollover using millis_major_ and last_millis_.
friend uint64_t millis_64();
uint64_t millis_64_impl_(uint32_t now);
#endif
// Cleanup logically deleted items from the scheduler
// Returns the number of items remaining after cleanup
// IMPORTANT: This method should only be called from the main thread (loop task).
@@ -566,39 +560,6 @@ class Scheduler {
// can stall the entire system, causing timing issues and dropped events for any components that need
// to synchronize between tasks (see https://github.com/esphome/backlog/issues/52)
std::vector<SchedulerItemPtr> scheduler_item_pool_;
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
// On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040), no rollover tracking needed.
// On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_().
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
/*
* Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
*
* MEMORY-ORDERING NOTE
* --------------------
* `last_millis_` and `millis_major_` form a single 64-bit timestamp split in half.
* Writers publish `last_millis_` with memory_order_release and readers use
* memory_order_acquire. This ensures that once a reader sees the new low word,
* it also observes the corresponding increment of `millis_major_`.
*/
std::atomic<uint32_t> last_millis_{0};
#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
// Platforms without atomic support or single-threaded platforms
uint32_t last_millis_{0};
#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
/*
* Upper 16 bits of the 64-bit millis counter. Incremented only while holding
* `lock_`; read concurrently. Atomic (relaxed) avoids a formal data race.
* Ordering relative to `last_millis_` is provided by its release store and the
* corresponding acquire loads.
*/
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
std::atomic<uint16_t> millis_major_{0};
#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
uint16_t millis_major_{0};
#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 */
};
} // namespace esphome
+207
View File
@@ -0,0 +1,207 @@
#include "esphome/core/defines.h"
#ifndef USE_NATIVE_64BIT_TIME
#include "time_64.h"
#include "esphome/core/helpers.h"
#ifdef ESPHOME_DEBUG_SCHEDULER
#include "esphome/core/log.h"
#include <cinttypes>
#endif
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
#include <atomic>
#endif
#include <limits>
namespace esphome {
#ifdef ESPHOME_DEBUG_SCHEDULER
static const char *const TAG = "time_64";
#endif
uint64_t Millis64Impl::compute(uint32_t now) {
// Half the 32-bit range - used to detect rollovers vs normal time progression
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
// State variables for rollover tracking - static to persist across calls
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Mutex for rollover serialization (taken only every ~49.7 days).
// A spinlock would be smaller (~1 byte vs ~80-100 bytes) but is unsafe on
// preemptive single-core RTOS platforms due to priority inversion: a high-priority
// task spinning would prevent the lock holder from running to release it.
static Mutex lock;
/*
* Multi-threaded platforms with atomic support: last_millis needs atomic for lock-free updates.
* Writers publish last_millis with memory_order_release and readers use memory_order_acquire.
* This ensures that once a reader sees the new low word, it also observes the corresponding
* increment of millis_major.
*/
static std::atomic<uint32_t> last_millis{0};
/*
* Upper 16 bits of the 64-bit millis counter. Incremented only while holding lock;
* read concurrently. Atomic (relaxed) avoids a formal data race. Ordering relative
* to last_millis is provided by its release store and the corresponding acquire loads.
*/
static std::atomic<uint16_t> millis_major{0};
#elif !defined(ESPHOME_THREAD_SINGLE) /* ESPHOME_THREAD_MULTI_NO_ATOMICS */
static Mutex lock;
static uint32_t last_millis{0};
static uint16_t millis_major{0};
#else /* ESPHOME_THREAD_SINGLE */
static uint32_t last_millis{0};
static uint16_t millis_major{0};
#endif
// THREAD SAFETY NOTE:
// This function has three implementations, based on the precompiler flags
// - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, etc.)
// - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny BK72xx)
// - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (LibreTiny RTL87xx/LN882x, etc.)
//
// Make sure all changes are synchronized if you edit this function.
//
// IMPORTANT: Always pass fresh millis() values to this function. The implementation
// handles out-of-order timestamps between threads, but minimizing time differences
// helps maintain accuracy.
#ifdef ESPHOME_THREAD_SINGLE
// Single-core platforms have no concurrency, so this is a simple implementation
// that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics.
uint16_t major = millis_major;
uint32_t last = last_millis;
// Check for rollover
if (now < last && (last - now) > HALF_MAX_UINT32) {
millis_major++;
major++;
last_millis = now;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
} else if (now > last) {
// Only update if time moved forward
last_millis = now;
}
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
// Without atomics, this implementation uses locks more aggressively:
// 1. Always locks when near the rollover boundary (within 10 seconds)
// 2. Always locks when detecting a large backwards jump
// 3. Updates without lock in normal forward progression (accepting minor races)
// This is less efficient but necessary without atomic operations.
uint16_t major = millis_major;
uint32_t last = last_millis;
// Define a safe window around the rollover point (10 seconds)
// This covers any reasonable scheduler delays or thread preemption
static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds
// Check if we're near the rollover boundary (close to std::numeric_limits<uint32_t>::max() or just past 0)
bool near_rollover = (last > (std::numeric_limits<uint32_t>::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW);
if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) {
// Near rollover or detected a rollover - need lock for safety
LockGuard guard{lock};
// Re-read with lock held
last = last_millis;
if (now < last && (last - now) > HALF_MAX_UINT32) {
// True rollover detected (happens every ~49.7 days)
millis_major++;
major++;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
}
// Update last_millis while holding lock
last_millis = now;
} else if (now > last) {
// Normal case: Not near rollover and time moved forward
// Update without lock. While this may cause minor races (microseconds of
// backwards time movement), they're acceptable because:
// 1. The scheduler operates at millisecond resolution, not microsecond
// 2. We've already prevented the critical rollover race condition
// 3. Any backwards movement is orders of magnitude smaller than scheduler delays
last_millis = now;
}
// If now <= last and we're not near rollover, don't update
// This minimizes backwards time movement
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
// Uses atomic operations with acquire/release semantics to ensure coherent
// reads of millis_major and last_millis across cores. Features:
// 1. Epoch-coherency retry loop to handle concurrent updates
// 2. Lock only taken for actual rollover detection and update
// 3. Lock-free CAS updates for normal forward time progression
// 4. Memory ordering ensures cores see consistent time values
for (;;) {
uint16_t major = millis_major.load(std::memory_order_acquire);
/*
* Acquire so that if we later decide **not** to take the lock we still
* observe a millis_major value coherent with the loaded last_millis.
* The acquire load ensures any later read of millis_major sees its
* corresponding increment.
*/
uint32_t last = last_millis.load(std::memory_order_acquire);
// If we might be near a rollover (large backwards jump), take the lock
// This ensures rollover detection and last_millis update are atomic together
if (now < last && (last - now) > HALF_MAX_UINT32) {
// Potential rollover - need lock for atomic rollover detection + update
LockGuard guard{lock};
// Re-read with lock held; mutex already provides ordering
last = last_millis.load(std::memory_order_relaxed);
if (now < last && (last - now) > HALF_MAX_UINT32) {
// True rollover detected (happens every ~49.7 days)
millis_major.fetch_add(1, std::memory_order_relaxed);
major++;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
}
/*
* Update last_millis while holding the lock to prevent races.
* Publish the new low-word *after* bumping millis_major (done above)
* so readers never see a mismatched pair.
*/
last_millis.store(now, std::memory_order_release);
} else {
// Normal case: Try lock-free update, but only allow forward movement within same epoch
// This prevents accidentally moving backwards across a rollover boundary
while (now > last && (now - last) < HALF_MAX_UINT32) {
if (last_millis.compare_exchange_weak(last, now,
std::memory_order_release, // success
std::memory_order_relaxed)) { // failure
break;
}
// CAS failure means no data was published; relaxed is fine
// last is automatically updated by compare_exchange_weak if it fails
}
}
uint16_t major_end = millis_major.load(std::memory_order_relaxed);
if (major_end == major)
return now + (static_cast<uint64_t>(major) << 32);
}
// Unreachable - the loop always returns when major_end == major
__builtin_unreachable();
#else
#error \
"No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined."
#endif
}
} // namespace esphome
#endif // !USE_NATIVE_64BIT_TIME
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "esphome/core/defines.h"
#ifndef USE_NATIVE_64BIT_TIME
#include <cstdint>
namespace esphome {
class Scheduler;
/// Extends 32-bit millis() to 64-bit using rollover tracking.
/// Access restricted to platform HAL (millis_64()) and Scheduler.
/// All other code should call millis_64() from hal.h instead.
class Millis64Impl {
friend uint64_t millis_64();
friend class Scheduler;
static uint64_t compute(uint32_t now);
};
} // namespace esphome
#endif // !USE_NATIVE_64BIT_TIME
+26
View File
@@ -247,6 +247,23 @@ class LogStringLiteral(Literal):
return f"LOG_STR({cpp_string_escape(self.string)})"
class FlashStringLiteral(Literal):
"""A string literal wrapped in ESPHOME_F() for PROGMEM storage on ESP8266.
On ESP8266, ESPHOME_F(s) expands to F(s) which stores the string in flash (PROGMEM).
On other platforms, ESPHOME_F(s) expands to plain s (no-op).
"""
__slots__ = ("string",)
def __init__(self, string: str) -> None:
super().__init__()
self.string = string
def __str__(self) -> str:
return f"ESPHOME_F({cpp_string_escape(self.string)})"
class IntLiteral(Literal):
__slots__ = ("i",)
@@ -761,6 +778,15 @@ async def templatable(
if is_template(value):
return await process_lambda(value, args, return_type=output_type)
if to_exp is None:
# Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266.
# On other platforms ESPHOME_F() is a no-op returning const char*.
# Lazy import to avoid circular dependency (cpp_generator <-> cpp_types).
# Identity check (is) avoids brittle string comparison.
if isinstance(value, str) and output_type is not None:
from esphome.cpp_types import std_string
if output_type is std_string:
return FlashStringLiteral(value)
return value
if isinstance(to_exp, dict):
return to_exp[value]
+5
View File
@@ -80,6 +80,11 @@ async def register_component(var, config):
add(var.set_component_source(LogStringLiteral(name)))
add(App.register_component_(var))
# Collect C++ type for compile-time looping component count
comp_entries = CORE.data.setdefault("looping_component_entries", [])
comp_entries.append(str(var.base.type))
return var
+12 -3
View File
@@ -55,10 +55,12 @@ def has_remote_file_changed(url: str, local_file_path: Path) -> bool:
_LOGGER.debug("has_remote_file_changed: File modified")
return True
except requests.exceptions.RequestException as e:
raise cv.Invalid(
f"Could not check if {url} has changed, please check if file exists "
f"({e})"
_LOGGER.warning(
"Could not check if %s has changed due to network error (%s), using cached file",
url,
e,
)
return False
_LOGGER.debug("has_remote_file_changed: File doesn't exists at %s", local_file_path)
return True
@@ -98,6 +100,13 @@ def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> bytes:
)
req.raise_for_status()
except requests.exceptions.RequestException as e:
if path.exists():
_LOGGER.warning(
"Could not download from %s due to network error (%s), using cached file",
url,
e,
)
return path.read_bytes()
raise cv.Invalid(f"Could not download from {url}: {e}")
path.parent.mkdir(parents=True, exist_ok=True)
+78
View File
@@ -248,6 +248,12 @@ class TestLiterals:
(cg.FloatLiteral(4.2), "4.2f"),
(cg.FloatLiteral(1.23456789), "1.23456789f"),
(cg.FloatLiteral(math.nan), "NAN"),
(cg.FlashStringLiteral("hello"), 'ESPHOME_F("hello")'),
(cg.FlashStringLiteral(""), 'ESPHOME_F("")'),
(
cg.FlashStringLiteral('quote"here'),
'ESPHOME_F("quote\\042here")',
),
),
)
def test_str__simple(self, target: cg.Literal, expected: str):
@@ -624,3 +630,75 @@ class TestProcessLambda:
# Test invalid tuple format (single element)
with pytest.raises(AssertionError):
await cg.process_lambda(lambda_obj, [(int,)])
@pytest.mark.asyncio
async def test_templatable__string_with_std_string_returns_flash_literal() -> None:
"""Static string with std::string output_type returns FlashStringLiteral."""
result = await cg.templatable("hello", [], ct.std_string)
assert isinstance(result, cg.FlashStringLiteral)
assert str(result) == 'ESPHOME_F("hello")'
@pytest.mark.asyncio
async def test_templatable__empty_string_with_std_string() -> None:
"""Empty static string with std::string output_type returns FlashStringLiteral."""
result = await cg.templatable("", [], ct.std_string)
assert isinstance(result, cg.FlashStringLiteral)
assert str(result) == 'ESPHOME_F("")'
@pytest.mark.asyncio
async def test_templatable__string_with_none_output_type() -> None:
"""Static string with output_type=None returns raw string (no wrapping)."""
result = await cg.templatable("hello", [], None)
assert isinstance(result, str)
assert result == "hello"
@pytest.mark.asyncio
async def test_templatable__int_with_std_string() -> None:
"""Non-string value with std::string output_type returns raw value."""
result = await cg.templatable(42, [], ct.std_string)
assert result == 42
@pytest.mark.asyncio
async def test_templatable__string_with_non_string_output_type() -> None:
"""Static string with non-std::string output_type returns raw string."""
result = await cg.templatable("hello", [], ct.bool_)
assert isinstance(result, str)
assert result == "hello"
@pytest.mark.asyncio
async def test_templatable__with_to_exp_callable() -> None:
"""When to_exp is provided, it is applied to non-template values."""
result = await cg.templatable(42, [], None, to_exp=lambda x: x * 2)
assert result == 84
@pytest.mark.asyncio
async def test_templatable__with_to_exp_dict() -> None:
"""When to_exp is a dict, value is looked up."""
mapping: dict[str, int] = {"on": 1, "off": 0}
result = await cg.templatable("on", [], None, to_exp=mapping)
assert result == 1
@pytest.mark.asyncio
async def test_templatable__lambda_with_std_string() -> None:
"""Lambda value returns LambdaExpression, not FlashStringLiteral."""
from esphome.core import Lambda
lambda_obj = Lambda('return "hello";')
result = await cg.templatable(lambda_obj, [], ct.std_string)
assert isinstance(result, cg.LambdaExpression)
+10 -2
View File
@@ -14,7 +14,11 @@ async def test_gpio_pin_expression__conf_is_none(monkeypatch):
@pytest.mark.asyncio
async def test_register_component(monkeypatch):
var = Mock(base="foo.bar")
base_mock = Mock()
base_mock.__str__ = lambda self: "foo.bar"
base_mock.type = Mock()
base_mock.type.__str__ = lambda self: "foo::Bar"
var = Mock(base=base_mock)
app_mock = Mock(register_component_=Mock(return_value=var))
monkeypatch.setattr(ch, "App", app_mock)
@@ -46,7 +50,11 @@ async def test_register_component__no_component_id(monkeypatch):
@pytest.mark.asyncio
async def test_register_component__with_setup_priority(monkeypatch):
var = Mock(base="foo.bar")
base_mock = Mock()
base_mock.__str__ = lambda self: "foo.bar"
base_mock.type = Mock()
base_mock.type.__str__ = lambda self: "foo::Bar"
var = Mock(base=base_mock)
app_mock = Mock(register_component_=Mock(return_value=var))
monkeypatch.setattr(ch, "App", app_mock)
+41 -3
View File
@@ -144,16 +144,16 @@ def test_has_remote_file_changed_no_local_file(setup_core: Path) -> None:
def test_has_remote_file_changed_network_error(
mock_head: MagicMock, setup_core: Path
) -> None:
"""Test has_remote_file_changed handles network errors gracefully."""
"""Test has_remote_file_changed returns False on network error when file is cached."""
test_file = setup_core / "cached.txt"
test_file.write_text("cached content")
mock_head.side_effect = requests.exceptions.RequestException("Network error")
url = "https://example.com/file.txt"
result = external_files.has_remote_file_changed(url, test_file)
with pytest.raises(Invalid, match="Could not check if.*Network error"):
external_files.has_remote_file_changed(url, test_file)
assert result is False
@patch("esphome.external_files.requests.head")
@@ -198,3 +198,41 @@ def test_is_file_recent_handles_float_seconds(setup_core: Path) -> None:
result = external_files.is_file_recent(test_file, refresh)
assert result is True
@patch("esphome.external_files.requests.get")
@patch("esphome.external_files.has_remote_file_changed")
def test_download_content_with_network_error_uses_cache(
mock_has_changed: MagicMock, mock_get: MagicMock, setup_core: Path
) -> None:
"""Test download_content uses cached file when network fails."""
test_file = setup_core / "cached.txt"
cached_content = b"cached content"
test_file.write_bytes(cached_content)
# Simulate file has changed, so it tries to download
mock_has_changed.return_value = True
mock_get.side_effect = requests.exceptions.RequestException("Network error")
url = "https://example.com/file.txt"
result = external_files.download_content(url, test_file)
assert result == cached_content
@patch("esphome.external_files.requests.get")
@patch("esphome.external_files.has_remote_file_changed")
def test_download_content_with_network_error_no_cache_fails(
mock_has_changed: MagicMock, mock_get: MagicMock, setup_core: Path
) -> None:
"""Test download_content raises error when network fails and no cache exists."""
test_file = setup_core / "nonexistent.txt"
# Simulate file has changed (doesn't exist), so it tries to download
mock_has_changed.return_value = True
mock_get.side_effect = requests.exceptions.RequestException("Network error")
url = "https://example.com/file.txt"
with pytest.raises(Invalid, match="Could not download from.*Network error"):
external_files.download_content(url, test_file)