mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 17:18:40 +00:00
Merge branch 'dev' into esp32-millis-euclidean-decomposition
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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, [])
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, ®_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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user