From 0843ec6ae8c9fc94ef61b54abc6fd03054460157 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 28 Jan 2026 22:39:40 -0600 Subject: [PATCH 01/20] [const] Move `CONF_AUDIO_DAC` (#13614) --- esphome/components/es8156/audio_dac.py | 4 +--- esphome/components/speaker/__init__.py | 4 +--- esphome/const.py | 1 + 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/es8156/audio_dac.py b/esphome/components/es8156/audio_dac.py index a805fb3f70..c5fb6096da 100644 --- a/esphome/components/es8156/audio_dac.py +++ b/esphome/components/es8156/audio_dac.py @@ -2,14 +2,12 @@ import esphome.codegen as cg from esphome.components import i2c from esphome.components.audio_dac import AudioDac import esphome.config_validation as cv -from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID +from esphome.const import CONF_AUDIO_DAC, CONF_BITS_PER_SAMPLE, CONF_ID import esphome.final_validate as fv CODEOWNERS = ["@kbx81"] DEPENDENCIES = ["i2c"] -CONF_AUDIO_DAC = "audio_dac" - es8156_ns = cg.esphome_ns.namespace("es8156") ES8156 = es8156_ns.class_("ES8156", AudioDac, cg.Component, i2c.I2CDevice) diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 18e1d9782c..10ee6d5212 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import audio, audio_dac import esphome.config_validation as cv -from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME +from esphome.const import CONF_AUDIO_DAC, CONF_DATA, CONF_ID, CONF_VOLUME from esphome.core import CORE, ID from esphome.coroutine import CoroPriority, coroutine_with_priority @@ -11,8 +11,6 @@ CODEOWNERS = ["@jesserockz", "@kahrendt"] IS_PLATFORM_COMPONENT = True -CONF_AUDIO_DAC = "audio_dac" - speaker_ns = cg.esphome_ns.namespace("speaker") Speaker = speaker_ns.class_("Speaker") diff --git a/esphome/const.py b/esphome/const.py index 4243b2e25d..4bf47b8f83 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -149,6 +149,7 @@ CONF_ASSUMED_STATE = "assumed_state" CONF_AT = "at" CONF_ATTENUATION = "attenuation" CONF_ATTRIBUTE = "attribute" +CONF_AUDIO_DAC = "audio_dac" CONF_AUTH = "auth" CONF_AUTO_CLEAR_ENABLED = "auto_clear_enabled" CONF_AUTO_MODE = "auto_mode" From 6a17db885764f4bbb0439a28ca3d6ae3023796d1 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 29 Jan 2026 17:52:46 +0100 Subject: [PATCH 02/20] [nrf52,zigbee] Support for number component (#13581) --- esphome/components/number/__init__.py | 6 +- esphome/components/zigbee/__init__.py | 27 +++- esphome/components/zigbee/const_zephyr.py | 3 + .../zigbee/zigbee_number_zephyr.cpp | 111 ++++++++++++++++ .../components/zigbee/zigbee_number_zephyr.h | 118 ++++++++++++++++++ .../zigbee/zigbee_switch_zephyr.cpp | 2 +- esphome/components/zigbee/zigbee_zephyr.cpp | 4 +- esphome/components/zigbee/zigbee_zephyr.h | 6 + esphome/components/zigbee/zigbee_zephyr.py | 51 ++++++++ tests/components/zigbee/common.yaml | 10 +- 10 files changed, 331 insertions(+), 7 deletions(-) create mode 100644 esphome/components/zigbee/zigbee_number_zephyr.cpp create mode 100644 esphome/components/zigbee/zigbee_number_zephyr.h diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 368b431d7b..b23da7799f 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import mqtt, web_server +from esphome.components import mqtt, web_server, zigbee import esphome.config_validation as cv from esphome.const import ( CONF_ABOVE, @@ -189,6 +189,7 @@ validate_unit_of_measurement = cv.string_strict _NUMBER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend(zigbee.NUMBER_SCHEMA) .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), @@ -214,6 +215,7 @@ _NUMBER_SCHEMA = ( _NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number")) +_NUMBER_SCHEMA.add_extra(zigbee.validate_number) def number_schema( @@ -277,6 +279,8 @@ async def setup_number_core_( if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) + await zigbee.setup_number(var, config, min_value, max_value, step) + async def register_number( var, config, *, min_value: float, max_value: float, step: float diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 1b1da78308..c044148b32 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -24,7 +24,12 @@ from .const_zephyr import ( ZigbeeComponent, zigbee_ns, ) -from .zigbee_zephyr import zephyr_binary_sensor, zephyr_sensor, zephyr_switch +from .zigbee_zephyr import ( + zephyr_binary_sensor, + zephyr_number, + zephyr_sensor, + zephyr_switch, +) _LOGGER = logging.getLogger(__name__) @@ -43,6 +48,7 @@ def zigbee_set_core_data(config: ConfigType) -> ConfigType: BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_binary_sensor) SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor) SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch) +NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number) CONFIG_SCHEMA = cv.All( cv.Schema( @@ -125,6 +131,21 @@ async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None: await zephyr_setup_switch(entity, config) +async def setup_number( + entity: cg.MockObj, + config: ConfigType, + min_value: float, + max_value: float, + step: float, +) -> None: + if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): + return + if CORE.using_zephyr: + from .zigbee_zephyr import zephyr_setup_number + + await zephyr_setup_number(entity, config, min_value, max_value, step) + + def consume_endpoint(config: ConfigType) -> ConfigType: if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): return config @@ -152,6 +173,10 @@ def validate_switch(config: ConfigType) -> ConfigType: return consume_endpoint(config) +def validate_number(config: ConfigType) -> ConfigType: + return consume_endpoint(config) + + ZIGBEE_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { diff --git a/esphome/components/zigbee/const_zephyr.py b/esphome/components/zigbee/const_zephyr.py index 03c1bb546f..2d233755ac 100644 --- a/esphome/components/zigbee/const_zephyr.py +++ b/esphome/components/zigbee/const_zephyr.py @@ -4,6 +4,7 @@ zigbee_ns = cg.esphome_ns.namespace("zigbee") ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component) BinaryAttrs = zigbee_ns.struct("BinaryAttrs") AnalogAttrs = zigbee_ns.struct("AnalogAttrs") +AnalogAttrsOutput = zigbee_ns.struct("AnalogAttrsOutput") CONF_MAX_EP_NUMBER = 8 CONF_ZIGBEE_ID = "zigbee_id" @@ -12,6 +13,7 @@ CONF_WIPE_ON_BOOT = "wipe_on_boot" CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor" CONF_ZIGBEE_SENSOR = "zigbee_sensor" CONF_ZIGBEE_SWITCH = "zigbee_switch" +CONF_ZIGBEE_NUMBER = "zigbee_number" CONF_POWER_SOURCE = "power_source" POWER_SOURCE = { "UNKNOWN": "ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN", @@ -38,3 +40,4 @@ ZB_ZCL_CLUSTER_ID_IDENTIFY = "ZB_ZCL_CLUSTER_ID_IDENTIFY" ZB_ZCL_CLUSTER_ID_BINARY_INPUT = "ZB_ZCL_CLUSTER_ID_BINARY_INPUT" ZB_ZCL_CLUSTER_ID_ANALOG_INPUT = "ZB_ZCL_CLUSTER_ID_ANALOG_INPUT" ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT = "ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT" +ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT = "ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT" diff --git a/esphome/components/zigbee/zigbee_number_zephyr.cpp b/esphome/components/zigbee/zigbee_number_zephyr.cpp new file mode 100644 index 0000000000..ceb318480c --- /dev/null +++ b/esphome/components/zigbee/zigbee_number_zephyr.cpp @@ -0,0 +1,111 @@ +#include "zigbee_number_zephyr.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_NUMBER) +#include "esphome/core/log.h" +extern "C" { +#include +#include +#include +#include +#include +} +namespace esphome::zigbee { + +static const char *const TAG = "zigbee.number"; + +void ZigbeeNumber::setup() { + this->parent_->add_callback(this->endpoint_, [this](zb_bufid_t bufid) { this->zcl_device_cb_(bufid); }); + this->number_->add_on_state_callback([this](float state) { + this->cluster_attributes_->present_value = state; + ESP_LOGD(TAG, "Set attribute endpoint: %d, present_value %f", this->endpoint_, + this->cluster_attributes_->present_value); + ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, + ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID, (zb_uint8_t *) &cluster_attributes_->present_value, + ZB_FALSE); + this->parent_->force_report(); + }); +} + +void ZigbeeNumber::dump_config() { + ESP_LOGCONFIG(TAG, + "Zigbee Number\n" + " Endpoint: %d, present_value %f", + this->endpoint_, this->cluster_attributes_->present_value); +} + +void ZigbeeNumber::zcl_device_cb_(zb_bufid_t bufid) { + zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t); + zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id; + zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id; + zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id; + + switch (device_cb_id) { + /* ZCL set attribute value */ + case ZB_ZCL_SET_ATTR_VALUE_CB_ID: + if (cluster_id == ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT) { + ESP_LOGI(TAG, "Analog output attribute setting"); + if (attr_id == ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID) { + float value = + *reinterpret_cast(&p_device_cb_param->cb_param.set_attr_value_param.values.data32); + this->defer([this, value]() { + this->cluster_attributes_->present_value = value; + auto call = this->number_->make_call(); + call.set_value(value); + call.perform(); + }); + } + } else { + /* other clusters attribute handled here */ + ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id); + p_device_cb_param->status = RET_NOT_IMPLEMENTED; + } + break; + default: + p_device_cb_param->status = RET_NOT_IMPLEMENTED; + break; + } + + ESP_LOGD(TAG, "%s status: %hd", __func__, p_device_cb_param->status); +} + +const zb_uint8_t ZB_ZCL_ANALOG_OUTPUT_STATUS_FLAG_MAX_VALUE = 0x0F; + +static zb_ret_t check_value_analog_server(zb_uint16_t attr_id, zb_uint8_t endpoint, + zb_uint8_t *value) { // NOLINT(readability-non-const-parameter) + zb_ret_t ret = RET_OK; + ZVUNUSED(endpoint); + + switch (attr_id) { + case ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID: + ret = ZB_ZCL_CHECK_BOOL_VALUE(*value) ? RET_OK : RET_ERROR; + break; + case ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID: + break; + + case ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID: + if (*value > ZB_ZCL_ANALOG_OUTPUT_STATUS_FLAG_MAX_VALUE) { + ret = RET_ERROR; + } + break; + + default: + break; + } + + return ret; +} + +} // namespace esphome::zigbee + +void zb_zcl_analog_output_init_server() { + zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, + esphome::zigbee::check_value_analog_server, (zb_zcl_cluster_write_attr_hook_t) NULL, + (zb_zcl_cluster_handler_t) NULL); +} + +void zb_zcl_analog_output_init_client() { + zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_CLIENT_ROLE, + (zb_zcl_cluster_check_value_t) NULL, (zb_zcl_cluster_write_attr_hook_t) NULL, + (zb_zcl_cluster_handler_t) NULL); +} + +#endif diff --git a/esphome/components/zigbee/zigbee_number_zephyr.h b/esphome/components/zigbee/zigbee_number_zephyr.h new file mode 100644 index 0000000000..aabb0392be --- /dev/null +++ b/esphome/components/zigbee/zigbee_number_zephyr.h @@ -0,0 +1,118 @@ +#pragma once + +#include "esphome/core/defines.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_NUMBER) +#include "esphome/components/zigbee/zigbee_zephyr.h" +#include "esphome/core/component.h" +#include "esphome/components/number/number.h" +extern "C" { +#include +#include +} + +enum { + ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID = 0x001C, + ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID = 0x0041, + ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID = 0x0045, + ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID = 0x0051, + ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID = 0x0055, + ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID = 0x006A, + ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID = 0x006F, + ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID = 0x0075, +}; + +#define ZB_ZCL_ANALOG_OUTPUT_CLUSTER_REVISION_DEFAULT ((zb_uint16_t) 0x0001u) + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID, ZB_ZCL_ATTR_TYPE_CHAR_STRING, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ + (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID, ZB_ZCL_ATTR_TYPE_BOOL, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } +// PresentValue +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \ + ZB_ZCL_ATTR_ACCESS_READ_WRITE | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } +// MaxPresentValue +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } +// MinPresentValue +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } +// Resolution +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID, ZB_ZCL_ATTR_TYPE_8BITMAP, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID, ZB_ZCL_ATTR_TYPE_16BIT_ENUM, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ + (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \ + } + +#define ESPHOME_ZB_ZCL_DECLARE_ANALOG_OUTPUT_ATTRIB_LIST(attr_list, out_of_service, present_value, status_flag, \ + max_present_value, min_present_value, resolution, \ + engineering_units, description) \ + ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_ANALOG_OUTPUT) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID, (out_of_service)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID, (present_value)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID, (status_flag)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID, (max_present_value)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID, (min_present_value)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID, (resolution)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID, (engineering_units)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID, (description)) \ + ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST + +void zb_zcl_analog_output_init_server(); +void zb_zcl_analog_output_init_client(); +#define ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT_SERVER_ROLE_INIT zb_zcl_analog_output_init_server +#define ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT_CLIENT_ROLE_INIT zb_zcl_analog_output_init_client + +namespace esphome::zigbee { + +class ZigbeeNumber : public ZigbeeEntity, public Component { + public: + ZigbeeNumber(number::Number *n) : number_(n) {} + void set_cluster_attributes(AnalogAttrsOutput &cluster_attributes) { + this->cluster_attributes_ = &cluster_attributes; + } + + void setup() override; + void dump_config() override; + + protected: + number::Number *number_; + AnalogAttrsOutput *cluster_attributes_{nullptr}; + void zcl_device_cb_(zb_bufid_t bufid); +}; + +} // namespace esphome::zigbee +#endif diff --git a/esphome/components/zigbee/zigbee_switch_zephyr.cpp b/esphome/components/zigbee/zigbee_switch_zephyr.cpp index fef02e5a0c..935140e9df 100644 --- a/esphome/components/zigbee/zigbee_switch_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_switch_zephyr.cpp @@ -50,7 +50,7 @@ void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) { if (attr_id == ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID) { this->defer([this, value]() { this->cluster_attributes_->present_value = value ? ZB_TRUE : ZB_FALSE; - this->switch_->publish_state(value); + this->switch_->control(value); }); } } else { diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index eabf5c30d4..4763943e88 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -101,8 +101,8 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) { zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id; auto endpoint = p_device_cb_param->endpoint; - ESP_LOGI(TAG, "Zcl_device_cb %s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id, - attr_id, endpoint); + ESP_LOGI(TAG, "%s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id, attr_id, + endpoint); /* Set default response value. */ p_device_cb_param->status = RET_OK; diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index b75c192c1a..05895e8e61 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -60,6 +60,12 @@ struct AnalogAttrs { zb_uchar_t description[ZB_ZCL_MAX_STRING_SIZE]; }; +struct AnalogAttrsOutput : AnalogAttrs { + float max_present_value; + float min_present_value; + float resolution; +}; + class ZigbeeComponent : public Component { public: void setup() override; diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index b3bd10bfab..0b6daa9476 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -55,6 +55,7 @@ from .const_zephyr import ( CONF_WIPE_ON_BOOT, CONF_ZIGBEE_BINARY_SENSOR, CONF_ZIGBEE_ID, + CONF_ZIGBEE_NUMBER, CONF_ZIGBEE_SENSOR, CONF_ZIGBEE_SWITCH, KEY_EP_NUMBER, @@ -62,12 +63,14 @@ from .const_zephyr import ( POWER_SOURCE, ZB_ZCL_BASIC_ATTRS_EXT_T, ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, + ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_ID_BASIC, ZB_ZCL_CLUSTER_ID_BINARY_INPUT, ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_ID_IDENTIFY, ZB_ZCL_IDENTIFY_ATTRS_T, AnalogAttrs, + AnalogAttrsOutput, BinaryAttrs, ZigbeeComponent, zigbee_ns, @@ -76,6 +79,7 @@ from .const_zephyr import ( ZigbeeBinarySensor = zigbee_ns.class_("ZigbeeBinarySensor", cg.Component) ZigbeeSensor = zigbee_ns.class_("ZigbeeSensor", cg.Component) ZigbeeSwitch = zigbee_ns.class_("ZigbeeSwitch", cg.Component) +ZigbeeNumber = zigbee_ns.class_("ZigbeeNumber", cg.Component) # BACnet engineering units mapping (ZCL uses BACnet unit codes) # See: https://github.com/zigpy/zha/blob/dev/zha/application/platforms/number/bacnet.py @@ -139,6 +143,15 @@ zephyr_switch = cv.Schema( } ) +zephyr_number = cv.Schema( + { + cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent), + cv.OnlyWith(CONF_ZIGBEE_NUMBER, ["nrf52", "zigbee"]): cv.declare_id( + ZigbeeNumber + ), + } +) + async def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("ZIGBEE", True) @@ -344,6 +357,16 @@ async def zephyr_setup_switch(entity: cg.MockObj, config: ConfigType) -> None: CORE.add_job(_add_switch, entity, config) +async def zephyr_setup_number( + entity: cg.MockObj, + config: ConfigType, + min_value: float, + max_value: float, + step: float, +) -> None: + CORE.add_job(_add_number, entity, config, min_value, max_value, step) + + def get_slot_index() -> int: """Find the next available endpoint slot.""" slot = next( @@ -451,3 +474,31 @@ async def _add_switch(entity: cg.MockObj, config: ConfigType) -> None: ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, "ZB_HA_CUSTOM_ATTR_DEVICE_ID", ) + + +async def _add_number( + entity: cg.MockObj, + config: ConfigType, + min_value: float, + max_value: float, + step: float, +) -> None: + # Get BACnet engineering unit from unit_of_measurement + unit = config.get(CONF_UNIT_OF_MEASUREMENT, "") + bacnet_unit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS) + + await _add_zigbee_ep( + entity, + config, + CONF_ZIGBEE_NUMBER, + AnalogAttrsOutput, + "ESPHOME_ZB_ZCL_DECLARE_ANALOG_OUTPUT_ATTRIB_LIST", + ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, + "ZB_HA_CUSTOM_ATTR_DEVICE_ID", + extra_field_values={ + "max_present_value": max_value, + "min_present_value": min_value, + "resolution": step, + "engineering_units": bacnet_unit, + }, + ) diff --git a/tests/components/zigbee/common.yaml b/tests/components/zigbee/common.yaml index e4dee5f74a..2af35ff148 100644 --- a/tests/components/zigbee/common.yaml +++ b/tests/components/zigbee/common.yaml @@ -6,8 +6,6 @@ binary_sensor: name: "Garage Door Open 2" - platform: template name: "Garage Door Open 3" - - platform: template - name: "Garage Door Open 4" - platform: template name: "Garage Door Internal" internal: True @@ -44,3 +42,11 @@ switch: - platform: template name: "Template Switch" optimistic: true + +number: + - platform: template + name: "Template number" + optimistic: true + min_value: 2 + max_value: 100 + step: 1 From 044fa4d72a024d59483d4fb16836e42a7efc9ab3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 12:12:53 -0600 Subject: [PATCH 03/20] wip --- esphome/components/esp32/__init__.py | 113 +++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 90e1b29b61..011a51fb94 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -146,6 +146,40 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation ) +# Additional IDF managed components to exclude for Arduino framework builds +# These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome +# Note: Component names include the namespace prefix (e.g., "espressif__cbor") because +# that's how managed components are registered in the IDF build system +ARDUINO_EXCLUDED_IDF_COMPONENTS = ( + "chmorgan__esp-libhelix-mp3", # MP3 decoder - not used + "espressif__cbor", # CBOR library - only used by RainMaker/Insights + "espressif__esp-dsp", # DSP library - not used + "espressif__esp-modbus", # Modbus - ESPHome has its own + "espressif__esp-serial-flasher", # Serial flasher - not used + "espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used + "espressif__esp-zigbee-lib", # Zigbee library - not used + "espressif__esp_diag_data_store", # Diagnostics - not used + "espressif__esp_diagnostics", # Diagnostics - not used + "espressif__esp_hosted", # ESP hosted - not used + "espressif__esp_insights", # ESP Insights - not used + "espressif__esp_modem", # Modem library - not used + "espressif__esp_rainmaker", # RainMaker - not used + "espressif__esp_rcp_update", # RCP update - not used + "espressif__esp_schedule", # Schedule - not used + "espressif__esp_secure_cert_mgr", # Secure cert manager - not used + "espressif__esp_wifi_remote", # WiFi remote - not used + "espressif__esp-sr", # Speech recognition - not used + "espressif__json_generator", # JSON generator - not used + "espressif__json_parser", # JSON parser - not used + "espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly + "espressif__lan86xx_common", # Ethernet common - ESPHome uses ESP-IDF ethernet directly + "espressif__libsodium", # Crypto - ESPHome uses its own noise-c library + "espressif__network_provisioning", # Network provisioning - not used + "espressif__qrcode", # QR code - not used + "espressif__rmaker_common", # RainMaker common - not used + "joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem +) + # ESP32 (original) chip revision options # Setting minimum revision to 3.0 or higher: # - Reduces flash size by excluding workaround code for older chip bugs @@ -237,7 +271,11 @@ def set_core_data(config): CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} # Initialize with default exclusions - components can call include_idf_component() # to re-enable any they need - CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS) + excluded = set(DEFAULT_EXCLUDED_IDF_COMPONENTS) + # Add Arduino-specific managed component exclusions when using Arduino framework + if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS) + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) @@ -1242,6 +1280,50 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) + # Enable Arduino selective compilation to disable all Arduino libraries + # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core + # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) + add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True) + # Disable ALL optional Arduino libraries - ESPHome doesn't use them + for lib in ( + "ArduinoOTA", + "AsyncUDP", + "BLE", + "BluetoothSerial", + "DNSServer", + "EEPROM", + "ESPmDNS", + "ESP_SR", + "Ethernet", + "FFat", + "FS", + "Hash", + "HTTPClient", + "Insights", + "LittleFS", + "Matter", + "NetBIOS", + "Network", + "NetworkClientSecure", + "OpenThread", + "PPP", + "Preferences", + "RainMaker", + "SD", + "SD_MMC", + "SimpleBLE", + "SPI", + "SPIFFS", + "Ticker", + "Update", + "WebServer", + "WiFi", + "WiFiProv", + "Wire", + "Zigbee", + ): + add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", False) + cg.add_build_flag("-Wno-nonnull-compare") # Use CMN (common CAs) bundle by default to save ~51KB flash @@ -1603,9 +1685,31 @@ def _write_sdkconfig(): def _write_idf_component_yml(): yml_path = CORE.relative_build_path("src/idf_component.yml") + dependencies = {} + + # For Arduino builds, override unused managed components from the Arduino framework + # by pointing them to empty stub directories using override_path + # This prevents the IDF component manager from downloading the real components + if CORE.using_arduino: + stubs_dir = CORE.relative_build_path("component_stubs") + stubs_dir.mkdir(exist_ok=True) + for component_name in ARDUINO_EXCLUDED_IDF_COMPONENTS: + # Create stub directory with minimal CMakeLists.txt + stub_name = component_name.replace("espressif__", "") + stub_path = stubs_dir / stub_name + stub_path.mkdir(exist_ok=True) + stub_cmake = stub_path / "CMakeLists.txt" + if not stub_cmake.exists(): + stub_cmake.write_text("idf_component_register()\n") + # Convert from directory name format (espressif__cbor) to dependency format (espressif/cbor) + dep_name = component_name.replace("__", "/") + dependencies[dep_name] = { + "version": "*", + "override_path": str(stub_path), + } + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] - dependencies = {} for name, component in components.items(): dependency = {} if component[KEY_REF]: @@ -1615,9 +1719,8 @@ def _write_idf_component_yml(): if component[KEY_PATH]: dependency["path"] = component[KEY_PATH] dependencies[name] = dependency - contents = yaml_util.dump({"dependencies": dependencies}) - else: - contents = "" + + contents = yaml_util.dump({"dependencies": dependencies}) if dependencies else "" if write_file_if_changed(yml_path, contents): dependencies_lock = CORE.relative_build_path("dependencies.lock") if dependencies_lock.is_file(): From eed835794817c90f2725db3791582bb25aaef7fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 12:13:22 -0600 Subject: [PATCH 04/20] wip --- esphome/components/esp32/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 011a51fb94..d98f179180 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1685,7 +1685,7 @@ def _write_sdkconfig(): def _write_idf_component_yml(): yml_path = CORE.relative_build_path("src/idf_component.yml") - dependencies = {} + dependencies: dict[str, dict] = {} # For Arduino builds, override unused managed components from the Arduino framework # by pointing them to empty stub directories using override_path From 04442579cacad9cecc21481d3f500c71565baa7d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 12:13:26 -0600 Subject: [PATCH 05/20] wip --- esphome/components/esp32/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d98f179180..1d1b379e27 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1711,7 +1711,7 @@ def _write_idf_component_yml(): if CORE.data[KEY_ESP32][KEY_COMPONENTS]: components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] for name, component in components.items(): - dependency = {} + dependency: dict[str, str] = {} if component[KEY_REF]: dependency["version"] = component[KEY_REF] if component[KEY_REPO]: From 9f0ddcff54677fd95f30875da03edee9ef9d0a92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 12:16:51 -0600 Subject: [PATCH 06/20] tweak --- esphome/components/esp32/__init__.py | 99 ++++++++++++++++++++++++++++ esphome/components/esp32/const.py | 1 + 2 files changed, 100 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1d1b379e27..ccd6c685af 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -50,6 +50,7 @@ from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa + KEY_ARDUINO_LIBRARIES, KEY_BOARD, KEY_COMPONENTS, KEY_ESP32, @@ -87,6 +88,7 @@ IS_TARGET_PLATFORM = True CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" +CONF_INCLUDE_ARDUINO_LIBRARIES = "include_arduino_libraries" CONF_INCLUDE_IDF_COMPONENTS = "include_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" @@ -180,6 +182,74 @@ ARDUINO_EXCLUDED_IDF_COMPONENTS = ( "joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem ) +# Mapping of Arduino libraries to IDF components they require +# When an Arduino library is enabled, these IDF components are automatically included +ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { + "BLE": ("esp_driver_gptimer",), + "BluetoothSerial": ("esp_driver_gptimer",), + "Ethernet": ("espressif__lan867x", "espressif__lan86xx_common"), + "FFat": ("fatfs",), + "LittleFS": ("joltwallet__littlefs",), + "Matter": ("espressif__esp_matter",), + "RainMaker": ( + "espressif__cbor", + "espressif__esp_rainmaker", + "espressif__esp_insights", + "espressif__esp_diagnostics", + "espressif__esp_diag_data_store", + "espressif__rmaker_common", + "espressif__json_generator", + "espressif__json_parser", + "espressif__qrcode", + ), + "SD": ("fatfs",), + "SD_MMC": ("fatfs",), + "SPIFFS": ("spiffs",), + "Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"), +} + +# Arduino libraries to disable by default when using Arduino framework +# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core +# (HardwareSerial, Print, Stream, GPIO functions which are always compiled) +# Components can call enable_arduino_library() to re-enable any they need +ARDUINO_DISABLED_LIBRARIES = ( + "ArduinoOTA", + "AsyncUDP", + "BLE", + "BluetoothSerial", + "DNSServer", + "EEPROM", + "ESPmDNS", + "ESP_SR", + "Ethernet", + "FFat", + "FS", + "Hash", + "HTTPClient", + "Insights", + "LittleFS", + "Matter", + "NetBIOS", + "Network", + "NetworkClientSecure", + "OpenThread", + "PPP", + "Preferences", + "RainMaker", + "SD", + "SD_MMC", + "SimpleBLE", + "SPI", + "SPIFFS", + "Ticker", + "Update", + "WebServer", + "WiFi", + "WiFiProv", + "Wire", + "Zigbee", +) + # ESP32 (original) chip revision options # Setting minimum revision to 3.0 or higher: # - Reduces flash size by excluding workaround code for older chip bugs @@ -276,6 +346,9 @@ def set_core_data(config): if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS) CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded + # Initialize Arduino library tracking - components can call enable_arduino_library() + # to re-enable libraries that are disabled by default + CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set() CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) @@ -423,6 +496,25 @@ def include_idf_component(name: str) -> None: CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name) +def enable_arduino_library(name: str) -> None: + """Enable an Arduino library that is disabled by default. + + Call this from components that need an Arduino library that is + disabled by default in ARDUINO_DISABLED_LIBRARIES. This ensures the + library will be compiled when needed. + + This also automatically enables any IDF components required by the library + (defined in ARDUINO_LIBRARY_IDF_COMPONENTS). + + Args: + name: The library name (e.g., "Wire", "SPI", "WiFi") + """ + CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES].add(name) + # Also enable any required IDF components + for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()): + include_idf_component(idf_component) + + def add_extra_script(stage: str, filename: str, path: Path): """Add an extra script to the project.""" key = f"{stage}:{filename}" @@ -942,6 +1034,9 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_INCLUDE_IDF_COMPONENTS, default=[]): cv.ensure_list( cv.string_strict ), + cv.Optional(CONF_INCLUDE_ARDUINO_LIBRARIES, default=[]): cv.ensure_list( + cv.one_of(*ARDUINO_DISABLED_LIBRARIES) + ), cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean, cv.Optional( @@ -1416,6 +1511,10 @@ async def to_code(config): for component_name in advanced.get(CONF_INCLUDE_IDF_COMPONENTS, []): include_idf_component(component_name) + # Re-enable any Arduino libraries the user explicitly requested + for lib_name in advanced.get(CONF_INCLUDE_ARDUINO_LIBRARIES, []): + enable_arduino_library(lib_name) + # DHCP server: only disable if explicitly set to false # WiFi component handles its own optimization when AP mode is not used # When using Arduino with Ethernet, DHCP server functions must be available diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index db3eddebd5..7874c1c759 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -7,6 +7,7 @@ KEY_VARIANT = "variant" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_COMPONENTS = "components" KEY_EXCLUDE_COMPONENTS = "exclude_components" +KEY_ARDUINO_LIBRARIES = "arduino_libraries" KEY_REPO = "repo" KEY_REF = "ref" KEY_REFRESH = "refresh" From ad7f62a36821f90a80375ea5fce3463c308b2144 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 12:18:03 -0600 Subject: [PATCH 07/20] tweak --- esphome/components/esp32/__init__.py | 49 +++++----------------------- 1 file changed, 9 insertions(+), 40 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index ccd6c685af..83219c4f32 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1375,49 +1375,18 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) - # Enable Arduino selective compilation to disable all Arduino libraries + # Enable Arduino selective compilation to disable unused Arduino libraries # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) + # Components can call enable_arduino_library() or users can specify + # include_arduino_libraries in the advanced config to re-enable any needed add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True) - # Disable ALL optional Arduino libraries - ESPHome doesn't use them - for lib in ( - "ArduinoOTA", - "AsyncUDP", - "BLE", - "BluetoothSerial", - "DNSServer", - "EEPROM", - "ESPmDNS", - "ESP_SR", - "Ethernet", - "FFat", - "FS", - "Hash", - "HTTPClient", - "Insights", - "LittleFS", - "Matter", - "NetBIOS", - "Network", - "NetworkClientSecure", - "OpenThread", - "PPP", - "Preferences", - "RainMaker", - "SD", - "SD_MMC", - "SimpleBLE", - "SPI", - "SPIFFS", - "Ticker", - "Update", - "WebServer", - "WiFi", - "WiFiProv", - "Wire", - "Zigbee", - ): - add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", False) + enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) + for lib in ARDUINO_DISABLED_LIBRARIES: + # Enable if explicitly requested, disable otherwise + add_idf_sdkconfig_option( + f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs + ) cg.add_build_flag("-Wno-nonnull-compare") From d833c78c5b78993685a33c345eb2810048b6937e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 12:20:50 -0600 Subject: [PATCH 08/20] tweak --- esphome/components/esp32/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 83219c4f32..35e31ae278 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -187,10 +187,18 @@ ARDUINO_EXCLUDED_IDF_COMPONENTS = ( ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { "BLE": ("esp_driver_gptimer",), "BluetoothSerial": ("esp_driver_gptimer",), + "ESP_SR": ("espressif__esp-sr",), "Ethernet": ("espressif__lan867x", "espressif__lan86xx_common"), "FFat": ("fatfs",), + "Insights": ( + "espressif__cbor", + "espressif__esp_insights", + "espressif__esp_diagnostics", + "espressif__esp_diag_data_store", + ), "LittleFS": ("joltwallet__littlefs",), "Matter": ("espressif__esp_matter",), + "PPP": ("espressif__esp_modem",), "RainMaker": ( "espressif__cbor", "espressif__esp_rainmaker", @@ -201,10 +209,13 @@ ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { "espressif__json_generator", "espressif__json_parser", "espressif__qrcode", + "espressif__esp_schedule", + "espressif__network_provisioning", ), "SD": ("fatfs",), "SD_MMC": ("fatfs",), "SPIFFS": ("spiffs",), + "WiFiProv": ("espressif__network_provisioning", "espressif__qrcode"), "Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"), } From bb3179f26d1ddecaf1c1ff960da8a07cf08ef2d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 12:59:24 -0600 Subject: [PATCH 09/20] more fixes --- esphome/components/esp32/__init__.py | 8 +++++++- esphome/components/espnow/__init__.py | 4 +++- esphome/components/ethernet/__init__.py | 4 +++- esphome/components/web_server_base/__init__.py | 5 +---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b1bbd5919e..9f08aaa249 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -223,6 +223,7 @@ ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) # Components can call enable_arduino_library() to re-enable any they need +# This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt ARDUINO_DISABLED_LIBRARIES = ( "ArduinoOTA", "AsyncUDP", @@ -230,13 +231,17 @@ ARDUINO_DISABLED_LIBRARIES = ( "BluetoothSerial", "DNSServer", "EEPROM", - "ESPmDNS", + "ESP_HostedOTA", + "ESP_I2S", + "ESP_NOW", "ESP_SR", + "ESPmDNS", "Ethernet", "FFat", "FS", "Hash", "HTTPClient", + "HTTPUpdate", "Insights", "LittleFS", "Matter", @@ -254,6 +259,7 @@ ARDUINO_DISABLED_LIBRARIES = ( "SPIFFS", "Ticker", "Update", + "USB", "WebServer", "WiFi", "WiFiProv", diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 1f5ca1104a..50869acf9b 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -124,7 +124,9 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CORE.using_arduino: + # ESP32 with Arduino framework uses ESP-IDF APIs directly for ESP-NOW, + # so we don't need the Arduino WiFi library + if CORE.using_arduino and not CORE.is_esp32: cg.add_library("WiFi", None) # ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 8d4a1aaf8e..0a100c3c78 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -427,7 +427,9 @@ async def to_code(config): # Add LAN867x 10BASE-T1S PHY support component add_idf_component(name="espressif/lan867x", ref="2.0.0") - if CORE.using_arduino: + # ESP32 with Arduino framework uses ESP-IDF APIs directly for ethernet, + # so we don't need the Arduino WiFi library + if CORE.using_arduino and not CORE.is_esp32: cg.add_library("WiFi", None) CORE.add_job(final_step) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 6c756575d4..97cca5776b 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -38,11 +38,8 @@ async def to_code(config): cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1) return + # ESP32 uses IDF web server (early return above), so this is for other Arduino platforms if CORE.using_arduino: - if CORE.is_esp32: - cg.add_library("WiFi", None) - cg.add_library("FS", None) - cg.add_library("Update", None) if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) if CORE.is_libretiny: From f0e04169f7fb07b3acff4715e30fcc17e7f7ff3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 13:01:25 -0600 Subject: [PATCH 10/20] more fixes --- esphome/components/network/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 5b63bbfce9..1f75b12178 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -137,8 +137,7 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.NETWORK) async def to_code(config): cg.add_define("USE_NETWORK") - if CORE.using_arduino and CORE.is_esp32: - cg.add_library("Networking", None) + # ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed # Apply high performance networking settings # Config can explicitly enable/disable, or default to component-driven behavior From d4465dd63fbab482dd1dc205dd8472d84fe52980 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 13:04:39 -0600 Subject: [PATCH 11/20] more fixes --- esphome/components/esp32/__init__.py | 32 +++++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 9f08aaa249..b0dece3cf9 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -152,29 +152,29 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( # These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome # Note: Component names include the namespace prefix (e.g., "espressif__cbor") because # that's how managed components are registered in the IDF build system +# List includes direct dependencies from arduino-esp32/idf_component.yml +# plus transitive dependencies from RainMaker/Insights (except espressif/mdns which we need) ARDUINO_EXCLUDED_IDF_COMPONENTS = ( "chmorgan__esp-libhelix-mp3", # MP3 decoder - not used "espressif__cbor", # CBOR library - only used by RainMaker/Insights "espressif__esp-dsp", # DSP library - not used "espressif__esp-modbus", # Modbus - ESPHome has its own - "espressif__esp-serial-flasher", # Serial flasher - not used + "espressif__esp-sr", # Speech recognition - not used "espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used "espressif__esp-zigbee-lib", # Zigbee library - not used "espressif__esp_diag_data_store", # Diagnostics - not used "espressif__esp_diagnostics", # Diagnostics - not used - "espressif__esp_hosted", # ESP hosted - not used + "espressif__esp_hosted", # ESP hosted - only for ESP32-P4 "espressif__esp_insights", # ESP Insights - not used "espressif__esp_modem", # Modem library - not used "espressif__esp_rainmaker", # RainMaker - not used - "espressif__esp_rcp_update", # RCP update - not used - "espressif__esp_schedule", # Schedule - not used - "espressif__esp_secure_cert_mgr", # Secure cert manager - not used - "espressif__esp_wifi_remote", # WiFi remote - not used - "espressif__esp-sr", # Speech recognition - not used - "espressif__json_generator", # JSON generator - not used - "espressif__json_parser", # JSON parser - not used + "espressif__esp_rcp_update", # RCP update - RainMaker transitive dep + "espressif__esp_schedule", # Schedule - RainMaker transitive dep + "espressif__esp_secure_cert_mgr", # Secure cert - RainMaker transitive dep + "espressif__esp_wifi_remote", # WiFi remote - only for ESP32-P4 + "espressif__json_generator", # JSON generator - RainMaker transitive dep + "espressif__json_parser", # JSON parser - RainMaker transitive dep "espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly - "espressif__lan86xx_common", # Ethernet common - ESPHome uses ESP-IDF ethernet directly "espressif__libsodium", # Crypto - ESPHome uses its own noise-c library "espressif__network_provisioning", # Network provisioning - not used "espressif__qrcode", # QR code - not used @@ -187,29 +187,35 @@ ARDUINO_EXCLUDED_IDF_COMPONENTS = ( ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { "BLE": ("esp_driver_gptimer",), "BluetoothSerial": ("esp_driver_gptimer",), + "ESP_HostedOTA": ("espressif__esp_hosted", "espressif__esp_wifi_remote"), "ESP_SR": ("espressif__esp-sr",), - "Ethernet": ("espressif__lan867x", "espressif__lan86xx_common"), + "Ethernet": ("espressif__lan867x",), "FFat": ("fatfs",), "Insights": ( "espressif__cbor", "espressif__esp_insights", "espressif__esp_diagnostics", "espressif__esp_diag_data_store", + "espressif__rmaker_common", # Transitive dep from esp_insights ), "LittleFS": ("joltwallet__littlefs",), "Matter": ("espressif__esp_matter",), "PPP": ("espressif__esp_modem",), "RainMaker": ( + # Direct deps from idf_component.yml "espressif__cbor", "espressif__esp_rainmaker", "espressif__esp_insights", "espressif__esp_diagnostics", "espressif__esp_diag_data_store", "espressif__rmaker_common", + "espressif__qrcode", + # Transitive deps from esp_rainmaker + "espressif__esp_rcp_update", + "espressif__esp_schedule", + "espressif__esp_secure_cert_mgr", "espressif__json_generator", "espressif__json_parser", - "espressif__qrcode", - "espressif__esp_schedule", "espressif__network_provisioning", ), "SD": ("fatfs",), From 2839d1179e5710c0c25ab766ff8a1788dca4d4f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 13:06:59 -0600 Subject: [PATCH 12/20] more fixes --- esphome/components/esp32/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b0dece3cf9..2c659af238 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -182,8 +182,15 @@ ARDUINO_EXCLUDED_IDF_COMPONENTS = ( "joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem ) -# Mapping of Arduino libraries to IDF components they require -# When an Arduino library is enabled, these IDF components are automatically included +# Mapping of Arduino libraries to IDF managed components they require +# When an Arduino library is enabled via enable_arduino_library(), these components +# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS. +# +# Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have +# conditional maybe_add_component() calls in arduino-esp32/CMakeLists.txt that handle +# their managed component dependencies. Our mapping is primarily needed for libraries +# that don't have such conditionals (Ethernet, PPP, Zigbee, RainMaker, Insights, etc.) +# and to ensure the stubs are removed from our idf_component.yml overrides. ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { "BLE": ("esp_driver_gptimer",), "BluetoothSerial": ("esp_driver_gptimer",), From ecc0b366b3320131d53df133fd175c612776b5c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 09:21:12 -1000 Subject: [PATCH 13/20] [esp32] Reduce compile time by excluding unused IDF components (#13610) --- esphome/components/adc/sensor.py | 5 +- esphome/components/audio/__init__.py | 5 +- esphome/components/display/__init__.py | 7 +- esphome/components/esp32/__init__.py | 83 +++++++++++++++++++ esphome/components/esp32/const.py | 1 + .../components/esp32_rmt_led_strip/light.py | 4 + esphome/components/esp32_touch/__init__.py | 4 + esphome/components/ethernet/__init__.py | 4 + esphome/components/http_request/__init__.py | 3 + esphome/components/i2s_audio/__init__.py | 11 ++- esphome/components/mqtt/__init__.py | 7 +- esphome/components/neopixelbus/light.py | 11 ++- esphome/components/nextion/display.py | 2 + .../components/remote_receiver/__init__.py | 3 + .../components/remote_transmitter/__init__.py | 3 + tests/components/esp32/test.esp32-idf.yaml | 2 + 16 files changed, 148 insertions(+), 7 deletions(-) diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 64dd22b0c3..bab2762f00 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -2,7 +2,7 @@ import logging import esphome.codegen as cg from esphome.components import sensor, voltage_sampler -from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC from esphome.components.zephyr import ( zephyr_add_overlay, @@ -118,6 +118,9 @@ async def to_code(config): cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) if CORE.is_esp32: + # Re-enable ESP-IDF's ADC driver (excluded by default to save compile time) + include_builtin_idf_component("esp_adc") + if attenuation := config.get(CONF_ATTENUATION): if attenuation == "auto": cg.add(var.set_autorange(cg.global_ns.true)) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 6c721652e1..f48b776ddd 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components.esp32 import add_idf_component +from esphome.components.esp32 import add_idf_component, include_builtin_idf_component import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE import esphome.final_validate as fv @@ -166,6 +166,9 @@ def final_validate_audio_schema( async def to_code(config): + # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) + include_builtin_idf_component("esp_http_client") + add_idf_component( name="esphome/esp-audio-libs", ref="2.0.3", diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index ccbeedcd2f..695e7cde47 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -15,7 +15,7 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, SCHEDULER_DONT_RUN, ) -from esphome.core import CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -222,3 +222,8 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg, async def to_code(config): cg.add_global(display_ns.using) cg.add_define("USE_DISPLAY") + if CORE.is_esp32: + # Re-enable ESP-IDF's LCD driver (excluded by default to save compile time) + from esphome.components.esp32 import include_builtin_idf_component + + include_builtin_idf_component("esp_lcd") diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 4e425792dc..1d7ea5395f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -53,6 +53,7 @@ from .const import ( # noqa KEY_BOARD, KEY_COMPONENTS, KEY_ESP32, + KEY_EXCLUDE_COMPONENTS, KEY_EXTRA_BUILD_FILES, KEY_FLASH_SIZE, KEY_FULL_CERT_BUNDLE, @@ -86,6 +87,7 @@ IS_TARGET_PLATFORM = True CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" +CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" @@ -114,6 +116,36 @@ COMPILER_OPTIMIZATIONS = { "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", } +# ESP-IDF components excluded by default to reduce compile time. +# Components can be re-enabled by calling include_builtin_idf_component() in to_code(). +# +# Cannot be excluded (dependencies of required components): +# - "console": espressif/mdns unconditionally depends on it +# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain +DEFAULT_EXCLUDED_IDF_COMPONENTS = ( + "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing + "esp_adc", # ADC driver - only needed by adc component + "esp_driver_i2s", # I2S driver - only needed by i2s_audio component + "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus + "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch + "esp_eth", # Ethernet driver - only needed by ethernet component + "esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality + "esp_http_client", # HTTP client - only needed by http_request component + "esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation + "esp_https_server", # HTTPS server - ESPHome has its own web server + "esp_lcd", # LCD controller drivers - only needed by display component + "esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API + "espcoredump", # Core dump support - ESPHome has its own debug component + "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage + "mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation + "perfmon", # Xtensa performance monitor - ESPHome has its own debug component + "protocomm", # Protocol communication for provisioning - unused by ESPHome + "spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only) + "unity", # Unit testing framework - ESPHome doesn't use IDF's testing + "wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused + "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation +) + # ESP32 (original) chip revision options # Setting minimum revision to 3.0 or higher: # - Reduces flash size by excluding workaround code for older chip bugs @@ -203,6 +235,9 @@ def set_core_data(config): ) CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} + # Initialize with default exclusions - components can call include_builtin_idf_component() + # to re-enable any they need + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS) CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) @@ -328,6 +363,28 @@ def add_idf_component( } +def exclude_builtin_idf_component(name: str) -> None: + """Exclude an ESP-IDF component from the build. + + This reduces compile time by skipping components that are not needed. + The component will be passed to ESP-IDF's EXCLUDE_COMPONENTS cmake variable. + + Note: Components that are dependencies of other required components + cannot be excluded - ESP-IDF will still build them. + """ + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].add(name) + + +def include_builtin_idf_component(name: str) -> None: + """Remove an ESP-IDF component from the exclusion list. + + Call this from components that need an ESP-IDF component that is + excluded by default in DEFAULT_EXCLUDED_IDF_COMPONENTS. This ensures the + component will be built when needed. + """ + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name) + + def add_extra_script(stage: str, filename: str, path: Path): """Add an extra script to the project.""" key = f"{stage}:{filename}" @@ -844,6 +901,9 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional( CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False ): cv.boolean, + cv.Optional( + CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[] + ): cv.ensure_list(cv.string_strict), cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean, cv.Optional( @@ -1043,6 +1103,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None: add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_exclude_components() -> None: + """Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions.""" + if KEY_ESP32 not in CORE.data: + return + excluded = CORE.data[KEY_ESP32].get(KEY_EXCLUDE_COMPONENTS) + if excluded: + exclude_list = ";".join(sorted(excluded)) + cg.add_platformio_option( + "board_build.cmake_extra_args", f"-DEXCLUDE_COMPONENTS={exclude_list}" + ) + + @coroutine_with_priority(CoroPriority.FINAL) async def _add_yaml_idf_components(components: list[ConfigType]): """Add IDF components from YAML config with final priority to override code-added components.""" @@ -1256,6 +1329,11 @@ async def to_code(config): # Apply LWIP optimization settings advanced = conf[CONF_ADVANCED] + + # Re-include any IDF components the user explicitly requested + for component_name in advanced.get(CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, []): + include_builtin_idf_component(component_name) + # DHCP server: only disable if explicitly set to false # WiFi component handles its own optimization when AP mode is not used # When using Arduino with Ethernet, DHCP server functions must be available @@ -1440,6 +1518,11 @@ async def to_code(config): if conf[CONF_COMPONENTS]: CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS]) + # Write EXCLUDE_COMPONENTS at FINAL priority after all components have had + # a chance to call include_builtin_idf_component() to re-enable components they need. + # Default exclusions are added in set_core_data() during config validation. + CORE.add_job(_write_exclude_components) + APP_PARTITION_SIZES = { "2MB": 0x0C0000, # 768 KB diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index 9f8165818b..db3eddebd5 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -6,6 +6,7 @@ KEY_FLASH_SIZE = "flash_size" KEY_VARIANT = "variant" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_COMPONENTS = "components" +KEY_EXCLUDE_COMPONENTS = "exclude_components" KEY_REPO = "repo" KEY_REF = "ref" KEY_REFRESH = "refresh" diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 3be3c758f1..6d41f6b5b8 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -5,6 +5,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import esp32, light from esphome.components.const import CONF_USE_PSRAM +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -129,6 +130,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_rmt") + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(var, config) await cg.register_component(var, config) diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index c54ed8b9ea..6accb89c35 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -6,6 +6,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S3, get_esp32_variant, gpio, + include_builtin_idf_component, ) import esphome.config_validation as cv from esphome.const import ( @@ -266,6 +267,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + # Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_touch_sens") + touch = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(touch, config) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 1f2fe61fe1..8d4a1aaf8e 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -14,6 +14,7 @@ from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, get_esp32_variant, + include_builtin_idf_component, ) from esphome.components.network import ip_address_literal from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface @@ -419,6 +420,9 @@ async def to_code(config): # Also disable WiFi/BT coexistence since WiFi is disabled add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) + # Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time) + include_builtin_idf_component("esp_eth") + if config[CONF_TYPE] == "LAN8670": # Add LAN867x 10BASE-T1S PHY support component add_idf_component(name="espressif/lan867x", ref="2.0.0") diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 07bc758037..64d74323d6 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -155,6 +155,9 @@ async def to_code(config): cg.add(var.set_watchdog_timeout(timeout_ms)) if CORE.is_esp32: + # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) + esp32.include_builtin_idf_component("esp_http_client") + cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL])) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index d3128c5f4c..1cd2e97a5e 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,6 +1,11 @@ from esphome import pins import esphome.codegen as cg from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + get_esp32_variant, + include_builtin_idf_component, +) +from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32C5, @@ -10,8 +15,6 @@ from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, - add_idf_sdkconfig_option, - get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE @@ -272,6 +275,10 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + # Re-enable ESP-IDF's I2S driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_i2s") + if use_legacy(): cg.add_define("USE_I2S_LEGACY") diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index f53df5564c..fe153fedfa 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -4,7 +4,10 @@ from esphome import automation from esphome.automation import Condition import esphome.codegen as cg from esphome.components import logger, socket -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + include_builtin_idf_component, +) from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -360,6 +363,8 @@ async def to_code(config): # This enables low-latency MQTT event processing instead of waiting for select() timeout if CORE.is_esp32: socket.require_wake_loop_threadsafe() + # Re-enable ESP-IDF's mqtt component (excluded by default to save compile time) + include_builtin_idf_component("mqtt") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index c77217243c..104762c69e 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -1,7 +1,12 @@ from esphome import pins import esphome.codegen as cg from esphome.components import light -from esphome.components.esp32 import VARIANT_ESP32C3, VARIANT_ESP32S3, get_esp32_variant +from esphome.components.esp32 import ( + VARIANT_ESP32C3, + VARIANT_ESP32S3, + get_esp32_variant, + include_builtin_idf_component, +) import esphome.config_validation as cv from esphome.const import ( CONF_CHANNEL, @@ -205,6 +210,10 @@ async def to_code(config): has_white = "W" in config[CONF_TYPE] method = config[CONF_METHOD] + # Re-enable ESP-IDF's RMT driver if using RMT method (excluded by default) + if CORE.is_esp32 and method[CONF_TYPE] == METHOD_ESP32_RMT: + include_builtin_idf_component("esp_driver_rmt") + method_template = METHODS[method[CONF_TYPE]].to_code( method, config[CONF_VARIANT], config[CONF_INVERT] ) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index ffc509fc64..3bfcc95995 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -177,6 +177,8 @@ async def to_code(config): cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) if CORE.is_esp32: + # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) + esp32.include_builtin_idf_component("esp_http_client") esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) esp32.add_idf_sdkconfig_option( "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index f5d89f2f0f..b3dc213c5f 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -170,6 +170,9 @@ CONFIG_SCHEMA = remote_base.validate_triggers( async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: + # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) + esp32.include_builtin_idf_component("esp_driver_rmt") + var = cg.new_Pvariable(config[CONF_ID], pin) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS])) diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index f182a1ec0d..8383b9dd75 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -112,6 +112,9 @@ async def digital_write_action_to_code(config, action_id, template_arg, args): async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: + # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) + esp32.include_builtin_idf_component("esp_driver_rmt") + var = cg.new_Pvariable(config[CONF_ID], pin) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING])) diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index d4ab6b4a87..f80c854de5 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -8,6 +8,8 @@ esp32: enable_lwip_bridge_interface: true disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization use_full_certificate_bundle: false # Test CMN bundle (default) + include_builtin_idf_components: + - freertos # Test escape hatch (freertos is always included anyway) disable_debug_stubs: true disable_ocd_aware: true disable_usb_serial_jtag_secondary: true From 79805f07b3ac2ff65e9d2abe78e99e29e98e05c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 13:31:29 -0600 Subject: [PATCH 14/20] tweaks --- esphome/components/esp32/__init__.py | 108 +++++++++++++-------------- esphome/core/__init__.py | 10 +++ 2 files changed, 63 insertions(+), 55 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 2c659af238..9093167fda 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -183,7 +183,7 @@ ARDUINO_EXCLUDED_IDF_COMPONENTS = ( ) # Mapping of Arduino libraries to IDF managed components they require -# When an Arduino library is enabled via enable_arduino_library(), these components +# When an Arduino library is enabled via cg.add_library(), these components # are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS. # # Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have @@ -235,49 +235,51 @@ ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { # Arduino libraries to disable by default when using Arduino framework # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) -# Components can call enable_arduino_library() to re-enable any they need +# Components use cg.add_library() which auto-enables any they need # This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt -ARDUINO_DISABLED_LIBRARIES = ( - "ArduinoOTA", - "AsyncUDP", - "BLE", - "BluetoothSerial", - "DNSServer", - "EEPROM", - "ESP_HostedOTA", - "ESP_I2S", - "ESP_NOW", - "ESP_SR", - "ESPmDNS", - "Ethernet", - "FFat", - "FS", - "Hash", - "HTTPClient", - "HTTPUpdate", - "Insights", - "LittleFS", - "Matter", - "NetBIOS", - "Network", - "NetworkClientSecure", - "OpenThread", - "PPP", - "Preferences", - "RainMaker", - "SD", - "SD_MMC", - "SimpleBLE", - "SPI", - "SPIFFS", - "Ticker", - "Update", - "USB", - "WebServer", - "WiFi", - "WiFiProv", - "Wire", - "Zigbee", +ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset( + { + "ArduinoOTA", + "AsyncUDP", + "BLE", + "BluetoothSerial", + "DNSServer", + "EEPROM", + "ESP_HostedOTA", + "ESP_I2S", + "ESP_NOW", + "ESP_SR", + "ESPmDNS", + "Ethernet", + "FFat", + "FS", + "Hash", + "HTTPClient", + "HTTPUpdate", + "Insights", + "LittleFS", + "Matter", + "NetBIOS", + "Network", + "NetworkClientSecure", + "OpenThread", + "PPP", + "Preferences", + "RainMaker", + "SD", + "SD_MMC", + "SimpleBLE", + "SPI", + "SPIFFS", + "Ticker", + "Update", + "USB", + "WebServer", + "WiFi", + "WiFiProv", + "Wire", + "Zigbee", + } ) # ESP32 (original) chip revision options @@ -376,8 +378,7 @@ def set_core_data(config): if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS) CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded - # Initialize Arduino library tracking - components can call enable_arduino_library() - # to re-enable libraries that are disabled by default + # Initialize Arduino library tracking - cg.add_library() auto-enables libraries CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set() CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] @@ -526,15 +527,12 @@ def include_builtin_idf_component(name: str) -> None: CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name) -def enable_arduino_library(name: str) -> None: +def _enable_arduino_library(name: str) -> None: """Enable an Arduino library that is disabled by default. - Call this from components that need an Arduino library that is - disabled by default in ARDUINO_DISABLED_LIBRARIES. This ensures the - library will be compiled when needed. - - This also automatically enables any IDF components required by the library - (defined in ARDUINO_LIBRARY_IDF_COMPONENTS). + This is called automatically by CORE.add_library() when a component adds + an Arduino library via cg.add_library(). Components should not call this + directly - just use cg.add_library("LibName", None). Args: name: The library name (e.g., "Wire", "SPI", "WiFi") @@ -1408,8 +1406,8 @@ async def to_code(config): # Enable Arduino selective compilation to disable unused Arduino libraries # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) - # Components can call enable_arduino_library() or users can specify - # include_arduino_libraries in the advanced config to re-enable any needed + # cg.add_library() auto-enables needed libraries; users can also specify + # include_arduino_libraries in the advanced config add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True) enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) for lib in ARDUINO_DISABLED_LIBRARIES: @@ -1512,7 +1510,7 @@ async def to_code(config): # Re-enable any Arduino libraries the user explicitly requested for lib_name in advanced.get(CONF_INCLUDE_ARDUINO_LIBRARIES, []): - enable_arduino_library(lib_name) + _enable_arduino_library(lib_name) # DHCP server: only disable if explicitly set to false # WiFi component handles its own optimization when AP mode is not used diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 9a7dd49609..6916f79ce3 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -888,6 +888,16 @@ class EsphomeCore: library.name if "/" not in library.name else library.name.split("/")[-1] ) + # Auto-enable Arduino libraries on ESP32 Arduino builds + if self.is_esp32 and self.using_arduino: + from esphome.components.esp32 import ( + ARDUINO_DISABLED_LIBRARIES, + _enable_arduino_library, + ) + + if short_name in ARDUINO_DISABLED_LIBRARIES: + _enable_arduino_library(short_name) + if short_name not in self.platformio_libraries: _LOGGER.debug("Adding library: %s", library) self.platformio_libraries[short_name] = library From cfe7ad538d2ed5c2102dfd20f0cf3d81c73eb55c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 13:43:44 -0600 Subject: [PATCH 15/20] no need to add a new config option --- esphome/components/esp32/__init__.py | 12 +---- tests/unit_tests/test_core.py | 75 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 9093167fda..5ab7f07319 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -88,7 +88,6 @@ IS_TARGET_PLATFORM = True CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" -CONF_INCLUDE_ARDUINO_LIBRARIES = "include_arduino_libraries" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" @@ -1062,9 +1061,6 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional( CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[] ): cv.ensure_list(cv.string_strict), - cv.Optional(CONF_INCLUDE_ARDUINO_LIBRARIES, default=[]): cv.ensure_list( - cv.one_of(*ARDUINO_DISABLED_LIBRARIES) - ), cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean, cv.Optional( @@ -1406,8 +1402,8 @@ async def to_code(config): # Enable Arduino selective compilation to disable unused Arduino libraries # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) - # cg.add_library() auto-enables needed libraries; users can also specify - # include_arduino_libraries in the advanced config + # cg.add_library() auto-enables needed libraries; users can also add + # libraries via esphome: libraries: config which calls cg.add_library() add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True) enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) for lib in ARDUINO_DISABLED_LIBRARIES: @@ -1508,10 +1504,6 @@ async def to_code(config): for component_name in advanced.get(CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, []): include_builtin_idf_component(component_name) - # Re-enable any Arduino libraries the user explicitly requested - for lib_name in advanced.get(CONF_INCLUDE_ARDUINO_LIBRARIES, []): - _enable_arduino_library(lib_name) - # DHCP server: only disable if explicitly set to false # WiFi component handles its own optimization when AP mode is not used # When using Arduino with Ethernet, DHCP server functions must be available diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 1fc8dab358..174b3fec85 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -780,3 +780,78 @@ class TestEsphomeCore: target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}} assert target.has_networking is False + + def test_add_library__esp32_arduino_enables_disabled_library(self, target): + """Test add_library auto-enables Arduino libraries on ESP32 Arduino builds.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_called_once_with("WiFi") + + assert "WiFi" in target.platformio_libraries + + def test_add_library__esp32_arduino_ignores_non_arduino_library(self, target): + """Test add_library doesn't enable libraries not in ARDUINO_DISABLED_LIBRARIES.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("SomeOtherLib", "1.0.0") + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "SomeOtherLib" in target.platformio_libraries + + def test_add_library__esp32_idf_does_not_enable_arduino_library(self, target): + """Test add_library doesn't auto-enable Arduino libraries on ESP32 IDF builds.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "esp-idf", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "WiFi" in target.platformio_libraries + + def test_add_library__esp8266_does_not_enable_arduino_library(self, target): + """Test add_library doesn't auto-enable Arduino libraries on ESP8266.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp8266", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "WiFi" in target.platformio_libraries + + def test_add_library__extracts_short_name_from_path(self, target): + """Test add_library extracts short name from library paths like owner/lib.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("arduino/Wire", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_called_once_with("Wire") + + assert "Wire" in target.platformio_libraries From 21da776b71550ab88427a4de30044808d169d9cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 13:58:04 -0600 Subject: [PATCH 16/20] bsec needs wire --- esphome/components/bme680_bsec/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 06e641d34d..a86e061cd4 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -89,8 +89,9 @@ async def to_code(config): var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) ) - # Although this component does not use SPI, the BSEC library requires the SPI library + # Although this component does not use SPI/Wire directly, the BSEC library requires them cg.add_library("SPI", None) + cg.add_library("Wire", None) cg.add_define("USE_BSEC") cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480") From 9c398b1ad4fc06cdda5a1416c2c7fdaf8169ff8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 14:19:24 -0600 Subject: [PATCH 17/20] network is a special case since ard assumes it will never be disabled --- esphome/components/esp32/__init__.py | 50 ++++++++++++++++++++-------- esphome/components/midea/climate.py | 2 ++ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 5ab7f07319..305bab04fc 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -231,6 +231,14 @@ ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { "Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"), } +# Arduino library to Arduino library dependencies +# When enabling one library, also enable its dependencies +# Kconfig "select" statements don't work with CONFIG_ARDUINO_SELECTIVE_COMPILATION +ARDUINO_LIBRARY_DEPENDENCIES: dict[str, tuple[str, ...]] = { + "Ethernet": ("Network",), + "WiFi": ("Network",), +} + # Arduino libraries to disable by default when using Arduino framework # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) @@ -537,6 +545,9 @@ def _enable_arduino_library(name: str) -> None: name: The library name (e.g., "Wire", "SPI", "WiFi") """ CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES].add(name) + # Also enable any required Arduino library dependencies + for dep_lib in ARDUINO_LIBRARY_DEPENDENCIES.get(name, ()): + CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES].add(dep_lib) # Also enable any required IDF components for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()): include_builtin_idf_component(idf_component) @@ -1273,6 +1284,27 @@ async def _write_exclude_components() -> None: ) +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_arduino_libraries_sdkconfig() -> None: + """Write Arduino selective compilation sdkconfig after all components have added libraries. + + This must run at FINAL priority so that all components have had a chance to call + cg.add_library() which auto-enables Arduino libraries via _enable_arduino_library(). + """ + if KEY_ESP32 not in CORE.data: + return + # Enable Arduino selective compilation to disable unused Arduino libraries + # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core + # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) + # cg.add_library() auto-enables needed libraries; users can also add + # libraries via esphome: libraries: config which calls cg.add_library() + add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True) + enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) + for lib in ARDUINO_DISABLED_LIBRARIES: + # Enable if explicitly requested, disable otherwise + add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs) + + @coroutine_with_priority(CoroPriority.FINAL) async def _add_yaml_idf_components(components: list[ConfigType]): """Add IDF components from YAML config with final priority to override code-added components.""" @@ -1399,19 +1431,6 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) - # Enable Arduino selective compilation to disable unused Arduino libraries - # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core - # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) - # cg.add_library() auto-enables needed libraries; users can also add - # libraries via esphome: libraries: config which calls cg.add_library() - add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True) - enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) - for lib in ARDUINO_DISABLED_LIBRARIES: - # Enable if explicitly requested, disable otherwise - add_idf_sdkconfig_option( - f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs - ) - cg.add_build_flag("-Wno-nonnull-compare") # Use CMN (common CAs) bundle by default to save ~51KB flash @@ -1693,6 +1712,11 @@ async def to_code(config): # Default exclusions are added in set_core_data() during config validation. CORE.add_job(_write_exclude_components) + # Write Arduino selective compilation sdkconfig at FINAL priority after all + # components have had a chance to call cg.add_library() to enable libraries they need. + if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + CORE.add_job(_write_arduino_libraries_sdkconfig) + APP_PARTITION_SIZES = { "2MB": 0x0C0000, # 768 KB diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index b08a47afa9..a7348d1538 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -290,4 +290,6 @@ async def to_code(config): if CONF_HUMIDITY_SETPOINT in config: sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) cg.add(var.set_humidity_setpoint_sensor(sens)) + # MideaUART library requires WiFi (WiFi auto-enables Network via dependency mapping) + cg.add_library("WiFi", None) cg.add_library("dudanov/MideaUART", "1.1.9") From cea87b41903e356067e1964f890afa794a620a3a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 14:21:54 -0600 Subject: [PATCH 18/20] wled --- esphome/components/wled/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py index fb20a03010..00cc2f8f92 100644 --- a/esphome/components/wled/__init__.py +++ b/esphome/components/wled/__init__.py @@ -27,4 +27,5 @@ async def wled_light_effect_to_code(config, effect_id): cg.add(effect.set_port(config[CONF_PORT])) cg.add(effect.set_sync_group_mask(config[CONF_SYNC_GROUP_MASK])) cg.add(effect.set_blank_on_start(config[CONF_BLANK_ON_START])) + cg.add_library("WiFi", None) return effect From ff128ecc7e7753e26c02d1b44d1aa2fb65b58a17 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 14:25:56 -0600 Subject: [PATCH 19/20] i2s --- .../components/i2s_audio/media_player/__init__.py | 1 + tests/components/i2s_audio/test.esp32-ard.yaml | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 tests/components/i2s_audio/test.esp32-ard.yaml diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 35c42e1b06..426b211f47 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -114,6 +114,7 @@ async def to_code(config): cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) + cg.add_library("WiFi", None) cg.add_library("NetworkClientSecure", None) cg.add_library("HTTPClient", None) cg.add_library("esphome/ESP32-audioI2S", "2.3.0") diff --git a/tests/components/i2s_audio/test.esp32-ard.yaml b/tests/components/i2s_audio/test.esp32-ard.yaml new file mode 100644 index 0000000000..98dd5add97 --- /dev/null +++ b/tests/components/i2s_audio/test.esp32-ard.yaml @@ -0,0 +1,12 @@ +substitutions: + i2s_bclk_pin: GPIO15 + i2s_lrclk_pin: GPIO4 + i2s_mclk_pin: GPIO5 + +<<: !include common.yaml + +media_player: + - platform: i2s_audio + name: Test Media Player + dac_type: internal + mode: stereo From 13c01403754b48a45c1a85bcc60a79f9796ef3c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jan 2026 14:29:45 -0600 Subject: [PATCH 20/20] fix new test --- tests/components/i2s_audio/test.esp32-ard.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/i2s_audio/test.esp32-ard.yaml b/tests/components/i2s_audio/test.esp32-ard.yaml index 98dd5add97..4276b4f922 100644 --- a/tests/components/i2s_audio/test.esp32-ard.yaml +++ b/tests/components/i2s_audio/test.esp32-ard.yaml @@ -5,6 +5,10 @@ substitutions: <<: !include common.yaml +wifi: + ssid: test + password: test1234 + media_player: - platform: i2s_audio name: Test Media Player