diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index 04c01948c8..1e850a18fe 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "ac_dimmer.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -9,12 +7,12 @@ #ifdef USE_ESP8266 #include #endif -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include + +#ifdef USE_ESP32 +#include "hw_timer_esp_idf.h" #endif -namespace esphome { -namespace ac_dimmer { +namespace esphome::ac_dimmer { static const char *const TAG = "ac_dimmer"; @@ -27,7 +25,14 @@ static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-no /// However other factors like gate driver propagation time /// are also considered and a really low value is not important /// See also: https://github.com/esphome/issues/issues/1632 -static const uint32_t GATE_ENABLE_TIME = 50; +static constexpr uint32_t GATE_ENABLE_TIME = 50; + +#ifdef USE_ESP32 +/// Timer frequency in Hz (1 MHz = 1µs resolution) +static constexpr uint32_t TIMER_FREQUENCY_HZ = 1000000; +/// Timer interrupt interval in microseconds +static constexpr uint64_t TIMER_INTERVAL_US = 50; +#endif /// Function called from timer interrupt /// Input is current time in microseconds (micros()) @@ -154,7 +159,7 @@ void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) { #ifdef USE_ESP32 // ESP32 implementation, uses basically the same code but needs to wrap // timer_interrupt() function to auto-reschedule -static hw_timer_t *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static HWTimer *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); } #endif @@ -194,15 +199,15 @@ void AcDimmer::setup() { setTimer1Callback(&timer_interrupt); #endif #ifdef USE_ESP32 - // timer frequency of 1mhz - dimmer_timer = timerBegin(1000000); - timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); + dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ); + timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); // For ESP32, we can't use dynamic interval calculation because the timerX functions // are not callable from ISR (placed in flash storage). // Here we just use an interrupt firing every 50 µs. - timerAlarm(dimmer_timer, 50, true, 0); + timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0); #endif } + void AcDimmer::write_state(float state) { state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation auto new_value = static_cast(roundf(state * 65535)); @@ -210,6 +215,7 @@ void AcDimmer::write_state(float state) { this->store_.init_cycle = this->init_with_half_cycle_; this->store_.value = new_value; } + void AcDimmer::dump_config() { ESP_LOGCONFIG(TAG, "AcDimmer:\n" @@ -230,7 +236,4 @@ void AcDimmer::dump_config() { ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2); } -} // namespace ac_dimmer -} // namespace esphome - -#endif // USE_ARDUINO +} // namespace esphome::ac_dimmer diff --git a/esphome/components/ac_dimmer/ac_dimmer.h b/esphome/components/ac_dimmer/ac_dimmer.h index fd1bbc28db..ca2a19210a 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.h +++ b/esphome/components/ac_dimmer/ac_dimmer.h @@ -1,13 +1,10 @@ #pragma once -#ifdef USE_ARDUINO - #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace ac_dimmer { +namespace esphome::ac_dimmer { enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING }; @@ -64,7 +61,4 @@ class AcDimmer : public output::FloatOutput, public Component { DimMethod method_; }; -} // namespace ac_dimmer -} // namespace esphome - -#endif // USE_ARDUINO +} // namespace esphome::ac_dimmer diff --git a/esphome/components/ac_dimmer/hw_timer_esp_idf.cpp b/esphome/components/ac_dimmer/hw_timer_esp_idf.cpp new file mode 100644 index 0000000000..543b476085 --- /dev/null +++ b/esphome/components/ac_dimmer/hw_timer_esp_idf.cpp @@ -0,0 +1,152 @@ +#ifdef USE_ESP32 + +#include "hw_timer_esp_idf.h" + +#include "freertos/FreeRTOS.h" +#include "esphome/core/log.h" + +#include "driver/gptimer.h" +#include "esp_clk_tree.h" +#include "soc/clk_tree_defs.h" + +static const char *const TAG = "hw_timer_esp_idf"; + +namespace esphome::ac_dimmer { + +// GPTimer divider constraints from ESP-IDF documentation +static constexpr uint32_t GPTIMER_DIVIDER_MIN = 2; +static constexpr uint32_t GPTIMER_DIVIDER_MAX = 65536; + +using voidFuncPtr = void (*)(); +using voidFuncPtrArg = void (*)(void *); + +struct InterruptConfigT { + voidFuncPtr fn{nullptr}; + void *arg{nullptr}; +}; + +struct HWTimer { + gptimer_handle_t timer_handle{nullptr}; + InterruptConfigT interrupt_handle{}; + bool timer_started{false}; +}; + +HWTimer *timer_begin(uint32_t frequency) { + esp_err_t err = ESP_OK; + uint32_t counter_src_hz = 0; + uint32_t divider = 0; + soc_module_clk_t clk; + for (auto clk_candidate : SOC_GPTIMER_CLKS) { + clk = clk_candidate; + esp_clk_tree_src_get_freq_hz(clk, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &counter_src_hz); + divider = counter_src_hz / frequency; + if ((divider >= GPTIMER_DIVIDER_MIN) && (divider <= GPTIMER_DIVIDER_MAX)) { + break; + } else { + divider = 0; + } + } + + if (divider == 0) { + ESP_LOGE(TAG, "Resolution not possible; aborting"); + return nullptr; + } + + gptimer_config_t config = { + .clk_src = static_cast(clk), + .direction = GPTIMER_COUNT_UP, + .resolution_hz = frequency, + .flags = {.intr_shared = true}, + }; + + HWTimer *timer = new HWTimer(); + + err = gptimer_new_timer(&config, &timer->timer_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "GPTimer creation failed; error %d", err); + delete timer; + return nullptr; + } + + err = gptimer_enable(timer->timer_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "GPTimer enable failed; error %d", err); + gptimer_del_timer(timer->timer_handle); + delete timer; + return nullptr; + } + + err = gptimer_start(timer->timer_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "GPTimer start failed; error %d", err); + gptimer_disable(timer->timer_handle); + gptimer_del_timer(timer->timer_handle); + delete timer; + return nullptr; + } + + timer->timer_started = true; + return timer; +} + +bool IRAM_ATTR timer_fn_wrapper(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *args) { + auto *isr = static_cast(args); + if (isr->fn) { + if (isr->arg) { + reinterpret_cast(isr->fn)(isr->arg); + } else { + isr->fn(); + } + } + // Return false to indicate that no higher-priority task was woken and no context switch is requested. + return false; +} + +static void timer_attach_interrupt_functional_arg(HWTimer *timer, void (*user_func)(void *), void *arg) { + if (timer == nullptr) { + ESP_LOGE(TAG, "Timer handle is nullptr"); + return; + } + gptimer_event_callbacks_t cbs = { + .on_alarm = timer_fn_wrapper, + }; + + timer->interrupt_handle.fn = reinterpret_cast(user_func); + timer->interrupt_handle.arg = arg; + + if (timer->timer_started) { + gptimer_stop(timer->timer_handle); + } + gptimer_disable(timer->timer_handle); + esp_err_t err = gptimer_register_event_callbacks(timer->timer_handle, &cbs, &timer->interrupt_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Timer Attach Interrupt failed; error %d", err); + } + gptimer_enable(timer->timer_handle); + if (timer->timer_started) { + gptimer_start(timer->timer_handle); + } +} + +void timer_attach_interrupt(HWTimer *timer, voidFuncPtr user_func) { + timer_attach_interrupt_functional_arg(timer, reinterpret_cast(user_func), nullptr); +} + +void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count) { + if (timer == nullptr) { + ESP_LOGE(TAG, "Timer handle is nullptr"); + return; + } + gptimer_alarm_config_t alarm_cfg = { + .alarm_count = alarm_value, + .reload_count = reload_count, + .flags = {.auto_reload_on_alarm = autoreload}, + }; + esp_err_t err = gptimer_set_alarm_action(timer->timer_handle, &alarm_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Timer Alarm Write failed; error %d", err); + } +} + +} // namespace esphome::ac_dimmer +#endif diff --git a/esphome/components/ac_dimmer/hw_timer_esp_idf.h b/esphome/components/ac_dimmer/hw_timer_esp_idf.h new file mode 100644 index 0000000000..1b2401ebda --- /dev/null +++ b/esphome/components/ac_dimmer/hw_timer_esp_idf.h @@ -0,0 +1,17 @@ +#pragma once +#ifdef USE_ESP32 + +#include "driver/gptimer_types.h" + +namespace esphome::ac_dimmer { + +struct HWTimer; + +HWTimer *timer_begin(uint32_t frequency); + +void timer_attach_interrupt(HWTimer *timer, void (*user_func)()); +void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count); + +} // namespace esphome::ac_dimmer + +#endif diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py index 9f9afb6d80..efc24b65e7 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -32,7 +32,6 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_with_arduino, ) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 4ececfec94..336672f50b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -186,14 +186,17 @@ void APIServer::loop() { } // Rare case: handle disconnection -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - this->client_disconnected_trigger_->trigger(std::string(client->get_name()), std::string(client->get_peername())); -#endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES this->unregister_active_action_calls_for_connection(client.get()); #endif ESP_LOGV(TAG, "Remove connection %s", client->get_name()); +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Save client info before removal for the trigger + std::string client_name(client->get_name()); + std::string client_peername(client->get_peername()); +#endif + // Swap with the last element and pop (avoids expensive vector shifts) if (client_index < this->clients_.size() - 1) { std::swap(this->clients_[client_index], this->clients_.back()); @@ -205,6 +208,11 @@ void APIServer::loop() { this->status_set_warning(); this->last_connected_ = App.get_loop_component_start_time(); } + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Fire trigger after client is removed so api.connected reflects the true state + this->client_disconnected_trigger_->trigger(client_name, client_peername); +#endif // Don't increment client_index since we need to process the swapped element } } diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 2d93adbb47..a336a9493d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -39,6 +39,24 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) { return (value & 1) ? static_cast(~(value >> 1)) : static_cast(value >> 1); } +/// Count number of varints in a packed buffer +inline uint16_t count_packed_varints(const uint8_t *data, size_t len) { + uint16_t count = 0; + while (len > 0) { + // Skip varint bytes until we find one without continuation bit + while (len > 0 && (*data & 0x80)) { + data++; + len--; + } + if (len > 0) { + data++; + len--; + count++; + } + } + return count; +} + /* * StringRef Ownership Model for API Protocol Messages * =================================================== @@ -180,9 +198,10 @@ class ProtoVarInt { uint64_t value_; }; -// Forward declaration for decode_to_message and encode_to_writer -class ProtoMessage; +// Forward declarations for decode_to_message, encode_message and encode_packed_sint32 class ProtoDecodableMessage; +class ProtoMessage; +class ProtoSize; class ProtoLengthDelimited { public: @@ -334,6 +353,8 @@ class ProtoWriteBuffer { void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { this->encode_uint64(field_id, encode_zigzag64(value), force); } + /// Encode a packed repeated sint32 field (zero-copy from vector) + void encode_packed_sint32(uint32_t field_id, const std::vector &values); void encode_message(uint32_t field_id, const ProtoMessage &value); std::vector *get_buffer() const { return buffer_; } @@ -341,9 +362,6 @@ class ProtoWriteBuffer { std::vector *buffer_; }; -// Forward declaration -class ProtoSize; - class ProtoMessage { public: virtual ~ProtoMessage() = default; @@ -792,8 +810,43 @@ class ProtoSize { } } } + + /** + * @brief Calculate size of a packed repeated sint32 field + */ + inline void add_packed_sint32(uint32_t field_id_size, const std::vector &values) { + if (values.empty()) + return; + + size_t packed_size = 0; + for (int value : values) { + packed_size += varint(encode_zigzag32(value)); + } + + // field_id + length varint + packed data + total_size_ += field_id_size + varint(static_cast(packed_size)) + static_cast(packed_size); + } }; +// Implementation of encode_packed_sint32 - must be after ProtoSize is defined +inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector &values) { + if (values.empty()) + return; + + // Calculate packed size + size_t packed_size = 0; + for (int value : values) { + packed_size += ProtoSize::varint(encode_zigzag32(value)); + } + + // Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values + this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED); + this->encode_varint_raw(packed_size); + for (int value : values) { + this->encode_varint_raw(encode_zigzag32(value)); + } +} + // Implementation of encode_message - must be after ProtoMessage is defined inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) { this->encode_field_raw(field_id, 2); // type 2: Length-delimited message diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index 40202e52ca..20c731e730 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -550,12 +550,12 @@ async def to_code(config: ConfigType) -> None: ref="0.2.2", ) - # Set compile-time configuration via defines + # Set compile-time configuration via build flags (so external library sees them) if CONF_BIT_DEPTH in config: - cg.add_define("HUB75_BIT_DEPTH", config[CONF_BIT_DEPTH]) + cg.add_build_flag(f"-DHUB75_BIT_DEPTH={config[CONF_BIT_DEPTH]}") if CONF_GAMMA_CORRECT in config: - cg.add_define("HUB75_GAMMA_MODE", config[CONF_GAMMA_CORRECT]) + cg.add_build_flag(f"-DHUB75_GAMMA_MODE={config[CONF_GAMMA_CORRECT].enum_value}") # Await all pin expressions pin_expressions = { diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 34aba236b3..2f1c107bf4 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -128,6 +128,39 @@ void RemoteReceiverBase::call_dumpers_() { void RemoteReceiverBinarySensorBase::dump_config() { LOG_BINARY_SENSOR("", "Remote Receiver Binary Sensor", this); } +/* RemoteTransmitData */ + +void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count) { + this->data_.clear(); + this->data_.reserve(count); + + while (len > 0) { + // Parse varint (inline, no dependency on api component) + uint32_t raw = 0; + uint32_t shift = 0; + uint32_t consumed = 0; + for (; consumed < len && consumed < 5; consumed++) { + uint8_t byte = data[consumed]; + raw |= (byte & 0x7F) << shift; + if ((byte & 0x80) == 0) { + consumed++; + break; + } + shift += 7; + } + if (consumed == 0) + break; // Parse error + + // Zigzag decode: (n >> 1) ^ -(n & 1) + int32_t decoded = static_cast((raw >> 1) ^ (~(raw & 1) + 1)); + this->data_.push_back(decoded); + data += consumed; + len -= consumed; + } +} + +/* RemoteTransmitterBase */ + void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) { #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE const auto &vec = this->temp_.get_data(); diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 2cb79bf571..a11e0271af 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -31,6 +31,11 @@ class RemoteTransmitData { uint32_t get_carrier_frequency() const { return this->carrier_frequency_; } const RawTimings &get_data() const { return this->data_; } void set_data(const RawTimings &data) { this->data_ = data; } + /// Set data from packed protobuf sint32 buffer (zigzag + varint encoded) + /// @param data Pointer to packed zigzag-varint-encoded sint32 values + /// @param len Length of the buffer in bytes + /// @param count Number of values (for reserve optimization) + void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count); void reset() { this->data_.clear(); this->carrier_frequency_ = 0; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a11d702f94..ba9e71bd1c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -502,14 +502,16 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J // Build id into stack buffer - ArduinoJson copies the string // Format: {prefix}/{device?}/{name} - // Buffer size guaranteed by schema validation (NAME_MAX_LENGTH=120): - // With devices: domain(20) + "/" + device(120) + "/" + name(120) + null = 263, rounded up to 280 for safety margin - // Without devices: domain(20) + "/" + name(120) + null = 142, rounded up to 150 for safety margin + // Buffer sizes use constants from entity_base.h validated in core/config.py + // Note: Device name uses ESPHOME_FRIENDLY_NAME_MAX_LEN (sub-device max 120), not ESPHOME_DEVICE_NAME_MAX_LEN + // (hostname) #ifdef USE_DEVICES - char id_buf[280]; + static constexpr size_t ID_BUF_SIZE = + ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; #else - char id_buf[150]; + static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; #endif + char id_buf[ID_BUF_SIZE]; char *p = id_buf; memcpy(p, prefix, prefix_len); p += prefix_len; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 7ba1b5e417..26aec29b6d 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -238,12 +238,21 @@ def _apply_min_auth_mode_default(config): def final_validate(config): has_sta = bool(config.get(CONF_NETWORKS, True)) has_ap = CONF_AP in config - has_improv = "esp32_improv" in fv.full_config.get() - has_improv_serial = "improv_serial" in fv.full_config.get() + full_config = fv.full_config.get() + has_improv = "esp32_improv" in full_config + has_improv_serial = "improv_serial" in full_config + has_captive_portal = "captive_portal" in full_config + has_web_server = "web_server" in full_config if not (has_sta or has_ap or has_improv or has_improv_serial): raise cv.Invalid( "Please specify at least an SSID or an Access Point to create." ) + if has_ap and not has_captive_portal and not has_web_server: + _LOGGER.warning( + "WiFi AP is configured but neither captive_portal nor web_server is enabled. " + "The AP will not be usable for configuration or monitoring. " + "Add 'captive_portal:' or 'web_server:' to your configuration." + ) FINAL_VALIDATE_SCHEMA = cv.All( @@ -466,7 +475,7 @@ async def to_code(config): ) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) cg.add_define("USE_WIFI_AP") - elif CORE.is_esp32: + elif CORE.is_esp32 and not CORE.using_arduino: add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False) add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) diff --git a/esphome/core/config.py b/esphome/core/config.py index b7e6ab9bee..21ed8ced1a 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -184,17 +184,24 @@ if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ: else: _compile_process_limit_default = cv.UNDEFINED +# Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h +FRIENDLY_NAME_MAX_LEN = 120 + AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), - cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)), + cv.Required(CONF_NAME): cv.All( + cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN) + ), } ) DEVICE_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Device), - cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)), + cv.Required(CONF_NAME): cv.All( + cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN) + ), cv.Optional(CONF_AREA_ID): cv.use_id(Area), } ) @@ -210,7 +217,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NAME): cv.valid_name, # Keep max=120 in sync with OBJECT_ID_MAX_LEN in esphome/core/entity_base.h cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All( - cv.string_no_slash, cv.Length(max=120) + cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN) ), cv.Optional(CONF_AREA): validate_area_config, cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)), diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 1649077dd0..5f75872a0f 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -16,7 +16,14 @@ namespace esphome { // Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31; -// Maximum size for object_id buffer - keep in sync with friendly_name cv.Length(max=120) in esphome/core/config.py +// Maximum friendly name length for entities and sub-devices - keep in sync with FRIENDLY_NAME_MAX_LEN in +// esphome/core/config.py +static constexpr size_t ESPHOME_FRIENDLY_NAME_MAX_LEN = 120; + +// Maximum domain length (longest: "alarm_control_panel" = 19) +static constexpr size_t ESPHOME_DOMAIN_MAX_LEN = 20; + +// Maximum size for object_id buffer (friendly_name + null + margin) static constexpr size_t OBJECT_ID_MAX_LEN = 128; enum EntityCategory : uint8_t { diff --git a/requirements.txt b/requirements.txt index 56df559cd7..00f81f793f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 -esphome-dashboard==20251013.0 +esphome-dashboard==20260110.0 aioesphomeapi==43.10.1 zeroconf==0.148.0 puremagic==1.30 diff --git a/tests/components/ac_dimmer/test.esp32-ard.yaml b/tests/components/ac_dimmer/test.esp32-ard.yaml deleted file mode 100644 index eaa4901f03..0000000000 --- a/tests/components/ac_dimmer/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - gate_pin: GPIO4 - zero_cross_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ac_dimmer/test.esp32-idf.yaml b/tests/components/ac_dimmer/test.esp32-idf.yaml new file mode 100644 index 0000000000..3ec069f430 --- /dev/null +++ b/tests/components/ac_dimmer/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + gate_pin: GPIO18 + zero_cross_pin: GPIO19 + +<<: !include common.yaml diff --git a/tests/components/hub75/test.esp32-s3-idf-board.yaml b/tests/components/hub75/test.esp32-s3-idf-board.yaml index 3723a80006..8c73a0fd44 100644 --- a/tests/components/hub75/test.esp32-s3-idf-board.yaml +++ b/tests/components/hub75/test.esp32-s3-idf-board.yaml @@ -6,6 +6,7 @@ display: panel_height: 32 double_buffer: true brightness: 128 + gamma_correct: gamma_2_2 pages: - id: page1 lambda: |- diff --git a/tests/components/hub75/test.esp32-s3-idf.yaml b/tests/components/hub75/test.esp32-s3-idf.yaml index f8ee26e73d..0adb4f991b 100644 --- a/tests/components/hub75/test.esp32-s3-idf.yaml +++ b/tests/components/hub75/test.esp32-s3-idf.yaml @@ -5,6 +5,7 @@ display: panel_height: 32 double_buffer: true brightness: 128 + gamma_correct: cie1931 r1_pin: GPIO42 g1_pin: GPIO41 b1_pin: GPIO40 diff --git a/tests/components/mapping/test.rp2040-ard.yaml b/tests/components/mapping/test.rp2040-ard.yaml index acc305a701..fdfed5f6ab 100644 --- a/tests/components/mapping/test.rp2040-ard.yaml +++ b/tests/components/mapping/test.rp2040-ard.yaml @@ -7,6 +7,7 @@ display: platform: ili9xxx id: main_lcd model: ili9342 + data_rate: 31.25MHz cs_pin: 20 dc_pin: 21 reset_pin: 22 diff --git a/tests/components/mdns/test-comprehensive.esp8266-ard.yaml b/tests/components/mdns/test-comprehensive.esp8266-ard.yaml index 3129ca3143..7d3f61cf08 100644 --- a/tests/components/mdns/test-comprehensive.esp8266-ard.yaml +++ b/tests/components/mdns/test-comprehensive.esp8266-ard.yaml @@ -42,4 +42,3 @@ status_led: # Include API at priority 40 api: - password: "apipassword" diff --git a/tests/integration/fixtures/api_conditional_memory.yaml b/tests/integration/fixtures/api_conditional_memory.yaml index 22e8ed79d6..4a0923b93f 100644 --- a/tests/integration/fixtures/api_conditional_memory.yaml +++ b/tests/integration/fixtures/api_conditional_memory.yaml @@ -24,6 +24,14 @@ api: - logger.log: format: "Client %s disconnected from %s" args: [client_info.c_str(), client_address.c_str()] + # Verify fix for issue #11131: api.connected should reflect true state in trigger + - if: + condition: + api.connected: + then: + - logger.log: "Other clients still connected" + else: + - logger.log: "No clients remaining" logger: level: DEBUG diff --git a/tests/integration/test_api_conditional_memory.py b/tests/integration/test_api_conditional_memory.py index 349b572859..91625770d9 100644 --- a/tests/integration/test_api_conditional_memory.py +++ b/tests/integration/test_api_conditional_memory.py @@ -23,12 +23,14 @@ async def test_api_conditional_memory( # Track log messages connected_future = loop.create_future() disconnected_future = loop.create_future() + no_clients_future = loop.create_future() service_simple_future = loop.create_future() service_args_future = loop.create_future() # Patterns to match in logs connected_pattern = re.compile(r"Client .* connected from") disconnected_pattern = re.compile(r"Client .* disconnected from") + no_clients_pattern = re.compile(r"No clients remaining") service_simple_pattern = re.compile(r"Simple service called") service_args_pattern = re.compile( r"Service called with: test_string, 123, 1, 42\.50" @@ -40,6 +42,8 @@ async def test_api_conditional_memory( connected_future.set_result(True) elif not disconnected_future.done() and disconnected_pattern.search(line): disconnected_future.set_result(True) + elif not no_clients_future.done() and no_clients_pattern.search(line): + no_clients_future.set_result(True) elif not service_simple_future.done() and service_simple_pattern.search(line): service_simple_future.set_result(True) elif not service_args_future.done() and service_args_pattern.search(line): @@ -109,3 +113,7 @@ async def test_api_conditional_memory( # Client disconnected here, wait for disconnect log await asyncio.wait_for(disconnected_future, timeout=5.0) + + # Verify fix for issue #11131: api.connected should be false in trigger + # when the last client disconnects + await asyncio.wait_for(no_clients_future, timeout=5.0)