From e200f82d7a441066e87f26213e3232bbe5c72055 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 21:48:03 -1000 Subject: [PATCH 01/46] fixes --- esphome/analyze_memory/cli.py | 23 +++++++++++++++++++++++ esphome/platformio_api.py | 9 +++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 1695a00c19..2986922ac2 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -1,6 +1,7 @@ """CLI interface for memory analysis with report generation.""" from collections import defaultdict +import json import sys from . import ( @@ -270,6 +271,28 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): 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: """Dump uncategorized symbols for analysis.""" # Sort by size descending diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index b7b6cf399d..19d355efd3 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -408,7 +408,8 @@ class IDEData: 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 import MemoryAnalyzer + from esphome.analyze_memory.cli import MemoryAnalyzerCLI + from esphome.analyze_memory.helpers import get_esphome_components idedata = get_idedata(config) @@ -435,8 +436,6 @@ def analyze_memory_usage(config: dict[str, Any]) -> None: external_components = set() # Get the list of built-in ESPHome components - from esphome.analyze_memory import get_esphome_components - builtin_components = get_esphome_components() # Special non-component keys that appear in configs @@ -457,7 +456,9 @@ def analyze_memory_usage(config: dict[str, Any]) -> None: _LOGGER.debug("Detected external components: %s", external_components) # Create analyzer and run analysis - analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components) + analyzer = MemoryAnalyzerCLI( + elf_path, objdump_path, readelf_path, external_components + ) analyzer.analyze() # Generate and print report From 5ad22620c94348dc47e09034a1fd24b1cb605247 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 23:35:52 -1000 Subject: [PATCH 02/46] [mqtt] Reduce flash usage by optimizing ArduinoJson assignments --- esphome/components/mqtt/mqtt_client.cpp | 7 +-- esphome/components/mqtt/mqtt_component.cpp | 51 ++++++++-------------- 2 files changed, 19 insertions(+), 39 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 16f54ab8a0..9055b4421e 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -140,11 +140,8 @@ void MQTTClientComponent::send_device_info_() { #endif #ifdef USE_API_NOISE - if (api::global_api_server->get_noise_ctx()->has_psk()) { - root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; - } else { - root["api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; - } + root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] = + "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; #endif }, 2, this->discovery_info_.retain); diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 6ceaf219ff..d6ff34a641 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -85,24 +85,20 @@ bool MQTTComponent::send_discovery_() { } // Fields from EntityBase - if (this->get_entity()->has_own_name()) { - root[MQTT_NAME] = this->friendly_name(); - } else { - root[MQTT_NAME] = ""; - } + root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : ""; + if (this->is_disabled_by_default()) root[MQTT_ENABLED_BY_DEFAULT] = false; if (!this->get_icon().empty()) root[MQTT_ICON] = this->get_icon(); - switch (this->get_entity()->get_entity_category()) { + const auto entity_category = this->get_entity()->get_entity_category(); + switch (entity_category) { case ENTITY_CATEGORY_NONE: break; case ENTITY_CATEGORY_CONFIG: - root[MQTT_ENTITY_CATEGORY] = "config"; - break; case ENTITY_CATEGORY_DIAGNOSTIC: - root[MQTT_ENTITY_CATEGORY] = "diagnostic"; + root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic"; break; } @@ -113,20 +109,14 @@ bool MQTTComponent::send_discovery_() { if (this->command_retain_) root[MQTT_COMMAND_RETAIN] = true; - if (this->availability_ == nullptr) { - if (!global_mqtt_client->get_availability().topic.empty()) { - root[MQTT_AVAILABILITY_TOPIC] = global_mqtt_client->get_availability().topic; - if (global_mqtt_client->get_availability().payload_available != "online") - root[MQTT_PAYLOAD_AVAILABLE] = global_mqtt_client->get_availability().payload_available; - if (global_mqtt_client->get_availability().payload_not_available != "offline") - root[MQTT_PAYLOAD_NOT_AVAILABLE] = global_mqtt_client->get_availability().payload_not_available; - } - } else if (!this->availability_->topic.empty()) { - root[MQTT_AVAILABILITY_TOPIC] = this->availability_->topic; - if (this->availability_->payload_available != "online") - root[MQTT_PAYLOAD_AVAILABLE] = this->availability_->payload_available; - if (this->availability_->payload_not_available != "offline") - root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available; + const Availability &avail = + this->availability_ == nullptr ? global_mqtt_client->get_availability() : *this->availability_; + if (!avail.topic.empty()) { + root[MQTT_AVAILABILITY_TOPIC] = avail.topic; + if (avail.payload_available != "online") + root[MQTT_PAYLOAD_AVAILABLE] = avail.payload_available; + if (avail.payload_not_available != "offline") + root[MQTT_PAYLOAD_NOT_AVAILABLE] = avail.payload_not_available; } const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); @@ -145,10 +135,7 @@ bool MQTTComponent::send_discovery_() { if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_(); - std::string node_friendly_name = App.get_friendly_name(); - if (node_friendly_name.empty()) { - node_friendly_name = node_name; - } + const std::string &node_friendly_name = App.get_friendly_name().empty() ? node_name : App.get_friendly_name(); std::string node_area = App.get_area(); JsonObject device_info = root[MQTT_DEVICE].to(); @@ -158,13 +145,9 @@ bool MQTTComponent::send_discovery_() { #ifdef ESPHOME_PROJECT_NAME device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")"; const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.'); - if (model == nullptr) { // must never happen but check anyway - device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; - device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME; - } else { - device_info[MQTT_DEVICE_MODEL] = model + 1; - device_info[MQTT_DEVICE_MANUFACTURER] = std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME); - } + device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1; + device_info[MQTT_DEVICE_MANUFACTURER] = + model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME); #else device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")"; device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; From a0008d6f4454f175b18af318973fc4c9c79ba52c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 10:41:37 -1000 Subject: [PATCH 03/46] fix --- esphome/components/api/api_pb2_dump.cpp | 6 +++--- script/api_protobuf/api_protobuf.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index f9f45ad071..c47d95ed5d 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -913,9 +913,9 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { dump_field(out, "object_id", this->object_id_ref_); dump_field(out, "key", this->key); dump_field(out, "name", this->name_ref_); - out.append(" supported_color_modes: 0x"); - out.append(uint32_to_string(this->supported_color_modes)); - out.append("\n"); + char buffer[32]; + snprintf(buffer, sizeof(buffer), " supported_color_modes: 0x%08" PRIX32 "\n", this->supported_color_modes); + out.append(buffer); dump_field(out, "min_mireds", this->min_mireds); dump_field(out, "max_mireds", this->max_mireds); for (const auto &it : this->effects) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 9e140ca9ce..8a841354a9 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1564,9 +1564,9 @@ class RepeatedTypeInfo(TypeInfo): if self._use_bitmask: # For bitmask fields, dump the hex value of the bitmask return ( - f'out.append(" {self.field_name}: 0x");\n' - f"out.append(uint32_to_string(this->{self.field_name}));\n" - f'out.append("\\n");' + f"char buffer[32];\n" + f'snprintf(buffer, sizeof(buffer), " {self.field_name}: 0x%08" PRIX32 "\\n", this->{self.field_name});\n' + f"out.append(buffer);" ) if self._use_pointer: # For pointer fields, dereference and use the existing helper From 76ad649bf95fb238b661e940478723813da5d82e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 14:41:59 -1000 Subject: [PATCH 04/46] review comments --- esphome/components/light/color_mode.h | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index 1583bde4d3..a26f917167 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -199,12 +199,13 @@ class ColorModeMask { constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; } constexpr size_t size() const { - // Count set bits + // Count set bits using Brian Kernighan's algorithm + // More efficient for sparse bitmasks (typical case: 2-4 modes out of 10) uint16_t n = this->mask_; size_t count = 0; while (n) { - count += n & 1; - n >>= 1; + n &= n - 1; // Clear the least significant set bit + count++; } return count; } @@ -276,11 +277,17 @@ class ColorModeMask { // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5 // We need to convert the power-of-2 value to an index uint8_t cap_val = static_cast(capability); +#if defined(__GNUC__) || defined(__clang__) + // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) + int index = __builtin_ctz(cap_val); +#else + // Fallback for compilers without __builtin_ctz int index = 0; while (cap_val > 1) { cap_val >>= 1; ++index; } +#endif return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0; } From 9fc3ad1fa533954ae573bfccddba1a2e818bf62d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 22:16:09 -1000 Subject: [PATCH 05/46] bot --- esphome/components/mqtt/mqtt_component.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index d6ff34a641..eb6114008a 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -135,7 +135,8 @@ bool MQTTComponent::send_discovery_() { if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_(); - const std::string &node_friendly_name = App.get_friendly_name().empty() ? node_name : App.get_friendly_name(); + const std::string &friendly_name_ref = App.get_friendly_name(); + const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; std::string node_area = App.get_area(); JsonObject device_info = root[MQTT_DEVICE].to(); From db5c78acb9570961a75e8b6c6e0b7e31bcdc200a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Oct 2025 09:36:55 -1000 Subject: [PATCH 06/46] preen --- esphome/analyze_memory.py | 1630 ------------------------------------- 1 file changed, 1630 deletions(-) delete mode 100644 esphome/analyze_memory.py diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py deleted file mode 100644 index 70c324b33f..0000000000 --- a/esphome/analyze_memory.py +++ /dev/null @@ -1,1630 +0,0 @@ -"""Memory usage analyzer for ESPHome compiled binaries.""" - -from collections import defaultdict -import json -import logging -from pathlib import Path -import re -import subprocess - -_LOGGER = logging.getLogger(__name__) - -# Pattern to extract ESPHome component namespaces dynamically -ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::") - -# Component identification rules -# Symbol patterns: patterns found in raw symbol names -SYMBOL_PATTERNS = { - "freertos": [ - "vTask", - "xTask", - "xQueue", - "pvPort", - "vPort", - "uxTask", - "pcTask", - "prvTimerTask", - "prvAddNewTaskToReadyList", - "pxReadyTasksLists", - "prvAddCurrentTaskToDelayedList", - "xEventGroupWaitBits", - "xRingbufferSendFromISR", - "prvSendItemDoneNoSplit", - "prvReceiveGeneric", - "prvSendAcquireGeneric", - "prvCopyItemAllowSplit", - "xEventGroup", - "xRingbuffer", - "prvSend", - "prvReceive", - "prvCopy", - "xPort", - "ulTaskGenericNotifyTake", - "prvIdleTask", - "prvInitialiseNewTask", - "prvIsYieldRequiredSMP", - "prvGetItemByteBuf", - "prvInitializeNewRingbuffer", - "prvAcquireItemNoSplit", - "prvNotifyQueueSetContainer", - "ucStaticTimerQueueStorage", - "eTaskGetState", - "main_task", - "do_system_init_fn", - "xSemaphoreCreateGenericWithCaps", - "vListInsert", - "uxListRemove", - "vRingbufferReturnItem", - "vRingbufferReturnItemFromISR", - "prvCheckItemFitsByteBuffer", - "prvGetCurMaxSizeAllowSplit", - "tick_hook", - "sys_sem_new", - "sys_arch_mbox_fetch", - "sys_arch_sem_wait", - "prvDeleteTCB", - "vQueueDeleteWithCaps", - "vRingbufferDeleteWithCaps", - "vSemaphoreDeleteWithCaps", - "prvCheckItemAvail", - "prvCheckTaskCanBeScheduledSMP", - "prvGetCurMaxSizeNoSplit", - "prvResetNextTaskUnblockTime", - "prvReturnItemByteBuf", - "vApplicationStackOverflowHook", - "vApplicationGetIdleTaskMemory", - "sys_init", - "sys_mbox_new", - "sys_arch_mbox_tryfetch", - ], - "xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"], - "heap": ["heap_", "multi_heap"], - "spi_flash": ["spi_flash"], - "rtc": ["rtc_", "rtcio_ll_"], - "gpio_driver": ["gpio_", "pins"], - "uart_driver": ["uart", "_uart", "UART"], - "timer": ["timer_", "esp_timer"], - "peripherals": ["periph_", "periman"], - "network_stack": [ - "vj_compress", - "raw_sendto", - "raw_input", - "etharp_", - "icmp_input", - "socket_ipv6", - "ip_napt", - "socket_ipv4_multicast", - "socket_ipv6_multicast", - "netconn_", - "recv_raw", - "accept_function", - "netconn_recv_data", - "netconn_accept", - "netconn_write_vectors_partly", - "netconn_drain", - "raw_connect", - "raw_bind", - "icmp_send_response", - "sockets", - "icmp_dest_unreach", - "inet_chksum_pseudo", - "alloc_socket", - "done_socket", - "set_global_fd_sets", - "inet_chksum_pbuf", - "tryget_socket_unconn_locked", - "tryget_socket_unconn", - "cs_create_ctrl_sock", - "netbuf_alloc", - ], - "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], - "wifi_stack": [ - "ieee80211", - "hostap", - "sta_", - "ap_", - "scan_", - "wifi_", - "wpa_", - "wps_", - "esp_wifi", - "cnx_", - "wpa3_", - "sae_", - "wDev_", - "ic_", - "mac_", - "esf_buf", - "gWpaSm", - "sm_WPA", - "eapol_", - "owe_", - "wifiLowLevelInit", - "s_do_mapping", - "gScanStruct", - "ppSearchTxframe", - "ppMapWaitTxq", - "ppFillAMPDUBar", - "ppCheckTxConnTrafficIdle", - "ppCalTkipMic", - ], - "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"], - "wifi_bt_coex": ["coex"], - "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], - "bluedroid_bt": [ - "bluedroid", - "btc_", - "bta_", - "btm_", - "btu_", - "BTM_", - "GATT", - "L2CA_", - "smp_", - "gatts_", - "attp_", - "l2cu_", - "l2cb", - "smp_cb", - "BTA_GATTC_", - "SMP_", - "BTU_", - "BTA_Dm", - "GAP_Ble", - "BT_tx_if", - "host_recv_pkt_cb", - "saved_local_oob_data", - "string_to_bdaddr", - "string_is_bdaddr", - "CalConnectParamTimeout", - "transmit_fragment", - "transmit_data", - "event_command_ready", - "read_command_complete_header", - "parse_read_local_extended_features_response", - "parse_read_local_version_info_response", - "should_request_high", - "btdm_wakeup_request", - "BTA_SetAttributeValue", - "BTA_EnableBluetooth", - "transmit_command_futured", - "transmit_command", - "get_waiting_command", - "make_command", - "transmit_downward", - "host_recv_adv_packet", - "copy_extra_byte_in_db", - "parse_read_local_supported_commands_response", - ], - "crypto_math": [ - "ecp_", - "bignum_", - "mpi_", - "sswu", - "modp", - "dragonfly_", - "gcm_mult", - "__multiply", - "quorem", - "__mdiff", - "__lshift", - "__mprec_tens", - "ECC_", - "multiprecision_", - "mix_sub_columns", - "sbox", - "gfm2_sbox", - "gfm3_sbox", - "curve_p256", - "curve", - "p_256_init_curve", - "shift_sub_rows", - "rshift", - ], - "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], - "libc": [ - "printf", - "scanf", - "malloc", - "free", - "memcpy", - "memset", - "strcpy", - "strlen", - "_dtoa", - "_fopen", - "__sfvwrite_r", - "qsort", - "__sf", - "__sflush_r", - "__srefill_r", - "_impure_data", - "_reclaim_reent", - "_open_r", - "strncpy", - "_strtod_l", - "__gethex", - "__hexnan", - "_setenv_r", - "_tzset_unlocked_r", - "__tzcalc_limits", - "select", - "scalbnf", - "strtof", - "strtof_l", - "__d2b", - "__b2d", - "__s2b", - "_Balloc", - "__multadd", - "__lo0bits", - "__atexit0", - "__smakebuf_r", - "__swhatbuf_r", - "_sungetc_r", - "_close_r", - "_link_r", - "_unsetenv_r", - "_rename_r", - "__month_lengths", - "tzinfo", - "__ratio", - "__hi0bits", - "__ulp", - "__any_on", - "__copybits", - "L_shift", - "_fcntl_r", - "_lseek_r", - "_read_r", - "_write_r", - "_unlink_r", - "_fstat_r", - "access", - "fsync", - "tcsetattr", - "tcgetattr", - "tcflush", - "tcdrain", - "__ssrefill_r", - "_stat_r", - "__hexdig_fun", - "__mcmp", - "_fwalk_sglue", - "__fpclassifyf", - "_setlocale_r", - "_mbrtowc_r", - "fcntl", - "__match", - "_lock_close", - "__c$", - "__func__$", - "__FUNCTION__$", - "DAYS_IN_MONTH", - "_DAYS_BEFORE_MONTH", - "CSWTCH$", - "dst$", - "sulp", - ], - "string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"], - "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], - "file_io": [ - "fread", - "fwrite", - "fopen", - "fclose", - "fseek", - "ftell", - "fflush", - "s_fd_table", - ], - "string_formatting": [ - "snprintf", - "vsnprintf", - "sprintf", - "vsprintf", - "sscanf", - "vsscanf", - ], - "cpp_anonymous": ["_GLOBAL__N_", "n$"], - "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"], - "exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"], - "static_init": ["_GLOBAL__sub_I_"], - "mdns_lib": ["mdns"], - "phy_radio": [ - "phy_", - "rf_", - "chip_", - "register_chipv7", - "pbus_", - "bb_", - "fe_", - "rfcal_", - "ram_rfcal", - "tx_pwctrl", - "rx_chan", - "set_rx_gain", - "set_chan", - "agc_reg", - "ram_txiq", - "ram_txdc", - "ram_gen_rx_gain", - "rx_11b_opt", - "set_rx_sense", - "set_rx_gain_cal", - "set_chan_dig_gain", - "tx_pwctrl_init_cal", - "rfcal_txiq", - "set_tx_gain_table", - "correct_rfpll_offset", - "pll_correct_dcap", - "txiq_cal_init", - "pwdet_sar", - "pwdet_sar2_init", - "ram_iq_est_enable", - "ram_rfpll_set_freq", - "ant_wifirx_cfg", - "ant_btrx_cfg", - "force_txrxoff", - "force_txrx_off", - "tx_paon_set", - "opt_11b_resart", - "rfpll_1p2_opt", - "ram_dc_iq_est", - "ram_start_tx_tone", - "ram_en_pwdet", - "ram_cbw2040_cfg", - "rxdc_est_min", - "i2cmst_reg_init", - "temprature_sens_read", - "ram_restart_cal", - "ram_write_gain_mem", - "ram_wait_rfpll_cal_end", - "txcal_debuge_mode", - "ant_wifitx_cfg", - "reg_init_begin", - ], - "wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"], - "wifi_lmac": ["lmac"], - "wifi_device": ["wdev", "wDev_"], - "power_mgmt": [ - "pm_", - "sleep", - "rtc_sleep", - "light_sleep", - "deep_sleep", - "power_down", - "g_pm", - ], - "memory_mgmt": [ - "mem_", - "memory_", - "tlsf_", - "memp_", - "pbuf_", - "pbuf_alloc", - "pbuf_copy_partial_pbuf", - ], - "hal_layer": ["hal_"], - "clock_mgmt": [ - "clk_", - "clock_", - "rtc_clk", - "apb_", - "cpu_freq", - "setCpuFrequencyMhz", - ], - "cache_mgmt": ["cache"], - "flash_ops": ["flash", "image_load"], - "interrupt_handlers": [ - "isr", - "interrupt", - "intr_", - "exc_", - "exception", - "port_IntStack", - ], - "wrapper_functions": ["_wrapper"], - "error_handling": ["panic", "abort", "assert", "error_", "fault"], - "authentication": ["auth"], - "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"], - "dhcp": ["dhcp", "handle_dhcp"], - "ethernet_phy": [ - "emac_", - "eth_phy_", - "phy_tlk110", - "phy_lan87", - "phy_ip101", - "phy_rtl", - "phy_dp83", - "phy_ksz", - "lan87xx_", - "rtl8201_", - "ip101_", - "ksz80xx_", - "jl1101_", - "dp83848_", - "eth_on_state_changed", - ], - "threading": ["pthread_", "thread_", "_task_"], - "pthread": ["pthread"], - "synchronization": ["mutex", "semaphore", "spinlock", "portMUX"], - "math_lib": [ - "sin", - "cos", - "tan", - "sqrt", - "pow", - "exp", - "log", - "atan", - "asin", - "acos", - "floor", - "ceil", - "fabs", - "round", - ], - "random": ["rand", "random", "rng_", "prng"], - "time_lib": [ - "time", - "clock", - "gettimeofday", - "settimeofday", - "localtime", - "gmtime", - "mktime", - "strftime", - ], - "console_io": ["console_", "uart_tx", "uart_rx", "puts", "putchar", "getchar"], - "rom_functions": ["r_", "rom_"], - "compiler_runtime": [ - "__divdi3", - "__udivdi3", - "__moddi3", - "__muldi3", - "__ashldi3", - "__ashrdi3", - "__lshrdi3", - "__cmpdi2", - "__fixdfdi", - "__floatdidf", - ], - "libgcc": ["libgcc", "_divdi3", "_udivdi3"], - "boot_startup": ["boot", "start_cpu", "call_start", "startup", "bootloader"], - "bootloader": ["bootloader_", "esp_bootloader"], - "app_framework": ["app_", "initArduino", "setup", "loop", "Update"], - "weak_symbols": ["__weak_"], - "compiler_builtins": ["__builtin_"], - "vfs": ["vfs_", "VFS"], - "esp32_sdk": ["esp32_", "esp32c", "esp32s"], - "usb": ["usb_", "USB", "cdc_", "CDC"], - "i2c_driver": ["i2c_", "I2C"], - "i2s_driver": ["i2s_", "I2S"], - "spi_driver": ["spi_", "SPI"], - "adc_driver": ["adc_", "ADC"], - "dac_driver": ["dac_", "DAC"], - "touch_driver": ["touch_", "TOUCH"], - "pwm_driver": ["pwm_", "PWM", "ledc_", "LEDC"], - "rmt_driver": ["rmt_", "RMT"], - "pcnt_driver": ["pcnt_", "PCNT"], - "can_driver": ["can_", "CAN", "twai_", "TWAI"], - "sdmmc_driver": ["sdmmc_", "SDMMC", "sdcard", "sd_card"], - "temp_sensor": ["temp_sensor", "tsens_"], - "watchdog": ["wdt_", "WDT", "watchdog"], - "brownout": ["brownout", "bod_"], - "ulp": ["ulp_", "ULP"], - "psram": ["psram", "PSRAM", "spiram", "SPIRAM"], - "efuse": ["efuse", "EFUSE"], - "partition": ["partition", "esp_partition"], - "esp_event": ["esp_event", "event_loop", "event_callback"], - "esp_console": ["esp_console", "console_"], - "chip_specific": ["chip_", "esp_chip"], - "esp_system_utils": ["esp_system", "esp_hw", "esp_clk", "esp_sleep"], - "ipc": ["esp_ipc", "ipc_"], - "wifi_config": [ - "g_cnxMgr", - "gChmCxt", - "g_ic", - "TxRxCxt", - "s_dp", - "s_ni", - "s_reg_dump", - "packet$", - "d_mult_table", - "K", - "fcstab", - ], - "smartconfig": ["sc_ack_send"], - "rc_calibration": ["rc_cal", "rcUpdate"], - "noise_floor": ["noise_check"], - "rf_calibration": [ - "set_rx_sense", - "set_rx_gain_cal", - "set_chan_dig_gain", - "tx_pwctrl_init_cal", - "rfcal_txiq", - "set_tx_gain_table", - "correct_rfpll_offset", - "pll_correct_dcap", - "txiq_cal_init", - "pwdet_sar", - "rx_11b_opt", - ], - "wifi_crypto": [ - "pk_use_ecparams", - "process_segments", - "ccmp_", - "rc4_", - "aria_", - "mgf_mask", - "dh_group", - "ccmp_aad_nonce", - "ccmp_encrypt", - "rc4_skip", - "aria_sb1", - "aria_sb2", - "aria_is1", - "aria_is2", - "aria_sl", - "aria_a", - ], - "radio_control": ["fsm_input", "fsm_sconfreq"], - "pbuf": [ - "pbuf_", - ], - "event_group": ["xEventGroup"], - "ringbuffer": ["xRingbuffer", "prvSend", "prvReceive", "prvCopy"], - "provisioning": ["prov_", "prov_stop_and_notify"], - "scan": ["gScanStruct"], - "port": ["xPort"], - "elf_loader": [ - "elf_add", - "elf_add_note", - "elf_add_segment", - "process_image", - "read_encoded", - "read_encoded_value", - "read_encoded_value_with_base", - "process_image_header", - ], - "socket_api": [ - "sockets", - "netconn_", - "accept_function", - "recv_raw", - "socket_ipv4_multicast", - "socket_ipv6_multicast", - ], - "igmp": ["igmp_", "igmp_send", "igmp_input"], - "icmp6": ["icmp6_"], - "arp": ["arp_table"], - "ampdu": [ - "ampdu_", - "rcAmpdu", - "trc_onAmpduOp", - "rcAmpduLowerRate", - "ampdu_dispatch_upto", - ], - "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], - "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"], - "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"], - "channel_mgmt": ["chm_init", "chm_set_current_channel"], - "trace": ["trc_init", "trc_onAmpduOp"], - "country_code": ["country_info", "country_info_24ghz"], - "multicore": ["do_multicore_settings"], - "Update_lib": ["Update"], - "stdio": [ - "__sf", - "__sflush_r", - "__srefill_r", - "_impure_data", - "_reclaim_reent", - "_open_r", - ], - "strncpy_ops": ["strncpy"], - "math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"], - "character_class": ["__chclass"], - "camellia": ["camellia_", "camellia_feistel"], - "crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"], - "event_buffer": ["g_eb_list_desc", "eb_space"], - "base_node": ["base_node_", "base_node_add_handler"], - "file_descriptor": ["s_fd_table"], - "tx_delay": ["tx_delay_cfg"], - "deinit": ["deinit_functions"], - "lcp_echo": ["LcpEchoCheck"], - "raw_api": ["raw_bind", "raw_connect"], - "checksum": ["process_checksum"], - "entry_management": ["add_entry"], - "esp_ota": ["esp_ota", "ota_", "read_otadata"], - "http_server": [ - "httpd_", - "parse_url_char", - "cb_headers_complete", - "delete_entry", - "validate_structure", - "config_save", - "config_new", - "verify_url", - "cb_url", - ], - "misc_system": [ - "alarm_cbs", - "start_up", - "tokens", - "unhex", - "osi_funcs_ro", - "enum_function", - "fragment_and_dispatch", - "alarm_set", - "osi_alarm_new", - "config_set_string", - "config_update_newest_section", - "config_remove_key", - "method_strings", - "interop_match", - "interop_database", - "__state_table", - "__action_table", - "s_stub_table", - "s_context", - "s_mmu_ctx", - "s_get_bus_mask", - "hli_queue_put", - "list_remove", - "list_delete", - "lock_acquire_generic", - "is_vect_desc_usable", - "io_mode_str", - "__c$20233", - "interface", - "read_id_core", - "subscribe_idle", - "unsubscribe_idle", - "s_clkout_handle", - "lock_release_generic", - "config_set_int", - "config_get_int", - "config_get_string", - "config_has_key", - "config_remove_section", - "osi_alarm_init", - "osi_alarm_deinit", - "fixed_queue_enqueue", - "fixed_queue_dequeue", - "fixed_queue_new", - "fixed_pkt_queue_enqueue", - "fixed_pkt_queue_new", - "list_append", - "list_prepend", - "list_insert_after", - "list_contains", - "list_get_node", - "hash_function_blob", - "cb_no_body", - "cb_on_body", - "profile_tab", - "get_arg", - "trim", - "buf$", - "process_appended_hash_and_sig$constprop$0", - "uuidType", - "allocate_svc_db_buf", - "_hostname_is_ours", - "s_hli_handlers", - "tick_cb", - "idle_cb", - "input", - "entry_find", - "section_find", - "find_bucket_entry_", - "config_has_section", - "hli_queue_create", - "hli_queue_get", - "hli_c_handler", - "future_ready", - "future_await", - "future_new", - "pkt_queue_enqueue", - "pkt_queue_dequeue", - "pkt_queue_cleanup", - "pkt_queue_create", - "pkt_queue_destroy", - "fixed_pkt_queue_dequeue", - "osi_alarm_cancel", - "osi_alarm_is_active", - "osi_sem_take", - "osi_event_create", - "osi_event_bind", - "alarm_cb_handler", - "list_foreach", - "list_back", - "list_front", - "list_clear", - "fixed_queue_try_peek_first", - "translate_path", - "get_idx", - "find_key", - "init", - "end", - "start", - "set_read_value", - "copy_address_list", - "copy_and_key", - "sdk_cfg_opts", - "leftshift_onebit", - "config_section_end", - "config_section_begin", - "find_entry_and_check_all_reset", - "image_validate", - "xPendingReadyList", - "vListInitialise", - "lock_init_generic", - "ant_bttx_cfg", - "ant_dft_cfg", - "cs_send_to_ctrl_sock", - "config_llc_util_funcs_reset", - "make_set_adv_report_flow_control", - "make_set_event_mask", - "raw_new", - "raw_remove", - "BTE_InitStack", - "parse_read_local_supported_features_response", - "__math_invalidf", - "tinytens", - "__mprec_tinytens", - "__mprec_bigtens", - "vRingbufferDelete", - "vRingbufferDeleteWithCaps", - "vRingbufferReturnItem", - "vRingbufferReturnItemFromISR", - "get_acl_data_size_ble", - "get_features_ble", - "get_features_classic", - "get_acl_packet_size_ble", - "get_acl_packet_size_classic", - "supports_extended_inquiry_response", - "supports_rssi_with_inquiry_results", - "supports_interlaced_inquiry_scan", - "supports_reading_remote_extended_features", - ], - "bluetooth_ll": [ - "lld_pdu_", - "ld_acl_", - "lld_stop_ind_handler", - "lld_evt_winsize_change", - "config_lld_evt_funcs_reset", - "config_lld_funcs_reset", - "config_llm_funcs_reset", - "llm_set_long_adv_data", - "lld_retry_tx_prog", - "llc_link_sup_to_ind_handler", - "config_llc_funcs_reset", - "lld_evt_rxwin_compute", - "config_btdm_funcs_reset", - "config_ea_funcs_reset", - "llc_defalut_state_tab_reset", - "config_rwip_funcs_reset", - "ke_lmp_rx_flooding_detect", - ], -} - -# Demangled patterns: patterns found in demangled C++ names -DEMANGLED_PATTERNS = { - "gpio_driver": ["GPIO"], - "uart_driver": ["UART"], - "network_stack": [ - "lwip", - "tcp", - "udp", - "ip4", - "ip6", - "dhcp", - "dns", - "netif", - "ethernet", - "ppp", - "slip", - ], - "wifi_stack": ["NetworkInterface"], - "nimble_bt": [ - "nimble", - "NimBLE", - "ble_hs", - "ble_gap", - "ble_gatt", - "ble_att", - "ble_l2cap", - "ble_sm", - ], - "crypto": ["mbedtls", "crypto", "sha", "aes", "rsa", "ecc", "tls", "ssl"], - "cpp_stdlib": ["std::", "__gnu_cxx::", "__cxxabiv"], - "static_init": ["__static_initialization"], - "rtti": ["__type_info", "__class_type_info"], - "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"], - "async_tcp": ["AsyncClient", "AsyncServer"], - "mdns_lib": ["mdns"], - "json_lib": [ - "ArduinoJson", - "JsonDocument", - "JsonArray", - "JsonObject", - "deserialize", - "serialize", - ], - "http_lib": ["HTTP", "http_", "Request", "Response", "Uri", "WebSocket"], - "logging": ["log", "Log", "print", "Print", "diag_"], - "authentication": ["checkDigestAuthentication"], - "libgcc": ["libgcc"], - "esp_system": ["esp_", "ESP"], - "arduino": ["arduino"], - "nvs": ["nvs_", "_ZTVN3nvs", "nvs::"], - "filesystem": ["spiffs", "vfs"], - "libc": ["newlib"], -} - - -# Get the list of actual ESPHome components by scanning the components directory -def get_esphome_components(): - """Get set of actual ESPHome components from the components directory.""" - components = set() - - # Find the components directory relative to this file - current_dir = Path(__file__).parent - components_dir = current_dir / "components" - - if components_dir.exists() and components_dir.is_dir(): - for item in components_dir.iterdir(): - if ( - item.is_dir() - and not item.name.startswith(".") - and not item.name.startswith("__") - ): - components.add(item.name) - - return components - - -# Cache the component list -ESPHOME_COMPONENTS = get_esphome_components() - - -class MemorySection: - """Represents a memory section with its symbols.""" - - def __init__(self, name: str): - self.name = name - self.symbols: list[tuple[str, int, str]] = [] # (symbol_name, size, component) - self.total_size = 0 - - -class ComponentMemory: - """Tracks memory usage for a component.""" - - def __init__(self, name: str): - self.name = name - self.text_size = 0 # Code in flash - self.rodata_size = 0 # Read-only data in flash - self.data_size = 0 # Initialized data (flash + ram) - self.bss_size = 0 # Uninitialized data (ram only) - self.symbol_count = 0 - - @property - def flash_total(self) -> int: - return self.text_size + self.rodata_size + self.data_size - - @property - def ram_total(self) -> int: - return self.data_size + self.bss_size - - -class MemoryAnalyzer: - """Analyzes memory usage from ELF files.""" - - def __init__( - self, - elf_path: str, - objdump_path: str | None = None, - readelf_path: str | None = None, - external_components: set[str] | None = None, - ): - self.elf_path = Path(elf_path) - if not self.elf_path.exists(): - raise FileNotFoundError(f"ELF file not found: {elf_path}") - - self.objdump_path = objdump_path or "objdump" - self.readelf_path = readelf_path or "readelf" - self.external_components = external_components or set() - - self.sections: dict[str, MemorySection] = {} - self.components: dict[str, ComponentMemory] = defaultdict( - lambda: ComponentMemory("") - ) - self._demangle_cache: dict[str, str] = {} - self._uncategorized_symbols: list[tuple[str, str, int]] = [] - self._esphome_core_symbols: list[ - tuple[str, str, int] - ] = [] # Track core symbols - self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict( - list - ) # Track symbols for all components - - def analyze(self) -> dict[str, ComponentMemory]: - """Analyze the ELF file and return component memory usage.""" - self._parse_sections() - self._parse_symbols() - self._categorize_symbols() - return dict(self.components) - - def _parse_sections(self) -> None: - """Parse section headers from ELF file.""" - try: - result = subprocess.run( - [self.readelf_path, "-S", str(self.elf_path)], - capture_output=True, - text=True, - check=True, - ) - - # Parse section headers - for line in result.stdout.splitlines(): - # Look for section entries - match = re.match( - r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)", - line, - ) - if match: - section_name = match.group(1) - size_hex = match.group(2) - size = int(size_hex, 16) - - # Map various section names to standard categories - mapped_section = None - if ".text" in section_name or ".iram" in section_name: - mapped_section = ".text" - elif ".rodata" in section_name: - mapped_section = ".rodata" - elif ".data" in section_name and "bss" not in section_name: - mapped_section = ".data" - elif ".bss" in section_name: - mapped_section = ".bss" - - if mapped_section: - if mapped_section not in self.sections: - self.sections[mapped_section] = MemorySection( - mapped_section - ) - self.sections[mapped_section].total_size += size - - except subprocess.CalledProcessError as e: - _LOGGER.error(f"Failed to parse sections: {e}") - raise - - def _parse_symbols(self) -> None: - """Parse symbols from ELF file.""" - # Section mapping - centralizes the logic - SECTION_MAPPING = { - ".text": [".text", ".iram"], - ".rodata": [".rodata"], - ".data": [".data", ".dram"], - ".bss": [".bss"], - } - - def map_section_name(raw_section: str) -> str | None: - """Map raw section name to standard section.""" - for standard_section, patterns in SECTION_MAPPING.items(): - if any(pattern in raw_section for pattern in patterns): - return standard_section - return None - - def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None: - """Parse a single symbol line from objdump output. - - Returns (section, name, size, address) or None if not a valid symbol. - Format: address l/g w/d F/O section size name - Example: 40084870 l F .iram0.text 00000000 _xt_user_exc - """ - parts = line.split() - if len(parts) < 5: - return None - - try: - # Validate and extract address - address = parts[0] - int(address, 16) - except ValueError: - return None - - # Look for F (function) or O (object) flag - if "F" not in parts and "O" not in parts: - return None - - # Find section, size, and name - for i, part in enumerate(parts): - if part.startswith("."): - section = map_section_name(part) - if section and i + 1 < len(parts): - try: - size = int(parts[i + 1], 16) - if i + 2 < len(parts) and size > 0: - name = " ".join(parts[i + 2 :]) - return (section, name, size, address) - except ValueError: - pass - break - return None - - try: - result = subprocess.run( - [self.objdump_path, "-t", str(self.elf_path)], - capture_output=True, - text=True, - check=True, - ) - - # Track seen addresses to avoid duplicates - seen_addresses: set[str] = set() - - for line in result.stdout.splitlines(): - symbol_info = parse_symbol_line(line) - if symbol_info: - section, name, size, address = symbol_info - # Skip duplicate symbols at the same address (e.g., C1/C2 constructors) - if address not in seen_addresses and section in self.sections: - self.sections[section].symbols.append((name, size, "")) - seen_addresses.add(address) - - except subprocess.CalledProcessError as e: - _LOGGER.error(f"Failed to parse symbols: {e}") - raise - - def _categorize_symbols(self) -> None: - """Categorize symbols by component.""" - # First, collect all unique symbol names for batch demangling - all_symbols = set() - for section in self.sections.values(): - for symbol_name, _, _ in section.symbols: - all_symbols.add(symbol_name) - - # Batch demangle all symbols at once - self._batch_demangle_symbols(list(all_symbols)) - - # Now categorize with cached demangled names - for section_name, section in self.sections.items(): - for symbol_name, size, _ in section.symbols: - component = self._identify_component(symbol_name) - - if component not in self.components: - self.components[component] = ComponentMemory(component) - - comp_mem = self.components[component] - comp_mem.symbol_count += 1 - - if section_name == ".text": - comp_mem.text_size += size - elif section_name == ".rodata": - comp_mem.rodata_size += size - elif section_name == ".data": - comp_mem.data_size += size - elif section_name == ".bss": - comp_mem.bss_size += size - - # Track uncategorized symbols - if component == "other" and size > 0: - demangled = self._demangle_symbol(symbol_name) - self._uncategorized_symbols.append((symbol_name, demangled, size)) - - # Track ESPHome core symbols for detailed analysis - if component == "[esphome]core" and size > 0: - demangled = self._demangle_symbol(symbol_name) - self._esphome_core_symbols.append((symbol_name, demangled, size)) - - # Track all component symbols for detailed analysis - if size > 0: - demangled = self._demangle_symbol(symbol_name) - self._component_symbols[component].append( - (symbol_name, demangled, size) - ) - - def _identify_component(self, symbol_name: str) -> str: - """Identify which component a symbol belongs to.""" - # Demangle C++ names if needed - demangled = self._demangle_symbol(symbol_name) - - # Check for special component classes first (before namespace pattern) - # This handles cases like esphome::ESPHomeOTAComponent which should map to ota - if "esphome::" in demangled: - # Check for special component classes that include component name in the class - # For example: esphome::ESPHomeOTAComponent -> ota component - for component_name in ESPHOME_COMPONENTS: - # Check various naming patterns - component_upper = component_name.upper() - component_camel = component_name.replace("_", "").title() - patterns = [ - f"esphome::{component_upper}Component", # e.g., esphome::OTAComponent - f"esphome::ESPHome{component_upper}Component", # e.g., esphome::ESPHomeOTAComponent - f"esphome::{component_camel}Component", # e.g., esphome::OtaComponent - f"esphome::ESPHome{component_camel}Component", # e.g., esphome::ESPHomeOtaComponent - ] - - if any(pattern in demangled for pattern in patterns): - return f"[esphome]{component_name}" - - # Check for ESPHome component namespaces - match = ESPHOME_COMPONENT_PATTERN.search(demangled) - if match: - component_name = match.group(1) - # Strip trailing underscore if present (e.g., switch_ -> switch) - component_name = component_name.rstrip("_") - - # Check if this is an actual component in the components directory - if component_name in ESPHOME_COMPONENTS: - return f"[esphome]{component_name}" - # Check if this is a known external component from the config - if component_name in self.external_components: - return f"[external]{component_name}" - # Everything else in esphome:: namespace is core - return "[esphome]core" - - # Check for esphome core namespace (no component namespace) - if "esphome::" in demangled: - # If no component match found, it's core - return "[esphome]core" - - # Check against symbol patterns - for component, patterns in SYMBOL_PATTERNS.items(): - if any(pattern in symbol_name for pattern in patterns): - return component - - # Check against demangled patterns - for component, patterns in DEMANGLED_PATTERNS.items(): - if any(pattern in demangled for pattern in patterns): - return component - - # Special cases that need more complex logic - - # Check if spi_flash vs spi_driver - if "spi_" in symbol_name or "SPI" in symbol_name: - if "spi_flash" in symbol_name: - return "spi_flash" - return "spi_driver" - - # libc special printf variants - if symbol_name.startswith("_") and symbol_name[1:].replace("_r", "").replace( - "v", "" - ).replace("s", "") in ["printf", "fprintf", "sprintf", "scanf"]: - return "libc" - - # Track uncategorized symbols for analysis - return "other" - - def _batch_demangle_symbols(self, symbols: list[str]) -> None: - """Batch demangle C++ symbol names for efficiency.""" - if not symbols: - return - - # Try to find the appropriate c++filt for the platform - cppfilt_cmd = "c++filt" - - # Check if we have a toolchain-specific c++filt - if self.objdump_path and self.objdump_path != "objdump": - # Replace objdump with c++filt in the path - potential_cppfilt = self.objdump_path.replace("objdump", "c++filt") - if Path(potential_cppfilt).exists(): - cppfilt_cmd = potential_cppfilt - - try: - # Send all symbols to c++filt at once - result = subprocess.run( - [cppfilt_cmd], - input="\n".join(symbols), - capture_output=True, - text=True, - check=False, - ) - if result.returncode == 0: - demangled_lines = result.stdout.strip().split("\n") - # Map original to demangled names - for original, demangled in zip(symbols, demangled_lines): - self._demangle_cache[original] = demangled - else: - # If batch fails, cache originals - for symbol in symbols: - self._demangle_cache[symbol] = symbol - except Exception: - # On error, cache originals - for symbol in symbols: - self._demangle_cache[symbol] = symbol - - def _demangle_symbol(self, symbol: str) -> str: - """Get demangled C++ symbol name from cache.""" - return self._demangle_cache.get(symbol, symbol) - - def _categorize_esphome_core_symbol(self, demangled: str) -> str: - """Categorize ESPHome core symbols into subcategories.""" - # Dictionary of patterns for core subcategories - CORE_SUBCATEGORY_PATTERNS = { - "Component Framework": ["Component"], - "Application Core": ["Application"], - "Scheduler": ["Scheduler"], - "Logging": ["Logger", "log_"], - "Preferences": ["preferences", "Preferences"], - "Synchronization": ["Mutex", "Lock"], - "Helpers": ["Helper"], - "Network Utilities": ["network", "Network"], - "Time Management": ["time", "Time"], - "String Utilities": ["str_", "string"], - "Parsing/Formatting": ["parse_", "format_"], - "Optional Types": ["optional", "Optional"], - "Callbacks": ["Callback", "callback"], - "Color Utilities": ["Color"], - "C++ Operators": ["operator"], - "Global Variables": ["global_", "_GLOBAL"], - "Setup/Loop": ["setup", "loop"], - "System Control": ["reboot", "restart"], - "GPIO Management": ["GPIO", "gpio"], - "Interrupt Handling": ["ISR", "interrupt"], - "Hooks": ["Hook", "hook"], - "Entity Base Classes": ["Entity"], - "Automation Framework": ["automation", "Automation"], - "Automation Components": ["Condition", "Action", "Trigger"], - "Lambda Support": ["lambda"], - } - - # Special patterns that need to be checked separately - if any(pattern in demangled for pattern in ["vtable", "typeinfo", "thunk"]): - return "C++ Runtime (vtables/RTTI)" - - if demangled.startswith("std::"): - return "C++ STL" - - # Check against patterns - for category, patterns in CORE_SUBCATEGORY_PATTERNS.items(): - if any(pattern in demangled for pattern in patterns): - return category - - return "Other Core" - - def generate_report(self, detailed: bool = False) -> str: - """Generate a formatted memory report.""" - components = sorted( - self.components.items(), key=lambda x: x[1].flash_total, reverse=True - ) - - # Calculate totals - total_flash = sum(c.flash_total for _, c in components) - total_ram = sum(c.ram_total for _, c in components) - - # Build report - lines = [] - - # Column width constants - COL_COMPONENT = 29 - COL_FLASH_TEXT = 14 - COL_FLASH_DATA = 14 - COL_RAM_DATA = 12 - COL_RAM_BSS = 12 - COL_TOTAL_FLASH = 15 - COL_TOTAL_RAM = 12 - COL_SEPARATOR = 3 # " | " - - # Core analysis column widths - COL_CORE_SUBCATEGORY = 30 - COL_CORE_SIZE = 12 - COL_CORE_COUNT = 6 - COL_CORE_PERCENT = 10 - - # Calculate the exact table width - table_width = ( - COL_COMPONENT - + COL_SEPARATOR - + COL_FLASH_TEXT - + COL_SEPARATOR - + COL_FLASH_DATA - + COL_SEPARATOR - + COL_RAM_DATA - + COL_SEPARATOR - + COL_RAM_BSS - + COL_SEPARATOR - + COL_TOTAL_FLASH - + COL_SEPARATOR - + COL_TOTAL_RAM - ) - - lines.append("=" * table_width) - lines.append("Component Memory Analysis".center(table_width)) - lines.append("=" * table_width) - lines.append("") - - # Main table - fixed column widths - lines.append( - f"{'Component':<{COL_COMPONENT}} | {'Flash (text)':>{COL_FLASH_TEXT}} | {'Flash (data)':>{COL_FLASH_DATA}} | {'RAM (data)':>{COL_RAM_DATA}} | {'RAM (bss)':>{COL_RAM_BSS}} | {'Total Flash':>{COL_TOTAL_FLASH}} | {'Total RAM':>{COL_TOTAL_RAM}}" - ) - lines.append( - "-" * COL_COMPONENT - + "-+-" - + "-" * COL_FLASH_TEXT - + "-+-" - + "-" * COL_FLASH_DATA - + "-+-" - + "-" * COL_RAM_DATA - + "-+-" - + "-" * COL_RAM_BSS - + "-+-" - + "-" * COL_TOTAL_FLASH - + "-+-" - + "-" * COL_TOTAL_RAM - ) - - for name, mem in components: - if mem.flash_total > 0 or mem.ram_total > 0: - flash_rodata = mem.rodata_size + mem.data_size - lines.append( - f"{name:<{COL_COMPONENT}} | {mem.text_size:>{COL_FLASH_TEXT - 2},} B | {flash_rodata:>{COL_FLASH_DATA - 2},} B | " - f"{mem.data_size:>{COL_RAM_DATA - 2},} B | {mem.bss_size:>{COL_RAM_BSS - 2},} B | " - f"{mem.flash_total:>{COL_TOTAL_FLASH - 2},} B | {mem.ram_total:>{COL_TOTAL_RAM - 2},} B" - ) - - lines.append( - "-" * COL_COMPONENT - + "-+-" - + "-" * COL_FLASH_TEXT - + "-+-" - + "-" * COL_FLASH_DATA - + "-+-" - + "-" * COL_RAM_DATA - + "-+-" - + "-" * COL_RAM_BSS - + "-+-" - + "-" * COL_TOTAL_FLASH - + "-+-" - + "-" * COL_TOTAL_RAM - ) - lines.append( - f"{'TOTAL':<{COL_COMPONENT}} | {' ':>{COL_FLASH_TEXT}} | {' ':>{COL_FLASH_DATA}} | " - f"{' ':>{COL_RAM_DATA}} | {' ':>{COL_RAM_BSS}} | " - f"{total_flash:>{COL_TOTAL_FLASH - 2},} B | {total_ram:>{COL_TOTAL_RAM - 2},} B" - ) - - # Top consumers - lines.append("") - lines.append("Top Flash Consumers:") - for i, (name, mem) in enumerate(components[:25]): - if mem.flash_total > 0: - percentage = ( - (mem.flash_total / total_flash * 100) if total_flash > 0 else 0 - ) - lines.append( - f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash" - ) - - lines.append("") - lines.append("Top RAM Consumers:") - ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True) - for i, (name, mem) in enumerate(ram_components[:25]): - if mem.ram_total > 0: - percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0 - lines.append( - f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM" - ) - - lines.append("") - lines.append( - "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." - ) - lines.append("=" * table_width) - - # Add ESPHome core detailed analysis if there are core symbols - if self._esphome_core_symbols: - lines.append("") - lines.append("=" * table_width) - lines.append("[esphome]core Detailed Analysis".center(table_width)) - lines.append("=" * table_width) - lines.append("") - - # Group core symbols by subcategory - core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict( - list - ) - - for symbol, demangled, size in self._esphome_core_symbols: - # Categorize based on demangled name patterns - subcategory = self._categorize_esphome_core_symbol(demangled) - core_subcategories[subcategory].append((symbol, demangled, size)) - - # Sort subcategories by total size - sorted_subcategories = sorted( - [ - (name, symbols, sum(s[2] for s in symbols)) - for name, symbols in core_subcategories.items() - ], - key=lambda x: x[2], - reverse=True, - ) - - lines.append( - f"{'Subcategory':<{COL_CORE_SUBCATEGORY}} | {'Size':>{COL_CORE_SIZE}} | " - f"{'Count':>{COL_CORE_COUNT}} | {'% of Core':>{COL_CORE_PERCENT}}" - ) - lines.append( - "-" * COL_CORE_SUBCATEGORY - + "-+-" - + "-" * COL_CORE_SIZE - + "-+-" - + "-" * COL_CORE_COUNT - + "-+-" - + "-" * COL_CORE_PERCENT - ) - - core_total = sum(size for _, _, size in self._esphome_core_symbols) - - for subcategory, symbols, total_size in sorted_subcategories: - percentage = (total_size / core_total * 100) if core_total > 0 else 0 - lines.append( - f"{subcategory:<{COL_CORE_SUBCATEGORY}} | {total_size:>{COL_CORE_SIZE - 2},} B | " - f"{len(symbols):>{COL_CORE_COUNT}} | {percentage:>{COL_CORE_PERCENT - 1}.1f}%" - ) - - # Top 10 largest core symbols - lines.append("") - lines.append("Top 10 Largest [esphome]core Symbols:") - sorted_core_symbols = sorted( - self._esphome_core_symbols, key=lambda x: x[2], reverse=True - ) - - for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]): - lines.append(f"{i + 1}. {demangled} ({size:,} B)") - - lines.append("=" * table_width) - - # Add detailed analysis for top ESPHome and external components - esphome_components = [ - (name, mem) - for name, mem in components - if name.startswith("[esphome]") and name != "[esphome]core" - ] - external_components = [ - (name, mem) for name, mem in components if name.startswith("[external]") - ] - - top_esphome_components = sorted( - esphome_components, key=lambda x: x[1].flash_total, reverse=True - )[:30] - - # Include all external components (they're usually important) - top_external_components = sorted( - external_components, key=lambda x: x[1].flash_total, reverse=True - ) - - # Check if API component exists and ensure it's included - api_component = None - for name, mem in components: - if name == "[esphome]api": - api_component = (name, mem) - break - - # Combine all components to analyze: top ESPHome + all external + API if not already included - components_to_analyze = list(top_esphome_components) + list( - top_external_components - ) - if api_component and api_component not in components_to_analyze: - components_to_analyze.append(api_component) - - if components_to_analyze: - for comp_name, comp_mem in components_to_analyze: - comp_symbols = self._component_symbols.get(comp_name, []) - if comp_symbols: - lines.append("") - lines.append("=" * table_width) - lines.append(f"{comp_name} Detailed Analysis".center(table_width)) - lines.append("=" * table_width) - lines.append("") - - # Sort symbols by size - sorted_symbols = sorted( - comp_symbols, key=lambda x: x[2], reverse=True - ) - - lines.append(f"Total symbols: {len(sorted_symbols)}") - lines.append(f"Total size: {comp_mem.flash_total:,} B") - lines.append("") - - # Show all symbols > 100 bytes for better visibility - large_symbols = [ - (sym, dem, size) - for sym, dem, size in sorted_symbols - if size > 100 - ] - - lines.append( - f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):" - ) - for i, (symbol, demangled, size) in enumerate(large_symbols): - lines.append(f"{i + 1}. {demangled} ({size:,} B)") - - lines.append("=" * table_width) - - 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: - """Dump uncategorized symbols for analysis.""" - # Sort by size descending - sorted_symbols = sorted( - self._uncategorized_symbols, key=lambda x: x[2], reverse=True - ) - - lines = ["Uncategorized Symbols Analysis", "=" * 80] - lines.append(f"Total uncategorized symbols: {len(sorted_symbols)}") - lines.append( - f"Total uncategorized size: {sum(s[2] for s in sorted_symbols):,} bytes" - ) - lines.append("") - lines.append(f"{'Size':>10} | {'Symbol':<60} | Demangled") - lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40) - - for symbol, demangled, size in sorted_symbols[:100]: # Top 100 - if symbol != demangled: - lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled[:100]}") - else: - lines.append(f"{size:>10,} | {symbol[:60]:<60} | [not demangled]") - - if len(sorted_symbols) > 100: - lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols") - - content = "\n".join(lines) - - if output_file: - with open(output_file, "w") as f: - f.write(content) - else: - print(content) - - -def analyze_elf( - elf_path: str, - objdump_path: str | None = None, - readelf_path: str | None = None, - detailed: bool = False, - external_components: set[str] | None = None, -) -> str: - """Analyze an ELF file and return a memory report.""" - analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components) - analyzer.analyze() - return analyzer.generate_report(detailed) - - -if __name__ == "__main__": - import sys - - if len(sys.argv) < 2: - print("Usage: analyze_memory.py ") - sys.exit(1) - - try: - report = analyze_elf(sys.argv[1]) - print(report) - except Exception as e: - print(f"Error: {e}") - sys.exit(1) From 82f7b7f0d50fc8b7835b8fe42b08b0813575c938 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Oct 2025 09:41:04 -1000 Subject: [PATCH 07/46] debug --- script/ci_memory_impact_comment.py | 51 +++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py index 4e3fbb9086..0f6ef6f376 100755 --- a/script/ci_memory_impact_comment.py +++ b/script/ci_memory_impact_comment.py @@ -24,6 +24,37 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) # Comment marker to identify our memory impact comments COMMENT_MARKER = "" + +def run_gh_command(args: list[str], operation: str) -> subprocess.CompletedProcess: + """Run a gh CLI command with error handling. + + Args: + args: Command arguments (including 'gh') + operation: Description of the operation for error messages + + Returns: + CompletedProcess result + + Raises: + subprocess.CalledProcessError: If command fails (with detailed error output) + """ + try: + return subprocess.run( + args, + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + print( + f"ERROR: {operation} failed with exit code {e.returncode}", file=sys.stderr + ) + print(f"ERROR: Command: {' '.join(args)}", file=sys.stderr) + print(f"ERROR: stdout: {e.stdout}", file=sys.stderr) + print(f"ERROR: stderr: {e.stderr}", file=sys.stderr) + raise + + # Thresholds for emoji significance indicators (percentage) OVERALL_CHANGE_THRESHOLD = 1.0 # Overall RAM/Flash changes COMPONENT_CHANGE_THRESHOLD = 3.0 # Component breakdown changes @@ -356,7 +387,7 @@ def find_existing_comment(pr_number: str) -> str | None: print(f"DEBUG: Looking for existing comment on PR #{pr_number}", file=sys.stderr) # Use gh api to get comments directly - this returns the numeric id field - result = subprocess.run( + result = run_gh_command( [ "gh", "api", @@ -364,9 +395,7 @@ def find_existing_comment(pr_number: str) -> str | None: "--jq", ".[] | {id, body}", ], - capture_output=True, - text=True, - check=True, + operation="Get PR comments", ) print( @@ -420,7 +449,8 @@ def update_existing_comment(comment_id: str, comment_body: str) -> None: subprocess.CalledProcessError: If gh command fails """ print(f"DEBUG: Updating existing comment {comment_id}", file=sys.stderr) - result = subprocess.run( + print(f"DEBUG: Comment body length: {len(comment_body)} bytes", file=sys.stderr) + result = run_gh_command( [ "gh", "api", @@ -430,9 +460,7 @@ def update_existing_comment(comment_id: str, comment_body: str) -> None: "-f", f"body={comment_body}", ], - check=True, - capture_output=True, - text=True, + operation="Update PR comment", ) print(f"DEBUG: Update response: {result.stdout}", file=sys.stderr) @@ -448,11 +476,10 @@ def create_new_comment(pr_number: str, comment_body: str) -> None: subprocess.CalledProcessError: If gh command fails """ print(f"DEBUG: Posting new comment on PR #{pr_number}", file=sys.stderr) - result = subprocess.run( + print(f"DEBUG: Comment body length: {len(comment_body)} bytes", file=sys.stderr) + result = run_gh_command( ["gh", "pr", "comment", pr_number, "--body", comment_body], - check=True, - capture_output=True, - text=True, + operation="Create PR comment", ) print(f"DEBUG: Post response: {result.stdout}", file=sys.stderr) From f7bcf8721313ce0a73415bd0711fd784ad6a09fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Oct 2025 19:13:12 -1000 Subject: [PATCH 08/46] more filter cleanups --- esphome/components/sensor/filter.cpp | 16 ++++--- esphome/components/sensor/filter.h | 13 +++--- tests/components/sensor/common.yaml | 63 ++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 0d57c792db..e8d04d161b 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -313,7 +313,7 @@ optional DeltaFilter::new_value(float value) { } // OrFilter -OrFilter::OrFilter(std::vector filters) : filters_(std::move(filters)), phi_(this) {} +OrFilter::OrFilter(std::initializer_list filters) : filters_(filters), phi_(this) {} OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} optional OrFilter::PhiNode::new_value(float value) { @@ -326,14 +326,14 @@ optional OrFilter::PhiNode::new_value(float value) { } optional OrFilter::new_value(float value) { this->has_value_ = false; - for (Filter *filter : this->filters_) + for (auto *filter : this->filters_) filter->input(value); return {}; } void OrFilter::initialize(Sensor *parent, Filter *next) { Filter::initialize(parent, next); - for (Filter *filter : this->filters_) { + for (auto *filter : this->filters_) { filter->initialize(parent, &this->phi_); } this->phi_.initialize(parent, nullptr); @@ -386,18 +386,24 @@ void HeartbeatFilter::setup() { } float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } +CalibrateLinearFilter::CalibrateLinearFilter(std::initializer_list> linear_functions) + : linear_functions_(linear_functions) {} + optional CalibrateLinearFilter::new_value(float value) { - for (std::array f : this->linear_functions_) { + for (const auto &f : this->linear_functions_) { if (!std::isfinite(f[2]) || value < f[2]) return (value * f[0]) + f[1]; } return NAN; } +CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list coefficients) + : coefficients_(coefficients) {} + optional CalibratePolynomialFilter::new_value(float value) { float res = 0.0f; float x = 1.0f; - for (float coefficient : this->coefficients_) { + for (const auto &coefficient : this->coefficients_) { res += x * coefficient; x *= value; } diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index e09c66afcb..03a1e0f24c 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -422,7 +422,7 @@ class DeltaFilter : public Filter { class OrFilter : public Filter { public: - explicit OrFilter(std::vector filters); + explicit OrFilter(std::initializer_list filters); void initialize(Sensor *parent, Filter *next) override; @@ -438,28 +438,27 @@ class OrFilter : public Filter { OrFilter *or_parent_; }; - std::vector filters_; + FixedVector filters_; PhiNode phi_; bool has_value_{false}; }; class CalibrateLinearFilter : public Filter { public: - CalibrateLinearFilter(std::vector> linear_functions) - : linear_functions_(std::move(linear_functions)) {} + explicit CalibrateLinearFilter(std::initializer_list> linear_functions); optional new_value(float value) override; protected: - std::vector> linear_functions_; + FixedVector> linear_functions_; }; class CalibratePolynomialFilter : public Filter { public: - CalibratePolynomialFilter(std::vector coefficients) : coefficients_(std::move(coefficients)) {} + explicit CalibratePolynomialFilter(std::initializer_list coefficients); optional new_value(float value) override; protected: - std::vector coefficients_; + FixedVector coefficients_; }; class ClampFilter : public Filter { diff --git a/tests/components/sensor/common.yaml b/tests/components/sensor/common.yaml index 3f81f3f9ef..2180f66da8 100644 --- a/tests/components/sensor/common.yaml +++ b/tests/components/sensor/common.yaml @@ -173,3 +173,66 @@ sensor: timeout: 1000ms value: [42.0] - multiply: 2.0 + + # CalibrateLinearFilter - piecewise linear calibration + - platform: copy + source_id: source_sensor + name: "Calibrate Linear Two Points" + filters: + - calibrate_linear: + - 0.0 -> 0.0 + - 100.0 -> 100.0 + + - platform: copy + source_id: source_sensor + name: "Calibrate Linear Multiple Segments" + filters: + - calibrate_linear: + - 0.0 -> 0.0 + - 50.0 -> 55.0 + - 100.0 -> 102.5 + + - platform: copy + source_id: source_sensor + name: "Calibrate Linear Least Squares" + filters: + - calibrate_linear: + method: least_squares + datapoints: + - 0.0 -> 0.0 + - 50.0 -> 55.0 + - 100.0 -> 102.5 + + # CalibratePolynomialFilter - polynomial calibration + - platform: copy + source_id: source_sensor + name: "Calibrate Polynomial Degree 2" + filters: + - calibrate_polynomial: + degree: 2 + datapoints: + - 0.0 -> 0.0 + - 50.0 -> 55.0 + - 100.0 -> 102.5 + + - platform: copy + source_id: source_sensor + name: "Calibrate Polynomial Degree 3" + filters: + - calibrate_polynomial: + degree: 3 + datapoints: + - 0.0 -> 0.0 + - 25.0 -> 26.0 + - 50.0 -> 55.0 + - 100.0 -> 102.5 + + # OrFilter - filter branching + - platform: copy + source_id: source_sensor + name: "Or Filter with Multiple Branches" + filters: + - or: + - multiply: 2.0 + - offset: 10.0 + - lambda: return x * 3.0; From 8c115ab07b1e7b3f4ccdc583af87fa8c47944269 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Oct 2025 20:12:51 -1000 Subject: [PATCH 09/46] more cleanup --- esphome/analyze_memory/cli.py | 27 ++- esphome/analyze_memory/const.py | 288 ++++++++++++++++++++++++-------- 2 files changed, 242 insertions(+), 73 deletions(-) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 2986922ac2..1621eeaf93 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -232,9 +232,30 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): api_component = (name, mem) break - # Combine all components to analyze: top ESPHome + all external + API if not already included - components_to_analyze = list(top_esphome_components) + list( - top_external_components + # Also include wifi_stack and other important system components if they exist + system_components_to_include = [ + "wifi_stack", + "bluetooth", + "network_stack", + "cpp_runtime", + "other", + "libc", + "phy_radio", + "mdns_lib", + "nvs", + "ota", + "arduino_core", + ] + system_components = [] + for name, mem in components: + if name in system_components_to_include: + system_components.append((name, mem)) + + # Combine all components to analyze: top ESPHome + all external + API if not already included + system components + components_to_analyze = ( + list(top_esphome_components) + + list(top_external_components) + + system_components ) if api_component and api_component not in components_to_analyze: components_to_analyze.append(api_component) diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index c60b70aeec..0410788fdd 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -127,40 +127,39 @@ SYMBOL_PATTERNS = { "tryget_socket_unconn", "cs_create_ctrl_sock", "netbuf_alloc", + "tcp_", # TCP protocol functions + "udp_", # UDP protocol functions + "lwip_", # LwIP stack functions + "eagle_lwip", # ESP-specific LwIP functions + "new_linkoutput", # Link output function + "acd_", # Address Conflict Detection (ACD) + "eth_", # Ethernet functions + "mac_enable_bb", # MAC baseband enable + "reassemble_and_dispatch", # Packet reassembly ], + # dhcp must come before libc to avoid "dhcp_select" matching "select" pattern + "dhcp": ["dhcp", "handle_dhcp"], "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], - "wifi_stack": [ - "ieee80211", - "hostap", - "sta_", - "ap_", - "scan_", - "wifi_", - "wpa_", - "wps_", - "esp_wifi", - "cnx_", - "wpa3_", - "sae_", - "wDev_", - "ic_", - "mac_", - "esf_buf", - "gWpaSm", - "sm_WPA", - "eapol_", - "owe_", - "wifiLowLevelInit", - "s_do_mapping", - "gScanStruct", - "ppSearchTxframe", - "ppMapWaitTxq", - "ppFillAMPDUBar", - "ppCheckTxConnTrafficIdle", - "ppCalTkipMic", + # Order matters! More specific categories must come before general ones. + # mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern + "mdns_lib": ["mdns"], + # memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols + "memory_mgmt": [ + "mem_", + "memory_", + "tlsf_", + "memp_", + "pbuf_", + "pbuf_alloc", + "pbuf_copy_partial_pbuf", + "esp_mmu_map", + "mmu_hal_", + "s_do_mapping", # Memory mapping function, not WiFi + "hash_map_", # Hash map data structure + "umm_assimilate", # UMM malloc assimilation ], - "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"], - "wifi_bt_coex": ["coex"], + # Bluetooth categories must come BEFORE wifi_stack to avoid misclassification + # Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], "bluedroid_bt": [ "bluedroid", @@ -207,6 +206,60 @@ SYMBOL_PATTERNS = { "copy_extra_byte_in_db", "parse_read_local_supported_commands_response", ], + "bluetooth": [ + "bt_", + "_ble_", # More specific than "ble_" to avoid matching "able_", "enable_", "disable_" + "l2c_", + "l2ble_", # L2CAP for BLE + "gatt_", + "gap_", + "hci_", + "btsnd_hcic_", # Bluetooth HCI command send functions + "BT_init", + "BT_tx_", # Bluetooth transmit functions + "esp_ble_", # Catch esp_ble_* functions + ], + "bluetooth_ll": [ + "llm_", # Link layer manager + "llc_", # Link layer control + "lld_", # Link layer driver + "llcp_", # Link layer control protocol + "lmp_", # Link manager protocol + ], + "wifi_bt_coex": ["coex"], + "wifi_stack": [ + "ieee80211", + "hostap", + "sta_", + "wifi_ap_", # More specific than "ap_" to avoid matching "cap_", "map_" + "wifi_scan_", # More specific than "scan_" to avoid matching "_scan_" in other contexts + "wifi_", + "wpa_", + "wps_", + "esp_wifi", + "cnx_", + "wpa3_", + "sae_", + "wDev_", + "ic_mac_", # More specific than "mac_" to avoid matching emac_ + "esf_buf", + "gWpaSm", + "sm_WPA", + "eapol_", + "owe_", + "wifiLowLevelInit", + # Removed "s_do_mapping" - this is memory management, not WiFi + "gScanStruct", + "ppSearchTxframe", + "ppMapWaitTxq", + "ppFillAMPDUBar", + "ppCheckTxConnTrafficIdle", + "ppCalTkipMic", + "phy_force_wifi", + "phy_unforce_wifi", + "write_wifi_chan", + "wifi_track_pll", + ], "crypto_math": [ "ecp_", "bignum_", @@ -231,13 +284,36 @@ SYMBOL_PATTERNS = { "p_256_init_curve", "shift_sub_rows", "rshift", + "rijndaelEncrypt", # AES Rijndael encryption + ], + # System and Arduino core functions must come before libc + "esp_system": [ + "system_", # ESP system functions + "postmortem_", # Postmortem reporting + ], + "arduino_core": [ + "pinMode", + "resetPins", + "millis", + "micros", + "delay(", # More specific - Arduino delay function with parenthesis + "delayMicroseconds", + "digitalWrite", + "digitalRead", + ], + "sntp": ["sntp_", "sntp_recv"], + "scheduler": [ + "run_scheduled_", + "compute_scheduled_", + "event_TaskQueue", ], "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], "libc": [ "printf", "scanf", "malloc", - "free", + "_free", # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring + "umm_free", # UMM malloc free function "memcpy", "memset", "strcpy", @@ -259,7 +335,7 @@ SYMBOL_PATTERNS = { "_setenv_r", "_tzset_unlocked_r", "__tzcalc_limits", - "select", + "_select", # More specific than "select" to avoid matching "dhcp_select", etc. "scalbnf", "strtof", "strtof_l", @@ -316,8 +392,24 @@ SYMBOL_PATTERNS = { "CSWTCH$", "dst$", "sulp", + "_strtol_l", # String to long with locale + "__cvt", # Convert + "__utoa", # Unsigned to ASCII + "__global_locale", # Global locale + "_ctype_", # Character type + "impure_data", # Impure data + ], + "string_ops": [ + "strcmp", + "strncmp", + "strchr", + "strstr", + "strtok", + "strdup", + "strncasecmp_P", # String compare (case insensitive, from program memory) + "strnlen_P", # String length (from program memory) + "strncat_P", # String concatenate (from program memory) ], - "string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"], "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], "file_io": [ "fread", @@ -338,10 +430,26 @@ SYMBOL_PATTERNS = { "vsscanf", ], "cpp_anonymous": ["_GLOBAL__N_", "n$"], - "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"], - "exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"], + # Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS + "nvs": ["nvs_"], # Plain C NVS functions + "ota": ["ota_", "OTA", "esp_ota", "app_desc"], + # cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly + # Only keep patterns that are truly runtime-specific and not categorizable by namespace + "cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"], + "exception_handling": [ + "__cxa_", + "_Unwind_", + "__gcc_personality", + "uw_frame_state", + "search_object", # Search for exception handling object + "get_cie_encoding", # Get CIE encoding + "add_fdes", # Add frame description entries + "fde_unencoded_compare", # Compare FDEs + "fde_mixed_encoding_compare", # Compare mixed encoding FDEs + "frame_downheap", # Frame heap operations + "frame_heapsort", # Frame heap sorting + ], "static_init": ["_GLOBAL__sub_I_"], - "mdns_lib": ["mdns"], "phy_radio": [ "phy_", "rf_", @@ -394,10 +502,47 @@ SYMBOL_PATTERNS = { "txcal_debuge_mode", "ant_wifitx_cfg", "reg_init_begin", + "tx_cap_init", # TX capacitance init + "ram_set_txcap", # RAM TX capacitance setting + "tx_atten_", # TX attenuation + "txiq_", # TX I/Q calibration + "ram_cal_", # RAM calibration + "ram_rxiq_", # RAM RX I/Q + "readvdd33", # Read VDD33 + "test_tout", # Test timeout + "tsen_meas", # Temperature sensor measurement + "bbpll_cal", # Baseband PLL calibration + "set_cal_", # Set calibration + "set_rfanagain_", # Set RF analog gain + "set_txdc_", # Set TX DC + "get_vdd33_", # Get VDD33 + "gen_rx_gain_table", # Generate RX gain table + "ram_ana_inf_gating_en", # RAM analog interface gating enable + "tx_cont_en", # TX continuous enable + "tx_delay_cfg", # TX delay configuration + "tx_gain_table_set", # TX gain table set + "check_and_reset_hw_deadlock", # Hardware deadlock check + "s_config", # System/hardware config + "chan14_mic_cfg", # Channel 14 MIC config + ], + "wifi_phy_pp": [ + "pp_", + "ppT", + "ppR", + "ppP", + "ppInstall", + "ppCalTxAMPDULength", + "ppCheckTx", # Packet processor TX check + "ppCal", # Packet processor calibration + "HdlAllBuffedEb", # Handle buffered EB ], - "wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"], "wifi_lmac": ["lmac"], - "wifi_device": ["wdev", "wDev_"], + "wifi_device": [ + "wdev", + "wDev_", + "ic_set_sta", # Set station mode + "ic_set_vif", # Set virtual interface + ], "power_mgmt": [ "pm_", "sleep", @@ -406,15 +551,7 @@ SYMBOL_PATTERNS = { "deep_sleep", "power_down", "g_pm", - ], - "memory_mgmt": [ - "mem_", - "memory_", - "tlsf_", - "memp_", - "pbuf_", - "pbuf_alloc", - "pbuf_copy_partial_pbuf", + "pmc", # Power Management Controller ], "hal_layer": ["hal_"], "clock_mgmt": [ @@ -439,7 +576,6 @@ SYMBOL_PATTERNS = { "error_handling": ["panic", "abort", "assert", "error_", "fault"], "authentication": ["auth"], "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"], - "dhcp": ["dhcp", "handle_dhcp"], "ethernet_phy": [ "emac_", "eth_phy_", @@ -618,7 +754,15 @@ SYMBOL_PATTERNS = { "ampdu_dispatch_upto", ], "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], - "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"], + "rate_control": [ + "rssi_margin", + "rcGetSched", + "get_rate_fcc_index", + "rcGetRate", # Get rate + "rc_get_", # Rate control getters + "rc_set_", # Rate control setters + "rc_enable_", # Rate control enable functions + ], "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"], "channel_mgmt": ["chm_init", "chm_set_current_channel"], "trace": ["trc_init", "trc_onAmpduOp"], @@ -799,31 +943,18 @@ SYMBOL_PATTERNS = { "supports_interlaced_inquiry_scan", "supports_reading_remote_extended_features", ], - "bluetooth_ll": [ - "lld_pdu_", - "ld_acl_", - "lld_stop_ind_handler", - "lld_evt_winsize_change", - "config_lld_evt_funcs_reset", - "config_lld_funcs_reset", - "config_llm_funcs_reset", - "llm_set_long_adv_data", - "lld_retry_tx_prog", - "llc_link_sup_to_ind_handler", - "config_llc_funcs_reset", - "lld_evt_rxwin_compute", - "config_btdm_funcs_reset", - "config_ea_funcs_reset", - "llc_defalut_state_tab_reset", - "config_rwip_funcs_reset", - "ke_lmp_rx_flooding_detect", - ], } # Demangled patterns: patterns found in demangled C++ names DEMANGLED_PATTERNS = { "gpio_driver": ["GPIO"], "uart_driver": ["UART"], + # mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder + "mdns_lib": [ + "MDNSResponder", + "MDNSImplementation", + "MDNS", + ], "network_stack": [ "lwip", "tcp", @@ -836,6 +967,24 @@ DEMANGLED_PATTERNS = { "ethernet", "ppp", "slip", + "UdpContext", # UDP context class + "DhcpServer", # DHCP server class + ], + "arduino_core": [ + "String::", # Arduino String class + "Print::", # Arduino Print class + "HardwareSerial::", # Serial class + "IPAddress::", # IP address class + "EspClass::", # ESP class + "experimental::_SPI", # Experimental SPI + ], + "ota": [ + "UpdaterClass", + "Updater::", + ], + "wifi": [ + "ESP8266WiFi", + "WiFi::", ], "wifi_stack": ["NetworkInterface"], "nimble_bt": [ @@ -854,7 +1003,6 @@ DEMANGLED_PATTERNS = { "rtti": ["__type_info", "__class_type_info"], "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"], "async_tcp": ["AsyncClient", "AsyncServer"], - "mdns_lib": ["mdns"], "json_lib": [ "ArduinoJson", "JsonDocument", From 5b4e50d27933845d7cf39f58254e029e0b485ff3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Oct 2025 20:13:20 -1000 Subject: [PATCH 10/46] more cleanup --- esphome/analyze_memory/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 0410788fdd..78af82059f 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -223,6 +223,7 @@ SYMBOL_PATTERNS = { "llm_", # Link layer manager "llc_", # Link layer control "lld_", # Link layer driver + "ld_acl_", # Link layer ACL (Asynchronous Connection-Oriented) "llcp_", # Link layer control protocol "lmp_", # Link manager protocol ], From b9efaabdf0dae300b6547c684d7b12bf779e09ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Oct 2025 20:15:12 -1000 Subject: [PATCH 11/46] more cleanup --- esphome/analyze_memory/cli.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 1621eeaf93..a38a30ac34 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -234,17 +234,8 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): # Also include wifi_stack and other important system components if they exist system_components_to_include = [ - "wifi_stack", - "bluetooth", - "network_stack", - "cpp_runtime", - "other", - "libc", - "phy_radio", - "mdns_lib", - "nvs", - "ota", - "arduino_core", + # Empty list - we've finished debugging symbol categorization + # Add component names here if you need to debug their symbols ] system_components = [] for name, mem in components: From 110f23caff2e48b29ade33e64bbfdcee56acc62b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Oct 2025 21:34:14 -1000 Subject: [PATCH 12/46] fix --- esphome/components/openthread/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5277455eca..4865399d02 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -107,6 +107,14 @@ _CONNECTION_SCHEMA = cv.Schema( } ) + +def _require_vfs_select(config): + """Register VFS select requirement during config validation.""" + # OpenThread uses esp_vfs_eventfd which requires VFS select support + require_vfs_select() + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -123,6 +131,7 @@ CONFIG_SCHEMA = cv.All( cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), cv.only_with_esp_idf, only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), + _require_vfs_select, ) @@ -142,9 +151,6 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): cg.add_define("USE_OPENTHREAD") - # OpenThread uses esp_vfs_eventfd which requires VFS select support - require_vfs_select() - # OpenThread SRP needs access to mDNS services after setup enable_mdns_storage() From 375adbb86f6eb9da19ecfa773290c76e92e6a57f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Oct 2025 22:09:22 -1000 Subject: [PATCH 13/46] [binary_sensor] Optimize AutorepeatFilter with FixedVector --- esphome/components/binary_sensor/__init__.py | 35 +++++++++++++------- esphome/components/binary_sensor/filter.cpp | 3 +- esphome/components/binary_sensor/filter.h | 11 ++---- tests/components/binary_sensor/common.yaml | 33 ++++++++++++++++++ 4 files changed, 59 insertions(+), 23 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 6aa97d6e05..26e784a0b8 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -264,20 +264,31 @@ async def delayed_off_filter_to_code(config, filter_id): ), ) async def autorepeat_filter_to_code(config, filter_id): - timings = [] if len(config) > 0: - timings.extend( - (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON]) - for conf in config - ) - else: - timings.append( - ( - cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds, - cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds, - cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds, + timings = [ + cg.StructInitializer( + cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"), + ("delay", conf[CONF_DELAY]), + ("time_off", conf[CONF_TIME_OFF]), + ("time_on", conf[CONF_TIME_ON]), ) - ) + for conf in config + ] + else: + timings = [ + cg.StructInitializer( + cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"), + ("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds), + ( + "time_off", + cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds, + ), + ( + "time_on", + cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds, + ), + ) + ] var = cg.new_Pvariable(filter_id, timings) await cg.register_component(var, {}) return var diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 3567e9c72b..8f31cf6fc2 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -1,7 +1,6 @@ #include "filter.h" #include "binary_sensor.h" -#include namespace esphome { @@ -68,7 +67,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD optional InvertFilter::new_value(bool value) { return !value; } -AutorepeatFilter::AutorepeatFilter(std::vector timings) : timings_(std::move(timings)) {} +AutorepeatFilter::AutorepeatFilter(std::initializer_list timings) : timings_(timings) {} optional AutorepeatFilter::new_value(bool value) { if (value) { diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 16f44aa5fe..a7eb080feb 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -4,8 +4,6 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include - namespace esphome { namespace binary_sensor { @@ -82,11 +80,6 @@ class InvertFilter : public Filter { }; struct AutorepeatFilterTiming { - AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) { - this->delay = delay; - this->time_off = off; - this->time_on = on; - } uint32_t delay; uint32_t time_off; uint32_t time_on; @@ -94,7 +87,7 @@ struct AutorepeatFilterTiming { class AutorepeatFilter : public Filter, public Component { public: - explicit AutorepeatFilter(std::vector timings); + explicit AutorepeatFilter(std::initializer_list timings); optional new_value(bool value) override; @@ -104,7 +97,7 @@ class AutorepeatFilter : public Filter, public Component { void next_timing_(); void next_value_(bool val); - std::vector timings_; + FixedVector timings_; uint8_t active_timing_{0}; }; diff --git a/tests/components/binary_sensor/common.yaml b/tests/components/binary_sensor/common.yaml index ed6322768f..6965c1feeb 100644 --- a/tests/components/binary_sensor/common.yaml +++ b/tests/components/binary_sensor/common.yaml @@ -37,3 +37,36 @@ binary_sensor: format: "New state is %s" args: ['x.has_value() ? ONOFF(x) : "Unknown"'] - binary_sensor.invalidate_state: some_binary_sensor + + # Test autorepeat with default configuration (no timings) + - platform: template + id: autorepeat_default + name: "Autorepeat Default" + filters: + - autorepeat: + + # Test autorepeat with single timing entry + - platform: template + id: autorepeat_single + name: "Autorepeat Single" + filters: + - autorepeat: + - delay: 2s + time_off: 200ms + time_on: 800ms + + # Test autorepeat with three timing entries + - platform: template + id: autorepeat_multiple + name: "Autorepeat Multiple" + filters: + - autorepeat: + - delay: 500ms + time_off: 50ms + time_on: 950ms + - delay: 2s + time_off: 100ms + time_on: 900ms + - delay: 10s + time_off: 200ms + time_on: 800ms From bc296d05fb83c901d8f36ac11c971ab94d4ed1db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 16:57:18 -1000 Subject: [PATCH 14/46] wip --- esphome/components/api/api_connection.cpp | 12 +- .../components/climate/climate_mode_bitmask.h | 101 ++++++++++++ esphome/components/climate/climate_traits.h | 128 +++++++++------ esphome/core/enum_bitmask.h | 155 ++++++++++++++++++ 4 files changed, 336 insertions(+), 60 deletions(-) create mode 100644 esphome/components/climate/climate_mode_bitmask.h create mode 100644 esphome/core/enum_bitmask.h diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7c135946f8..6f6bd27e6e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -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); // Current feature flags and other supported parameters msg.feature_flags = traits.get_feature_flags(); - msg.supported_modes = &traits.get_supported_modes_for_api_(); + msg.supported_modes = &traits.get_supported_modes(); msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity(); - msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); - msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); - msg.supported_presets = &traits.get_supported_presets_for_api_(); - msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); - msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); + msg.supported_fan_modes = &traits.get_supported_fan_modes(); + msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes(); + msg.supported_presets = &traits.get_supported_presets(); + msg.supported_custom_presets = &traits.get_supported_custom_presets(); + msg.supported_swing_modes = &traits.get_supported_swing_modes(); return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/climate/climate_mode_bitmask.h b/esphome/components/climate/climate_mode_bitmask.h new file mode 100644 index 0000000000..236d153659 --- /dev/null +++ b/esphome/components/climate/climate_mode_bitmask.h @@ -0,0 +1,101 @@ +#pragma once + +#include "esphome/core/enum_bitmask.h" +#include "climate_mode.h" + +namespace esphome { +namespace climate { + +// Type aliases for climate enum bitmasks +// These replace std::set to eliminate red-black tree overhead + +using ClimateModeMask = EnumBitmask; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) +using ClimateFanModeMask = + EnumBitmask; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) +using ClimateSwingModeMask = EnumBitmask; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) +using ClimatePresetMask = + EnumBitmask; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) + +} // namespace climate +} // namespace esphome + +// Template specializations for enum-to-bit conversions +// All climate enums are sequential starting from 0, so conversions are trivial + +namespace esphome { + +// ClimateMode specialization (7 values: 0-6) +template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> constexpr climate::ClimateMode EnumBitmask::bit_to_enum(int bit) { + // Compile-time lookup array mapping bit positions to enum values + static constexpr climate::ClimateMode MODES[] = { + climate::CLIMATE_MODE_OFF, // bit 0 + climate::CLIMATE_MODE_HEAT_COOL, // bit 1 + climate::CLIMATE_MODE_COOL, // bit 2 + climate::CLIMATE_MODE_HEAT, // bit 3 + climate::CLIMATE_MODE_FAN_ONLY, // bit 4 + climate::CLIMATE_MODE_DRY, // bit 5 + climate::CLIMATE_MODE_AUTO, // bit 6 + }; + return (bit >= 0 && bit < 7) ? MODES[bit] : climate::CLIMATE_MODE_OFF; +} + +// ClimateFanMode specialization (10 values: 0-9) +template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateFanMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> constexpr climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateFanMode MODES[] = { + climate::CLIMATE_FAN_ON, // bit 0 + climate::CLIMATE_FAN_OFF, // bit 1 + climate::CLIMATE_FAN_AUTO, // bit 2 + climate::CLIMATE_FAN_LOW, // bit 3 + climate::CLIMATE_FAN_MEDIUM, // bit 4 + climate::CLIMATE_FAN_HIGH, // bit 5 + climate::CLIMATE_FAN_MIDDLE, // bit 6 + climate::CLIMATE_FAN_FOCUS, // bit 7 + climate::CLIMATE_FAN_DIFFUSE, // bit 8 + climate::CLIMATE_FAN_QUIET, // bit 9 + }; + return (bit >= 0 && bit < 10) ? MODES[bit] : climate::CLIMATE_FAN_ON; +} + +// ClimateSwingMode specialization (4 values: 0-3) +template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateSwingMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> constexpr climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateSwingMode MODES[] = { + climate::CLIMATE_SWING_OFF, // bit 0 + climate::CLIMATE_SWING_BOTH, // bit 1 + climate::CLIMATE_SWING_VERTICAL, // bit 2 + climate::CLIMATE_SWING_HORIZONTAL, // bit 3 + }; + return (bit >= 0 && bit < 4) ? MODES[bit] : climate::CLIMATE_SWING_OFF; +} + +// ClimatePreset specialization (8 values: 0-7) +template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimatePreset preset) { + return static_cast(preset); // Direct mapping: enum value = bit position +} + +template<> constexpr climate::ClimatePreset EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimatePreset PRESETS[] = { + climate::CLIMATE_PRESET_NONE, // bit 0 + climate::CLIMATE_PRESET_HOME, // bit 1 + climate::CLIMATE_PRESET_AWAY, // bit 2 + climate::CLIMATE_PRESET_BOOST, // bit 3 + climate::CLIMATE_PRESET_COMFORT, // bit 4 + climate::CLIMATE_PRESET_ECO, // bit 5 + climate::CLIMATE_PRESET_SLEEP, // bit 6 + climate::CLIMATE_PRESET_ACTIVITY, // bit 7 + }; + return (bit >= 0 && bit < 8) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; +} + +} // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 2962a147d7..45287689c9 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -1,9 +1,26 @@ #pragma once -#include +#include #include "climate_mode.h" +#include "climate_mode_bitmask.h" #include "esphome/core/helpers.h" +namespace esphome { +namespace climate { + +// Lightweight linear search for small vectors (1-20 items) +// Avoids std::find template overhead +template inline bool vector_contains(const std::vector &vec, const T &value) { + for (const auto &item : vec) { + if (item == value) + return true; + } + return false; +} + +} // namespace climate +} // namespace esphome + namespace esphome { #ifdef USE_API @@ -107,48 +124,68 @@ class ClimateTraits { } } - void set_supported_modes(std::set modes) { this->supported_modes_ = std::move(modes); } - void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } - bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } - const std::set &get_supported_modes() const { return this->supported_modes_; } + void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } + void set_supported_modes(std::initializer_list modes) { + this->supported_modes_ = ClimateModeMask(modes); + } + void add_supported_mode(ClimateMode mode) { this->supported_modes_.add(mode); } + bool supports_mode(ClimateMode mode) const { return this->supported_modes_.contains(mode); } + const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } - void set_supported_fan_modes(std::set modes) { this->supported_fan_modes_ = std::move(modes); } - 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_.insert(mode); } - bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } + void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } + void set_supported_fan_modes(std::initializer_list modes) { + this->supported_fan_modes_ = ClimateFanModeMask(modes); + } + void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.add(mode); } + void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } + bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.contains(fan_mode); } bool get_supports_fan_modes() const { return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); } - const std::set &get_supported_fan_modes() const { return this->supported_fan_modes_; } + const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } - void set_supported_custom_fan_modes(std::set supported_custom_fan_modes) { + void set_supported_custom_fan_modes(std::vector supported_custom_fan_modes) { this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); } - const std::set &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } + void set_supported_custom_fan_modes(std::initializer_list modes) { + this->supported_custom_fan_modes_ = modes; + } + const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { - return this->supported_custom_fan_modes_.count(custom_fan_mode); + return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); } - void set_supported_presets(std::set presets) { this->supported_presets_ = std::move(presets); } - void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(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); } + void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } + void set_supported_presets(std::initializer_list presets) { + this->supported_presets_ = ClimatePresetMask(presets); + } + void add_supported_preset(ClimatePreset preset) { this->supported_presets_.add(preset); } + void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } + bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.contains(preset); } bool get_supports_presets() const { return !this->supported_presets_.empty(); } - const std::set &get_supported_presets() const { return this->supported_presets_; } + const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } - void set_supported_custom_presets(std::set supported_custom_presets) { + void set_supported_custom_presets(std::vector supported_custom_presets) { this->supported_custom_presets_ = std::move(supported_custom_presets); } - const std::set &get_supported_custom_presets() const { return this->supported_custom_presets_; } + void set_supported_custom_presets(std::initializer_list presets) { + this->supported_custom_presets_ = presets; + } + const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const std::string &custom_preset) const { - return this->supported_custom_presets_.count(custom_preset); + return vector_contains(this->supported_custom_presets_, custom_preset); } - void set_supported_swing_modes(std::set modes) { this->supported_swing_modes_ = std::move(modes); } - 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); } + void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } + void set_supported_swing_modes(std::initializer_list modes) { + this->supported_swing_modes_ = ClimateSwingModeMask(modes); + } + void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.add(mode); } + bool supports_swing_mode(ClimateSwingMode swing_mode) const { + return this->supported_swing_modes_.contains(swing_mode); + } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } - const std::set &get_supported_swing_modes() const { return this->supported_swing_modes_; } + const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } float get_visual_min_temperature() const { return this->visual_min_temperature_; } void set_visual_min_temperature(float visual_min_temperature) { @@ -179,42 +216,25 @@ class ClimateTraits { void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } 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 &get_supported_modes_for_api_() const { return this->supported_modes_; } - const std::set &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; } - const std::set &get_supported_custom_fan_modes_for_api_() const { - return this->supported_custom_fan_modes_; - } - const std::set &get_supported_presets_for_api_() const { return this->supported_presets_; } - const std::set &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; } - const std::set &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; } -#endif - void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { - this->supported_modes_.insert(mode); + this->supported_modes_.add(mode); } else { - this->supported_modes_.erase(mode); + this->supported_modes_.remove(mode); } } void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) { if (supported) { - this->supported_fan_modes_.insert(mode); + this->supported_fan_modes_.add(mode); } else { - this->supported_fan_modes_.erase(mode); + this->supported_fan_modes_.remove(mode); } } void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) { if (supported) { - this->supported_swing_modes_.insert(mode); + this->supported_swing_modes_.add(mode); } else { - this->supported_swing_modes_.erase(mode); + this->supported_swing_modes_.remove(mode); } } @@ -226,12 +246,12 @@ class ClimateTraits { float visual_min_humidity_{30}; float visual_max_humidity_{99}; - std::set supported_modes_ = {climate::CLIMATE_MODE_OFF}; - std::set supported_fan_modes_; - std::set supported_swing_modes_; - std::set supported_presets_; - std::set supported_custom_fan_modes_; - std::set supported_custom_presets_; + climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF}; + climate::ClimateFanModeMask supported_fan_modes_; + climate::ClimateSwingModeMask supported_swing_modes_; + climate::ClimatePresetMask supported_presets_; + std::vector supported_custom_fan_modes_; + std::vector supported_custom_presets_; }; } // namespace climate diff --git a/esphome/core/enum_bitmask.h b/esphome/core/enum_bitmask.h new file mode 100644 index 0000000000..9c208f9efb --- /dev/null +++ b/esphome/core/enum_bitmask.h @@ -0,0 +1,155 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace esphome { + +/// Generic bitmask for storing a set of enum values efficiently. +/// Replaces std::set to eliminate red-black tree overhead (~586 bytes per instantiation). +/// +/// Template parameters: +/// EnumType: The enum type to store (must be uint8_t-based) +/// MaxBits: Maximum number of bits needed (auto-selects uint8_t/uint16_t/uint32_t) +/// +/// Requirements: +/// - EnumType must be an enum with sequential values starting from 0 +/// - Specialization must provide enum_to_bit() and bit_to_enum() static methods +/// - MaxBits must be sufficient to hold all enum values +/// +/// Example usage: +/// using ClimateModeMask = EnumBitmask; +/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); +/// if (modes.contains(CLIMATE_MODE_HEAT)) { ... } +/// for (auto mode : modes) { ... } // Iterate over set bits +/// +/// Design notes: +/// - Uses compile-time type selection for optimal size (uint8_t/uint16_t/uint32_t) +/// - Iterator converts bit positions to actual enum values during traversal +/// - All operations are constexpr-compatible for compile-time initialization +/// - Drop-in replacement for std::set with simpler API +/// +template class EnumBitmask { + public: + // Automatic bitmask type selection based on MaxBits + // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t + using bitmask_t = + typename std::conditional<(MaxBits <= 8), uint8_t, + typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; + + constexpr EnumBitmask() = default; + + /// Construct from initializer list: {VALUE1, VALUE2, ...} + constexpr EnumBitmask(std::initializer_list values) { + for (auto value : values) { + this->add(value); + } + } + + /// Add a single enum value to the set + constexpr void add(EnumType value) { this->mask_ |= (static_cast(1) << enum_to_bit(value)); } + + /// Add multiple enum values from initializer list + constexpr void add(std::initializer_list values) { + for (auto value : values) { + this->add(value); + } + } + + /// Remove an enum value from the set + constexpr void remove(EnumType value) { this->mask_ &= ~(static_cast(1) << enum_to_bit(value)); } + + /// Clear all values from the set + constexpr void clear() { this->mask_ = 0; } + + /// Check if the set contains a specific enum value + constexpr bool contains(EnumType value) const { + return (this->mask_ & (static_cast(1) << enum_to_bit(value))) != 0; + } + + /// Count the number of enum values in the set + constexpr size_t size() const { + // Brian Kernighan's algorithm - efficient for sparse bitmasks + // Typical case: 2-4 modes out of 10 possible + bitmask_t n = this->mask_; + size_t count = 0; + while (n) { + n &= n - 1; // Clear the least significant set bit + count++; + } + return count; + } + + /// Check if the set is empty + constexpr bool empty() const { return this->mask_ == 0; } + + /// Iterator support for range-based for loops and API encoding + /// Iterates over set bits and converts bit positions to enum values + class Iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = EnumType; + using difference_type = std::ptrdiff_t; + using pointer = const EnumType *; + using reference = EnumType; + + constexpr Iterator(bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); } + + constexpr EnumType operator*() const { return bit_to_enum(bit_); } + + constexpr Iterator &operator++() { + ++bit_; + advance_to_next_set_bit_(); + return *this; + } + + constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; } + + constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } + + private: + constexpr void advance_to_next_set_bit_() { bit_ = find_next_set_bit(mask_, bit_); } + + bitmask_t mask_; + int bit_; + }; + + constexpr Iterator begin() const { return Iterator(mask_, 0); } + constexpr Iterator end() const { return Iterator(mask_, MaxBits); } + + /// Get the raw bitmask value for optimized operations + constexpr bitmask_t get_mask() const { return this->mask_; } + + /// Check if a specific enum value is present in a raw bitmask + /// Useful for checking intersection results without creating temporary objects + static constexpr bool mask_contains(bitmask_t mask, EnumType value) { + return (mask & (static_cast(1) << enum_to_bit(value))) != 0; + } + + /// Get the first enum value from a raw bitmask + /// Used for optimizing intersection logic (e.g., "pick first suitable mode") + static constexpr EnumType first_value_from_mask(bitmask_t mask) { return bit_to_enum(find_next_set_bit(mask, 0)); } + + /// Find the next set bit in a bitmask starting from a given position + /// Returns the bit position, or MaxBits if no more bits are set + static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) { + int bit = start_bit; + while (bit < MaxBits && !(mask & (static_cast(1) << bit))) { + ++bit; + } + return bit; + } + + protected: + // Must be provided by template specialization + // These convert between enum values and bit positions (0, 1, 2, ...) + static constexpr int enum_to_bit(EnumType value); + static constexpr EnumType bit_to_enum(int bit); + + bitmask_t mask_{0}; +}; + +} // namespace esphome From a59fdd8e04b7dd9dbf2f97d1eccb0ab15acd7889 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 16:58:15 -1000 Subject: [PATCH 15/46] wip --- esphome/components/climate/climate.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 87d03f78c5..0e49c443c6 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -385,7 +385,7 @@ void Climate::save_state_() { if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { state.uses_custom_fan_mode = true; const auto &supported = traits.get_supported_custom_fan_modes(); - // std::set has consistent order (lexicographic for strings) + // std::vector maintains insertion order size_t i = 0; for (const auto &mode : supported) { if (mode == custom_fan_mode) { @@ -402,7 +402,7 @@ void Climate::save_state_() { if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { state.uses_custom_preset = true; const auto &supported = traits.get_supported_custom_presets(); - // std::set has consistent order (lexicographic for strings) + // std::vector maintains insertion order size_t i = 0; for (const auto &preset : supported) { if (preset == custom_preset) { @@ -553,7 +553,7 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->fan_mode = this->fan_mode; } if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) { - // std::set has consistent order (lexicographic for strings) + // std::vector maintains insertion order const auto &modes = traits.get_supported_custom_fan_modes(); if (custom_fan_mode < modes.size()) { size_t i = 0; @@ -570,7 +570,7 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->preset = this->preset; } if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) { - // std::set has consistent order (lexicographic for strings) + // std::vector maintains insertion order const auto &presets = traits.get_supported_custom_presets(); if (custom_preset < presets.size()) { size_t i = 0; From dfa51a5137d2463c7f1dfd022b6951032e1b126f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 17:16:04 -1000 Subject: [PATCH 16/46] merge --- esphome/components/api/api.proto | 12 ++-- esphome/components/api/api_pb2.h | 12 ++-- esphome/components/bedjet/bedjet_const.h | 3 +- .../bedjet/climate/bedjet_climate.h | 2 +- .../components/climate/climate_mode_bitmask.h | 63 +++++++++++++------ esphome/components/climate/climate_traits.h | 6 ++ esphome/components/climate_ir/climate_ir.h | 19 +++--- esphome/components/haier/haier_base.cpp | 18 +++++- esphome/components/haier/haier_base.h | 11 ++-- esphome/components/haier/hon_climate.cpp | 6 +- esphome/components/heatpumpir/heatpumpir.h | 11 ++-- esphome/components/midea/air_conditioner.h | 34 +++++++--- esphome/components/toshiba/toshiba.h | 6 +- .../components/tuya/climate/tuya_climate.cpp | 14 ++--- esphome/core/enum_bitmask.h | 2 +- 15 files changed, 137 insertions(+), 82 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d202486cfa..fae0f2e75a 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -989,7 +989,7 @@ message ListEntitiesClimateResponse { bool supports_current_temperature = 5; // Deprecated: use feature_flags bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags - repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set"]; + repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"]; float visual_min_temperature = 8; float visual_max_temperature = 9; float visual_target_temperature_step = 10; @@ -998,11 +998,11 @@ message ListEntitiesClimateResponse { // Deprecated in API version 1.5 bool legacy_supports_away = 11 [deprecated=true]; bool supports_action = 12; // Deprecated: use feature_flags - repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set"]; - repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set"]; - repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; - repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set"]; - repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; + repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; + repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; + repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"]; + repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; + repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"]; bool disabled_by_default = 18; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 20; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ed49498176..3e9a10c1f7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; - const std::set *supported_modes{}; + const climate::ClimateModeMask *supported_modes{}; float visual_min_temperature{0.0f}; float visual_max_temperature{0.0f}; float visual_target_temperature_step{0.0f}; bool supports_action{false}; - const std::set *supported_fan_modes{}; - const std::set *supported_swing_modes{}; - const std::set *supported_custom_fan_modes{}; - const std::set *supported_presets{}; - const std::set *supported_custom_presets{}; + const climate::ClimateFanModeMask *supported_fan_modes{}; + const climate::ClimateSwingModeMask *supported_swing_modes{}; + const std::vector *supported_custom_fan_modes{}; + const climate::ClimatePresetMask *supported_presets{}; + const std::vector *supported_custom_presets{}; float visual_current_temperature_step{0.0f}; bool supports_current_humidity{false}; bool supports_target_humidity{false}; diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index 7cac1b61ff..0693be1092 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -99,9 +99,8 @@ enum BedjetCommand : uint8_t { static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; -static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; +static constexpr 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::set BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; } // namespace bedjet } // namespace esphome diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h index 963f2e585a..dbbb73aeae 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -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. - traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); + traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); traits.set_supported_presets({ // If we support NONE, then have to decide what happens if the user switches to it (turn off?) // climate::CLIMATE_PRESET_NONE, diff --git a/esphome/components/climate/climate_mode_bitmask.h b/esphome/components/climate/climate_mode_bitmask.h index 236d153659..4166829c3f 100644 --- a/esphome/components/climate/climate_mode_bitmask.h +++ b/esphome/components/climate/climate_mode_bitmask.h @@ -9,12 +9,17 @@ namespace climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead -using ClimateModeMask = EnumBitmask; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) -using ClimateFanModeMask = - EnumBitmask; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) -using ClimateSwingModeMask = EnumBitmask; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) -using ClimatePresetMask = - EnumBitmask; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) +// Bitmask size constants - sized to fit all enum values +constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) +constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = + 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) +constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) +constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) + +using ClimateModeMask = EnumBitmask; +using ClimateFanModeMask = EnumBitmask; +using ClimateSwingModeMask = EnumBitmask; +using ClimatePresetMask = EnumBitmask; } // namespace climate } // namespace esphome @@ -25,12 +30,16 @@ using ClimatePresetMask = namespace esphome { // ClimateMode specialization (7 values: 0-6) -template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateMode mode) { +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } -template<> constexpr climate::ClimateMode EnumBitmask::bit_to_enum(int bit) { - // Compile-time lookup array mapping bit positions to enum values +template<> +inline climate::ClimateMode EnumBitmask::bit_to_enum( + int bit) { + // Lookup array mapping bit positions to enum values static constexpr climate::ClimateMode MODES[] = { climate::CLIMATE_MODE_OFF, // bit 0 climate::CLIMATE_MODE_HEAT_COOL, // bit 1 @@ -40,15 +49,20 @@ template<> constexpr climate::ClimateMode EnumBitmask:: climate::CLIMATE_MODE_DRY, // bit 5 climate::CLIMATE_MODE_AUTO, // bit 6 }; - return (bit >= 0 && bit < 7) ? MODES[bit] : climate::CLIMATE_MODE_OFF; + static constexpr int MODE_COUNT = 7; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; } // ClimateFanMode specialization (10 values: 0-9) -template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateFanMode mode) { +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateFanMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } -template<> constexpr climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { +template<> +inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { static constexpr climate::ClimateFanMode MODES[] = { climate::CLIMATE_FAN_ON, // bit 0 climate::CLIMATE_FAN_OFF, // bit 1 @@ -61,30 +75,40 @@ template<> constexpr climate::ClimateFanMode EnumBitmask= 0 && bit < 10) ? MODES[bit] : climate::CLIMATE_FAN_ON; + static constexpr int MODE_COUNT = 10; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; } // ClimateSwingMode specialization (4 values: 0-3) -template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateSwingMode mode) { +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateSwingMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } -template<> constexpr climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { +template<> +inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { static constexpr climate::ClimateSwingMode MODES[] = { climate::CLIMATE_SWING_OFF, // bit 0 climate::CLIMATE_SWING_BOTH, // bit 1 climate::CLIMATE_SWING_VERTICAL, // bit 2 climate::CLIMATE_SWING_HORIZONTAL, // bit 3 }; - return (bit >= 0 && bit < 4) ? MODES[bit] : climate::CLIMATE_SWING_OFF; + static constexpr int MODE_COUNT = 4; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; } // ClimatePreset specialization (8 values: 0-7) -template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimatePreset preset) { +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimatePreset preset) { return static_cast(preset); // Direct mapping: enum value = bit position } -template<> constexpr climate::ClimatePreset EnumBitmask::bit_to_enum(int bit) { +template<> +inline climate::ClimatePreset EnumBitmask::bit_to_enum( + int bit) { static constexpr climate::ClimatePreset PRESETS[] = { climate::CLIMATE_PRESET_NONE, // bit 0 climate::CLIMATE_PRESET_HOME, // bit 1 @@ -95,7 +119,8 @@ template<> constexpr climate::ClimatePreset EnumBitmask= 0 && bit < 8) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; + static constexpr int PRESET_COUNT = 8; + return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; } } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 45287689c9..238c527981 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -150,6 +150,9 @@ class ClimateTraits { void set_supported_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } + template void set_supported_custom_fan_modes(const char *const (&modes)[N]) { + this->supported_custom_fan_modes_.assign(modes, modes + N); + } const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); @@ -171,6 +174,9 @@ class ClimateTraits { void set_supported_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } + template void set_supported_custom_presets(const char *const (&presets)[N]) { + this->supported_custom_presets_.assign(presets, presets + N); + } const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const std::string &custom_preset) const { return vector_contains(this->supported_custom_presets_, custom_preset); diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index ea0656121f..92eb4a550e 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -3,6 +3,7 @@ #include #include "esphome/components/climate/climate.h" +#include "esphome/components/climate/climate_mode_bitmask.h" #include "esphome/components/remote_base/remote_base.h" #include "esphome/components/remote_transmitter/remote_transmitter.h" #include "esphome/components/sensor/sensor.h" @@ -24,16 +25,18 @@ class ClimateIR : public Component, public remote_base::RemoteTransmittable { public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, - bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, - std::set swing_modes = {}, std::set presets = {}) { + bool supports_dry = false, bool supports_fan_only = false, + climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(), + climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(), + climate::ClimatePresetMask presets = climate::ClimatePresetMask()) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; this->supports_dry_ = supports_dry; this->supports_fan_only_ = supports_fan_only; - this->fan_modes_ = std::move(fan_modes); - this->swing_modes_ = std::move(swing_modes); - this->presets_ = std::move(presets); + this->fan_modes_ = fan_modes; + this->swing_modes_ = swing_modes; + this->presets_ = presets; } void setup() override; @@ -60,9 +63,9 @@ class ClimateIR : public Component, bool supports_heat_{true}; bool supports_dry_{false}; bool supports_fan_only_{false}; - std::set fan_modes_ = {}; - std::set swing_modes_ = {}; - std::set presets_ = {}; + climate::ClimateFanModeMask fan_modes_{}; + climate::ClimateSwingModeMask swing_modes_{}; + climate::ClimatePresetMask presets_{}; sensor::Sensor *sensor_{nullptr}; }; diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 5709b8e9b5..1fc971a04e 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -171,26 +171,38 @@ void HaierClimateBase::toggle_power() { PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional()}); } -void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { +void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) { this->traits_.set_supported_swing_modes(modes); if (!modes.empty()) this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); } +void HaierClimateBase::set_supported_swing_modes(std::initializer_list modes) { + this->set_supported_swing_modes(climate::ClimateSwingModeMask(modes)); +} + void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } -void HaierClimateBase::set_supported_modes(const std::set &modes) { +void HaierClimateBase::set_supported_modes(climate::ClimateModeMask 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_HEAT_COOL); // Always available } -void HaierClimateBase::set_supported_presets(const std::set &presets) { +void HaierClimateBase::set_supported_modes(std::initializer_list modes) { + this->set_supported_modes(climate::ClimateModeMask(modes)); +} + +void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) { this->traits_.set_supported_presets(presets); if (!presets.empty()) this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); } +void HaierClimateBase::set_supported_presets(std::initializer_list presets) { + this->set_supported_presets(climate::ClimatePresetMask(presets)); +} + void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) { diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index f0597c49ff..630a5f20e9 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -1,8 +1,8 @@ #pragma once #include -#include #include "esphome/components/climate/climate.h" +#include "esphome/components/climate/climate_mode_bitmask.h" #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" // HaierProtocol @@ -60,9 +60,12 @@ class HaierClimateBase : public esphome::Component, void send_power_off_command(); void toggle_power(); void reset_protocol() { this->reset_protocol_request_ = true; }; - void set_supported_modes(const std::set &modes); - void set_supported_swing_modes(const std::set &modes); - void set_supported_presets(const std::set &presets); + void set_supported_modes(esphome::climate::ClimateModeMask modes); + void set_supported_modes(std::initializer_list modes); + void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes); + void set_supported_swing_modes(std::initializer_list modes); + void set_supported_presets(esphome::climate::ClimatePresetMask presets); + void set_supported_presets(std::initializer_list presets); bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t read_array(uint8_t *data, size_t len) noexcept override { diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 76558f2ebb..3ab1dab29f 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * { // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; - const std::set &swing_modes = traits_.get_supported_swing_modes(); - bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end(); - bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end(); + const auto &swing_modes = traits_.get_supported_swing_modes(); + bool vertical_swing_supported = swing_modes.contains(CLIMATE_SWING_VERTICAL); + bool horizontal_swing_supported = swing_modes.contains(CLIMATE_SWING_HORIZONTAL); if (horizontal_swing_supported && (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { if (vertical_swing_supported && diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index 3e14c11861..ed43ffdc83 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -97,12 +97,11 @@ const float TEMP_MAX = 100; // Celsius class HeatpumpIRClimate : public climate_ir::ClimateIR { public: HeatpumpIRClimate() - : climate_ir::ClimateIR( - TEMP_MIN, TEMP_MAX, 1.0f, true, true, - std::set{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO}, - std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, - climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} + : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_AUTO}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} void setup() override; void set_protocol(Protocol protocol) { this->protocol_ = protocol; } void set_horizontal_default(HorizontalDirection horizontal_direction) { diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index e70bd34e71..60ee096ec6 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -19,6 +19,9 @@ using climate::ClimateTraits; using climate::ClimateMode; using climate::ClimateSwingMode; using climate::ClimateFanMode; +using climate::ClimateModeMask; +using climate::ClimateSwingModeMask; +using climate::ClimatePresetMask; class AirConditioner : public ApplianceBase, public climate::Climate { public: @@ -40,20 +43,31 @@ class AirConditioner : public ApplianceBase, void do_power_on() { this->base_.setPowerState(true); } void do_power_off() { this->base_.setPowerState(false); } void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } - void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } - void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } - void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } - void set_custom_presets(const std::set &presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(const std::set &modes) { this->supported_custom_fan_modes_ = modes; } + void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } + void set_supported_modes(std::initializer_list modes) { + this->supported_modes_ = ClimateModeMask(modes); + } + void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } + void set_supported_swing_modes(std::initializer_list modes) { + this->supported_swing_modes_ = ClimateSwingModeMask(modes); + } + void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } + void set_supported_presets(std::initializer_list presets) { + this->supported_presets_ = ClimatePresetMask(presets); + } + void set_custom_presets(const std::vector &presets) { this->supported_custom_presets_ = presets; } + void set_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } + void set_custom_fan_modes(const std::vector &modes) { this->supported_custom_fan_modes_ = modes; } + void set_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } protected: void control(const ClimateCall &call) override; ClimateTraits traits() override; - std::set supported_modes_{}; - std::set supported_swing_modes_{}; - std::set supported_presets_{}; - std::set supported_custom_presets_{}; - std::set supported_custom_fan_modes_{}; + ClimateModeMask supported_modes_{}; + ClimateSwingModeMask supported_swing_modes_{}; + ClimatePresetMask supported_presets_{}; + std::vector supported_custom_presets_{}; + std::vector supported_custom_fan_modes_{}; Sensor *outdoor_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr}; Sensor *power_sensor_{nullptr}; diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h index d76833f406..ee1dec5cc9 100644 --- a/esphome/components/toshiba/toshiba.h +++ b/esphome/components/toshiba/toshiba.h @@ -71,10 +71,10 @@ class ToshibaClimate : public climate_ir::ClimateIR { return TOSHIBA_RAS_2819T_TEMP_C_MAX; return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models } - std::set toshiba_swing_modes_() { + climate::ClimateSwingModeMask toshiba_swing_modes_() { return (this->model_ == MODEL_GENERIC) - ? std::set{} - : std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; + ? climate::ClimateSwingModeMask() + : climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; } 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); diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 04fb14acff..97de3da353 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -306,18 +306,12 @@ climate::ClimateTraits TuyaClimate::traits() { traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); } if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) { - std::set supported_swing_modes = { - 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)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); } else if (this->swing_vertical_id_.has_value()) { - std::set supported_swing_modes = {climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_VERTICAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}); } else if (this->swing_horizontal_id_.has_value()) { - std::set supported_swing_modes = {climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_HORIZONTAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}); } if (fan_speed_id_) { diff --git a/esphome/core/enum_bitmask.h b/esphome/core/enum_bitmask.h index 9c208f9efb..4c29c7047e 100644 --- a/esphome/core/enum_bitmask.h +++ b/esphome/core/enum_bitmask.h @@ -147,7 +147,7 @@ template class EnumBitmask { // Must be provided by template specialization // These convert between enum values and bit positions (0, 1, 2, ...) static constexpr int enum_to_bit(EnumType value); - static constexpr EnumType bit_to_enum(int bit); + static EnumType bit_to_enum(int bit); // Not constexpr due to static array limitation in C++20 bitmask_t mask_{0}; }; From bbce28c18da3ea67d021c46ad4c644bf3dd9b8f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 17:21:59 -1000 Subject: [PATCH 17/46] fix compile --- esphome/components/thermostat/thermostat_climate.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 363d2b09fc..42adab7751 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -40,6 +40,10 @@ enum OnBootRestoreFrom : uint8_t { }; struct ThermostatClimateTimer { + ThermostatClimateTimer() = default; + ThermostatClimateTimer(bool active, uint32_t time, uint32_t started, std::function func) + : active(active), time(time), started(started), func(std::move(func)) {} + bool active; uint32_t time; uint32_t started; From f3bf25d203b1af8b2da41d48ec5175b6132517ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 17:25:20 -1000 Subject: [PATCH 18/46] fix compile --- esphome/components/haier/hon_climate.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 3ab1dab29f..9607343be0 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::QUIET_MODE, quiet_mode_buf, 2); } - if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) { + if ((fast_mode_buf[1] != 0xFF) && presets.contains(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::FAST_MODE, fast_mode_buf, 2); } - if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) { + if ((away_mode_buf[1] != 0xFF) && presets.contains(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, From f7a45783906ea302d6da5cddb7f3ce758360e25f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 17:27:01 -1000 Subject: [PATCH 19/46] fix compile --- esphome/components/climate/climate_mode_bitmask.h | 6 ++---- esphome/components/climate/climate_traits.h | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate_mode_bitmask.h b/esphome/components/climate/climate_mode_bitmask.h index 4166829c3f..d04e7b6ec7 100644 --- a/esphome/components/climate/climate_mode_bitmask.h +++ b/esphome/components/climate/climate_mode_bitmask.h @@ -3,8 +3,7 @@ #include "esphome/core/enum_bitmask.h" #include "climate_mode.h" -namespace esphome { -namespace climate { +namespace esphome::climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead @@ -21,8 +20,7 @@ using ClimateFanModeMask = EnumBitmask; using ClimatePresetMask = EnumBitmask; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate // Template specializations for enum-to-bit conversions // All climate enums are sequential starting from 0, so conversions are trivial diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 238c527981..1aef00956b 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -5,8 +5,7 @@ #include "climate_mode_bitmask.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace climate { +namespace esphome::climate { // Lightweight linear search for small vectors (1-20 items) // Avoids std::find template overhead @@ -18,8 +17,7 @@ template inline bool vector_contains(const std::vector &vec, cons return false; } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate namespace esphome { From d3927fe33f7c848b64258a85b3cb33f4653ceb08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 17:35:24 -1000 Subject: [PATCH 20/46] fix compile --- esphome/components/toshiba/toshiba.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 36e5a21ffa..5d824b4be8 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -405,7 +405,7 @@ void ToshibaClimate::setup() { this->swing_modes_ = this->toshiba_swing_modes_(); // Ensure swing mode is always initialized to a valid value - if (this->swing_modes_.empty() || this->swing_modes_.find(this->swing_mode) == this->swing_modes_.end()) { + if (this->swing_modes_.empty() || !this->swing_modes_.contains(this->swing_mode)) { // No swing support for this model or current swing mode not supported, reset to OFF this->swing_mode = climate::CLIMATE_SWING_OFF; } From 4dba68589819d870a98909b803c64ddb566d4d49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:01:39 -1000 Subject: [PATCH 21/46] merge --- esphome/components/light/color_mode.h | 214 +++++++++++++++++------- esphome/components/light/light_call.cpp | 2 +- esphome/components/light/light_traits.h | 2 +- 3 files changed, 156 insertions(+), 62 deletions(-) diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index 9c6a4d147b..a26f917167 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -1,7 +1,6 @@ #pragma once #include -#include "esphome/core/enum_bitmask.h" namespace esphome { namespace light { @@ -105,16 +104,16 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } -// Type alias for raw color mode bitmask values (retained for compatibility) +// Type alias for raw color mode bitmask values using color_mode_bitmask_t = uint16_t; -// Number of ColorMode enum values -constexpr int COLOR_MODE_BITMASK_SIZE = 10; +// Constants for ColorMode count and bit range +static constexpr int COLOR_MODE_COUNT = 10; // UNKNOWN through RGB_COLD_WARM_WHITE +static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8; // Number of bits in bitmask type -// Shared lookup table for ColorMode bit mapping -// This array defines the canonical order of color modes (bit 0-9) -// Declared early so it can be used by constexpr functions -constexpr ColorMode COLOR_MODE_LOOKUP[COLOR_MODE_BITMASK_SIZE] = { +// Compile-time array of all ColorMode values in declaration order +// Bit positions (0-9) map directly to enum declaration order +static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = { ColorMode::UNKNOWN, // bit 0 ColorMode::ON_OFF, // bit 1 ColorMode::BRIGHTNESS, // bit 2 @@ -127,20 +126,33 @@ constexpr ColorMode COLOR_MODE_LOOKUP[COLOR_MODE_BITMASK_SIZE] = { ColorMode::RGB_COLD_WARM_WHITE, // bit 9 }; -// Type alias for ColorMode bitmask using generic EnumBitmask template -using ColorModeMask = EnumBitmask; +/// Map ColorMode enum values to bit positions (0-9) +/// Bit positions follow the enum declaration order +static constexpr int mode_to_bit(ColorMode mode) { + // Linear search through COLOR_MODES array + // Compiler optimizes this to efficient code since array is constexpr + for (int i = 0; i < COLOR_MODE_COUNT; ++i) { + if (COLOR_MODES[i] == mode) + return i; + } + return 0; +} -// Number of ColorCapability enum values -constexpr int COLOR_CAPABILITY_COUNT = 6; +/// Map bit positions (0-9) to ColorMode enum values +/// Bit positions follow the enum declaration order +static constexpr ColorMode bit_to_mode(int bit) { + // Direct lookup in COLOR_MODES array + return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN; +} /// Helper to compute capability bitmask at compile time -constexpr uint16_t compute_capability_bitmask(ColorCapability capability) { - uint16_t mask = 0; +static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) { + color_mode_bitmask_t mask = 0; uint8_t cap_bit = static_cast(capability); // Check each ColorMode to see if it has this capability - for (int bit = 0; bit < COLOR_MODE_BITMASK_SIZE; ++bit) { - uint8_t mode_val = static_cast(COLOR_MODE_LOOKUP[bit]); + for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) { + uint8_t mode_val = static_cast(bit_to_mode(bit)); if ((mode_val & cap_bit) != 0) { mask |= (1 << bit); } @@ -148,9 +160,12 @@ constexpr uint16_t compute_capability_bitmask(ColorCapability capability) { return mask; } +// Number of ColorCapability enum values +static constexpr int COLOR_CAPABILITY_COUNT = 6; + /// Compile-time lookup table mapping ColorCapability to bitmask /// This array is computed at compile time using constexpr -constexpr uint16_t CAPABILITY_BITMASKS[] = { +static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = { compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0 compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1 compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2 @@ -159,51 +174,130 @@ constexpr uint16_t CAPABILITY_BITMASKS[] = { compute_capability_bitmask(ColorCapability::RGB), // 1 << 5 }; -/// Check if any mode in the bitmask has a specific capability -/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) -inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) { - // Lookup the pre-computed bitmask for this capability and check intersection with our mask - // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5 - // We need to convert the power-of-2 value to an index - uint8_t cap_val = static_cast(capability); -#if defined(__GNUC__) || defined(__clang__) - // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) - int index = __builtin_ctz(cap_val); -#else - // Fallback for compilers without __builtin_ctz - int index = 0; - while (cap_val > 1) { - cap_val >>= 1; - ++index; +/// Bitmask for storing a set of ColorMode values efficiently. +/// Replaces std::set to eliminate red-black tree overhead (~586 bytes). +class ColorModeMask { + public: + constexpr ColorModeMask() = default; + + /// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE} + constexpr ColorModeMask(std::initializer_list modes) { + for (auto mode : modes) { + this->add(mode); + } } + + constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); } + + /// Add multiple modes at once using initializer list + constexpr void add(std::initializer_list modes) { + for (auto mode : modes) { + this->add(mode); + } + } + + constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; } + + constexpr size_t size() const { + // Count set bits using Brian Kernighan's algorithm + // More efficient for sparse bitmasks (typical case: 2-4 modes out of 10) + uint16_t n = this->mask_; + size_t count = 0; + while (n) { + n &= n - 1; // Clear the least significant set bit + count++; + } + return count; + } + + constexpr bool empty() const { return this->mask_ == 0; } + + /// Iterator support for API encoding + class Iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = ColorMode; + using difference_type = std::ptrdiff_t; + using pointer = const ColorMode *; + using reference = ColorMode; + + constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); } + + constexpr ColorMode operator*() const { return bit_to_mode(bit_); } + + constexpr Iterator &operator++() { + ++bit_; + advance_to_next_set_bit_(); + return *this; + } + + constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; } + + constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } + + private: + constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); } + + color_mode_bitmask_t mask_; + int bit_; + }; + + constexpr Iterator begin() const { return Iterator(mask_, 0); } + constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); } + + /// Get the raw bitmask value for API encoding + constexpr color_mode_bitmask_t get_mask() const { return this->mask_; } + + /// Find the next set bit in a bitmask starting from a given position + /// Returns the bit position, or MAX_BIT_INDEX if no more bits are set + static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) { + int bit = start_bit; + while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) { + ++bit; + } + return bit; + } + + /// Find the first set bit in a bitmask and return the corresponding ColorMode + /// Used for optimizing compute_color_mode_() intersection logic + static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) { + return bit_to_mode(find_next_set_bit(mask, 0)); + } + + /// Check if a ColorMode is present in a raw bitmask value + /// Useful for checking intersection results without creating a temporary ColorModeMask + static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) { + return (mask & (1 << mode_to_bit(mode))) != 0; + } + + /// Check if any mode in the bitmask has a specific capability + /// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) + bool has_capability(ColorCapability capability) const { + // Lookup the pre-computed bitmask for this capability and check intersection with our mask + // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5 + // We need to convert the power-of-2 value to an index + uint8_t cap_val = static_cast(capability); +#if defined(__GNUC__) || defined(__clang__) + // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) + int index = __builtin_ctz(cap_val); +#else + // Fallback for compilers without __builtin_ctz + int index = 0; + while (cap_val > 1) { + cap_val >>= 1; + ++index; + } #endif - return (mask.get_mask() & CAPABILITY_BITMASKS[index]) != 0; -} + return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0; + } + + private: + // Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan). + // Currently only 10 ColorMode values exist, so 16 bits is sufficient. + // Can be changed to uint32_t if more than 16 color modes are needed in the future. + // Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes). + color_mode_bitmask_t mask_{0}; +}; } // namespace light } // namespace esphome - -// Template specializations for ColorMode must be in global namespace - -/// Map ColorMode enum values to bit positions (0-9) -/// Bit positions follow the enum declaration order -template<> -constexpr int esphome::EnumBitmask::enum_to_bit( - esphome::light::ColorMode mode) { - // Linear search through COLOR_MODE_LOOKUP array - // Compiler optimizes this to efficient code since array is constexpr - for (int i = 0; i < esphome::light::COLOR_MODE_BITMASK_SIZE; ++i) { - if (esphome::light::COLOR_MODE_LOOKUP[i] == mode) - return i; - } - return 0; -} - -/// Map bit positions (0-9) to ColorMode enum values -/// Bit positions follow the enum declaration order -template<> -inline esphome::light::ColorMode esphome::EnumBitmask::bit_to_enum(int bit) { - return (bit >= 0 && bit < esphome::light::COLOR_MODE_BITMASK_SIZE) ? esphome::light::COLOR_MODE_LOOKUP[bit] - : esphome::light::ColorMode::UNKNOWN; -} diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 26d14d7bb4..af193e1f11 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -437,7 +437,7 @@ ColorMode LightCall::compute_color_mode_() { // Use the preferred suitable mode. if (intersection != 0) { - ColorMode mode = ColorModeMask::first_value_from_mask(intersection); + ColorMode mode = ColorModeMask::first_mode_from_mask(intersection); ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 9dec9fb577..4532edca83 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -28,7 +28,7 @@ class LightTraits { bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); } bool supports_color_capability(ColorCapability color_capability) const { - return has_capability(this->supported_color_modes_, color_capability); + return this->supported_color_modes_.has_capability(color_capability); } float get_min_mireds() const { return this->min_mireds_; } From 960e6da4f7eb19c655decdc49f7d47d758addb01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:02:53 -1000 Subject: [PATCH 22/46] [gree] Use EnumBitmask add() instead of insert() for climate traits --- esphome/components/gree/gree.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index e0cacb4f1e..90c5042d69 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -8,9 +8,9 @@ static const char *const TAG = "gree.climate"; void GreeClimate::set_model(Model model) { if (model == GREE_YX1FF) { - this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed - this->presets_.insert(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode - this->presets_.insert(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode + this->fan_modes_.add(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed + this->presets_.add(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode + this->presets_.add(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode } this->model_ = model; From 15d4e30df212d13b68a9c173d347481dbe8bfaf6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:04:46 -1000 Subject: [PATCH 23/46] merge --- .../components/climate/climate_mode_bitmask.h | 124 ------------------ esphome/components/climate/climate_traits.h | 118 ++++++++++++++++- esphome/components/climate_ir/climate_ir.h | 1 - esphome/components/haier/haier_base.h | 1 - 4 files changed, 117 insertions(+), 127 deletions(-) delete mode 100644 esphome/components/climate/climate_mode_bitmask.h diff --git a/esphome/components/climate/climate_mode_bitmask.h b/esphome/components/climate/climate_mode_bitmask.h deleted file mode 100644 index d04e7b6ec7..0000000000 --- a/esphome/components/climate/climate_mode_bitmask.h +++ /dev/null @@ -1,124 +0,0 @@ -#pragma once - -#include "esphome/core/enum_bitmask.h" -#include "climate_mode.h" - -namespace esphome::climate { - -// Type aliases for climate enum bitmasks -// These replace std::set to eliminate red-black tree overhead - -// Bitmask size constants - sized to fit all enum values -constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) -constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = - 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) -constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) -constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) - -using ClimateModeMask = EnumBitmask; -using ClimateFanModeMask = EnumBitmask; -using ClimateSwingModeMask = EnumBitmask; -using ClimatePresetMask = EnumBitmask; - -} // namespace esphome::climate - -// Template specializations for enum-to-bit conversions -// All climate enums are sequential starting from 0, so conversions are trivial - -namespace esphome { - -// ClimateMode specialization (7 values: 0-6) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateMode EnumBitmask::bit_to_enum( - int bit) { - // Lookup array mapping bit positions to enum values - static constexpr climate::ClimateMode MODES[] = { - climate::CLIMATE_MODE_OFF, // bit 0 - climate::CLIMATE_MODE_HEAT_COOL, // bit 1 - climate::CLIMATE_MODE_COOL, // bit 2 - climate::CLIMATE_MODE_HEAT, // bit 3 - climate::CLIMATE_MODE_FAN_ONLY, // bit 4 - climate::CLIMATE_MODE_DRY, // bit 5 - climate::CLIMATE_MODE_AUTO, // bit 6 - }; - static constexpr int MODE_COUNT = 7; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; -} - -// ClimateFanMode specialization (10 values: 0-9) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateFanMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { - static constexpr climate::ClimateFanMode MODES[] = { - climate::CLIMATE_FAN_ON, // bit 0 - climate::CLIMATE_FAN_OFF, // bit 1 - climate::CLIMATE_FAN_AUTO, // bit 2 - climate::CLIMATE_FAN_LOW, // bit 3 - climate::CLIMATE_FAN_MEDIUM, // bit 4 - climate::CLIMATE_FAN_HIGH, // bit 5 - climate::CLIMATE_FAN_MIDDLE, // bit 6 - climate::CLIMATE_FAN_FOCUS, // bit 7 - climate::CLIMATE_FAN_DIFFUSE, // bit 8 - climate::CLIMATE_FAN_QUIET, // bit 9 - }; - static constexpr int MODE_COUNT = 10; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; -} - -// ClimateSwingMode specialization (4 values: 0-3) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateSwingMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { - static constexpr climate::ClimateSwingMode MODES[] = { - climate::CLIMATE_SWING_OFF, // bit 0 - climate::CLIMATE_SWING_BOTH, // bit 1 - climate::CLIMATE_SWING_VERTICAL, // bit 2 - climate::CLIMATE_SWING_HORIZONTAL, // bit 3 - }; - static constexpr int MODE_COUNT = 4; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; -} - -// ClimatePreset specialization (8 values: 0-7) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimatePreset preset) { - return static_cast(preset); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimatePreset EnumBitmask::bit_to_enum( - int bit) { - static constexpr climate::ClimatePreset PRESETS[] = { - climate::CLIMATE_PRESET_NONE, // bit 0 - climate::CLIMATE_PRESET_HOME, // bit 1 - climate::CLIMATE_PRESET_AWAY, // bit 2 - climate::CLIMATE_PRESET_BOOST, // bit 3 - climate::CLIMATE_PRESET_COMFORT, // bit 4 - climate::CLIMATE_PRESET_ECO, // bit 5 - climate::CLIMATE_PRESET_SLEEP, // bit 6 - climate::CLIMATE_PRESET_ACTIVITY, // bit 7 - }; - static constexpr int PRESET_COUNT = 8; - return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; -} - -} // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 1aef00956b..8004ba4002 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -2,11 +2,26 @@ #include #include "climate_mode.h" -#include "climate_mode_bitmask.h" +#include "esphome/core/enum_bitmask.h" #include "esphome/core/helpers.h" namespace esphome::climate { +// Type aliases for climate enum bitmasks +// These replace std::set to eliminate red-black tree overhead + +// Bitmask size constants - sized to fit all enum values +constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) +constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = + 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) +constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) +constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) + +using ClimateModeMask = EnumBitmask; +using ClimateFanModeMask = EnumBitmask; +using ClimateSwingModeMask = EnumBitmask; +using ClimatePresetMask = EnumBitmask; + // Lightweight linear search for small vectors (1-20 items) // Avoids std::find template overhead template inline bool vector_contains(const std::vector &vec, const T &value) { @@ -260,3 +275,104 @@ class ClimateTraits { } // namespace climate } // namespace esphome + +// Template specializations for enum-to-bit conversions +// All climate enums are sequential starting from 0, so conversions are trivial + +namespace esphome { + +// ClimateMode specialization (7 values: 0-6) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateMode EnumBitmask::bit_to_enum( + int bit) { + // Lookup array mapping bit positions to enum values + static constexpr climate::ClimateMode MODES[] = { + climate::CLIMATE_MODE_OFF, // bit 0 + climate::CLIMATE_MODE_HEAT_COOL, // bit 1 + climate::CLIMATE_MODE_COOL, // bit 2 + climate::CLIMATE_MODE_HEAT, // bit 3 + climate::CLIMATE_MODE_FAN_ONLY, // bit 4 + climate::CLIMATE_MODE_DRY, // bit 5 + climate::CLIMATE_MODE_AUTO, // bit 6 + }; + static constexpr int MODE_COUNT = 7; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; +} + +// ClimateFanMode specialization (10 values: 0-9) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateFanMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateFanMode MODES[] = { + climate::CLIMATE_FAN_ON, // bit 0 + climate::CLIMATE_FAN_OFF, // bit 1 + climate::CLIMATE_FAN_AUTO, // bit 2 + climate::CLIMATE_FAN_LOW, // bit 3 + climate::CLIMATE_FAN_MEDIUM, // bit 4 + climate::CLIMATE_FAN_HIGH, // bit 5 + climate::CLIMATE_FAN_MIDDLE, // bit 6 + climate::CLIMATE_FAN_FOCUS, // bit 7 + climate::CLIMATE_FAN_DIFFUSE, // bit 8 + climate::CLIMATE_FAN_QUIET, // bit 9 + }; + static constexpr int MODE_COUNT = 10; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; +} + +// ClimateSwingMode specialization (4 values: 0-3) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateSwingMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateSwingMode MODES[] = { + climate::CLIMATE_SWING_OFF, // bit 0 + climate::CLIMATE_SWING_BOTH, // bit 1 + climate::CLIMATE_SWING_VERTICAL, // bit 2 + climate::CLIMATE_SWING_HORIZONTAL, // bit 3 + }; + static constexpr int MODE_COUNT = 4; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; +} + +// ClimatePreset specialization (8 values: 0-7) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimatePreset preset) { + return static_cast(preset); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimatePreset EnumBitmask::bit_to_enum( + int bit) { + static constexpr climate::ClimatePreset PRESETS[] = { + climate::CLIMATE_PRESET_NONE, // bit 0 + climate::CLIMATE_PRESET_HOME, // bit 1 + climate::CLIMATE_PRESET_AWAY, // bit 2 + climate::CLIMATE_PRESET_BOOST, // bit 3 + climate::CLIMATE_PRESET_COMFORT, // bit 4 + climate::CLIMATE_PRESET_ECO, // bit 5 + climate::CLIMATE_PRESET_SLEEP, // bit 6 + climate::CLIMATE_PRESET_ACTIVITY, // bit 7 + }; + static constexpr int PRESET_COUNT = 8; + return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; +} + +} // namespace esphome diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 92eb4a550e..62a43f0b2d 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -3,7 +3,6 @@ #include #include "esphome/components/climate/climate.h" -#include "esphome/components/climate/climate_mode_bitmask.h" #include "esphome/components/remote_base/remote_base.h" #include "esphome/components/remote_transmitter/remote_transmitter.h" #include "esphome/components/sensor/sensor.h" diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 630a5f20e9..5f57bf6cd0 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -2,7 +2,6 @@ #include #include "esphome/components/climate/climate.h" -#include "esphome/components/climate/climate_mode_bitmask.h" #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" // HaierProtocol From e9e6b9ddf9515a7325e9ccae21ade6d4b82fb2e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:32:36 -1000 Subject: [PATCH 24/46] minimize changes --- esphome/components/climate/climate_traits.h | 16 ++++++++-------- esphome/components/gree/gree.cpp | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 8004ba4002..21adf5b99c 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -141,17 +141,17 @@ class ClimateTraits { void set_supported_modes(std::initializer_list modes) { this->supported_modes_ = ClimateModeMask(modes); } - void add_supported_mode(ClimateMode mode) { this->supported_modes_.add(mode); } - bool supports_mode(ClimateMode mode) const { return this->supported_modes_.contains(mode); } + void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } + bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode) > 0; } const ClimateModeMask &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::initializer_list modes) { this->supported_fan_modes_ = ClimateFanModeMask(modes); } - void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.add(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); } - bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.contains(fan_mode); } + bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode) > 0; } bool get_supports_fan_modes() const { return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); } @@ -175,9 +175,9 @@ class ClimateTraits { void set_supported_presets(std::initializer_list presets) { this->supported_presets_ = ClimatePresetMask(presets); } - void add_supported_preset(ClimatePreset preset) { this->supported_presets_.add(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); } - bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.contains(preset); } + bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset) > 0; } bool get_supports_presets() const { return !this->supported_presets_.empty(); } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } @@ -199,9 +199,9 @@ class ClimateTraits { void set_supported_swing_modes(std::initializer_list modes) { this->supported_swing_modes_ = ClimateSwingModeMask(modes); } - void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.add(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_.contains(swing_mode); + return this->supported_swing_modes_.count(swing_mode) > 0; } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index 90c5042d69..e0cacb4f1e 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -8,9 +8,9 @@ static const char *const TAG = "gree.climate"; void GreeClimate::set_model(Model model) { if (model == GREE_YX1FF) { - this->fan_modes_.add(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed - this->presets_.add(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode - this->presets_.add(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode + this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed + this->presets_.insert(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode + this->presets_.insert(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode } this->model_ = model; From 2debf04a48f95e3157bd266868ec82f65a38ce04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:32:58 -1000 Subject: [PATCH 25/46] [climate] Use std::set API for EnumBitmask - Change .add() to .insert() - Change .remove() to .erase() - Change .contains() to .count() > 0 - Consistent with std::set API --- esphome/components/climate/climate_traits.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 21adf5b99c..09197a5de3 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -237,23 +237,23 @@ class ClimateTraits { protected: void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { - this->supported_modes_.add(mode); + this->supported_modes_.insert(mode); } else { - this->supported_modes_.remove(mode); + this->supported_modes_.erase(mode); } } void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) { if (supported) { - this->supported_fan_modes_.add(mode); + this->supported_fan_modes_.insert(mode); } else { - this->supported_fan_modes_.remove(mode); + this->supported_fan_modes_.erase(mode); } } void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) { if (supported) { - this->supported_swing_modes_.add(mode); + this->supported_swing_modes_.insert(mode); } else { - this->supported_swing_modes_.remove(mode); + this->supported_swing_modes_.erase(mode); } } From 55d1b823e8e1ba6623b45d7a60c48dc0dc3daa5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:34:45 -1000 Subject: [PATCH 26/46] minimize changes --- esphome/components/haier/hon_climate.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 9607343be0..b7ec065261 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1034,8 +1034,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; const auto &swing_modes = traits_.get_supported_swing_modes(); - bool vertical_swing_supported = swing_modes.contains(CLIMATE_SWING_VERTICAL); - bool horizontal_swing_supported = swing_modes.contains(CLIMATE_SWING_HORIZONTAL); + bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL) > 0; + bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL) > 0; if (horizontal_swing_supported && (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { if (vertical_swing_supported && @@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::QUIET_MODE, quiet_mode_buf, 2); } - if ((fast_mode_buf[1] != 0xFF) && presets.contains(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { + if ((fast_mode_buf[1] != 0xFF) && (presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST) > 0)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::FAST_MODE, fast_mode_buf, 2); } - if ((away_mode_buf[1] != 0xFF) && presets.contains(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { + if ((away_mode_buf[1] != 0xFF) && (presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY) > 0)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, From d8e8c2832ea7c5a1972ef6e2ed9bc35922c906ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:34:58 -1000 Subject: [PATCH 27/46] minimize changes --- esphome/components/toshiba/toshiba.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 5d824b4be8..ef96caf238 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -405,7 +405,7 @@ void ToshibaClimate::setup() { this->swing_modes_ = this->toshiba_swing_modes_(); // Ensure swing mode is always initialized to a valid value - if (this->swing_modes_.empty() || !this->swing_modes_.contains(this->swing_mode)) { + if (this->swing_modes_.empty() || (this->swing_modes_.count(this->swing_mode) == 0)) { // No swing support for this model or current swing mode not supported, reset to OFF this->swing_mode = climate::CLIMATE_SWING_OFF; } From 1eca67bb4c379cecba123ea509bcbacf3a7c3032 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:36:33 -1000 Subject: [PATCH 28/46] [climate] Remove redundant initializer_list overloads EnumBitmask already has a constructor that takes initializer_list, so the explicit overloads are unnecessary and add code duplication. --- esphome/components/climate/climate_traits.h | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 09197a5de3..5ee6b40230 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -138,17 +138,11 @@ class ClimateTraits { } void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } - void set_supported_modes(std::initializer_list modes) { - this->supported_modes_ = ClimateModeMask(modes); - } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode) > 0; } const ClimateModeMask &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::initializer_list modes) { - this->supported_fan_modes_ = ClimateFanModeMask(modes); - } 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); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode) > 0; } @@ -172,9 +166,6 @@ class ClimateTraits { } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } - void set_supported_presets(std::initializer_list presets) { - this->supported_presets_ = ClimatePresetMask(presets); - } 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); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset) > 0; } @@ -196,9 +187,6 @@ class ClimateTraits { } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } - void set_supported_swing_modes(std::initializer_list modes) { - this->supported_swing_modes_ = ClimateSwingModeMask(modes); - } 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) > 0; From 0ad42ec79bb351e34b04109b2b1e6de8d56859ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:37:19 -1000 Subject: [PATCH 29/46] minimize changes --- esphome/components/haier/haier_base.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 5f57bf6cd0..e24217bfd9 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -60,11 +60,8 @@ class HaierClimateBase : public esphome::Component, void toggle_power(); void reset_protocol() { this->reset_protocol_request_ = true; }; void set_supported_modes(esphome::climate::ClimateModeMask modes); - void set_supported_modes(std::initializer_list modes); void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes); - void set_supported_swing_modes(std::initializer_list modes); void set_supported_presets(esphome::climate::ClimatePresetMask presets); - void set_supported_presets(std::initializer_list presets); bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t read_array(uint8_t *data, size_t len) noexcept override { From 0d256e12a6ea805e3067e141e6f00bc84ae9065f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:37:48 -1000 Subject: [PATCH 30/46] [climate] Remove redundant initializer_list overloads from haier and midea EnumBitmask and std::vector already handle initializer_list via implicit conversion, so explicit overloads are unnecessary. --- esphome/components/haier/haier_base.cpp | 12 ------------ esphome/components/midea/air_conditioner.h | 11 ----------- 2 files changed, 23 deletions(-) diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 1fc971a04e..cd2673a272 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -177,10 +177,6 @@ void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask m this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); } -void HaierClimateBase::set_supported_swing_modes(std::initializer_list modes) { - this->set_supported_swing_modes(climate::ClimateSwingModeMask(modes)); -} - void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) { @@ -189,20 +185,12 @@ void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) { this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available } -void HaierClimateBase::set_supported_modes(std::initializer_list modes) { - this->set_supported_modes(climate::ClimateModeMask(modes)); -} - void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) { this->traits_.set_supported_presets(presets); if (!presets.empty()) this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); } -void HaierClimateBase::set_supported_presets(std::initializer_list presets) { - this->set_supported_presets(climate::ClimatePresetMask(presets)); -} - void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) { diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 60ee096ec6..6c2401efe7 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -44,21 +44,10 @@ class AirConditioner : public ApplianceBase, void do_power_off() { this->base_.setPowerState(false); } 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(std::initializer_list modes) { - this->supported_modes_ = ClimateModeMask(modes); - } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } - void set_supported_swing_modes(std::initializer_list modes) { - this->supported_swing_modes_ = ClimateSwingModeMask(modes); - } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } - void set_supported_presets(std::initializer_list presets) { - this->supported_presets_ = ClimatePresetMask(presets); - } void set_custom_presets(const std::vector &presets) { this->supported_custom_presets_ = presets; } - void set_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } void set_custom_fan_modes(const std::vector &modes) { this->supported_custom_fan_modes_ = modes; } - void set_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } protected: void control(const ClimateCall &call) override; From ae1af5f16e18b4e769973d1029abf8eae7970c29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:38:44 -1000 Subject: [PATCH 31/46] minimize changes --- esphome/components/climate/climate_traits.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 5ee6b40230..7cf4a307e3 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -139,13 +139,13 @@ class ClimateTraits { void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } - bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode) > 0; } + bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } 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); } - bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode) > 0; } + bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool get_supports_fan_modes() const { return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); } @@ -168,7 +168,7 @@ class ClimateTraits { void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } 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); } - bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset) > 0; } + bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool get_supports_presets() const { return !this->supported_presets_.empty(); } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } From 7310d7557985167380dbfaadc48c1aa042cf5477 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:39:11 -1000 Subject: [PATCH 32/46] minimize changes --- esphome/components/climate/climate_traits.h | 4 +--- esphome/components/haier/hon_climate.cpp | 8 ++++---- esphome/components/toshiba/toshiba.cpp | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 7cf4a307e3..f84133aa2a 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -188,9 +188,7 @@ class ClimateTraits { void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } 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) > 0; - } + 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(); } const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index b7ec065261..23d28bfd47 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1034,8 +1034,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; const auto &swing_modes = traits_.get_supported_swing_modes(); - bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL) > 0; - bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL) > 0; + bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL); + bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL); if (horizontal_swing_supported && (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { if (vertical_swing_supported && @@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::QUIET_MODE, quiet_mode_buf, 2); } - if ((fast_mode_buf[1] != 0xFF) && (presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST) > 0)) { + if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::FAST_MODE, fast_mode_buf, 2); } - if ((away_mode_buf[1] != 0xFF) && (presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY) > 0)) { + if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index ef96caf238..5efa70d6b4 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -405,7 +405,7 @@ void ToshibaClimate::setup() { this->swing_modes_ = this->toshiba_swing_modes_(); // Ensure swing mode is always initialized to a valid value - if (this->swing_modes_.empty() || (this->swing_modes_.count(this->swing_mode) == 0)) { + if (this->swing_modes_.empty() || !this->swing_modes_.count(this->swing_mode)) { // No swing support for this model or current swing mode not supported, reset to OFF this->swing_mode = climate::CLIMATE_SWING_OFF; } From 44c24100179b67b46f0d5eeff80b99d30fba052e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:48:42 -1000 Subject: [PATCH 33/46] preen --- esphome/components/climate/climate_traits.h | 213 ++++++++++---------- 1 file changed, 107 insertions(+), 106 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index f84133aa2a..bdb04a65cc 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -5,18 +5,120 @@ #include "esphome/core/enum_bitmask.h" #include "esphome/core/helpers.h" +// Forward declare climate enums and bitmask sizes namespace esphome::climate { - -// Type aliases for climate enum bitmasks -// These replace std::set to eliminate red-black tree overhead - -// Bitmask size constants - sized to fit all enum values constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) +} // namespace esphome::climate +// Template specializations for enum-to-bit conversions +// MUST be declared before any instantiation of EnumBitmask, etc. +namespace esphome { + +// ClimateMode specialization (7 values: 0-6) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateMode EnumBitmask::bit_to_enum( + int bit) { + // Lookup array mapping bit positions to enum values + static constexpr climate::ClimateMode MODES[] = { + climate::CLIMATE_MODE_OFF, // bit 0 + climate::CLIMATE_MODE_HEAT_COOL, // bit 1 + climate::CLIMATE_MODE_COOL, // bit 2 + climate::CLIMATE_MODE_HEAT, // bit 3 + climate::CLIMATE_MODE_FAN_ONLY, // bit 4 + climate::CLIMATE_MODE_DRY, // bit 5 + climate::CLIMATE_MODE_AUTO, // bit 6 + }; + static constexpr int MODE_COUNT = 7; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; +} + +// ClimateFanMode specialization (10 values: 0-9) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateFanMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateFanMode MODES[] = { + climate::CLIMATE_FAN_ON, // bit 0 + climate::CLIMATE_FAN_OFF, // bit 1 + climate::CLIMATE_FAN_AUTO, // bit 2 + climate::CLIMATE_FAN_LOW, // bit 3 + climate::CLIMATE_FAN_MEDIUM, // bit 4 + climate::CLIMATE_FAN_HIGH, // bit 5 + climate::CLIMATE_FAN_MIDDLE, // bit 6 + climate::CLIMATE_FAN_FOCUS, // bit 7 + climate::CLIMATE_FAN_DIFFUSE, // bit 8 + climate::CLIMATE_FAN_QUIET, // bit 9 + }; + static constexpr int MODE_COUNT = 10; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; +} + +// ClimateSwingMode specialization (4 values: 0-3) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateSwingMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateSwingMode MODES[] = { + climate::CLIMATE_SWING_OFF, // bit 0 + climate::CLIMATE_SWING_BOTH, // bit 1 + climate::CLIMATE_SWING_VERTICAL, // bit 2 + climate::CLIMATE_SWING_HORIZONTAL, // bit 3 + }; + static constexpr int MODE_COUNT = 4; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; +} + +// ClimatePreset specialization (8 values: 0-7) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimatePreset preset) { + return static_cast(preset); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimatePreset EnumBitmask::bit_to_enum( + int bit) { + static constexpr climate::ClimatePreset PRESETS[] = { + climate::CLIMATE_PRESET_NONE, // bit 0 + climate::CLIMATE_PRESET_HOME, // bit 1 + climate::CLIMATE_PRESET_AWAY, // bit 2 + climate::CLIMATE_PRESET_BOOST, // bit 3 + climate::CLIMATE_PRESET_COMFORT, // bit 4 + climate::CLIMATE_PRESET_ECO, // bit 5 + climate::CLIMATE_PRESET_SLEEP, // bit 6 + climate::CLIMATE_PRESET_ACTIVITY, // bit 7 + }; + static constexpr int PRESET_COUNT = 8; + return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; +} + +} // namespace esphome + +// Now we can safely create the type aliases +namespace esphome::climate { + +// Type aliases for climate enum bitmasks +// These replace std::set to eliminate red-black tree overhead using ClimateModeMask = EnumBitmask; using ClimateFanModeMask = EnumBitmask; using ClimateSwingModeMask = EnumBitmask; @@ -261,104 +363,3 @@ class ClimateTraits { } // namespace climate } // namespace esphome - -// Template specializations for enum-to-bit conversions -// All climate enums are sequential starting from 0, so conversions are trivial - -namespace esphome { - -// ClimateMode specialization (7 values: 0-6) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateMode EnumBitmask::bit_to_enum( - int bit) { - // Lookup array mapping bit positions to enum values - static constexpr climate::ClimateMode MODES[] = { - climate::CLIMATE_MODE_OFF, // bit 0 - climate::CLIMATE_MODE_HEAT_COOL, // bit 1 - climate::CLIMATE_MODE_COOL, // bit 2 - climate::CLIMATE_MODE_HEAT, // bit 3 - climate::CLIMATE_MODE_FAN_ONLY, // bit 4 - climate::CLIMATE_MODE_DRY, // bit 5 - climate::CLIMATE_MODE_AUTO, // bit 6 - }; - static constexpr int MODE_COUNT = 7; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; -} - -// ClimateFanMode specialization (10 values: 0-9) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateFanMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { - static constexpr climate::ClimateFanMode MODES[] = { - climate::CLIMATE_FAN_ON, // bit 0 - climate::CLIMATE_FAN_OFF, // bit 1 - climate::CLIMATE_FAN_AUTO, // bit 2 - climate::CLIMATE_FAN_LOW, // bit 3 - climate::CLIMATE_FAN_MEDIUM, // bit 4 - climate::CLIMATE_FAN_HIGH, // bit 5 - climate::CLIMATE_FAN_MIDDLE, // bit 6 - climate::CLIMATE_FAN_FOCUS, // bit 7 - climate::CLIMATE_FAN_DIFFUSE, // bit 8 - climate::CLIMATE_FAN_QUIET, // bit 9 - }; - static constexpr int MODE_COUNT = 10; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; -} - -// ClimateSwingMode specialization (4 values: 0-3) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateSwingMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { - static constexpr climate::ClimateSwingMode MODES[] = { - climate::CLIMATE_SWING_OFF, // bit 0 - climate::CLIMATE_SWING_BOTH, // bit 1 - climate::CLIMATE_SWING_VERTICAL, // bit 2 - climate::CLIMATE_SWING_HORIZONTAL, // bit 3 - }; - static constexpr int MODE_COUNT = 4; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; -} - -// ClimatePreset specialization (8 values: 0-7) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimatePreset preset) { - return static_cast(preset); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimatePreset EnumBitmask::bit_to_enum( - int bit) { - static constexpr climate::ClimatePreset PRESETS[] = { - climate::CLIMATE_PRESET_NONE, // bit 0 - climate::CLIMATE_PRESET_HOME, // bit 1 - climate::CLIMATE_PRESET_AWAY, // bit 2 - climate::CLIMATE_PRESET_BOOST, // bit 3 - climate::CLIMATE_PRESET_COMFORT, // bit 4 - climate::CLIMATE_PRESET_ECO, // bit 5 - climate::CLIMATE_PRESET_SLEEP, // bit 6 - climate::CLIMATE_PRESET_ACTIVITY, // bit 7 - }; - static constexpr int PRESET_COUNT = 8; - return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; -} - -} // namespace esphome From 35afa7ae059b1475ce94a106a2a68db3c1640976 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 08:52:27 -1000 Subject: [PATCH 34/46] migrate --- esphome/components/climate/climate_traits.h | 34 ++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index bdb04a65cc..9bff36f69f 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -2,7 +2,7 @@ #include #include "climate_mode.h" -#include "esphome/core/enum_bitmask.h" +#include "esphome/core/finite_set_mask.h" #include "esphome/core/helpers.h" // Forward declare climate enums and bitmask sizes @@ -14,19 +14,19 @@ constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERT constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) } // namespace esphome::climate -// Template specializations for enum-to-bit conversions -// MUST be declared before any instantiation of EnumBitmask, etc. +// Template specializations for value-to-bit conversions +// MUST be declared before any instantiation of FiniteSetMask, etc. namespace esphome { // ClimateMode specialization (7 values: 0-6) template<> -constexpr int EnumBitmask::enum_to_bit( +constexpr int FiniteSetMask::value_to_bit( climate::ClimateMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } template<> -inline climate::ClimateMode EnumBitmask::bit_to_enum( +inline climate::ClimateMode FiniteSetMask::bit_to_value( int bit) { // Lookup array mapping bit positions to enum values static constexpr climate::ClimateMode MODES[] = { @@ -44,14 +44,14 @@ inline climate::ClimateMode EnumBitmask -constexpr int EnumBitmask::enum_to_bit( +constexpr int FiniteSetMask::value_to_bit( climate::ClimateFanMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } template<> -inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { +inline climate::ClimateFanMode FiniteSetMask::bit_to_value(int bit) { static constexpr climate::ClimateFanMode MODES[] = { climate::CLIMATE_FAN_ON, // bit 0 climate::CLIMATE_FAN_OFF, // bit 1 @@ -70,14 +70,14 @@ inline climate::ClimateFanMode EnumBitmask -constexpr int EnumBitmask::enum_to_bit( +constexpr int FiniteSetMask::value_to_bit( climate::ClimateSwingMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } template<> -inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { +inline climate::ClimateSwingMode FiniteSetMask::bit_to_value(int bit) { static constexpr climate::ClimateSwingMode MODES[] = { climate::CLIMATE_SWING_OFF, // bit 0 climate::CLIMATE_SWING_BOTH, // bit 1 @@ -90,13 +90,13 @@ inline climate::ClimateSwingMode EnumBitmask -constexpr int EnumBitmask::enum_to_bit( +constexpr int FiniteSetMask::value_to_bit( climate::ClimatePreset preset) { return static_cast(preset); // Direct mapping: enum value = bit position } template<> -inline climate::ClimatePreset EnumBitmask::bit_to_enum( +inline climate::ClimatePreset FiniteSetMask::bit_to_value( int bit) { static constexpr climate::ClimatePreset PRESETS[] = { climate::CLIMATE_PRESET_NONE, // bit 0 @@ -119,10 +119,10 @@ namespace esphome::climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead -using ClimateModeMask = EnumBitmask; -using ClimateFanModeMask = EnumBitmask; -using ClimateSwingModeMask = EnumBitmask; -using ClimatePresetMask = EnumBitmask; +using ClimateModeMask = FiniteSetMask; +using ClimateFanModeMask = FiniteSetMask; +using ClimateSwingModeMask = FiniteSetMask; +using ClimatePresetMask = FiniteSetMask; // Lightweight linear search for small vectors (1-20 items) // Avoids std::find template overhead From d7f32bf27f24cd62656a75469bb0271e1a49c84b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:44:14 -1000 Subject: [PATCH 35/46] reduce --- esphome/components/climate/climate_traits.h | 36 +++++---------------- esphome/core/finite_set_mask.h | 26 +++++++++++---- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 9bff36f69f..b90ef963a7 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -18,13 +18,8 @@ constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWA // MUST be declared before any instantiation of FiniteSetMask, etc. namespace esphome { -// ClimateMode specialization (7 values: 0-6) -template<> -constexpr int FiniteSetMask::value_to_bit( - climate::ClimateMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - +// ClimateMode uses 1:1 mapping (value_to_bit is just a cast) +// Only bit_to_value needs specialization template<> inline climate::ClimateMode FiniteSetMask::bit_to_value( int bit) { @@ -42,13 +37,8 @@ inline climate::ClimateMode FiniteSetMask= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; } -// ClimateFanMode specialization (10 values: 0-9) -template<> -constexpr int FiniteSetMask::value_to_bit( - climate::ClimateFanMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - +// ClimateFanMode uses 1:1 mapping (value_to_bit is just a cast) +// Only bit_to_value needs specialization template<> inline climate::ClimateFanMode FiniteSetMask::bit_to_value(int bit) { @@ -68,13 +58,8 @@ inline climate::ClimateFanMode FiniteSetMask= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; } -// ClimateSwingMode specialization (4 values: 0-3) -template<> -constexpr int FiniteSetMask::value_to_bit( - climate::ClimateSwingMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - +// ClimateSwingMode uses 1:1 mapping (value_to_bit is just a cast) +// Only bit_to_value needs specialization template<> inline climate::ClimateSwingMode FiniteSetMask::bit_to_value(int bit) { @@ -88,13 +73,8 @@ inline climate::ClimateSwingMode FiniteSetMask= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; } -// ClimatePreset specialization (8 values: 0-7) -template<> -constexpr int FiniteSetMask::value_to_bit( - climate::ClimatePreset preset) { - return static_cast(preset); // Direct mapping: enum value = bit position -} - +// ClimatePreset uses 1:1 mapping (value_to_bit is just a cast) +// Only bit_to_value needs specialization template<> inline climate::ClimatePreset FiniteSetMask::bit_to_value( int bit) { diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index e6e7564d4b..ebf134960b 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -17,17 +17,27 @@ namespace esphome { /// /// Requirements: /// - ValueType must have a bounded discrete range that maps to bit positions -/// - Specialization must provide value_to_bit() and bit_to_value() static methods +/// - Specialization must provide bit_to_value() static method +/// - For 1:1 mappings (enum value = bit position), default value_to_bit() is used +/// - For custom mappings (like ColorMode), specialize value_to_bit() as well /// - MaxBits must be sufficient to hold all possible values /// -/// Example usage: +/// Example usage (1:1 mapping - climate enums): +/// // For enums with contiguous values starting at 0, only bit_to_value() needs specialization +/// template<> +/// inline ClimateMode FiniteSetMask::bit_to_value(int bit) { +/// static constexpr ClimateMode MODES[] = {CLIMATE_MODE_OFF, CLIMATE_MODE_HEAT, ...}; +/// return (bit >= 0 && bit < 7) ? MODES[bit] : CLIMATE_MODE_OFF; +/// } +/// /// using ClimateModeMask = FiniteSetMask; /// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); /// if (modes.count(CLIMATE_MODE_HEAT)) { ... } /// for (auto mode : modes) { ... } // Iterate over set bits /// -/// For complete usage examples with template specializations, see: -/// - esphome/components/light/color_mode.h (ColorMode enum example) +/// Example usage (custom mapping - ColorMode): +/// // For custom mappings, specialize both value_to_bit() and bit_to_value() +/// // See esphome/components/light/color_mode.h for complete example /// /// Design notes: /// - Uses compile-time type selection for optimal size (uint8_t/uint16_t/uint32_t) @@ -150,9 +160,13 @@ template class FiniteSetMask { } protected: + // Default implementation for 1:1 mapping (enum value = bit position) + // For enums with contiguous values starting at 0, this is all you need. + // If you need custom mapping (like ColorMode), provide a specialization. + static constexpr int value_to_bit(ValueType value) { return static_cast(value); } + // Must be provided by template specialization - // These convert between values and bit positions (0, 1, 2, ...) - static constexpr int value_to_bit(ValueType value); + // Converts bit positions (0, 1, 2, ...) to actual values static ValueType bit_to_value(int bit); // Not constexpr: array indexing with runtime bounds checking bitmask_t mask_{0}; From ce80baa3c92a7e329332a45f1d5b073b3599b749 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:46:13 -1000 Subject: [PATCH 36/46] reduce --- esphome/components/climate/climate_traits.h | 83 +-------------------- esphome/core/finite_set_mask.h | 26 ++----- 2 files changed, 10 insertions(+), 99 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index b90ef963a7..4def5044ca 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -14,87 +14,8 @@ constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERT constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) } // namespace esphome::climate -// Template specializations for value-to-bit conversions -// MUST be declared before any instantiation of FiniteSetMask, etc. -namespace esphome { - -// ClimateMode uses 1:1 mapping (value_to_bit is just a cast) -// Only bit_to_value needs specialization -template<> -inline climate::ClimateMode FiniteSetMask::bit_to_value( - int bit) { - // Lookup array mapping bit positions to enum values - static constexpr climate::ClimateMode MODES[] = { - climate::CLIMATE_MODE_OFF, // bit 0 - climate::CLIMATE_MODE_HEAT_COOL, // bit 1 - climate::CLIMATE_MODE_COOL, // bit 2 - climate::CLIMATE_MODE_HEAT, // bit 3 - climate::CLIMATE_MODE_FAN_ONLY, // bit 4 - climate::CLIMATE_MODE_DRY, // bit 5 - climate::CLIMATE_MODE_AUTO, // bit 6 - }; - static constexpr int MODE_COUNT = 7; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; -} - -// ClimateFanMode uses 1:1 mapping (value_to_bit is just a cast) -// Only bit_to_value needs specialization -template<> -inline climate::ClimateFanMode FiniteSetMask::bit_to_value(int bit) { - static constexpr climate::ClimateFanMode MODES[] = { - climate::CLIMATE_FAN_ON, // bit 0 - climate::CLIMATE_FAN_OFF, // bit 1 - climate::CLIMATE_FAN_AUTO, // bit 2 - climate::CLIMATE_FAN_LOW, // bit 3 - climate::CLIMATE_FAN_MEDIUM, // bit 4 - climate::CLIMATE_FAN_HIGH, // bit 5 - climate::CLIMATE_FAN_MIDDLE, // bit 6 - climate::CLIMATE_FAN_FOCUS, // bit 7 - climate::CLIMATE_FAN_DIFFUSE, // bit 8 - climate::CLIMATE_FAN_QUIET, // bit 9 - }; - static constexpr int MODE_COUNT = 10; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; -} - -// ClimateSwingMode uses 1:1 mapping (value_to_bit is just a cast) -// Only bit_to_value needs specialization -template<> -inline climate::ClimateSwingMode FiniteSetMask::bit_to_value(int bit) { - static constexpr climate::ClimateSwingMode MODES[] = { - climate::CLIMATE_SWING_OFF, // bit 0 - climate::CLIMATE_SWING_BOTH, // bit 1 - climate::CLIMATE_SWING_VERTICAL, // bit 2 - climate::CLIMATE_SWING_HORIZONTAL, // bit 3 - }; - static constexpr int MODE_COUNT = 4; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; -} - -// ClimatePreset uses 1:1 mapping (value_to_bit is just a cast) -// Only bit_to_value needs specialization -template<> -inline climate::ClimatePreset FiniteSetMask::bit_to_value( - int bit) { - static constexpr climate::ClimatePreset PRESETS[] = { - climate::CLIMATE_PRESET_NONE, // bit 0 - climate::CLIMATE_PRESET_HOME, // bit 1 - climate::CLIMATE_PRESET_AWAY, // bit 2 - climate::CLIMATE_PRESET_BOOST, // bit 3 - climate::CLIMATE_PRESET_COMFORT, // bit 4 - climate::CLIMATE_PRESET_ECO, // bit 5 - climate::CLIMATE_PRESET_SLEEP, // bit 6 - climate::CLIMATE_PRESET_ACTIVITY, // bit 7 - }; - static constexpr int PRESET_COUNT = 8; - return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; -} - -} // namespace esphome - -// Now we can safely create the type aliases +// No template specializations needed - all climate enums use 1:1 mapping (enum value = bit position) +// FiniteSetMask's default implementations handle this automatically. namespace esphome::climate { // Type aliases for climate enum bitmasks diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index ebf134960b..fdb9bcbc08 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -17,26 +17,19 @@ namespace esphome { /// /// Requirements: /// - ValueType must have a bounded discrete range that maps to bit positions -/// - Specialization must provide bit_to_value() static method -/// - For 1:1 mappings (enum value = bit position), default value_to_bit() is used -/// - For custom mappings (like ColorMode), specialize value_to_bit() as well +/// - For 1:1 mappings (contiguous enums starting at 0), no specialization needed +/// - For custom mappings (like ColorMode), specialize value_to_bit() and/or bit_to_value() /// - MaxBits must be sufficient to hold all possible values /// /// Example usage (1:1 mapping - climate enums): -/// // For enums with contiguous values starting at 0, only bit_to_value() needs specialization -/// template<> -/// inline ClimateMode FiniteSetMask::bit_to_value(int bit) { -/// static constexpr ClimateMode MODES[] = {CLIMATE_MODE_OFF, CLIMATE_MODE_HEAT, ...}; -/// return (bit >= 0 && bit < 7) ? MODES[bit] : CLIMATE_MODE_OFF; -/// } -/// +/// // For enums with contiguous values starting at 0, no specialization needed! /// using ClimateModeMask = FiniteSetMask; /// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); /// if (modes.count(CLIMATE_MODE_HEAT)) { ... } /// for (auto mode : modes) { ... } // Iterate over set bits /// /// Example usage (custom mapping - ColorMode): -/// // For custom mappings, specialize both value_to_bit() and bit_to_value() +/// // For non-contiguous enums or custom mappings, specialize value_to_bit() and/or bit_to_value() /// // See esphome/components/light/color_mode.h for complete example /// /// Design notes: @@ -160,14 +153,11 @@ template class FiniteSetMask { } protected: - // Default implementation for 1:1 mapping (enum value = bit position) - // For enums with contiguous values starting at 0, this is all you need. - // If you need custom mapping (like ColorMode), provide a specialization. + // Default implementations for 1:1 mapping (enum value = bit position) + // For enums with contiguous values starting at 0, these defaults work as-is. + // If you need custom mapping (like ColorMode), provide specializations. static constexpr int value_to_bit(ValueType value) { return static_cast(value); } - - // Must be provided by template specialization - // Converts bit positions (0, 1, 2, ...) to actual values - static ValueType bit_to_value(int bit); // Not constexpr: array indexing with runtime bounds checking + static constexpr ValueType bit_to_value(int bit) { return static_cast(bit); } bitmask_t mask_{0}; }; From 56d084bcffcaad385d6e99fde2eeb739ffaed039 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:47:31 -1000 Subject: [PATCH 37/46] reduce --- esphome/components/climate/climate_traits.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 4def5044ca..d0855d58b1 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -5,18 +5,17 @@ #include "esphome/core/finite_set_mask.h" #include "esphome/core/helpers.h" -// Forward declare climate enums and bitmask sizes namespace esphome::climate { + +// Bitmask sizes for climate enums constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) -} // namespace esphome::climate // No template specializations needed - all climate enums use 1:1 mapping (enum value = bit position) // FiniteSetMask's default implementations handle this automatically. -namespace esphome::climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead From 73944d4077886bde010641665edd33599d0f6aaa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:48:39 -1000 Subject: [PATCH 38/46] reduce --- esphome/components/climate/climate_traits.h | 22 +++++++++------------ 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index d0855d58b1..42affba3e9 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -5,7 +5,15 @@ #include "esphome/core/finite_set_mask.h" #include "esphome/core/helpers.h" -namespace esphome::climate { +namespace esphome { + +#ifdef USE_API +namespace api { +class APIConnection; +} // namespace api +#endif + +namespace climate { // Bitmask sizes for climate enums constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) @@ -34,18 +42,6 @@ template inline bool vector_contains(const std::vector &vec, cons return false; } -} // namespace esphome::climate - -namespace esphome { - -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif - -namespace climate { - /** This class contains all static data for climate devices. * * All climate devices must support these features: From 8e9a438c4679decfe48da861df176cd4f6fbe375 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:51:15 -1000 Subject: [PATCH 39/46] reduce --- esphome/components/climate/climate_mode.h | 12 ++++++++---- esphome/components/climate/climate_traits.h | 19 +++++-------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index faec5d2537..44423d2f22 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -7,6 +7,7 @@ namespace esphome { namespace climate { /// 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 { /// The climate device is off CLIMATE_MODE_OFF = 0, @@ -24,7 +25,7 @@ enum ClimateMode : uint8_t { * 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. */ - CLIMATE_MODE_AUTO = 6 + CLIMATE_MODE_AUTO = 6 // Update ClimateModeMask in climate_traits.h if adding values after this }; /// Enum for the current action of the climate device. Values match those of ClimateMode. @@ -43,6 +44,7 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_FAN = 6, }; +/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value enum ClimateFanMode : uint8_t { /// The fan mode is set to On CLIMATE_FAN_ON = 0, @@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t { /// The fan mode is set to Diffuse CLIMATE_FAN_DIFFUSE = 8, /// The fan mode is set to Quiet - CLIMATE_FAN_QUIET = 9, + CLIMATE_FAN_QUIET = 9, // Update ClimateFanModeMask in climate_traits.h if adding values after this }; /// 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 { /// The swing mode is set to Off CLIMATE_SWING_OFF = 0, @@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t { /// The fan mode is set to Vertical CLIMATE_SWING_VERTICAL = 2, /// The fan mode is set to Horizontal - CLIMATE_SWING_HORIZONTAL = 3, + CLIMATE_SWING_HORIZONTAL = 3, // Update ClimateSwingModeMask in climate_traits.h if adding values after this }; /// 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 { /// No preset is active CLIMATE_PRESET_NONE = 0, @@ -95,7 +99,7 @@ enum ClimatePreset : uint8_t { /// Device is prepared for sleep CLIMATE_PRESET_SLEEP = 6, /// Device is reacting to activity (e.g., movement sensors) - CLIMATE_PRESET_ACTIVITY = 7, + CLIMATE_PRESET_ACTIVITY = 7, // Update ClimatePresetMask in climate_traits.h if adding values after this }; enum ClimateFeature : uint32_t { diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 42affba3e9..cddd10e47a 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -15,22 +15,13 @@ class APIConnection; namespace climate { -// Bitmask sizes for climate enums -constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) -constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = - 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) -constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) -constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) - -// No template specializations needed - all climate enums use 1:1 mapping (enum value = bit position) -// FiniteSetMask's default implementations handle this automatically. - // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead -using ClimateModeMask = FiniteSetMask; -using ClimateFanModeMask = FiniteSetMask; -using ClimateSwingModeMask = FiniteSetMask; -using ClimatePresetMask = FiniteSetMask; +// For contiguous enums starting at 0, bitmask size is automatically calculated from the last enum value +using ClimateModeMask = FiniteSetMask; +using ClimateFanModeMask = FiniteSetMask; +using ClimateSwingModeMask = FiniteSetMask; +using ClimatePresetMask = FiniteSetMask; // Lightweight linear search for small vectors (1-20 items) // Avoids std::find template overhead From 7c7f1e755dafe4c9459897e6a35ed0e49d835205 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:55:10 -1000 Subject: [PATCH 40/46] merge --- esphome/core/finite_set_mask.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index fdb9bcbc08..ab2454508f 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -23,7 +23,7 @@ namespace esphome { /// /// Example usage (1:1 mapping - climate enums): /// // For enums with contiguous values starting at 0, no specialization needed! -/// using ClimateModeMask = FiniteSetMask; +/// using ClimateModeMask = FiniteSetMask; /// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); /// if (modes.count(CLIMATE_MODE_HEAT)) { ... } /// for (auto mode : modes) { ... } // Iterate over set bits From 94809c4687511ce06964b3933fe0fade8347ea56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 10:07:36 -1000 Subject: [PATCH 41/46] merge --- esphome/core/finite_set_mask.h | 70 ++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index ab2454508f..d3f0b52a71 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -8,44 +8,54 @@ namespace esphome { +/// Default bit mapping policy for contiguous enums starting at 0 +/// Provides 1:1 mapping where enum value equals bit position +template struct DefaultBitPolicy { + // Automatic bitmask type selection based on MaxBits + // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t + using mask_t = typename std::conditional<(MaxBits <= 8), uint8_t, + typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; + + static constexpr int max_bits = MaxBits; + + static constexpr unsigned to_bit(ValueType value) { return static_cast(value); } + + static constexpr ValueType from_bit(unsigned bit) { return static_cast(bit); } +}; + /// Generic bitmask for storing a finite set of discrete values efficiently. /// Replaces std::set to eliminate red-black tree overhead (~586 bytes per instantiation). /// /// Template parameters: /// ValueType: The type to store (typically enum, but can be any discrete bounded type) -/// MaxBits: Maximum number of bits needed (auto-selects uint8_t/uint16_t/uint32_t) +/// BitPolicy: Policy class defining bit mapping and mask type (defaults to DefaultBitPolicy) /// -/// Requirements: -/// - ValueType must have a bounded discrete range that maps to bit positions -/// - For 1:1 mappings (contiguous enums starting at 0), no specialization needed -/// - For custom mappings (like ColorMode), specialize value_to_bit() and/or bit_to_value() -/// - MaxBits must be sufficient to hold all possible values +/// BitPolicy requirements: +/// - using mask_t = // Bitmask storage type +/// - static constexpr int max_bits // Maximum number of bits +/// - static constexpr unsigned to_bit(ValueType) // Convert value to bit position +/// - static constexpr ValueType from_bit(unsigned) // Convert bit position to value /// /// Example usage (1:1 mapping - climate enums): -/// // For enums with contiguous values starting at 0, no specialization needed! -/// using ClimateModeMask = FiniteSetMask; +/// // For contiguous enums starting at 0, use DefaultBitPolicy +/// using ClimateModeMask = FiniteSetMask>; /// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); /// if (modes.count(CLIMATE_MODE_HEAT)) { ... } -/// for (auto mode : modes) { ... } // Iterate over set bits +/// for (auto mode : modes) { ... } /// /// Example usage (custom mapping - ColorMode): -/// // For non-contiguous enums or custom mappings, specialize value_to_bit() and/or bit_to_value() +/// // For custom mappings, define a custom BitPolicy /// // See esphome/components/light/color_mode.h for complete example /// /// Design notes: -/// - Uses compile-time type selection for optimal size (uint8_t/uint16_t/uint32_t) +/// - Policy-based design allows custom bit mappings without template specialization /// - Iterator converts bit positions to actual values during traversal /// - All operations are constexpr-compatible for compile-time initialization /// - Drop-in replacement for std::set with simpler API -/// - Despite the name, works with any discrete bounded type, not just enums /// -template class FiniteSetMask { +template> class FiniteSetMask { public: - // Automatic bitmask type selection based on MaxBits - // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t - using bitmask_t = - typename std::conditional<(MaxBits <= 8), uint8_t, - typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; + using bitmask_t = typename BitPolicy::mask_t; constexpr FiniteSetMask() = default; @@ -57,7 +67,7 @@ template class FiniteSetMask { } /// Add a single value to the set (std::set compatibility) - constexpr void insert(ValueType value) { this->mask_ |= (static_cast(1) << value_to_bit(value)); } + constexpr void insert(ValueType value) { this->mask_ |= (static_cast(1) << BitPolicy::to_bit(value)); } /// Add multiple values from initializer list constexpr void insert(std::initializer_list values) { @@ -67,7 +77,7 @@ template class FiniteSetMask { } /// Remove a value from the set (std::set compatibility) - constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast(1) << value_to_bit(value)); } + constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast(1) << BitPolicy::to_bit(value)); } /// Clear all values from the set constexpr void clear() { this->mask_ = 0; } @@ -75,7 +85,7 @@ template class FiniteSetMask { /// Check if the set contains a specific value (std::set compatibility) /// Returns 1 if present, 0 if not (same as std::set for unique elements) constexpr size_t count(ValueType value) const { - return (this->mask_ & (static_cast(1) << value_to_bit(value))) != 0 ? 1 : 0; + return (this->mask_ & (static_cast(1) << BitPolicy::to_bit(value))) != 0 ? 1 : 0; } /// Count the number of values in the set @@ -109,7 +119,7 @@ template class FiniteSetMask { constexpr ValueType operator*() const { // Return value for the first set bit - return bit_to_value(find_next_set_bit(mask_, 0)); + return BitPolicy::from_bit(find_next_set_bit(mask_, 0)); } constexpr Iterator &operator++() { @@ -135,30 +145,26 @@ template class FiniteSetMask { /// Check if a specific value is present in a raw bitmask /// Useful for checking intersection results without creating temporary objects static constexpr bool mask_contains(bitmask_t mask, ValueType value) { - return (mask & (static_cast(1) << value_to_bit(value))) != 0; + return (mask & (static_cast(1) << BitPolicy::to_bit(value))) != 0; } /// Get the first value from a raw bitmask /// Used for optimizing intersection logic (e.g., "pick first suitable mode") - static constexpr ValueType first_value_from_mask(bitmask_t mask) { return bit_to_value(find_next_set_bit(mask, 0)); } + static constexpr ValueType first_value_from_mask(bitmask_t mask) { + return BitPolicy::from_bit(find_next_set_bit(mask, 0)); + } /// Find the next set bit in a bitmask starting from a given position - /// Returns the bit position, or MaxBits if no more bits are set + /// Returns the bit position, or max_bits if no more bits are set static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) { int bit = start_bit; - while (bit < MaxBits && !(mask & (static_cast(1) << bit))) { + while (bit < BitPolicy::max_bits && !(mask & (static_cast(1) << bit))) { ++bit; } return bit; } protected: - // Default implementations for 1:1 mapping (enum value = bit position) - // For enums with contiguous values starting at 0, these defaults work as-is. - // If you need custom mapping (like ColorMode), provide specializations. - static constexpr int value_to_bit(ValueType value) { return static_cast(value); } - static constexpr ValueType bit_to_value(int bit) { return static_cast(bit); } - bitmask_t mask_{0}; }; From a284a06916df260c74e8a7bcacc2fdfd46bff3a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 10:08:27 -1000 Subject: [PATCH 42/46] policy --- esphome/components/climate/climate_traits.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index cddd10e47a..97fb4d0432 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -17,11 +17,13 @@ namespace climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead -// For contiguous enums starting at 0, bitmask size is automatically calculated from the last enum value -using ClimateModeMask = FiniteSetMask; -using ClimateFanModeMask = FiniteSetMask; -using ClimateSwingModeMask = FiniteSetMask; -using ClimatePresetMask = FiniteSetMask; +// 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>; +using ClimateFanModeMask = FiniteSetMask>; +using ClimateSwingModeMask = + FiniteSetMask>; +using ClimatePresetMask = FiniteSetMask>; // Lightweight linear search for small vectors (1-20 items) // Avoids std::find template overhead From 42a86fe3330b0f6daac8ea52311963927510a1fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 10:18:51 -1000 Subject: [PATCH 43/46] merge --- esphome/core/finite_set_mask.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index d3f0b52a71..f9cd0377c7 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -16,7 +16,7 @@ template struct DefaultBitPolicy { using mask_t = typename std::conditional<(MaxBits <= 8), uint8_t, typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; - static constexpr int max_bits = MaxBits; + static constexpr int MAX_BITS = MaxBits; static constexpr unsigned to_bit(ValueType value) { return static_cast(value); } @@ -32,7 +32,7 @@ template struct DefaultBitPolicy { /// /// BitPolicy requirements: /// - using mask_t = // Bitmask storage type -/// - static constexpr int max_bits // Maximum number of bits +/// - static constexpr int MAX_BITS // Maximum number of bits /// - static constexpr unsigned to_bit(ValueType) // Convert value to bit position /// - static constexpr ValueType from_bit(unsigned) // Convert bit position to value /// @@ -155,10 +155,10 @@ template(1) << bit))) { + while (bit < BitPolicy::MAX_BITS && !(mask & (static_cast(1) << bit))) { ++bit; } return bit; From f58b90a67c31496cb07bb41b5c8a2d133bb13fe6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 10:34:44 -1000 Subject: [PATCH 44/46] preen --- esphome/components/climate/climate_traits.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 97fb4d0432..1161a54f4e 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -6,13 +6,6 @@ #include "esphome/core/helpers.h" namespace esphome { - -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif - namespace climate { // Type aliases for climate enum bitmasks From d8cb5d4aa40cf11f86bf9574258bacb377f93e2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 14:33:02 -1000 Subject: [PATCH 45/46] Fix light_traits.h to use correct FiniteSetMask API - Use count() instead of contains() (std::set compatible API) - Use has_capability() free function instead of method - Matches enum_mask_helper implementation --- esphome/components/light/light_traits.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 4532edca83..294b0cad1d 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -26,9 +26,9 @@ class LightTraits { this->supported_color_modes_ = ColorModeMask(modes); } - bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); } + bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode) > 0; } bool supports_color_capability(ColorCapability color_capability) const { - return this->supported_color_modes_.has_capability(color_capability); + return has_capability(this->supported_color_modes_, color_capability); } float get_min_mireds() const { return this->min_mireds_; } From ae41ae80caf1840ef9912adb2a7f9b36c814b1c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 14:33:48 -1000 Subject: [PATCH 46/46] Fix light_call.cpp to use first_value_from_mask instead of first_mode_from_mask The generic FiniteSetMask uses first_value_from_mask, not first_mode_from_mask. This aligns with the enum_mask_helper implementation. --- esphome/components/light/light_call.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index f611baba71..df17f53adc 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -437,7 +437,7 @@ ColorMode LightCall::compute_color_mode_() { // Use the preferred suitable mode. if (intersection != 0) { - ColorMode mode = ColorModeMask::first_mode_from_mask(intersection); + ColorMode mode = ColorModeMask::first_value_from_mask(intersection); ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode;