1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-14 22:05:54 +00:00

Merge branch 'integration' into memory_api

This commit is contained in:
J. Nick Koston
2025-11-03 21:03:49 -06:00
14 changed files with 349 additions and 1 deletions

View File

@@ -480,6 +480,7 @@ esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse
esphome/components/thermostat/* @kbx81
esphome/components/time/* @esphome/core
esphome/components/tinyusb/* @kbx81
esphome/components/tlc5947/* @rnauber
esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12

View File

@@ -0,0 +1,60 @@
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 (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@kbx81"]
CONFLICTS_WITH = ["usb_host"]
CONF_USB_LANG_ID = "usb_lang_id"
CONF_USB_MANUFACTURER_STR = "usb_manufacturer_str"
CONF_USB_PRODUCT_ID = "usb_product_id"
CONF_USB_PRODUCT_STR = "usb_product_str"
CONF_USB_SERIAL_STR = "usb_serial_str"
CONF_USB_VENDOR_ID = "usb_vendor_id"
tinyusb_ns = cg.esphome_ns.namespace("tinyusb")
TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(TinyUSB),
cv.Optional(CONF_USB_PRODUCT_ID, default=0x4001): cv.uint16_t,
cv.Optional(CONF_USB_VENDOR_ID, default=0x303A): cv.uint16_t,
cv.Optional(CONF_USB_LANG_ID, default=0x0409): cv.uint16_t,
cv.Optional(CONF_USB_MANUFACTURER_STR, default="ESPHome"): cv.string,
cv.Optional(CONF_USB_PRODUCT_STR, default="ESPHome"): cv.string,
cv.Optional(CONF_USB_SERIAL_STR, default=""): cv.string,
}
).extend(cv.COMPONENT_SCHEMA),
esp32.only_on_variant(
supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3],
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Set USB device descriptor properties
cg.add(var.set_usb_desc_product_id(config[CONF_USB_PRODUCT_ID]))
cg.add(var.set_usb_desc_vendor_id(config[CONF_USB_VENDOR_ID]))
cg.add(var.set_usb_desc_lang_id(config[CONF_USB_LANG_ID]))
cg.add(var.set_usb_desc_manufacturer(config[CONF_USB_MANUFACTURER_STR]))
cg.add(var.set_usb_desc_product(config[CONF_USB_PRODUCT_STR]))
if config[CONF_USB_SERIAL_STR]:
cg.add(var.set_usb_desc_serial(config[CONF_USB_SERIAL_STR]))
add_idf_component(name="espressif/esp_tinyusb", ref="1.7.6~1")
add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID", False)
add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_DEFAULT_PID", False)
add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_BCD_DEVICE", 0x0100)

View File

@@ -0,0 +1,44 @@
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "tinyusb_component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::tinyusb {
static const char *TAG = "tinyusb";
void TinyUSB::setup() {
// Use the device's MAC address as its serial number if no serial number is defined
if (this->string_descriptor_[SERIAL_NUMBER] == nullptr) {
static char mac_addr_buf[13];
get_mac_address_into_buffer(mac_addr_buf);
this->string_descriptor_[SERIAL_NUMBER] = mac_addr_buf;
}
this->tusb_cfg_ = {
.descriptor = &this->usb_descriptor_,
.string_descriptor = this->string_descriptor_,
.string_descriptor_count = SIZE,
.external_phy = false,
};
esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_);
if (result != ESP_OK) {
this->mark_failed();
}
}
void TinyUSB::dump_config() {
ESP_LOGCONFIG(TAG,
"TinyUSB:\n"
" Product ID: 0x%04X\n"
" Vendor ID: 0x%04X\n"
" Manufacturer: '%s'\n"
" Product: '%s'\n"
" Serial: '%s'\n",
this->usb_descriptor_.idProduct, this->usb_descriptor_.idVendor, this->string_descriptor_[MANUFACTURER],
this->string_descriptor_[PRODUCT], this->string_descriptor_[SERIAL_NUMBER]);
}
} // namespace esphome::tinyusb
#endif

View File

@@ -0,0 +1,72 @@
#pragma once
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "esphome/core/component.h"
#include "tinyusb.h"
#include "tusb.h"
namespace esphome::tinyusb {
enum USBDStringDescriptor : uint8_t {
LANGUAGE_ID = 0,
MANUFACTURER = 1,
PRODUCT = 2,
SERIAL_NUMBER = 3,
INTERFACE = 4,
TERMINATOR = 5,
SIZE = 6,
};
static const char *DEFAULT_USB_STR = "ESPHome";
class TinyUSB : public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void set_usb_desc_product_id(uint16_t product_id) { this->usb_descriptor_.idProduct = product_id; }
void set_usb_desc_vendor_id(uint16_t vendor_id) { this->usb_descriptor_.idVendor = vendor_id; }
void set_usb_desc_lang_id(uint16_t lang_id) {
this->usb_desc_lang_id_[0] = lang_id & 0xFF;
this->usb_desc_lang_id_[1] = lang_id >> 8;
}
void set_usb_desc_manufacturer(const char *usb_desc_manufacturer) {
this->string_descriptor_[MANUFACTURER] = usb_desc_manufacturer;
}
void set_usb_desc_product(const char *usb_desc_product) { this->string_descriptor_[PRODUCT] = usb_desc_product; }
void set_usb_desc_serial(const char *usb_desc_serial) { this->string_descriptor_[SERIAL_NUMBER] = usb_desc_serial; }
protected:
char usb_desc_lang_id_[2] = {0x09, 0x04}; // defaults to english
const char *string_descriptor_[SIZE] = {
this->usb_desc_lang_id_, // 0: supported language is English (0x0409)
DEFAULT_USB_STR, // 1: Manufacturer
DEFAULT_USB_STR, // 2: Product
nullptr, // 3: Serial Number
nullptr, // 4: Interface
nullptr, // 5: Terminator
};
tinyusb_config_t tusb_cfg_{};
tusb_desc_device_t usb_descriptor_{
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A,
.idProduct = 0x4001,
.bcdDevice = CONFIG_TINYUSB_DESC_BCD_DEVICE,
.iManufacturer = 1,
.iProduct = 2,
.iSerialNumber = 3,
.bNumConfigurations = 1,
};
};
} // namespace esphome::tinyusb
#endif

View File

@@ -641,6 +641,12 @@ std::string get_mac_address_pretty() {
return format_mac_address_pretty(mac);
}
void get_mac_address_into_buffer(std::span<char, 13> buf) {
uint8_t mac[6];
get_mac_address_raw(mac);
format_mac_addr_lower_no_sep(mac, buf.data());
}
#ifndef USE_ESP32
bool has_custom_mac_address() { return false; }
#endif

View File

@@ -8,6 +8,7 @@
#include <iterator>
#include <limits>
#include <memory>
#include <span>
#include <string>
#include <type_traits>
#include <vector>
@@ -1030,6 +1031,10 @@ std::string get_mac_address();
/// Get the device MAC address as a string, in colon-separated uppercase hex notation.
std::string get_mac_address_pretty();
/// Get the device MAC address into the given buffer, in lowercase hex notation.
/// Assumes buffer length is 13 (12 digits for hexadecimal representation followed by null terminator).
void get_mac_address_into_buffer(std::span<char, 13> buf);
#ifdef USE_ESP32
/// Set the MAC address to use from the provided byte array (6 bytes).
void set_mac_address(uint8_t *mac);

View File

@@ -84,6 +84,9 @@ struct ESPTime {
*/
static ESPTime from_epoch_local(time_t epoch) {
struct tm *c_tm = ::localtime(&epoch);
if (c_tm == nullptr) {
return ESPTime{}; // Return an invalid ESPTime
}
return ESPTime::from_c_tm(c_tm, epoch);
}
/** Convert an UTC epoch timestamp to a UTC time ESPTime instance.
@@ -93,6 +96,9 @@ struct ESPTime {
*/
static ESPTime from_epoch_utc(time_t epoch) {
struct tm *c_tm = ::gmtime(&epoch);
if (c_tm == nullptr) {
return ESPTime{}; // Return an invalid ESPTime
}
return ESPTime::from_c_tm(c_tm, epoch);
}

View File

@@ -23,3 +23,7 @@ dependencies:
version: "2.0.0"
rules:
- if: "target in [esp32, esp32p4]"
espressif/esp_tinyusb:
version: "1.7.6~1"
rules:
- if: "target in [esp32s2, esp32s3, esp32p4]"

View File

@@ -94,6 +94,22 @@ class Platform(StrEnum):
MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core changes
MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform
# Platform-specific components that can only be built on their respective platforms
# These components contain platform-specific code and cannot be cross-compiled
# Regular components (wifi, logger, api, etc.) are cross-platform and not listed here
PLATFORM_SPECIFIC_COMPONENTS = frozenset(
{
"esp32", # ESP32 platform implementation
"esp8266", # ESP8266 platform implementation
"rp2040", # Raspberry Pi Pico / RP2040 platform implementation
"bk72xx", # Beken BK72xx platform implementation (uses LibreTiny)
"rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny)
"ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny)
"host", # Host platform (for testing on development machine)
"nrf52", # Nordic nRF52 platform implementation
}
)
# Platform preference order for memory impact analysis
# This order is used when no platform-specific hints are detected from filenames
# Priority rationale:
@@ -568,6 +584,20 @@ def detect_memory_impact_config(
)
platform = _select_platform_by_count(platform_counts)
# Filter out platform-specific components that are incompatible with selected platform
# Platform components (esp32, esp8266, rp2040, etc.) can only build on their own platform
# Other components (wifi, logger, etc.) are cross-platform and can build anywhere
compatible_components = [
component
for component in components_with_tests
if component not in PLATFORM_SPECIFIC_COMPONENTS
or platform in component_platforms_map.get(component, set())
]
# If no components are compatible with the selected platform, don't run
if not compatible_components:
return {"should_run": "false"}
# Debug output
print("Memory impact analysis:", file=sys.stderr)
print(f" Changed components: {sorted(changed_component_set)}", file=sys.stderr)
@@ -579,10 +609,11 @@ def detect_memory_impact_config(
print(f" Platform hints from filenames: {platform_hints}", file=sys.stderr)
print(f" Common platforms: {sorted(common_platforms)}", file=sys.stderr)
print(f" Selected platform: {platform}", file=sys.stderr)
print(f" Compatible components: {compatible_components}", file=sys.stderr)
return {
"should_run": "true",
"components": components_with_tests,
"components": compatible_components,
"platform": platform,
"use_merged_config": "true",
}

View File

@@ -0,0 +1,8 @@
tinyusb:
id: tinyusb_test
usb_lang_id: 0x0123
usb_manufacturer_str: ESPHomeTestManufacturer
usb_product_id: 0x1234
usb_product_str: ESPHomeTestProduct
usb_serial_str: ESPHomeTestSerialNumber
usb_vendor_id: 0x2345

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -1130,3 +1130,111 @@ def test_main_core_files_changed_still_detects_components(
assert "select" in output["changed_components"]
assert "api" in output["changed_components"]
assert len(output["changed_components"]) > 0
def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266(
tmp_path: Path,
) -> None:
"""Test that ESP32 components are filtered out when ESP8266 platform is selected.
This test verifies the fix for the issue where ESP32 components were being included
when ESP8266 was selected as the platform, causing build failures in PR 10387.
"""
# Create test directory structure
tests_dir = tmp_path / "tests" / "components"
# esp32 component only has esp32-idf tests (NOT compatible with esp8266)
esp32_dir = tests_dir / "esp32"
esp32_dir.mkdir(parents=True)
(esp32_dir / "test.esp32-idf.yaml").write_text("test: esp32")
(esp32_dir / "test.esp32-s3-idf.yaml").write_text("test: esp32")
# esp8266 component only has esp8266-ard test (NOT compatible with esp32)
esp8266_dir = tests_dir / "esp8266"
esp8266_dir.mkdir(parents=True)
(esp8266_dir / "test.esp8266-ard.yaml").write_text("test: esp8266")
# Mock changed_files to return both esp32 and esp8266 component changes
# Include esp8266-specific filename to trigger esp8266 platform hint
with (
patch.object(determine_jobs, "root_path", str(tmp_path)),
patch.object(helpers, "root_path", str(tmp_path)),
patch.object(determine_jobs, "changed_files") as mock_changed_files,
):
mock_changed_files.return_value = [
"tests/components/esp32/common.yaml",
"tests/components/esp8266/test.esp8266-ard.yaml",
"esphome/core/helpers_esp8266.h", # ESP8266-specific file to hint platform
]
determine_jobs._component_has_tests.cache_clear()
result = determine_jobs.detect_memory_impact_config()
# Memory impact should run
assert result["should_run"] == "true"
# Platform should be esp8266-ard (due to ESP8266 filename hint)
assert result["platform"] == "esp8266-ard"
# CRITICAL: Only esp8266 component should be included, not esp32
# This prevents trying to build ESP32 components on ESP8266 platform
assert result["components"] == ["esp8266"], (
"When esp8266-ard platform is selected, only esp8266 component should be included, "
"not esp32. This prevents trying to build ESP32 components on ESP8266 platform."
)
assert result["use_merged_config"] == "true"
def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32(
tmp_path: Path,
) -> None:
"""Test that ESP8266 components are filtered out when ESP32 platform is selected.
This is the inverse of the ESP8266 test - ensures filtering works both ways.
"""
# Create test directory structure
tests_dir = tmp_path / "tests" / "components"
# esp32 component only has esp32-idf tests (NOT compatible with esp8266)
esp32_dir = tests_dir / "esp32"
esp32_dir.mkdir(parents=True)
(esp32_dir / "test.esp32-idf.yaml").write_text("test: esp32")
(esp32_dir / "test.esp32-s3-idf.yaml").write_text("test: esp32")
# esp8266 component only has esp8266-ard test (NOT compatible with esp32)
esp8266_dir = tests_dir / "esp8266"
esp8266_dir.mkdir(parents=True)
(esp8266_dir / "test.esp8266-ard.yaml").write_text("test: esp8266")
# Mock changed_files to return both esp32 and esp8266 component changes
# Include MORE esp32-specific filenames to ensure esp32-idf wins the hint count
with (
patch.object(determine_jobs, "root_path", str(tmp_path)),
patch.object(helpers, "root_path", str(tmp_path)),
patch.object(determine_jobs, "changed_files") as mock_changed_files,
):
mock_changed_files.return_value = [
"tests/components/esp32/common.yaml",
"tests/components/esp8266/test.esp8266-ard.yaml",
"esphome/components/wifi/wifi_component_esp_idf.cpp", # ESP-IDF hint
"esphome/components/ethernet/ethernet_esp32.cpp", # ESP32 hint
]
determine_jobs._component_has_tests.cache_clear()
result = determine_jobs.detect_memory_impact_config()
# Memory impact should run
assert result["should_run"] == "true"
# Platform should be esp32-idf (due to more ESP32-IDF hints)
assert result["platform"] == "esp32-idf"
# CRITICAL: Only esp32 component should be included, not esp8266
# This prevents trying to build ESP8266 components on ESP32 platform
assert result["components"] == ["esp32"], (
"When esp32-idf platform is selected, only esp32 component should be included, "
"not esp8266. This prevents trying to build ESP8266 components on ESP32 platform."
)
assert result["use_merged_config"] == "true"