From 666660406926e1cf94c500ed5982fc01f3165b2e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 27 May 2025 15:47:42 -0500 Subject: [PATCH 1/6] [i2s-audio] ensure mic task isn't pinned to a core (#8879) --- .../microphone/i2s_audio_microphone.cpp | 122 ++++++++---------- .../microphone/i2s_audio_microphone.h | 4 + 2 files changed, 60 insertions(+), 66 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 2ff1daa197..7c11d4f47d 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -30,11 +30,11 @@ static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000; static const char *const TAG = "i2s_audio.microphone"; enum MicrophoneEventGroupBits : uint32_t { - COMMAND_STOP = (1 << 0), // stops the microphone task - TASK_STARTING = (1 << 10), - TASK_RUNNING = (1 << 11), - TASK_STOPPING = (1 << 12), - TASK_STOPPED = (1 << 13), + COMMAND_STOP = (1 << 0), // stops the microphone task, set and cleared by ``loop`` + + TASK_STARTING = (1 << 10), // set by mic task, cleared by ``loop`` + TASK_RUNNING = (1 << 11), // set by mic task, cleared by ``loop`` + TASK_STOPPED = (1 << 13), // set by mic task, cleared by ``loop`` ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits }; @@ -151,24 +151,21 @@ bool I2SAudioMicrophone::start_driver_() { config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); - this->status_set_error(); + ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); return false; } err = i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error setting ADC mode: %s", esp_err_to_name(err)); - this->status_set_error(); - return false; - } - err = i2s_adc_enable(this->parent_->get_port()); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Error enabling ADC: %s", esp_err_to_name(err)); - this->status_set_error(); + ESP_LOGE(TAG, "Error setting ADC mode: %s", esp_err_to_name(err)); return false; } + err = i2s_adc_enable(this->parent_->get_port()); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error enabling ADC: %s", esp_err_to_name(err)); + return false; + } } else #endif { @@ -177,8 +174,7 @@ bool I2SAudioMicrophone::start_driver_() { err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); - this->status_set_error(); + ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); return false; } @@ -187,8 +183,7 @@ bool I2SAudioMicrophone::start_driver_() { err = i2s_set_pin(this->parent_->get_port(), &pin_config); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error setting I2S pin: %s", esp_err_to_name(err)); - this->status_set_error(); + ESP_LOGE(TAG, "Error setting I2S pin: %s", esp_err_to_name(err)); return false; } } @@ -203,8 +198,7 @@ bool I2SAudioMicrophone::start_driver_() { /* Allocate a new RX channel and get the handle of this channel */ err = i2s_new_channel(&chan_cfg, NULL, &this->rx_handle_); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err)); - this->status_set_error(); + ESP_LOGE(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err)); return false; } @@ -276,22 +270,20 @@ bool I2SAudioMicrophone::start_driver_() { err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg); } if (err != ESP_OK) { - ESP_LOGW(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err)); - this->status_set_error(); + ESP_LOGE(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err)); return false; } /* Before reading data, start the RX channel first */ i2s_channel_enable(this->rx_handle_); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error enabling I2S Microphone: %s", esp_err_to_name(err)); - this->status_set_error(); + ESP_LOGE(TAG, "Error enabling I2S Microphone: %s", esp_err_to_name(err)); return false; } #endif - this->status_clear_error(); this->configure_stream_settings_(); // redetermine the settings in case some settings were changed after compilation + return true; } @@ -303,71 +295,55 @@ void I2SAudioMicrophone::stop() { } void I2SAudioMicrophone::stop_driver_() { + // There is no harm continuing to unload the driver if an error is ever returned by the various functions. This + // ensures that we stop/unload the driver when it only partially starts. + esp_err_t err; #ifdef USE_I2S_LEGACY #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { err = i2s_adc_disable(this->parent_->get_port()); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error disabling ADC: %s", esp_err_to_name(err)); - this->status_set_error(); - return; + ESP_LOGW(TAG, "Error disabling ADC - it may not have started: %s", esp_err_to_name(err)); } } #endif err = i2s_stop(this->parent_->get_port()); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error stopping I2S microphone: %s", esp_err_to_name(err)); - this->status_set_error(); - return; + ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); } err = i2s_driver_uninstall(this->parent_->get_port()); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error uninstalling I2S driver: %s", esp_err_to_name(err)); - this->status_set_error(); - return; + ESP_LOGW(TAG, "Error uninstalling I2S driver - it may not have started: %s", esp_err_to_name(err)); } #else /* Have to stop the channel before deleting it */ err = i2s_channel_disable(this->rx_handle_); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error stopping I2S microphone: %s", esp_err_to_name(err)); - this->status_set_error(); - return; + ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); } /* If the handle is not needed any more, delete it to release the channel resources */ err = i2s_del_channel(this->rx_handle_); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error deleting I2S channel: %s", esp_err_to_name(err)); - this->status_set_error(); - return; + ESP_LOGW(TAG, "Error deleting I2S channel - it may not have started: %s", esp_err_to_name(err)); } #endif this->parent_->unlock(); - this->status_clear_error(); } void I2SAudioMicrophone::mic_task(void *params) { I2SAudioMicrophone *this_microphone = (I2SAudioMicrophone *) params; - xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STARTING); - uint8_t start_counter = 0; - bool started = this_microphone->start_driver_(); - while (!started && start_counter < 10) { - // Attempt to load the driver again in 100 ms. Doesn't slow down main loop since its in a task. - vTaskDelay(pdMS_TO_TICKS(100)); - ++start_counter; - started = this_microphone->start_driver_(); - } + { // Ensures the samples vector is freed when the task stops - if (started) { - xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING); const size_t bytes_to_read = this_microphone->audio_stream_info_.ms_to_bytes(READ_DURATION_MS); std::vector samples; samples.reserve(bytes_to_read); - while (!(xEventGroupGetBits(this_microphone->event_group_) & COMMAND_STOP)) { + xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING); + + while (!(xEventGroupGetBits(this_microphone->event_group_) & MicrophoneEventGroupBits::COMMAND_STOP)) { if (this_microphone->data_callbacks_.size() > 0) { samples.resize(bytes_to_read); size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * pdMS_TO_TICKS(READ_DURATION_MS)); @@ -382,9 +358,6 @@ void I2SAudioMicrophone::mic_task(void *params) { } } - xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPING); - this_microphone->stop_driver_(); - xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPED); while (true) { // Continuously delay until the loop method deletes the task @@ -425,7 +398,10 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w #endif if ((err != ESP_OK) && ((err != ESP_ERR_TIMEOUT) || (ticks_to_wait != 0))) { // Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call - ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); + if (!this->status_has_warning()) { + // Avoid spamming the logs with the error message if its repeated + ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); + } this->status_set_warning(); return 0; } @@ -452,7 +428,7 @@ void I2SAudioMicrophone::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits & MicrophoneEventGroupBits::TASK_STARTING) { - ESP_LOGD(TAG, "Task has started, attempting to setup I2S audio driver"); + ESP_LOGD(TAG, "Task started, attempting to allocate buffer"); xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STARTING); } @@ -463,23 +439,25 @@ void I2SAudioMicrophone::loop() { this->state_ = microphone::STATE_RUNNING; } - if (event_group_bits & MicrophoneEventGroupBits::TASK_STOPPING) { - ESP_LOGD(TAG, "Task is stopping, attempting to unload the I2S audio driver"); - xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STOPPING); - } - if ((event_group_bits & MicrophoneEventGroupBits::TASK_STOPPED)) { - ESP_LOGD(TAG, "Task is finished, freeing resources"); + ESP_LOGD(TAG, "Task finished, freeing resources and uninstalling I2S driver"); + vTaskDelete(this->task_handle_); this->task_handle_ = nullptr; + this->stop_driver_(); xEventGroupClearBits(this->event_group_, ALL_BITS); + this->status_clear_error(); + this->state_ = microphone::STATE_STOPPED; } + // Start the microphone if any semaphores are taken if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) < MAX_LISTENERS) && (this->state_ == microphone::STATE_STOPPED)) { this->state_ = microphone::STATE_STARTING; } + + // Stop the microphone if all semaphores are returned if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) == MAX_LISTENERS) && (this->state_ == microphone::STATE_RUNNING)) { this->state_ = microphone::STATE_STOPPING; @@ -487,14 +465,26 @@ void I2SAudioMicrophone::loop() { switch (this->state_) { case microphone::STATE_STARTING: - if ((this->task_handle_ == nullptr) && !this->status_has_error()) { + if (this->status_has_error()) { + break; + } + + if (!this->start_driver_()) { + this->status_momentary_error("I2S driver failed to start, unloading it and attempting again in 1 second", 1000); + this->stop_driver_(); // Stop/frees whatever possibly started + break; + } + + if (this->task_handle_ == nullptr) { xTaskCreate(I2SAudioMicrophone::mic_task, "mic_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, &this->task_handle_); if (this->task_handle_ == nullptr) { this->status_momentary_error("Task failed to start, attempting again in 1 second", 1000); + this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt } } + break; case microphone::STATE_RUNNING: break; diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 39249e879b..c35f88f8ee 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -43,7 +43,11 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub #endif protected: + /// @brief Starts the I2S driver. Updates the ``audio_stream_info_`` member variable with the current setttings. + /// @return True if succesful, false otherwise bool start_driver_(); + + /// @brief Stops the I2S driver. void stop_driver_(); /// @brief Attempts to correct a microphone DC offset; e.g., a microphones silent level is offset from 0. Applies a From 8583466c6a6977ee14084dd74121189eedf71bcc Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 30 May 2025 04:38:50 -0700 Subject: [PATCH 2/6] [rp2040] use low-level control for ISR gpio and add IRAM_ATTR (#8950) Co-authored-by: Samuel Sieb --- esphome/components/rp2040/gpio.cpp | 35 ++++++++++++++++++++++-------- esphome/core/hal.h | 5 +++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/esphome/components/rp2040/gpio.cpp b/esphome/components/rp2040/gpio.cpp index e32b93b5c2..3927815e46 100644 --- a/esphome/components/rp2040/gpio.cpp +++ b/esphome/components/rp2040/gpio.cpp @@ -8,7 +8,7 @@ namespace rp2040 { static const char *const TAG = "rp2040"; -static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { +static int flags_to_mode(gpio::Flags flags, uint8_t pin) { if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone) return INPUT; } else if (flags == gpio::FLAG_OUTPUT) { @@ -25,14 +25,16 @@ static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { } struct ISRPinArg { + uint32_t mask; uint8_t pin; bool inverted; }; ISRInternalGPIOPin RP2040GPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) - arg->pin = pin_; - arg->inverted = inverted_; + arg->pin = this->pin_; + arg->inverted = this->inverted_; + arg->mask = 1 << this->pin_; return ISRInternalGPIOPin((void *) arg); } @@ -81,21 +83,36 @@ void RP2040GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } using namespace rp2040; bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { - auto *arg = reinterpret_cast(arg_); - return bool(digitalRead(arg->pin)) != arg->inverted; // NOLINT + auto *arg = reinterpret_cast(this->arg_); + return bool(sio_hw->gpio_in & arg->mask) != arg->inverted; } + void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { - auto *arg = reinterpret_cast(arg_); - digitalWrite(arg->pin, value != arg->inverted ? 1 : 0); // NOLINT + auto *arg = reinterpret_cast(this->arg_); + if (value != arg->inverted) { + sio_hw->gpio_set = arg->mask; + } else { + sio_hw->gpio_clr = arg->mask; + } } + void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { // TODO: implement // auto *arg = reinterpret_cast(arg_); // GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin); } + void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { - auto *arg = reinterpret_cast(arg_); - pinMode(arg->pin, flags_to_mode(flags, arg->pin)); // NOLINT + auto *arg = reinterpret_cast(this->arg_); + if (flags & gpio::FLAG_OUTPUT) { + sio_hw->gpio_oe_set = arg->mask; + } else if (flags & gpio::FLAG_INPUT) { + sio_hw->gpio_oe_clr = arg->mask; + hw_write_masked(&padsbank0_hw->io[arg->pin], + (bool_to_bit(flags & gpio::FLAG_PULLUP) << PADS_BANK0_GPIO0_PUE_LSB) | + (bool_to_bit(flags & gpio::FLAG_PULLDOWN) << PADS_BANK0_GPIO0_PDE_LSB), + PADS_BANK0_GPIO0_PUE_BITS | PADS_BANK0_GPIO0_PDE_BITS); + } } } // namespace esphome diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 034f9d692f..0ccf21ad83 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -24,6 +24,11 @@ #define PROGMEM ICACHE_RODATA_ATTR #endif +#elif defined(USE_RP2040) + +#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical"))) +#define PROGMEM + #else #define IRAM_ATTR From 6554af21b9b039356787f480cac9664aa243c501 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sat, 31 May 2025 12:21:08 -0700 Subject: [PATCH 3/6] [esp8266] fix isr pin (#8981) Co-authored-by: Samuel Sieb --- esphome/components/esp8266/gpio.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index 9f23e8e67e..4a997a790c 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -40,6 +40,7 @@ struct ISRPinArg { volatile uint32_t *mode_set_reg; volatile uint32_t *mode_clr_reg; volatile uint32_t *func_reg; + volatile uint32_t *control_reg; uint32_t mask; }; @@ -54,6 +55,7 @@ ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const { arg->mode_set_reg = &GPES; arg->mode_clr_reg = &GPEC; arg->func_reg = &GPF(this->pin_); + arg->control_reg = &GPC(this->pin_); arg->mask = 1 << this->pin_; } else { arg->in_reg = &GP16I; @@ -62,6 +64,7 @@ ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const { arg->mode_set_reg = &GP16E; arg->mode_clr_reg = nullptr; arg->func_reg = &GPF16; + arg->control_reg = nullptr; arg->mask = 1; } return ISRInternalGPIOPin((void *) arg); @@ -143,11 +146,17 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { if (arg->pin < 16) { if (flags & gpio::FLAG_OUTPUT) { *arg->mode_set_reg = arg->mask; - } else { + if (flags & gpio::FLAG_OPEN_DRAIN) { + *arg->control_reg |= 1 << GPCD; + } else { + *arg->control_reg &= ~(1 << GPCD); + } + } else if (flags & gpio::FLAG_INPUT) { *arg->mode_clr_reg = arg->mask; } if (flags & gpio::FLAG_PULLUP) { *arg->func_reg |= 1 << GPFPU; + *arg->control_reg |= 1 << GPCD; } else { *arg->func_reg &= ~(1 << GPFPU); } From aecac1580968e1abf761af095aaa27d0122e48e3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 1 Jun 2025 05:27:48 +1000 Subject: [PATCH 4/6] [debug] Make sensors work without logger debug level (#8980) --- esphome/components/debug/debug_component.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index c4de42c7e9..41e07dac35 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -15,10 +15,6 @@ namespace debug { static const char *const TAG = "debug"; void DebugComponent::dump_config() { -#ifndef ESPHOME_LOG_HAS_DEBUG - return; // Can't log below if debug logging is disabled -#endif - ESP_LOGCONFIG(TAG, "Debug component:"); #ifdef USE_TEXT_SENSOR LOG_TEXT_SENSOR(" ", "Device info", this->device_info_); From 162472bdc23b8af1515df647d82ecbccb2471087 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jun 2025 17:46:17 +0100 Subject: [PATCH 5/6] Fix logger stack overflow (#8988) --- esphome/components/logger/logger.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 5c53c4d40c..6030d9e8f2 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -212,9 +212,9 @@ class Logger : public Component { } // Format string to explicit buffer with varargs - inline void printf_to_buffer_(const char *format, char *buffer, int *buffer_at, int buffer_size, ...) { + inline void printf_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, ...) { va_list arg; - va_start(arg, buffer_size); + va_start(arg, format); this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg); va_end(arg); } @@ -312,13 +312,13 @@ class Logger : public Component { #if defined(USE_ESP32) || defined(USE_LIBRETINY) if (thread_name != nullptr) { // Non-main task with thread name - this->printf_to_buffer_("%s[%s][%s:%03u]%s[%s]%s: ", buffer, buffer_at, buffer_size, color, letter, tag, line, + this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color); return; } #endif // Main task or non ESP32/LibreTiny platform - this->printf_to_buffer_("%s[%s][%s:%03u]: ", buffer, buffer_at, buffer_size, color, letter, tag, line); + this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line); } inline void HOT format_body_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, From 1bbc6db1c38a54f14f1b9c0d904538108f6f5f90 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 3 Jun 2025 20:04:39 +1200 Subject: [PATCH 6/6] Bump version to 2025.5.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index e5c3c1802d..c2bde26ded 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.5.1 +PROJECT_NUMBER = 2025.5.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 2fc30beaaa..aad89e0d1a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.5.1" +__version__ = "2025.5.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = (