diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index 790cd62db0..ddb669e0eb 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -2,6 +2,8 @@ #ifdef USE_ESP32 +#include + #include "esphome/core/helpers.h" namespace esphome { @@ -75,12 +77,32 @@ bool AudioTransferBuffer::has_buffered_data() const { } bool AudioTransferBuffer::reallocate(size_t new_buffer_size) { - if (this->buffer_length_ > 0) { - // Buffer currently has data, so reallocation is impossible + if (this->buffer_ == nullptr) { + return this->allocate_buffer_(new_buffer_size); + } + + if (new_buffer_size < this->buffer_length_) { + // New size is too small to hold existing data return false; } - this->deallocate_buffer_(); - return this->allocate_buffer_(new_buffer_size); + + // Shift existing data to the start of the buffer so realloc preserves it + if ((this->buffer_length_ > 0) && (this->data_start_ != this->buffer_)) { + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); + this->data_start_ = this->buffer_; + } + + RAMAllocator allocator; + uint8_t *new_buffer = allocator.reallocate(this->buffer_, new_buffer_size); + if (new_buffer == nullptr) { + // Reallocation failed, but the original buffer is still valid + return false; + } + + this->buffer_ = new_buffer; + this->data_start_ = this->buffer_; + this->buffer_size_ = new_buffer_size; + return true; } bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { @@ -115,7 +137,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_ if (pre_shift) { // Shift data in buffer to start if (this->buffer_length_ > 0) { - memmove(this->buffer_, this->data_start_, this->buffer_length_); + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); } this->data_start_ = this->buffer_; } @@ -150,7 +172,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, if (post_shift) { // Shift unwritten data to the start of the buffer - memmove(this->buffer_, this->data_start_, this->buffer_length_); + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); this->data_start_ = this->buffer_; } diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index edb484e7d2..24c0670d1a 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -56,6 +56,9 @@ class AudioTransferBuffer { /// @return True if there is data, false otherwise. virtual bool has_buffered_data() const; + /// @brief Reallocates the transfer buffer, preserving any existing data. + /// @param new_buffer_size The new size in bytes. Must be at least as large as available(). + /// @return True if successful, false otherwise. On failure, the original buffer remains valid. bool reallocate(size_t new_buffer_size); protected: diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 716d270390..ece7cca482 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -126,7 +126,7 @@ void LinearCombinationComponent::setup() { } void LinearCombinationComponent::handle_new_value(float value) { - // Multiplies each sensor state by a configured coeffecient and then sums + // Multiplies each sensor state by a configured coefficient and then sums if (!std::isfinite(value)) return; diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index f5255fec03..0204162e8d 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv @@ -15,6 +17,8 @@ from esphome.const import ( ) from esphome.core.entity_helpers import inherit_property_from +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@Cat-Ion", "@kahrendt"] combination_ns = cg.esphome_ns.namespace("combination") @@ -47,7 +51,8 @@ SumCombinationComponent = combination_ns.class_( "SumCombinationComponent", cg.Component, sensor.Sensor ) -CONF_COEFFECIENT = "coeffecient" +CONF_COEFFICIENT = "coefficient" +CONF_COEFFECIENT = "coeffecient" # Deprecated, remove before 2026.12.0 CONF_ERROR = "error" CONF_KALMAN = "kalman" CONF_LINEAR = "linear" @@ -68,11 +73,34 @@ KALMAN_SOURCE_SCHEMA = cv.Schema( } ) -LINEAR_SOURCE_SCHEMA = cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), - } + +def _migrate_coeffecient(config): + """Migrate deprecated 'coeffecient' spelling to 'coefficient'.""" + if CONF_COEFFECIENT in config: + if CONF_COEFFICIENT in config: + raise cv.Invalid( + f"Cannot specify both '{CONF_COEFFICIENT}' and '{CONF_COEFFECIENT}'" + ) + _LOGGER.warning( + "'%s' is deprecated, use '%s' instead. Will be removed in 2026.12.0", + CONF_COEFFECIENT, + CONF_COEFFICIENT, + ) + config[CONF_COEFFICIENT] = config.pop(CONF_COEFFECIENT) + elif CONF_COEFFICIENT not in config: + raise cv.Invalid(f"'{CONF_COEFFICIENT}' is a required option") + return config + + +LINEAR_SOURCE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Optional(CONF_COEFFICIENT): cv.templatable(cv.float_), + cv.Optional(CONF_COEFFECIENT): cv.templatable(cv.float_), + } + ), + _migrate_coeffecient, ) SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( @@ -162,12 +190,12 @@ async def to_code(config): ) cg.add(var.add_source(source, error)) elif config[CONF_TYPE] == CONF_LINEAR: - coeffecient = await cg.templatable( - source_conf[CONF_COEFFECIENT], + coefficient = await cg.templatable( + source_conf[CONF_COEFFICIENT], [(float, "x")], cg.float_, ) - cg.add(var.add_source(source, coeffecient)) + cg.add(var.add_source(source, coefficient)) else: cg.add(var.add_source(source)) diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 13f66691b2..2657071f45 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -49,10 +49,6 @@ EPaperBase = epaper_spi_ns.class_( ) Transform = epaper_spi_ns.enum("Transform") -EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) -EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) - - # Import all models dynamically from the models package for module_info in pkgutil.iter_modules(models.__path__): importlib.import_module(f".models.{module_info.name}", package=__package__) diff --git a/esphome/components/epaper_spi/epaper_weact_3c.cpp b/esphome/components/epaper_spi/epaper_weact_3c.cpp new file mode 100644 index 0000000000..bd83105dd7 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_weact_3c.cpp @@ -0,0 +1,231 @@ +#include "epaper_weact_3c.h" +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_weact_3c"; + +// SSD1680 3-color display notes: +// - Buffer uses 1 bit per pixel, 8 pixels per byte +// - Buffer first half (black_offset): Black/White plane (1=black, 0=white) +// - Buffer second half (red_offset): Red plane (1=red, 0=no red) +// - Total buffer: width * height / 4 bytes = 2 * (width * height / 8) +// - For 128x296: 128*296/4 = 9472 bytes total (4736 per color) + +void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + + // Calculate position in the 1-bit buffer + const uint32_t pos = (x / 8) + (y * this->row_width_); + const uint8_t bit = 0x80 >> (x & 0x07); + const uint32_t red_offset = this->buffer_length_ / 2u; + + // Use luminance threshold for B/W mapping + // Split at halfway point (382 = (255*3)/2) + bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + + // Update black/white plane (first half of buffer) + if (is_white) { + // White pixel - clear bit in black plane + this->buffer_[pos] &= ~bit; + } else { + // Black pixel - set bit in black plane + this->buffer_[pos] |= bit; + } + + // Update red plane (second half of buffer) + // Red if red component is dominant (r > g+b) + if (color.r > color.g + color.b) { + // Red pixel - set bit in red plane + this->buffer_[red_offset + pos] |= bit; + } else { + // Not red - clear bit in red plane + this->buffer_[red_offset + pos] &= ~bit; + } +} + +void EPaperWeAct3C::fill(Color color) { + // For 3-color e-paper with 1-bit buffer format: + // - Black buffer: 1=black, 0=white + // - Red buffer: 1=red, 0=no red + // The buffer is stored as two halves: [black plane][red plane] + const size_t half_buffer = this->buffer_length_ / 2u; + + // Use luminance threshold for B/W mapping + bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + bool is_red = color.r > color.g + color.b; + + // Fill both planes + if (is_white) { + // White - both planes = 0x00 + this->buffer_.fill(0x00); + } else if (is_red) { + // Red - black plane = 0x00, red plane = 0xFF + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[i] = 0x00; + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[half_buffer + i] = 0xFF; + } else { + // Black - black plane = 0xFF, red plane = 0x00 + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[i] = 0xFF; + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[half_buffer + i] = 0x00; + } +} + +void EPaperWeAct3C::clear() { + // Clear buffer to white, just like real paper. + this->fill(COLOR_ON); +} + +void EPaperWeAct3C::set_window_() { + // For full screen refresh, we always start from (0,0) + // The y_low_/y_high_ values track the dirty region for optimization, + // but for display refresh we need to write from the beginning + uint16_t x_start = 0; + uint16_t x_end = this->width_ - 1; + uint16_t y_start = 0; + uint16_t y_end = this->height_ - 1; // height = 296 for 2.9" display + + // Set RAM X address boundaries (0x44) + // X coordinates are byte-aligned (divided by 8) + this->cmd_data(0x44, {(uint8_t) (x_start / 8), (uint8_t) (x_end / 8)}); + + // Set RAM Y address boundaries (0x45) + // Format: Y start (LSB, MSB), Y end (LSB, MSB) + this->cmd_data(0x45, {(uint8_t) y_start, (uint8_t) (y_start >> 8), (uint8_t) (y_end & 0xFF), (uint8_t) (y_end >> 8)}); + + // Reset RAM X counter to start (0x4E) - 1 byte + this->cmd_data(0x4E, {(uint8_t) (x_start / 8)}); + + // Reset RAM Y counter to start (0x4F) - 2 bytes (LSB, MSB) + this->cmd_data(0x4F, {(uint8_t) y_start, (uint8_t) (y_start >> 8)}); +} + +bool HOT EPaperWeAct3C::transfer_data() { + const uint32_t start_time = millis(); + const size_t buffer_length = this->buffer_length_; + const size_t half_buffer = buffer_length / 2u; + + ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer); + + // Use a local buffer for SPI transfers + static constexpr size_t MAX_TRANSFER_SIZE = 128; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + + // First, send the RED buffer (0x26 = WRITE_COLOR) + // The red plane is in the second half of our buffer + // NOTE: Must set RAM window first to reset address counters! + if (this->current_data_index_ < half_buffer) { + if (this->current_data_index_ == 0) { + ESP_LOGV(TAG, "transfer_data: sending RED buffer (0x26)"); + this->set_window_(); // Reset RAM X/Y counters to start position + this->command(0x26); + } + + this->start_data_(); + size_t red_offset = half_buffer; + while (this->current_data_index_ < half_buffer) { + size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, half_buffer - this->current_data_index_); + + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = this->buffer_[red_offset + this->current_data_index_ + i]; + } + + this->write_array(bytes_to_send, bytes_to_copy); + + this->current_data_index_ += bytes_to_copy; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + this->disable(); + } + + // Finished the red buffer, now send the BLACK buffer (0x24 = WRITE_BLACK) + // The black plane is in the first half of our buffer + if (this->current_data_index_ < buffer_length) { + if (this->current_data_index_ == half_buffer) { + ESP_LOGV(TAG, "transfer_data: finished red buffer, sending BLACK buffer (0x24)"); + + // Do NOT reset RAM counters here for WeAct displays (Reference implementation behavior) + // this->set_window(); + this->command(0x24); + // Continue using current_data_index_, but we need to map it to the start of the buffer + } + + this->start_data_(); + while (this->current_data_index_ < buffer_length) { + size_t remaining = buffer_length - this->current_data_index_; + size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, remaining); + + // Calculate offset into the BLACK buffer (which is at the start of this->buffer_) + // current_data_index_ goes from half_buffer to buffer_length + size_t buffer_offset = this->current_data_index_ - half_buffer; + + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = this->buffer_[buffer_offset + i]; + } + + this->write_array(bytes_to_send, bytes_to_copy); + + this->current_data_index_ += bytes_to_copy; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + this->disable(); + } + + this->current_data_index_ = 0; + ESP_LOGV(TAG, "transfer_data: completed (red=%u, black=%u bytes)", half_buffer, half_buffer); + return true; +} + +void EPaperWeAct3C::refresh_screen(bool partial) { + // SSD1680 refresh sequence: + // Reset RAM X/Y address counters to 0,0 so display reads from start + // 0x4E: RAM X counter - 1 byte (X / 8) + // 0x4F: RAM Y counter - 2 bytes (Y LSB, Y MSB) + this->cmd_data(0x4E, {0x00}); // RAM X counter = 0 (1 byte) + this->cmd_data(0x4F, {0x00, 0x00}); // RAM Y counter = 0 (2 bytes) + + // Send UPDATE_FULL command (0x22) with display update control parameter + // Both WeAct and waveshare reference use 0xF7: {0x22, 0xF7} + // 0xF7 = Display update: Load temperature, Load LUT, Enable RAM content + this->cmd_data(0x22, {0xF7}); // Command 0x22 with parameter 0xF7 + this->command(0x20); // Activate display update + + // COMMAND TERMINATE FRAME READ WRITE (required by SSD1680) + // Removed 0xFF based on working reference implementation + // this->command(0xFF); +} + +void EPaperWeAct3C::power_on() { + // Power on sequence - send command to turn on power + // According to SSD1680 spec: 0x22, 0xF8 powers on the display + this->cmd_data(0x22, {0xF8}); // Power on + this->command(0x20); // Activate +} + +void EPaperWeAct3C::power_off() { + // Power off sequence - send command to turn off power + // According to SSD1680 spec: 0x22, 0x83 powers off the display + this->cmd_data(0x22, {0x83}); // Power off + this->command(0x20); // Activate +} + +void EPaperWeAct3C::deep_sleep() { + // Deep sleep sequence + this->cmd_data(0x10, {0x01}); // Deep sleep mode +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_weact_3c.h b/esphome/components/epaper_spi/epaper_weact_3c.h new file mode 100644 index 0000000000..2df6f1ba09 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_weact_3c.h @@ -0,0 +1,39 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * WeAct 3-color e-paper displays (SSD1683 controller). + * Supports multiple sizes: 2.9" (128x296), 4.2" (400x300), etc. + * + * Color scheme: Black, White, Red (BWR) + * Buffer layout: 1 bit per pixel, separate planes + * - Buffer first half: Black/White plane (1=black, 0=white) + * - Buffer second half: Red plane (1=red, 0=no red) + * - Total buffer: width * height / 4 bytes (2 * width * height / 8) + */ +class EPaperWeAct3C : public EPaperBase { + public: + EPaperWeAct3C(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) { + this->buffer_length_ = this->row_width_ * height * 2; + } + + void fill(Color color) override; + void clear() override; + + protected: + void set_window_(); + void refresh_screen(bool partial) override; + void power_on() override; + void power_off() override; + void deep_sleep() override; + void draw_pixel_at(int x, int y, Color color) override; + + bool transfer_data() override; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/jd79660.py b/esphome/components/epaper_spi/models/jd79660.py index 2d8830ebd2..a0457c5812 100644 --- a/esphome/components/epaper_spi/models/jd79660.py +++ b/esphome/components/epaper_spi/models/jd79660.py @@ -84,3 +84,35 @@ jd79660.extend( (0xA5, 0x00,), ), ) + +# Waveshare 7.5-H +# +# Vendor init derived from vendor sample code +# +# Compatible MIT license, see esphome/LICENSE file. +# +# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML. +# +# fmt: off +jd79660.extend( + "Waveshare-7.5in-H", + width=800, + height=480, + + initsequence=( + (0x00, 0x0F, 0x29,), + (0x06, 0x0F, 0x8B, 0x93, 0xA1,), + (0x41, 0x00,), + (0x50, 0x37,), + (0x60, 0x02, 0x02,), + (0x61, 800 // 256, 800 % 256, 480 // 256, 480 % 256,), # RES: 800x480 + (0x62, 0x98, 0x98, 0x98, 0x75, 0xCA, 0xB2, 0x98, 0x7E,), + (0x65, 0x00, 0x00, 0x00, 0x00,), + (0xE7, 0x1C,), + (0xE3, 0x00,), + (0xE9, 0x01,), + (0x30, 0x08,), + # Power On (0x04): Must be early part of init seq = Disabled later! + (0x04,), + ), +) diff --git a/esphome/components/epaper_spi/models/weact_bwr.py b/esphome/components/epaper_spi/models/weact_bwr.py new file mode 100644 index 0000000000..21a015b08d --- /dev/null +++ b/esphome/components/epaper_spi/models/weact_bwr.py @@ -0,0 +1,75 @@ +"""WeAct Black/White/Red e-paper displays using SSD1683 controller. + +Supported models: +- weact-2.13in-3c: 122x250 pixels (2.13" display) +- weact-2.9in-3c: 128x296 pixels (2.9" display) +- weact-4.2in-3c: 400x300 pixels (4.2" display) + +These displays use SSD1680 or SSD1683 controller and require a specific initialization +sequence. The DRV_OUT_CTL command is calculated from the display height. +""" + +from . import EpaperModel + + +class WeActBWR(EpaperModel): + """Base EpaperModel class for WeAct Black/White/Red displays using SSD1683 controller.""" + + def __init__(self, name, **defaults): + super().__init__(name, "EPaperWeAct3C", **defaults) + + def get_init_sequence(self, config): + """Generate initialization sequence for WeAct BWR displays. + + The initialization sequence is based on SSD1680 and SSD1683 controller datasheet + and the WeAct display specifications. + """ + _, height = self.get_dimensions(config) + # DRV_OUT_CTL: MSB of (height-1), LSB of (height-1), gate setting (0x00) + height_minus_1 = height - 1 + msb = height_minus_1 >> 8 + lsb = height_minus_1 & 0xFF + return ( + # Step 1: Software Reset (0x12) - REQUIRED per SSD1680, but works without it as well, so it's commented out for now + # (0x12,), + # Step 2: Wait 10ms after SWRESET (?) not sure how to implement wht waiting for 10ms after SWRESET, so it's commented out for now + # Step 3: DRV_OUT_CTL - driver output control (height-dependent) + # Format: (command, LSB, MSB, gate setting) + (0x01, lsb, msb, 0x00), + # Step 4: DATA_ENTRY - data entry mode (0x03 = decrement Y, increment X) + (0x11, 0x03), + # Step 5: BORDER_FULL - border waveform control + (0x3C, 0x05), + # Step 6: TEMP_SENS - internal temperature sensor + (0x18, 0x80), + # Step 7: DISPLAY_UPDATE - display update control + (0x21, 0x00, 0x80), + ) + + +# Model: WeAct 2.9" 3C - 128x296 pixels, SSD1680 controller +weact_2p9in3c = WeActBWR( + "weact-2.9in-3c", + width=128, + height=296, + data_rate="10MHz", + minimum_update_interval="1s", +) + +# Model: WeAct 2.13" 3C - 122x250 pixels, SSD1680 controller +weact_2p13in3c = WeActBWR( + "weact-2.13in-3c", + width=122, + height=250, + data_rate="10MHz", + minimum_update_interval="1s", +) + +# Model: WeAct 4.2" 3C - 400x300 pixels, SSD1683 controller +weact_4p2in3c = WeActBWR( + "weact-4.2in-3c", + width=400, + height=300, + data_rate="10MHz", + minimum_update_interval="10s", +) diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 272c7c81ba..1076bcabdc 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,8 +1,30 @@ from esphome.components import esp32 import esphome.config_validation as cv +from esphome.core import CORE CODEOWNERS = ["@jesserockz"] +VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61} + + +def validate_rmt_not_supported(rmt_only_keys): + """Validate that RMT-only config keys are not used on variants without RMT hardware.""" + rmt_only_keys = set(rmt_only_keys) + + def _validator(config): + if CORE.is_esp32: + variant = esp32.get_esp32_variant() + if variant in VARIANTS_NO_RMT: + unsupported = rmt_only_keys.intersection(config) + if unsupported: + keys = ", ".join(sorted(f"'{k}'" for k in unsupported)) + raise cv.Invalid( + f"{keys} not available on {variant} (no RMT hardware)" + ) + return config + + return _validator + def validate_clock_resolution(): def _validator(value): diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 6d41f6b5b8..1c6943b003 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,7 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32, light +from esphome.components import esp32, esp32_rmt, light from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv @@ -71,6 +71,10 @@ CONF_RESET_LOW = "reset_low" CONFIG_SCHEMA = cv.All( + esp32.only_on_variant( + unsupported=list(esp32_rmt.VARIANTS_NO_RMT), + msg_prefix="ESP32 RMT LED strip", + ), light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 09ec395841..d744263c76 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -221,12 +221,17 @@ void Fan::publish_state() { } // Random 32-bit value, change this every time the layout of the FanRestoreState struct changes. -constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; +constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB; optional Fan::restore_state_() { FanRestoreState recovered{}; this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); + if (!restored) { + // No valid saved data; ensure preset_mode sentinel is set + recovered.preset_mode = FanRestoreState::NO_PRESET; + } + switch (this->restore_mode_) { case FanRestoreMode::NO_RESTORE: return {}; @@ -264,6 +269,7 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + state.preset_mode = FanRestoreState::NO_PRESET; if (this->has_preset_mode()) { const auto &preset_modes = traits.supported_preset_modes(); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 55d4ba8825..2caf3a712a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -91,11 +91,13 @@ class FanCall { }; struct FanRestoreState { + static constexpr uint8_t NO_PRESET = UINT8_MAX; + bool state; int speed; bool oscillating; FanDirection direction; - uint8_t preset_mode; + uint8_t preset_mode{NO_PRESET}; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 9bf58f9d1e..38e4129e66 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -28,15 +28,15 @@ fan::FanCall HBridgeFan::brake() { } void HBridgeFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void HBridgeFan::dump_config() { diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 0cac28506f..d73fff2b0a 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -119,6 +119,8 @@ class RemoteComponentBase { }; #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED class RemoteRMTChannel { public: void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; } @@ -137,7 +139,8 @@ class RemoteRMTChannel { uint32_t clock_resolution_{1000000}; uint32_t rmt_symbols_; }; -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 class RemoteTransmitterBase : public RemoteComponentBase { public: diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index b3dc213c5f..362f6e99db 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -65,6 +65,8 @@ RemoteReceiverComponent = remote_receiver_ns.class_( def validate_config(config): if CORE.is_esp32: variant = esp32.get_esp32_variant() + if variant in esp32_rmt.VARIANTS_NO_RMT: + return config if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2): max_idle = 65535 else: @@ -110,6 +112,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_BUFFER_SIZE, esp32="10000b", + esp32_c2="1000b", + esp32_c61="1000b", esp8266="1000b", bk72xx="1000b", ln882x="1000b", @@ -131,9 +135,11 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RMT_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, esp32_c3=96, esp32_c5=96, esp32_c6=96, + esp32_c61=cv.UNDEFINED, esp32_h2=96, esp32_p4=192, esp32_s2=192, @@ -145,6 +151,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RECEIVE_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( @@ -152,24 +160,45 @@ CONFIG_SCHEMA = remote_base.validate_triggers( ), cv.boolean, ), - cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All( + cv.SplitDefault( + CONF_CARRIER_DUTY_PERCENT, + esp32=100, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All( cv.only_on_esp32, cv.percentage_int, cv.Range(min=1, max=100), ), - cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All( - cv.only_on_esp32, cv.frequency, cv.int_ - ), + cv.SplitDefault( + CONF_CARRIER_FREQUENCY, + esp32="0Hz", + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All(cv.only_on_esp32, cv.frequency, cv.int_), } ) .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_FILTER_SYMBOLS, + CONF_RECEIVE_SYMBOLS, + CONF_CARRIER_DUTY_PERCENT, + CONF_CARRIER_FREQUENCY, + ] + ) + ) .add_extra(validate_config) ) async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -213,6 +242,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_receiver.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index de47457dac..d59ee63695 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -3,7 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) namespace esphome::remote_receiver { diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 3d9199a904..5da9283a6e 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -6,12 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 namespace esphome::remote_receiver { -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); @@ -35,7 +38,7 @@ struct RemoteReceiverComponentStore { volatile bool prev_level{false}; volatile bool overflow{false}; }; -#elif defined(USE_ESP32) +#elif defined(USE_ESP32) && SOC_RMT_SUPPORTED struct RemoteReceiverComponentStore { /// Stores RMT symbols and rx done event data volatile uint8_t *buffer{nullptr}; @@ -54,7 +57,7 @@ struct RemoteReceiverComponentStore { class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -66,7 +69,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void dump_config() override; void loop() override; -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } @@ -78,7 +81,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } protected: -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); rmt_channel_handle_t channel_{NULL}; uint32_t filter_symbols_{0}; @@ -94,7 +97,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, RemoteReceiverComponentStore store_; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index f95244ea45..357a36d052 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include #include @@ -248,4 +250,5 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c } // namespace esphome::remote_receiver -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 8383b9dd75..fc772f88b2 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -40,45 +40,66 @@ DigitalWriteAction = remote_transmitter_ns.class_( cg.Parented.template(RemoteTransmitterComponent), ) + MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( - cv.percentage_int, cv.Range(min=1, max=100) - ), - cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( - cv.only_on_esp32, - esp32_rmt.validate_clock_resolution(), - ), - cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_USE_DMA): cv.All( - esp32.only_on_variant( - supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( + cv.percentage_int, cv.Range(min=1, max=100) ), - cv.boolean, - ), - cv.SplitDefault( - CONF_RMT_SYMBOLS, - esp32=64, - esp32_c3=48, - esp32_c5=48, - esp32_c6=48, - esp32_h2=48, - esp32_p4=48, - esp32_s2=64, - esp32_s3=48, - ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), - cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), - cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), - } -).extend(cv.COMPONENT_SCHEMA) + cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( + cv.only_on_esp32, + esp32_rmt.validate_clock_resolution(), + ), + cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_USE_DMA): cv.All( + esp32.only_on_variant( + supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] + ), + cv.boolean, + ), + cv.SplitDefault( + CONF_RMT_SYMBOLS, + esp32=64, + esp32_c2=cv.UNDEFINED, + esp32_c3=48, + esp32_c5=48, + esp32_c6=48, + esp32_c61=cv.UNDEFINED, + esp32_h2=48, + esp32_p4=48, + esp32_s2=64, + esp32_s3=48, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), + cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_EOT_LEVEL, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_NON_BLOCKING, + ] + ) + ) +) def _validate_non_blocking(config): - if CORE.is_esp32 and CONF_NON_BLOCKING not in config: + if ( + CORE.is_esp32 + and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT + and CONF_NON_BLOCKING not in config + ): _LOGGER.warning( "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" "The default behavior changed in 2025.11.0; previously blocking mode was used.\n" @@ -111,7 +132,7 @@ async def digital_write_action_to_code(config, action_id, template_arg, args): async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -155,6 +176,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_transmitter.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_transmitter/automation.h b/esphome/components/remote_transmitter/automation.h index bee1d0be8a..8da4cfd95d 100644 --- a/esphome/components/remote_transmitter/automation.h +++ b/esphome/components/remote_transmitter/automation.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { template class DigitalWriteAction : public Action, public Parented { public: @@ -14,5 +13,4 @@ template class DigitalWriteAction : public Action, public void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); } }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index d35541e2e1..51a3c0b1d4 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,10 +2,9 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -105,7 +104,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->complete_trigger_.trigger(); } -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter #endif diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 65bd2ac8b2..aee52ea170 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -6,13 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) // IDF version 5.5.1 and above is required because of a bug in // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 @@ -33,7 +35,7 @@ struct RemoteTransmitterComponentStore { class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -51,7 +53,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void digital_write(bool value); -#if defined(USE_ESP32) +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } @@ -62,7 +64,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); @@ -73,7 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, uint32_t target_time_; #endif -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void configure_rmt_(); void wait_for_rmt_(); @@ -100,5 +102,4 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, Trigger<> complete_trigger_; }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 89d97895b2..71773e3ddf 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -3,10 +3,11 @@ #include "esphome/core/application.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -358,7 +359,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } #endif -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index af98e3a51f..55f7fd162c 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -7,15 +7,15 @@ namespace speed { static const char *const TAG = "speed.fan"; void SpeedFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 0e1920a984..cd267bd552 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -6,15 +6,15 @@ namespace esphome::template_ { static const char *const TAG = "template.fan"; void TemplateFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = + fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); } - - // Construct traits - this->traits_ = - fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml index 0e5d512d08..5d46419399 100644 --- a/tests/components/combination/common.yaml +++ b/tests/components/combination/common.yaml @@ -27,9 +27,9 @@ sensor: name: Linearly combined temperatures sources: - source: template_temperature1 - coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + coefficient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" - source: template_temperature2 - coeffecient: 1.5 + coefficient: 1.5 - platform: combination type: max name: Max of combined temperatures diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 333ab567cd..9593d0f6f0 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -57,6 +57,23 @@ display: allow_other_uses: true number: GPIO4 + - platform: epaper_spi + spi_id: spi_bus + model: waveshare-7.5in-H + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + inverted: true + - platform: epaper_spi model: seeed-reterminal-e1002 - platform: epaper_spi @@ -64,3 +81,66 @@ display: # Override pins to avoid conflict with other display configs busy_pin: 43 dc_pin: 42 + + # WeAct 2.13" 3-color e-paper (122x250, SSD1680) + - platform: epaper_spi + spi_id: spi_bus + model: weact-2.13in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0)); + + # WeAct 2.9" 3-color e-paper (128x296, SSD1683) + - platform: epaper_spi + spi_id: spi_bus + model: weact-2.9in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0)); + + # WeAct 4.2" 3-color e-paper (400x300, SSD1683) + - platform: epaper_spi + spi_id: spi_bus + model: weact-4.2in-3c + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK); + it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color(255, 0, 0)); diff --git a/tests/components/remote_receiver/test.esp32-c2-idf.yaml b/tests/components/remote_receiver/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..87154e19fc --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-c2-idf.yaml @@ -0,0 +1,12 @@ +remote_receiver: + id: rcvr + pin: GPIO2 + dump: all + <<: !include common-actions.yaml + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_transmitter/test.esp32-c2-idf.yaml b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..424cd8d249 --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: xmitr + pin: GPIO2 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml