Merge remote-tracking branch 'upstream/dev' into pack-entity-strings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
J. Nick Koston
2026-03-02 07:05:08 -10:00
co-authored by Claude Opus 4.6
135 changed files with 4643 additions and 1729 deletions
+53 -4
View File
@@ -57,8 +57,23 @@ def maybe_conf(conf, *validators):
return validate
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
return ACTION_REGISTRY.register(name, action_type, schema)
def register_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool = False,
):
"""Register an action type.
Actions default to ``synchronous=False`` (safe default), meaning string
arguments use owning std::string to prevent dangling references.
Set ``synchronous=True`` only for actions that complete synchronously
and never store trigger arguments for later execution. This allows
the code generator to use non-owning StringRef for zero-copy access.
"""
return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous)
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
@@ -335,7 +350,9 @@ async def component_is_idle_condition_to_code(
@register_action(
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
"delay",
DelayAction,
cv.templatable(cv.positive_time_period_milliseconds),
)
async def delay_action_to_code(
config: ConfigType,
@@ -366,6 +383,7 @@ async def delay_action_to_code(
cv.has_at_least_one_key(CONF_THEN, CONF_ELSE),
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
),
synchronous=True,
)
async def if_action_to_code(
config: ConfigType,
@@ -394,6 +412,7 @@ async def if_action_to_code(
cv.Required(CONF_THEN): validate_action_list,
}
),
synchronous=True,
)
async def while_action_to_code(
config: ConfigType,
@@ -417,6 +436,7 @@ async def while_action_to_code(
cv.Required(CONF_THEN): validate_action_list,
}
),
synchronous=True,
)
async def repeat_action_to_code(
config: ConfigType,
@@ -461,7 +481,12 @@ async def wait_until_action_to_code(
return var
@register_action("lambda", LambdaAction, cv.lambda_)
# Lambda executes user C++ inline and returns — synchronous by execution model.
# User code could theoretically store the StringRef for deferred use, but StringRef
# is a view type and storing views beyond their scope is always unsafe regardless
# of this optimization. Marking non-synchronous would disable StringRef for nearly
# all user services since most use lambda.
@register_action("lambda", LambdaAction, cv.lambda_, synchronous=True)
async def lambda_action_to_code(
config: ConfigType,
action_id: ID,
@@ -480,6 +505,7 @@ async def lambda_action_to_code(
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
synchronous=True,
)
async def component_update_action_to_code(
config: ConfigType,
@@ -499,6 +525,7 @@ async def component_update_action_to_code(
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
synchronous=True,
)
async def component_suspend_action_to_code(
config: ConfigType,
@@ -521,6 +548,7 @@ async def component_suspend_action_to_code(
),
}
),
synchronous=True,
)
async def component_resume_action_to_code(
config: ConfigType,
@@ -578,6 +606,27 @@ async def build_condition_list(
return conditions
def has_non_synchronous_actions(actions: ConfigType) -> bool:
"""Check if a validated action list contains any non-synchronous actions.
Non-synchronous actions (delay, wait_until, script.wait, etc.) store
trigger args for later execution, making non-owning types like StringRef
unsafe. Actions that haven't been audited default to non-synchronous.
"""
if isinstance(actions, list):
return any(has_non_synchronous_actions(item) for item in actions)
if isinstance(actions, dict):
for key in actions:
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
return True
return any(
has_non_synchronous_actions(v)
for v in actions.values()
if isinstance(v, (list, dict))
)
return False
async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj:
+1
View File
@@ -11,6 +11,7 @@
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
Expression,
FlashStringLiteral,
LineComment,
LogStringLiteral,
MockObj,
@@ -12,7 +12,14 @@ AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
if (code != nullptr) {
this->code_ = std::string(code);
return this->set_code(code, strlen(code));
}
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code, size_t len) {
if (code != nullptr) {
this->code_ = std::string(code, len);
}
return *this;
}
@@ -15,7 +15,8 @@ class AlarmControlPanelCall {
AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const char *code);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
AlarmControlPanelCall &set_code(const char *code, size_t len);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str(), code.size()); }
AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night();
+38 -11
View File
@@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
"bool": cg.bool_,
"int": cg.int32,
"float": cg.float_,
"string": cg.std_string,
"string": cg.StringRef,
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
@@ -380,9 +380,18 @@ async def to_code(config: ConfigType) -> None:
if is_optional:
func_args.append((cg.bool_, "return_response"))
# Check if action chain has non-synchronous actions that would make
# non-owning StringRef dangle (rx_buf_ reused after delay)
has_non_synchronous = automation.has_non_synchronous_actions(
conf.get(CONF_THEN, [])
)
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
# Fall back to std::string for string args if non-synchronous actions exist
if has_non_synchronous and native is cg.StringRef:
native = cg.std_string
service_template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)
@@ -509,11 +518,13 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
"homeassistant.action",
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"homeassistant.service",
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
synchronous=True,
)
async def homeassistant_service_to_code(
config: ConfigType,
@@ -524,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")
@@ -604,29 +622,37 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
"homeassistant.event",
HomeAssistantServiceCallAction,
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
synchronous=True,
)
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
@@ -644,16 +670,17 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
"homeassistant.tag_scanned",
HomeAssistantServiceCallAction,
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
synchronous=True,
)
async def homeassistant_tag_scanned_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)
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
+28
View File
@@ -834,6 +834,33 @@ message GetTimeRequest {
option (source) = SOURCE_SERVER;
}
enum DSTRuleType {
DST_RULE_TYPE_NONE = 0;
DST_RULE_TYPE_MONTH_WEEK_DAY = 1;
DST_RULE_TYPE_JULIAN_NO_LEAP = 2;
DST_RULE_TYPE_DAY_OF_YEAR = 3;
}
message DSTRule {
option (source) = SOURCE_CLIENT;
sint32 time_seconds = 1;
uint32 day = 2;
DSTRuleType type = 3;
uint32 month = 4;
uint32 week = 5;
uint32 day_of_week = 6;
}
message ParsedTimezone {
option (source) = SOURCE_CLIENT;
sint32 std_offset_seconds = 1;
sint32 dst_offset_seconds = 2;
DSTRule dst_start = 3;
DSTRule dst_end = 4;
}
message GetTimeResponse {
option (id) = 37;
option (source) = SOURCE_CLIENT;
@@ -841,6 +868,7 @@ message GetTimeResponse {
fixed32 epoch_seconds = 1;
string timezone = 2;
ParsedTimezone parsed_timezone = 3;
}
// ==================== USER-DEFINES SERVICES ====================
+50 -22
View File
@@ -889,7 +889,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
}
void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
call.set_value(msg.state);
call.set_value(msg.state.c_str(), msg.state.size());
call.perform();
}
#endif
@@ -1123,7 +1123,30 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
#ifdef USE_TIME_TIMEZONE
if (!value.timezone.empty()) {
homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size());
// Check if the sender provided pre-parsed timezone data.
// If std_offset is non-zero or DST rules are present, the parsed data was populated.
// For UTC (all zeros), string parsing produces the same result, so the fallback is equivalent.
const auto &pt = value.parsed_timezone;
if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) {
time::ParsedTimezone tz{};
tz.std_offset_seconds = pt.std_offset_seconds;
tz.dst_offset_seconds = pt.dst_offset_seconds;
tz.dst_start.time_seconds = pt.dst_start.time_seconds;
tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
tz.dst_end.time_seconds = pt.dst_end.time_seconds;
tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
time::set_global_tz(tz);
} else {
homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size());
}
}
#endif
}
@@ -1337,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
@@ -1711,37 +1734,42 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
return;
}
// Null-terminate state in-place for safe c_str() usage (e.g., parse_number in callbacks).
// Safe: decode is complete, byte after string data was already consumed during parse,
// and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte in rx_buf_.
// const_cast is safe: msg references mutable rx_buf_ data; the const& handler
// signature is a generated protobuf pattern, not a true immutability contract.
if (!msg.state.empty()) {
const_cast<char *>(msg.state.c_str())[msg.state.size()] = '\0';
}
for (auto &it : this->parent_->get_state_subs()) {
// Compare entity_id: check length matches and content matches
size_t entity_id_len = strlen(it.entity_id);
if (entity_id_len != msg.entity_id.size() ||
memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) {
if (msg.entity_id != it.entity_id) {
continue;
}
// Compare attribute: either both have matching attribute, or both have none
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
if (sub_attr_len != msg.attribute.size() ||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) {
// If subscriber has attribute filter (non-null), message attribute must match it;
// if subscriber has no filter (nullptr), message must have no attribute.
if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) {
continue;
}
// Create null-terminated state for callback (parse_number needs null-termination)
// HA state max length is 255 characters, but attributes can be much longer
// Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
}
state_buf[state_len] = '\0';
it.callback(StringRef(state_buf, state_len));
it.callback(msg.state);
}
}
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
// Null-terminate string args in-place for safe c_str() usage in YAML service triggers.
// Safe: full ExecuteServiceRequest decode is complete, all bytes in rx_buf_ consumed,
// and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte for the last field.
// const_cast is safe: msg references mutable rx_buf_ data; the const& handler
// signature is a generated protobuf pattern, not a true immutability contract.
for (auto &arg : const_cast<ExecuteServiceRequest &>(msg).args) {
if (!arg.string_.empty()) {
const_cast<char *>(arg.string_.c_str())[arg.string_.size()] = '\0';
}
}
bool found = false;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Register the call and get a unique server-generated action_call_id
@@ -29,6 +29,10 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
#endif
// Extra byte reserved in rx_buf_ beyond the message size so protobuf
// StringRef fields can be null-terminated in-place after decode.
static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
// Maximum number of messages to batch in a single write operation
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
@@ -194,16 +194,21 @@ APIError APINoiseFrameHelper::try_read_frame_() {
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
// Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
bool is_data = (state_ == State::DATA);
uint16_t limit = is_data ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
if (msg_size > limit) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
return is_data ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// Reserve space for body
if (this->rx_buf_.size() != msg_size) {
this->rx_buf_.resize(msg_size);
// Reserve space for body (+ null terminator in DATA state so protobuf
// StringRef fields can be safely null-terminated in-place after decode.
// During handshake, rx_buf_.size() is used in prologue construction, so
// the buffer must be exactly msg_size to avoid prologue mismatch.)
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
if (this->rx_buf_.size() != alloc_size) {
this->rx_buf_.resize(alloc_size);
}
if (rx_buf_len_ < msg_size) {
@@ -407,7 +412,18 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
// read_packet() must only be called in DATA state; the extra
// RX_BUF_NULL_TERMINATOR byte is only allocated in DATA state
// (see try_read_frame_), so calling this during handshake would
// underflow the size calculation below.
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
#endif
// rx_buf_ has RX_BUF_NULL_TERMINATOR extra byte for null termination
// (only added in DATA state — see try_read_frame_), so subtract it
// to get the actual encrypted data size for decryption.
size_t encrypted_size = this->rx_buf_.size() - RX_BUF_NULL_TERMINATOR;
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), encrypted_size, encrypted_size);
int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
APIError decrypt_err =
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
@@ -574,7 +590,9 @@ APIError APINoiseFrameHelper::init_handshake_() {
}
APIError APINoiseFrameHelper::check_handshake_finished_() {
#ifdef ESPHOME_DEBUG_API
assert(state_ == State::HANDSHAKE);
#endif
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
@@ -163,9 +163,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
}
// header reading done
// Reserve space for body
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
this->rx_buf_.resize(this->rx_header_parsed_len_);
// Reserve space for body (+ null terminator so protobuf StringRef fields
// can be safely null-terminated in-place after decode)
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) {
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
}
if (rx_buf_len_ < rx_header_parsed_len_) {
+54
View File
@@ -954,12 +954,66 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
return true;
}
#endif
bool DSTRule::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1:
this->time_seconds = value.as_sint32();
break;
case 2:
this->day = value.as_uint32();
break;
case 3:
this->type = static_cast<enums::DSTRuleType>(value.as_uint32());
break;
case 4:
this->month = value.as_uint32();
break;
case 5:
this->week = value.as_uint32();
break;
case 6:
this->day_of_week = value.as_uint32();
break;
default:
return false;
}
return true;
}
bool ParsedTimezone::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1:
this->std_offset_seconds = value.as_sint32();
break;
case 2:
this->dst_offset_seconds = value.as_sint32();
break;
default:
return false;
}
return true;
}
bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 3:
value.decode_to_message(this->dst_start);
break;
case 4:
value.decode_to_message(this->dst_end);
break;
default:
return false;
}
return true;
}
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->timezone = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
break;
}
case 3:
value.decode_to_message(this->parsed_timezone);
break;
default:
return false;
}
+40 -4
View File
@@ -63,6 +63,12 @@ enum LogLevel : uint32_t {
LOG_LEVEL_VERBOSE = 6,
LOG_LEVEL_VERY_VERBOSE = 7,
};
enum DSTRuleType : uint32_t {
DST_RULE_TYPE_NONE = 0,
DST_RULE_TYPE_MONTH_WEEK_DAY = 1,
DST_RULE_TYPE_JULIAN_NO_LEAP = 2,
DST_RULE_TYPE_DAY_OF_YEAR = 3,
};
#ifdef USE_API_USER_DEFINED_ACTIONS
enum ServiceArgType : uint32_t {
SERVICE_ARG_TYPE_BOOL = 0,
@@ -316,7 +322,6 @@ enum ZWaveProxyRequestType : uint32_t {
class InfoResponseProtoMessage : public ProtoMessage {
public:
~InfoResponseProtoMessage() override = default;
StringRef object_id{};
uint32_t key{0};
StringRef name{};
@@ -330,28 +335,29 @@ class InfoResponseProtoMessage : public ProtoMessage {
#endif
protected:
~InfoResponseProtoMessage() = default;
};
class StateResponseProtoMessage : public ProtoMessage {
public:
~StateResponseProtoMessage() override = default;
uint32_t key{0};
#ifdef USE_DEVICES
uint32_t device_id{0};
#endif
protected:
~StateResponseProtoMessage() = default;
};
class CommandProtoMessage : public ProtoDecodableMessage {
public:
~CommandProtoMessage() override = default;
uint32_t key{0};
#ifdef USE_DEVICES
uint32_t device_id{0};
#endif
protected:
~CommandProtoMessage() = default;
};
class HelloRequest final : public ProtoDecodableMessage {
public:
@@ -1116,15 +1122,45 @@ class GetTimeRequest final : public ProtoMessage {
protected:
};
class DSTRule final : public ProtoDecodableMessage {
public:
int32_t time_seconds{0};
uint32_t day{0};
enums::DSTRuleType type{};
uint32_t month{0};
uint32_t week{0};
uint32_t day_of_week{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ParsedTimezone final : public ProtoDecodableMessage {
public:
int32_t std_offset_seconds{0};
int32_t dst_offset_seconds{0};
DSTRule dst_start{};
DSTRule dst_end{};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class GetTimeResponse final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 37;
static constexpr uint8_t ESTIMATED_SIZE = 14;
static constexpr uint8_t ESTIMATED_SIZE = 31;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "get_time_response"; }
#endif
uint32_t epoch_seconds{0};
StringRef timezone{};
ParsedTimezone parsed_timezone{};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
+39
View File
@@ -208,6 +208,20 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::DSTRuleType>(enums::DSTRuleType value) {
switch (value) {
case enums::DST_RULE_TYPE_NONE:
return "DST_RULE_TYPE_NONE";
case enums::DST_RULE_TYPE_MONTH_WEEK_DAY:
return "DST_RULE_TYPE_MONTH_WEEK_DAY";
case enums::DST_RULE_TYPE_JULIAN_NO_LEAP:
return "DST_RULE_TYPE_JULIAN_NO_LEAP";
case enums::DST_RULE_TYPE_DAY_OF_YEAR:
return "DST_RULE_TYPE_DAY_OF_YEAR";
default:
return "UNKNOWN";
}
}
#ifdef USE_API_USER_DEFINED_ACTIONS
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
switch (value) {
@@ -1254,10 +1268,35 @@ const char *GetTimeRequest::dump_to(DumpBuffer &out) const {
out.append("GetTimeRequest {}");
return out.c_str();
}
const char *DSTRule::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "DSTRule");
dump_field(out, "time_seconds", this->time_seconds);
dump_field(out, "day", this->day);
dump_field(out, "type", static_cast<enums::DSTRuleType>(this->type));
dump_field(out, "month", this->month);
dump_field(out, "week", this->week);
dump_field(out, "day_of_week", this->day_of_week);
return out.c_str();
}
const char *ParsedTimezone::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "ParsedTimezone");
dump_field(out, "std_offset_seconds", this->std_offset_seconds);
dump_field(out, "dst_offset_seconds", this->dst_offset_seconds);
out.append(" dst_start: ");
this->dst_start.dump_to(out);
out.append("\n");
out.append(" dst_end: ");
this->dst_end.dump_to(out);
out.append("\n");
return out.c_str();
}
const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "GetTimeResponse");
dump_field(out, "epoch_seconds", this->epoch_seconds);
dump_field(out, "timezone", this->timezone);
out.append(" parsed_timezone: ");
this->parsed_timezone.dump_to(out);
out.append("\n");
return out.c_str();
}
#ifdef USE_API_USER_DEFINED_ACTIONS
+1 -1
View File
@@ -257,7 +257,7 @@ class APIServer : public Component,
}
void socket_failed_(const LogString *msg);
// Pointers and pointer-like types first (4 bytes each)
socket::Socket *socket_{nullptr};
socket::ListenSocket *socket_{nullptr};
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> client_connected_trigger_;
#endif
+43 -3
View File
@@ -130,6 +130,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->add_kv_(this->variables_, key, std::forward<V>(value));
}
#ifdef USE_ESP8266
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
@@ -221,7 +235,32 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Ts... x) {
dest.init(source.size());
// Count non-static strings to allocate exact storage needed
#ifdef USE_ESP8266
// On ESP8266, all static strings from codegen are FLASH_STRING (PROGMEM),
// so is_static_string() is always false — the zero-copy STATIC_STRING fast
// path from the non-ESP8266 branch cannot trigger. We copy all keys and
// values unconditionally: keys via _P functions (may be in PROGMEM), values
// via value() which handles FLASH_STRING internally.
value_storage.init(source.size() * 2);
for (auto &it : source) {
auto &kv = dest.emplace_back();
// Key: copy from possible PROGMEM
{
size_t key_len = strlen_P(it.key);
value_storage.push_back(std::string(key_len, '\0'));
memcpy_P(value_storage.back().data(), it.key, key_len);
kv.key = StringRef(value_storage.back());
}
// Value: value() handles FLASH_STRING via _P functions internally
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
#else
// On non-ESP8266, strings are directly readable from flash-mapped memory.
// Count non-static strings to allocate exact storage needed.
size_t lambda_count = 0;
for (const auto &it : source) {
if (!it.value.is_static_string()) {
@@ -235,14 +274,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.key = StringRef(it.key);
if (it.value.is_static_string()) {
// Static string from YAML - zero allocation
// Static string — pointer directly readable, zero allocation
kv.value = StringRef(it.value.get_static_string());
} else {
// Lambda evaluation - store result, reference it
// Lambda evaluate and store result
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
}
#endif
}
APIServer *parent_;
+6 -1
View File
@@ -452,7 +452,6 @@ class DumpBuffer {
class ProtoMessage {
public:
virtual ~ProtoMessage() = default;
// Default implementation for messages with no fields
virtual void encode(ProtoWriteBuffer &buffer) const {}
// Default implementation for messages with no fields
@@ -463,6 +462,11 @@ class ProtoMessage {
virtual const char *dump_to(DumpBuffer &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif
protected:
// Non-virtual: messages are never deleted polymorphically.
// Protected prevents accidental `delete base_ptr` (compile error).
~ProtoMessage() = default;
};
// Base class for messages that support decoding
@@ -482,6 +486,7 @@ class ProtoDecodableMessage : public ProtoMessage {
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
protected:
~ProtoDecodableMessage() = default;
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
+5
View File
@@ -1,5 +1,6 @@
#include "user_services.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
@@ -11,6 +12,8 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
}
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode)
template<> StringRef get_execute_arg_value<StringRef>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
@@ -61,6 +64,8 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Zero-copy StringRef version for YAML-generated services
template<> enums::ServiceArgType to_service_arg_type<StringRef>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Legacy std::vector versions for external components using custom_api_device.h
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
+3 -1
View File
@@ -125,7 +125,9 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id(
)
@automation.register_action("button.press", PressAction, BUTTON_PRESS_SCHEMA)
@automation.register_action(
"button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True
)
async def button_press_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@@ -22,7 +22,7 @@ class DNSServer {
}
static constexpr size_t DNS_BUFFER_SIZE = 192;
socket::Socket *socket_{nullptr};
socket::ListenSocket *socket_{nullptr};
network::IPAddress server_ip_;
uint8_t buffer_[DNS_BUFFER_SIZE];
};
+13 -5
View File
@@ -173,14 +173,17 @@ ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
return *this;
}
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
ClimateCall &ClimateCall::set_mode(const std::string &mode) { return this->set_mode(mode.c_str(), mode.size()); }
ClimateCall &ClimateCall::set_mode(const char *mode, size_t len) {
StringRef mode_ref(mode, len);
for (const auto &mode_entry : CLIMATE_MODES_BY_STR) {
if (str_equals_case_insensitive(mode, mode_entry.str)) {
if (str_equals_case_insensitive(mode_ref, mode_entry.str)) {
this->set_mode(static_cast<ClimateMode>(mode_entry.value));
return *this;
}
}
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode);
return *this;
}
@@ -266,13 +269,18 @@ ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
}
ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) {
return this->set_swing_mode(swing_mode.c_str(), swing_mode.size());
}
ClimateCall &ClimateCall::set_swing_mode(const char *swing_mode, size_t len) {
StringRef mode_ref(swing_mode, len);
for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) {
if (str_equals_case_insensitive(swing_mode, mode_entry.str)) {
if (str_equals_case_insensitive(mode_ref, mode_entry.str)) {
this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value));
return *this;
}
}
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %.*s", this->parent_->get_name().c_str(), (int) len, swing_mode);
return *this;
}
+4
View File
@@ -41,6 +41,8 @@ class ClimateCall {
ClimateCall &set_mode(optional<ClimateMode> mode);
/// Set the mode of the climate device based on a string.
ClimateCall &set_mode(const std::string &mode);
/// Set the mode of the climate device based on a C string.
ClimateCall &set_mode(const char *mode, size_t len);
/// Set the target temperature of the climate device.
ClimateCall &set_target_temperature(float target_temperature);
/// Set the target temperature of the climate device.
@@ -87,6 +89,8 @@ class ClimateCall {
ClimateCall &set_swing_mode(optional<ClimateSwingMode> swing_mode);
/// Set the swing mode of the climate device based on a string.
ClimateCall &set_swing_mode(const std::string &swing_mode);
/// Set the swing mode of the climate device based on a C string.
ClimateCall &set_swing_mode(const char *swing_mode, size_t len);
/// Set the preset of the climate device.
ClimateCall &set_preset(ClimatePreset preset);
/// Set the preset of the climate device.
+15 -5
View File
@@ -250,25 +250,33 @@ COVER_ACTION_SCHEMA = maybe_simple_id(
)
@automation.register_action("cover.open", OpenAction, COVER_ACTION_SCHEMA)
@automation.register_action(
"cover.open", OpenAction, COVER_ACTION_SCHEMA, synchronous=True
)
async def cover_open_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("cover.close", CloseAction, COVER_ACTION_SCHEMA)
@automation.register_action(
"cover.close", CloseAction, COVER_ACTION_SCHEMA, synchronous=True
)
async def cover_close_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("cover.stop", StopAction, COVER_ACTION_SCHEMA)
@automation.register_action(
"cover.stop", StopAction, COVER_ACTION_SCHEMA, synchronous=True
)
async def cover_stop_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA)
@automation.register_action(
"cover.toggle", ToggleAction, COVER_ACTION_SCHEMA, synchronous=True
)
async def cover_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@@ -285,7 +293,9 @@ COVER_CONTROL_ACTION_SCHEMA = cv.Schema(
)
@automation.register_action("cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA)
@automation.register_action(
"cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA, synchronous=True
)
async def cover_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
+1
View File
@@ -1438,6 +1438,7 @@ async def to_code(config):
cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.add_build_flag("-Wl,-z,noexecstack")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
variant = config[CONF_VARIANT]
+1
View File
@@ -49,6 +49,7 @@ void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
const char *progmem_read_ptr(const char *const *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;
+12 -21
View File
@@ -19,16 +19,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -41,14 +32,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.set_data(data, len);
obj.data.set(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -56,11 +47,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.len != len) {
if (obj.data.size() != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.get(), len);
memcpy(data, obj.data.data(), len);
return true;
}
}
@@ -133,10 +124,10 @@ class ESP32Preferences : public ESPPreferences {
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
@@ -144,7 +135,7 @@ class ESP32Preferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
cached++;
}
}
@@ -176,7 +167,7 @@ class ESP32Preferences : public ESPPreferences {
return true;
}
// Check size first before allocating memory
if (actual_len != to_save.len) {
if (actual_len != to_save.data.size()) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
@@ -186,7 +177,7 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
}
bool reset() override {
+5 -2
View File
@@ -3,7 +3,7 @@
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "esphome/core/time_64.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <Arduino.h>
@@ -17,7 +17,7 @@ namespace esphome {
void HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); }
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
void HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
@@ -37,6 +37,9 @@ uint8_t progmem_read_byte(const uint8_t *addr) {
const char *progmem_read_ptr(const char *const *addr) {
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
}
uint16_t progmem_read_uint16(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
+1 -1
View File
@@ -84,7 +84,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
std::unique_ptr<uint8_t[]> auth_buf_;
#endif // USE_OTA_PASSWORD
socket::Socket *server_{nullptr};
socket::ListenSocket *server_{nullptr};
std::unique_ptr<socket::Socket> client_;
std::unique_ptr<ota::OTABackend> backend_;
+7 -2
View File
@@ -310,13 +310,17 @@ FAN_ACTION_SCHEMA = maybe_simple_id(
)
@automation.register_action("fan.toggle", ToggleAction, FAN_ACTION_SCHEMA)
@automation.register_action(
"fan.toggle", ToggleAction, FAN_ACTION_SCHEMA, synchronous=True
)
async def fan_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA)
@automation.register_action(
"fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA, synchronous=True
)
async def fan_turn_off_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@@ -335,6 +339,7 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
),
}
),
synchronous=True,
)
async def fan_turn_on_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+1
View File
@@ -102,6 +102,7 @@ async def to_code(config):
cv.Required(CONF_VALUE): cv.templatable(cv.string_strict),
}
),
synchronous=True,
)
async def globals_set_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
+1
View File
@@ -41,6 +41,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
cg.add_build_flag("-DUSE_HOST")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
cg.add_build_flag("-std=gnu++20")
cg.add_define("ESPHOME_BOARD", "host")
+1
View File
@@ -60,6 +60,7 @@ void HOT arch_feed_wdt() {
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
const char *progmem_read_ptr(const char *const *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);
@@ -43,3 +43,4 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await improv_base.setup_improv_core(var, config, "improv_serial")
cg.add_define("USE_IMPROV_SERIAL")
+8 -13
View File
@@ -173,8 +173,6 @@ static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0x
// MAC address the module uses when Bluetooth is disabled
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; }
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
}
@@ -361,17 +359,14 @@ void LD2410Component::handle_periodic_data_() {
Detect distance: 16~17th bytes
*/
#ifdef USE_SENSOR
SAFE_PUBLISH_SENSOR(
this->moving_target_distance_sensor_,
ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]))
SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]))
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
SAFE_PUBLISH_SENSOR(
this->still_target_distance_sensor_,
ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]));
SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]));
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]);
SAFE_PUBLISH_SENSOR(
this->detection_distance_sensor_,
ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH]));
SAFE_PUBLISH_SENSOR(this->detection_distance_sensor_,
encode_uint16(this->buffer_data_[DETECT_DISTANCE_HIGH], this->buffer_data_[DETECT_DISTANCE_LOW]));
if (engineering_mode) {
/*
@@ -578,8 +573,8 @@ bool LD2410Component::handle_ack_data_() {
/*
None Duration: 33~34th bytes
*/
updates.push_back(set_number_value(this->timeout_number_,
ld2410::two_byte_to_int(this->buffer_data_[32], this->buffer_data_[33])));
updates.push_back(
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32])));
for (auto &update : updates) {
update();
}
+9 -14
View File
@@ -192,8 +192,6 @@ static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0x
// MAC address the module uses when Bluetooth is disabled
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; }
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
}
@@ -398,22 +396,19 @@ void LD2412Component::handle_periodic_data_() {
Detect distance: 16~17th bytes
*/
#ifdef USE_SENSOR
SAFE_PUBLISH_SENSOR(
this->moving_target_distance_sensor_,
ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]))
SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]))
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
SAFE_PUBLISH_SENSOR(
this->still_target_distance_sensor_,
ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]))
SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]))
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY])
if (this->detection_distance_sensor_ != nullptr) {
int new_detect_distance = 0;
if (target_state != 0x00 && (target_state & MOVE_BITMASK)) {
new_detect_distance =
ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]);
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]);
} else if (target_state != 0x00) {
new_detect_distance =
ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]);
new_detect_distance = encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]);
}
this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance);
}
@@ -637,9 +632,9 @@ bool LD2412Component::handle_ack_data_() {
/*
None Duration: 11~12th bytes
*/
updates.push_back(set_number_value(this->timeout_number_,
ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13])));
ESP_LOGV(TAG, "timeout_number_: %u", ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13]));
updates.push_back(
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12])));
ESP_LOGV(TAG, "timeout_number_: %u", encode_uint16(this->buffer_data_[13], this->buffer_data_[12]));
/*
Output pin configuration: 13th bytes
*/
+19 -28
View File
@@ -168,15 +168,6 @@ static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) {
return dec_val;
}
static inline float calculate_angle(float base, float hypotenuse) {
if (base < 0.0f || hypotenuse <= 0.0f) {
return 0.0f;
}
float angle_radians = acosf(base / hypotenuse);
float angle_degrees = angle_radians * (180.0f / std::numbers::pi_v<float>);
return angle_degrees;
}
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
}
@@ -292,16 +283,19 @@ void LD2450Component::loop() {
}
}
// Count targets in zone
uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) {
uint8_t count = 0;
for (auto &index : this->target_info_) {
if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 &&
index.is_moving == is_moving) {
count++;
// Count targets in zone (single pass for both still and moving)
void LD2450Component::count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving) {
still = 0;
moving = 0;
for (auto &target : this->target_info_) {
if (target.x > zone.x1 && target.x < zone.x2 && target.y > zone.y1 && target.y < zone.y2) {
if (target.is_moving) {
moving++;
} else {
still++;
}
}
}
return count;
}
// Service reset_radar_zone
@@ -510,11 +504,8 @@ void LD2450Component::handle_periodic_data_() {
}
#ifdef USE_SENSOR
SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td);
// ANGLE
angle = ld2450::calculate_angle(static_cast<float>(ty), static_cast<float>(td));
if (tx > 0) {
angle = angle * -1;
}
// ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed
angle = atan2f(static_cast<float>(-tx), static_cast<float>(ty)) * (180.0f / std::numbers::pi_v<float>);
SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle);
#endif
#ifdef USE_TEXT_SENSOR
@@ -528,10 +519,11 @@ void LD2450Component::handle_periodic_data_() {
} else {
direction = DIRECTION_STATIONARY;
}
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction);
if (tsd != nullptr && (!tsd->has_state() || tsd->get_state() != dir_str)) {
tsd->publish_state(dir_str);
if (this->direction_dedup_[index].next(direction)) {
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
if (tsd != nullptr) {
tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction));
}
}
#endif
@@ -551,8 +543,7 @@ void LD2450Component::handle_periodic_data_() {
uint8_t zone_moving_targets = 0;
uint8_t zone_all_targets = 0;
for (index = 0; index < MAX_ZONES; index++) {
zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false);
zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true);
this->count_targets_in_zone_(this->zone_config_[index], zone_still_targets, zone_moving_targets);
zone_all_targets = zone_still_targets + zone_moving_targets;
// Publish Still Target Count in Zones
+3 -2
View File
@@ -163,7 +163,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
void save_to_flash_(float value);
float restore_from_flash_();
bool get_timeout_status_(uint32_t check_millis);
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving);
void count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving);
uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0;
@@ -194,7 +194,8 @@ class LD2450Component : public Component, public uart::UARTDevice {
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_moving_target_count_sensors_{};
#endif
#ifdef USE_TEXT_SENSOR
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
std::array<Deduplicator<uint8_t>, MAX_TARGETS> direction_dedup_{};
#endif
LazyCallbackManager<void()> data_callback_;
+3 -2
View File
@@ -3,7 +3,7 @@
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "esphome/core/time_64.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
@@ -14,7 +14,7 @@ namespace esphome {
void HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); }
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void HOT delay(uint32_t ms) { ::delay(ms); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
@@ -37,6 +37,7 @@ 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; }
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
} // namespace esphome
+12 -21
View File
@@ -17,16 +17,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -41,14 +32,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.set_data(data, len);
obj.data.set(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -57,11 +48,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.len != len) {
if (obj.data.size() != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.get(), len);
memcpy(data, obj.data.data(), len);
return true;
}
}
@@ -122,11 +113,11 @@ class LibreTinyPreferences : public ESPPreferences {
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
if (this->is_changed_(&this->db, save, key_str)) {
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
fdb_blob_make(&this->blob, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
if (err != FDB_NO_ERR) {
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
failed++;
last_err = err;
last_key = save.key;
@@ -134,7 +125,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
cached++;
}
}
@@ -159,7 +150,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Check size first - if different, data has changed
if (kv.value_len != to_save.len) {
if (kv.value_len != to_save.data.size()) {
return true;
}
@@ -173,7 +164,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Compare the actual data
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
}
bool reset() override {
+39 -1
View File
@@ -1,3 +1,4 @@
from dataclasses import dataclass, field
import enum
import esphome.automation as auto
@@ -37,7 +38,7 @@ from esphome.const import (
CONF_WEB_SERVER,
CONF_WHITE,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
@@ -66,6 +67,40 @@ from .types import ( # noqa
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
DOMAIN = "light"
@dataclass
class LightData:
gamma_tables: dict = field(default_factory=dict) # gamma_value -> fwd_arr
def _get_data() -> LightData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = LightData()
return CORE.data[DOMAIN]
def _get_or_create_gamma_table(gamma_correct):
data = _get_data()
if gamma_correct in data.gamma_tables:
return data.gamma_tables[gamma_correct]
if gamma_correct > 0:
forward = [
HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535))))
for i in range(256)
]
else:
forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
gamma_str = f"{gamma_correct}".replace(".", "_")
fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16)
fwd_arr = cg.progmem_array(fwd_id, forward)
data.gamma_tables[gamma_correct] = fwd_arr
return fwd_arr
LightRestoreMode = light_ns.enum("LightRestoreMode")
RESTORE_MODES = {
"RESTORE_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF,
@@ -238,6 +273,9 @@ async def setup_light_core_(light_var, config, output_var):
cg.add(light_var.set_flash_transition_length(flash_transition_length))
if (gamma_correct := config.get(CONF_GAMMA_CORRECT)) is not None:
cg.add(light_var.set_gamma_correct(gamma_correct))
fwd_arr = _get_or_create_gamma_table(gamma_correct)
cg.add(light_var.set_gamma_table(fwd_arr))
cg.add_define("USE_LIGHT_GAMMA_LUT")
effects = await cg.build_registry_list(
EFFECTS_REGISTRY, config.get(CONF_EFFECTS, [])
)
+3 -1
View File
@@ -66,7 +66,9 @@ class AddressableLight : public LightOutput, public Component {
Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white)));
}
void setup_state(LightState *state) override {
this->correction_.calculate_gamma_table(state->get_gamma_correct());
#ifdef USE_LIGHT_GAMMA_LUT
this->correction_.set_gamma_table(state->get_gamma_table());
#endif
this->state_parent_ = state;
}
void update_state(LightState *state) override;
@@ -74,11 +74,10 @@ class AddressableLightWrapper : public light::AddressableLight {
return;
}
float gamma = this->light_state_->get_gamma_correct();
float r = gamma_uncorrect(this->wrapper_state_[0] / 255.0f, gamma);
float g = gamma_uncorrect(this->wrapper_state_[1] / 255.0f, gamma);
float b = gamma_uncorrect(this->wrapper_state_[2] / 255.0f, gamma);
float w = gamma_uncorrect(this->wrapper_state_[3] / 255.0f, gamma);
float r = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[0] / 255.0f);
float g = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[1] / 255.0f);
float b = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[2] / 255.0f);
float w = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[3] / 255.0f);
auto call = this->light_state_->make_call();
+1 -1
View File
@@ -41,7 +41,7 @@ template<typename... Ts> class LightControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, color_temperature)
TEMPLATABLE_VALUE(float, cold_white)
TEMPLATABLE_VALUE(float, warm_white)
TEMPLATABLE_VALUE(std::string, effect)
TEMPLATABLE_VALUE(uint32_t, effect)
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
+56 -6
View File
@@ -10,12 +10,14 @@ from esphome.const import (
CONF_COLOR_MODE,
CONF_COLOR_TEMPERATURE,
CONF_EFFECT,
CONF_EFFECTS,
CONF_FLASH_LENGTH,
CONF_GREEN,
CONF_ID,
CONF_LIMIT_MODE,
CONF_MAX_BRIGHTNESS,
CONF_MIN_BRIGHTNESS,
CONF_NAME,
CONF_RANGE_FROM,
CONF_RANGE_TO,
CONF_RED,
@@ -24,6 +26,9 @@ from esphome.const import (
CONF_WARM_WHITE,
CONF_WHITE,
)
from esphome.core import CORE, Lambda
from esphome.cpp_generator import LambdaExpression
from esphome.types import ConfigType
from .types import (
COLOR_MODES,
@@ -51,6 +56,7 @@ from .types import (
),
}
),
synchronous=True,
)
async def light_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -110,14 +116,34 @@ 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
"light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA
"light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA
"light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA, synchronous=True
)
async def light_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -164,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
@@ -193,7 +240,10 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema(
@automation.register_action(
"light.dim_relative", DimRelativeAction, LIGHT_DIM_RELATIVE_ACTION_SCHEMA
"light.dim_relative",
DimRelativeAction,
LIGHT_DIM_RELATIVE_ACTION_SCHEMA,
synchronous=True,
)
async def light_dim_relative_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -1,25 +1,25 @@
#include "esp_color_correction.h"
#include "light_color_values.h"
#include "esphome/core/log.h"
namespace esphome::light {
void ESPColorCorrection::calculate_gamma_table(float gamma) {
for (uint16_t i = 0; i < 256; i++) {
// corrected = val ^ gamma
auto corrected = to_uint8_scale(gamma_correct(i / 255.0f, gamma));
this->gamma_table_[i] = corrected;
}
if (gamma == 0.0f) {
for (uint16_t i = 0; i < 256; i++)
this->gamma_reverse_table_[i] = i;
return;
}
for (uint16_t i = 0; i < 256; i++) {
// val = corrected ^ (1/gamma)
auto uncorrected = to_uint8_scale(powf(i / 255.0f, 1.0f / gamma));
this->gamma_reverse_table_[i] = uncorrected;
}
uint8_t ESPColorCorrection::gamma_correct_(uint8_t value) const {
if (this->gamma_table_ == nullptr)
return value;
return static_cast<uint8_t>((progmem_read_uint16(&this->gamma_table_[value]) + 128) / 257);
}
uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const {
if (this->gamma_table_ == nullptr)
return value;
if (value == 0)
return 0;
uint16_t target = value * 257; // Scale 0-255 to 0-65535
uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target);
if (lo >= 255)
return 255;
uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]);
uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]);
return (target - a <= b - target) ? lo : lo + 1;
}
} // namespace esphome::light
+32 -13
View File
@@ -1,15 +1,30 @@
#pragma once
#include "esphome/core/color.h"
#include "esphome/core/hal.h"
namespace esphome::light {
/// Binary search a monotonically increasing uint16[256] PROGMEM table.
/// Returns the largest index where table[index] <= target.
inline uint8_t gamma_table_reverse_search(const uint16_t *table, uint16_t target) {
uint8_t lo = 0, hi = 255;
while (lo < hi) {
uint8_t mid = (lo + hi + 1) / 2;
if (progmem_read_uint16(&table[mid]) <= target) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
class ESPColorCorrection {
public:
ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {}
void set_max_brightness(const Color &max_brightness) { this->max_brightness_ = max_brightness; }
void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; }
void calculate_gamma_table(float gamma);
void set_gamma_table(const uint16_t *table) { this->gamma_table_ = table; }
inline Color color_correct(Color color) const ESPHOME_ALWAYS_INLINE {
// corrected = (uncorrected * max_brightness * local_brightness) ^ gamma
return Color(this->color_correct_red(color.red), this->color_correct_green(color.green),
@@ -17,19 +32,19 @@ class ESPColorCorrection {
}
inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_);
return this->gamma_table_[res];
return this->gamma_correct_(res);
}
inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE {
// uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness)
@@ -39,36 +54,40 @@ class ESPColorCorrection {
inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.red == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(red) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.green == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(green) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(blue) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.white == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL;
uint16_t uncorrected = this->gamma_uncorrect_(white) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
protected:
uint8_t gamma_table_[256];
uint8_t gamma_reverse_table_[256];
Color max_brightness_;
/// Forward gamma: read uint16 PROGMEM table, convert to uint8
uint8_t gamma_correct_(uint8_t value) const;
/// Reverse gamma: binary search the forward PROGMEM table
uint8_t gamma_uncorrect_(uint8_t value) const;
const uint16_t *gamma_table_{nullptr};
Color max_brightness_{255, 255, 255, 255};
uint8_t local_brightness_{255};
};
+2 -3
View File
@@ -389,9 +389,8 @@ void LightCall::transform_parameters_() {
const float ww_fraction = (color_temp - min_mireds) / range;
const float cw_fraction = 1.0f - ww_fraction;
const float max_cw_ww = std::max(ww_fraction, cw_fraction);
const float gamma = this->parent_->get_gamma_correct();
this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma);
this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma);
this->cold_white_ = this->parent_->gamma_uncorrect_lut(cw_fraction / max_cw_ww);
this->warm_white_ = this->parent_->gamma_uncorrect_lut(ww_fraction / max_cw_ww);
this->set_flag_(FLAG_HAS_COLD_WHITE);
this->set_flag_(FLAG_HAS_WARM_WHITE);
}
+21 -27
View File
@@ -111,60 +111,54 @@ class LightColorValues {
}
}
// Note that method signature of as_* methods is kept as-is for compatibility reasons, so not all parameters
// are always used or necessary. Methods will be deprecated later.
/// Convert these light color values to a binary representation and write them to binary.
void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; }
/// Convert these light color values to a brightness-only representation and write them to brightness.
void as_brightness(float *brightness, float gamma = 0) const {
*brightness = gamma_correct(this->state_ * this->brightness_, gamma);
}
void as_brightness(float *brightness) const { *brightness = this->state_ * this->brightness_; }
/// Convert these light color values to an RGB representation and write them to red, green, blue.
void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const {
void as_rgb(float *red, float *green, float *blue) const {
if (this->color_mode_ & ColorCapability::RGB) {
float brightness = this->state_ * this->brightness_ * this->color_brightness_;
*red = gamma_correct(brightness * this->red_, gamma);
*green = gamma_correct(brightness * this->green_, gamma);
*blue = gamma_correct(brightness * this->blue_, gamma);
*red = brightness * this->red_;
*green = brightness * this->green_;
*blue = brightness * this->blue_;
} else {
*red = *green = *blue = 0;
}
}
/// Convert these light color values to an RGBW representation and write them to red, green, blue, white.
void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0,
bool color_interlock = false) const {
this->as_rgb(red, green, blue, gamma);
void as_rgbw(float *red, float *green, float *blue, float *white) const {
this->as_rgb(red, green, blue);
if (this->color_mode_ & ColorCapability::WHITE) {
*white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma);
*white = this->state_ * this->brightness_ * this->white_;
} else {
*white = 0;
}
}
/// Convert these light color values to an RGBWW representation with the given parameters.
void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, float gamma = 0,
void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
bool constant_brightness = false) const {
this->as_rgb(red, green, blue, gamma);
this->as_cwww(cold_white, warm_white, gamma, constant_brightness);
this->as_rgb(red, green, blue);
this->as_cwww(cold_white, warm_white, constant_brightness);
}
/// Convert these light color values to an RGB+CT+BR representation with the given parameters.
void as_rgbct(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue,
float *color_temperature, float *white_brightness, float gamma = 0) const {
this->as_rgb(red, green, blue, gamma);
this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness, gamma);
float *color_temperature, float *white_brightness) const {
this->as_rgb(red, green, blue);
this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness);
}
/// Convert these light color values to an CWWW representation with the given parameters.
void as_cwww(float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const {
void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const {
if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) {
const float cw_level = gamma_correct(this->cold_white_, gamma);
const float ww_level = gamma_correct(this->warm_white_, gamma);
const float white_level = gamma_correct(this->state_ * this->brightness_, gamma);
const float cw_level = this->cold_white_;
const float ww_level = this->warm_white_;
const float white_level = this->state_ * this->brightness_;
if (!constant_brightness) {
*cold_white = white_level * cw_level;
*warm_white = white_level * ww_level;
@@ -184,13 +178,13 @@ class LightColorValues {
}
/// Convert these light color values to a CT+BR representation with the given parameters.
void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, float *white_brightness,
float gamma = 0) const {
void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature,
float *white_brightness) const {
const float white_level = this->color_mode_ & ColorCapability::RGB ? this->white_ : 1;
if (this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) {
*color_temperature =
(this->color_temperature_ - color_temperature_cw) / (color_temperature_ww - color_temperature_cw);
*white_brightness = gamma_correct(this->state_ * this->brightness_ * white_level, gamma);
*white_brightness = this->state_ * this->brightness_ * white_level;
} else { // Probably won't get here but put this here anyway.
*white_brightness = 0;
}
+68 -10
View File
@@ -1,4 +1,5 @@
#include "light_state.h"
#include "esp_color_correction.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
@@ -204,33 +205,90 @@ void LightState::add_effects(const std::initializer_list<LightEffect *> &effects
void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
void LightState::current_values_as_brightness(float *brightness) {
this->current_values.as_brightness(brightness, this->gamma_correct_);
this->current_values.as_brightness(brightness);
*brightness = this->gamma_correct_lut(*brightness);
}
void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) {
this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false);
void LightState::current_values_as_rgb(float *red, float *green, float *blue) {
this->current_values.as_rgb(red, green, blue);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
}
void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) {
this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false);
void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white) {
this->current_values.as_rgbw(red, green, blue, white);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
*white = this->gamma_correct_lut(*white);
}
void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
bool constant_brightness) {
this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, this->gamma_correct_, constant_brightness);
this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
*cold_white = this->gamma_correct_lut(*cold_white);
*warm_white = this->gamma_correct_lut(*warm_white);
}
void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature,
float *white_brightness) {
auto traits = this->get_traits();
this->current_values.as_rgbct(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, color_temperature,
white_brightness, this->gamma_correct_);
white_brightness);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
*white_brightness = this->gamma_correct_lut(*white_brightness);
}
void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) {
this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness);
this->current_values.as_cwww(cold_white, warm_white, constant_brightness);
*cold_white = this->gamma_correct_lut(*cold_white);
*warm_white = this->gamma_correct_lut(*warm_white);
}
void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) {
auto traits = this->get_traits();
this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness,
this->gamma_correct_);
this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness);
*white_brightness = this->gamma_correct_lut(*white_brightness);
}
#ifdef USE_LIGHT_GAMMA_LUT
float LightState::gamma_correct_lut(float value) const {
if (value <= 0.0f)
return 0.0f;
if (value >= 1.0f)
return 1.0f;
if (this->gamma_table_ == nullptr)
return value;
float scaled = value * 255.0f;
auto idx = static_cast<uint8_t>(scaled);
if (idx >= 255)
return progmem_read_uint16(&this->gamma_table_[255]) / 65535.0f;
float frac = scaled - idx;
float a = progmem_read_uint16(&this->gamma_table_[idx]);
float b = progmem_read_uint16(&this->gamma_table_[idx + 1]);
return (a + frac * (b - a)) / 65535.0f;
}
float LightState::gamma_uncorrect_lut(float value) const {
if (value <= 0.0f)
return 0.0f;
if (value >= 1.0f)
return 1.0f;
if (this->gamma_table_ == nullptr)
return value;
uint16_t target = static_cast<uint16_t>(value * 65535.0f);
uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target);
if (lo >= 255)
return 1.0f;
// Interpolate between lo and lo+1
uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]);
uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]);
if (b == a)
return lo / 255.0f;
float frac = static_cast<float>(target - a) / static_cast<float>(b - a);
return (lo + frac) / 255.0f;
}
#endif // USE_LIGHT_GAMMA_LUT
bool LightState::is_transformer_active() { return this->is_transformer_active_; }
void LightState::start_effect_(uint32_t effect_index) {
+39 -2
View File
@@ -11,7 +11,9 @@
#include "light_traits.h"
#include "light_transformer.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include <strings.h>
#include <vector>
@@ -166,6 +168,23 @@ class LightState : public EntityBase, public Component {
void set_gamma_correct(float gamma_correct);
float get_gamma_correct() const { return this->gamma_correct_; }
#ifdef USE_LIGHT_GAMMA_LUT
/// Set pre-computed gamma forward lookup table (256-entry uint16 PROGMEM array)
void set_gamma_table(const uint16_t *forward) { this->gamma_table_ = forward; }
/// Get the forward gamma lookup table
const uint16_t *get_gamma_table() const { return this->gamma_table_; }
/// Apply gamma correction using the pre-computed forward LUT
float gamma_correct_lut(float value) const;
/// Reverse gamma correction by binary-searching the forward LUT
float gamma_uncorrect_lut(float value) const;
#else
/// No gamma LUT — passthrough
float gamma_correct_lut(float value) const { return value; }
float gamma_uncorrect_lut(float value) const { return value; }
#endif // USE_LIGHT_GAMMA_LUT
/// Set the restore mode of this light
void set_restore_mode(LightRestoreMode restore_mode);
@@ -200,6 +219,20 @@ class LightState : public EntityBase, public Component {
return 0; // Effect not found
}
/// Get effect index by name (const char* overload, avoids std::string construction).
uint32_t get_effect_index(const char *name, size_t len) const {
if (len == 4 && ESPHOME_strncasecmp_P(name, ESPHOME_PSTR("none"), 4) == 0) {
return 0;
}
StringRef ref(name, len);
for (size_t i = 0; i < this->effects_.size(); i++) {
if (str_equals_case_insensitive(ref, this->effects_[i]->get_name())) {
return i + 1;
}
}
return 0;
}
/// Get effect by index. Returns nullptr if index is invalid.
LightEffect *get_effect_by_index(uint32_t index) const {
if (index == 0 || index > this->effects_.size()) {
@@ -224,9 +257,9 @@ class LightState : public EntityBase, public Component {
void current_values_as_brightness(float *brightness);
void current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock = false);
void current_values_as_rgb(float *red, float *green, float *blue);
void current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock = false);
void current_values_as_rgbw(float *red, float *green, float *blue, float *white);
void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
bool constant_brightness = false);
@@ -297,6 +330,10 @@ class LightState : public EntityBase, public Component {
uint32_t flash_transition_length_{};
/// Gamma correction factor for the light.
float gamma_correct_{};
#ifdef USE_LIGHT_GAMMA_LUT
const uint16_t *gamma_table_{nullptr};
#endif // USE_LIGHT_GAMMA_LUT
/// Whether the light value should be written in the next cycle.
bool next_write_{true};
// for effects, true if a transformer (transition) is active.
+3 -1
View File
@@ -519,7 +519,9 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All(
)
@automation.register_action(CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA)
@automation.register_action(
CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA, synchronous=True
)
async def logger_log_action_to_code(config, action_id, template_arg, args):
esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]]
args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]]
+1 -1
View File
@@ -16,7 +16,7 @@ class LVLight : public light::LightOutput {
}
void write_state(light::LightState *state) override {
float red, green, blue;
state->current_values_as_rgb(&red, &green, &blue, false);
state->current_values_as_rgb(&red, &green, &blue);
auto color = lv_color_make(red * 255, green * 255, blue * 255);
if (this->obj_ != nullptr) {
this->set_value_(color);
+5 -2
View File
@@ -492,7 +492,7 @@ MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema(
@automation.register_action(
"mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA
"mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA, synchronous=True
)
async def mqtt_publish_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -521,7 +521,10 @@ MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema(
@automation.register_action(
"mqtt.publish_json", MQTTPublishJsonAction, MQTT_PUBLISH_JSON_ACTION_SCHEMA
"mqtt.publish_json",
MQTTPublishJsonAction,
MQTT_PUBLISH_JSON_ACTION_SCHEMA,
synchronous=True,
)
async def mqtt_publish_json_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+6
View File
@@ -354,6 +354,7 @@ OPERATION_BASE_SCHEMA = cv.Schema(
cv.Required(CONF_VALUE): cv.templatable(cv.float_),
}
),
synchronous=True,
)
async def number_set_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -376,6 +377,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"number.decrement",
@@ -390,6 +392,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"number.to_min",
@@ -403,6 +406,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"number.to_max",
@@ -416,6 +420,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"number.operation",
@@ -428,6 +433,7 @@ async def number_set_to_code(config, action_id, template_arg, args):
cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
}
),
synchronous=True,
)
async def number_to_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -52,6 +52,11 @@ def set_sdkconfig_options(config):
# There is a conflict if the logger's uart also uses the default UART, which is seen as a watchdog failure on "ot_cli"
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CLI", False)
# Console is the transport layer for CLI; disable it too since CLI is disabled
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CONSOLE_ENABLE", False)
# Diag unused, if needed for lab/cert/etc tests then enable separately
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DIAG", False)
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True)
+5 -2
View File
@@ -74,14 +74,16 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id(
)
@automation.register_action("output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA)
@automation.register_action(
"output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True
)
async def output_turn_on_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA
"output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True
)
async def output_turn_off_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -97,6 +99,7 @@ async def output_turn_off_to_code(config, action_id, template_arg, args):
cv.Required(CONF_LEVEL): cv.templatable(cv.percentage),
}
),
synchronous=True,
)
async def output_set_level_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+1 -1
View File
@@ -20,7 +20,7 @@ class RGBLightOutput : public light::LightOutput {
}
void write_state(light::LightState *state) override {
float red, green, blue;
state->current_values_as_rgb(&red, &green, &blue, false);
state->current_values_as_rgb(&red, &green, &blue);
this->red_->set_level(red);
this->green_->set_level(green);
this->blue_->set_level(blue);
+1 -1
View File
@@ -25,7 +25,7 @@ class RGBWLightOutput : public light::LightOutput {
}
void write_state(light::LightState *state) override {
float red, green, blue, white;
state->current_values_as_rgbw(&red, &green, &blue, &white, this->color_interlock_);
state->current_values_as_rgbw(&red, &green, &blue, &white);
this->red_->set_level(red);
this->green_->set_level(green);
this->blue_->set_level(blue);
+1
View File
@@ -169,6 +169,7 @@ async def to_code(config):
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_RP2040")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.set_cpp_standard("gnu++20")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "RP2040")
+1
View File
@@ -37,6 +37,7 @@ uint8_t progmem_read_byte(const uint8_t *addr) {
const char *progmem_read_ptr(const char *const *addr) {
return reinterpret_cast<const char *>(pgm_read_ptr(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(); }
+103 -86
View File
@@ -8,18 +8,26 @@ namespace esphome::rtttl {
static const char *const TAG = "rtttl";
// These values can also be found as constants in the Tone library (Tone.h)
static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047,
1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217,
2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951};
static constexpr uint8_t SONG_NAME_LENGTH_LIMIT = 64;
static constexpr uint8_t SEMITONES_IN_OCTAVE = 12;
#if defined(USE_OUTPUT) || defined(USE_SPEAKER)
static const uint32_t DOUBLE_NOTE_GAP_MS = 10;
#endif // USE_OUTPUT || USE_SPEAKER
static constexpr uint8_t MIN_OCTAVE = 4;
static constexpr uint8_t MAX_OCTAVE = 7;
static constexpr uint8_t DEFAULT_BPM = 63; // Default beats per minute
// These values can also be found as constants in the Tone library (Tone.h)
static constexpr uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047,
1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217,
2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951};
static constexpr uint8_t NOTES_COUNT = static_cast<uint8_t>(sizeof(NOTES) / sizeof(NOTES[0]));
static constexpr uint8_t REPEATING_NOTE_GAP_MS = 10;
#ifdef USE_SPEAKER
static const size_t SAMPLE_BUFFER_SIZE = 2048;
static constexpr uint16_t SAMPLE_BUFFER_SIZE = 2048;
static constexpr uint16_t SAMPLE_RATE = 16000;
struct SpeakerSample {
int8_t left{0};
@@ -27,7 +35,7 @@ struct SpeakerSample {
};
inline double deg2rad(double degrees) {
static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0;
static constexpr double PI_ON_180 = M_PI / 180.0;
return degrees * PI_ON_180;
}
#endif // USE_SPEAKER
@@ -85,7 +93,7 @@ void Rtttl::loop() {
}
#ifdef USE_OUTPUT
if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) {
if (this->output_ != nullptr && millis() - this->last_note_start_time_ < this->note_duration_) {
return;
}
#endif // USE_OUTPUT
@@ -113,36 +121,34 @@ void Rtttl::loop() {
}
if (this->samples_sent_ != this->samples_count_) {
SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2];
int x = 0;
uint16_t sample_index = 0;
double rem = 0.0;
while (true) {
// Try and send out the remainder of the existing note, one per loop()
if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note//
// Try and send out the remainder of the existing note, one per `loop()`
if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note
rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_);
int16_t val = (127 * this->gain_) * sin(deg2rad(rem)); // 16bit = 49152
sample[x].left = val;
sample[x].right = val;
int8_t val = (127 * this->gain_) * sin(deg2rad(rem));
sample[sample_index].left = val;
sample[sample_index].right = val;
} else {
sample[x].left = 0;
sample[x].right = 0;
sample[sample_index].left = 0;
sample[sample_index].right = 0;
}
if (static_cast<size_t>(x) >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) {
if (sample_index >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) {
break;
}
this->samples_sent_++;
x++;
sample_index++;
}
if (x > 0) {
size_t bytes_to_send = x * sizeof(SpeakerSample);
if (sample_index > 0) {
size_t bytes_to_send = sample_index * sizeof(SpeakerSample);
size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send);
if (send != bytes_to_send) {
this->samples_sent_ -= (x - (send / sizeof(SpeakerSample)));
this->samples_sent_ -= (sample_index - (send / sizeof(SpeakerSample)));
}
return;
}
@@ -155,83 +161,84 @@ void Rtttl::loop() {
return;
}
// align to note: most rtttl's out there does not add and space after the ',' separator but just in case...
// Align to note: most rtttl's out there does not add any space after the ',' separator but just in case
while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') {
this->position_++;
}
// first, get note duration, if available
uint8_t num = this->get_integer_();
// First, get note duration, if available
uint8_t note_denominator = this->get_integer_();
if (num) {
this->note_duration_ = this->wholenote_ / num;
if (note_denominator) {
this->note_duration_ = this->wholenote_duration_ / note_denominator;
} else {
this->note_duration_ =
this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after
// We will need to check if we are a dotted note after
this->note_duration_ = this->wholenote_duration_ / this->default_note_denominator_;
}
uint8_t note = note_index_from_char(this->rtttl_[this->position_]);
uint8_t note_index_in_octave = note_index_from_char(this->rtttl_[this->position_]);
this->position_++;
// now, get optional '#' sharp
// Now, get optional '#' sharp
if (this->rtttl_[this->position_] == '#') {
note++;
note_index_in_octave++;
this->position_++;
}
// now, get scale
// Now, get scale
uint8_t scale = this->get_integer_();
if (scale == 0) {
scale = this->default_octave_;
}
if (scale < 4 || scale > 7) {
ESP_LOGE(TAG, "Octave must be between 4 and 7 (it is %d)", scale);
if (scale < MIN_OCTAVE || scale > MAX_OCTAVE) {
ESP_LOGE(TAG, "Octave must be between %d and %d (it is %d)", MIN_OCTAVE, MAX_OCTAVE, scale);
this->finish_();
return;
}
// now, get optional '.' dotted note
// Now, get optional '.' dotted note
if (this->rtttl_[this->position_] == '.') {
this->note_duration_ += this->note_duration_ / 2;
this->note_duration_ += this->note_duration_ / 2; // Duration +50%
this->position_++;
}
// Now play the note
bool need_note_gap = false;
if (note) {
auto note_index = (scale - 4) * 12 + note;
if (note_index < 0 || note_index >= (int) (sizeof(NOTES) / sizeof(NOTES[0]))) {
ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index,
(int) (sizeof(NOTES) / sizeof(NOTES[0])));
// Now play the note
if (note_index_in_octave == 0) {
this->output_freq_ = 0;
ESP_LOGVV(TAG, "Waiting: %dms", this->note_duration_);
} else {
uint8_t note_index = (scale - MIN_OCTAVE) * SEMITONES_IN_OCTAVE + note_index_in_octave;
if (note_index >= NOTES_COUNT) {
ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note_index_in_octave, scale,
note_index, NOTES_COUNT);
this->finish_();
return;
}
auto freq = NOTES[note_index];
uint16_t freq = NOTES[note_index];
need_note_gap = freq == this->output_freq_;
// Add small silence gap between same note
this->output_freq_ = freq;
ESP_LOGVV(TAG, "playing note: %d for %dms", note, this->note_duration_);
} else {
ESP_LOGVV(TAG, "waiting: %dms", this->note_duration_);
this->output_freq_ = 0;
ESP_LOGVV(TAG, "Playing note: %d for %dms", note_index_in_octave, this->note_duration_);
}
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
if (need_note_gap && this->note_duration_ > DOUBLE_NOTE_GAP_MS) {
if (this->output_freq_ == 0) {
this->output_->set_level(0.0);
delay(DOUBLE_NOTE_GAP_MS);
this->note_duration_ -= DOUBLE_NOTE_GAP_MS;
}
if (this->output_freq_ != 0) {
} else {
if (need_note_gap && this->note_duration_ > REPEATING_NOTE_GAP_MS) {
this->output_->set_level(0.0);
delay(REPEATING_NOTE_GAP_MS);
this->note_duration_ -= REPEATING_NOTE_GAP_MS;
}
this->output_->update_frequency(this->output_freq_);
this->output_->set_level(this->gain_);
} else {
this->output_->set_level(0.0);
}
}
#endif // USE_OUTPUT
@@ -241,28 +248,26 @@ void Rtttl::loop() {
this->samples_sent_ = 0;
this->samples_gap_ = 0;
this->samples_per_wave_ = 0;
this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000;
this->samples_count_ = (SAMPLE_RATE * this->note_duration_) / 1000;
if (need_note_gap) {
this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000;
this->samples_gap_ = (SAMPLE_RATE * REPEATING_NOTE_GAP_MS) / 1000;
}
if (this->output_freq_ != 0) {
// make sure there is enough samples to add a full last sinus.
uint16_t samples_wish = this->samples_count_;
this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_;
// Make sure there is enough samples to add a full last sinus.
uint32_t samples_wish = this->samples_count_;
this->samples_per_wave_ = (SAMPLE_RATE << 10) / this->output_freq_;
uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1;
this->samples_count_ = (division * this->samples_per_wave_);
this->samples_count_ = this->samples_count_ >> 10;
ESP_LOGVV(TAG, "- Calc play time: wish: %d gets: %d (div: %d spw: %d)", samples_wish, this->samples_count_,
division, this->samples_per_wave_);
this->samples_count_ = (division * this->samples_per_wave_) >> 10;
ESP_LOGVV(TAG, "Calc play time: wish: %" PRIu32 " gets: %" PRIu32 " (div: %d spw: %" PRIu32 ")", samples_wish,
this->samples_count_, division, this->samples_per_wave_);
}
// Convert from frequency in Hz to high and low samples in fixed point
}
#endif // USE_SPEAKER
this->last_note_ = millis();
this->last_note_start_time_ = millis();
}
void Rtttl::play(std::string rtttl) {
@@ -275,25 +280,28 @@ void Rtttl::play(std::string rtttl) {
this->rtttl_ = std::move(rtttl);
this->default_duration_ = 4;
this->default_octave_ = 6;
this->default_note_denominator_ = DEFAULT_NOTE_DENOMINATOR;
this->default_octave_ = DEFAULT_OCTAVE;
this->note_duration_ = 0;
int bpm = 63;
uint16_t num;
uint16_t bpm = DEFAULT_BPM;
uint16_t num; // Used for: default note-denominator, default octave, BPM
// Get name
this->position_ = this->rtttl_.find(':');
// it's somewhat documented to be up to 10 characters but let's be a bit flexible here
if (this->position_ == std::string::npos || this->position_ > 15) {
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Unable to determine name; missing ':'");
return;
}
if (this->position_ >= SONG_NAME_LENGTH_LIMIT) {
ESP_LOGE(TAG, "Name is too long: length=%u, limit=%u", static_cast<unsigned>(this->position_),
static_cast<unsigned>(SONG_NAME_LENGTH_LIMIT));
return;
}
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
// get default duration
// Get default duration
this->position_ = this->rtttl_.find("d=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'd='");
@@ -301,11 +309,14 @@ void Rtttl::play(std::string rtttl) {
}
this->position_ += 2;
num = this->get_integer_();
if (num > 0) {
this->default_duration_ = num;
if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
this->default_note_denominator_ = num;
} else {
ESP_LOGE(TAG, "Invalid default duration: %d", num);
return;
}
// get default octave
// Get default octave
this->position_ = this->rtttl_.find("o=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'o=");
@@ -313,11 +324,14 @@ void Rtttl::play(std::string rtttl) {
}
this->position_ += 2;
num = this->get_integer_();
if (num >= 3 && num <= 7) {
if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
this->default_octave_ = num;
} else {
ESP_LOGE(TAG, "Invalid default octave: %d", num);
return;
}
// get BPM
// Get BPM
this->position_ = this->rtttl_.find("b=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing b=");
@@ -325,8 +339,11 @@ void Rtttl::play(std::string rtttl) {
}
this->position_ += 2;
num = this->get_integer_();
if (num != 0) {
if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
bpm = num;
} else {
ESP_LOGE(TAG, "Invalid BPM: %d", num);
return;
}
this->position_ = this->rtttl_.find(':', this->position_);
@@ -337,10 +354,10 @@ void Rtttl::play(std::string rtttl) {
this->position_++;
// BPM usually expresses the number of quarter notes per minute
this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds)
this->wholenote_duration_ = 60 * 1000L * 4 / bpm; // This is the time for whole note (in milliseconds)
this->output_freq_ = 0;
this->last_note_ = millis();
this->last_note_start_time_ = millis();
this->note_duration_ = 1;
#ifdef USE_OUTPUT
+16 -15
View File
@@ -13,6 +13,10 @@
namespace esphome::rtttl {
inline constexpr uint8_t DEFAULT_NOTE_DENOMINATOR = 4; // Default note-denominator (quarter note)
inline constexpr uint8_t DEFAULT_OCTAVE =
6; // Default octave for a note (see: `MIN_OCTAVE`, `MAX_OCTAVE` in `rtttl.cpp`)
enum class State : uint8_t {
STOPPED = 0,
INIT,
@@ -67,19 +71,18 @@ class Rtttl : public Component {
std::string rtttl_{""};
/// The current position in the RTTTL string.
size_t position_{0};
/// The duration of a whole note in milliseconds.
uint16_t wholenote_;
/// The default duration of a note (e.g. 4 for a quarter note).
uint16_t default_duration_;
uint8_t default_note_denominator_{DEFAULT_NOTE_DENOMINATOR};
/// The default octave for a note.
uint16_t default_octave_;
/// The time the last note was started.
uint32_t last_note_;
uint8_t default_octave_{DEFAULT_OCTAVE};
/// The duration of the current note in milliseconds.
uint16_t note_duration_;
uint16_t note_duration_{0};
/// The duration of a whole note in milliseconds.
uint16_t wholenote_duration_;
/// The time in milliseconds since microcontroller boot when the last note was started.
uint32_t last_note_start_time_;
/// The frequency of the current note in Hz.
uint32_t output_freq_;
uint32_t output_freq_{0};
/// The gain of the output.
float gain_{0.6f};
/// The current state of the RTTTL player.
@@ -93,16 +96,14 @@ class Rtttl : public Component {
#ifdef USE_SPEAKER
/// The speaker to write the sound to.
speaker::Speaker *speaker_{nullptr};
/// The sample rate of the speaker.
int sample_rate_{16000};
/// The number of samples for one full cycle of a note's waveform, in Q10 fixed-point format.
int samples_per_wave_{0};
uint32_t samples_per_wave_{0};
/// The number of samples sent.
int samples_sent_{0};
uint32_t samples_sent_{0};
/// The total number of samples to send.
int samples_count_{0};
uint32_t samples_count_{0};
/// The number of samples for the gap between notes.
int samples_gap_{0};
uint32_t samples_gap_{0};
#endif // USE_SPEAKER
/// The callback to call when playback is finished.
+2
View File
@@ -160,6 +160,7 @@ async def to_code(config):
cv.Optional(validate_parameter_name): cv.templatable(cv.valid),
},
),
synchronous=True,
)
async def script_execute_action_to_code(config, action_id, template_arg, args):
def convert(type: str):
@@ -208,6 +209,7 @@ async def script_execute_action_to_code(config, action_id, template_arg, args):
"script.stop",
ScriptStopAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
synchronous=True,
)
async def script_stop_action_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
+7
View File
@@ -144,6 +144,7 @@ OPERATION_BASE_SCHEMA = cv.Schema(
cv.Required(CONF_OPTION): cv.templatable(cv.string_strict),
}
),
synchronous=True,
)
async def select_set_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -161,6 +162,7 @@ async def select_set_to_code(config, action_id, template_arg, args):
cv.Required(CONF_INDEX): cv.templatable(cv.positive_int),
}
),
synchronous=True,
)
async def select_set_index_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -216,6 +218,7 @@ async def select_is_to_code(config, condition_id, template_arg, args):
cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
}
),
synchronous=True,
)
@automation.register_action(
"select.next",
@@ -228,6 +231,7 @@ async def select_is_to_code(config, condition_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"select.previous",
@@ -242,6 +246,7 @@ async def select_is_to_code(config, condition_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"select.first",
@@ -253,6 +258,7 @@ async def select_is_to_code(config, condition_id, template_arg, args):
}
)
),
synchronous=True,
)
@automation.register_action(
"select.last",
@@ -264,6 +270,7 @@ async def select_is_to_code(config, condition_id, template_arg, args):
}
)
),
synchronous=True,
)
async def select_operation_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+4 -3
View File
@@ -189,9 +189,10 @@ async def to_code(config):
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
cg.add_define("USE_SOCKET_SELECT_SUPPORT")
# ESP32 and LibreTiny both have LwIP >= 2.1.3 with lwip_socket_dbg_get_socket()
# and FreeRTOS task notifications — enable fast select to bypass lwip_select()
if CORE.is_esp32 or CORE.is_libretiny:
cg.add_define("USE_LWIP_FAST_SELECT")
# and FreeRTOS task notifications — enable fast select to bypass lwip_select().
# Only when not using lwip_tcp, which does not provide select() support.
if (CORE.is_esp32 or CORE.is_libretiny) and impl != IMPLEMENTATION_LWIP_TCP:
cg.add_build_flag("-DUSE_LWIP_FAST_SELECT")
def FILTER_SOURCE_FILES() -> list[str]:
+63 -109
View File
@@ -1,135 +1,81 @@
#include "socket.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "socket.h"
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <cstring>
#include "esphome/core/application.h"
#ifdef USE_ESP32
#include <esp_idf_version.h>
#include <lwip/sockets.h>
#endif
namespace esphome::socket {
class BSDSocketImpl final : public Socket {
public:
BSDSocketImpl(int fd, bool monitor_loop = false) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
}
}
~BSDSocketImpl() override {
if (!this->closed_) {
this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
}
}
int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(this->fd_, addr, addrlen); }
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
int fd = ::accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<BSDSocketImpl>(fd, false);
}
std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override {
int fd = ::accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<BSDSocketImpl>(fd, true);
BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
}
}
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
int close() override {
if (!this->closed_) {
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
int ret = ::close(this->fd_);
this->closed_ = true;
return ret;
BSDSocketImpl::~BSDSocketImpl() {
if (!this->closed_) {
this->close();
}
}
int BSDSocketImpl::close() {
if (!this->closed_) {
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
int ret = ::close(this->fd_);
this->closed_ = true;
return ret;
}
return 0;
}
int BSDSocketImpl::setblocking(bool blocking) {
int fl = ::fcntl(this->fd_, F_GETFL, 0);
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
::fcntl(this->fd_, F_SETFL, fl);
return 0;
}
bool BSDSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); }
size_t BSDSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getpeername(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
int shutdown(int how) override { return ::shutdown(this->fd_, how); }
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return ::getpeername(this->fd_, addr, addrlen);
}
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override {
return ::getsockname(this->fd_, addr, addrlen);
}
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
return ::getsockopt(this->fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
return ::setsockopt(this->fd_, level, optname, optval, optlen);
}
int listen(int backlog) override { return ::listen(this->fd_, backlog); }
ssize_t read(void *buf, size_t len) override {
#ifdef USE_ESP32
return ::lwip_read(this->fd_, buf, len);
#else
return ::read(this->fd_, buf, len);
#endif
}
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override {
#if defined(USE_ESP32) || defined(USE_HOST)
return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len);
#else
return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
#endif
}
ssize_t readv(const struct iovec *iov, int iovcnt) override {
#if defined(USE_ESP32)
return ::lwip_readv(this->fd_, iov, iovcnt);
#else
return ::readv(this->fd_, iov, iovcnt);
#endif
}
ssize_t write(const void *buf, size_t len) override {
#ifdef USE_ESP32
return ::lwip_write(this->fd_, buf, len);
#else
return ::write(this->fd_, buf, len);
#endif
}
ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); }
ssize_t writev(const struct iovec *iov, int iovcnt) override {
#if defined(USE_ESP32)
return ::lwip_writev(this->fd_, iov, iovcnt);
#else
return ::writev(this->fd_, iov, iovcnt);
#endif
}
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument)
}
int setblocking(bool blocking) override {
int fl = ::fcntl(this->fd_, F_GETFL, 0);
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
::fcntl(this->fd_, F_SETFL, fl);
size_t BSDSocketImpl::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getsockname(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
};
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
// Helper to create a socket with optional monitoring
static std::unique_ptr<Socket> create_socket(int domain, int type, int protocol, bool loop_monitored = false) {
static std::unique_ptr<BSDSocketImpl> create_socket(int domain, int type, int protocol, bool loop_monitored = false) {
int ret = ::socket(domain, type, protocol);
if (ret == -1)
return nullptr;
return std::unique_ptr<Socket>{new BSDSocketImpl(ret, loop_monitored)};
return make_unique<BSDSocketImpl>(ret, loop_monitored);
}
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
@@ -140,6 +86,14 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
return create_socket(domain, type, protocol, true);
}
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, false);
}
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, true);
}
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_BSD_SOCKETS
@@ -0,0 +1,114 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <memory>
#include <span>
#include "esphome/core/helpers.h"
#include "headers.h"
#ifdef USE_ESP32
#include <lwip/sockets.h>
#endif
namespace esphome::socket {
class BSDSocketImpl {
public:
BSDSocketImpl(int fd, bool monitor_loop = false);
~BSDSocketImpl();
BSDSocketImpl(const BSDSocketImpl &) = delete;
BSDSocketImpl &operator=(const BSDSocketImpl &) = delete;
int connect(const struct sockaddr *addr, socklen_t addrlen) { return ::connect(this->fd_, addr, addrlen); }
std::unique_ptr<BSDSocketImpl> accept(struct sockaddr *addr, socklen_t *addrlen) {
int fd = ::accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<BSDSocketImpl>(fd, false);
}
std::unique_ptr<BSDSocketImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
int fd = ::accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<BSDSocketImpl>(fd, true);
}
int bind(const struct sockaddr *addr, socklen_t addrlen) { return ::bind(this->fd_, addr, addrlen); }
int close();
int shutdown(int how) { return ::shutdown(this->fd_, how); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return ::getpeername(this->fd_, addr, addrlen); }
int getsockname(struct sockaddr *addr, socklen_t *addrlen) { return ::getsockname(this->fd_, addr, addrlen); }
/// Format peer address into a fixed-size buffer (no heap allocation)
size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
/// Format local address into a fixed-size buffer (no heap allocation)
size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) {
return ::getsockopt(this->fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
return ::setsockopt(this->fd_, level, optname, optval, optlen);
}
int listen(int backlog) { return ::listen(this->fd_, backlog); }
ssize_t read(void *buf, size_t len) {
#ifdef USE_ESP32
return ::lwip_read(this->fd_, buf, len);
#else
return ::read(this->fd_, buf, len);
#endif
}
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) {
#if defined(USE_ESP32) || defined(USE_HOST)
return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len);
#else
return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
#endif
}
ssize_t readv(const struct iovec *iov, int iovcnt) {
#if defined(USE_ESP32)
return ::lwip_readv(this->fd_, iov, iovcnt);
#else
return ::readv(this->fd_, iov, iovcnt);
#endif
}
ssize_t write(const void *buf, size_t len) {
#ifdef USE_ESP32
return ::lwip_write(this->fd_, buf, len);
#else
return ::write(this->fd_, buf, len);
#endif
}
ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); }
ssize_t writev(const struct iovec *iov, int iovcnt) {
#if defined(USE_ESP32)
return ::lwip_writev(this->fd_, iov, iovcnt);
#else
return ::writev(this->fd_, iov, iovcnt);
#endif
}
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) {
return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument)
}
int setblocking(bool blocking);
int loop() { return 0; }
bool ready() const;
int get_fd() const { return this->fd_; }
protected:
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
};
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_BSD_SOCKETS
+17
View File
@@ -183,3 +183,20 @@ using socklen_t = uint32_t;
#endif
#endif // USE_SOCKET_IMPL_BSD_SOCKETS
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
namespace esphome::socket {
// Maximum length for formatted socket address string (IP address without port)
// IPv4: "255.255.255.255" = 15 chars + null = 16
// IPv6: full address = 45 chars + null = 46
#if USE_NETWORK_IPV6
static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN
#else
static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN
#endif
} // namespace esphome::socket
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,200 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_SOCKET_IMPL_LWIP_TCP
#include <array>
#include <cerrno>
#include <cstring>
#include <memory>
#include <span>
#include "esphome/core/helpers.h"
#include "headers.h"
#include "lwip/ip.h"
#include "lwip/netif.h"
#include "lwip/opt.h"
#include "lwip/tcp.h"
namespace esphome::socket {
// Forward declaration
class LWIPRawImpl;
/// Non-virtual common base for LWIP raw TCP sockets.
/// Provides shared fields and methods for both connected and listening sockets.
/// No virtual methods — pure code sharing.
class LWIPRawCommon {
public:
LWIPRawCommon(sa_family_t family, struct tcp_pcb *pcb) : pcb_(pcb), family_(family) {}
~LWIPRawCommon();
LWIPRawCommon(const LWIPRawCommon &) = delete;
LWIPRawCommon &operator=(const LWIPRawCommon &) = delete;
int bind(const struct sockaddr *name, socklen_t addrlen);
int close();
int shutdown(int how);
int getpeername(struct sockaddr *name, socklen_t *addrlen);
int getsockname(struct sockaddr *name, socklen_t *addrlen);
/// Format peer address into a fixed-size buffer (no heap allocation)
size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
/// Format local address into a fixed-size buffer (no heap allocation)
size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);
int getsockopt(int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int level, int optname, const void *optval, socklen_t optlen);
int get_fd() const { return -1; }
protected:
int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen);
// Member ordering optimized to minimize padding on 32-bit systems
struct tcp_pcb *pcb_;
// don't use lwip nodelay flag, it sometimes causes reconnect
// instead use it for determining whether to call lwip_output
bool nodelay_ = false;
sa_family_t family_ = 0;
};
/// Connected socket implementation for LWIP raw TCP.
/// No virtual methods — callers always use the concrete type.
class LWIPRawImpl : public LWIPRawCommon {
public:
using LWIPRawCommon::LWIPRawCommon;
~LWIPRawImpl();
void init();
// Non-listening sockets return error
std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *, socklen_t *) {
errno = EINVAL;
return nullptr;
}
std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
return this->accept(addr, addrlen);
}
// Regular sockets can't be converted to listening - this shouldn't happen
// as listen() should only be called on sockets created for listening
int listen(int) {
errno = EOPNOTSUPP;
return -1;
}
ssize_t read(void *buf, size_t len);
ssize_t readv(const struct iovec *iov, int iovcnt);
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
errno = ENOTSUP;
return -1;
}
ssize_t write(const void *buf, size_t len);
ssize_t writev(const struct iovec *iov, int iovcnt);
ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
// return ::sendto(fd_, buf, len, flags, to, tolen);
errno = ENOSYS;
return -1;
}
bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
int setblocking(bool blocking) {
if (this->pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
if (blocking) {
// blocking operation not supported
errno = EINVAL;
return -1;
}
return 0;
}
int loop() { return 0; }
err_t recv_fn(struct pbuf *pb, err_t err);
static void s_err_fn(void *arg, err_t err);
static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
protected:
ssize_t internal_write_(const void *buf, size_t len);
int internal_output_();
pbuf *rx_buf_ = nullptr;
size_t rx_buf_offset_ = 0;
bool rx_closed_ = false;
};
/// Listening socket implementation for LWIP raw TCP.
/// Separate from LWIPRawImpl — no virtual dispatch needed.
class LWIPRawListenImpl : public LWIPRawCommon {
public:
using LWIPRawCommon::LWIPRawCommon;
~LWIPRawListenImpl();
void init();
bool ready() const { return this->accepted_socket_count_ > 0; }
std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *addr, socklen_t *addrlen);
std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
return this->accept(addr, addrlen);
}
int listen(int backlog);
// Listening sockets don't do I/O
ssize_t read(void *, size_t) {
errno = ENOTSUP;
return -1;
}
ssize_t write(const void *, size_t) {
errno = ENOTSUP;
return -1;
}
ssize_t readv(const struct iovec *, int) {
errno = ENOTSUP;
return -1;
}
ssize_t writev(const struct iovec *, int) {
errno = ENOTSUP;
return -1;
}
ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
errno = ENOTSUP;
return -1;
}
ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
errno = ENOTSUP;
return -1;
}
int setblocking(bool) { return 0; }
int loop() { return 0; }
static void s_err_fn(void *arg, err_t err);
private:
err_t accept_fn_(struct tcp_pcb *newpcb, err_t err);
static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err);
// Accept queue - holds incoming connections briefly until the event loop calls accept()
// This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop
// 3 slots is plenty since connections are pulled out quickly by the event loop
//
// Memory analysis: std::array<3> vs original std::queue implementation:
// - std::queue uses std::deque internally which on 32-bit systems needs:
// 24 bytes (deque object) + 32+ bytes (map array) + heap allocations
// Total: ~56+ bytes minimum, plus heap fragmentation
// - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes)
// Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations
// Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation)
//
// By using a separate listening socket class, regular connected sockets save
// 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) of memory overhead on 32-bit systems
static constexpr size_t MAX_ACCEPTED_SOCKETS = 3;
std::array<std::unique_ptr<LWIPRawImpl>, MAX_ACCEPTED_SOCKETS> accepted_sockets_;
uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue
};
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_LWIP_TCP
+63 -76
View File
@@ -1,6 +1,6 @@
#include "socket.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "socket.h"
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
@@ -9,94 +9,73 @@
namespace esphome::socket {
class LwIPSocketImpl final : public Socket {
public:
LwIPSocketImpl(int fd, bool monitor_loop = false) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
}
}
~LwIPSocketImpl() override {
if (!this->closed_) {
this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
}
}
int connect(const struct sockaddr *addr, socklen_t addrlen) override {
return lwip_connect(this->fd_, addr, addrlen);
}
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
int fd = lwip_accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<LwIPSocketImpl>(fd, false);
}
std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override {
int fd = lwip_accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<LwIPSocketImpl>(fd, true);
LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
}
}
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
int close() override {
if (!this->closed_) {
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
int ret = lwip_close(this->fd_);
this->closed_ = true;
return ret;
LwIPSocketImpl::~LwIPSocketImpl() {
if (!this->closed_) {
this->close();
}
}
int LwIPSocketImpl::close() {
if (!this->closed_) {
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
int ret = lwip_close(this->fd_);
this->closed_ = true;
return ret;
}
return 0;
}
int LwIPSocketImpl::setblocking(bool blocking) {
int fl = lwip_fcntl(this->fd_, F_GETFL, 0);
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
lwip_fcntl(this->fd_, F_SETFL, fl);
return 0;
}
bool LwIPSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); }
size_t LwIPSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getpeername(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
int shutdown(int how) override { return lwip_shutdown(this->fd_, how); }
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return lwip_getpeername(this->fd_, addr, addrlen);
}
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override {
return lwip_getsockname(this->fd_, addr, addrlen);
}
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
return lwip_getsockopt(this->fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
return lwip_setsockopt(this->fd_, level, optname, optval, optlen);
}
int listen(int backlog) override { return lwip_listen(this->fd_, backlog); }
ssize_t read(void *buf, size_t len) override { return lwip_read(this->fd_, buf, len); }
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override {
return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
}
ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(this->fd_, iov, iovcnt); }
ssize_t write(const void *buf, size_t len) override { return lwip_write(this->fd_, buf, len); }
ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); }
ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(this->fd_, iov, iovcnt); }
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
return lwip_sendto(this->fd_, buf, len, flags, to, tolen);
}
int setblocking(bool blocking) override {
int fl = lwip_fcntl(this->fd_, F_GETFL, 0);
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
lwip_fcntl(this->fd_, F_SETFL, fl);
size_t LwIPSocketImpl::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getsockname(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
};
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
// Helper to create a socket with optional monitoring
static std::unique_ptr<Socket> create_socket(int domain, int type, int protocol, bool loop_monitored = false) {
static std::unique_ptr<LwIPSocketImpl> create_socket(int domain, int type, int protocol, bool loop_monitored = false) {
int ret = lwip_socket(domain, type, protocol);
if (ret == -1)
return nullptr;
return std::unique_ptr<Socket>{new LwIPSocketImpl(ret, loop_monitored)};
return make_unique<LwIPSocketImpl>(ret, loop_monitored);
}
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
@@ -107,6 +86,14 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
return create_socket(domain, type, protocol, true);
}
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, false);
}
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, true);
}
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_LWIP_SOCKETS
@@ -0,0 +1,80 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
#include <memory>
#include <span>
#include "esphome/core/helpers.h"
#include "headers.h"
namespace esphome::socket {
class LwIPSocketImpl {
public:
LwIPSocketImpl(int fd, bool monitor_loop = false);
~LwIPSocketImpl();
LwIPSocketImpl(const LwIPSocketImpl &) = delete;
LwIPSocketImpl &operator=(const LwIPSocketImpl &) = delete;
int connect(const struct sockaddr *addr, socklen_t addrlen) { return lwip_connect(this->fd_, addr, addrlen); }
std::unique_ptr<LwIPSocketImpl> accept(struct sockaddr *addr, socklen_t *addrlen) {
int fd = lwip_accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<LwIPSocketImpl>(fd, false);
}
std::unique_ptr<LwIPSocketImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
int fd = lwip_accept(this->fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<LwIPSocketImpl>(fd, true);
}
int bind(const struct sockaddr *addr, socklen_t addrlen) { return lwip_bind(this->fd_, addr, addrlen); }
int close();
int shutdown(int how) { return lwip_shutdown(this->fd_, how); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return lwip_getpeername(this->fd_, addr, addrlen); }
int getsockname(struct sockaddr *addr, socklen_t *addrlen) { return lwip_getsockname(this->fd_, addr, addrlen); }
/// Format peer address into a fixed-size buffer (no heap allocation)
size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
/// Format local address into a fixed-size buffer (no heap allocation)
size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) {
return lwip_getsockopt(this->fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
return lwip_setsockopt(this->fd_, level, optname, optval, optlen);
}
int listen(int backlog) { return lwip_listen(this->fd_, backlog); }
ssize_t read(void *buf, size_t len) { return lwip_read(this->fd_, buf, len); }
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) {
return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
}
ssize_t readv(const struct iovec *iov, int iovcnt) { return lwip_readv(this->fd_, iov, iovcnt); }
ssize_t write(const void *buf, size_t len) { return lwip_write(this->fd_, buf, len); }
ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); }
ssize_t writev(const struct iovec *iov, int iovcnt) { return lwip_writev(this->fd_, iov, iovcnt); }
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) {
return lwip_sendto(this->fd_, buf, len, flags, to, tolen);
}
int setblocking(bool blocking);
int loop() { return 0; }
bool ready() const;
int get_fd() const { return this->fd_; }
protected:
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
};
} // namespace esphome::socket
#endif // USE_SOCKET_IMPL_LWIP_SOCKETS
+6 -26
View File
@@ -8,10 +8,10 @@
namespace esphome::socket {
Socket::~Socket() {}
#ifdef USE_SOCKET_SELECT_SUPPORT
bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); }
// Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets).
// Checks if the Application's select() loop has marked this fd as ready.
bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); }
#endif
// Platform-specific inet_ntop wrappers
@@ -81,26 +81,6 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s
return 0;
}
size_t Socket::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getpeername(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
size_t Socket::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getsockname(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
std::unique_ptr<Socket> socket_ip(int type, int protocol) {
#if USE_NETWORK_IPV6
return socket(AF_INET6, type, protocol);
@@ -109,11 +89,11 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol) {
#endif /* USE_NETWORK_IPV6 */
}
std::unique_ptr<Socket> socket_ip_loop_monitored(int type, int protocol) {
std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol) {
#if USE_NETWORK_IPV6
return socket_loop_monitored(AF_INET6, type, protocol);
return socket_listen_loop_monitored(AF_INET6, type, protocol);
#else
return socket_loop_monitored(AF_INET, type, protocol);
return socket_listen_loop_monitored(AF_INET, type, protocol);
#endif /* USE_NETWORK_IPV6 */
}
+35 -75
View File
@@ -7,87 +7,41 @@
#include "headers.h"
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
// Include only the active implementation's header.
// SOCKADDR_STR_LEN is defined in headers.h.
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include "bsd_sockets_impl.h"
#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
#include "lwip_sockets_impl.h"
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
#include "lwip_raw_tcp_impl.h"
#endif
namespace esphome::socket {
// Maximum length for formatted socket address string (IP address without port)
// IPv4: "255.255.255.255" = 15 chars + null = 16
// IPv6: full address = 45 chars + null = 46
#if USE_NETWORK_IPV6
static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN
#else
static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN
// Type aliases — only one implementation is active per build.
// Socket is the concrete type for connected sockets.
// ListenSocket is the concrete type for listening/server sockets.
// On BSD and LWIP_SOCKETS, both aliases resolve to the same type.
// On LWIP_TCP, they are different types (no virtual dispatch between them).
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
using Socket = BSDSocketImpl;
using ListenSocket = BSDSocketImpl;
#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
using Socket = LwIPSocketImpl;
using ListenSocket = LwIPSocketImpl;
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
using Socket = LWIPRawImpl;
using ListenSocket = LWIPRawListenImpl;
#endif
class Socket {
public:
Socket() = default;
virtual ~Socket();
Socket(const Socket &) = delete;
Socket &operator=(const Socket &) = delete;
virtual std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) = 0;
/// Accept a connection and monitor it in the main loop
/// NOTE: This function is NOT thread-safe and must only be called from the main loop
virtual std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
return accept(addr, addrlen); // Default implementation for backward compatibility
}
virtual int bind(const struct sockaddr *addr, socklen_t addrlen) = 0;
virtual int close() = 0;
// not supported yet:
// virtual int connect(const std::string &address) = 0;
#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0;
#endif
virtual int shutdown(int how) = 0;
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0;
/// Format peer address into a fixed-size buffer (no heap allocation)
/// Non-virtual wrapper around getpeername() - can be optimized away if unused
/// Returns number of characters written (excluding null terminator), or 0 on error
size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
/// Format local address into a fixed-size buffer (no heap allocation)
/// Non-virtual wrapper around getsockname() - can be optimized away if unused
size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);
virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0;
virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0;
virtual int listen(int backlog) = 0;
virtual ssize_t read(void *buf, size_t len) = 0;
virtual ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) = 0;
virtual ssize_t readv(const struct iovec *iov, int iovcnt) = 0;
virtual ssize_t write(const void *buf, size_t len) = 0;
virtual ssize_t writev(const struct iovec *iov, int iovcnt) = 0;
virtual ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) = 0;
virtual int setblocking(bool blocking) = 0;
virtual int loop() { return 0; };
/// Get the underlying file descriptor (returns -1 if not supported)
/// Non-virtual: only one socket implementation is active per build.
#ifdef USE_SOCKET_SELECT_SUPPORT
int get_fd() const { return this->fd_; }
#else
int get_fd() const { return -1; }
/// Shared ready() helper for fd-based socket implementations.
/// Checks if the Application's select() loop has marked this fd as ready.
bool socket_ready_fd(int fd, bool loop_monitored);
#endif
/// Check if socket has data ready to read. Must only be called from the main loop thread.
/// For select()-based sockets: non-virtual, checks Application's select() results
/// For LWIP raw TCP sockets: virtual, checks internal buffer state
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const;
#else
virtual bool ready() const { return true; }
#endif
protected:
#ifdef USE_SOCKET_SELECT_SUPPORT
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
#endif
};
/// Create a socket of the given domain, type and protocol.
std::unique_ptr<Socket> socket(int domain, int type, int protocol);
/// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol.
@@ -100,7 +54,13 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol);
/// NOTE: On ESP platforms, FD_SETSIZE is typically 10, limiting the number of monitored sockets.
/// File descriptors >= FD_SETSIZE will not be monitored and will log an error.
std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol);
std::unique_ptr<Socket> socket_ip_loop_monitored(int type, int protocol);
/// Create a listening socket of the given domain, type and protocol.
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol);
/// Create a listening socket and monitor it for data in the main loop.
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol);
/// Create a listening socket in the newest available IP domain and monitor it.
std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol);
/// Set a sockaddr to the specified address and port for the IP version used by socket_ip().
/// @param addr Destination sockaddr structure
+10 -4
View File
@@ -206,7 +206,7 @@ SWITCH_CONTROL_ACTION_SCHEMA = automation.maybe_simple_id(
@automation.register_action(
"switch.control", ControlAction, SWITCH_CONTROL_ACTION_SCHEMA
"switch.control", ControlAction, SWITCH_CONTROL_ACTION_SCHEMA, synchronous=True
)
async def switch_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -216,9 +216,15 @@ async def switch_control_to_code(config, action_id, template_arg, args):
return var
@automation.register_action("switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA)
@automation.register_action("switch.turn_off", TurnOffAction, SWITCH_ACTION_SCHEMA)
@automation.register_action("switch.turn_on", TurnOnAction, SWITCH_ACTION_SCHEMA)
@automation.register_action(
"switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"switch.turn_off", TurnOffAction, SWITCH_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"switch.turn_on", TurnOnAction, SWITCH_ACTION_SCHEMA, synchronous=True
)
async def switch_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
+1
View File
@@ -163,6 +163,7 @@ OPERATION_BASE_SCHEMA = cv.Schema(
cv.Required(CONF_VALUE): cv.templatable(cv.string_strict),
}
),
synchronous=True,
)
async def text_set_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+5
View File
@@ -11,6 +11,11 @@ TextCall &TextCall::set_value(const std::string &value) {
return *this;
}
TextCall &TextCall::set_value(const char *value, size_t len) {
this->value_ = std::string(value, len);
return *this;
}
void TextCall::validate_() {
const auto *name = this->parent_->get_name().c_str();
+1
View File
@@ -13,6 +13,7 @@ class TextCall {
void perform();
TextCall &set_value(const std::string &value);
TextCall &set_value(const char *value, size_t len);
protected:
Text *const parent_;
+63 -5
View File
@@ -1,6 +1,10 @@
from importlib import resources
import logging
from aioesphomeapi.posix_tz import (
DSTRuleType as PyDSTRuleType,
parse_posix_tz as parse_posix_tz_python,
)
import tzlocal
from esphome import automation
@@ -39,6 +43,19 @@ CronTrigger = time_ns.class_("CronTrigger", automation.Trigger.template(), cg.Co
SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Component)
TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition)
# C++ types for pre-parsed timezone struct generation
DSTRuleType_cpp = time_ns.enum("DSTRuleType", is_class=True)
DSTRule_cpp = time_ns.struct("DSTRule")
ParsedTimezone_cpp = time_ns.struct("ParsedTimezone")
# Map Python DSTRuleType enum values to C++ enum expressions
_DST_RULE_TYPE_MAP = {
PyDSTRuleType.NONE: DSTRuleType_cpp.NONE,
PyDSTRuleType.MONTH_WEEK_DAY: DSTRuleType_cpp.MONTH_WEEK_DAY,
PyDSTRuleType.JULIAN_NO_LEAP: DSTRuleType_cpp.JULIAN_NO_LEAP,
PyDSTRuleType.DAY_OF_YEAR: DSTRuleType_cpp.DAY_OF_YEAR,
}
def _load_tzdata(iana_key: str) -> bytes | None:
# From https://tzdata.readthedocs.io/en/latest/#examples
@@ -260,11 +277,17 @@ def validate_tz(value: str) -> str:
value = cv.string_strict(value)
tzfile = _load_tzdata(value)
if tzfile is None:
# Not a IANA key, probably a TZ string
return value
if tzfile is not None:
value = _extract_tz_string(tzfile)
return _extract_tz_string(tzfile)
# Validate that the POSIX TZ string is parseable (skip empty strings)
if value:
try:
parse_posix_tz_python(value)
except ValueError as e:
raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e
return value
TIME_SCHEMA = cv.Schema(
@@ -305,11 +328,46 @@ TIME_SCHEMA = cv.Schema(
).extend(cv.polling_component_schema("15min"))
def _emit_dst_rule_fields(prefix, rule):
"""Emit field-by-field assignments for a DSTRule to avoid rodata struct blob."""
cg.add(cg.RawExpression(f"{prefix}.time_seconds = {rule.time_seconds}"))
cg.add(cg.RawExpression(f"{prefix}.day = {rule.day}"))
cg.add(cg.RawExpression(f"{prefix}.type = {_DST_RULE_TYPE_MAP[rule.type]}"))
cg.add(cg.RawExpression(f"{prefix}.month = {rule.month}"))
cg.add(cg.RawExpression(f"{prefix}.week = {rule.week}"))
cg.add(cg.RawExpression(f"{prefix}.day_of_week = {rule.day_of_week}"))
def _emit_parsed_timezone_fields(parsed):
"""Emit field-by-field assignments for a local ParsedTimezone, then set_global_tz().
Uses individual assignments on a stack variable instead of a struct initializer
to keep constants as immediate operands in instructions (.irom0.text/flash)
rather than a const blob in .rodata (which maps to RAM on ESP8266).
Wrapped in a scope block to allow multiple time platforms in the same build.
"""
cg.add(cg.RawStatement("{"))
cg.add(cg.RawExpression("time::ParsedTimezone tz{}"))
cg.add(cg.RawExpression(f"tz.std_offset_seconds = {parsed.std_offset_seconds}"))
cg.add(cg.RawExpression(f"tz.dst_offset_seconds = {parsed.dst_offset_seconds}"))
_emit_dst_rule_fields("tz.dst_start", parsed.dst_start)
_emit_dst_rule_fields("tz.dst_end", parsed.dst_end)
cg.add(time_ns.set_global_tz(cg.RawExpression("tz")))
cg.add(cg.RawStatement("}"))
async def setup_time_core_(time_var, config):
if timezone := config.get(CONF_TIMEZONE):
cg.add(time_var.set_timezone(timezone))
cg.add_define("USE_TIME_TIMEZONE")
if CORE.is_host:
# Host platform needs setenv("TZ")/tzset() for libc compatibility
cg.add(time_var.set_timezone(timezone))
else:
# Embedded: pre-parse at codegen time, emit struct directly
parsed = parse_posix_tz_python(timezone)
_emit_parsed_timezone_fields(parsed)
for conf in config.get(CONF_ON_TIME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var)
+11 -37
View File
@@ -1,4 +1,3 @@
from dataclasses import dataclass
from logging import getLogger
import math
import re
@@ -12,6 +11,7 @@ from esphome.const import (
CONF_BAUD_RATE,
CONF_BYTES,
CONF_DATA,
CONF_DATA_BITS,
CONF_DEBUG,
CONF_DELIMITER,
CONF_DIRECTION,
@@ -21,10 +21,12 @@ from esphome.const import (
CONF_ID,
CONF_LAMBDA,
CONF_NUMBER,
CONF_PARITY,
CONF_PORT,
CONF_RX_BUFFER_SIZE,
CONF_RX_PIN,
CONF_SEQUENCE,
CONF_STOP_BITS,
CONF_TIMEOUT,
CONF_TRIGGER_ID,
CONF_TX_PIN,
@@ -114,38 +116,6 @@ MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
@dataclass
class UARTData:
"""State data for UART component configuration generation."""
wake_loop_on_rx: bool = False
def _get_data() -> UARTData:
"""Get UART component data from CORE.data."""
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = UARTData()
return CORE.data[DOMAIN]
def request_wake_loop_on_rx() -> None:
"""Request that the UART wake the main loop when data is received.
Components that need low-latency notification of incoming UART data
should call this function during their code generation.
This enables the RX event task which wakes the main loop when data arrives.
"""
data = _get_data()
if not data.wake_loop_on_rx:
data.wake_loop_on_rx = True
# UART RX event task uses wake_loop_threadsafe() to notify the main loop
# Automatically enable the socket wake infrastructure when RX wake is requested
from esphome.components import socket
socket.require_wake_loop_threadsafe()
def validate_raw_data(value):
if isinstance(value, str):
return value.encode("utf-8")
@@ -215,9 +185,6 @@ UART_PARITY_OPTIONS = {
"ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD,
}
CONF_STOP_BITS = "stop_bits"
CONF_DATA_BITS = "data_bits"
CONF_PARITY = "parity"
CONF_RX_FULL_THRESHOLD = "rx_full_threshold"
CONF_RX_TIMEOUT = "rx_timeout"
@@ -542,7 +509,14 @@ async def uart_write_to_code(config, action_id, template_arg, args):
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure optional UART features."""
if _get_data().wake_loop_on_rx:
if CORE.is_esp32 and CORE.has_networking:
# Wake-on-RX is essentially free on ESP32 (just an ISR function pointer
# registration) — enable by default to reduce RX buffer overflow risk
# by waking the main loop immediately when data arrives.
# Requires networking for the wake_loop_isrsafe() infrastructure.
from esphome.components import socket
socket.require_wake_loop_threadsafe()
cg.add_define("USE_UART_WAKE_LOOP_ON_RX")
@@ -2,7 +2,6 @@
#include "uart_component_esp_idf.h"
#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -10,6 +9,10 @@
#include "driver/gpio.h"
#include "soc/gpio_num.h"
#ifdef USE_UART_WAKE_LOOP_ON_RX
#include "esphome/core/application.h"
#endif
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
@@ -107,12 +110,6 @@ void IDFUARTComponent::load_settings(bool dump_config) {
esp_err_t err;
if (uart_is_driver_installed(this->uart_num_)) {
#ifdef USE_UART_WAKE_LOOP_ON_RX
if (this->rx_event_task_handle_ != nullptr) {
vTaskDelete(this->rx_event_task_handle_);
this->rx_event_task_handle_ = nullptr;
}
#endif
err = uart_driver_delete(this->uart_num_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err));
@@ -120,20 +117,13 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
}
#ifdef USE_UART_WAKE_LOOP_ON_RX
constexpr int event_queue_size = 20;
QueueHandle_t *event_queue_ptr = &this->uart_event_queue_;
#else
constexpr int event_queue_size = 0;
QueueHandle_t *event_queue_ptr = nullptr;
#endif
err = uart_driver_install(this->uart_num_, // UART number
this->rx_buffer_size_, // RX ring buffer size
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
// block task until all data has been sent out
event_queue_size, // event queue size/depth
event_queue_ptr, // event queue
0 // Flags used to allocate the interrupt
0, // event queue size/depth
nullptr, // event queue
0 // Flags used to allocate the interrupt
);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
@@ -213,8 +203,10 @@ void IDFUARTComponent::load_settings(bool dump_config) {
}
#ifdef USE_UART_WAKE_LOOP_ON_RX
// Start the RX event task to enable low-latency data notifications
this->start_rx_event_task_();
// Register ISR callback to wake the main loop when UART data arrives.
// The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to
// wake the main loop task directly — no queue or FreeRTOS task needed.
uart_set_select_notif_callback(this->uart_num_, IDFUARTComponent::uart_rx_isr_callback);
#endif // USE_UART_WAKE_LOOP_ON_RX
if (dump_config) {
@@ -345,71 +337,12 @@ void IDFUARTComponent::flush() {
void IDFUARTComponent::check_logger_conflict() {}
#ifdef USE_UART_WAKE_LOOP_ON_RX
void IDFUARTComponent::start_rx_event_task_() {
// Create FreeRTOS task to monitor UART events
BaseType_t result = xTaskCreate(rx_event_task_func, // Task function
"uart_rx_evt", // Task name (max 16 chars)
2240, // Stack size in bytes (~2.2KB); increase if needed for logging
this, // Task parameter (this pointer)
tskIDLE_PRIORITY + 1, // Priority (low, just above idle)
&this->rx_event_task_handle_ // Task handle
);
if (result != pdPASS) {
ESP_LOGE(TAG, "Failed to create RX event task");
return;
}
ESP_LOGV(TAG, "RX event task started");
}
// FreeRTOS task that relays UART ISR events to the main loop.
// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a
// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline.
// IMPORTANT: This task must NOT call any UART wrapper methods (read_array,
// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading
// is done by the main loop. This task only reads from the event queue and
// calls App.wake_loop_threadsafe().
void IDFUARTComponent::rx_event_task_func(void *param) {
auto *self = static_cast<IDFUARTComponent *>(param);
uart_event_t event;
ESP_LOGV(TAG, "RX event task running");
// Run forever - task lifecycle matches component lifecycle
while (true) {
// Wait for UART events (blocks efficiently)
if (xQueueReceive(self->uart_event_queue_, &event, portMAX_DELAY) == pdTRUE) {
switch (event.type) {
case UART_DATA:
// Data available in UART RX buffer - wake the main loop
ESP_LOGVV(TAG, "Data event: %d bytes", event.size);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
break;
case UART_FIFO_OVF:
case UART_BUFFER_FULL:
// Don't call uart_flush_input() here — this task does not own the read side.
// ESP-IDF examples flush on overflow because the same task handles both events
// and reads, so flush and read are serialized. Here, reads happen on the main
// loop, so flushing from this task races with read_array() and can destroy data
// mid-read. The driver self-heals without an explicit flush: uart_read_bytes()
// calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes
// into the ring buffer and re-enables RX interrupts once space is freed.
ESP_LOGW(TAG, "FIFO overflow or ring buffer full");
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
break;
default:
// Ignore other event types
ESP_LOGVV(TAG, "Event type: %d", event.type);
break;
}
}
// ISR callback invoked by the ESP-IDF UART driver when data arrives.
// Wakes the main loop directly via vTaskNotifyGiveFromISR() — no queue or task needed.
void IRAM_ATTR IDFUARTComponent::uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif,
BaseType_t *task_woken) {
if (uart_select_notif == UART_SELECT_READ_NOTIF) {
Application::wake_loop_isrsafe(task_woken);
}
}
#endif // USE_UART_WAKE_LOOP_ON_RX
@@ -5,6 +5,9 @@
#include <driver/uart.h>
#include "esphome/core/component.h"
#include "uart_component.h"
#ifdef USE_UART_WAKE_LOOP_ON_RX
#include <driver/uart_select.h>
#endif
namespace esphome::uart {
@@ -12,9 +15,7 @@ namespace esphome::uart {
///
/// Thread safety: All public methods must only be called from the main loop.
/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's
/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task
/// (when enabled) must not call any of these methods — it communicates with the
/// main loop exclusively via App.wake_loop_threadsafe().
/// peek byte state (has_peek_/peek_byte_) is not synchronized.
class IDFUARTComponent : public UARTComponent, public Component {
public:
void setup() override;
@@ -33,9 +34,6 @@ class IDFUARTComponent : public UARTComponent, public Component {
void flush() override;
uint8_t get_hw_serial_number() { return this->uart_num_; }
#ifdef USE_UART_WAKE_LOOP_ON_RX
QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; }
#endif
/**
* Load the UART with the current settings.
@@ -61,15 +59,8 @@ class IDFUARTComponent : public UARTComponent, public Component {
uint8_t peek_byte_;
#ifdef USE_UART_WAKE_LOOP_ON_RX
// RX notification support — runs on a separate FreeRTOS task.
// IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array,
// write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the
// event queue and call App.wake_loop_threadsafe().
void start_rx_event_task_();
static void rx_event_task_func(void *param);
QueueHandle_t uart_event_queue_;
TaskHandle_t rx_event_task_handle_{nullptr};
// ISR callback for UART RX data notification — wakes the main loop directly.
static void uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif, BaseType_t *task_woken);
#endif // USE_UART_WAKE_LOOP_ON_RX
};
+4 -6
View File
@@ -1,20 +1,18 @@
import esphome.codegen as cg
from esphome.components import socket
from esphome.components.uart import (
CONF_DATA_BITS,
CONF_PARITY,
CONF_STOP_BITS,
UARTComponent,
)
from esphome.components.uart import UARTComponent
from esphome.components.usb_host import register_usb_client, usb_device_schema
import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_BUFFER_SIZE,
CONF_CHANNELS,
CONF_DATA_BITS,
CONF_DEBUG,
CONF_DUMMY_RECEIVER,
CONF_ID,
CONF_PARITY,
CONF_STOP_BITS,
)
from esphome.cpp_types import Component
@@ -5,6 +5,7 @@
#include "esphome/core/progmem.h"
#include <cmath>
#include <cstring>
namespace esphome::water_heater {
@@ -23,23 +24,25 @@ WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) {
return *this;
}
WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) {
if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("OFF")) == 0) {
WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) { return this->set_mode(mode, strlen(mode)); }
WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode, size_t len) {
if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("OFF"), 3) == 0) {
this->set_mode(WATER_HEATER_MODE_OFF);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ECO")) == 0) {
} else if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("ECO"), 3) == 0) {
this->set_mode(WATER_HEATER_MODE_ECO);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ELECTRIC")) == 0) {
} else if (len == 8 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("ELECTRIC"), 8) == 0) {
this->set_mode(WATER_HEATER_MODE_ELECTRIC);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE")) == 0) {
} else if (len == 11 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE"), 11) == 0) {
this->set_mode(WATER_HEATER_MODE_PERFORMANCE);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND")) == 0) {
} else if (len == 11 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND"), 11) == 0) {
this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP")) == 0) {
} else if (len == 9 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP"), 9) == 0) {
this->set_mode(WATER_HEATER_MODE_HEAT_PUMP);
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("GAS")) == 0) {
} else if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("GAS"), 3) == 0) {
this->set_mode(WATER_HEATER_MODE_GAS);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode);
ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode);
}
return *this;
}
@@ -76,7 +76,8 @@ class WaterHeaterCall {
WaterHeaterCall &set_mode(WaterHeaterMode mode);
WaterHeaterCall &set_mode(const char *mode);
WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str()); }
WaterHeaterCall &set_mode(const char *mode, size_t len);
WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str(), mode.size()); }
WaterHeaterCall &set_target_temperature(float temperature);
WaterHeaterCall &set_target_temperature_low(float temperature);
WaterHeaterCall &set_target_temperature_high(float temperature);
+29 -9
View File
@@ -969,7 +969,9 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
parse_light_param_uint_(request, ESPHOME_F("transition"), call, &decltype(call)::set_transition_length, 1000);
if (is_on) {
parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect);
parse_cstr_param_(
request, ESPHOME_F("effect"), call,
static_cast<light::LightCall &(light::LightCall::*) (const char *, size_t)>(&decltype(call)::set_effect));
}
DEFER_ACTION(call, call.perform());
@@ -1368,7 +1370,9 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
}
auto call = obj->make_call();
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
parse_cstr_param_(
request, ESPHOME_F("value"), call,
static_cast<text::TextCall &(text::TextCall::*) (const char *, size_t)>(&decltype(call)::set_value));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1426,7 +1430,9 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
}
auto call = obj->make_call();
parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option);
parse_cstr_param_(
request, ESPHOME_F("option"), call,
static_cast<select::SelectCall &(select::SelectCall::*) (const char *, size_t)>(&decltype(call)::set_option));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1487,10 +1493,18 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
auto call = obj->make_call();
// Parse string mode parameters
parse_string_param_(request, ESPHOME_F("mode"), call, &decltype(call)::set_mode);
parse_string_param_(request, ESPHOME_F("fan_mode"), call, &decltype(call)::set_fan_mode);
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
parse_string_param_(request, ESPHOME_F("preset"), call, &decltype(call)::set_preset);
parse_cstr_param_(
request, ESPHOME_F("mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(&decltype(call)::set_mode));
parse_cstr_param_(request, ESPHOME_F("fan_mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
&decltype(call)::set_fan_mode));
parse_cstr_param_(request, ESPHOME_F("swing_mode"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
&decltype(call)::set_swing_mode));
parse_cstr_param_(request, ESPHOME_F("preset"), call,
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
&decltype(call)::set_preset));
// Parse temperature parameters
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
@@ -1804,7 +1818,10 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
}
auto call = obj->make_call();
parse_string_param_(request, ESPHOME_F("code"), call, &decltype(call)::set_code);
parse_cstr_param_(
request, ESPHOME_F("code"), call,
static_cast<alarm_control_panel::AlarmControlPanelCall &(
alarm_control_panel::AlarmControlPanelCall::*) (const char *, size_t)>(&decltype(call)::set_code));
// Lookup table for alarm control panel methods
static const struct {
@@ -1892,7 +1909,10 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
water_heater::WaterHeaterCall &base_call = call;
// Parse mode parameter
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
parse_cstr_param_(
request, ESPHOME_F("mode"), base_call,
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*) (const char *, size_t)>(
&water_heater::WaterHeaterCall::set_mode));
// Parse temperature parameters
parse_num_param_(request, ESPHOME_F("target_temperature"), base_call,
+4 -4
View File
@@ -533,13 +533,13 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
}
}
// Generic helper to parse and apply a string parameter
// Generic helper to parse and apply a string parameter using const char* setter (avoids std::string allocation)
template<typename T, typename Ret>
void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
Ret (T::*setter)(const std::string &)) {
void parse_cstr_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
Ret (T::*setter)(const char *, size_t)) {
if (request->hasArg(param_name)) {
const auto &value = request->arg(param_name);
(call.*setter)(std::string(value.c_str(), value.length()));
(call.*setter)(value.c_str(), value.length());
}
}
+3 -3
View File
@@ -4,21 +4,21 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_CHANNEL,
CONF_DATA_BITS,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_PARITY,
CONF_STOP_BITS,
)
CODEOWNERS = ["@DrCoolZic"]
AUTO_LOAD = ["uart"]
MULTI_CONF = True
CONF_DATA_BITS = "data_bits"
CONF_STOP_BITS = "stop_bits"
CONF_PARITY = "parity"
CONF_CRYSTAL = "crystal"
CONF_UART = "uart"
CONF_TEST_MODE = "test_mode"
+1 -1
View File
@@ -2121,7 +2121,7 @@ bool WiFiComponent::can_proceed() {
#endif
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
bool WiFiComponent::is_connected() {
bool WiFiComponent::is_connected() const {
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_;
}
+2 -2
View File
@@ -443,7 +443,7 @@ class WiFiComponent : public Component {
void set_reboot_timeout(uint32_t reboot_timeout);
bool is_connected();
bool is_connected() const;
void set_power_save_mode(WiFiPowerSaveMode power_save);
void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; }
@@ -677,7 +677,7 @@ class WiFiComponent : public Component {
bool wifi_apply_hostname_();
bool wifi_sta_connect_(const WiFiAP &ap);
void wifi_pre_setup_();
WiFiSTAConnectStatus wifi_sta_connect_status_();
WiFiSTAConnectStatus wifi_sta_connect_status_() const;
bool wifi_scan_start_(bool passive);
#ifdef USE_WIFI_AP
@@ -622,7 +622,7 @@ void WiFiComponent::wifi_pre_setup_() {
this->wifi_mode_(false, false);
}
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
station_status_t status = wifi_station_get_connect_status();
if (status == STATION_GOT_IP)
return WiFiSTAConnectStatus::CONNECTED;
@@ -921,7 +921,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
}
}
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
if (s_sta_connected && this->got_ipv4_address_) {
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
if (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT) {
@@ -634,7 +634,7 @@ void WiFiComponent::wifi_pre_setup_() {
// Make sure WiFi is in clean state before anything starts
this->wifi_mode_(false, false);
}
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
// Use state machine instead of querying WiFi.status() directly
// State is updated in main loop from queued events, ensuring thread safety
switch (s_sta_state) {
@@ -120,7 +120,7 @@ const char *get_disconnect_reason_str(uint8_t reason) {
return "UNKNOWN";
}
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
// Use cyw43_wifi_link_status instead of cyw43_tcpip_link_status because the Arduino
// framework's __wrap_cyw43_cb_tcpip_init is a no-op — the SDK's internal netif
// (cyw43_state.netif[]) is never initialized. cyw43_tcpip_link_status checks that netif's
+1
View File
@@ -112,6 +112,7 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None:
def zephyr_to_code(config):
cg.add_build_flag("-DUSE_ZEPHYR")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.set_cpp_standard("gnu++20")
# build is done by west so bypass board checking in platformio
cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards"))
+1
View File
@@ -61,6 +61,7 @@ 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; }
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
Mutex::Mutex() {
auto *mutex = new k_mutex();
@@ -41,6 +41,3 @@ async def to_code(config):
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add_define("USE_ZWAVE_PROXY")
# Request UART to wake the main loop when data arrives for low-latency processing
uart.request_wake_loop_on_rx()
+3
View File
@@ -280,6 +280,7 @@ CONF_CUSTOM_PRESETS = "custom_presets"
CONF_CYCLE = "cycle"
CONF_DALLAS_ID = "dallas_id"
CONF_DATA = "data"
CONF_DATA_BITS = "data_bits"
CONF_DATA_PIN = "data_pin"
CONF_DATA_PINS = "data_pins"
CONF_DATA_RATE = "data_rate"
@@ -759,6 +760,7 @@ CONF_PAGE_ID = "page_id"
CONF_PAGES = "pages"
CONF_PANASONIC = "panasonic"
CONF_PARAMETERS = "parameters"
CONF_PARITY = "parity"
CONF_PASSWORD = "password"
CONF_PATH = "path"
CONF_PATTERN = "pattern"
@@ -961,6 +963,7 @@ CONF_STEP_PIN = "step_pin"
CONF_STILL_THRESHOLD = "still_threshold"
CONF_STOP = "stop"
CONF_STOP_ACTION = "stop_action"
CONF_STOP_BITS = "stop_bits"
CONF_STORE_BASELINE = "store_baseline"
CONF_SUBNET = "subnet"
CONF_SUBSCRIBE_QOS = "subscribe_qos"
+8 -11
View File
@@ -79,7 +79,12 @@ static void insertion_sort_by_priority(Iterator first, Iterator last) {
}
}
void Application::register_component_(Component *comp) { this->components_.push_back(comp); }
void Application::register_component_impl_(Component *comp, bool has_loop) {
if (has_loop) {
comp->component_state_ |= COMPONENT_HAS_LOOP;
}
this->components_.push_back(comp);
}
void Application::setup() {
ESP_LOGI(TAG, "Running through setup()");
ESP_LOGV(TAG, "Sorting components by setup priority");
@@ -382,16 +387,8 @@ void Application::teardown_components(uint32_t timeout_ms) {
}
void Application::calculate_looping_components_() {
// Count total components that need looping
size_t total_looping = 0;
for (auto *obj : this->components_) {
if (obj->has_overridden_loop()) {
total_looping++;
}
}
// Initialize FixedVector with exact size - no reallocation possible
this->looping_components_.init(total_looping);
// FixedVector capacity was pre-initialized by codegen with the exact count
// of components that override loop(), computed at C++ compile time.
// Add all components with loop override that aren't already LOOP_DONE
// Some components (like logger) may call disable_loop() during initialization
+32 -3
View File
@@ -5,6 +5,7 @@
#include <limits>
#include <span>
#include <string>
#include <type_traits>
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
@@ -105,7 +106,10 @@
#endif
namespace esphome::socket {
class Socket;
#ifdef USE_SOCKET_SELECT_SUPPORT
/// Shared ready() helper for fd-based socket implementations.
bool socket_ready_fd(int fd, bool loop_monitored); // NOLINT(readability-redundant-declaration)
#endif
} // namespace esphome::socket
// Forward declarations for friend access from codegen-generated setup()
@@ -500,11 +504,29 @@ class Application {
/// On other platforms: uses UDP loopback socket
void wake_loop_threadsafe();
#endif
#ifdef USE_LWIP_FAST_SELECT
/// Wake the main event loop from an ISR.
/// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe.
/// Only available on platforms with fast select (ESP32, LibreTiny).
/// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed.
static void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) {
esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken);
}
#ifdef USE_ESP32
/// Wake the main event loop from any context (ISR, thread, or main loop).
/// Detects the calling context and uses the appropriate FreeRTOS API.
static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); }
#endif
#endif
#endif
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();
@@ -521,7 +543,14 @@ class Application {
#endif
#endif
void register_component_(Component *comp);
/// Register a component, detecting loop() override at compile time.
/// The template resolves &T::loop vs &Component::loop as a constexpr bool
/// and forwards it to register_component_impl_ which stores it in component_state_.
template<typename T> void register_component_(T *comp) {
this->register_component_impl_(comp, !std::is_same_v<decltype(&T::loop), decltype(&Component::loop)>);
}
void register_component_impl_(Component *comp, bool has_loop);
void calculate_looping_components_();
void add_looping_components_by_state_(bool match_loop_done);
+51 -5
View File
@@ -4,6 +4,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
#include <concepts>
#include <functional>
@@ -56,6 +57,16 @@ template<typename T, typename... X> class TemplatableValue {
this->static_str_ = str;
}
#ifdef USE_ESP8266
// On ESP8266, __FlashStringHelper* is a distinct type from const char*.
// ESPHOME_F(s) expands to F(s) which returns __FlashStringHelper* pointing to PROGMEM.
// Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions
// to access the PROGMEM pointer safely.
TemplatableValue(const __FlashStringHelper *str) requires std::same_as<T, std::string> : type_(FLASH_STRING) {
this->static_str_ = reinterpret_cast<const char *>(str);
}
#endif
template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) {
if constexpr (USE_HEAP_STORAGE) {
this->value_ = new T(std::move(value));
@@ -89,7 +100,7 @@ template<typename T, typename... X> class TemplatableValue {
this->f_ = new std::function<T(X...)>(*other.f_);
} else if (this->type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
} else if (this->type_ == STATIC_STRING) {
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
this->static_str_ = other.static_str_;
}
}
@@ -108,7 +119,7 @@ template<typename T, typename... X> class TemplatableValue {
other.f_ = nullptr;
} else if (this->type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
} else if (this->type_ == STATIC_STRING) {
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
this->static_str_ = other.static_str_;
}
other.type_ = NONE;
@@ -141,7 +152,7 @@ template<typename T, typename... X> class TemplatableValue {
} else if (this->type_ == LAMBDA) {
delete this->f_;
}
// STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
// STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
}
bool has_value() const { return this->type_ != NONE; }
@@ -165,6 +176,17 @@ template<typename T, typename... X> class TemplatableValue {
return std::string(this->static_str_);
}
__builtin_unreachable();
#ifdef USE_ESP8266
case FLASH_STRING:
// PROGMEM pointer — must use _P functions to access on ESP8266
if constexpr (std::same_as<T, std::string>) {
size_t len = strlen_P(this->static_str_);
std::string result(len, '\0');
memcpy_P(result.data(), this->static_str_, len);
return result;
}
__builtin_unreachable();
#endif
case NONE:
default:
return T{};
@@ -186,9 +208,12 @@ template<typename T, typename... X> class TemplatableValue {
}
/// Check if this holds a static string (const char* stored without allocation)
/// The pointer is always directly readable (RAM or flash-mapped).
/// Returns false for FLASH_STRING (PROGMEM on ESP8266, requires _P functions).
bool is_static_string() const { return this->type_ == STATIC_STRING; }
/// Get the static string pointer (only valid if is_static_string() returns true)
/// The pointer is always directly readable — FLASH_STRING uses a separate type.
const char *get_static_string() const { return this->static_str_; }
/// Check if the string value is empty without allocating (for std::string specialization).
@@ -200,6 +225,12 @@ template<typename T, typename... X> class TemplatableValue {
return true;
case STATIC_STRING:
return this->static_str_ == nullptr || this->static_str_[0] == '\0';
#ifdef USE_ESP8266
case FLASH_STRING:
// PROGMEM pointer — must use progmem_read_byte on ESP8266
return this->static_str_ == nullptr ||
progmem_read_byte(reinterpret_cast<const uint8_t *>(this->static_str_)) == '\0';
#endif
case VALUE:
return this->value_->empty();
default: // LAMBDA/STATELESS_LAMBDA - must call value()
@@ -209,8 +240,9 @@ template<typename T, typename... X> class TemplatableValue {
/// Get a StringRef to the string value without heap allocation when possible.
/// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
/// For FLASH_STRING (ESP8266 PROGMEM), copies to provided buffer via _P functions.
/// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
/// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used).
/// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used).
/// @param lambda_buf_size Size of the buffer.
/// @return StringRef pointing to the string data.
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
@@ -221,6 +253,19 @@ template<typename T, typename... X> class TemplatableValue {
if (this->static_str_ == nullptr)
return StringRef();
return StringRef(this->static_str_, strlen(this->static_str_));
#ifdef USE_ESP8266
case FLASH_STRING:
if (this->static_str_ == nullptr)
return StringRef();
{
// PROGMEM pointer — copy to buffer via _P functions
size_t len = strlen_P(this->static_str_);
size_t copy_len = std::min(len, lambda_buf_size - 1);
memcpy_P(lambda_buf, this->static_str_, copy_len);
lambda_buf[copy_len] = '\0';
return StringRef(lambda_buf, copy_len);
}
#endif
case VALUE:
return StringRef(this->value_->data(), this->value_->size());
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
@@ -239,6 +284,7 @@ template<typename T, typename... X> class TemplatableValue {
LAMBDA,
STATELESS_LAMBDA,
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms
} type_;
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
// For other types, store value inline as before.
@@ -247,7 +293,7 @@ template<typename T, typename... X> class TemplatableValue {
ValueStorage value_; // T for inline storage, T* for heap storage
std::function<T(X...)> *f_;
T (*stateless_f_)(X...);
const char *static_str_; // For STATIC_STRING type
const char *static_str_; // For STATIC_STRING and FLASH_STRING types
};
};

Some files were not shown because too many files have changed in this diff Show More