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/.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}}" 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 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/README.md b/README.md index 0439b1bc06..b8ce8d091d 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ - - ESPHome Logo + + ESPHome Logo diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 8f751c496e..96c8334a6d 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1,15 +1,17 @@ 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_ESP32C61, 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 @@ -99,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, @@ -174,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/api/api_connection.h b/esphome/components/api/api_connection.h index 4dad222ab4..6bf4f45a5c 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -506,7 +506,7 @@ class APIConnection final : public APIServerConnection { class MessageCreator { public: MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } - MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; } + explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; } // Call operator - uses message_type to determine union type uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, 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..8849fad7d6 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,15 +1,18 @@ 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, + VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + get_esp32_variant, ) from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv @@ -54,8 +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, @@ -122,8 +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, ], @@ -218,7 +227,9 @@ CONFIG_SCHEMA = cv.All( unsupported=[ VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C5, 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 c6d0100673..04f2adeed3 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 5cee27506a..0899a0dc2b 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -4,15 +4,17 @@ 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_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import ( @@ -58,14 +60,18 @@ 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} 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, 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/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..b4b1fcd9f6 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -3,16 +3,17 @@ 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_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, 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 @@ -303,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/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/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 5707c1348d..67fda7a795 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -10,6 +10,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32C3, VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, @@ -75,6 +76,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/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/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/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). diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index c81ade8fc3..fb0ce92cc9 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -3,17 +3,19 @@ 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, VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, 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 ( @@ -104,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/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/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, 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_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}; 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..88bb3406e1 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -3,16 +3,18 @@ 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, + VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, 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 @@ -128,7 +130,9 @@ def get_hw_interface_list(): if get_target_variant() in [ VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, ]: return [["spi", "spi2"]] 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/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/esphome/core/defines.h b/esphome/core/defines.h index 4a0a7a7a21..0e3cd3ab86 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -235,8 +235,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 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/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/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"); 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 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,