From 19fa76873070356fda4b277bfee7b3db152e9b49 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Sat, 6 Dec 2025 02:48:04 +1300
Subject: [PATCH 01/12] Update readme logo (#12294)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 0439b1bc06..b8ce8d091d 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
-
-
+
+
From 7fd79fdded14b48e9a6cc2b3bba2191a749972f1 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Sat, 6 Dec 2025 03:53:08 +1300
Subject: [PATCH 02/12] [esp32] Change imports to use esp32 only, not .const
(#12243)
---
esphome/components/adc/__init__.py | 5 +++--
esphome/components/bme680_bsec/__init__.py | 2 +-
esphome/components/deep_sleep/__init__.py | 4 ++--
esphome/components/esp32_can/canbus.py | 4 ++--
esphome/components/esp32_dac/output.py | 3 +--
esphome/components/esp32_hosted/update/__init__.py | 4 ++--
esphome/components/esp32_rmt/__init__.py | 2 +-
esphome/components/esp32_rmt_led_strip/light.py | 2 +-
esphome/components/esp32_touch/__init__.py | 11 ++++++-----
esphome/components/ethernet/__init__.py | 8 +++-----
esphome/components/i2s_audio/__init__.py | 5 +++--
esphome/components/i2s_audio/media_player/__init__.py | 2 +-
esphome/components/i2s_audio/microphone/__init__.py | 4 ++--
esphome/components/i2s_audio/speaker/__init__.py | 2 +-
esphome/components/improv_serial/__init__.py | 3 +--
esphome/components/logger/__init__.py | 5 +++--
esphome/components/mipi_dsi/display.py | 4 ++--
esphome/components/mipi_rgb/display.py | 4 ++--
esphome/components/neopixelbus/_methods.py | 4 ++--
esphome/components/neopixelbus/light.py | 3 +--
esphome/components/psram/__init__.py | 6 ++----
esphome/components/remote_receiver/__init__.py | 4 ++--
esphome/components/remote_transmitter/__init__.py | 2 +-
esphome/components/rpi_dpi_rgb/display.py | 4 ++--
esphome/components/spi/__init__.py | 4 ++--
esphome/components/st7701s/display.py | 4 ++--
esphome/components/tinyusb/__init__.py | 5 +++--
esphome/config_validation.py | 3 +--
tests/component_tests/esp32/test_esp32.py | 3 +--
tests/component_tests/psram/test_psram.py | 2 +-
tests/unit_tests/test_config_validation.py | 4 ++--
tests/unit_tests/test_main.py | 2 +-
32 files changed, 60 insertions(+), 64 deletions(-)
diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py
index 8f751c496e..62c1a5fffa 100644
--- a/esphome/components/adc/__init__.py
+++ b/esphome/components/adc/__init__.py
@@ -1,15 +1,16 @@
from esphome import pins
import esphome.codegen as cg
-from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
+ VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py
index 8a8d74b5f3..06e641d34d 100644
--- a/esphome/components/bme680_bsec/__init__.py
+++ b/esphome/components/bme680_bsec/__init__.py
@@ -69,7 +69,7 @@ CONFIG_SCHEMA = cv.All(
cv.only_on_esp8266,
cv.All(
cv.only_on_esp32,
- esp32.only_on_variant(supported=[esp32.const.VARIANT_ESP32]),
+ esp32.only_on_variant(supported=[esp32.VARIANT_ESP32]),
),
),
)
diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py
index 18ba167952..75affd7843 100644
--- a/esphome/components/deep_sleep/__init__.py
+++ b/esphome/components/deep_sleep/__init__.py
@@ -1,8 +1,7 @@
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import esp32, time
-from esphome.components.esp32 import get_esp32_variant
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
@@ -10,6 +9,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32H2,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ get_esp32_variant,
)
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py
index 5cee27506a..8708c6fb36 100644
--- a/esphome/components/esp32_can/canbus.py
+++ b/esphome/components/esp32_can/canbus.py
@@ -4,8 +4,7 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import canbus
from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed
-from esphome.components.esp32 import get_esp32_variant
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
@@ -13,6 +12,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import (
diff --git a/esphome/components/esp32_dac/output.py b/esphome/components/esp32_dac/output.py
index cf4f12c46d..daace596d3 100644
--- a/esphome/components/esp32_dac/output.py
+++ b/esphome/components/esp32_dac/output.py
@@ -1,8 +1,7 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import output
-from esphome.components.esp32 import get_esp32_variant
-from esphome.components.esp32.const import VARIANT_ESP32, VARIANT_ESP32S2
+from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN
diff --git a/esphome/components/esp32_hosted/update/__init__.py b/esphome/components/esp32_hosted/update/__init__.py
index 040f989a64..fff0d3623a 100644
--- a/esphome/components/esp32_hosted/update/__init__.py
+++ b/esphome/components/esp32_hosted/update/__init__.py
@@ -40,8 +40,8 @@ CONFIG_SCHEMA = cv.All(
),
esp32.only_on_variant(
supported=[
- esp32.const.VARIANT_ESP32H2,
- esp32.const.VARIANT_ESP32P4,
+ esp32.VARIANT_ESP32H2,
+ esp32.VARIANT_ESP32P4,
]
),
)
diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py
index 1e72185e3e..272c7c81ba 100644
--- a/esphome/components/esp32_rmt/__init__.py
+++ b/esphome/components/esp32_rmt/__init__.py
@@ -9,7 +9,7 @@ def validate_clock_resolution():
cv.only_on_esp32(value)
value = cv.int_(value)
variant = esp32.get_esp32_variant()
- if variant == esp32.const.VARIANT_ESP32H2 and value > 32000000:
+ if variant == esp32.VARIANT_ESP32H2 and value > 32000000:
raise cv.Invalid(
f"ESP32 variant {variant} has a max clock_resolution of 32000000."
)
diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py
index 2ec0750ae6..f020d02e86 100644
--- a/esphome/components/esp32_rmt_led_strip/light.py
+++ b/esphome/components/esp32_rmt_led_strip/light.py
@@ -91,7 +91,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_IS_WRGB, default=False): cv.boolean,
cv.Optional(CONF_USE_DMA): cv.All(
esp32.only_on_variant(
- supported=[esp32.const.VARIANT_ESP32P4, esp32.const.VARIANT_ESP32S3]
+ supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3]
),
cv.boolean,
),
diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py
index b6cb19ebb1..c54ed8b9ea 100644
--- a/esphome/components/esp32_touch/__init__.py
+++ b/esphome/components/esp32_touch/__init__.py
@@ -1,10 +1,11 @@
import esphome.codegen as cg
from esphome.components import esp32
-from esphome.components.esp32 import get_esp32_variant, gpio
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ get_esp32_variant,
+ gpio,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -255,9 +256,9 @@ CONFIG_SCHEMA = cv.All(
cv.has_none_or_all_keys(CONF_WATERPROOF_GUARD_RING, CONF_WATERPROOF_SHIELD_DRIVER),
esp32.only_on_variant(
supported=[
- esp32.const.VARIANT_ESP32,
- esp32.const.VARIANT_ESP32S2,
- esp32.const.VARIANT_ESP32S3,
+ esp32.VARIANT_ESP32,
+ esp32.VARIANT_ESP32S2,
+ esp32.VARIANT_ESP32S3,
]
),
validate_variant_vars,
diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py
index b4d67635c1..39af1ff4b9 100644
--- a/esphome/components/ethernet/__init__.py
+++ b/esphome/components/ethernet/__init__.py
@@ -3,16 +3,14 @@ import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import (
- add_idf_component,
- add_idf_sdkconfig_option,
- get_esp32_variant,
-)
-from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ add_idf_component,
+ add_idf_sdkconfig_option,
+ get_esp32_variant,
)
from esphome.components.network import ip_address_literal
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py
index 0c7c8f6642..802db06f48 100644
--- a/esphome/components/i2s_audio/__init__.py
+++ b/esphome/components/i2s_audio/__init__.py
@@ -1,7 +1,6 @@
from esphome import pins
import esphome.codegen as cg
-from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
@@ -10,6 +9,8 @@ from esphome.components.esp32.const import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ add_idf_sdkconfig_option,
+ get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py
index 316ce7c48b..35c42e1b06 100644
--- a/esphome/components/i2s_audio/media_player/__init__.py
+++ b/esphome/components/i2s_audio/media_player/__init__.py
@@ -40,7 +40,7 @@ INTERNAL_DAC_OPTIONS = {
EXTERNAL_DAC_OPTIONS = [CONF_MONO, CONF_STEREO]
-NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
+NO_INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32S2]
I2C_COMM_FMT_OPTIONS = ["lsb", "msb"]
diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py
index f919199c60..dd23673db5 100644
--- a/esphome/components/i2s_audio/microphone/__init__.py
+++ b/esphome/components/i2s_audio/microphone/__init__.py
@@ -37,8 +37,8 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
)
-INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32]
-PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3]
+INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32]
+PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3]
def _validate_esp32_variant(config):
diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py
index 98322d3a18..2e009a1de1 100644
--- a/esphome/components/i2s_audio/speaker/__init__.py
+++ b/esphome/components/i2s_audio/speaker/__init__.py
@@ -62,7 +62,7 @@ I2C_COMM_FMT_OPTIONS = {
"pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG,
}
-INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32]
+INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32]
def _set_num_channels_from_config(config):
diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py
index fb2b541707..7f88b17e11 100644
--- a/esphome/components/improv_serial/__init__.py
+++ b/esphome/components/improv_serial/__init__.py
@@ -1,7 +1,6 @@
import esphome.codegen as cg
from esphome.components import improv_base
-from esphome.components.esp32 import get_esp32_variant
-from esphome.components.esp32.const import VARIANT_ESP32S3
+from esphome.components.esp32 import VARIANT_ESP32S3, get_esp32_variant
from esphome.components.logger import USB_CDC
import esphome.config_validation as cv
from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER
diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py
index c81ade8fc3..7369e99c85 100644
--- a/esphome/components/logger/__init__.py
+++ b/esphome/components/logger/__init__.py
@@ -3,8 +3,7 @@ import re
from esphome import automation
from esphome.automation import LambdaAction, StatelessLambdaAction
import esphome.codegen as cg
-from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
@@ -14,6 +13,8 @@ from esphome.components.esp32.const import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ add_idf_sdkconfig_option,
+ get_esp32_variant,
)
from esphome.components.libretiny import get_libretiny_component, get_libretiny_family
from esphome.components.libretiny.const import (
diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py
index 4fc837be67..90c4cc082e 100644
--- a/esphome/components/mipi_dsi/display.py
+++ b/esphome/components/mipi_dsi/display.py
@@ -12,7 +12,7 @@ from esphome.components.const import (
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD
-from esphome.components.esp32 import const, only_on_variant
+from esphome.components.esp32 import VARIANT_ESP32P4, only_on_variant
from esphome.components.mipi import (
COLOR_ORDERS,
CONF_COLOR_DEPTH,
@@ -165,7 +165,7 @@ def model_schema(config):
)
return cv.All(
schema,
- only_on_variant(supported=[const.VARIANT_ESP32P4]),
+ only_on_variant(supported=[VARIANT_ESP32P4]),
cv.only_with_esp_idf,
)
diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py
index 9d6b1fa729..2d2e022045 100644
--- a/esphome/components/mipi_rgb/display.py
+++ b/esphome/components/mipi_rgb/display.py
@@ -11,7 +11,7 @@ from esphome.components.const import (
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD
-from esphome.components.esp32 import const, only_on_variant
+from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant
from esphome.components.mipi import (
COLOR_ORDERS,
CONF_DE_PIN,
@@ -224,7 +224,7 @@ def _config_schema(config):
schema = model_schema(config)
return cv.All(
schema,
- only_on_variant(supported=[const.VARIANT_ESP32S3]),
+ only_on_variant(supported=[VARIANT_ESP32S3]),
cv.only_with_esp_idf,
)(config)
diff --git a/esphome/components/neopixelbus/_methods.py b/esphome/components/neopixelbus/_methods.py
index 5a00fa2804..9072f78035 100644
--- a/esphome/components/neopixelbus/_methods.py
+++ b/esphome/components/neopixelbus/_methods.py
@@ -2,12 +2,12 @@ from dataclasses import dataclass
from typing import Any
import esphome.codegen as cg
-from esphome.components.esp32 import get_esp32_variant
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import (
diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py
index 0c9604e932..d071059185 100644
--- a/esphome/components/neopixelbus/light.py
+++ b/esphome/components/neopixelbus/light.py
@@ -1,8 +1,7 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import light
-from esphome.components.esp32 import get_esp32_variant
-from esphome.components.esp32.const import VARIANT_ESP32C3, VARIANT_ESP32S3
+from esphome.components.esp32 import VARIANT_ESP32C3, VARIANT_ESP32S3, get_esp32_variant
import esphome.config_validation as cv
from esphome.const import (
CONF_CHANNEL,
diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py
index 529097889d..fcbe9ed043 100644
--- a/esphome/components/psram/__init__.py
+++ b/esphome/components/psram/__init__.py
@@ -7,14 +7,12 @@ from esphome.components.esp32 import (
CONF_CPU_FREQUENCY,
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES,
VARIANT_ESP32,
- add_idf_sdkconfig_option,
- get_esp32_variant,
-)
-from esphome.components.esp32.const import (
VARIANT_ESP32C5,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ add_idf_sdkconfig_option,
+ get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import (
diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py
index 7f70e2c2a2..f5d89f2f0f 100644
--- a/esphome/components/remote_receiver/__init__.py
+++ b/esphome/components/remote_receiver/__init__.py
@@ -65,7 +65,7 @@ RemoteReceiverComponent = remote_receiver_ns.class_(
def validate_config(config):
if CORE.is_esp32:
variant = esp32.get_esp32_variant()
- if variant in (esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S2):
+ if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2):
max_idle = 65535
else:
max_idle = 32767
@@ -148,7 +148,7 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
): cv.All(cv.only_on_esp32, cv.int_range(min=2)),
cv.Optional(CONF_USE_DMA): cv.All(
esp32.only_on_variant(
- supported=[esp32.const.VARIANT_ESP32P4, esp32.const.VARIANT_ESP32S3]
+ supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3]
),
cv.boolean,
),
diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py
index ec4f62666d..f182a1ec0d 100644
--- a/esphome/components/remote_transmitter/__init__.py
+++ b/esphome/components/remote_transmitter/__init__.py
@@ -55,7 +55,7 @@ CONFIG_SCHEMA = cv.Schema(
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.const.VARIANT_ESP32P4, esp32.const.VARIANT_ESP32S3]
+ supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3]
),
cv.boolean,
),
diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py
index 513ed8eb58..8e9da43a74 100644
--- a/esphome/components/rpi_dpi_rgb/display.py
+++ b/esphome/components/rpi_dpi_rgb/display.py
@@ -1,7 +1,7 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import display
-from esphome.components.esp32 import const, only_on_variant
+from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant
from esphome.components.mipi import (
CONF_DE_PIN,
CONF_HSYNC_BACK_PORCH,
@@ -121,7 +121,7 @@ CONFIG_SCHEMA = cv.All(
}
)
),
- only_on_variant(supported=[const.VARIANT_ESP32S3]),
+ only_on_variant(supported=[VARIANT_ESP32S3]),
cv.only_with_esp_idf,
)
diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py
index 8f23735fff..1530ffb882 100644
--- a/esphome/components/spi/__init__.py
+++ b/esphome/components/spi/__init__.py
@@ -3,8 +3,7 @@ from typing import Any
from esphome import pins
import esphome.codegen as cg
-from esphome.components.esp32 import only_on_variant
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
KEY_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
@@ -13,6 +12,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ only_on_variant,
)
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py
index 497740b8d2..6e4bff6431 100644
--- a/esphome/components/st7701s/display.py
+++ b/esphome/components/st7701s/display.py
@@ -1,7 +1,7 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import display, spi
-from esphome.components.esp32 import const, only_on_variant
+from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant
from esphome.components.mipi import (
CONF_DE_PIN,
CONF_HSYNC_BACK_PORCH,
@@ -161,7 +161,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(spi.spi_device_schema(cs_pin_required=False, default_data_rate=1e6))
),
- only_on_variant(supported=[const.VARIANT_ESP32S3]),
+ only_on_variant(supported=[VARIANT_ESP32S3]),
cv.only_with_esp_idf,
)
diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py
index 72afc18387..90043e969c 100644
--- a/esphome/components/tinyusb/__init__.py
+++ b/esphome/components/tinyusb/__init__.py
@@ -1,10 +1,11 @@
import esphome.codegen as cg
from esphome.components import esp32
-from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
+ add_idf_component,
+ add_idf_sdkconfig_option,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID
diff --git a/esphome/config_validation.py b/esphome/config_validation.py
index ee926b1b6d..c52b791120 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -1744,8 +1744,7 @@ class SplitDefault(Optional):
def default(self):
keys = []
if CORE.is_esp32:
- from esphome.components.esp32 import get_esp32_variant
- from esphome.components.esp32.const import VARIANT_ESP32
+ from esphome.components.esp32 import VARIANT_ESP32, get_esp32_variant
variant = get_esp32_variant().replace(VARIANT_ESP32, "").lower()
framework = CORE.target_framework.replace("esp-", "")
diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py
index 91e96f24d6..68bd3a5965 100644
--- a/tests/component_tests/esp32/test_esp32.py
+++ b/tests/component_tests/esp32/test_esp32.py
@@ -17,8 +17,7 @@ def test_esp32_config(
) -> None:
set_core_config(PlatformFramework.ESP32_IDF)
- from esphome.components.esp32 import CONFIG_SCHEMA
- from esphome.components.esp32.const import VARIANT_ESP32, VARIANT_FRIENDLY
+ from esphome.components.esp32 import CONFIG_SCHEMA, VARIANT_ESP32, VARIANT_FRIENDLY
# Example ESP32 configuration
config = {
diff --git a/tests/component_tests/psram/test_psram.py b/tests/component_tests/psram/test_psram.py
index 86bc29cc84..0924e66adc 100644
--- a/tests/component_tests/psram/test_psram.py
+++ b/tests/component_tests/psram/test_psram.py
@@ -4,7 +4,7 @@ from typing import Any
import pytest
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32C2,
diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py
index 73b15aaadf..c9d7b7486e 100644
--- a/tests/unit_tests/test_config_validation.py
+++ b/tests/unit_tests/test_config_validation.py
@@ -6,7 +6,7 @@ import pytest
import voluptuous as vol
from esphome import config_validation
-from esphome.components.esp32.const import (
+from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
@@ -221,7 +221,7 @@ def hex_int__valid(value):
],
)
def test_split_default(framework, platform, variant, full, idf, arduino, simple):
- from esphome.components.esp32.const import KEY_ESP32
+ from esphome.components.esp32 import KEY_ESP32
from esphome.const import (
KEY_CORE,
KEY_TARGET_FRAMEWORK,
diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py
index 670d6c16fc..bd14395037 100644
--- a/tests/unit_tests/test_main.py
+++ b/tests/unit_tests/test_main.py
@@ -35,7 +35,7 @@ from esphome.__main__ import (
upload_program,
upload_using_esptool,
)
-from esphome.components.esp32.const import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32
+from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32
from esphome.const import (
CONF_API,
CONF_BROKER,
From f4d1c9df714b0478557683153c6c142157ead80a Mon Sep 17 00:00:00 2001
From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Date: Fri, 5 Dec 2025 10:56:11 -0500
Subject: [PATCH 03/12] [remote_receiver] Fix Zephyr clang tidy (#12299)
Co-authored-by: Claude
---
esphome/components/remote_receiver/remote_receiver.h | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h
index fabf0a481a..3d9199a904 100644
--- a/esphome/components/remote_receiver/remote_receiver.h
+++ b/esphome/components/remote_receiver/remote_receiver.h
@@ -90,12 +90,14 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
std::string error_string_{""};
#endif
+#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || defined(USE_ESP32)
+ RemoteReceiverComponentStore store_;
+#endif
+
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040)
HighFrequencyLoopRequester high_freq_;
#endif
- RemoteReceiverComponentStore store_;
-
uint32_t buffer_size_{};
uint32_t filter_us_{10};
uint32_t idle_us_{10000};
From 27fcff2092293442459741483f723c5d4b0b3671 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Fri, 5 Dec 2025 10:27:41 -0600
Subject: [PATCH 04/12] [api] Simplify MessageCreator to trivially copyable
type (#12295)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
esphome/components/api/api_connection.cpp | 6 ++---
esphome/components/api/api_connection.h | 27 ++++-------------------
2 files changed, 7 insertions(+), 26 deletions(-)
diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index 9ad45dc6b7..31f90d9474 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -1662,13 +1662,13 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
for (auto &item : items) {
if (item.entity == entity && item.message_type == message_type) {
// Replace with new creator
- item.creator = std::move(creator);
+ item.creator = creator;
return;
}
}
// No existing item found, add new one
- items.emplace_back(entity, std::move(creator), message_type, estimated_size);
+ items.emplace_back(entity, creator, message_type, estimated_size);
}
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type,
@@ -1677,7 +1677,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCre
// This avoids expensive vector::insert which shifts all elements
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
- items.emplace_back(entity, std::move(creator), message_type, estimated_size);
+ items.emplace_back(entity, creator, message_type, estimated_size);
if (items.size() > 1) {
// Swap the new high-priority item to the front
std::swap(items.front(), items.back());
diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h
index 05af0ccde7..6bf4f45a5c 100644
--- a/esphome/components/api/api_connection.h
+++ b/esphome/components/api/api_connection.h
@@ -505,28 +505,9 @@ class APIConnection final : public APIServerConnection {
class MessageCreator {
public:
- // Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
-
- // Constructor for const char * (Event types - no allocation needed)
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
- // Delete copy operations - MessageCreator should only be moved
- MessageCreator(const MessageCreator &other) = delete;
- MessageCreator &operator=(const MessageCreator &other) = delete;
-
- // Move constructor
- MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; }
-
- // Move assignment
- MessageCreator &operator=(MessageCreator &&other) noexcept {
- if (this != &other) {
- data_ = other.data_;
- other.data_.function_ptr = nullptr;
- }
- return *this;
- }
-
// Call operator - uses message_type to determine union type
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint8_t message_type) const;
@@ -535,7 +516,7 @@ class APIConnection final : public APIServerConnection {
union Data {
MessageCreatorPtr function_ptr;
const char *const_char_ptr;
- } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
+ } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit
};
// Generic batching mechanism for both state updates and entity info
@@ -548,7 +529,7 @@ class APIConnection final : public APIServerConnection {
// Constructor for creating BatchItem
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
- : entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
+ : entity(entity), creator(creator), message_type(message_type), estimated_size(estimated_size) {}
};
std::vector items;
@@ -716,12 +697,12 @@ class APIConnection final : public APIServerConnection {
}
// Fall back to scheduled batching
- return this->schedule_message_(entity, std::move(creator), message_type, estimated_size);
+ return this->schedule_message_(entity, creator, message_type, estimated_size);
}
// Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
- this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
+ this->deferred_batch_.add_item(entity, creator, message_type, estimated_size);
return this->schedule_batch_();
}
From 1a308583b339965b23003607ed100d3815fadac2 Mon Sep 17 00:00:00 2001
From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Date: Fri, 5 Dec 2025 12:16:19 -0500
Subject: [PATCH 05/12] [esp32] Add support for ESP32-C61 variant (#12285)
Co-authored-by: Claude
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
---
esphome/components/adc/__init__.py | 10 ++++
esphome/components/adc/adc_sensor_esp32.cpp | 21 +++++----
esphome/components/deep_sleep/__init__.py | 4 ++
.../deep_sleep/deep_sleep_component.h | 2 +-
.../deep_sleep/deep_sleep_esp32.cpp | 10 ++--
esphome/components/esp32/__init__.py | 2 +
esphome/components/esp32/boards.py | 2 +
esphome/components/esp32/const.py | 3 ++
esphome/components/esp32/gpio.py | 6 +++
esphome/components/esp32/gpio_esp32_c61.py | 46 +++++++++++++++++++
esphome/components/esp32_can/canbus.py | 3 ++
esphome/components/esp32_can/esp32_can.cpp | 5 +-
esphome/components/i2s_audio/__init__.py | 2 +
.../improv_serial/improv_serial_component.cpp | 7 +--
.../improv_serial/improv_serial_component.h | 4 +-
.../internal_temperature.cpp | 16 +++----
esphome/components/logger/__init__.py | 2 +
esphome/components/spi/__init__.py | 2 +
esphome/core/defines.h | 4 +-
19 files changed, 119 insertions(+), 32 deletions(-)
create mode 100644 esphome/components/esp32/gpio_esp32_c61.py
diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py
index 62c1a5fffa..96c8334a6d 100644
--- a/esphome/components/adc/__init__.py
+++ b/esphome/components/adc/__init__.py
@@ -6,6 +6,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
@@ -100,6 +101,13 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
5: adc_channel_t.ADC_CHANNEL_5,
6: adc_channel_t.ADC_CHANNEL_6,
},
+ # https://docs.espressif.com/projects/esp-idf/en/latest/esp32c61/api-reference/peripherals/gpio.html
+ VARIANT_ESP32C61: {
+ 1: adc_channel_t.ADC_CHANNEL_0,
+ 3: adc_channel_t.ADC_CHANNEL_1,
+ 4: adc_channel_t.ADC_CHANNEL_2,
+ 5: adc_channel_t.ADC_CHANNEL_3,
+ },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {
1: adc_channel_t.ADC_CHANNEL_0,
@@ -175,6 +183,8 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
VARIANT_ESP32C5: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2
+ # ESP32-C61 has no ADC2
+ VARIANT_ESP32C61: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp
index e25b275cd6..120cb1c926 100644
--- a/esphome/components/adc/adc_sensor_esp32.cpp
+++ b/esphome/components/adc/adc_sensor_esp32.cpp
@@ -42,10 +42,11 @@ void ADCSensor::setup() {
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
init_config.unit_id = this->adc_unit_;
init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
-#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
+#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
+ USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
- // USE_ESP32_VARIANT_ESP32H2
+ // USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
@@ -74,7 +75,7 @@ void ADCSensor::setup() {
adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
- USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
+ USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
// RISC-V variants and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
@@ -111,7 +112,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
-#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32H2 || ESP32P4 || ESP32S3
+#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
}
this->setup_flags_.init_complete = true;
@@ -186,11 +187,11 @@ float ADCSensor::sample_fixed_attenuation_() {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
- USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
+ USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
-#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32H2 || ESP32P4 || ESP32S3
+#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
this->calibration_handle_ = nullptr;
}
}
@@ -219,7 +220,7 @@ float ADCSensor::sample_autorange_() {
if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
- USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
+ USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
@@ -231,7 +232,7 @@ float ADCSensor::sample_autorange_() {
adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
- USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
+ USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
@@ -266,7 +267,7 @@ float ADCSensor::sample_autorange_() {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
- USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
+ USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(handle);
#else
adc_cali_delete_scheme_line_fitting(handle);
@@ -288,7 +289,7 @@ float ADCSensor::sample_autorange_() {
}
// Clean up calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
- USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
+ USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(handle);
#else
adc_cali_delete_scheme_line_fitting(handle);
diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py
index 75affd7843..fa3ea449e2 100644
--- a/esphome/components/deep_sleep/__init__.py
+++ b/esphome/components/deep_sleep/__init__.py
@@ -6,6 +6,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
@@ -55,6 +56,7 @@ WAKEUP_PINS = {
VARIANT_ESP32C2: [0, 1, 2, 3, 4, 5],
VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5],
VARIANT_ESP32C6: [0, 1, 2, 3, 4, 5, 6, 7],
+ VARIANT_ESP32C61: [0, 1, 2, 3, 4, 5, 6],
VARIANT_ESP32H2: [7, 8, 9, 10, 11, 12, 13, 14],
VARIANT_ESP32S2: [
0,
@@ -123,6 +125,7 @@ def _validate_ex1_wakeup_mode(value):
esp32.only_on_variant(
supported=[
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
@@ -219,6 +222,7 @@ CONFIG_SCHEMA = cv.All(
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
],
msg_prefix="Wakeup from touch",
diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h
index 80381e767c..bca3aa5e4d 100644
--- a/esphome/components/deep_sleep/deep_sleep_component.h
+++ b/esphome/components/deep_sleep/deep_sleep_component.h
@@ -81,7 +81,7 @@ class DeepSleepComponent : public Component {
#endif
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
- !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
+ !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
void set_touch_wakeup(bool touch_wakeup);
#endif
diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp
index b93d9ce601..833be8e76c 100644
--- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp
+++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp
@@ -18,6 +18,7 @@ namespace deep_sleep {
// | ESP32-C3 | | | | ✓ |
// | ESP32-C5 | | (✓) | | (✓) |
// | ESP32-C6 | | ✓ | | ✓ |
+// | ESP32-C61 | | ✓ | | ✓ |
// | ESP32-H2 | | ✓ | | |
//
// Notes:
@@ -55,7 +56,7 @@ void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wa
#endif
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
- !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
+ !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
#endif
@@ -121,8 +122,9 @@ void DeepSleepComponent::deep_sleep_() {
}
#endif
- // GPIO wakeup - C2, C3, C6 only
-#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6)
+ // GPIO wakeup - C2, C3, C6, C61 only
+#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
+ defined(USE_ESP32_VARIANT_ESP32C61)
if (this->wakeup_pin_ != nullptr) {
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
@@ -155,7 +157,7 @@ void DeepSleepComponent::deep_sleep_() {
// Touch wakeup - ESP32, S2, S3 only
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
- !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
+ !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) {
esp_sleep_enable_touchpad_wakeup();
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py
index 1d05e16ebd..94280308bd 100644
--- a/esphome/components/esp32/__init__.py
+++ b/esphome/components/esp32/__init__.py
@@ -59,6 +59,7 @@ from .const import ( # noqa
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
@@ -126,6 +127,7 @@ CPU_FREQUENCIES = {
VARIANT_ESP32C3: get_cpu_frequencies(80, 160),
VARIANT_ESP32C5: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
+ VARIANT_ESP32C61: get_cpu_frequencies(80, 120, 160),
VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96),
VARIANT_ESP32P4: get_cpu_frequencies(40, 360, 400),
VARIANT_ESP32S2: get_cpu_frequencies(80, 160, 240),
diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py
index cbb314650a..7107874a5b 100644
--- a/esphome/components/esp32/boards.py
+++ b/esphome/components/esp32/boards.py
@@ -4,6 +4,7 @@ from .const import (
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
@@ -17,6 +18,7 @@ STANDARD_BOARDS = {
VARIANT_ESP32C3: "esp32-c3-devkitm-1",
VARIANT_ESP32C5: "esp32-c5-devkitc-1",
VARIANT_ESP32C6: "esp32-c6-devkitm-1",
+ VARIANT_ESP32C61: "esp32-c61-devkitc1-n8r2",
VARIANT_ESP32H2: "esp32-h2-devkitm-1",
VARIANT_ESP32P4: "esp32-p4-evboard",
VARIANT_ESP32S2: "esp32-s2-kaluga-1",
diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py
index 4358a4b712..dfb736f615 100644
--- a/esphome/components/esp32/const.py
+++ b/esphome/components/esp32/const.py
@@ -17,6 +17,7 @@ VARIANT_ESP32C2 = "ESP32C2"
VARIANT_ESP32C3 = "ESP32C3"
VARIANT_ESP32C5 = "ESP32C5"
VARIANT_ESP32C6 = "ESP32C6"
+VARIANT_ESP32C61 = "ESP32C61"
VARIANT_ESP32H2 = "ESP32H2"
VARIANT_ESP32P4 = "ESP32P4"
VARIANT_ESP32S2 = "ESP32S2"
@@ -27,6 +28,7 @@ VARIANTS = [
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
@@ -39,6 +41,7 @@ VARIANT_FRIENDLY = {
VARIANT_ESP32C3: "ESP32-C3",
VARIANT_ESP32C5: "ESP32-C5",
VARIANT_ESP32C6: "ESP32-C6",
+ VARIANT_ESP32C61: "ESP32-C61",
VARIANT_ESP32H2: "ESP32-H2",
VARIANT_ESP32P4: "ESP32-P4",
VARIANT_ESP32S2: "ESP32-S2",
diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py
index 954891ea8d..c0803f40a8 100644
--- a/esphome/components/esp32/gpio.py
+++ b/esphome/components/esp32/gpio.py
@@ -29,6 +29,7 @@ from .const import (
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
@@ -40,6 +41,7 @@ from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_support
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_supports
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
+from .gpio_esp32_c61 import esp32_c61_validate_gpio_pin, esp32_c61_validate_supports
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
@@ -110,6 +112,10 @@ _esp32_validations = {
pin_validation=esp32_c6_validate_gpio_pin,
usage_validation=esp32_c6_validate_supports,
),
+ VARIANT_ESP32C61: ESP32ValidationFunctions(
+ pin_validation=esp32_c61_validate_gpio_pin,
+ usage_validation=esp32_c61_validate_supports,
+ ),
VARIANT_ESP32H2: ESP32ValidationFunctions(
pin_validation=esp32_h2_validate_gpio_pin,
usage_validation=esp32_h2_validate_supports,
diff --git a/esphome/components/esp32/gpio_esp32_c61.py b/esphome/components/esp32/gpio_esp32_c61.py
new file mode 100644
index 0000000000..77be42db3e
--- /dev/null
+++ b/esphome/components/esp32/gpio_esp32_c61.py
@@ -0,0 +1,46 @@
+import logging
+
+import esphome.config_validation as cv
+from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
+from esphome.pins import check_strapping_pin
+
+# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM
+_ESP32C61_SPI_PSRAM_PINS = {
+ 14: "SPICS0",
+ 15: "SPICLK",
+ 16: "SPID",
+ 17: "SPIQ",
+ 19: "SPIWP",
+ 20: "SPIHD",
+ 21: "VDD_SPI",
+}
+
+_ESP32C61_STRAPPING_PINS = {8, 9}
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def esp32_c61_validate_gpio_pin(value):
+ if value < 0 or value > 29:
+ raise cv.Invalid(f"Invalid pin number: {value} (must be 0-29)")
+ if value in _ESP32C61_SPI_PSRAM_PINS:
+ raise cv.Invalid(
+ f"This pin cannot be used on ESP32-C61s and is already used by the SPI/PSRAM interface (function: {_ESP32C61_SPI_PSRAM_PINS[value]})"
+ )
+
+ return value
+
+
+def esp32_c61_validate_supports(value):
+ num = value[CONF_NUMBER]
+ mode = value[CONF_MODE]
+ is_input = mode[CONF_INPUT]
+
+ if num < 0 or num > 29:
+ raise cv.Invalid(f"Invalid pin number: {num} (must be 0-29)")
+ if is_input:
+ # All ESP32-C61 pins support input mode
+ pass
+
+ check_strapping_pin(value, _ESP32C61_STRAPPING_PINS, _LOGGER)
+ return value
diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py
index 8708c6fb36..000ef303fe 100644
--- a/esphome/components/esp32_can/canbus.py
+++ b/esphome/components/esp32_can/canbus.py
@@ -8,6 +8,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
@@ -59,6 +60,7 @@ CAN_SPEEDS_ESP32_S2 = {
CAN_SPEEDS_ESP32_S3 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_C3 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_C6 = {**CAN_SPEEDS_ESP32_S2}
+CAN_SPEEDS_ESP32_C61 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_H2 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_P4 = {**CAN_SPEEDS_ESP32_S2}
@@ -66,6 +68,7 @@ CAN_SPEEDS = {
VARIANT_ESP32: CAN_SPEEDS_ESP32,
VARIANT_ESP32C3: CAN_SPEEDS_ESP32_C3,
VARIANT_ESP32C6: CAN_SPEEDS_ESP32_C6,
+ VARIANT_ESP32C61: CAN_SPEEDS_ESP32_C61,
VARIANT_ESP32H2: CAN_SPEEDS_ESP32_H2,
VARIANT_ESP32P4: CAN_SPEEDS_ESP32_P4,
VARIANT_ESP32S2: CAN_SPEEDS_ESP32_S2,
diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp
index c10ad01450..d50964187d 100644
--- a/esphome/components/esp32_can/esp32_can.cpp
+++ b/esphome/components/esp32_can/esp32_can.cpp
@@ -16,8 +16,9 @@ static const char *const TAG = "esp32_can";
static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config) {
switch (bitrate) {
-#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) || \
- defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
+#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \
+ defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
+ defined(USE_ESP32_VARIANT_ESP32S3)
case canbus::CAN_1KBPS:
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1KBITS();
return true;
diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py
index 802db06f48..61c5ca4ec1 100644
--- a/esphome/components/i2s_audio/__init__.py
+++ b/esphome/components/i2s_audio/__init__.py
@@ -5,6 +5,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
@@ -72,6 +73,7 @@ I2S_PORTS = {
VARIANT_ESP32C3: 1,
VARIANT_ESP32C5: 1,
VARIANT_ESP32C6: 1,
+ VARIANT_ESP32C61: 1,
VARIANT_ESP32H2: 1,
VARIANT_ESP32P4: 3,
VARIANT_ESP32S2: 1,
diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp
index 70260eeab3..281e95d12b 100644
--- a/esphome/components/improv_serial/improv_serial_component.cpp
+++ b/esphome/components/improv_serial/improv_serial_component.cpp
@@ -70,9 +70,10 @@ optional ImprovSerialComponent::read_byte_() {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
- !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
+ !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
case logger::UART_SELECTION_UART2:
-#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
+#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32C6 && !USE_ESP32_VARIANT_ESP32C61 &&
+ // !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
if (this->uart_num_ >= 0) {
size_t available;
uart_get_buffered_data_len(this->uart_num_, &available);
@@ -137,7 +138,7 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size)
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
- !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
+ !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
case logger::UART_SELECTION_UART2:
#endif
uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len);
diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h
index abe50b87f2..dd8f5e4719 100644
--- a/esphome/components/improv_serial/improv_serial_component.h
+++ b/esphome/components/improv_serial/improv_serial_component.h
@@ -11,8 +11,8 @@
#ifdef USE_ESP32
#include
-#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) || \
- defined(USE_ESP32_VARIANT_ESP32S3)
+#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \
+ defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include
#include
#endif
diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp
index 6365392ce9..2ef8cf2649 100644
--- a/esphome/components/internal_temperature/internal_temperature.cpp
+++ b/esphome/components/internal_temperature/internal_temperature.cpp
@@ -8,8 +8,8 @@ extern "C" {
uint8_t temprature_sens_read();
}
#elif defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || \
- defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || \
- defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
+ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
+ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "driver/temperature_sensor.h"
#endif // USE_ESP32_VARIANT
#endif // USE_ESP32
@@ -28,8 +28,8 @@ namespace internal_temperature {
static const char *const TAG = "internal_temperature";
#ifdef USE_ESP32
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
- defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
- defined(USE_ESP32_VARIANT_ESP32S3)
+ defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || \
+ defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
static temperature_sensor_handle_t tsensNew = NULL;
#endif // USE_ESP32_VARIANT
#endif // USE_ESP32
@@ -44,8 +44,8 @@ void InternalTemperatureSensor::update() {
temperature = (raw - 32) / 1.8f;
success = (raw != 128);
#elif defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || \
- defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || \
- defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
+ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
+ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
esp_err_t result = temperature_sensor_get_celsius(tsensNew, &temperature);
success = (result == ESP_OK);
if (!success) {
@@ -82,8 +82,8 @@ void InternalTemperatureSensor::update() {
void InternalTemperatureSensor::setup() {
#ifdef USE_ESP32
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
- defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
- defined(USE_ESP32_VARIANT_ESP32S3)
+ defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || \
+ defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
esp_err_t result = temperature_sensor_install(&tsens_config, &tsensNew);
diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py
index 7369e99c85..fb0ce92cc9 100644
--- a/esphome/components/logger/__init__.py
+++ b/esphome/components/logger/__init__.py
@@ -9,6 +9,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
@@ -105,6 +106,7 @@ UART_SELECTION_ESP32 = {
VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32C5: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
+ VARIANT_ESP32C61: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32P4: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32S2: [UART0, UART1, USB_CDC],
diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py
index 1530ffb882..0b531b9ed6 100644
--- a/esphome/components/spi/__init__.py
+++ b/esphome/components/spi/__init__.py
@@ -8,6 +8,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
@@ -129,6 +130,7 @@ def get_hw_interface_list():
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32H2,
]:
return [["spi", "spi2"]]
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index 5d3bca55a2..358334d7b3 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -234,8 +234,8 @@
#if defined(USE_ESP32_VARIANT_ESP32S2)
#define USE_LOGGER_USB_CDC
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \
- defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || \
- defined(USE_ESP32_VARIANT_ESP32S3)
+ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
+ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S3)
#define USE_LOGGER_USB_CDC
#define USE_LOGGER_USB_SERIAL_JTAG
#endif
From 7f7c913a853db92d269cc7e3f63040d0b112317c Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Fri, 5 Dec 2025 11:47:54 -0600
Subject: [PATCH 06/12] [light] Fix schedule_show not enabling loop for idle
addressable lights (#12302)
---
esphome/components/light/addressable_light.h | 2 +-
esphome/components/light/light_state.cpp | 3 +--
esphome/components/light/light_state.h | 6 ++++++
3 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h
index 2e4b984ce4..fcaf07f578 100644
--- a/esphome/components/light/addressable_light.h
+++ b/esphome/components/light/addressable_light.h
@@ -70,7 +70,7 @@ class AddressableLight : public LightOutput, public Component {
this->state_parent_ = state;
}
void update_state(LightState *state) override;
- void schedule_show() { this->state_parent_->next_write_ = true; }
+ void schedule_show() { this->state_parent_->schedule_write_(); }
#ifdef USE_POWER_SUPPLY
void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); }
diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp
index af619a426a..5a50bae50b 100644
--- a/esphome/components/light/light_state.cpp
+++ b/esphome/components/light/light_state.cpp
@@ -305,8 +305,7 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
this->remote_values = target;
}
this->output_->update_state(this);
- this->next_write_ = true;
- this->enable_loop();
+ this->schedule_write_();
}
void LightState::disable_loop_if_idle_() {
diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h
index 7ea72306f9..a21c2c7693 100644
--- a/esphome/components/light/light_state.h
+++ b/esphome/components/light/light_state.h
@@ -277,6 +277,12 @@ class LightState : public EntityBase, public Component {
/// Disable loop if neither transformer nor effect is active
void disable_loop_if_idle_();
+ /// Schedule a write to the light output and enable the loop to process it
+ void schedule_write_() {
+ this->next_write_ = true;
+ this->enable_loop();
+ }
+
/// Store the output to allow effects to have more access.
LightOutput *output_;
/// The currently active transformer for this light (transition/flash).
From 78bef42473a049781cdcc115d9dc7d3a916c045a Mon Sep 17 00:00:00 2001
From: c0mputerguru
Date: Fri, 5 Dec 2025 10:33:00 -0800
Subject: [PATCH 07/12] [sps30] Add idle mode functionality (#12255)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
---
esphome/components/sps30/automation.h | 17 +++++---
esphome/components/sps30/sensor.py | 20 ++++++++--
esphome/components/sps30/sps30.cpp | 56 +++++++++++++++++++++++++++
esphome/components/sps30/sps30.h | 7 ++++
tests/components/sps30/common.yaml | 1 +
5 files changed, 92 insertions(+), 9 deletions(-)
diff --git a/esphome/components/sps30/automation.h b/esphome/components/sps30/automation.h
index 67af813687..5eafc1b6c2 100644
--- a/esphome/components/sps30/automation.h
+++ b/esphome/components/sps30/automation.h
@@ -1,20 +1,25 @@
#pragma once
-#include "esphome/core/component.h"
#include "esphome/core/automation.h"
+#include "esphome/core/helpers.h"
#include "sps30.h"
namespace esphome {
namespace sps30 {
-template class StartFanAction : public Action {
+template class StartFanAction : public Action, public Parented {
public:
- explicit StartFanAction(SPS30Component *sps30) : sps30_(sps30) {}
+ void play(const Ts &...x) override { this->parent_->start_fan_cleaning(); }
+};
- void play(const Ts &...x) override { this->sps30_->start_fan_cleaning(); }
+template class StartMeasurementAction : public Action, public Parented {
+ public:
+ void play(const Ts &...x) override { this->parent_->start_measurement(); }
+};
- protected:
- SPS30Component *sps30_;
+template class StopMeasurementAction : public Action, public Parented {
+ public:
+ void play(const Ts &...x) override { this->parent_->stop_measurement(); }
};
} // namespace sps30
diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py
index d4f91b4188..3c967fc01b 100644
--- a/esphome/components/sps30/sensor.py
+++ b/esphome/components/sps30/sensor.py
@@ -38,8 +38,11 @@ SPS30Component = sps30_ns.class_(
# Actions
StartFanAction = sps30_ns.class_("StartFanAction", automation.Action)
+StartMeasurementAction = sps30_ns.class_("StartMeasurementAction", automation.Action)
+StopMeasurementAction = sps30_ns.class_("StopMeasurementAction", automation.Action)
CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval"
+CONF_IDLE_INTERVAL = "idle_interval"
CONFIG_SCHEMA = (
cv.Schema(
@@ -109,6 +112,7 @@ CONFIG_SCHEMA = (
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval,
+ cv.Optional(CONF_IDLE_INTERVAL): cv.update_interval,
}
)
.extend(cv.polling_component_schema("60s"))
@@ -164,6 +168,9 @@ async def to_code(config):
if CONF_AUTO_CLEANING_INTERVAL in config:
cg.add(var.set_auto_cleaning_interval(config[CONF_AUTO_CLEANING_INTERVAL]))
+ if CONF_IDLE_INTERVAL in config:
+ cg.add(var.set_idle_interval(config[CONF_IDLE_INTERVAL]))
+
SPS30_ACTION_SCHEMA = maybe_simple_id(
{
@@ -175,6 +182,13 @@ SPS30_ACTION_SCHEMA = maybe_simple_id(
@automation.register_action(
"sps30.start_fan_autoclean", StartFanAction, SPS30_ACTION_SCHEMA
)
-async def sps30_fan_to_code(config, action_id, template_arg, args):
- paren = await cg.get_variable(config[CONF_ID])
- return cg.new_Pvariable(action_id, template_arg, paren)
+@automation.register_action(
+ "sps30.start_measurement", StartMeasurementAction, SPS30_ACTION_SCHEMA
+)
+@automation.register_action(
+ "sps30.stop_measurement", StopMeasurementAction, SPS30_ACTION_SCHEMA
+)
+async def sps30_action_to_code(config, action_id, template_arg, args):
+ var = cg.new_Pvariable(action_id, template_arg)
+ await cg.register_parented(var, config[CONF_ID])
+ return var
diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp
index 21a782e49a..dbb44743d2 100644
--- a/esphome/components/sps30/sps30.cpp
+++ b/esphome/components/sps30/sps30.cpp
@@ -20,6 +20,7 @@ static const uint16_t SPS30_CMD_START_FAN_CLEANING = 0x5607;
static const uint16_t SPS30_CMD_SOFT_RESET = 0xD304;
static const size_t SERIAL_NUMBER_LENGTH = 8;
static const uint8_t MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR = 5;
+static const uint32_t SPS30_WARM_UP_SEC = 30;
void SPS30Component::setup() {
this->write_command(SPS30_CMD_SOFT_RESET);
@@ -63,6 +64,8 @@ void SPS30Component::setup() {
this->status_clear_warning();
this->skipped_data_read_cycles_ = 0;
this->start_continuous_measurement_();
+ this->next_state_ms_ = millis() + SPS30_WARM_UP_SEC * 1000;
+ this->next_state_ = READ;
this->setup_complete_ = true;
});
});
@@ -101,6 +104,9 @@ void SPS30Component::dump_config() {
" Serial number: %s\n"
" Firmware version v%0d.%0d",
this->serial_number_, this->raw_firmware_version_ >> 8, this->raw_firmware_version_ & 0xFF);
+ if (this->idle_interval_.has_value()) {
+ ESP_LOGCONFIG(TAG, " Idle interval: %us", this->idle_interval_.value() / 1000);
+ }
LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_);
LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_);
LOG_SENSOR(" ", "PM4 Weight Concentration", this->pm_4_0_sensor_);
@@ -132,6 +138,26 @@ void SPS30Component::update() {
}
return;
}
+
+ // If its not time to take an action, do nothing.
+ const uint32_t update_start_ms = millis();
+ if (this->next_state_ != NONE && (int32_t) (this->next_state_ms_ - update_start_ms) > 0) {
+ ESP_LOGD(TAG, "Sensor waiting for %ums before transitioning to state %d.", (this->next_state_ms_ - update_start_ms),
+ this->next_state_);
+ return;
+ }
+
+ switch (this->next_state_) {
+ case WAKE:
+ this->start_measurement();
+ return;
+ case NONE:
+ return;
+ case READ:
+ // Read logic continues below
+ break;
+ }
+
/// Check if measurement is ready before reading the value
if (!this->write_command(SPS30_CMD_GET_DATA_READY_STATUS)) {
this->status_set_warning();
@@ -211,6 +237,16 @@ void SPS30Component::update() {
this->status_clear_warning();
this->skipped_data_read_cycles_ = 0;
+
+ // Stop measurements and wait if we have an idle interval. If not using idle mode, let the next state just execute
+ // on next update.
+ if (this->idle_interval_.has_value()) {
+ this->stop_measurement();
+ this->next_state_ms_ = millis() + this->idle_interval_.value();
+ this->next_state_ = WAKE;
+ } else {
+ this->next_state_ms_ = millis();
+ }
});
}
@@ -219,6 +255,26 @@ bool SPS30Component::start_continuous_measurement_() {
ESP_LOGE(TAG, "Error initiating measurements");
return false;
}
+ ESP_LOGD(TAG, "Started measurements");
+
+ // Notify the state machine to wait the warm up interval before reading
+ this->next_state_ms_ = millis() + SPS30_WARM_UP_SEC * 1000;
+ this->next_state_ = READ;
+ return true;
+}
+
+bool SPS30Component::start_measurement() { return start_continuous_measurement_(); }
+
+bool SPS30Component::stop_measurement() {
+ if (!write_command(SPS30_CMD_STOP_MEASUREMENTS)) {
+ ESP_LOGE(TAG, "Error stopping measurements");
+ return false;
+ } else {
+ ESP_LOGD(TAG, "Stopped measurements");
+ // Exit the state machine if measurement is stopped.
+ this->next_state_ms_ = 0;
+ this->next_state_ = NONE;
+ }
return true;
}
diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h
index 18847e16d9..4e9b90ba7e 100644
--- a/esphome/components/sps30/sps30.h
+++ b/esphome/components/sps30/sps30.h
@@ -23,17 +23,23 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri
void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; }
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { fan_interval_ = auto_cleaning_interval; }
+ void set_idle_interval(uint32_t idle_interval) { idle_interval_ = idle_interval; }
void setup() override;
void update() override;
void dump_config() override;
bool start_fan_cleaning();
+ bool stop_measurement();
+ bool start_measurement();
protected:
bool setup_complete_{false};
uint16_t raw_firmware_version_;
char serial_number_[17] = {0}; /// Terminating NULL character
uint8_t skipped_data_read_cycles_ = 0;
+ uint32_t next_state_ms_ = 0;
+
+ enum NextState : uint8_t { WAKE, READ, NONE } next_state_{NONE};
bool start_continuous_measurement_();
@@ -58,6 +64,7 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri
sensor::Sensor *pmc_10_0_sensor_{nullptr};
sensor::Sensor *pm_size_sensor_{nullptr};
optional fan_interval_;
+ optional idle_interval_;
};
} // namespace sps30
diff --git a/tests/components/sps30/common.yaml b/tests/components/sps30/common.yaml
index d40cd16b6d..a83477b764 100644
--- a/tests/components/sps30/common.yaml
+++ b/tests/components/sps30/common.yaml
@@ -30,3 +30,4 @@ sensor:
id: workshop_PMC_10_0
address: 0x69
update_interval: 10s
+ idle_interval: 5min
From 7421f31160142f9bfd726ad75dcf1ba3c9c199d3 Mon Sep 17 00:00:00 2001
From: Stuart Parmenter
Date: Fri, 5 Dec 2025 10:51:32 -0800
Subject: [PATCH 08/12] [hub75] HUB75 display component (#11153)
Co-authored-by: J. Nick Koston
---
.clang-tidy.hash | 2 +-
CODEOWNERS | 1 +
esphome/components/hub75/__init__.py | 6 +
esphome/components/hub75/boards/__init__.py | 80 +++
esphome/components/hub75/boards/adafruit.py | 23 +
esphome/components/hub75/boards/apollo.py | 41 ++
esphome/components/hub75/boards/huidu.py | 22 +
esphome/components/hub75/boards/trinity.py | 24 +
esphome/components/hub75/display.py | 578 ++++++++++++++++++
esphome/components/hub75/hub75.cpp | 192 ++++++
esphome/components/hub75/hub75_component.h | 55 ++
platformio.ini | 2 +
tests/components/hub75/test.esp32-idf.yaml | 39 ++
.../hub75/test.esp32-s3-idf-board.yaml | 26 +
tests/components/hub75/test.esp32-s3-idf.yaml | 39 ++
15 files changed, 1129 insertions(+), 1 deletion(-)
create mode 100644 esphome/components/hub75/__init__.py
create mode 100644 esphome/components/hub75/boards/__init__.py
create mode 100644 esphome/components/hub75/boards/adafruit.py
create mode 100644 esphome/components/hub75/boards/apollo.py
create mode 100644 esphome/components/hub75/boards/huidu.py
create mode 100644 esphome/components/hub75/boards/trinity.py
create mode 100644 esphome/components/hub75/display.py
create mode 100644 esphome/components/hub75/hub75.cpp
create mode 100644 esphome/components/hub75/hub75_component.h
create mode 100644 tests/components/hub75/test.esp32-idf.yaml
create mode 100644 tests/components/hub75/test.esp32-s3-idf-board.yaml
create mode 100644 tests/components/hub75/test.esp32-s3-idf.yaml
diff --git a/.clang-tidy.hash b/.clang-tidy.hash
index ab3217b5e5..7dabee48f1 100644
--- a/.clang-tidy.hash
+++ b/.clang-tidy.hash
@@ -1 +1 @@
-29270eecb86ffa07b2b1d2a4ca56dd7f84762ddc89c6248dbf3f012eca8780b6
+c01eec15857a784dd603c0afd194ab3b29a632422fe6f6b0a806ad4d81b5efc0
diff --git a/CODEOWNERS b/CODEOWNERS
index 65405f79d1..4f9fb7ef55 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -227,6 +227,7 @@ esphome/components/hte501/* @Stock-M
esphome/components/http_request/ota/* @oarcher
esphome/components/http_request/update/* @jesserockz
esphome/components/htu31d/* @betterengineering
+esphome/components/hub75/* @stuartparmenter
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core
diff --git a/esphome/components/hub75/__init__.py b/esphome/components/hub75/__init__.py
new file mode 100644
index 0000000000..cd5441f749
--- /dev/null
+++ b/esphome/components/hub75/__init__.py
@@ -0,0 +1,6 @@
+from esphome.cpp_generator import MockObj
+
+CODEOWNERS = ["@stuartparmenter"]
+
+# Use fully-qualified namespace to avoid collision with external hub75 library's global ::hub75 namespace
+hub75_ns = MockObj("::esphome::hub75", "::")
diff --git a/esphome/components/hub75/boards/__init__.py b/esphome/components/hub75/boards/__init__.py
new file mode 100644
index 0000000000..52f8864c60
--- /dev/null
+++ b/esphome/components/hub75/boards/__init__.py
@@ -0,0 +1,80 @@
+"""Board presets for HUB75 displays.
+
+Each board preset defines standard pin mappings for HUB75 controller boards.
+"""
+
+from dataclasses import dataclass, field
+import importlib
+import pkgutil
+from typing import ClassVar
+
+
+class BoardRegistry:
+ """Global registry for board configurations."""
+
+ _boards: ClassVar[dict[str, "BoardConfig"]] = {}
+
+ @classmethod
+ def register(cls, board: "BoardConfig") -> None:
+ """Register a board configuration."""
+ cls._boards[board.name] = board
+
+ @classmethod
+ def get_boards(cls) -> dict[str, "BoardConfig"]:
+ """Return all registered boards."""
+ return cls._boards
+
+
+@dataclass
+class BoardConfig:
+ """Board configuration storing HUB75 pin mappings."""
+
+ name: str
+ r1_pin: int
+ g1_pin: int
+ b1_pin: int
+ r2_pin: int
+ g2_pin: int
+ b2_pin: int
+ a_pin: int
+ b_pin: int
+ c_pin: int
+ d_pin: int
+ e_pin: int | None
+ lat_pin: int
+ oe_pin: int
+ clk_pin: int
+ ignore_strapping_pins: tuple[str, ...] = () # e.g., ("a_pin", "clk_pin")
+
+ # Derived field for pin lookup
+ pins: dict[str, int | None] = field(default_factory=dict, init=False, repr=False)
+
+ def __post_init__(self):
+ """Initialize derived fields and register board."""
+ self.name = self.name.lower()
+ self.pins = {
+ "r1": self.r1_pin,
+ "g1": self.g1_pin,
+ "b1": self.b1_pin,
+ "r2": self.r2_pin,
+ "g2": self.g2_pin,
+ "b2": self.b2_pin,
+ "a": self.a_pin,
+ "b": self.b_pin,
+ "c": self.c_pin,
+ "d": self.d_pin,
+ "e": self.e_pin,
+ "lat": self.lat_pin,
+ "oe": self.oe_pin,
+ "clk": self.clk_pin,
+ }
+ BoardRegistry.register(self)
+
+ def get_pin(self, pin_name: str) -> int | None:
+ """Get pin number for a given pin name."""
+ return self.pins.get(pin_name)
+
+
+# Dynamically import all board definition modules
+for module_info in pkgutil.iter_modules(__path__):
+ importlib.import_module(f".{module_info.name}", package=__package__)
diff --git a/esphome/components/hub75/boards/adafruit.py b/esphome/components/hub75/boards/adafruit.py
new file mode 100644
index 0000000000..e27eeb9379
--- /dev/null
+++ b/esphome/components/hub75/boards/adafruit.py
@@ -0,0 +1,23 @@
+"""Adafruit Matrix Portal board definitions."""
+
+from . import BoardConfig
+
+# Adafruit Matrix Portal S3
+BoardConfig(
+ "adafruit-matrix-portal-s3",
+ r1_pin=42,
+ g1_pin=41,
+ b1_pin=40,
+ r2_pin=38,
+ g2_pin=39,
+ b2_pin=37,
+ a_pin=45,
+ b_pin=36,
+ c_pin=48,
+ d_pin=35,
+ e_pin=21,
+ lat_pin=47,
+ oe_pin=14,
+ clk_pin=2,
+ ignore_strapping_pins=("a_pin",), # GPIO45 is a strapping pin
+)
diff --git a/esphome/components/hub75/boards/apollo.py b/esphome/components/hub75/boards/apollo.py
new file mode 100644
index 0000000000..4b8b2c1f0a
--- /dev/null
+++ b/esphome/components/hub75/boards/apollo.py
@@ -0,0 +1,41 @@
+"""Apollo Automation M1 board definitions."""
+
+from . import BoardConfig
+
+# Apollo Automation M1 Rev4
+BoardConfig(
+ "apollo-automation-m1-rev4",
+ r1_pin=42,
+ g1_pin=41,
+ b1_pin=40,
+ r2_pin=38,
+ g2_pin=39,
+ b2_pin=37,
+ a_pin=45,
+ b_pin=36,
+ c_pin=48,
+ d_pin=35,
+ e_pin=21,
+ lat_pin=47,
+ oe_pin=14,
+ clk_pin=2,
+)
+
+# Apollo Automation M1 Rev6
+BoardConfig(
+ "apollo-automation-m1-rev6",
+ r1_pin=1,
+ g1_pin=5,
+ b1_pin=6,
+ r2_pin=7,
+ g2_pin=13,
+ b2_pin=9,
+ a_pin=16,
+ b_pin=48,
+ c_pin=47,
+ d_pin=21,
+ e_pin=38,
+ lat_pin=8,
+ oe_pin=4,
+ clk_pin=18,
+)
diff --git a/esphome/components/hub75/boards/huidu.py b/esphome/components/hub75/boards/huidu.py
new file mode 100644
index 0000000000..52744d397e
--- /dev/null
+++ b/esphome/components/hub75/boards/huidu.py
@@ -0,0 +1,22 @@
+"""Huidu board definitions."""
+
+from . import BoardConfig
+
+# Huidu HD-WF2
+BoardConfig(
+ "huidu-hd-wf2",
+ r1_pin=2,
+ g1_pin=6,
+ b1_pin=10,
+ r2_pin=3,
+ g2_pin=7,
+ b2_pin=11,
+ a_pin=39,
+ b_pin=38,
+ c_pin=37,
+ d_pin=36,
+ e_pin=21,
+ lat_pin=33,
+ oe_pin=35,
+ clk_pin=34,
+)
diff --git a/esphome/components/hub75/boards/trinity.py b/esphome/components/hub75/boards/trinity.py
new file mode 100644
index 0000000000..bfad779ad0
--- /dev/null
+++ b/esphome/components/hub75/boards/trinity.py
@@ -0,0 +1,24 @@
+"""ESP32 Trinity board definitions."""
+
+from . import BoardConfig
+
+# ESP32 Trinity
+# https://esp32trinity.com/
+# Pin assignments from: https://github.com/witnessmenow/ESP32-Trinity/blob/master/FAQ.md
+BoardConfig(
+ "esp32-trinity",
+ r1_pin=25,
+ g1_pin=26,
+ b1_pin=27,
+ r2_pin=14,
+ g2_pin=12,
+ b2_pin=13,
+ a_pin=23,
+ b_pin=19,
+ c_pin=5,
+ d_pin=17,
+ e_pin=18,
+ lat_pin=4,
+ oe_pin=15,
+ clk_pin=16,
+)
diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py
new file mode 100644
index 0000000000..81dd4ffc1c
--- /dev/null
+++ b/esphome/components/hub75/display.py
@@ -0,0 +1,578 @@
+from typing import Any
+
+from esphome import pins
+import esphome.codegen as cg
+from esphome.components import display
+from esphome.components.esp32 import add_idf_component
+import esphome.config_validation as cv
+from esphome.const import (
+ CONF_AUTO_CLEAR_ENABLED,
+ CONF_BIT_DEPTH,
+ CONF_BOARD,
+ CONF_BRIGHTNESS,
+ CONF_CLK_PIN,
+ CONF_GAMMA_CORRECT,
+ CONF_ID,
+ CONF_LAMBDA,
+ CONF_OE_PIN,
+ CONF_UPDATE_INTERVAL,
+)
+import esphome.final_validate as fv
+from esphome.types import ConfigType
+
+from . import boards, hub75_ns
+
+DEPENDENCIES = ["esp32"]
+CODEOWNERS = ["@stuartparmenter"]
+
+# Load all board presets
+BOARDS = boards.BoardRegistry.get_boards()
+
+# Constants
+CONF_HUB75_ID = "hub75_id"
+
+# Panel dimensions
+CONF_PANEL_WIDTH = "panel_width"
+CONF_PANEL_HEIGHT = "panel_height"
+
+# Multi-panel layout
+CONF_LAYOUT_ROWS = "layout_rows"
+CONF_LAYOUT_COLS = "layout_cols"
+CONF_LAYOUT = "layout"
+
+# Panel hardware
+CONF_SCAN_WIRING = "scan_wiring"
+CONF_SHIFT_DRIVER = "shift_driver"
+
+# RGB pins
+CONF_R1_PIN = "r1_pin"
+CONF_G1_PIN = "g1_pin"
+CONF_B1_PIN = "b1_pin"
+CONF_R2_PIN = "r2_pin"
+CONF_G2_PIN = "g2_pin"
+CONF_B2_PIN = "b2_pin"
+
+# Address pins
+CONF_A_PIN = "a_pin"
+CONF_B_PIN = "b_pin"
+CONF_C_PIN = "c_pin"
+CONF_D_PIN = "d_pin"
+CONF_E_PIN = "e_pin"
+
+# Control pins
+CONF_LAT_PIN = "lat_pin"
+
+NEVER = 4294967295 # uint32_t max - value used when update_interval is "never"
+
+# Pin mapping from config keys to board keys
+PIN_MAPPING = {
+ CONF_R1_PIN: "r1",
+ CONF_G1_PIN: "g1",
+ CONF_B1_PIN: "b1",
+ CONF_R2_PIN: "r2",
+ CONF_G2_PIN: "g2",
+ CONF_B2_PIN: "b2",
+ CONF_A_PIN: "a",
+ CONF_B_PIN: "b",
+ CONF_C_PIN: "c",
+ CONF_D_PIN: "d",
+ CONF_E_PIN: "e",
+ CONF_LAT_PIN: "lat",
+ CONF_OE_PIN: "oe",
+ CONF_CLK_PIN: "clk",
+}
+
+# Required pins (E pin is optional)
+REQUIRED_PINS = [key for key in PIN_MAPPING if key != CONF_E_PIN]
+
+# Configuration
+CONF_CLOCK_SPEED = "clock_speed"
+CONF_LATCH_BLANKING = "latch_blanking"
+CONF_CLOCK_PHASE = "clock_phase"
+CONF_DOUBLE_BUFFER = "double_buffer"
+CONF_MIN_REFRESH_RATE = "min_refresh_rate"
+
+# Map to hub75 library enums (in global namespace)
+ShiftDriver = cg.global_ns.enum("ShiftDriver", is_class=True)
+SHIFT_DRIVERS = {
+ "GENERIC": ShiftDriver.GENERIC,
+ "FM6126A": ShiftDriver.FM6126A,
+ "ICN2038S": ShiftDriver.ICN2038S,
+ "FM6124": ShiftDriver.FM6124,
+ "MBI5124": ShiftDriver.MBI5124,
+ "DP3246": ShiftDriver.DP3246,
+}
+
+PanelLayout = cg.global_ns.enum("PanelLayout", is_class=True)
+PANEL_LAYOUTS = {
+ "HORIZONTAL": PanelLayout.HORIZONTAL,
+ "TOP_LEFT_DOWN": PanelLayout.TOP_LEFT_DOWN,
+ "TOP_RIGHT_DOWN": PanelLayout.TOP_RIGHT_DOWN,
+ "BOTTOM_LEFT_UP": PanelLayout.BOTTOM_LEFT_UP,
+ "BOTTOM_RIGHT_UP": PanelLayout.BOTTOM_RIGHT_UP,
+ "TOP_LEFT_DOWN_ZIGZAG": PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
+ "TOP_RIGHT_DOWN_ZIGZAG": PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
+ "BOTTOM_LEFT_UP_ZIGZAG": PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
+ "BOTTOM_RIGHT_UP_ZIGZAG": PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
+}
+
+ScanPattern = cg.global_ns.enum("ScanPattern", is_class=True)
+SCAN_PATTERNS = {
+ "STANDARD_TWO_SCAN": ScanPattern.STANDARD_TWO_SCAN,
+ "FOUR_SCAN_16PX_HIGH": ScanPattern.FOUR_SCAN_16PX_HIGH,
+ "FOUR_SCAN_32PX_HIGH": ScanPattern.FOUR_SCAN_32PX_HIGH,
+ "FOUR_SCAN_64PX_HIGH": ScanPattern.FOUR_SCAN_64PX_HIGH,
+}
+
+Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
+CLOCK_SPEEDS = {
+ "8MHZ": Hub75ClockSpeed.HZ_8M,
+ "10MHZ": Hub75ClockSpeed.HZ_10M,
+ "16MHZ": Hub75ClockSpeed.HZ_16M,
+ "20MHZ": Hub75ClockSpeed.HZ_20M,
+}
+
+HUB75Display = hub75_ns.class_("HUB75Display", cg.PollingComponent, display.Display)
+Hub75Config = cg.global_ns.struct("Hub75Config")
+Hub75Pins = cg.global_ns.struct("Hub75Pins")
+
+
+def _merge_board_pins(config: ConfigType) -> ConfigType:
+ """Merge board preset pins with explicit pin overrides."""
+ board_name = config.get(CONF_BOARD)
+
+ if board_name is None:
+ # No board specified - validate that all required pins are present
+ errs = [
+ cv.Invalid(
+ f"Required pin '{pin_name}' is missing. "
+ f"Either specify a board preset or provide all pin mappings manually.",
+ path=[pin_name],
+ )
+ for pin_name in REQUIRED_PINS
+ if pin_name not in config
+ ]
+
+ if errs:
+ raise cv.MultipleInvalid(errs)
+
+ # E_PIN is optional
+ return config
+
+ # Get board configuration
+ if board_name not in BOARDS:
+ raise cv.Invalid(
+ f"Unknown board '{board_name}'. Available boards: {', '.join(sorted(BOARDS.keys()))}"
+ )
+
+ board = BOARDS[board_name]
+
+ # Merge board pins with explicit overrides
+ # Explicit pins in config take precedence over board defaults
+ for conf_key, board_key in PIN_MAPPING.items():
+ if conf_key in config or (board_pin := board.get_pin(board_key)) is None:
+ continue
+ # Create pin config
+ pin_config = {"number": board_pin}
+ if conf_key in board.ignore_strapping_pins:
+ pin_config["ignore_strapping_warning"] = True
+
+ # Validate through pin schema to add required fields (id, etc.)
+ config[conf_key] = pins.gpio_output_pin_schema(pin_config)
+
+ return config
+
+
+def _validate_config(config: ConfigType) -> ConfigType:
+ """Validate driver and layout requirements."""
+ errs: list[cv.Invalid] = []
+
+ # MBI5124 requires inverted clock phase
+ driver = config.get(CONF_SHIFT_DRIVER, "GENERIC")
+ if driver == "MBI5124" and not config.get(CONF_CLOCK_PHASE, False):
+ errs.append(
+ cv.Invalid(
+ "MBI5124 shift driver requires 'clock_phase: true' to be set",
+ path=[CONF_CLOCK_PHASE],
+ )
+ )
+
+ # Prevent conflicting min_refresh_rate + update_interval configuration
+ # min_refresh_rate is auto-calculated from update_interval unless using LVGL mode
+ update_interval = config.get(CONF_UPDATE_INTERVAL)
+ if CONF_MIN_REFRESH_RATE in config and update_interval is not None:
+ # Handle both integer (NEVER) and time object cases
+ interval_ms = (
+ update_interval
+ if isinstance(update_interval, int)
+ else update_interval.total_milliseconds
+ )
+ if interval_ms != NEVER:
+ errs.append(
+ cv.Invalid(
+ "Cannot set both 'min_refresh_rate' and 'update_interval' (except 'never'). "
+ "Refresh rate is auto-calculated from update_interval. "
+ "Remove 'min_refresh_rate' or use 'update_interval: never' for LVGL mode.",
+ path=[CONF_MIN_REFRESH_RATE],
+ )
+ )
+
+ # Validate layout configuration (validate effective config including C++ defaults)
+ layout = config.get(CONF_LAYOUT, "HORIZONTAL")
+ layout_rows = config.get(CONF_LAYOUT_ROWS, 1)
+ layout_cols = config.get(CONF_LAYOUT_COLS, 1)
+ is_zigzag = "ZIGZAG" in layout
+
+ # Single panel (1x1) should use HORIZONTAL
+ if layout_rows == 1 and layout_cols == 1 and layout != "HORIZONTAL":
+ errs.append(
+ cv.Invalid(
+ f"Single panel (layout_rows=1, layout_cols=1) should use 'layout: HORIZONTAL' (got {layout})",
+ path=[CONF_LAYOUT],
+ )
+ )
+
+ # HORIZONTAL layout requires single row
+ if layout == "HORIZONTAL" and layout_rows != 1:
+ errs.append(
+ cv.Invalid(
+ f"HORIZONTAL layout requires 'layout_rows: 1' (got {layout_rows}). "
+ "For multi-row grids, use TOP_LEFT_DOWN or other grid layouts.",
+ path=[CONF_LAYOUT_ROWS],
+ )
+ )
+
+ # Grid layouts (non-HORIZONTAL) require more than one panel
+ if layout != "HORIZONTAL" and layout_rows == 1 and layout_cols == 1:
+ errs.append(
+ cv.Invalid(
+ f"Grid layout '{layout}' requires multiple panels (layout_rows > 1 or layout_cols > 1)",
+ path=[CONF_LAYOUT],
+ )
+ )
+
+ # Serpentine layouts (non-ZIGZAG) require multiple rows
+ # Serpentine physically rotates alternate rows upside down (Y-coordinate inversion)
+ # Single-row chains should use HORIZONTAL or ZIGZAG variants
+ if not is_zigzag and layout != "HORIZONTAL" and layout_rows == 1:
+ errs.append(
+ cv.Invalid(
+ f"Serpentine layout '{layout}' requires layout_rows > 1 "
+ f"(got layout_rows={layout_rows}). "
+ "Serpentine wiring physically rotates alternate rows upside down. "
+ "For single-row chains, use 'layout: HORIZONTAL' or add '_ZIGZAG' suffix.",
+ path=[CONF_LAYOUT_ROWS],
+ )
+ )
+
+ # ZIGZAG layouts require actual grid (both rows AND cols > 1)
+ if is_zigzag and (layout_rows == 1 or layout_cols == 1):
+ errs.append(
+ cv.Invalid(
+ f"ZIGZAG layout '{layout}' requires both layout_rows > 1 AND layout_cols > 1 "
+ f"(got rows={layout_rows}, cols={layout_cols}). "
+ "For single row/column chains, use non-zigzag layouts or HORIZONTAL.",
+ path=[CONF_LAYOUT],
+ )
+ )
+
+ if errs:
+ raise cv.MultipleInvalid(errs)
+
+ return config
+
+
+def _final_validate(config: ConfigType) -> ConfigType:
+ """Validate requirements when using HUB75 display."""
+ # Local imports to avoid circular dependencies
+ from esphome.components.esp32 import get_esp32_variant
+ from esphome.components.esp32.const import VARIANT_ESP32P4
+ from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
+ from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
+
+ full_config = fv.full_config.get()
+ errs: list[cv.Invalid] = []
+
+ # ESP32-P4 requires PSRAM
+ variant = get_esp32_variant()
+ if variant == VARIANT_ESP32P4 and PSRAM_DOMAIN not in full_config:
+ errs.append(
+ cv.Invalid(
+ "HUB75 display on ESP32-P4 requires PSRAM. Add 'psram:' to your configuration.",
+ path=[CONF_ID],
+ )
+ )
+
+ # LVGL-specific validation
+ if LVGL_DOMAIN in full_config:
+ # Check update_interval (converted from "never" to NEVER constant)
+ update_interval = config.get(CONF_UPDATE_INTERVAL)
+ if update_interval is not None:
+ # Handle both integer (NEVER) and time object cases
+ interval_ms = (
+ update_interval
+ if isinstance(update_interval, int)
+ else update_interval.total_milliseconds
+ )
+ if interval_ms != NEVER:
+ errs.append(
+ cv.Invalid(
+ "HUB75 display with LVGL must have 'update_interval: never'. "
+ "LVGL manages its own refresh timing.",
+ path=[CONF_UPDATE_INTERVAL],
+ )
+ )
+
+ # Check auto_clear_enabled
+ auto_clear = config[CONF_AUTO_CLEAR_ENABLED]
+ if auto_clear is not False:
+ errs.append(
+ cv.Invalid(
+ f"HUB75 display with LVGL must have 'auto_clear_enabled: false' (got '{auto_clear}'). "
+ "LVGL manages screen clearing.",
+ path=[CONF_AUTO_CLEAR_ENABLED],
+ )
+ )
+
+ # Check double_buffer (C++ default: false)
+ double_buffer = config.get(CONF_DOUBLE_BUFFER, False)
+ if double_buffer is not False:
+ errs.append(
+ cv.Invalid(
+ f"HUB75 display with LVGL must have 'double_buffer: false' (got '{double_buffer}'). "
+ "LVGL uses its own buffering strategy.",
+ path=[CONF_DOUBLE_BUFFER],
+ )
+ )
+
+ if errs:
+ raise cv.MultipleInvalid(errs)
+
+ return config
+
+
+FINAL_VALIDATE_SCHEMA = cv.Schema(_final_validate)
+
+
+CONFIG_SCHEMA = cv.All(
+ display.FULL_DISPLAY_SCHEMA.extend(
+ {
+ cv.GenerateID(): cv.declare_id(HUB75Display),
+ # Board preset (optional - provides default pin mappings)
+ cv.Optional(CONF_BOARD): cv.one_of(*BOARDS.keys(), lower=True),
+ # Panel dimensions
+ cv.Required(CONF_PANEL_WIDTH): cv.positive_int,
+ cv.Required(CONF_PANEL_HEIGHT): cv.positive_int,
+ # Multi-panel layout
+ cv.Optional(CONF_LAYOUT_ROWS): cv.positive_int,
+ cv.Optional(CONF_LAYOUT_COLS): cv.positive_int,
+ cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"),
+ # Panel hardware configuration
+ cv.Optional(CONF_SCAN_WIRING): cv.enum(
+ SCAN_PATTERNS, upper=True, space="_"
+ ),
+ cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True),
+ # Display configuration
+ cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean,
+ cv.Optional(CONF_BRIGHTNESS): cv.int_range(min=0, max=255),
+ cv.Optional(CONF_BIT_DEPTH): cv.int_range(min=6, max=12),
+ cv.Optional(CONF_GAMMA_CORRECT): cv.enum(
+ {"LINEAR": 0, "CIE1931": 1, "GAMMA_2_2": 2}, upper=True
+ ),
+ cv.Optional(CONF_MIN_REFRESH_RATE): cv.int_range(min=40, max=200),
+ # RGB data pins
+ cv.Optional(CONF_R1_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_G1_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_B1_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_R2_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_G2_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_B2_PIN): pins.gpio_output_pin_schema,
+ # Address pins
+ cv.Optional(CONF_A_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_B_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_C_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_D_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_E_PIN): pins.gpio_output_pin_schema,
+ # Control pins
+ cv.Optional(CONF_LAT_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema,
+ cv.Optional(CONF_CLK_PIN): pins.gpio_output_pin_schema,
+ # Timing configuration
+ cv.Optional(CONF_CLOCK_SPEED): cv.enum(CLOCK_SPEEDS, upper=True),
+ cv.Optional(CONF_LATCH_BLANKING): cv.positive_int,
+ cv.Optional(CONF_CLOCK_PHASE): cv.boolean,
+ }
+ ),
+ _merge_board_pins,
+ _validate_config,
+)
+
+
+DEFAULT_REFRESH_RATE = 60 # Hz
+
+
+def _calculate_min_refresh_rate(config: ConfigType) -> int:
+ """Calculate minimum refresh rate for the display.
+
+ Priority:
+ 1. Explicit min_refresh_rate setting (user override)
+ 2. Derived from update_interval (ms to Hz conversion)
+ 3. Default 60 Hz (for LVGL or unspecified interval)
+ """
+ if CONF_MIN_REFRESH_RATE in config:
+ return config[CONF_MIN_REFRESH_RATE]
+
+ update_interval = config.get(CONF_UPDATE_INTERVAL)
+ if update_interval is None:
+ return DEFAULT_REFRESH_RATE
+
+ # update_interval can be TimePeriod object or NEVER constant (int)
+ interval_ms = (
+ update_interval
+ if isinstance(update_interval, int)
+ else update_interval.total_milliseconds
+ )
+
+ # "never" or zero means external refresh (e.g., LVGL)
+ if interval_ms in (NEVER, 0):
+ return DEFAULT_REFRESH_RATE
+
+ # Convert ms interval to Hz, clamped to valid range [40, 200]
+ return max(40, min(200, int(round(1000 / interval_ms))))
+
+
+def _build_pins_struct(
+ pin_expressions: dict[str, Any], e_pin_num: int | cg.RawExpression
+) -> cg.StructInitializer:
+ """Build Hub75Pins struct from pin expressions."""
+
+ def pin_cast(pin):
+ return cg.RawExpression(f"static_cast({pin.get_pin()})")
+
+ return cg.StructInitializer(
+ Hub75Pins,
+ ("r1", pin_cast(pin_expressions["r1"])),
+ ("g1", pin_cast(pin_expressions["g1"])),
+ ("b1", pin_cast(pin_expressions["b1"])),
+ ("r2", pin_cast(pin_expressions["r2"])),
+ ("g2", pin_cast(pin_expressions["g2"])),
+ ("b2", pin_cast(pin_expressions["b2"])),
+ ("a", pin_cast(pin_expressions["a"])),
+ ("b", pin_cast(pin_expressions["b"])),
+ ("c", pin_cast(pin_expressions["c"])),
+ ("d", pin_cast(pin_expressions["d"])),
+ ("e", e_pin_num),
+ ("lat", pin_cast(pin_expressions["lat"])),
+ ("oe", pin_cast(pin_expressions["oe"])),
+ ("clk", pin_cast(pin_expressions["clk"])),
+ )
+
+
+def _append_config_fields(
+ config: ConfigType,
+ field_mapping: list[tuple[str, str]],
+ config_fields: list[tuple[str, Any]],
+) -> None:
+ """Append config fields from mapping if present in config."""
+ for conf_key, struct_field in field_mapping:
+ if conf_key in config:
+ config_fields.append((struct_field, config[conf_key]))
+
+
+def _build_config_struct(
+ config: ConfigType, pins_struct: cg.StructInitializer, min_refresh: int
+) -> cg.StructInitializer:
+ """Build Hub75Config struct from config.
+
+ Fields must be added in declaration order (see hub75_types.h) to satisfy
+ C++ designated initializer requirements. The order is:
+ 1. fields_before_pins (panel_width through layout)
+ 2. pins
+ 3. output_clock_speed
+ 4. min_refresh_rate
+ 5. fields_after_min_refresh (latch_blanking through brightness)
+ """
+ fields_before_pins = [
+ (CONF_PANEL_WIDTH, "panel_width"),
+ (CONF_PANEL_HEIGHT, "panel_height"),
+ # scan_pattern - auto-calculated, not set
+ (CONF_SCAN_WIRING, "scan_wiring"),
+ (CONF_SHIFT_DRIVER, "shift_driver"),
+ (CONF_LAYOUT_ROWS, "layout_rows"),
+ (CONF_LAYOUT_COLS, "layout_cols"),
+ (CONF_LAYOUT, "layout"),
+ ]
+ fields_after_min_refresh = [
+ (CONF_LATCH_BLANKING, "latch_blanking"),
+ (CONF_DOUBLE_BUFFER, "double_buffer"),
+ (CONF_CLOCK_PHASE, "clk_phase_inverted"),
+ (CONF_BRIGHTNESS, "brightness"),
+ ]
+
+ config_fields: list[tuple[str, Any]] = []
+
+ _append_config_fields(config, fields_before_pins, config_fields)
+
+ config_fields.append(("pins", pins_struct))
+
+ if CONF_CLOCK_SPEED in config:
+ config_fields.append(("output_clock_speed", config[CONF_CLOCK_SPEED]))
+
+ config_fields.append(("min_refresh_rate", min_refresh))
+
+ _append_config_fields(config, fields_after_min_refresh, config_fields)
+
+ return cg.StructInitializer(Hub75Config, *config_fields)
+
+
+async def to_code(config: ConfigType) -> None:
+ add_idf_component(
+ name="esphome/esp-hub75",
+ ref="0.1.6",
+ )
+
+ # Set compile-time configuration via defines
+ if CONF_BIT_DEPTH in config:
+ cg.add_define("HUB75_BIT_DEPTH", config[CONF_BIT_DEPTH])
+
+ if CONF_GAMMA_CORRECT in config:
+ cg.add_define("HUB75_GAMMA_MODE", config[CONF_GAMMA_CORRECT])
+
+ # Await all pin expressions
+ pin_expressions = {
+ "r1": await cg.gpio_pin_expression(config[CONF_R1_PIN]),
+ "g1": await cg.gpio_pin_expression(config[CONF_G1_PIN]),
+ "b1": await cg.gpio_pin_expression(config[CONF_B1_PIN]),
+ "r2": await cg.gpio_pin_expression(config[CONF_R2_PIN]),
+ "g2": await cg.gpio_pin_expression(config[CONF_G2_PIN]),
+ "b2": await cg.gpio_pin_expression(config[CONF_B2_PIN]),
+ "a": await cg.gpio_pin_expression(config[CONF_A_PIN]),
+ "b": await cg.gpio_pin_expression(config[CONF_B_PIN]),
+ "c": await cg.gpio_pin_expression(config[CONF_C_PIN]),
+ "d": await cg.gpio_pin_expression(config[CONF_D_PIN]),
+ "lat": await cg.gpio_pin_expression(config[CONF_LAT_PIN]),
+ "oe": await cg.gpio_pin_expression(config[CONF_OE_PIN]),
+ "clk": await cg.gpio_pin_expression(config[CONF_CLK_PIN]),
+ }
+
+ # E pin is optional
+ if CONF_E_PIN in config:
+ e_pin = await cg.gpio_pin_expression(config[CONF_E_PIN])
+ e_pin_num = cg.RawExpression(f"static_cast({e_pin.get_pin()})")
+ else:
+ e_pin_num = -1
+
+ # Build structs
+ min_refresh = _calculate_min_refresh_rate(config)
+ pins_struct = _build_pins_struct(pin_expressions, e_pin_num)
+ hub75_config = _build_config_struct(config, pins_struct, min_refresh)
+
+ # Create display and register
+ var = cg.new_Pvariable(config[CONF_ID], hub75_config)
+ await display.register_display(var, config)
+
+ if CONF_LAMBDA in config:
+ lambda_ = await cg.process_lambda(
+ config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
+ )
+ cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/hub75/hub75.cpp b/esphome/components/hub75/hub75.cpp
new file mode 100644
index 0000000000..e023e446c4
--- /dev/null
+++ b/esphome/components/hub75/hub75.cpp
@@ -0,0 +1,192 @@
+#include "hub75_component.h"
+#include "esphome/core/application.h"
+
+#ifdef USE_ESP32
+
+namespace esphome::hub75 {
+
+static const char *const TAG = "hub75";
+
+// ========================================
+// Constructor
+// ========================================
+
+HUB75Display::HUB75Display(const Hub75Config &config) : config_(config) {
+ // Initialize runtime state from config
+ this->brightness_ = config.brightness;
+ this->enabled_ = (config.brightness > 0);
+}
+
+// ========================================
+// Core Component methods
+// ========================================
+
+void HUB75Display::setup() {
+ ESP_LOGCONFIG(TAG, "Setting up HUB75Display...");
+
+ // Create driver with pre-configured config
+ driver_ = new Hub75Driver(config_);
+ if (!driver_->begin()) {
+ ESP_LOGE(TAG, "Failed to initialize HUB75 driver!");
+ return;
+ }
+
+ this->enabled_ = true;
+}
+
+void HUB75Display::dump_config() {
+ LOG_DISPLAY("", "HUB75", this);
+
+ ESP_LOGCONFIG(TAG,
+ " Panel: %dx%d pixels\n"
+ " Layout: %dx%d panels\n"
+ " Virtual Display: %dx%d pixels",
+ config_.panel_width, config_.panel_height, config_.layout_cols, config_.layout_rows,
+ config_.panel_width * config_.layout_cols, config_.panel_height * config_.layout_rows);
+
+ ESP_LOGCONFIG(TAG,
+ " Scan Wiring: %d\n"
+ " Shift Driver: %d",
+ static_cast(config_.scan_wiring), static_cast(config_.shift_driver));
+
+ ESP_LOGCONFIG(TAG,
+ " Pins: R1:%i, G1:%i, B1:%i, R2:%i, G2:%i, B2:%i\n"
+ " Pins: A:%i, B:%i, C:%i, D:%i, E:%i\n"
+ " Pins: LAT:%i, OE:%i, CLK:%i",
+ config_.pins.r1, config_.pins.g1, config_.pins.b1, config_.pins.r2, config_.pins.g2, config_.pins.b2,
+ config_.pins.a, config_.pins.b, config_.pins.c, config_.pins.d, config_.pins.e, config_.pins.lat,
+ config_.pins.oe, config_.pins.clk);
+
+ ESP_LOGCONFIG(TAG,
+ " Clock Speed: %u MHz\n"
+ " Latch Blanking: %i\n"
+ " Clock Phase: %s\n"
+ " Min Refresh Rate: %i Hz\n"
+ " Bit Depth: %i\n"
+ " Double Buffer: %s",
+ static_cast(config_.output_clock_speed) / 1000000, config_.latch_blanking,
+ TRUEFALSE(config_.clk_phase_inverted), config_.min_refresh_rate, HUB75_BIT_DEPTH,
+ YESNO(config_.double_buffer));
+}
+
+// ========================================
+// Display/PollingComponent methods
+// ========================================
+
+void HUB75Display::update() {
+ if (!driver_) [[unlikely]]
+ return;
+ if (!this->enabled_) [[unlikely]]
+ return;
+
+ this->do_update_();
+
+ if (config_.double_buffer) {
+ driver_->flip_buffer();
+ }
+}
+
+void HUB75Display::fill(Color color) {
+ if (!driver_) [[unlikely]]
+ return;
+ if (!this->enabled_) [[unlikely]]
+ return;
+
+ // Special case: black (off) - use fast hardware clear
+ if (!color.is_on()) {
+ driver_->clear();
+ return;
+ }
+
+ // For non-black colors, fall back to base class (pixel-by-pixel)
+ Display::fill(color);
+}
+
+void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) {
+ if (!driver_) [[unlikely]]
+ return;
+ if (!this->enabled_) [[unlikely]]
+ return;
+
+ if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]]
+ return;
+
+ driver_->set_pixel(x, y, color.r, color.g, color.b);
+ App.feed_wdt();
+}
+
+void HOT HUB75Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
+ ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
+ if (!driver_) [[unlikely]]
+ return;
+ if (!this->enabled_) [[unlikely]]
+ return;
+
+ // Map ESPHome enums to hub75 enums
+ Hub75PixelFormat format;
+ Hub75ColorOrder color_order = Hub75ColorOrder::RGB;
+ int bytes_per_pixel;
+
+ // Determine format based on bitness
+ if (bitness == ColorBitness::COLOR_BITNESS_565) {
+ format = Hub75PixelFormat::RGB565;
+ bytes_per_pixel = 2;
+ } else if (bitness == ColorBitness::COLOR_BITNESS_888) {
+#ifdef USE_LVGL
+#if LV_COLOR_DEPTH == 32
+ // 32-bit: 4 bytes per pixel with padding byte (LVGL mode)
+ format = Hub75PixelFormat::RGB888_32;
+ bytes_per_pixel = 4;
+
+ // Map ESPHome ColorOrder to Hub75ColorOrder
+ // ESPHome ColorOrder is typically BGR for little-endian 32-bit
+ color_order = (order == ColorOrder::COLOR_ORDER_RGB) ? Hub75ColorOrder::RGB : Hub75ColorOrder::BGR;
+#elif LV_COLOR_DEPTH == 24
+ // 24-bit: 3 bytes per pixel, tightly packed
+ format = Hub75PixelFormat::RGB888;
+ bytes_per_pixel = 3;
+ // Note: 24-bit is always RGB order in LVGL
+#else
+ ESP_LOGE(TAG, "Unsupported LV_COLOR_DEPTH: %d", LV_COLOR_DEPTH);
+ return;
+#endif
+#else
+ // Non-LVGL mode: standard 24-bit RGB888
+ format = Hub75PixelFormat::RGB888;
+ bytes_per_pixel = 3;
+ color_order = (order == ColorOrder::COLOR_ORDER_RGB) ? Hub75ColorOrder::RGB : Hub75ColorOrder::BGR;
+#endif
+ } else {
+ ESP_LOGE(TAG, "Unsupported bitness: %d", static_cast(bitness));
+ return;
+ }
+
+ // Check if buffer is tightly packed (no stride)
+ const int stride_px = x_offset + w + x_pad;
+ const bool is_packed = (x_offset == 0 && x_pad == 0 && y_offset == 0);
+
+ if (is_packed) {
+ // Tightly packed buffer - single bulk call for best performance
+ driver_->draw_pixels(x_start, y_start, w, h, ptr, format, color_order, big_endian);
+ } else {
+ // Buffer has stride (padding between rows) - draw row by row
+ for (int yy = 0; yy < h; ++yy) {
+ const size_t row_offset = ((y_offset + yy) * stride_px + x_offset) * bytes_per_pixel;
+ const uint8_t *row_ptr = ptr + row_offset;
+
+ driver_->draw_pixels(x_start, y_start + yy, w, 1, row_ptr, format, color_order, big_endian);
+ }
+ }
+}
+
+void HUB75Display::set_brightness(int brightness) {
+ this->brightness_ = brightness;
+ this->enabled_ = (brightness > 0);
+ if (this->driver_ != nullptr) {
+ this->driver_->set_brightness(brightness);
+ }
+}
+
+} // namespace esphome::hub75
+
+#endif
diff --git a/esphome/components/hub75/hub75_component.h b/esphome/components/hub75/hub75_component.h
new file mode 100644
index 0000000000..49d4274483
--- /dev/null
+++ b/esphome/components/hub75/hub75_component.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#ifdef USE_ESP32
+
+#include
+
+#include "esphome/components/display/display_buffer.h"
+#include "esphome/core/component.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/log.h"
+#include "hub75.h" // hub75 library
+
+namespace esphome::hub75 {
+
+using esphome::display::ColorBitness;
+using esphome::display::ColorOrder;
+
+class HUB75Display : public display::Display {
+ public:
+ // Constructor accepting config
+ explicit HUB75Display(const Hub75Config &config);
+
+ // Core Component methods
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::PROCESSOR; }
+
+ // Display/PollingComponent methods
+ void update() override;
+ display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
+ void fill(Color color) override;
+ void draw_pixel_at(int x, int y, Color color) override;
+ void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
+ display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
+
+ // Brightness control (runtime mutable)
+ void set_brightness(int brightness);
+
+ protected:
+ // Display internal methods
+ int get_width_internal() override { return config_.panel_width * config_.layout_cols; }
+ int get_height_internal() override { return config_.panel_height * config_.layout_rows; }
+
+ // Member variables
+ Hub75Driver *driver_{nullptr};
+ Hub75Config config_; // Immutable configuration
+
+ // Runtime state (mutable)
+ int brightness_{128};
+ bool enabled_{false};
+};
+
+} // namespace esphome::hub75
+
+#endif
diff --git a/platformio.ini b/platformio.ini
index 81f8b3295b..9095d27af8 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -152,6 +152,7 @@ lib_deps =
esphome/ESP32-audioI2S@2.3.0 ; i2s_audio
droscy/esp_wireguard@0.4.2 ; wireguard
esphome/esp-audio-libs@2.0.1 ; audio
+ esphome/esp-hub75@0.1.6 ; hub75
build_flags =
${common:arduino.build_flags}
@@ -175,6 +176,7 @@ lib_deps =
droscy/esp_wireguard@0.4.2 ; wireguard
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
esphome/esp-audio-libs@2.0.1 ; audio
+ esphome/esp-hub75@0.1.6 ; hub75
build_flags =
${common:idf.build_flags}
-Wno-nonnull-compare
diff --git a/tests/components/hub75/test.esp32-idf.yaml b/tests/components/hub75/test.esp32-idf.yaml
new file mode 100644
index 0000000000..c275d24187
--- /dev/null
+++ b/tests/components/hub75/test.esp32-idf.yaml
@@ -0,0 +1,39 @@
+esp32:
+ board: esp32dev
+ framework:
+ type: esp-idf
+
+display:
+ - platform: hub75
+ id: my_hub75
+ panel_width: 64
+ panel_height: 32
+ double_buffer: true
+ brightness: 128
+ r1_pin: GPIO25
+ g1_pin: GPIO26
+ b1_pin: GPIO27
+ r2_pin: GPIO14
+ g2_pin: GPIO12
+ b2_pin: GPIO13
+ a_pin: GPIO23
+ b_pin: GPIO19
+ c_pin: GPIO5
+ d_pin: GPIO17
+ e_pin: GPIO21
+ lat_pin: GPIO4
+ oe_pin: GPIO15
+ clk_pin: GPIO16
+ pages:
+ - id: page1
+ lambda: |-
+ it.rectangle(0, 0, it.get_width(), it.get_height());
+ - id: page2
+ lambda: |-
+ it.rectangle(0, 0, it.get_width(), it.get_height());
+ on_page_change:
+ from: page1
+ to: page2
+ then:
+ lambda: |-
+ ESP_LOGD("display", "1 -> 2");
diff --git a/tests/components/hub75/test.esp32-s3-idf-board.yaml b/tests/components/hub75/test.esp32-s3-idf-board.yaml
new file mode 100644
index 0000000000..9568ccf3aa
--- /dev/null
+++ b/tests/components/hub75/test.esp32-s3-idf-board.yaml
@@ -0,0 +1,26 @@
+esp32:
+ board: esp32-s3-devkitc-1
+ framework:
+ type: esp-idf
+
+display:
+ - platform: hub75
+ id: hub75_display_board
+ board: adafruit-matrix-portal-s3
+ panel_width: 64
+ panel_height: 32
+ double_buffer: true
+ brightness: 128
+ pages:
+ - id: page1
+ lambda: |-
+ it.rectangle(0, 0, it.get_width(), it.get_height());
+ - id: page2
+ lambda: |-
+ it.rectangle(0, 0, it.get_width(), it.get_height());
+ on_page_change:
+ from: page1
+ to: page2
+ then:
+ lambda: |-
+ ESP_LOGD("display", "1 -> 2");
diff --git a/tests/components/hub75/test.esp32-s3-idf.yaml b/tests/components/hub75/test.esp32-s3-idf.yaml
new file mode 100644
index 0000000000..db678c98a4
--- /dev/null
+++ b/tests/components/hub75/test.esp32-s3-idf.yaml
@@ -0,0 +1,39 @@
+esp32:
+ board: esp32-s3-devkitc-1
+ framework:
+ type: esp-idf
+
+display:
+ - platform: hub75
+ id: my_hub75
+ panel_width: 64
+ panel_height: 32
+ double_buffer: true
+ brightness: 128
+ r1_pin: GPIO42
+ g1_pin: GPIO41
+ b1_pin: GPIO40
+ r2_pin: GPIO38
+ g2_pin: GPIO39
+ b2_pin: GPIO37
+ a_pin: GPIO45
+ b_pin: GPIO36
+ c_pin: GPIO48
+ d_pin: GPIO35
+ e_pin: GPIO21
+ lat_pin: GPIO47
+ oe_pin: GPIO14
+ clk_pin: GPIO2
+ pages:
+ - id: page1
+ lambda: |-
+ it.rectangle(0, 0, it.get_width(), it.get_height());
+ - id: page2
+ lambda: |-
+ it.rectangle(0, 0, it.get_width(), it.get_height());
+ on_page_change:
+ from: page1
+ to: page2
+ then:
+ lambda: |-
+ ESP_LOGD("display", "1 -> 2");
From 1fa7adbe8dd63e33c334a57179b6730ecc13e8d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ludovic=20BOU=C3=89?=
Date: Fri, 5 Dec 2025 21:24:57 +0100
Subject: [PATCH 09/12] [mipi_spi] Add M5CORE2 model (#12301)
---
esphome/components/mipi_spi/models/ili.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/esphome/components/mipi_spi/models/ili.py b/esphome/components/mipi_spi/models/ili.py
index 0102c0f665..60a25c32a9 100644
--- a/esphome/components/mipi_spi/models/ili.py
+++ b/esphome/components/mipi_spi/models/ili.py
@@ -148,6 +148,19 @@ ILI9341 = DriverChip(
),
),
)
+# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation
+ILI9341.extend(
+ "M5CORE2",
+ width=320,
+ height=240,
+ mirror_x=False,
+ cs_pin=5,
+ dc_pin=15,
+ invert_colors=True,
+ pixel_mode="18bit",
+ data_rate="40MHz",
+)
+
DriverChip(
"ILI9481",
mirror_x=True,
From bbb71b5359e459b6a4091ab95704b628d4c32802 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 5 Dec 2025 15:16:55 -0600
Subject: [PATCH 10/12] Bump peter-evans/create-pull-request from 7.0.9 to
7.0.11 (#12303)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/sync-device-classes.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml
index ea81a1e013..2c3219e38e 100644
--- a/.github/workflows/sync-device-classes.yml
+++ b/.github/workflows/sync-device-classes.yml
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
+ uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot
From 10b54df77194eea0f75116d76eeeff6fa6edc1e3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 5 Dec 2025 15:17:10 -0600
Subject: [PATCH 11/12] Bump github/codeql-action from 4.31.6 to 4.31.7
(#12304)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/codeql.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index d9b6bcdcca..481ad0ec34 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
+ uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
+ uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
with:
category: "/language:${{matrix.language}}"
From a517e0ec80a3b8d8b82e57e53c92ebfc1f61e7a1 Mon Sep 17 00:00:00 2001
From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Date: Fri, 5 Dec 2025 16:28:24 -0500
Subject: [PATCH 12/12] [esp32] Add missing variant support (#12305)
Co-authored-by: Claude
---
esphome/components/deep_sleep/__init__.py | 7 +++++++
esphome/components/esp32_can/canbus.py | 3 +++
esphome/components/ethernet/__init__.py | 12 +++++++++++-
esphome/components/ethernet/ethernet_component.cpp | 4 ++--
esphome/components/spi/__init__.py | 2 ++
5 files changed, 25 insertions(+), 3 deletions(-)
diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py
index fa3ea449e2..8849fad7d6 100644
--- a/esphome/components/deep_sleep/__init__.py
+++ b/esphome/components/deep_sleep/__init__.py
@@ -5,9 +5,11 @@ from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
+ VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
+ VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
@@ -55,9 +57,11 @@ WAKEUP_PINS = {
],
VARIANT_ESP32C2: [0, 1, 2, 3, 4, 5],
VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5],
+ VARIANT_ESP32C5: [0, 1, 2, 3, 4, 5, 6, 7],
VARIANT_ESP32C6: [0, 1, 2, 3, 4, 5, 6, 7],
VARIANT_ESP32C61: [0, 1, 2, 3, 4, 5, 6],
VARIANT_ESP32H2: [7, 8, 9, 10, 11, 12, 13, 14],
+ VARIANT_ESP32P4: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
VARIANT_ESP32S2: [
0,
1,
@@ -124,9 +128,11 @@ def _validate_ex1_wakeup_mode(value):
if value == "ANY_LOW":
esp32.only_on_variant(
supported=[
+ VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
+ VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
],
@@ -221,6 +227,7 @@ CONFIG_SCHEMA = cv.All(
unsupported=[
VARIANT_ESP32C2,
VARIANT_ESP32C3,
+ VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py
index 000ef303fe..0899a0dc2b 100644
--- a/esphome/components/esp32_can/canbus.py
+++ b/esphome/components/esp32_can/canbus.py
@@ -7,6 +7,7 @@ from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C3,
+ VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
@@ -59,6 +60,7 @@ CAN_SPEEDS_ESP32_S2 = {
CAN_SPEEDS_ESP32_S3 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_C3 = {**CAN_SPEEDS_ESP32_S2}
+CAN_SPEEDS_ESP32_C5 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_C6 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_C61 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS_ESP32_H2 = {**CAN_SPEEDS_ESP32_S2}
@@ -67,6 +69,7 @@ CAN_SPEEDS_ESP32_P4 = {**CAN_SPEEDS_ESP32_S2}
CAN_SPEEDS = {
VARIANT_ESP32: CAN_SPEEDS_ESP32,
VARIANT_ESP32C3: CAN_SPEEDS_ESP32_C3,
+ VARIANT_ESP32C5: CAN_SPEEDS_ESP32_C5,
VARIANT_ESP32C6: CAN_SPEEDS_ESP32_C6,
VARIANT_ESP32C61: CAN_SPEEDS_ESP32_C61,
VARIANT_ESP32H2: CAN_SPEEDS_ESP32_H2,
diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py
index 39af1ff4b9..b4b1fcd9f6 100644
--- a/esphome/components/ethernet/__init__.py
+++ b/esphome/components/ethernet/__init__.py
@@ -5,6 +5,9 @@ import esphome.codegen as cg
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C3,
+ VARIANT_ESP32C5,
+ VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
@@ -301,7 +304,14 @@ def _final_validate_spi(config):
return
if spi_configs := fv.full_config.get().get(CONF_SPI):
variant = get_esp32_variant()
- if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3):
+ if variant in (
+ VARIANT_ESP32C3,
+ VARIANT_ESP32C5,
+ VARIANT_ESP32C6,
+ VARIANT_ESP32C61,
+ VARIANT_ESP32S2,
+ VARIANT_ESP32S3,
+ ):
spi_host = "SPI2_HOST"
else:
spi_host = "SPI3_HOST"
diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp
index 757e358db3..793ebdec42 100644
--- a/esphome/components/ethernet/ethernet_component.cpp
+++ b/esphome/components/ethernet/ethernet_component.cpp
@@ -87,8 +87,8 @@ void EthernetComponent::setup() {
.intr_flags = 0,
};
-#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \
- defined(USE_ESP32_VARIANT_ESP32S3)
+#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \
+ defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
auto host = SPI2_HOST;
#else
auto host = SPI3_HOST;
diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py
index 0b531b9ed6..88bb3406e1 100644
--- a/esphome/components/spi/__init__.py
+++ b/esphome/components/spi/__init__.py
@@ -7,6 +7,7 @@ from esphome.components.esp32 import (
KEY_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
+ VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
@@ -129,6 +130,7 @@ def get_hw_interface_list():
if get_target_variant() in [
VARIANT_ESP32C2,
VARIANT_ESP32C3,
+ VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,