1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-13 05:15:45 +00:00

Compare commits

..

2 Commits

Author SHA1 Message Date
J. Nick Koston
8a84895774 DNM: test ci for #11580 2025-10-28 19:19:43 -05:00
J. Nick Koston
b1166b916f [ci] Fix component tests not running when only test files change 2025-10-28 14:30:17 -05:00
65 changed files with 331 additions and 733 deletions

View File

@@ -731,13 +731,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
exit_code = write_cpp(config) exit_code = write_cpp(config)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
@@ -1199,17 +1192,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.", help="Only generate source code, do not compile.",
action="store_true", action="store_true",
) )
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser( parser_upload = subparsers.add_parser(
"upload", "upload",

View File

@@ -1,7 +1,6 @@
"""CLI interface for memory analysis with report generation.""" """CLI interface for memory analysis with report generation."""
from collections import defaultdict from collections import defaultdict
import json
import sys import sys
from . import ( from . import (
@@ -284,28 +283,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines) return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis.""" """Dump uncategorized symbols for analysis."""
# Sort by size descending # Sort by size descending

View File

@@ -425,7 +425,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9; bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11; EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12 [(container_pointer) = "std::vector"]; repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"];
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
} }
// Deprecated in API version 1.6 - only used in deprecated fields // Deprecated in API version 1.6 - only used in deprecated fields
@@ -989,7 +989,7 @@ message ListEntitiesClimateResponse {
bool supports_current_temperature = 5; // Deprecated: use feature_flags bool supports_current_temperature = 5; // Deprecated: use feature_flags
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"]; repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
float visual_min_temperature = 8; float visual_min_temperature = 8;
float visual_max_temperature = 9; float visual_max_temperature = 9;
float visual_target_temperature_step = 10; float visual_target_temperature_step = 10;
@@ -998,11 +998,11 @@ message ListEntitiesClimateResponse {
// Deprecated in API version 1.5 // Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true]; bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12; // Deprecated: use feature_flags bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"]; repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"];
repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"]; repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"];
bool disabled_by_default = 18; bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20; EntityCategory entity_category = 20;
@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
reserved 4; // Deprecated: was string unique_id reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"]; repeated string options = 6 [(container_pointer) = "std::vector"];
bool disabled_by_default = 7; bool disabled_by_default = 7;
EntityCategory entity_category = 8; EntityCategory entity_category = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];

View File

@@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
// Current feature flags and other supported parameters // Current feature flags and other supported parameters
msg.feature_flags = traits.get_feature_flags(); msg.feature_flags = traits.get_feature_flags();
msg.supported_modes = &traits.get_supported_modes(); msg.supported_modes = &traits.get_supported_modes_for_api_();
msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_min_temperature = traits.get_visual_min_temperature();
msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature();
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.supported_fan_modes = &traits.get_supported_fan_modes(); msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes(); msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
msg.supported_presets = &traits.get_supported_presets(); msg.supported_presets = &traits.get_supported_presets_for_api_();
msg.supported_custom_presets = &traits.get_supported_custom_presets(); msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_();
msg.supported_swing_modes = &traits.get_supported_swing_modes(); msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_();
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }

View File

@@ -142,11 +142,6 @@ APIError APINoiseFrameHelper::loop() {
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/ */
APIError APINoiseFrameHelper::try_read_frame_() { APIError APINoiseFrameHelper::try_read_frame_() {
// Clear buffer when starting a new frame (rx_buf_len_ == 0 means not resuming after WOULD_BLOCK)
if (this->rx_buf_len_ == 0) {
this->rx_buf_.clear();
}
// read header // read header
if (rx_header_buf_len_ < 3) { if (rx_header_buf_len_ < 3) {
// no header information yet // no header information yet

View File

@@ -54,11 +54,6 @@ APIError APIPlaintextFrameHelper::loop() {
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/ */
APIError APIPlaintextFrameHelper::try_read_frame_() { APIError APIPlaintextFrameHelper::try_read_frame_() {
// Clear buffer when starting a new frame (rx_buf_len_ == 0 means not resuming after WOULD_BLOCK)
if (this->rx_buf_len_ == 0) {
this->rx_buf_.clear();
}
// read header // read header
while (!rx_header_parsed_) { while (!rx_header_parsed_) {
// Now that we know when the socket is ready, we can read up to 3 bytes // Now that we know when the socket is ready, we can read up to 3 bytes

View File

@@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
buffer.encode_string(5, this->icon_ref_); buffer.encode_string(5, this->icon_ref_);
#endif #endif
for (const char *it : *this->options) { for (const auto &it : *this->options) {
buffer.encode_string(6, it, strlen(it), true); buffer.encode_string(6, it, true);
} }
buffer.encode_bool(7, this->disabled_by_default); buffer.encode_bool(7, this->disabled_by_default);
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
@@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->icon_ref_.size()); size.add_length(1, this->icon_ref_.size());
#endif #endif
if (!this->options->empty()) { if (!this->options->empty()) {
for (const char *it : *this->options) { for (const auto &it : *this->options) {
size.add_length_force(1, strlen(it)); size.add_length_force(1, it.size());
} }
} }
size.add_bool(1, this->disabled_by_default); size.add_bool(1, this->disabled_by_default);

View File

@@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
bool supports_speed{false}; bool supports_speed{false};
bool supports_direction{false}; bool supports_direction{false};
int32_t supported_speed_count{0}; int32_t supported_speed_count{0};
const std::vector<std::string> *supported_preset_modes{}; const std::set<std::string> *supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
#endif #endif
bool supports_current_temperature{false}; bool supports_current_temperature{false};
bool supports_two_point_target_temperature{false}; bool supports_two_point_target_temperature{false};
const climate::ClimateModeMask *supported_modes{}; const std::set<climate::ClimateMode> *supported_modes{};
float visual_min_temperature{0.0f}; float visual_min_temperature{0.0f};
float visual_max_temperature{0.0f}; float visual_max_temperature{0.0f};
float visual_target_temperature_step{0.0f}; float visual_target_temperature_step{0.0f};
bool supports_action{false}; bool supports_action{false};
const climate::ClimateFanModeMask *supported_fan_modes{}; const std::set<climate::ClimateFanMode> *supported_fan_modes{};
const climate::ClimateSwingModeMask *supported_swing_modes{}; const std::set<climate::ClimateSwingMode> *supported_swing_modes{};
const std::vector<std::string> *supported_custom_fan_modes{}; const std::set<std::string> *supported_custom_fan_modes{};
const climate::ClimatePresetMask *supported_presets{}; const std::set<climate::ClimatePreset> *supported_presets{};
const std::vector<std::string> *supported_custom_presets{}; const std::set<std::string> *supported_custom_presets{};
float visual_current_temperature_step{0.0f}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false}; bool supports_current_humidity{false};
bool supports_target_humidity{false}; bool supports_target_humidity{false};
@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_select_response"; } const char *message_name() const override { return "list_entities_select_response"; }
#endif #endif
const FixedVector<const char *> *options{}; const std::vector<std::string> *options{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -88,12 +88,6 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
out.append("\n"); out.append("\n");
} }
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append(value).append("'");
out.append("\n");
}
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
append_field_prefix(out, field_name, indent); append_field_prefix(out, field_name, indent);
out.append(proto_enum_to_string<T>(value)); out.append(proto_enum_to_string<T>(value));

View File

@@ -7,7 +7,6 @@
#include <cassert> #include <cassert>
#include <cstring> #include <cstring>
#include <type_traits>
#include <vector> #include <vector>
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
@@ -160,6 +159,22 @@ class ProtoVarInt {
} }
} }
} }
void encode(std::vector<uint8_t> &out) {
uint64_t val = this->value_;
if (val <= 0x7F) {
out.push_back(val);
return;
}
while (val) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
out.push_back(temp | 0x80);
} else {
out.push_back(temp);
}
}
}
protected: protected:
uint64_t value_; uint64_t value_;
@@ -218,86 +233,8 @@ class ProtoWriteBuffer {
public: public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {} ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
void write(uint8_t value) { this->buffer_->push_back(value); } void write(uint8_t value) { this->buffer_->push_back(value); }
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
// Single implementation that all overloads delegate to void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
void encode_varint(uint64_t value) {
auto buffer = this->buffer_;
size_t start = buffer->size();
// Fast paths for common cases (1-3 bytes)
if (value < (1ULL << 7)) {
// 1 byte - very common for field IDs and small lengths
buffer->resize(start + 1);
buffer->data()[start] = static_cast<uint8_t>(value);
return;
}
uint8_t *p;
if (value < (1ULL << 14)) {
// 2 bytes - common for medium field IDs and lengths
buffer->resize(start + 2);
p = buffer->data() + start;
p[0] = (value & 0x7F) | 0x80;
p[1] = (value >> 7) & 0x7F;
return;
}
if (value < (1ULL << 21)) {
// 3 bytes - rare
buffer->resize(start + 3);
p = buffer->data() + start;
p[0] = (value & 0x7F) | 0x80;
p[1] = ((value >> 7) & 0x7F) | 0x80;
p[2] = (value >> 14) & 0x7F;
return;
}
// Rare case: 4-10 byte values - calculate size from bit position
// Value is guaranteed >= (1ULL << 21), so CLZ is safe (non-zero)
uint32_t size;
#if defined(__GNUC__) || defined(__clang__)
// Use compiler intrinsic for efficient bit position lookup
size = (64 - __builtin_clzll(value) + 6) / 7;
#else
// Fallback for compilers without __builtin_clzll
if (value < (1ULL << 28)) {
size = 4;
} else if (value < (1ULL << 35)) {
size = 5;
} else if (value < (1ULL << 42)) {
size = 6;
} else if (value < (1ULL << 49)) {
size = 7;
} else if (value < (1ULL << 56)) {
size = 8;
} else if (value < (1ULL << 63)) {
size = 9;
} else {
size = 10;
}
#endif
buffer->resize(start + size);
p = buffer->data() + start;
size_t bytes = 0;
while (value) {
uint8_t temp = value & 0x7F;
value >>= 7;
p[bytes++] = value ? temp | 0x80 : temp;
}
}
// Common case: uint32_t values (field IDs, lengths, most integers)
void encode_varint(uint32_t value) { this->encode_varint(static_cast<uint64_t>(value)); }
// size_t overload (only enabled if size_t is distinct from uint32_t and uint64_t)
template<typename T>
void encode_varint(T value) requires(std::is_same_v<T, size_t> && !std::is_same_v<size_t, uint32_t> &&
!std::is_same_v<size_t, uint64_t>) {
this->encode_varint(static_cast<uint64_t>(value));
}
// Rare case: ProtoVarInt wrapper
void encode_varint(ProtoVarInt value) { this->encode_varint(value.as_uint64()); }
/** /**
* Encode a field key (tag/wire type combination). * Encode a field key (tag/wire type combination).
* *
@@ -312,14 +249,14 @@ class ProtoWriteBuffer {
*/ */
void encode_field_raw(uint32_t field_id, uint32_t type) { void encode_field_raw(uint32_t field_id, uint32_t type) {
uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK); uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
this->encode_varint(val); this->encode_varint_raw(val);
} }
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
if (len == 0 && !force) if (len == 0 && !force)
return; return;
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint(len); this->encode_varint_raw(len);
// Using resize + memcpy instead of insert provides significant performance improvement: // Using resize + memcpy instead of insert provides significant performance improvement:
// ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings
@@ -341,13 +278,13 @@ class ProtoWriteBuffer {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 0); // type 0: Varint - uint32 this->encode_field_raw(field_id, 0); // type 0: Varint - uint32
this->encode_varint(value); this->encode_varint_raw(value);
} }
void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) { void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64 this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
this->encode_varint(value); this->encode_varint_raw(ProtoVarInt(value));
} }
void encode_bool(uint32_t field_id, bool value, bool force = false) { void encode_bool(uint32_t field_id, bool value, bool force = false) {
if (!value && !force) if (!value && !force)

View File

@@ -99,8 +99,9 @@ enum BedjetCommand : uint8_t {
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
} // namespace bedjet } // namespace bedjet
} // namespace esphome } // namespace esphome

View File

@@ -43,7 +43,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
}); });
// It would be better if we had a slider for the fan modes. // It would be better if we had a slider for the fan modes.
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET);
traits.set_supported_presets({ traits.set_supported_presets({
// If we support NONE, then have to decide what happens if the user switches to it (turn off?) // If we support NONE, then have to decide what happens if the user switches to it (turn off?)
// climate::CLIMATE_PRESET_NONE, // climate::CLIMATE_PRESET_NONE,

View File

@@ -385,7 +385,7 @@ void Climate::save_state_() {
if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
state.uses_custom_fan_mode = true; state.uses_custom_fan_mode = true;
const auto &supported = traits.get_supported_custom_fan_modes(); const auto &supported = traits.get_supported_custom_fan_modes();
// std::vector maintains insertion order // std::set has consistent order (lexicographic for strings)
size_t i = 0; size_t i = 0;
for (const auto &mode : supported) { for (const auto &mode : supported) {
if (mode == custom_fan_mode) { if (mode == custom_fan_mode) {
@@ -402,7 +402,7 @@ void Climate::save_state_() {
if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
state.uses_custom_preset = true; state.uses_custom_preset = true;
const auto &supported = traits.get_supported_custom_presets(); const auto &supported = traits.get_supported_custom_presets();
// std::vector maintains insertion order // std::set has consistent order (lexicographic for strings)
size_t i = 0; size_t i = 0;
for (const auto &preset : supported) { for (const auto &preset : supported) {
if (preset == custom_preset) { if (preset == custom_preset) {

View File

@@ -7,7 +7,6 @@ namespace esphome {
namespace climate { namespace climate {
/// Enum for all modes a climate device can be in. /// Enum for all modes a climate device can be in.
/// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value
enum ClimateMode : uint8_t { enum ClimateMode : uint8_t {
/// The climate device is off /// The climate device is off
CLIMATE_MODE_OFF = 0, CLIMATE_MODE_OFF = 0,
@@ -25,7 +24,7 @@ enum ClimateMode : uint8_t {
* For example, the target temperature can be adjusted based on a schedule, or learned behavior. * For example, the target temperature can be adjusted based on a schedule, or learned behavior.
* The target temperature can't be adjusted when in this mode. * The target temperature can't be adjusted when in this mode.
*/ */
CLIMATE_MODE_AUTO = 6 // Update ClimateModeMask in climate_traits.h if adding values after this CLIMATE_MODE_AUTO = 6
}; };
/// Enum for the current action of the climate device. Values match those of ClimateMode. /// Enum for the current action of the climate device. Values match those of ClimateMode.
@@ -44,7 +43,6 @@ enum ClimateAction : uint8_t {
CLIMATE_ACTION_FAN = 6, CLIMATE_ACTION_FAN = 6,
}; };
/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value
enum ClimateFanMode : uint8_t { enum ClimateFanMode : uint8_t {
/// The fan mode is set to On /// The fan mode is set to On
CLIMATE_FAN_ON = 0, CLIMATE_FAN_ON = 0,
@@ -65,11 +63,10 @@ enum ClimateFanMode : uint8_t {
/// The fan mode is set to Diffuse /// The fan mode is set to Diffuse
CLIMATE_FAN_DIFFUSE = 8, CLIMATE_FAN_DIFFUSE = 8,
/// The fan mode is set to Quiet /// The fan mode is set to Quiet
CLIMATE_FAN_QUIET = 9, // Update ClimateFanModeMask in climate_traits.h if adding values after this CLIMATE_FAN_QUIET = 9,
}; };
/// Enum for all modes a climate swing can be in /// Enum for all modes a climate swing can be in
/// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value
enum ClimateSwingMode : uint8_t { enum ClimateSwingMode : uint8_t {
/// The swing mode is set to Off /// The swing mode is set to Off
CLIMATE_SWING_OFF = 0, CLIMATE_SWING_OFF = 0,
@@ -78,11 +75,10 @@ enum ClimateSwingMode : uint8_t {
/// The fan mode is set to Vertical /// The fan mode is set to Vertical
CLIMATE_SWING_VERTICAL = 2, CLIMATE_SWING_VERTICAL = 2,
/// The fan mode is set to Horizontal /// The fan mode is set to Horizontal
CLIMATE_SWING_HORIZONTAL = 3, // Update ClimateSwingModeMask in climate_traits.h if adding values after this CLIMATE_SWING_HORIZONTAL = 3,
}; };
/// Enum for all preset modes /// Enum for all preset modes
/// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value
enum ClimatePreset : uint8_t { enum ClimatePreset : uint8_t {
/// No preset is active /// No preset is active
CLIMATE_PRESET_NONE = 0, CLIMATE_PRESET_NONE = 0,
@@ -99,7 +95,7 @@ enum ClimatePreset : uint8_t {
/// Device is prepared for sleep /// Device is prepared for sleep
CLIMATE_PRESET_SLEEP = 6, CLIMATE_PRESET_SLEEP = 6,
/// Device is reacting to activity (e.g., movement sensors) /// Device is reacting to activity (e.g., movement sensors)
CLIMATE_PRESET_ACTIVITY = 7, // Update ClimatePresetMask in climate_traits.h if adding values after this CLIMATE_PRESET_ACTIVITY = 7,
}; };
enum ClimateFeature : uint32_t { enum ClimateFeature : uint32_t {

View File

@@ -1,33 +1,19 @@
#pragma once #pragma once
#include <vector> #include <set>
#include "climate_mode.h" #include "climate_mode.h"
#include "esphome/core/finite_set_mask.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
namespace esphome { namespace esphome {
#ifdef USE_API
namespace api {
class APIConnection;
} // namespace api
#endif
namespace climate { namespace climate {
// Type aliases for climate enum bitmasks
// These replace std::set<EnumType> to eliminate red-black tree overhead
// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position)
// Bitmask size is automatically calculated from the last enum value
using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>;
using ClimateFanModeMask = FiniteSetMask<ClimateFanMode, DefaultBitPolicy<ClimateFanMode, CLIMATE_FAN_QUIET + 1>>;
using ClimateSwingModeMask =
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
// Lightweight linear search for small vectors (1-20 items)
// Avoids std::find template overhead
template<typename T> inline bool vector_contains(const std::vector<T> &vec, const T &value) {
for (const auto &item : vec) {
if (item == value)
return true;
}
return false;
}
/** This class contains all static data for climate devices. /** This class contains all static data for climate devices.
* *
* All climate devices must support these features: * All climate devices must support these features:
@@ -121,60 +107,48 @@ class ClimateTraits {
} }
} }
void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const { bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
} }
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; }
void set_supported_custom_fan_modes(std::vector<std::string> supported_custom_fan_modes) { void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) {
this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
} }
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) { const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
this->supported_custom_fan_modes_ = modes;
}
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->supported_custom_fan_modes_.assign(modes, modes + N);
}
const std::vector<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); return this->supported_custom_fan_modes_.count(custom_fan_mode);
} }
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); }
bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
bool get_supports_presets() const { return !this->supported_presets_.empty(); } bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; }
void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) { void set_supported_custom_presets(std::set<std::string> supported_custom_presets) {
this->supported_custom_presets_ = std::move(supported_custom_presets); this->supported_custom_presets_ = std::move(supported_custom_presets);
} }
void set_supported_custom_presets(std::initializer_list<std::string> presets) { const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
this->supported_custom_presets_ = presets;
}
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->supported_custom_presets_.assign(presets, presets + N);
}
const std::vector<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const std::string &custom_preset) const { bool supports_custom_preset(const std::string &custom_preset) const {
return vector_contains(this->supported_custom_presets_, custom_preset); return this->supported_custom_presets_.count(custom_preset);
} }
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
float get_visual_min_temperature() const { return this->visual_min_temperature_; } float get_visual_min_temperature() const { return this->visual_min_temperature_; }
void set_visual_min_temperature(float visual_min_temperature) { void set_visual_min_temperature(float visual_min_temperature) {
@@ -205,6 +179,23 @@ class ClimateTraits {
void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
protected: protected:
#ifdef USE_API
// The API connection is a friend class to access internal methods
friend class api::APIConnection;
// These methods return references to internal data structures.
// They are used by the API to avoid copying data when encoding messages.
// Warning: Do not use these methods outside of the API connection code.
// They return references to internal data that can be invalidated.
const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; }
const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; }
const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const {
return this->supported_custom_fan_modes_;
}
const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; }
const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; }
const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; }
#endif
void set_mode_support_(climate::ClimateMode mode, bool supported) { void set_mode_support_(climate::ClimateMode mode, bool supported) {
if (supported) { if (supported) {
this->supported_modes_.insert(mode); this->supported_modes_.insert(mode);
@@ -235,12 +226,12 @@ class ClimateTraits {
float visual_min_humidity_{30}; float visual_min_humidity_{30};
float visual_max_humidity_{99}; float visual_max_humidity_{99};
climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF}; std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
climate::ClimateFanModeMask supported_fan_modes_; std::set<climate::ClimateFanMode> supported_fan_modes_;
climate::ClimateSwingModeMask supported_swing_modes_; std::set<climate::ClimateSwingMode> supported_swing_modes_;
climate::ClimatePresetMask supported_presets_; std::set<climate::ClimatePreset> supported_presets_;
std::vector<std::string> supported_custom_fan_modes_; std::set<std::string> supported_custom_fan_modes_;
std::vector<std::string> supported_custom_presets_; std::set<std::string> supported_custom_presets_;
}; };
} // namespace climate } // namespace climate

View File

@@ -24,18 +24,16 @@ class ClimateIR : public Component,
public remote_base::RemoteTransmittable { public remote_base::RemoteTransmittable {
public: public:
ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
bool supports_dry = false, bool supports_fan_only = false, bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {},
climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(), std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) {
climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(),
climate::ClimatePresetMask presets = climate::ClimatePresetMask()) {
this->minimum_temperature_ = minimum_temperature; this->minimum_temperature_ = minimum_temperature;
this->maximum_temperature_ = maximum_temperature; this->maximum_temperature_ = maximum_temperature;
this->temperature_step_ = temperature_step; this->temperature_step_ = temperature_step;
this->supports_dry_ = supports_dry; this->supports_dry_ = supports_dry;
this->supports_fan_only_ = supports_fan_only; this->supports_fan_only_ = supports_fan_only;
this->fan_modes_ = fan_modes; this->fan_modes_ = std::move(fan_modes);
this->swing_modes_ = swing_modes; this->swing_modes_ = std::move(swing_modes);
this->presets_ = presets; this->presets_ = std::move(presets);
} }
void setup() override; void setup() override;
@@ -62,9 +60,9 @@ class ClimateIR : public Component,
bool supports_heat_{true}; bool supports_heat_{true};
bool supports_dry_{false}; bool supports_dry_{false};
bool supports_fan_only_{false}; bool supports_fan_only_{false};
climate::ClimateFanModeMask fan_modes_{}; std::set<climate::ClimateFanMode> fan_modes_ = {};
climate::ClimateSwingModeMask swing_modes_{}; std::set<climate::ClimateSwingMode> swing_modes_ = {};
climate::ClimatePresetMask presets_{}; std::set<climate::ClimatePreset> presets_ = {};
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
}; };

View File

@@ -76,10 +76,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
} }
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) { void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_(); this->advertising_init_();
this->advertising_->set_manufacturer_data(data); this->advertising_->set_manufacturer_data(data);
this->advertising_start(); this->advertising_start();

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start(); void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data); void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data); void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name); void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid); void advertising_add_service_uuid(ESPBTUUID uuid);

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
} }
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) { void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data; delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr; this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size(); this->advertising_data_.manufacturer_len = data.size();

View File

@@ -37,7 +37,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data); void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; } void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data); void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data); void set_service_data(std::span<const uint8_t> data);

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h" #include "esp32_ble_beacon.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32 #ifdef USE_ESP32

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>(); new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) { characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because: // Convert span to vector for trigger
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id); on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
}); });
return on_write_trigger; return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>(); new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) { descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because: // Convert span to vector for trigger
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id); on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
}); });
return on_write_trigger; return on_write_trigger;

View File

@@ -51,14 +51,7 @@ void FanCall::validate_() {
if (!this->preset_mode_.empty()) { if (!this->preset_mode_.empty()) {
const auto &preset_modes = traits.supported_preset_modes(); const auto &preset_modes = traits.supported_preset_modes();
bool found = false; if (preset_modes.find(this->preset_mode_) == preset_modes.end()) {
for (const auto &mode : preset_modes) {
if (mode == this->preset_mode_) {
found = true;
break;
}
}
if (!found) {
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
this->preset_mode_.clear(); this->preset_mode_.clear();
} }
@@ -198,14 +191,9 @@ void Fan::save_state_() {
if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) {
const auto &preset_modes = this->get_traits().supported_preset_modes(); const auto &preset_modes = this->get_traits().supported_preset_modes();
// Store index of current preset mode // Store index of current preset mode
size_t i = 0; auto preset_iterator = preset_modes.find(this->preset_mode);
for (const auto &mode : preset_modes) { if (preset_iterator != preset_modes.end())
if (mode == this->preset_mode) { state.preset_mode = std::distance(preset_modes.begin(), preset_iterator);
state.preset_mode = i;
break;
}
i++;
}
} }
this->rtc_.save(&state); this->rtc_.save(&state);

View File

@@ -1,6 +1,7 @@
#pragma once #include <set>
#include <utility>
#include <vector> #pragma once
namespace esphome { namespace esphome {
@@ -35,9 +36,9 @@ class FanTraits {
/// Set whether this fan supports changing direction /// Set whether this fan supports changing direction
void set_direction(bool direction) { this->direction_ = direction; } void set_direction(bool direction) { this->direction_ = direction; }
/// Return the preset modes supported by the fan. /// Return the preset modes supported by the fan.
const std::vector<std::string> &supported_preset_modes() const { return this->preset_modes_; } std::set<std::string> supported_preset_modes() const { return this->preset_modes_; }
/// Set the preset modes supported by the fan. /// Set the preset modes supported by the fan.
void set_supported_preset_modes(const std::vector<std::string> &preset_modes) { this->preset_modes_ = preset_modes; } void set_supported_preset_modes(const std::set<std::string> &preset_modes) { this->preset_modes_ = preset_modes; }
/// Return if preset modes are supported /// Return if preset modes are supported
bool supports_preset_modes() const { return !this->preset_modes_.empty(); } bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
@@ -45,17 +46,17 @@ class FanTraits {
#ifdef USE_API #ifdef USE_API
// The API connection is a friend class to access internal methods // The API connection is a friend class to access internal methods
friend class api::APIConnection; friend class api::APIConnection;
// This method returns a reference to the internal preset modes. // This method returns a reference to the internal preset modes set.
// It is used by the API to avoid copying data when encoding messages. // It is used by the API to avoid copying data when encoding messages.
// Warning: Do not use this method outside of the API connection code. // Warning: Do not use this method outside of the API connection code.
// It returns a reference to internal data that can be invalidated. // It returns a reference to internal data that can be invalidated.
const std::vector<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; } const std::set<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; }
#endif #endif
bool oscillation_{false}; bool oscillation_{false};
bool speed_{false}; bool speed_{false};
bool direction_{false}; bool direction_{false};
int speed_count_{}; int speed_count_{};
std::vector<std::string> preset_modes_{}; std::set<std::string> preset_modes_{};
}; };
} // namespace fan } // namespace fan

View File

@@ -171,7 +171,7 @@ void HaierClimateBase::toggle_power() {
PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()}); PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()});
} }
void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) { void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
this->traits_.set_supported_swing_modes(modes); this->traits_.set_supported_swing_modes(modes);
if (!modes.empty()) if (!modes.empty())
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
@@ -179,13 +179,13 @@ void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask m
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) { void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
this->traits_.set_supported_modes(modes); this->traits_.set_supported_modes(modes);
this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available
this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available
} }
void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) { void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePreset> &presets) {
this->traits_.set_supported_presets(presets); this->traits_.set_supported_presets(presets);
if (!presets.empty()) if (!presets.empty())
this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE);

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <set>
#include "esphome/components/climate/climate.h" #include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
@@ -59,9 +60,9 @@ class HaierClimateBase : public esphome::Component,
void send_power_off_command(); void send_power_off_command();
void toggle_power(); void toggle_power();
void reset_protocol() { this->reset_protocol_request_ = true; }; void reset_protocol() { this->reset_protocol_request_ = true; };
void set_supported_modes(esphome::climate::ClimateModeMask modes); void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes); void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
void set_supported_presets(esphome::climate::ClimatePresetMask presets); void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets);
bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
size_t read_array(uint8_t *data, size_t len) noexcept override { size_t read_array(uint8_t *data, size_t len) noexcept override {

View File

@@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
{ {
// Swing mode // Swing mode
ClimateSwingMode old_swing_mode = this->swing_mode; ClimateSwingMode old_swing_mode = this->swing_mode;
const auto &swing_modes = traits_.get_supported_swing_modes(); const std::set<ClimateSwingMode> &swing_modes = traits_.get_supported_swing_modes();
bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL); bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end();
bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL); bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end();
if (horizontal_swing_supported && if (horizontal_swing_supported &&
(packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) {
if (vertical_swing_supported && if (vertical_swing_supported &&
@@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::QUIET_MODE, (uint8_t) hon_protocol::DataParameters::QUIET_MODE,
quiet_mode_buf, 2); quiet_mode_buf, 2);
} }
if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) {
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::FAST_MODE, (uint8_t) hon_protocol::DataParameters::FAST_MODE,
fast_mode_buf, 2); fast_mode_buf, 2);
} }
if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) {
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::TEN_DEGREE, (uint8_t) hon_protocol::DataParameters::TEN_DEGREE,

View File

@@ -22,7 +22,7 @@ class HBridgeFan : public Component, public fan::Fan {
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
void set_preset_modes(const std::vector<std::string> &presets) { preset_modes_ = presets; } void set_preset_modes(const std::set<std::string> &presets) { preset_modes_ = presets; }
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
@@ -38,7 +38,7 @@ class HBridgeFan : public Component, public fan::Fan {
int speed_count_{}; int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW}; DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_; fan::FanTraits traits_;
std::vector<std::string> preset_modes_{}; std::set<std::string> preset_modes_{};
void control(const fan::FanCall &call) override; void control(const fan::FanCall &call) override;
void write_state_(); void write_state_();

View File

@@ -97,10 +97,11 @@ const float TEMP_MAX = 100; // Celsius
class HeatpumpIRClimate : public climate_ir::ClimateIR { class HeatpumpIRClimate : public climate_ir::ClimateIR {
public: public:
HeatpumpIRClimate() HeatpumpIRClimate()
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true, : climate_ir::ClimateIR(
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, TEMP_MIN, TEMP_MAX, 1.0f, true, true,
climate::CLIMATE_FAN_AUTO}, std::set<climate::ClimateFanMode>{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO},
std::set<climate::ClimateSwingMode>{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
void setup() override; void setup() override;
void set_protocol(Protocol protocol) { this->protocol_ = protocol; } void set_protocol(Protocol protocol) { this->protocol_ = protocol; }

View File

@@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() {
if (s.second) { if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first); ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else { } else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first); ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
} }
} }
} }

View File

@@ -358,7 +358,7 @@ class LvSelectable : public LvCompound {
virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0; virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0;
void set_selected_text(const std::string &text, lv_anim_enable_t anim); void set_selected_text(const std::string &text, lv_anim_enable_t anim);
std::string get_selected_text(); std::string get_selected_text();
const std::vector<std::string> &get_options() { return this->options_; } std::vector<std::string> get_options() { return this->options_; }
void set_options(std::vector<std::string> options); void set_options(std::vector<std::string> options);
protected: protected:

View File

@@ -53,17 +53,7 @@ class LVGLSelect : public select::Select, public Component {
this->widget_->set_selected_text(value, this->anim_); this->widget_->set_selected_text(value, this->anim_);
this->publish(); this->publish();
} }
void set_options_() { void set_options_() { this->traits.set_options(this->widget_->get_options()); }
// Widget uses std::vector<std::string>, SelectTraits uses FixedVector<const char*>
// Convert by extracting c_str() pointers
const auto &opts = this->widget_->get_options();
FixedVector<const char *> opt_ptrs;
opt_ptrs.init(opts.size());
for (size_t i = 0; i < opts.size(); i++) {
opt_ptrs[i] = opts[i].c_str();
}
this->traits.set_options(opt_ptrs);
}
LvSelectable *widget_; LvSelectable *widget_;
lv_anim_enable_t anim_; lv_anim_enable_t anim_;

View File

@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_reg_(pin, false, iodir); this->update_reg_(pin, false, iodir);
} }
} }
float MCP23016::get_setup_priority() const { return setup_priority::IO; } float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) { bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
if (this->is_failed()) if (this->is_failed())
return false; return false;

View File

@@ -19,9 +19,6 @@ using climate::ClimateTraits;
using climate::ClimateMode; using climate::ClimateMode;
using climate::ClimateSwingMode; using climate::ClimateSwingMode;
using climate::ClimateFanMode; using climate::ClimateFanMode;
using climate::ClimateModeMask;
using climate::ClimateSwingModeMask;
using climate::ClimatePresetMask;
class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, public climate::Climate { class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, public climate::Climate {
public: public:
@@ -43,20 +40,20 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
void do_power_on() { this->base_.setPowerState(true); } void do_power_on() { this->base_.setPowerState(true); }
void do_power_off() { this->base_.setPowerState(false); } void do_power_off() { this->base_.setPowerState(false); }
void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); }
void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void set_supported_modes(const std::set<ClimateMode> &modes) { this->supported_modes_ = modes; }
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void set_supported_swing_modes(const std::set<ClimateSwingMode> &modes) { this->supported_swing_modes_ = modes; }
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void set_supported_presets(const std::set<ClimatePreset> &presets) { this->supported_presets_ = presets; }
void set_custom_presets(const std::vector<std::string> &presets) { this->supported_custom_presets_ = presets; } void set_custom_presets(const std::set<std::string> &presets) { this->supported_custom_presets_ = presets; }
void set_custom_fan_modes(const std::vector<std::string> &modes) { this->supported_custom_fan_modes_ = modes; } void set_custom_fan_modes(const std::set<std::string> &modes) { this->supported_custom_fan_modes_ = modes; }
protected: protected:
void control(const ClimateCall &call) override; void control(const ClimateCall &call) override;
ClimateTraits traits() override; ClimateTraits traits() override;
ClimateModeMask supported_modes_{}; std::set<ClimateMode> supported_modes_{};
ClimateSwingModeMask supported_swing_modes_{}; std::set<ClimateSwingMode> supported_swing_modes_{};
ClimatePresetMask supported_presets_{}; std::set<ClimatePreset> supported_presets_{};
std::vector<std::string> supported_custom_presets_{}; std::set<std::string> supported_custom_presets_{};
std::vector<std::string> supported_custom_fan_modes_{}; std::set<std::string> supported_custom_fan_modes_{};
Sensor *outdoor_sensor_{nullptr}; Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr}; Sensor *power_sensor_{nullptr};

View File

@@ -28,7 +28,7 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
if (map_it != this->mapping_.cend()) { if (map_it != this->mapping_.cend()) {
size_t idx = std::distance(this->mapping_.cbegin(), map_it); size_t idx = std::distance(this->mapping_.cbegin(), map_it);
new_state = std::string(this->option_at(idx)); new_state = this->traits.get_options()[idx];
ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value);
} else { } else {
ESP_LOGE(TAG, "No option found for mapping %lld", value); ESP_LOGE(TAG, "No option found for mapping %lld", value);
@@ -41,12 +41,10 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
} }
void ModbusSelect::control(const std::string &value) { void ModbusSelect::control(const std::string &value) {
auto idx = this->index_of(value); auto options = this->traits.get_options();
if (!idx.has_value()) { auto opt_it = std::find(options.cbegin(), options.cend(), value);
ESP_LOGW(TAG, "Invalid option '%s'", value.c_str()); size_t idx = std::distance(options.cbegin(), opt_it);
return; optional<int64_t> mapval = this->mapping_[idx];
}
optional<int64_t> mapval = this->mapping_[idx.value()];
ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str()); ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str());
std::vector<uint16_t> data; std::vector<uint16_t> data;

View File

@@ -1,6 +1,5 @@
#include "select.h" #include "select.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cstring>
namespace esphome { namespace esphome {
namespace select { namespace select {
@@ -36,7 +35,7 @@ size_t Select::size() const {
optional<size_t> Select::index_of(const std::string &option) const { optional<size_t> Select::index_of(const std::string &option) const {
const auto &options = traits.get_options(); const auto &options = traits.get_options();
for (size_t i = 0; i < options.size(); i++) { for (size_t i = 0; i < options.size(); i++) {
if (strcmp(options[i], option.c_str()) == 0) { if (options[i] == option) {
return i; return i;
} }
} }
@@ -54,13 +53,11 @@ optional<size_t> Select::active_index() const {
optional<std::string> Select::at(size_t index) const { optional<std::string> Select::at(size_t index) const {
if (this->has_index(index)) { if (this->has_index(index)) {
const auto &options = traits.get_options(); const auto &options = traits.get_options();
return std::string(options.at(index)); return options.at(index);
} else { } else {
return {}; return {};
} }
} }
const char *Select::option_at(size_t index) const { return traits.get_options().at(index); }
} // namespace select } // namespace select
} // namespace esphome } // namespace esphome

View File

@@ -56,9 +56,6 @@ class Select : public EntityBase {
/// Return the (optional) option value at the provided index offset. /// Return the (optional) option value at the provided index offset.
optional<std::string> at(size_t index) const; optional<std::string> at(size_t index) const;
/// Return the option value at the provided index offset (as const char* from flash).
const char *option_at(size_t index) const;
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback); void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
protected: protected:

View File

@@ -3,16 +3,9 @@
namespace esphome { namespace esphome {
namespace select { namespace select {
void SelectTraits::set_options(const std::initializer_list<const char *> &options) { this->options_ = options; } void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
void SelectTraits::set_options(const FixedVector<const char *> &options) { const std::vector<std::string> &SelectTraits::get_options() const { return this->options_; }
this->options_.init(options.size());
for (size_t i = 0; i < options.size(); i++) {
this->options_[i] = options[i];
}
}
const FixedVector<const char *> &SelectTraits::get_options() const { return this->options_; }
} // namespace select } // namespace select
} // namespace esphome } // namespace esphome

View File

@@ -1,19 +1,18 @@
#pragma once #pragma once
#include "esphome/core/helpers.h" #include <vector>
#include <initializer_list> #include <string>
namespace esphome { namespace esphome {
namespace select { namespace select {
class SelectTraits { class SelectTraits {
public: public:
void set_options(const std::initializer_list<const char *> &options); void set_options(std::vector<std::string> options);
void set_options(const FixedVector<const char *> &options); const std::vector<std::string> &get_options() const;
const FixedVector<const char *> &get_options() const;
protected: protected:
FixedVector<const char *> options_; std::vector<std::string> options_;
}; };
} // namespace select } // namespace select

View File

@@ -18,7 +18,7 @@ class SpeedFan : public Component, public fan::Fan {
void set_output(output::FloatOutput *output) { this->output_ = output; } void set_output(output::FloatOutput *output) { this->output_ = output; }
void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; }
void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; }
void set_preset_modes(const std::vector<std::string> &presets) { this->preset_modes_ = presets; } void set_preset_modes(const std::set<std::string> &presets) { this->preset_modes_ = presets; }
fan::FanTraits get_traits() override { return this->traits_; } fan::FanTraits get_traits() override { return this->traits_; }
protected: protected:
@@ -30,7 +30,7 @@ class SpeedFan : public Component, public fan::Fan {
output::BinaryOutput *direction_{nullptr}; output::BinaryOutput *direction_{nullptr};
int speed_count_{}; int speed_count_{};
fan::FanTraits traits_; fan::FanTraits traits_;
std::vector<std::string> preset_modes_{}; std::set<std::string> preset_modes_{};
}; };
} // namespace speed } // namespace speed

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <vector> #include <set>
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/fan/fan.h" #include "esphome/components/fan/fan.h"
@@ -16,7 +16,7 @@ class TemplateFan : public Component, public fan::Fan {
void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; }
void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; }
void set_speed_count(int count) { this->speed_count_ = count; } void set_speed_count(int count) { this->speed_count_ = count; }
void set_preset_modes(const std::initializer_list<std::string> &presets) { this->preset_modes_ = presets; } void set_preset_modes(const std::set<std::string> &presets) { this->preset_modes_ = presets; }
fan::FanTraits get_traits() override { return this->traits_; } fan::FanTraits get_traits() override { return this->traits_; }
protected: protected:
@@ -26,7 +26,7 @@ class TemplateFan : public Component, public fan::Fan {
bool has_direction_{false}; bool has_direction_{false};
int speed_count_{0}; int speed_count_{0};
fan::FanTraits traits_; fan::FanTraits traits_;
std::vector<std::string> preset_modes_{}; std::set<std::string> preset_modes_{};
}; };
} // namespace template_ } // namespace template_

View File

@@ -16,12 +16,12 @@ void TemplateSelect::setup() {
size_t restored_index; size_t restored_index;
if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { if (this->pref_.load(&restored_index) && this->has_index(restored_index)) {
index = restored_index; index = restored_index;
ESP_LOGD(TAG, "State from restore: %s", this->option_at(index)); ESP_LOGD(TAG, "State from restore: %s", this->at(index).value().c_str());
} else { } else {
ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index)); ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->at(index).value().c_str());
} }
} else { } else {
ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); ESP_LOGD(TAG, "State from initial: %s", this->at(index).value().c_str());
} }
this->publish_state(this->at(index).value()); this->publish_state(this->at(index).value());
@@ -64,7 +64,8 @@ void TemplateSelect::dump_config() {
" Optimistic: %s\n" " Optimistic: %s\n"
" Initial Option: %s\n" " Initial Option: %s\n"
" Restore Value: %s", " Restore Value: %s",
YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_)); YESNO(this->optimistic_), this->at(this->initial_option_index_).value().c_str(),
YESNO(this->restore_value_));
} }
} // namespace template_ } // namespace template_

View File

@@ -40,10 +40,6 @@ enum OnBootRestoreFrom : uint8_t {
}; };
struct ThermostatClimateTimer { struct ThermostatClimateTimer {
ThermostatClimateTimer() = default;
ThermostatClimateTimer(bool active, uint32_t time, uint32_t started, std::function<void()> func)
: active(active), time(time), started(started), func(std::move(func)) {}
bool active; bool active;
uint32_t time; uint32_t time;
uint32_t started; uint32_t started;

View File

@@ -405,7 +405,7 @@ void ToshibaClimate::setup() {
this->swing_modes_ = this->toshiba_swing_modes_(); this->swing_modes_ = this->toshiba_swing_modes_();
// Ensure swing mode is always initialized to a valid value // Ensure swing mode is always initialized to a valid value
if (this->swing_modes_.empty() || !this->swing_modes_.count(this->swing_mode)) { if (this->swing_modes_.empty() || this->swing_modes_.find(this->swing_mode) == this->swing_modes_.end()) {
// No swing support for this model or current swing mode not supported, reset to OFF // No swing support for this model or current swing mode not supported, reset to OFF
this->swing_mode = climate::CLIMATE_SWING_OFF; this->swing_mode = climate::CLIMATE_SWING_OFF;
} }

View File

@@ -71,10 +71,10 @@ class ToshibaClimate : public climate_ir::ClimateIR {
return TOSHIBA_RAS_2819T_TEMP_C_MAX; return TOSHIBA_RAS_2819T_TEMP_C_MAX;
return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models
} }
climate::ClimateSwingModeMask toshiba_swing_modes_() { std::set<climate::ClimateSwingMode> toshiba_swing_modes_() {
return (this->model_ == MODEL_GENERIC) return (this->model_ == MODEL_GENERIC)
? climate::ClimateSwingModeMask() ? std::set<climate::ClimateSwingMode>{}
: climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; : std::set<climate::ClimateSwingMode>{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL};
} }
void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat); void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat);
bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes); bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes);

View File

@@ -312,12 +312,18 @@ climate::ClimateTraits TuyaClimate::traits() {
traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); traits.add_supported_preset(climate::CLIMATE_PRESET_NONE);
} }
if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) { if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) {
traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, std::set<climate::ClimateSwingMode> supported_swing_modes = {
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL};
traits.set_supported_swing_modes(std::move(supported_swing_modes));
} else if (this->swing_vertical_id_.has_value()) { } else if (this->swing_vertical_id_.has_value()) {
traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}); std::set<climate::ClimateSwingMode> supported_swing_modes = {climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_VERTICAL};
traits.set_supported_swing_modes(std::move(supported_swing_modes));
} else if (this->swing_horizontal_id_.has_value()) { } else if (this->swing_horizontal_id_.has_value()) {
traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}); std::set<climate::ClimateSwingMode> supported_swing_modes = {climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_HORIZONTAL};
traits.set_supported_swing_modes(std::move(supported_swing_modes));
} }
if (fan_speed_id_) { if (fan_speed_id_) {

View File

@@ -10,6 +10,7 @@ void TuyaSelect::setup() {
this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) { this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) {
uint8_t enum_value = datapoint.value_enum; uint8_t enum_value = datapoint.value_enum;
ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value); ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value);
auto options = this->traits.get_options();
auto mappings = this->mappings_; auto mappings = this->mappings_;
auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value); auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value);
if (it == mappings.end()) { if (it == mappings.end()) {
@@ -48,9 +49,9 @@ void TuyaSelect::dump_config() {
" Data type: %s\n" " Data type: %s\n"
" Options are:", " Options are:",
this->select_id_, this->is_int_ ? "int" : "enum"); this->select_id_, this->is_int_ ? "int" : "enum");
const auto &options = this->traits.get_options(); auto options = this->traits.get_options();
for (size_t i = 0; i < this->mappings_.size(); i++) { for (size_t i = 0; i < this->mappings_.size(); i++) {
ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i)); ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str());
} }
} }

View File

@@ -82,12 +82,6 @@ struct TransferStatus {
using transfer_cb_t = std::function<void(const TransferStatus &)>; using transfer_cb_t = std::function<void(const TransferStatus &)>;
enum TransferResult : uint8_t {
TRANSFER_OK = 0,
TRANSFER_ERROR_NO_SLOTS,
TRANSFER_ERROR_SUBMIT_FAILED,
};
class USBClient; class USBClient;
// struct used to capture all data needed for a transfer // struct used to capture all data needed for a transfer
@@ -140,7 +134,7 @@ class USBClient : public Component {
void on_opened(uint8_t addr); void on_opened(uint8_t addr);
void on_removed(usb_device_handle_t handle); void on_removed(usb_device_handle_t handle);
void control_transfer_callback(const usb_transfer_t *xfer) const; void control_transfer_callback(const usb_transfer_t *xfer) const;
TransferResult transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length);
void dump_config() override; void dump_config() override;
void release_trq(TransferRequest *trq); void release_trq(TransferRequest *trq);

View File

@@ -334,7 +334,7 @@ static void control_callback(const usb_transfer_t *xfer) {
// This multi-threaded access is intentional for performance - USB task can // This multi-threaded access is intentional for performance - USB task can
// immediately restart transfers without waiting for main loop scheduling. // immediately restart transfers without waiting for main loop scheduling.
TransferRequest *USBClient::get_trq_() { TransferRequest *USBClient::get_trq_() {
trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire); trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
// Find first available slot (bit = 0) and try to claim it atomically // Find first available slot (bit = 0) and try to claim it atomically
// We use a while loop to allow retrying the same slot after CAS failure // We use a while loop to allow retrying the same slot after CAS failure
@@ -443,15 +443,14 @@ static void transfer_callback(usb_transfer_t *xfer) {
* @param ep_address The endpoint address. * @param ep_address The endpoint address.
* @param callback The callback function to be called when the transfer is complete. * @param callback The callback function to be called when the transfer is complete.
* @param length The length of the data to be transferred. * @param length The length of the data to be transferred.
* @return TransferResult indicating success or specific failure reason
* *
* @throws None. * @throws None.
*/ */
TransferResult USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
auto *trq = this->get_trq_(); auto *trq = this->get_trq_();
if (trq == nullptr) { if (trq == nullptr) {
ESP_LOGE(TAG, "Too many requests queued"); ESP_LOGE(TAG, "Too many requests queued");
return TRANSFER_ERROR_NO_SLOTS; return;
} }
trq->callback = callback; trq->callback = callback;
trq->transfer->callback = transfer_callback; trq->transfer->callback = transfer_callback;
@@ -461,9 +460,7 @@ TransferResult USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &c
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
this->release_trq(trq); this->release_trq(trq);
return TRANSFER_ERROR_SUBMIT_FAILED;
} }
return TRANSFER_OK;
} }
/** /**

View File

@@ -169,98 +169,6 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) {
this->parent_->start_input(this); this->parent_->start_input(this);
return status; return status;
} }
void USBUartComponent::reset_input_state_(USBUartChannel *channel) {
channel->input_retry_count_.store(0);
channel->input_started_.store(false);
}
void USBUartComponent::restart_input_(USBUartChannel *channel) {
// Atomically verify it's still started (true) and keep it started
// This prevents the race window of toggling true->false->true
bool expected = true;
if (channel->input_started_.compare_exchange_strong(expected, true)) {
// Still started - do the actual restart work without toggling the flag
this->do_start_input_(channel);
}
}
void USBUartComponent::input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status) {
// CALLBACK CONTEXT: This function is executed in USB task via transfer_callback
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
// Transfer failed, slot already released
// Reset state so normal operations can restart later
this->reset_input_state_(channel);
return;
}
if (!channel->dummy_receiver_ && status.data_len > 0) {
// Allocate a chunk from the pool
UsbDataChunk *chunk = this->chunk_pool_.allocate();
if (chunk == nullptr) {
// No chunks available - queue is full, data dropped, slot already released
this->usb_data_queue_.increment_dropped_count();
// Reset state so normal operations can restart later
this->reset_input_state_(channel);
return;
}
// Copy data to chunk (this is fast, happens in USB task)
memcpy(chunk->data, status.data, status.data_len);
chunk->length = status.data_len;
chunk->channel = channel;
// Push to lock-free queue for main loop processing
// Push always succeeds because pool size == queue size
this->usb_data_queue_.push(chunk);
}
// On success, reset retry count and restart input immediately from USB task for performance
// The lock-free queue will handle backpressure
channel->input_retry_count_.store(0);
channel->input_started_.store(false);
this->start_input(channel);
}
void USBUartComponent::do_start_input_(USBUartChannel *channel) {
// This function does the actual work of starting input
// Caller must ensure input_started_ is already set to true
const auto *ep = channel->cdc_dev_.in_ep;
// input_started_ already set to true by caller
auto result = this->transfer_in(
ep->bEndpointAddress,
[this, channel](const usb_host::TransferStatus &status) { this->input_transfer_callback_(channel, status); },
ep->wMaxPacketSize);
if (result == usb_host::TRANSFER_ERROR_NO_SLOTS) {
// No slots available - defer retry to main loop
this->defer_input_retry_(channel);
} else if (result != usb_host::TRANSFER_OK) {
// Other error (submit failed) - don't retry, just reset state
// Error already logged by transfer_in()
this->reset_input_state_(channel);
}
}
void USBUartComponent::defer_input_retry_(USBUartChannel *channel) {
static constexpr uint8_t MAX_INPUT_RETRIES = 10;
// Atomically increment and get the NEW value (previous + 1)
uint8_t new_retry_count = channel->input_retry_count_.fetch_add(1) + 1;
if (new_retry_count > MAX_INPUT_RETRIES) {
ESP_LOGE(TAG, "Input retry limit reached for channel %d, stopping retries", channel->index_);
this->reset_input_state_(channel);
return;
}
// Keep input_started_ as true during defer to prevent multiple retries from queueing
// The deferred lambda will atomically restart
this->defer([this, channel] { this->restart_input_(channel); });
}
void USBUartComponent::setup() { USBClient::setup(); } void USBUartComponent::setup() { USBClient::setup(); }
void USBUartComponent::loop() { void USBUartComponent::loop() {
USBClient::loop(); USBClient::loop();
@@ -306,14 +214,8 @@ void USBUartComponent::dump_config() {
} }
} }
void USBUartComponent::start_input(USBUartChannel *channel) { void USBUartComponent::start_input(USBUartChannel *channel) {
if (!channel->initialised_.load()) if (!channel->initialised_.load() || channel->input_started_.load())
return; return;
// Atomically check if not started and set to started in one operation
bool expected = false;
if (!channel->input_started_.compare_exchange_strong(expected, true))
return; // Already started - prevents duplicate transfers from concurrent threads
// THREAD CONTEXT: Called from both USB task and main loop threads // THREAD CONTEXT: Called from both USB task and main loop threads
// - USB task: Immediate restart after successful transfer for continuous data flow // - USB task: Immediate restart after successful transfer for continuous data flow
// - Main loop: Controlled restart after consuming data (backpressure mechanism) // - Main loop: Controlled restart after consuming data (backpressure mechanism)
@@ -324,9 +226,45 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
// //
// The underlying transfer_in() uses lock-free atomic allocation from the // The underlying transfer_in() uses lock-free atomic allocation from the
// TransferRequest pool, making this multi-threaded access safe // TransferRequest pool, making this multi-threaded access safe
const auto *ep = channel->cdc_dev_.in_ep;
// CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
auto callback = [this, channel](const usb_host::TransferStatus &status) {
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
// On failure, don't restart - let next read_array() trigger it
channel->input_started_.store(false);
return;
}
// Do the actual work (input_started_ already set to true by CAS above) if (!channel->dummy_receiver_ && status.data_len > 0) {
this->do_start_input_(channel); // Allocate a chunk from the pool
UsbDataChunk *chunk = this->chunk_pool_.allocate();
if (chunk == nullptr) {
// No chunks available - queue is full or we're out of memory
this->usb_data_queue_.increment_dropped_count();
// Mark input as not started so we can retry
channel->input_started_.store(false);
return;
}
// Copy data to chunk (this is fast, happens in USB task)
memcpy(chunk->data, status.data, status.data_len);
chunk->length = status.data_len;
chunk->channel = channel;
// Push to lock-free queue for main loop processing
// Push always succeeds because pool size == queue size
this->usb_data_queue_.push(chunk);
}
// On success, restart input immediately from USB task for performance
// The lock-free queue will handle backpressure
channel->input_started_.store(false);
this->start_input(channel);
};
channel->input_started_.store(true);
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
} }
void USBUartComponent::start_output(USBUartChannel *channel) { void USBUartComponent::start_output(USBUartChannel *channel) {
@@ -432,7 +370,7 @@ void USBUartTypeCdcAcm::enable_channels() {
for (auto *channel : this->channels_) { for (auto *channel : this->channels_) {
if (!channel->initialised_.load()) if (!channel->initialised_.load())
continue; continue;
this->reset_input_state_(channel); channel->input_started_.store(false);
channel->output_started_.store(false); channel->output_started_.store(false);
this->start_input(channel); this->start_input(channel);
} }

View File

@@ -111,11 +111,10 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
CdcEps cdc_dev_{}; CdcEps cdc_dev_{};
// Enum (likely 4 bytes) // Enum (likely 4 bytes)
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
// Group atomics together // Group atomics together (each 1 byte)
std::atomic<bool> input_started_{true}; std::atomic<bool> input_started_{true};
std::atomic<bool> output_started_{true}; std::atomic<bool> output_started_{true};
std::atomic<bool> initialised_{false}; std::atomic<bool> initialised_{false};
std::atomic<uint8_t> input_retry_count_{0};
// Group regular bytes together to minimize padding // Group regular bytes together to minimize padding
const uint8_t index_; const uint8_t index_;
bool debug_{}; bool debug_{};
@@ -141,11 +140,6 @@ class USBUartComponent : public usb_host::USBClient {
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_; EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_;
protected: protected:
void defer_input_retry_(USBUartChannel *channel);
void reset_input_state_(USBUartChannel *channel);
void restart_input_(USBUartChannel *channel);
void do_start_input_(USBUartChannel *channel);
void input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status);
std::vector<USBUartChannel *> channels_{}; std::vector<USBUartChannel *> channels_{};
}; };

View File

@@ -707,15 +707,6 @@ class EsphomeCore:
def relative_piolibdeps_path(self, *path: str | Path) -> Path: def relative_piolibdeps_path(self, *path: str | Path) -> Path:
return self.relative_build_path(".piolibdeps", *path) return self.relative_build_path(".piolibdeps", *path)
@property
def platformio_cache_dir(self) -> str:
"""Get the PlatformIO cache directory path."""
# Check if running in Docker/HA addon with custom cache dir
if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip():
return cache_dir
# Default PlatformIO cache location
return os.path.expanduser("~/.platformio/.cache")
@property @property
def firmware_bin(self) -> Path: def firmware_bin(self) -> Path:
if self.is_libretiny: if self.is_libretiny:

View File

@@ -414,8 +414,10 @@ int8_t step_to_accuracy_decimals(float step) {
return str.length() - dot_pos - 1; return str.length() - dot_pos - 1;
} }
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms // Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
// Helper function to find the index of a base64 character in the lookup table. // Helper function to find the index of a base64 character in the lookup table.
// Returns the character's position (0-63) if found, or 0 if not found. // Returns the character's position (0-63) if found, or 0 if not found.
@@ -425,8 +427,8 @@ static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
// stops processing at the first invalid character due to the is_base64() check in its // stops processing at the first invalid character due to the is_base64() check in its
// while loop condition, making this edge case harmless in practice. // while loop condition, making this edge case harmless in practice.
static inline uint8_t base64_find_char(char c) { static inline uint8_t base64_find_char(char c) {
const void *ptr = memchr(BASE64_CHARS, c, sizeof(BASE64_CHARS)); const char *pos = strchr(BASE64_CHARS, c);
return ptr ? (static_cast<const char *>(ptr) - BASE64_CHARS) : 0; return pos ? (pos - BASE64_CHARS) : 0;
} }
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); } static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }

View File

@@ -143,9 +143,6 @@ template<typename T, size_t N> class StaticVector {
size_t size() const { return count_; } size_t size() const { return count_; }
bool empty() const { return count_ == 0; } bool empty() const { return count_ == 0; }
// Direct access to size counter for efficient in-place construction
size_t &count() { return count_; }
T &operator[](size_t i) { return data_[i]; } T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; }
@@ -307,11 +304,6 @@ template<typename T> class FixedVector {
return data_[size_ - 1]; return data_[size_ - 1];
} }
/// Access first element (no bounds checking - matches std::vector behavior)
/// Caller must ensure vector is not empty (size() > 0)
T &front() { return data_[0]; }
const T &front() const { return data_[0]; }
/// Access last element (no bounds checking - matches std::vector behavior) /// Access last element (no bounds checking - matches std::vector behavior)
/// Caller must ensure vector is not empty (size() > 0) /// Caller must ensure vector is not empty (size() > 0)
T &back() { return data_[size_ - 1]; } T &back() { return data_[size_ - 1]; }
@@ -325,11 +317,6 @@ template<typename T> class FixedVector {
T &operator[](size_t i) { return data_[i]; } T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; }
/// Access element with bounds checking (matches std::vector behavior)
/// Note: No exception thrown on out of bounds - caller must ensure index is valid
T &at(size_t i) { return data_[i]; }
const T &at(size_t i) const { return data_[i]; }
// Iterator support for range-based for loops // Iterator support for range-based for loops
T *begin() { return data_; } T *begin() { return data_; }
T *end() { return data_ + size_; } T *end() { return data_ + size_; }

View File

@@ -94,9 +94,10 @@ class Scheduler {
} name_; } name_;
uint32_t interval; uint32_t interval;
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis() // Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
// with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit // with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
// for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter // This is intentionally limited to 48 bits, not stored as a full 64-bit value.
// supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling // With 49.7 days per 32-bit rollover, the 16-bit counter supports
// 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// even when devices run for months. Split into two fields for better memory // even when devices run for months. Split into two fields for better memory
// alignment on 32-bit systems. // alignment on 32-bit systems.
uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value) uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)

View File

@@ -145,16 +145,7 @@ def run_compile(config, verbose):
args = [] args = []
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]: if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"] args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
result = run_platformio_cli_run(config, verbose, *args) return run_platformio_cli_run(config, verbose, *args)
# Run memory analysis if enabled
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
try:
analyze_memory_usage(config)
except Exception as e:
_LOGGER.warning("Failed to analyze memory usage: %s", e)
return result
def _run_idedata(config): def _run_idedata(config):
@@ -403,74 +394,3 @@ class IDEData:
if path.endswith(".exe") if path.endswith(".exe")
else f"{path[:-3]}readelf" else f"{path[:-3]}readelf"
) )
def analyze_memory_usage(config: dict[str, Any]) -> None:
"""Analyze memory usage by component after compilation."""
# Lazy import to avoid overhead when not needed
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from esphome.analyze_memory.helpers import get_esphome_components
idedata = get_idedata(config)
# Get paths to tools
elf_path = idedata.firmware_elf_path
objdump_path = idedata.objdump_path
readelf_path = idedata.readelf_path
# Debug logging
_LOGGER.debug("ELF path from idedata: %s", elf_path)
# Check if file exists
if not Path(elf_path).exists():
# Try alternate path
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
if alt_path.exists():
elf_path = str(alt_path)
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
else:
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
return
# Extract external components from config
external_components = set()
# Get the list of built-in ESPHome components
builtin_components = get_esphome_components()
# Special non-component keys that appear in configs
NON_COMPONENT_KEYS = {
CONF_ESPHOME,
"substitutions",
"packages",
"globals",
"<<",
}
# Check all top-level keys in config
for key in config:
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
# This is an external component
external_components.add(key)
_LOGGER.debug("Detected external components: %s", external_components)
# Create analyzer and run analysis
analyzer = MemoryAnalyzerCLI(
elf_path, objdump_path, readelf_path, external_components
)
analyzer.analyze()
# Generate and print report
report = analyzer.generate_report()
_LOGGER.info("\n%s", report)
# Optionally save to file
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
if report_file.suffix == ".json":
report_file.write_text(analyzer.to_json())
_LOGGER.info("Memory report saved to %s", report_file)
else:
report_file.write_text(report)
_LOGGER.info("Memory report saved to %s", report_file)

View File

@@ -1162,11 +1162,7 @@ class SInt64Type(TypeInfo):
def _generate_array_dump_content( def _generate_array_dump_content(
ti, ti, field_name: str, name: str, is_bool: bool = False
field_name: str,
name: str,
is_bool: bool = False,
is_const_char_ptr: bool = False,
) -> str: ) -> str:
"""Generate dump content for array types (repeated or fixed array). """Generate dump content for array types (repeated or fixed array).
@@ -1174,10 +1170,7 @@ def _generate_array_dump_content(
""" """
o = f"for (const auto {'' if is_bool else '&'}it : {field_name}) {{\n" o = f"for (const auto {'' if is_bool else '&'}it : {field_name}) {{\n"
# Check if underlying type can use dump_field # Check if underlying type can use dump_field
if is_const_char_ptr: if ti.can_use_dump_field():
# Special case for const char* - use it directly
o += f' dump_field(out, "{name}", it, 4);\n'
elif ti.can_use_dump_field():
# For types that have dump_field overloads, use them with extra indent # For types that have dump_field overloads, use them with extra indent
# std::vector<bool> iterators return proxy objects, need explicit cast # std::vector<bool> iterators return proxy objects, need explicit cast
value_expr = "static_cast<bool>(it)" if is_bool else ti.dump_field_value("it") value_expr = "static_cast<bool>(it)" if is_bool else ti.dump_field_value("it")
@@ -1540,11 +1533,6 @@ class RepeatedTypeInfo(TypeInfo):
def encode_content(self) -> str: def encode_content(self) -> str:
if self._use_pointer: if self._use_pointer:
# For pointer fields, just dereference (pointer should never be null in our use case) # For pointer fields, just dereference (pointer should never be null in our use case)
# Special handling for const char* elements (when container_no_template contains "const char")
if "const char" in self._container_no_template:
o = f"for (const char *it : *this->{self.field_name}) {{\n"
o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n"
else:
o = f"for (const auto &it : *this->{self.field_name}) {{\n" o = f"for (const auto &it : *this->{self.field_name}) {{\n"
if isinstance(self._ti, EnumType): if isinstance(self._ti, EnumType):
o += f" buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>(it), true);\n" o += f" buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>(it), true);\n"
@@ -1562,18 +1550,10 @@ class RepeatedTypeInfo(TypeInfo):
@property @property
def dump_content(self) -> str: def dump_content(self) -> str:
# Check if this is const char* elements
is_const_char_ptr = (
self._use_pointer and "const char" in self._container_no_template
)
if self._use_pointer: if self._use_pointer:
# For pointer fields, dereference and use the existing helper # For pointer fields, dereference and use the existing helper
return _generate_array_dump_content( return _generate_array_dump_content(
self._ti, self._ti, f"*this->{self.field_name}", self.name, is_bool=False
f"*this->{self.field_name}",
self.name,
is_bool=False,
is_const_char_ptr=is_const_char_ptr,
) )
return _generate_array_dump_content( return _generate_array_dump_content(
self._ti, f"this->{self.field_name}", self.name, is_bool=self._ti_is_bool self._ti, f"this->{self.field_name}", self.name, is_bool=self._ti_is_bool
@@ -1608,11 +1588,6 @@ class RepeatedTypeInfo(TypeInfo):
o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n" o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n"
else: else:
# Other types need the actual value # Other types need the actual value
# Special handling for const char* elements
if self._use_pointer and "const char" in self._container_no_template:
o += f" for (const char *it : {container_ref}) {{\n"
o += " size.add_length_force(1, strlen(it));\n"
else:
auto_ref = "" if self._ti_is_bool else "&" auto_ref = "" if self._ti_is_bool else "&"
o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" o += f" for (const auto {auto_ref}it : {container_ref}) {{\n"
o += f" {self._ti.get_size_calculation('it', True)}\n" o += f" {self._ti.get_size_calculation('it', True)}\n"
@@ -2567,12 +2542,6 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
out.append("\\n"); out.append("\\n");
} }
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append(value).append("'");
out.append("\\n");
}
template<typename T> template<typename T>
static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
append_field_prefix(out, field_name, indent); append_field_prefix(out, field_name, indent);

View File

@@ -90,15 +90,17 @@ def get_component_from_path(file_path: str) -> str | None:
"""Extract component name from a file path. """Extract component name from a file path.
Args: Args:
file_path: Path to a file (e.g., "esphome/components/wifi/wifi.cpp") file_path: Path to a file (e.g., "esphome/components/wifi/wifi.cpp"
or "tests/components/uart/test.esp32-idf.yaml")
Returns: Returns:
Component name if path is in components directory, None otherwise Component name if path is in components or tests directory, None otherwise
""" """
if not file_path.startswith(ESPHOME_COMPONENTS_PATH): if file_path.startswith(ESPHOME_COMPONENTS_PATH) or file_path.startswith(
return None ESPHOME_TESTS_COMPONENTS_PATH
):
parts = file_path.split("/") parts = file_path.split("/")
if len(parts) >= 3: if len(parts) >= 3 and parts[2]:
return parts[2] return parts[2]
return None return None

View File

@@ -66,6 +66,5 @@ def test_text_config_lamda_is_set(generate_main):
main_cpp = generate_main("tests/component_tests/text/test_text.yaml") main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
# Then # Then
# Stateless lambda optimization: empty capture list allows function pointer conversion
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
assert 'return std::string{"Hello"};' in main_cpp assert 'return std::string{"Hello"};' in main_cpp

View File

@@ -1,3 +1,5 @@
# comment
modbus: modbus:
id: mod_bus1 id: mod_bus1
flow_control_pin: ${flow_control_pin} flow_control_pin: ${flow_control_pin}

View File

@@ -44,7 +44,6 @@ class InitialStateHelper:
helper = InitialStateHelper(entities) helper = InitialStateHelper(entities)
client.subscribe_states(helper.on_state_wrapper(user_callback)) client.subscribe_states(helper.on_state_wrapper(user_callback))
await helper.wait_for_initial_states() await helper.wait_for_initial_states()
# Access initial states via helper.initial_states[key]
""" """
def __init__(self, entities: list[EntityInfo]) -> None: def __init__(self, entities: list[EntityInfo]) -> None:
@@ -64,8 +63,6 @@ class InitialStateHelper:
self._entities_by_id = { self._entities_by_id = {
(entity.device_id, entity.key): entity for entity in entities (entity.device_id, entity.key): entity for entity in entities
} }
# Store initial states by key for test access
self.initial_states: dict[int, EntityState] = {}
# Log all entities # Log all entities
_LOGGER.debug( _LOGGER.debug(
@@ -130,9 +127,6 @@ class InitialStateHelper:
# If this entity is waiting for initial state # If this entity is waiting for initial state
if entity_id in self._wait_initial_states: if entity_id in self._wait_initial_states:
# Store the initial state for test access
self.initial_states[state.key] = state
# Remove from waiting set # Remove from waiting set
self._wait_initial_states.discard(entity_id) self._wait_initial_states.discard(entity_id)

View File

@@ -2,11 +2,12 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import aioesphomeapi import aioesphomeapi
from aioesphomeapi import ClimateAction, ClimateInfo, ClimateMode, ClimatePreset from aioesphomeapi import ClimateAction, ClimateMode, ClimatePreset, EntityState
import pytest import pytest
from .state_utils import InitialStateHelper
from .types import APIClientConnectedFactory, RunCompiledFunction from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -17,27 +18,26 @@ async def test_host_mode_climate_basic_state(
api_client_connected: APIClientConnectedFactory, api_client_connected: APIClientConnectedFactory,
) -> None: ) -> None:
"""Test basic climate state reporting.""" """Test basic climate state reporting."""
loop = asyncio.get_running_loop()
async with run_compiled(yaml_config), api_client_connected() as client: async with run_compiled(yaml_config), api_client_connected() as client:
# Get entities and set up state synchronization states: dict[int, EntityState] = {}
entities, services = await client.list_entities_services() climate_future: asyncio.Future[EntityState] = loop.create_future()
initial_state_helper = InitialStateHelper(entities)
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
assert len(climate_infos) >= 1, "Expected at least 1 climate entity"
# Subscribe with the wrapper (no-op callback since we just want initial states) def on_state(state: EntityState) -> None:
client.subscribe_states(initial_state_helper.on_state_wrapper(lambda _: None)) states[state.key] = state
if (
isinstance(state, aioesphomeapi.ClimateState)
and not climate_future.done()
):
climate_future.set_result(state)
client.subscribe_states(on_state)
# Wait for all initial states to be broadcast
try: try:
await initial_state_helper.wait_for_initial_states() climate_state = await asyncio.wait_for(climate_future, timeout=5.0)
except TimeoutError: except TimeoutError:
pytest.fail("Timeout waiting for initial states") pytest.fail("Climate state not received within 5 seconds")
# Get the climate entity and its initial state
test_climate = climate_infos[0]
climate_state = initial_state_helper.initial_states.get(test_climate.key)
assert climate_state is not None, "Climate initial state not found"
assert isinstance(climate_state, aioesphomeapi.ClimateState) assert isinstance(climate_state, aioesphomeapi.ClimateState)
assert climate_state.mode == ClimateMode.OFF assert climate_state.mode == ClimateMode.OFF
assert climate_state.action == ClimateAction.OFF assert climate_state.action == ClimateAction.OFF

View File

@@ -1065,3 +1065,39 @@ def test_parse_list_components_output(output: str, expected: list[str]) -> None:
"""Test parse_list_components_output function.""" """Test parse_list_components_output function."""
result = helpers.parse_list_components_output(output) result = helpers.parse_list_components_output(output)
assert result == expected assert result == expected
@pytest.mark.parametrize(
("file_path", "expected_component"),
[
# Component files
("esphome/components/wifi/wifi.cpp", "wifi"),
("esphome/components/uart/uart.h", "uart"),
("esphome/components/api/api_server.cpp", "api"),
("esphome/components/sensor/sensor.cpp", "sensor"),
# Test files
("tests/components/uart/test.esp32-idf.yaml", "uart"),
("tests/components/wifi/test.esp8266-ard.yaml", "wifi"),
("tests/components/sensor/test.esp32-idf.yaml", "sensor"),
("tests/components/api/test_api.cpp", "api"),
("tests/components/uart/common.h", "uart"),
# Non-component files
("esphome/core/component.cpp", None),
("esphome/core/helpers.h", None),
("tests/integration/test_api.py", None),
("tests/unit_tests/test_helpers.py", None),
("README.md", None),
("script/helpers.py", None),
# Edge cases
("esphome/components/", None), # No component name
("tests/components/", None), # No component name
("esphome/components", None), # No trailing slash
("tests/components", None), # No trailing slash
],
)
def test_get_component_from_path(
file_path: str, expected_component: str | None
) -> None:
"""Test extraction of component names from file paths."""
result = helpers.get_component_from_path(file_path)
assert result == expected_component

View File

@@ -670,45 +670,3 @@ class TestEsphomeCore:
os.environ.pop("ESPHOME_IS_HA_ADDON", None) os.environ.pop("ESPHOME_IS_HA_ADDON", None)
os.environ.pop("ESPHOME_DATA_DIR", None) os.environ.pop("ESPHOME_DATA_DIR", None)
assert target.data_dir == Path(expected_default) assert target.data_dir == Path(expected_default)
def test_platformio_cache_dir_with_env_var(self):
"""Test platformio_cache_dir when PLATFORMIO_CACHE_DIR env var is set."""
target = core.EsphomeCore()
test_cache_dir = "/custom/cache/dir"
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": test_cache_dir}):
assert target.platformio_cache_dir == test_cache_dir
def test_platformio_cache_dir_without_env_var(self):
"""Test platformio_cache_dir defaults to ~/.platformio/.cache."""
target = core.EsphomeCore()
with patch.dict(os.environ, {}, clear=True):
# Ensure env var is not set
os.environ.pop("PLATFORMIO_CACHE_DIR", None)
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_empty_env_var(self):
"""Test platformio_cache_dir with empty env var falls back to default."""
target = core.EsphomeCore()
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": ""}):
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_whitespace_env_var(self):
"""Test platformio_cache_dir with whitespace-only env var falls back to default."""
target = core.EsphomeCore()
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": " "}):
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_docker_addon_path(self):
"""Test platformio_cache_dir in Docker/HA addon environment."""
target = core.EsphomeCore()
addon_cache = "/data/cache/platformio"
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": addon_cache}):
assert target.platformio_cache_dir == addon_cache

View File

@@ -355,7 +355,6 @@ def test_clean_build(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.return_value = dependencies_lock mock_core.relative_build_path.return_value = dependencies_lock
mock_core.platformio_cache_dir = str(platformio_cache_dir)
# Verify all exist before # Verify all exist before
assert pioenvs_dir.exists() assert pioenvs_dir.exists()