1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-12 14:53:49 +01:00

Merge branch 'dev' into jesserockz-2025-457

This commit is contained in:
J. Nick Koston
2025-10-07 21:12:50 -05:00
committed by GitHub
14 changed files with 241 additions and 286 deletions

View File

@@ -1 +1 @@
ab49c22900dd39c004623e450a1076b111d6741f31967a637ab6e0e3dd2e753e
049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f

View File

@@ -18,6 +18,17 @@ namespace esphome::api {
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
// Maximum message size limits to prevent OOM on constrained devices
// Handshake messages are limited to a small size for security
static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
// Data message limits vary by platform based on available memory
#ifdef USE_ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
#else
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
#endif
// Forward declaration
struct ClientInfo;

View File

@@ -132,26 +132,16 @@ APIError APINoiseFrameHelper::loop() {
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
/** Read a packet into the rx_buf_.
*
* @param frame: The struct to hold the frame information in.
* msg_start: points to the start of the payload - this pointer is only valid until the next
* try_receive_raw_ call
*
* @return 0 if a full packet is in rx_buf_
* @return -1 if error, check errno.
* @return APIError::OK if a full packet is in rx_buf_
*
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
* errno ENOMEM: Not enough memory for reading packet.
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
APIError APINoiseFrameHelper::try_read_frame_() {
// read header
if (rx_header_buf_len_ < 3) {
// no header information yet
@@ -178,16 +168,17 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
// read body
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) {
// for handshake message only permit up to 128 bytes
// 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;
if (msg_size > limit) {
state_ = State::FAILED;
HELPER_LOG("Bad packet len for handshake: %d", msg_size);
return APIError::BAD_HANDSHAKE_PACKET_LEN;
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;
}
// reserve space for body
if (rx_buf_.size() != msg_size) {
rx_buf_.resize(msg_size);
// Reserve space for body
if (this->rx_buf_.size() != msg_size) {
this->rx_buf_.resize(msg_size);
}
if (rx_buf_len_ < msg_size) {
@@ -205,12 +196,12 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
}
}
LOG_PACKET_RECEIVED(rx_buf_);
*frame = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_len_ = 0;
LOG_PACKET_RECEIVED(this->rx_buf_);
// Clear state for next frame (rx_buf_ still contains data for caller)
this->rx_buf_len_ = 0;
this->rx_header_buf_len_ = 0;
return APIError::OK;
}
@@ -232,18 +223,17 @@ APIError APINoiseFrameHelper::state_action_() {
}
if (state_ == State::CLIENT_HELLO) {
// waiting for client hello
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
aerr = this->try_read_frame_();
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = prologue_.size();
prologue_.resize(old_size + 2 + frame.size());
prologue_[old_size] = (uint8_t) (frame.size() >> 8);
prologue_[old_size + 1] = (uint8_t) frame.size();
std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
size_t old_size = this->prologue_.size();
this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
state_ = State::SERVER_HELLO;
}
@@ -285,24 +275,23 @@ APIError APINoiseFrameHelper::state_action_() {
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
aerr = this->try_read_frame_();
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
if (frame.empty()) {
if (this->rx_buf_.empty()) {
send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (frame[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", frame[0]);
} else if (this->rx_buf_[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) {
// Special handling for MAC failure
@@ -379,35 +368,33 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err;
APIError aerr;
aerr = state_action_();
APIError aerr = this->state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
if (this->state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
aerr = this->try_read_frame_();
if (aerr != APIError::OK)
return aerr;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.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);
if (decrypt_err != APIError::OK)
if (decrypt_err != APIError::OK) {
return decrypt_err;
}
uint16_t msg_size = mbuf.size;
uint8_t *msg_data = frame.data();
uint8_t *msg_data = this->rx_buf_.data();
if (msg_size < 4) {
state_ = State::FAILED;
this->state_ = State::FAILED;
HELPER_LOG("Bad data packet: size %d too short", msg_size);
return APIError::BAD_DATA_PACKET;
}
@@ -415,12 +402,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) {
state_ = State::FAILED;
this->state_ = State::FAILED;
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
return APIError::BAD_DATA_PACKET;
}
buffer->container = std::move(frame);
buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 4;
buffer->data_len = data_len;
buffer->type = type;

View File

@@ -28,7 +28,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
protected:
APIError state_action_();
APIError try_read_frame_(std::vector<uint8_t> *frame);
APIError try_read_frame_();
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError init_handshake_();
APIError check_handshake_finished_();

View File

@@ -47,21 +47,13 @@ APIError APIPlaintextFrameHelper::loop() {
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @param frame: The struct to hold the frame information in.
* msg: store the parsed frame in that struct
/** Read a packet into the rx_buf_.
*
* @return See APIError
*
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
APIError APIPlaintextFrameHelper::try_read_frame_() {
// read header
while (!rx_header_parsed_) {
// Now that we know when the socket is ready, we can read up to 3 bytes
@@ -123,10 +115,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
continue;
}
if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
MAX_MESSAGE_SIZE);
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();
@@ -150,9 +142,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
}
// header reading done
// reserve space for body
if (rx_buf_.size() != rx_header_parsed_len_) {
rx_buf_.resize(rx_header_parsed_len_);
// Reserve space for body
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
this->rx_buf_.resize(this->rx_header_parsed_len_);
}
if (rx_buf_len_ < rx_header_parsed_len_) {
@@ -170,24 +162,22 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
}
}
LOG_PACKET_RECEIVED(rx_buf_);
*frame = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_pos_ = 0;
rx_header_parsed_ = false;
LOG_PACKET_RECEIVED(this->rx_buf_);
// Clear state for next frame (rx_buf_ still contains data for caller)
this->rx_buf_len_ = 0;
this->rx_header_buf_pos_ = 0;
this->rx_header_parsed_ = false;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr;
if (state_ != State::DATA) {
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
if (this->state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
APIError aerr = this->try_read_frame_();
if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't
@@ -220,10 +210,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return aerr;
}
buffer->container = std::move(frame);
buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 0;
buffer->data_len = rx_header_parsed_len_;
buffer->type = rx_header_parsed_type_;
buffer->data_len = this->rx_header_parsed_len_;
buffer->type = this->rx_header_parsed_type_;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {

View File

@@ -24,7 +24,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
protected:
APIError try_read_frame_(std::vector<uint8_t> *frame);
APIError try_read_frame_();
// Group 2-byte aligned types
uint16_t rx_header_parsed_type_ = 0;

View File

@@ -165,4 +165,4 @@ def final_validate_audio_schema(
async def to_code(config):
cg.add_library("esphome/esp-audio-libs", "1.1.4")
cg.add_library("esphome/esp-audio-libs", "2.0.1")

View File

@@ -229,18 +229,18 @@ FileDecoderState AudioDecoder::decode_flac_() {
auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->available());
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::POTENTIALLY_FAILED;
}
if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) {
// Couldn't read FLAC header
if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
// Serrious error reading FLAC header, there is no recovery
return FileDecoderState::FAILED;
}
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::MORE_TO_PROCESS;
}
// Reallocate the output transfer buffer to the smallest necessary size
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
@@ -256,9 +256,9 @@ FileDecoderState AudioDecoder::decode_flac_() {
}
uint32_t output_samples = 0;
auto result = this->flac_decoder_->decode_frame(
this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(),
reinterpret_cast<int16_t *>(this->output_transfer_buffer_->get_buffer_end()), &output_samples);
auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->available(),
this->output_transfer_buffer_->get_buffer_end(), &output_samples);
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
// Not an issue, just needs more data that we'll get next time.

View File

@@ -42,32 +42,18 @@ ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) {
ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
ESPBTUUID ret;
if (data.length() == 4) {
ret.uuid_.len = ESP_UUID_LEN_16;
ret.uuid_.uuid.uuid16 = 0;
for (uint i = 0; i < data.length(); i += 2) {
uint8_t msb = data.c_str()[i];
uint8_t lsb = data.c_str()[i + 1];
uint8_t lsb_shift = i <= 2 ? (2 - i) * 4 : 0;
if (msb > '9')
msb -= 7;
if (lsb > '9')
lsb -= 7;
ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
// 16-bit UUID as 4-character hex string
auto parsed = parse_hex<uint16_t>(data);
if (parsed.has_value()) {
ret.uuid_.len = ESP_UUID_LEN_16;
ret.uuid_.uuid.uuid16 = parsed.value();
}
} else if (data.length() == 8) {
ret.uuid_.len = ESP_UUID_LEN_32;
ret.uuid_.uuid.uuid32 = 0;
for (uint i = 0; i < data.length(); i += 2) {
uint8_t msb = data.c_str()[i];
uint8_t lsb = data.c_str()[i + 1];
uint8_t lsb_shift = i <= 6 ? (6 - i) * 4 : 0;
if (msb > '9')
msb -= 7;
if (lsb > '9')
lsb -= 7;
ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
// 32-bit UUID as 8-character hex string
auto parsed = parse_hex<uint32_t>(data);
if (parsed.has_value()) {
ret.uuid_.len = ESP_UUID_LEN_32;
ret.uuid_.uuid.uuid32 = parsed.value();
}
} else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be
// investigated (lack of time)
@@ -145,28 +131,16 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
if (this->uuid_.len == uuid.uuid_.len) {
switch (this->uuid_.len) {
case ESP_UUID_LEN_16:
if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) {
return true;
}
break;
return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16;
case ESP_UUID_LEN_32:
if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) {
return true;
}
break;
return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32;
case ESP_UUID_LEN_128:
for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) {
if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) {
return false;
}
}
return true;
break;
return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0;
default:
return false;
}
} else {
return this->as_128bit() == uuid.as_128bit();
}
return false;
return this->as_128bit() == uuid.as_128bit();
}
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
std::string ESPBTUUID::to_string() const {

View File

@@ -10,11 +10,15 @@ namespace light {
static const char *const TAG = "light";
// Helper functions to reduce code size for logging
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
static void log_validation_warning(const char *name, const LogString *param_name, float val, float min, float max) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), val, min, max);
static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f,
float max = 1.0f) {
if (value < min || value > max) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max);
value = clamp(value, min, max);
}
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
static void log_feature_not_supported(const char *name, const LogString *feature) {
ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature));
}
@@ -27,7 +31,6 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message));
}
#else
#define log_validation_warning(name, param_name, val, min, max)
#define log_feature_not_supported(name, feature)
#define log_color_mode_not_supported(name, feature)
#define log_invalid_parameter(name, message)
@@ -44,7 +47,7 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
} \
LightCall &LightCall::set_##name(type name) { \
this->name##_ = name; \
this->set_flag_(flag, true); \
this->set_flag_(flag); \
return *this; \
}
@@ -181,6 +184,16 @@ void LightCall::perform() {
}
}
void LightCall::log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log) {
auto *name = this->parent_->get_name().c_str();
if (use_color_mode_log) {
log_color_mode_not_supported(name, feature);
} else {
log_feature_not_supported(name, feature);
}
this->clear_flag_(flag);
}
LightColorValues LightCall::validate_() {
auto *name = this->parent_->get_name().c_str();
auto traits = this->parent_->get_traits();
@@ -188,141 +201,108 @@ LightColorValues LightCall::validate_() {
// Color mode check
if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) {
ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_)));
this->set_flag_(FLAG_HAS_COLOR_MODE, false);
this->clear_flag_(FLAG_HAS_COLOR_MODE);
}
// Ensure there is always a color mode set
if (!this->has_color_mode()) {
this->color_mode_ = this->compute_color_mode_();
this->set_flag_(FLAG_HAS_COLOR_MODE, true);
this->set_flag_(FLAG_HAS_COLOR_MODE);
}
auto color_mode = this->color_mode_;
// Transform calls that use non-native parameters for the current mode.
this->transform_parameters_();
// Brightness exists check
if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
log_feature_not_supported(name, LOG_STR("brightness"));
this->set_flag_(FLAG_HAS_BRIGHTNESS, false);
}
// Transition length possible check
if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) {
log_feature_not_supported(name, LOG_STR("transitions"));
this->set_flag_(FLAG_HAS_TRANSITION, false);
}
// Color brightness exists check
if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
log_color_mode_not_supported(name, LOG_STR("RGB brightness"));
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false);
}
// RGB exists check
if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
(this->has_blue() && this->blue_ > 0.0f)) {
if (!(color_mode & ColorCapability::RGB)) {
log_color_mode_not_supported(name, LOG_STR("RGB color"));
this->set_flag_(FLAG_HAS_RED, false);
this->set_flag_(FLAG_HAS_GREEN, false);
this->set_flag_(FLAG_HAS_BLUE, false);
}
}
// White value exists check
if (this->has_white() && this->white_ > 0.0f &&
!(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("white value"));
this->set_flag_(FLAG_HAS_WHITE, false);
}
// Color temperature exists check
if (this->has_color_temperature() &&
!(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("color temperature"));
this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false);
}
// Cold/warm white value exists check
if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) {
if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("cold/warm white value"));
this->set_flag_(FLAG_HAS_COLD_WHITE, false);
this->set_flag_(FLAG_HAS_WARM_WHITE, false);
}
}
#define VALIDATE_RANGE_(name_, upper_name, min, max) \
if (this->has_##name_()) { \
auto val = this->name_##_; \
if (val < (min) || val > (max)) { \
log_validation_warning(name, LOG_STR(upper_name), val, (min), (max)); \
this->name_##_ = clamp(val, (min), (max)); \
} \
}
#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f)
// Range checks
VALIDATE_RANGE(brightness, "Brightness")
VALIDATE_RANGE(color_brightness, "Color brightness")
VALIDATE_RANGE(red, "Red")
VALIDATE_RANGE(green, "Green")
VALIDATE_RANGE(blue, "Blue")
VALIDATE_RANGE(white, "White")
VALIDATE_RANGE(cold_white, "Cold white")
VALIDATE_RANGE(warm_white, "Warm white")
VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
// Business logic adjustments before validation
// Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
bool explicit_turn_off_request = this->has_state() && !this->state_;
// Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
if (this->has_brightness() && this->brightness_ == 0.0f) {
this->state_ = false;
this->set_flag_(FLAG_HAS_STATE, true);
this->set_flag_(FLAG_HAS_STATE);
this->brightness_ = 1.0f;
}
// Set color brightness to 100% if currently zero and a color is set.
if (this->has_red() || this->has_green() || this->has_blue()) {
if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) {
this->color_brightness_ = 1.0f;
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true);
}
if ((this->has_red() || this->has_green() || this->has_blue()) && !this->has_color_brightness() &&
this->parent_->remote_values.get_color_brightness() == 0.0f) {
this->color_brightness_ = 1.0f;
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS);
}
// Create color values for the light with this call applied.
// Capability validation
if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS))
this->log_and_clear_unsupported_(FLAG_HAS_BRIGHTNESS, LOG_STR("brightness"), false);
// Transition length possible check
if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS))
this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false);
if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB))
this->log_and_clear_unsupported_(FLAG_HAS_COLOR_BRIGHTNESS, LOG_STR("RGB brightness"), true);
// RGB exists check
if (((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
(this->has_blue() && this->blue_ > 0.0f)) &&
!(color_mode & ColorCapability::RGB)) {
log_color_mode_not_supported(name, LOG_STR("RGB color"));
this->clear_flag_(FLAG_HAS_RED);
this->clear_flag_(FLAG_HAS_GREEN);
this->clear_flag_(FLAG_HAS_BLUE);
}
// White value exists check
if (this->has_white() && this->white_ > 0.0f &&
!(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE))
this->log_and_clear_unsupported_(FLAG_HAS_WHITE, LOG_STR("white value"), true);
// Color temperature exists check
if (this->has_color_temperature() &&
!(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE))
this->log_and_clear_unsupported_(FLAG_HAS_COLOR_TEMPERATURE, LOG_STR("color temperature"), true);
// Cold/warm white value exists check
if (((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) &&
!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("cold/warm white value"));
this->clear_flag_(FLAG_HAS_COLD_WHITE);
this->clear_flag_(FLAG_HAS_WARM_WHITE);
}
// Create color values and validate+apply ranges in one step to eliminate duplicate checks
auto v = this->parent_->remote_values;
if (this->has_color_mode())
v.set_color_mode(this->color_mode_);
if (this->has_state())
v.set_state(this->state_);
if (this->has_brightness())
v.set_brightness(this->brightness_);
if (this->has_color_brightness())
v.set_color_brightness(this->color_brightness_);
if (this->has_red())
v.set_red(this->red_);
if (this->has_green())
v.set_green(this->green_);
if (this->has_blue())
v.set_blue(this->blue_);
if (this->has_white())
v.set_white(this->white_);
if (this->has_color_temperature())
v.set_color_temperature(this->color_temperature_);
if (this->has_cold_white())
v.set_cold_white(this->cold_white_);
if (this->has_warm_white())
v.set_warm_white(this->warm_white_);
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
if (this->has_##field()) { \
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
v.setter(this->field##_); \
}
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
VALIDATE_AND_APPLY(red, set_red, "Red")
VALIDATE_AND_APPLY(green, set_green, "Green")
VALIDATE_AND_APPLY(blue, set_blue, "Blue")
VALIDATE_AND_APPLY(white, set_white, "White")
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
traits.get_max_mireds())
#undef VALIDATE_AND_APPLY
v.normalize_color();
// Flash length check
if (this->has_flash_() && this->flash_length_ == 0) {
log_invalid_parameter(name, LOG_STR("flash length must be greater than zero"));
this->set_flag_(FLAG_HAS_FLASH, false);
log_invalid_parameter(name, LOG_STR("flash length must be >0"));
this->clear_flag_(FLAG_HAS_FLASH);
}
// validate transition length/flash length/effect not used at the same time
@@ -330,42 +310,40 @@ LightColorValues LightCall::validate_() {
// If effect is already active, remove effect start
if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) {
this->set_flag_(FLAG_HAS_EFFECT, false);
this->clear_flag_(FLAG_HAS_EFFECT);
}
// validate effect index
if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) {
ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_);
this->set_flag_(FLAG_HAS_EFFECT, false);
this->clear_flag_(FLAG_HAS_EFFECT);
}
if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) {
log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash"));
this->set_flag_(FLAG_HAS_TRANSITION, false);
this->set_flag_(FLAG_HAS_FLASH, false);
this->clear_flag_(FLAG_HAS_TRANSITION);
this->clear_flag_(FLAG_HAS_FLASH);
}
if (this->has_flash_() && this->has_transition_()) {
log_invalid_parameter(name, LOG_STR("flash cannot be used with transition"));
this->set_flag_(FLAG_HAS_TRANSITION, false);
this->clear_flag_(FLAG_HAS_TRANSITION);
}
if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) &&
supports_transition) {
// nothing specified and light supports transitions, set default transition length
this->transition_length_ = this->parent_->default_transition_length_;
this->set_flag_(FLAG_HAS_TRANSITION, true);
this->set_flag_(FLAG_HAS_TRANSITION);
}
if (this->has_transition_() && this->transition_length_ == 0) {
// 0 transition is interpreted as no transition (instant change)
this->set_flag_(FLAG_HAS_TRANSITION, false);
this->clear_flag_(FLAG_HAS_TRANSITION);
}
if (this->has_transition_() && !supports_transition) {
log_feature_not_supported(name, LOG_STR("transitions"));
this->set_flag_(FLAG_HAS_TRANSITION, false);
}
if (this->has_transition_() && !supports_transition)
this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false);
// If not a flash and turning the light off, then disable the light
// Do not use light color values directly, so that effects can set 0% brightness
@@ -374,17 +352,17 @@ LightColorValues LightCall::validate_() {
if (!this->has_flash_() && !target_state) {
if (this->has_effect_()) {
log_invalid_parameter(name, LOG_STR("cannot start effect when turning off"));
this->set_flag_(FLAG_HAS_EFFECT, false);
this->clear_flag_(FLAG_HAS_EFFECT);
} else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) {
// Auto turn off effect
this->effect_ = 0;
this->set_flag_(FLAG_HAS_EFFECT, true);
this->set_flag_(FLAG_HAS_EFFECT);
}
}
// Disable saving for flashes
if (this->has_flash_())
this->set_flag_(FLAG_SAVE, false);
this->clear_flag_(FLAG_SAVE);
return v;
}
@@ -418,12 +396,12 @@ void LightCall::transform_parameters_() {
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->set_flag_(FLAG_HAS_COLD_WHITE, true);
this->set_flag_(FLAG_HAS_WARM_WHITE, true);
this->set_flag_(FLAG_HAS_COLD_WHITE);
this->set_flag_(FLAG_HAS_WARM_WHITE);
}
if (this->has_white()) {
this->brightness_ = this->white_;
this->set_flag_(FLAG_HAS_BRIGHTNESS, true);
this->set_flag_(FLAG_HAS_BRIGHTNESS);
}
}
}
@@ -630,7 +608,7 @@ LightCall &LightCall::set_effect(optional<std::string> effect) {
}
LightCall &LightCall::set_effect(uint32_t effect_number) {
this->effect_ = effect_number;
this->set_flag_(FLAG_HAS_EFFECT, true);
this->set_flag_(FLAG_HAS_EFFECT);
return *this;
}
LightCall &LightCall::set_effect(optional<uint32_t> effect_number) {

View File

@@ -4,6 +4,10 @@
#include <set>
namespace esphome {
// Forward declaration
struct LogString;
namespace light {
class LightState;
@@ -207,14 +211,14 @@ class LightCall {
FLAG_SAVE = 1 << 15,
};
bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
inline bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
inline bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
// Helper to set flag
void set_flag_(FieldFlags flag, bool value) {
// Helper to set flag - defaults to true for common case
void set_flag_(FieldFlags flag, bool value = true) {
if (value) {
this->flags_ |= flag;
} else {
@@ -222,6 +226,12 @@ class LightCall {
}
}
// Helper to clear flag - reduces code size for common case
void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; }
// Helper to log unsupported feature and clear flag - reduces code duplication
void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log);
LightState *parent_;
// Light state values - use flags_ to check if a value has been set.

View File

@@ -74,6 +74,9 @@ FILTER_PLATFORMIO_LINES = [
r"Creating BIN file .*",
r"Warning! Could not find file \".*.crt\"",
r"Warning! Arduino framework as an ESP-IDF component doesn't handle the `variant` field! The default `esp32` variant will be used.",
r"Warning: DEPRECATED: 'esptool.py' is deprecated. Please use 'esptool' instead. The '.py' suffix will be removed in a future major release.",
r"Warning: esp-idf-size exited with code 2",
r"esp_idf_size: error: unrecognized arguments: --ng",
]

View File

@@ -147,7 +147,7 @@ lib_deps =
makuna/NeoPixelBus@2.8.0 ; neopixelbus
esphome/ESP32-audioI2S@2.3.0 ; i2s_audio
droscy/esp_wireguard@0.4.2 ; wireguard
esphome/esp-audio-libs@1.1.4 ; audio
esphome/esp-audio-libs@2.0.1 ; audio
build_flags =
${common:arduino.build_flags}
@@ -170,7 +170,7 @@ lib_deps =
${common:idf.lib_deps}
droscy/esp_wireguard@0.4.2 ; wireguard
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
esphome/esp-audio-libs@1.1.4 ; audio
esphome/esp-audio-libs@2.0.1 ; audio
build_flags =
${common:idf.build_flags}
-Wno-nonnull-compare

View File

@@ -15,7 +15,7 @@ async def test_oversized_payload_plaintext(
run_compiled: RunCompiledFunction,
api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
"""Test that oversized payloads (>100KiB) from client cause disconnection without crashing."""
"""Test that oversized payloads (>32768 bytes) from client cause disconnection without crashing."""
process_exited = False
helper_log_found = False
@@ -39,8 +39,8 @@ async def test_oversized_payload_plaintext(
assert device_info is not None
assert device_info.name == "oversized-plaintext"
# Create an oversized payload (>100KiB)
oversized_data = b"X" * (100 * 1024 + 1) # 100KiB + 1 byte
# Create an oversized payload (>32768 bytes which is our new limit)
oversized_data = b"X" * 40000 # ~40KiB, exceeds the 32768 byte limit
# Access the internal connection to send raw data
frame_helper = client._connection._frame_helper
@@ -132,22 +132,24 @@ async def test_oversized_payload_noise(
run_compiled: RunCompiledFunction,
api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
"""Test that oversized payloads (>100KiB) from client cause disconnection without crashing with noise encryption."""
"""Test that oversized payloads from client cause disconnection without crashing with noise encryption."""
noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU="
process_exited = False
cipherstate_failed = False
helper_log_found = False
def check_logs(line: str) -> None:
nonlocal process_exited, cipherstate_failed
nonlocal process_exited, helper_log_found
# Check for signs that the process exited/crashed
if "Segmentation fault" in line or "core dumped" in line:
process_exited = True
# Check for the expected warning about decryption failure
# Check for HELPER_LOG message about message size exceeding maximum
# With our new protection, oversized messages are rejected at frame level
if (
"[W][api.connection" in line
and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line
"[VV]" in line
and "Bad packet: message size" in line
and "exceeds maximum" in line
):
cipherstate_failed = True
helper_log_found = True
async with run_compiled(yaml_config, line_callback=check_logs):
async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
@@ -159,8 +161,8 @@ async def test_oversized_payload_noise(
assert device_info is not None
assert device_info.name == "oversized-noise"
# Create an oversized payload (>100KiB)
oversized_data = b"Y" * (100 * 1024 + 1) # 100KiB + 1 byte
# Create an oversized payload (>32768 bytes which is our new limit)
oversized_data = b"Y" * 40000 # ~40KiB, exceeds the 32768 byte limit
# Access the internal connection to send raw data
frame_helper = client._connection._frame_helper
@@ -175,9 +177,9 @@ async def test_oversized_payload_noise(
# After disconnection, verify process didn't crash
assert not process_exited, "ESPHome process should not crash"
# Verify we saw the expected warning message
assert cipherstate_failed, (
"Expected to see warning about CIPHERSTATE_DECRYPT_FAILED"
# Verify we saw the expected HELPER_LOG message
assert helper_log_found, (
"Expected to see HELPER_LOG about message size exceeding maximum"
)
# Try to reconnect to verify the process is still running