Compare commits

..

6 Commits

90 changed files with 1368 additions and 2421 deletions

View File

@@ -105,7 +105,6 @@ jobs:
script/ci-custom.py
script/build_codeowners.py --check
script/build_language_schema.py --check
script/generate-esp32-boards.py --check
pytest:
name: Run pytest

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.0
rev: v0.12.12
hooks:
# Run the linter.
- id: ruff

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2025.10.0-dev
PROJECT_NUMBER = 2025.9.1
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -7,7 +7,7 @@ service APIConnection {
option (needs_setup_connection) = false;
option (needs_authentication) = false;
}
rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) {
rpc connect (ConnectRequest) returns (ConnectResponse) {
option (needs_setup_connection) = false;
option (needs_authentication) = false;
}
@@ -129,11 +129,10 @@ message HelloResponse {
// Message sent at the beginning of each connection to authenticate the client
// Can only be sent by the client and only at the beginning of the connection
message AuthenticationRequest {
message ConnectRequest {
option (id) = 3;
option (source) = SOURCE_CLIENT;
option (no_delay) = true;
option (ifdef) = "USE_API_PASSWORD";
// The password to log in with
string password = 1;
@@ -141,11 +140,10 @@ message AuthenticationRequest {
// Confirmation of successful connection. After this the connection is available for all traffic.
// Can only be sent by the server and only at the beginning of the connection
message AuthenticationResponse {
message ConnectResponse {
option (id) = 4;
option (source) = SOURCE_SERVER;
option (no_delay) = true;
option (ifdef) = "USE_API_PASSWORD";
bool invalid_password = 1;
}

View File

@@ -1386,17 +1386,20 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
}
bool APIConnection::send_connect_response(const ConnectRequest &msg) {
bool correct = true;
#ifdef USE_API_PASSWORD
bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) {
AuthenticationResponse resp;
correct = this->parent_->check_password(msg.password);
#endif
ConnectResponse resp;
// bool invalid_password = 1;
resp.invalid_password = !this->parent_->check_password(msg.password);
if (!resp.invalid_password) {
resp.invalid_password = !correct;
if (correct) {
this->complete_authentication_();
}
return this->send_message(resp, AuthenticationResponse::MESSAGE_TYPE);
return this->send_message(resp, ConnectResponse::MESSAGE_TYPE);
}
#endif // USE_API_PASSWORD
bool APIConnection::send_ping_response(const PingRequest &msg) {
PingResponse resp;

View File

@@ -197,9 +197,7 @@ class APIConnection final : public APIServerConnection {
void on_get_time_response(const GetTimeResponse &value) override;
#endif
bool send_hello_response(const HelloRequest &msg) override;
#ifdef USE_API_PASSWORD
bool send_authenticate_response(const AuthenticationRequest &msg) override;
#endif
bool send_connect_response(const ConnectRequest &msg) override;
bool send_disconnect_response(const DisconnectRequest &msg) override;
bool send_ping_response(const PingRequest &msg) override;
bool send_device_info_response(const DeviceInfoRequest &msg) override;

View File

@@ -42,8 +42,7 @@ void HelloResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->server_info_ref_.size());
size.add_length(1, this->name_ref_.size());
}
#ifdef USE_API_PASSWORD
bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->password = value.as_string();
@@ -53,9 +52,8 @@ bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimite
}
return true;
}
void AuthenticationResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); }
void AuthenticationResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->invalid_password); }
#endif
void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); }
void ConnectResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->invalid_password); }
#ifdef USE_AREAS
void AreaInfo::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(1, this->area_id);

View File

@@ -360,13 +360,12 @@ class HelloResponse final : public ProtoMessage {
protected:
};
#ifdef USE_API_PASSWORD
class AuthenticationRequest final : public ProtoDecodableMessage {
class ConnectRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 3;
static constexpr uint8_t ESTIMATED_SIZE = 9;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "authentication_request"; }
const char *message_name() const override { return "connect_request"; }
#endif
std::string password{};
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -376,12 +375,12 @@ class AuthenticationRequest final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class AuthenticationResponse final : public ProtoMessage {
class ConnectResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 4;
static constexpr uint8_t ESTIMATED_SIZE = 2;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "authentication_response"; }
const char *message_name() const override { return "connect_response"; }
#endif
bool invalid_password{false};
void encode(ProtoWriteBuffer buffer) const override;
@@ -392,7 +391,6 @@ class AuthenticationResponse final : public ProtoMessage {
protected:
};
#endif
class DisconnectRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 5;

View File

@@ -669,13 +669,8 @@ void HelloResponse::dump_to(std::string &out) const {
dump_field(out, "server_info", this->server_info_ref_);
dump_field(out, "name", this->name_ref_);
}
#ifdef USE_API_PASSWORD
void AuthenticationRequest::dump_to(std::string &out) const { dump_field(out, "password", this->password); }
void AuthenticationResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "AuthenticationResponse");
dump_field(out, "invalid_password", this->invalid_password);
}
#endif
void ConnectRequest::dump_to(std::string &out) const { dump_field(out, "password", this->password); }
void ConnectResponse::dump_to(std::string &out) const { dump_field(out, "invalid_password", this->invalid_password); }
void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); }
void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); }
void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); }

View File

@@ -24,17 +24,15 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_hello_request(msg);
break;
}
#ifdef USE_API_PASSWORD
case AuthenticationRequest::MESSAGE_TYPE: {
AuthenticationRequest msg;
case ConnectRequest::MESSAGE_TYPE: {
ConnectRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_authentication_request: %s", msg.dump().c_str());
ESP_LOGVV(TAG, "on_connect_request: %s", msg.dump().c_str());
#endif
this->on_authentication_request(msg);
this->on_connect_request(msg);
break;
}
#endif
case DisconnectRequest::MESSAGE_TYPE: {
DisconnectRequest msg;
// Empty message: no decode needed
@@ -599,13 +597,11 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) {
this->on_fatal_error();
}
}
#ifdef USE_API_PASSWORD
void APIServerConnection::on_authentication_request(const AuthenticationRequest &msg) {
if (!this->send_authenticate_response(msg)) {
void APIServerConnection::on_connect_request(const ConnectRequest &msg) {
if (!this->send_connect_response(msg)) {
this->on_fatal_error();
}
}
#endif
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
if (!this->send_disconnect_response(msg)) {
this->on_fatal_error();

View File

@@ -26,9 +26,7 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_hello_request(const HelloRequest &value){};
#ifdef USE_API_PASSWORD
virtual void on_authentication_request(const AuthenticationRequest &value){};
#endif
virtual void on_connect_request(const ConnectRequest &value){};
virtual void on_disconnect_request(const DisconnectRequest &value){};
virtual void on_disconnect_response(const DisconnectResponse &value){};
@@ -215,9 +213,7 @@ class APIServerConnectionBase : public ProtoService {
class APIServerConnection : public APIServerConnectionBase {
public:
virtual bool send_hello_response(const HelloRequest &msg) = 0;
#ifdef USE_API_PASSWORD
virtual bool send_authenticate_response(const AuthenticationRequest &msg) = 0;
#endif
virtual bool send_connect_response(const ConnectRequest &msg) = 0;
virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
virtual bool send_ping_response(const PingRequest &msg) = 0;
virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
@@ -338,9 +334,7 @@ class APIServerConnection : public APIServerConnectionBase {
#endif
protected:
void on_hello_request(const HelloRequest &msg) override;
#ifdef USE_API_PASSWORD
void on_authentication_request(const AuthenticationRequest &msg) override;
#endif
void on_connect_request(const ConnectRequest &msg) override;
void on_disconnect_request(const DisconnectRequest &msg) override;
void on_ping_request(const PingRequest &msg) override;
void on_device_info_request(const DeviceInfoRequest &msg) override;

View File

@@ -2,7 +2,6 @@ import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_CLEAR,
CONF_GAIN,
CONF_ID,
DEVICE_CLASS_ILLUMINANCE,
@@ -30,6 +29,7 @@ CONF_F5 = "f5"
CONF_F6 = "f6"
CONF_F7 = "f7"
CONF_F8 = "f8"
CONF_CLEAR = "clear"
CONF_NIR = "nir"
UNIT_COUNTS = "#"

View File

@@ -6,6 +6,8 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import BTLoggers
import esphome.config_validation as cv
from esphome.const import CONF_ACTIVE, CONF_ID
from esphome.core import CORE
from esphome.log import AnsiFore, color
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
DEPENDENCIES = ["api", "esp32"]
@@ -46,6 +48,26 @@ def validate_connections(config):
config
)
# Warn about connection slot waste when using Arduino framework
if CORE.using_arduino and connection_slots:
_LOGGER.warning(
"Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
"If BLE connections fail, they can waste connection slots for 10 seconds because\n"
"Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
"ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
"\n"
"To switch to ESP-IDF, add this to your YAML:\n"
" esp32:\n"
" framework:\n"
" type: esp-idf\n"
"\n"
"For detailed migration instructions, see:\n"
"%s",
color(
AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
),
)
return {
**config,
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
@@ -59,17 +81,19 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(BluetoothProxy),
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
cv.Optional(CONF_CACHE_SERVICES, default=True): cv.boolean,
cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean
),
cv.Optional(
CONF_CONNECTION_SLOTS,
default=DEFAULT_CONNECTION_SLOTS,
): cv.All(
cv.positive_int,
cv.Range(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
cv.Range(min=1, max=esp32_ble_tracker.max_connections()),
),
cv.Optional(CONF_CONNECTIONS): cv.All(
cv.ensure_list(CONNECTION_SCHEMA),
cv.Length(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
cv.Length(min=1, max=esp32_ble_tracker.max_connections()),
),
}
)

View File

@@ -10,7 +10,8 @@ from esphome.const import (
PLATFORM_LN882X,
PLATFORM_RTL87XX,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
AUTO_LOAD = ["web_server_base", "ota.web_server"]
DEPENDENCIES = ["wifi"]
@@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
)
@coroutine_with_priority(CoroPriority.COMMUNICATION)
@coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL)
async def to_code(config):
paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])

View File

@@ -36,6 +36,7 @@ from esphome.const import (
__version__,
)
from esphome.core import CORE, HexInt, TimePeriod
from esphome.cpp_generator import RawExpression
import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed
from esphome.types import ConfigType
@@ -156,6 +157,8 @@ def set_core_data(config):
conf = config[CONF_FRAMEWORK]
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "esp-idf"
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
if variant not in ARDUINO_ALLOWED_VARIANTS:
@@ -163,8 +166,6 @@ def set_core_data(config):
f"ESPHome does not support using the Arduino framework for the {variant}. Please use the ESP-IDF framework instead.",
path=[CONF_FRAMEWORK, CONF_TYPE],
)
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION]
)
@@ -235,6 +236,8 @@ SdkconfigValueType = bool | int | HexInt | str | RawSdkconfigValue
def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
"""Set an esp-idf sdkconfig value."""
if not CORE.using_esp_idf:
raise ValueError("Not an esp-idf project")
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS][name] = value
@@ -249,6 +252,8 @@ def add_idf_component(
submodules: list[str] | None = None,
):
"""Add an esp-idf component to the project."""
if not CORE.using_esp_idf:
raise ValueError("Not an esp-idf project")
if not repo and not ref and not path:
raise ValueError("Requires at least one of repo, ref or path")
if refresh or submodules or components:
@@ -348,7 +353,6 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
# pioarduino versions that don't require a release number
# List based on https://github.com/pioarduino/esp-idf/releases
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
cv.Version(5, 5, 1),
cv.Version(5, 5, 0),
cv.Version(5, 4, 2),
cv.Version(5, 4, 1),
@@ -362,49 +366,47 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
]
def _check_versions(value):
def _arduino_check_versions(value):
value = value.copy()
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
lookups = {
"dev": (
cv.Version(3, 2, 1),
"https://github.com/espressif/arduino-esp32.git",
),
"latest": (cv.Version(3, 2, 1), None),
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
}
lookups = {
"dev": (cv.Version(3, 2, 1), "https://github.com/espressif/arduino-esp32.git"),
"latest": (cv.Version(3, 2, 1), None),
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
}
if value[CONF_VERSION] in lookups:
if CONF_SOURCE in value:
raise cv.Invalid(
"Framework version needs to be explicitly specified when custom source is used."
)
version, source = lookups[value[CONF_VERSION]]
else:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None)
value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
value[CONF_PLATFORM_VERSION] = value.get(
CONF_PLATFORM_VERSION,
_parse_platform_version(str(ARDUINO_PLATFORM_VERSION)),
)
if value[CONF_SOURCE].startswith("http"):
# prefix is necessary or platformio will complain with a cryptic error
value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}"
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
_LOGGER.warning(
"The selected Arduino framework version is not the recommended one. "
"If there are connectivity or build issues please remove the manual version."
if value[CONF_VERSION] in lookups:
if CONF_SOURCE in value:
raise cv.Invalid(
"Framework version needs to be explicitly specified when custom source is used."
)
return value
version, source = lookups[value[CONF_VERSION]]
else:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None)
value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
value[CONF_PLATFORM_VERSION] = value.get(
CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION))
)
if value[CONF_SOURCE].startswith("http"):
# prefix is necessary or platformio will complain with a cryptic error
value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}"
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
_LOGGER.warning(
"The selected Arduino framework version is not the recommended one. "
"If there are connectivity or build issues please remove the manual version."
)
return value
def _esp_idf_check_versions(value):
value = value.copy()
lookups = {
"dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 2, 2), None),
@@ -586,6 +588,24 @@ def final_validate(config):
return config
ARDUINO_FRAMEWORK_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.string_strict,
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
{
cv.Optional(
CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False
): cv.boolean,
}
),
}
),
_arduino_check_versions,
)
CONF_SDKCONFIG_OPTIONS = "sdkconfig_options"
CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server"
CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
@@ -604,14 +624,9 @@ def _validate_idf_component(config: ConfigType) -> ConfigType:
return config
FRAMEWORK_ESP_IDF = "esp-idf"
FRAMEWORK_ARDUINO = "arduino"
FRAMEWORK_SCHEMA = cv.All(
ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_TYPE, default=FRAMEWORK_ARDUINO): cv.one_of(
FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO
),
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(CONF_RELEASE): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.string_strict,
@@ -675,7 +690,7 @@ FRAMEWORK_SCHEMA = cv.All(
),
}
),
_check_versions,
_esp_idf_check_versions,
)
@@ -742,18 +757,32 @@ def _set_default_framework(config):
config = config.copy()
variant = config[CONF_VARIANT]
config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({})
if variant in ARDUINO_ALLOWED_VARIANTS:
config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
# Show the migration message
_show_framework_migration_message(
config.get(CONF_NAME, "This device"), variant
)
else:
config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
return config
FRAMEWORK_ESP_IDF = "esp-idf"
FRAMEWORK_ARDUINO = "arduino"
FRAMEWORK_SCHEMA = cv.typed_schema(
{
FRAMEWORK_ESP_IDF: ESP_IDF_FRAMEWORK_SCHEMA,
FRAMEWORK_ARDUINO: ARDUINO_FRAMEWORK_SCHEMA,
},
lower=True,
space="-",
)
FLASH_SIZES = [
"2MB",
"4MB",
@@ -821,145 +850,139 @@ async def to_code(config):
os.path.join(os.path.dirname(__file__), "post_build.py.script"),
)
freq = config[CONF_CPU_FREQUENCY][:-3]
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_platformio_option("framework", "espidf")
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
else:
cg.add_platformio_option("framework", "arduino, espidf")
cg.add_build_flag("-Wno-nonnull-compare")
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True)
add_idf_sdkconfig_option(
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv"
)
# Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms
add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000)
# Setup watchdog
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
# Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
# Set default CPU frequency
add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True)
# Apply LWIP optimization settings
advanced = conf[CONF_ADVANCED]
# DHCP server: only disable if explicitly set to false
# WiFi component handles its own optimization when AP mode is not used
if (
CONF_ENABLE_LWIP_DHCP_SERVER in advanced
and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER]
):
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True):
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
# Apply LWIP core locking for better socket performance
# This is already enabled by default in Arduino framework, where it provides
# significant performance benefits. Our benchmarks show socket operations are
# 24-200% faster with core locking enabled:
# - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default)
# - Up to 200% slower under load when all operations queue through tcpip_thread
# Enabling this makes ESP-IDF socket performance match Arduino framework.
if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True):
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True)
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
)
if assertion_level := advanced.get(CONF_ASSERTION_LEVEL):
for key, flag in ASSERTION_LEVELS.items():
add_idf_sdkconfig_option(flag, assertion_level == key)
add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False)
compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION)
for key, flag in COMPILER_OPTIMIZATIONS.items():
add_idf_sdkconfig_option(flag, compiler_optimization == key)
add_idf_sdkconfig_option(
"CONFIG_LWIP_ESP_LWIP_ASSERT",
conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT],
)
if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC):
add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True)
add_idf_sdkconfig_option(
"CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False
)
if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES):
_LOGGER.warning(
"Using experimental features in ESP-IDF may result in unexpected failures."
)
add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
cg.add_define(
"USE_ESP_IDF_VERSION_CODE",
cg.RawExpression(
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
),
)
add_idf_sdkconfig_option(
f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True
)
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
for component in conf[CONF_COMPONENTS]:
add_idf_component(
name=component[CONF_NAME],
repo=component.get(CONF_SOURCE),
ref=component.get(CONF_REF),
path=component.get(CONF_PATH),
)
elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
cg.add_platformio_option(
"board_build.embed_txtfiles",
[
"managed_components/espressif__esp_insights/server_certs/https_server.crt",
"managed_components/espressif__esp_rainmaker/server_certs/rmaker_mqtt_server.crt",
"managed_components/espressif__esp_rainmaker/server_certs/rmaker_claim_service_server.crt",
"managed_components/espressif__esp_rainmaker/server_certs/rmaker_ota_server.crt",
],
)
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
if CONF_PARTITIONS in config:
cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])
else:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
cg.add_define(
"USE_ARDUINO_VERSION_CODE",
cg.RawExpression(
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
),
)
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
cg.add_build_flag("-Wno-nonnull-compare")
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv")
# Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms
add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000)
# Setup watchdog
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
# Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
# Set default CPU frequency
add_idf_sdkconfig_option(
f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True
)
# Apply LWIP optimization settings
advanced = conf[CONF_ADVANCED]
# 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
# for the Network library to compile, even if not actively used
if (
CONF_ENABLE_LWIP_DHCP_SERVER in advanced
and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER]
and not (
conf[CONF_TYPE] == FRAMEWORK_ARDUINO
and "ethernet" in CORE.loaded_integrations
)
):
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True):
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
# Apply LWIP core locking for better socket performance
# This is already enabled by default in Arduino framework, where it provides
# significant performance benefits. Our benchmarks show socket operations are
# 24-200% faster with core locking enabled:
# - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default)
# - Up to 200% slower under load when all operations queue through tcpip_thread
# Enabling this makes ESP-IDF socket performance match Arduino framework.
if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True):
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True)
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
)
if assertion_level := advanced.get(CONF_ASSERTION_LEVEL):
for key, flag in ASSERTION_LEVELS.items():
add_idf_sdkconfig_option(flag, assertion_level == key)
add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False)
compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION)
for key, flag in COMPILER_OPTIMIZATIONS.items():
add_idf_sdkconfig_option(flag, compiler_optimization == key)
add_idf_sdkconfig_option(
"CONFIG_LWIP_ESP_LWIP_ASSERT",
conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT],
)
if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC):
add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True)
add_idf_sdkconfig_option("CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False)
if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES):
_LOGGER.warning(
"Using experimental features in ESP-IDF may result in unexpected failures."
)
add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
cg.add_define(
"USE_ESP_IDF_VERSION_CODE",
cg.RawExpression(
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
),
)
add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True)
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
for component in conf[CONF_COMPONENTS]:
add_idf_component(
name=component[CONF_NAME],
repo=component.get(CONF_SOURCE),
ref=component.get(CONF_REF),
path=component.get(CONF_PATH),
)
cg.add(RawExpression(f"setCpuFrequencyMhz({freq})"))
APP_PARTITION_SIZES = {
@@ -1033,7 +1056,6 @@ def _write_sdkconfig():
)
+ "\n"
)
if write_file_if_changed(internal_path, contents):
# internal changed, update real one
write_file_if_changed(sdk_path, contents)
@@ -1065,32 +1087,34 @@ def _write_idf_component_yml():
# Called by writer.py
def copy_files():
_write_sdkconfig()
_write_idf_component_yml()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
if CORE.using_arduino:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
else:
if (
CORE.using_arduino
and "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]
):
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
if CORE.using_esp_idf:
_write_sdkconfig()
_write_idf_component_yml()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
# IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo,
# and no version.txt file exists, the CMake script fails for some setups.
# Fix by manually pasting a version.txt file, containing the ESPHome version
write_file_if_changed(
CORE.relative_build_path("version.txt"),
__version__,
)
# IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo,
# and no version.txt file exists, the CMake script fails for some setups.
# Fix by manually pasting a version.txt file, containing the ESPHome version
write_file_if_changed(
CORE.relative_build_path("version.txt"),
__version__,
)
for file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].values():
if file[KEY_PATH].startswith("http"):

View File

@@ -1504,10 +1504,6 @@ BOARDS = {
"name": "BPI-Bit",
"variant": VARIANT_ESP32,
},
"bpi-centi-s3": {
"name": "BPI-Centi-S3",
"variant": VARIANT_ESP32S3,
},
"bpi_leaf_s3": {
"name": "BPI-Leaf-S3",
"variant": VARIANT_ESP32S3,
@@ -1668,46 +1664,10 @@ BOARDS = {
"name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-devkitc-1-n32r8v": {
"name": "Espressif ESP32-S3-DevKitC-1-N32R8V (32 MB Flash Octal, 8 MB PSRAM Octal)",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-devkitc1-n16r16": {
"name": "Espressif ESP32-S3-DevKitC-1-N16R16V (16 MB Flash Quad, 16 MB PSRAM Octal)",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-devkitc1-n16r2": {
"name": "Espressif ESP32-S3-DevKitC-1-N16R2 (16 MB Flash Quad, 2 MB PSRAM Quad)",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-devkitc1-n16r8": {
"name": "Espressif ESP32-S3-DevKitC-1-N16R8V (16 MB Flash Quad, 8 MB PSRAM Octal)",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-devkitc1-n4r2": {
"name": "Espressif ESP32-S3-DevKitC-1-N4R2 (4 MB Flash Quad, 2 MB PSRAM Quad)",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-devkitc1-n4r8": {
"name": "Espressif ESP32-S3-DevKitC-1-N4R8 (4 MB Flash Quad, 8 MB PSRAM Octal)",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-devkitc1-n8r2": {
"name": "Espressif ESP32-S3-DevKitC-1-N8R2 (8 MB Flash Quad, 2 MB PSRAM quad)",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-devkitc1-n8r8": {
"name": "Espressif ESP32-S3-DevKitC-1-N8R8 (8 MB Flash Quad, 8 MB PSRAM Octal)",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-devkitm-1": {
"name": "Espressif ESP32-S3-DevKitM-1",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-fh4r2": {
"name": "Espressif ESP32-S3-FH4R2 (4 MB QD, 2MB PSRAM)",
"variant": VARIANT_ESP32S3,
},
"esp32-solo1": {
"name": "Espressif Generic ESP32-solo1 4M Flash",
"variant": VARIANT_ESP32,
@@ -1804,10 +1764,6 @@ BOARDS = {
"name": "Franzininho WiFi MSC",
"variant": VARIANT_ESP32S2,
},
"freenove-esp32-s3-n8r8": {
"name": "Freenove ESP32-S3 WROOM N8R8 (8MB Flash / 8MB PSRAM)",
"variant": VARIANT_ESP32S3,
},
"freenove_esp32_s3_wroom": {
"name": "Freenove ESP32-S3 WROOM N8R8 (8MB Flash / 8MB PSRAM)",
"variant": VARIANT_ESP32S3,
@@ -2008,10 +1964,6 @@ BOARDS = {
"name": "M5Stack AtomS3",
"variant": VARIANT_ESP32S3,
},
"m5stack-atoms3u": {
"name": "M5Stack AtomS3U",
"variant": VARIANT_ESP32S3,
},
"m5stack-core-esp32": {
"name": "M5Stack Core ESP32",
"variant": VARIANT_ESP32,
@@ -2132,10 +2084,6 @@ BOARDS = {
"name": "Ai-Thinker NodeMCU-32S2 (ESP-12K)",
"variant": VARIANT_ESP32S2,
},
"nologo_esp32c3_super_mini": {
"name": "Nologo ESP32C3 SuperMini",
"variant": VARIANT_ESP32C3,
},
"nscreen-32": {
"name": "YeaCreate NSCREEN-32",
"variant": VARIANT_ESP32,
@@ -2244,10 +2192,6 @@ BOARDS = {
"name": "SparkFun LoRa Gateway 1-Channel",
"variant": VARIANT_ESP32,
},
"sparkfun_pro_micro_esp32c3": {
"name": "SparkFun Pro Micro ESP32-C3",
"variant": VARIANT_ESP32C3,
},
"sparkfun_qwiic_pocket_esp32c6": {
"name": "SparkFun ESP32-C6 Qwiic Pocket",
"variant": VARIANT_ESP32C6,
@@ -2312,14 +2256,6 @@ BOARDS = {
"name": "Turta IoT Node",
"variant": VARIANT_ESP32,
},
"um_bling": {
"name": "Unexpected Maker BLING!",
"variant": VARIANT_ESP32S3,
},
"um_edges3_d": {
"name": "Unexpected Maker EDGES3[D]",
"variant": VARIANT_ESP32S3,
},
"um_feathers2": {
"name": "Unexpected Maker FeatherS2",
"variant": VARIANT_ESP32S2,
@@ -2332,18 +2268,10 @@ BOARDS = {
"name": "Unexpected Maker FeatherS3",
"variant": VARIANT_ESP32S3,
},
"um_feathers3_neo": {
"name": "Unexpected Maker FeatherS3 Neo",
"variant": VARIANT_ESP32S3,
},
"um_nanos3": {
"name": "Unexpected Maker NanoS3",
"variant": VARIANT_ESP32S3,
},
"um_omgs3": {
"name": "Unexpected Maker OMGS3",
"variant": VARIANT_ESP32S3,
},
"um_pros3": {
"name": "Unexpected Maker PROS3",
"variant": VARIANT_ESP32S3,
@@ -2352,14 +2280,6 @@ BOARDS = {
"name": "Unexpected Maker RMP",
"variant": VARIANT_ESP32S2,
},
"um_squixl": {
"name": "Unexpected Maker SQUiXL",
"variant": VARIANT_ESP32S3,
},
"um_tinyc6": {
"name": "Unexpected Maker TinyC6",
"variant": VARIANT_ESP32C6,
},
"um_tinys2": {
"name": "Unexpected Maker TinyS2",
"variant": VARIANT_ESP32S2,
@@ -2481,4 +2401,3 @@ BOARDS = {
"variant": VARIANT_ESP32S3,
},
}
# DO NOT ADD ANYTHING BELOW THIS LINE

View File

@@ -12,7 +12,7 @@ from esphome.const import (
CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX,
)
from esphome.core import TimePeriod
from esphome.core import CORE, TimePeriod
import esphome.final_validate as fv
DEPENDENCIES = ["esp32"]
@@ -174,12 +174,16 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(
CONF_ADVERTISING_CYCLE_TIME, default="10s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_DISABLE_BT_LOGS, default=True): cv.boolean,
cv.Optional(CONF_CONNECTION_TIMEOUT, default="20s"): cv.All(
cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean
),
cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All(
cv.only_with_esp_idf,
cv.positive_time_period_seconds,
cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)),
),
cv.Optional(CONF_MAX_NOTIFICATIONS, default=12): cv.All(
cv.SplitDefault(CONF_MAX_NOTIFICATIONS, esp32_idf=12): cv.All(
cv.only_with_esp_idf,
cv.positive_int,
cv.Range(min=1, max=64),
),
@@ -257,40 +261,43 @@ async def to_code(config):
cg.add(var.set_name(name))
await cg.register_component(var, config)
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
# Register the core BLE loggers that are always needed
register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI)
# Register the core BLE loggers that are always needed
register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI)
# Apply logger settings if log disabling is enabled
if config.get(CONF_DISABLE_BT_LOGS, False):
# Disable all Bluetooth loggers that are not required
for logger in BTLoggers:
if logger not in _required_loggers:
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
# Apply logger settings if log disabling is enabled
if config.get(CONF_DISABLE_BT_LOGS, False):
# Disable all Bluetooth loggers that are not required
for logger in BTLoggers:
if logger not in _required_loggers:
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
# Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to
# cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds,
# the connection slot remains occupied for the remaining time, preventing new connection
# attempts and wasting valuable connection slots.
if CONF_CONNECTION_TIMEOUT in config:
timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds)
add_idf_sdkconfig_option("CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds)
# Increase GATT client connection retry count for problematic devices
# Default in ESP-IDF is 3, we increase to 10 for better reliability with
# low-power/timing-sensitive devices
add_idf_sdkconfig_option("CONFIG_BT_GATTC_CONNECT_RETRY_COUNT", 10)
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
# Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to
# cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds,
# the connection slot remains occupied for the remaining time, preventing new connection
# attempts and wasting valuable connection slots.
if CONF_CONNECTION_TIMEOUT in config:
timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds)
add_idf_sdkconfig_option(
"CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
)
# Increase GATT client connection retry count for problematic devices
# Default in ESP-IDF is 3, we increase to 10 for better reliability with
# low-power/timing-sensitive devices
add_idf_sdkconfig_option("CONFIG_BT_GATTC_CONNECT_RETRY_COUNT", 10)
# Set the maximum number of notification registrations
# This controls how many BLE characteristics can have notifications enabled
# across all connections for a single GATT client interface
# https://github.com/esphome/issues/issues/6808
if CONF_MAX_NOTIFICATIONS in config:
add_idf_sdkconfig_option(
"CONFIG_BT_GATTC_NOTIF_REG_MAX", config[CONF_MAX_NOTIFICATIONS]
)
# Set the maximum number of notification registrations
# This controls how many BLE characteristics can have notifications enabled
# across all connections for a single GATT client interface
# https://github.com/esphome/issues/issues/6808
if CONF_MAX_NOTIFICATIONS in config:
add_idf_sdkconfig_option(
"CONFIG_BT_GATTC_NOTIF_REG_MAX", config[CONF_MAX_NOTIFICATIONS]
)
cg.add_define("USE_ESP32_BLE")

View File

@@ -4,7 +4,7 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import CONF_BLE_ID
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TX_POWER, CONF_TYPE, CONF_UUID
from esphome.core import TimePeriod
from esphome.core import CORE, TimePeriod
AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"]
@@ -86,5 +86,6 @@ async def to_code(config):
cg.add_define("USE_ESP32_BLE_ADVERTISING")
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)

View File

@@ -43,6 +43,13 @@ void BLEClientBase::setup() {
void BLEClientBase::set_state(espbt::ClientState st) {
ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st);
ESPBTClient::set_state(st);
if (st == espbt::ClientState::READY_TO_CONNECT) {
// Enable loop for state processing
this->enable_loop();
// Connect immediately instead of waiting for next loop
this->connect();
}
}
void BLEClientBase::loop() {
@@ -58,8 +65,8 @@ void BLEClientBase::loop() {
}
this->set_state(espbt::ClientState::IDLE);
}
// If idle, we can disable the loop as connect()
// will enable it again when a connection is needed.
// If its idle, we can disable the loop as set_state
// will enable it again when we need to connect.
else if (this->state_ == espbt::ClientState::IDLE) {
this->disable_loop();
}
@@ -101,20 +108,9 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
#endif
void BLEClientBase::connect() {
// Prevent duplicate connection attempts
if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
this->state_ == espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_,
this->address_str_.c_str(), espbt::client_state_to_string(this->state_));
return;
}
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(),
this->remote_addr_type_);
this->paired_ = false;
// Enable loop for state processing
this->enable_loop();
// Immediately transition to CONNECTING to prevent duplicate connection attempts
this->set_state(espbt::ClientState::CONNECTING);
// Determine connection parameters based on connection type
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
@@ -172,7 +168,7 @@ void BLEClientBase::unconditional_disconnect() {
this->log_gattc_warning_("esp_ble_gattc_close", err);
}
if (this->state_ == espbt::ClientState::DISCOVERED) {
if (this->state_ == espbt::ClientState::READY_TO_CONNECT || this->state_ == espbt::ClientState::DISCOVERED) {
this->set_address(0);
this->set_state(espbt::ClientState::IDLE);
} else {
@@ -216,6 +212,8 @@ void BLEClientBase::handle_connection_result_(esp_err_t ret) {
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_open", ret);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::CONNECTING);
}
}

View File

@@ -573,7 +573,8 @@ async def to_code(config):
)
cg.add_define("USE_ESP32_BLE_SERVER")
cg.add_define("USE_ESP32_BLE_ADVERTISING")
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
@automation.register_action(

View File

@@ -150,6 +150,10 @@ def as_reversed_hex_array(value):
)
def max_connections() -> int:
return IDF_MAX_CONNECTIONS if CORE.using_esp_idf else DEFAULT_MAX_CONNECTIONS
def consume_connection_slots(
value: int, consumer: str
) -> Callable[[MutableMapping], MutableMapping]:
@@ -168,7 +172,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(): cv.declare_id(ESP32BLETracker),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS)
cv.positive_int, cv.Range(min=0, max=max_connections())
),
cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(
cv.Schema(
@@ -234,8 +238,9 @@ def validate_remaining_connections(config):
if used_slots <= config[CONF_MAX_CONNECTIONS]:
return config
slot_users = ", ".join(slots)
hard_limit = max_connections()
if used_slots < IDF_MAX_CONNECTIONS:
if used_slots < hard_limit:
_LOGGER.warning(
"esp32_ble_tracker exceeded `%s`: components attempted to consume %d "
"connection slot(s) out of available configured maximum %d connection "
@@ -257,9 +262,9 @@ def validate_remaining_connections(config):
f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} "
f"connection slot(s); Decrease the number of BLE clients ({slot_users})"
)
if config[CONF_MAX_CONNECTIONS] < IDF_MAX_CONNECTIONS:
if config[CONF_MAX_CONNECTIONS] < hard_limit:
msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit."
msg += f" to stay under the {hard_limit} connection slot(s) limit."
raise cv.Invalid(msg)
@@ -337,18 +342,19 @@ async def to_code(config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
if config.get(CONF_SOFTWARE_COEXISTENCE):
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", True)
# https://github.com/espressif/esp-idf/issues/4101
# https://github.com/espressif/esp-idf/issues/2503
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
add_idf_sdkconfig_option(
"CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
)
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
if config.get(CONF_SOFTWARE_COEXISTENCE):
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", True)
# https://github.com/espressif/esp-idf/issues/4101
# https://github.com/espressif/esp-idf/issues/2503
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
add_idf_sdkconfig_option(
"CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
)
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT")

View File

@@ -51,6 +51,8 @@ const char *client_state_to_string(ClientState state) {
return "IDLE";
case ClientState::DISCOVERED:
return "DISCOVERED";
case ClientState::READY_TO_CONNECT:
return "READY_TO_CONNECT";
case ClientState::CONNECTING:
return "CONNECTING";
case ClientState::CONNECTED:
@@ -792,7 +794,7 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(true);
#endif
client->connect();
client->set_state(ClientState::READY_TO_CONNECT);
break;
}
}

View File

@@ -159,6 +159,8 @@ enum class ClientState : uint8_t {
IDLE,
// Device advertisement found.
DISCOVERED,
// Device is discovered and the scanner is stopped
READY_TO_CONNECT,
// Connection in progress.
CONNECTING,
// Initial connection established.
@@ -311,6 +313,7 @@ class ESP32BLETracker : public Component,
counts.discovered++;
break;
case ClientState::CONNECTING:
case ClientState::READY_TO_CONNECT:
counts.connecting++;
break;
default:

View File

@@ -21,6 +21,7 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_VSYNC_PIN,
)
from esphome.core import CORE
from esphome.core.entity_helpers import setup_entity
import esphome.final_validate as fv
@@ -343,7 +344,8 @@ async def to_code(config):
cg.add_define("USE_CAMERA")
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
if CORE.using_esp_idf:
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -16,7 +16,8 @@ from esphome.const import (
CONF_SAFE_MODE,
CONF_VERSION,
)
from esphome.core import CoroPriority, coroutine_with_priority
from esphome.core import coroutine_with_priority
from esphome.coroutine import CoroPriority
import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__)
@@ -121,7 +122,7 @@ CONFIG_SCHEMA = (
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
@coroutine_with_priority(CoroPriority.COMMUNICATION)
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_port(config[CONF_PORT]))

View File

@@ -322,8 +322,11 @@ async def to_code(config):
cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED]))
cg.add_define("USE_ETHERNET_SPI")
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
add_idf_sdkconfig_option(f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True)
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
add_idf_sdkconfig_option(
f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True
)
elif config[CONF_TYPE] == "OPENETH":
cg.add_define("USE_ETHERNET_OPENETH")
add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True)
@@ -356,9 +359,10 @@ async def to_code(config):
cg.add_define("USE_ETHERNET")
# Disable WiFi when using Ethernet to save memory
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False)
# Also disable WiFi/BT coexistence since WiFi is disabled
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False)
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False)
# Also disable WiFi/BT coexistence since WiFi is disabled
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False)
if CORE.using_arduino:
cg.add_library("WiFi", None)

View File

@@ -6,6 +6,7 @@ namespace gpio {
static const char *const TAG = "gpio.binary_sensor";
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
static const LogString *interrupt_type_to_string(gpio::InterruptType type) {
switch (type) {
case gpio::INTERRUPT_RISING_EDGE:
@@ -22,6 +23,7 @@ static const LogString *interrupt_type_to_string(gpio::InterruptType type) {
static const LogString *gpio_mode_to_string(bool use_interrupt) {
return use_interrupt ? LOG_STR("interrupt") : LOG_STR("polling");
}
#endif
void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
bool new_state = arg->isr_pin_.digital_read();

View File

@@ -3,7 +3,8 @@ import esphome.codegen as cg
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME
from esphome.core import CoroPriority, coroutine_with_priority
from esphome.core import coroutine_with_priority
from esphome.coroutine import CoroPriority
from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns
@@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
)
@coroutine_with_priority(CoroPriority.COMMUNICATION)
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ota_to_code(var, config)

View File

@@ -262,7 +262,8 @@ async def to_code(config):
cg.add_define("USE_I2S_LEGACY")
# Helps avoid callbacks being skipped due to processor load
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_I2S_BCLK_PIN in config:

View File

@@ -15,11 +15,12 @@ static const char *const TAG = "improv_serial";
void ImprovSerialComponent::setup() {
global_improv_serial_component = this;
#ifdef USE_ESP32
this->uart_num_ = logger::global_logger->get_uart_num();
#elif defined(USE_ARDUINO)
#ifdef USE_ARDUINO
this->hw_serial_ = logger::global_logger->get_hw_serial();
#endif
#ifdef USE_ESP_IDF
this->uart_num_ = logger::global_logger->get_uart_num();
#endif
if (wifi::global_wifi_component->has_sta()) {
this->state_ = improv::STATE_PROVISIONED;
@@ -33,7 +34,13 @@ void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:")
optional<uint8_t> ImprovSerialComponent::read_byte_() {
optional<uint8_t> byte;
uint8_t data = 0;
#ifdef USE_ESP32
#ifdef USE_ARDUINO
if (this->hw_serial_->available()) {
this->hw_serial_->readBytes(&data, 1);
byte = data;
}
#endif
#ifdef USE_ESP_IDF
switch (logger::global_logger->get_uart()) {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
@@ -69,18 +76,16 @@ optional<uint8_t> ImprovSerialComponent::read_byte_() {
default:
break;
}
#elif defined(USE_ARDUINO)
if (this->hw_serial_->available()) {
this->hw_serial_->readBytes(&data, 1);
byte = data;
}
#endif
return byte;
}
void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) {
data.push_back('\n');
#ifdef USE_ESP32
#ifdef USE_ARDUINO
this->hw_serial_->write(data.data(), data.size());
#endif
#ifdef USE_ESP_IDF
switch (logger::global_logger->get_uart()) {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
@@ -107,8 +112,6 @@ void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) {
default:
break;
}
#elif defined(USE_ARDUINO)
this->hw_serial_->write(data.data(), data.size());
#endif
}

View File

@@ -9,7 +9,10 @@
#include <improv.h>
#include <vector>
#ifdef USE_ESP32
#ifdef USE_ARDUINO
#include <HardwareSerial.h>
#endif
#ifdef USE_ESP_IDF
#include <driver/uart.h>
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \
defined(USE_ESP32_VARIANT_ESP32H2)
@@ -19,8 +22,6 @@
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include <esp_private/usb_console.h>
#endif
#elif defined(USE_ARDUINO)
#include <HardwareSerial.h>
#endif
namespace esphome {
@@ -59,11 +60,12 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
optional<uint8_t> read_byte_();
void write_data_(std::vector<uint8_t> &data);
#ifdef USE_ESP32
uart_port_t uart_num_;
#elif defined(USE_ARDUINO)
#ifdef USE_ARDUINO
Stream *hw_serial_{nullptr};
#endif
#ifdef USE_ESP_IDF
uart_port_t uart_num_;
#endif
std::vector<uint8_t> rx_buffer_;
uint32_t last_read_byte_{0};

View File

@@ -8,9 +8,7 @@ namespace json {
static const char *const TAG = "json";
#ifdef USE_PSRAM
// Build an allocator for the JSON Library using the RAMAllocator class
// This is only compiled when PSRAM is enabled
struct SpiRamAllocator : ArduinoJson::Allocator {
void *allocate(size_t size) override { return this->allocator_.allocate(size); }
@@ -31,16 +29,11 @@ struct SpiRamAllocator : ArduinoJson::Allocator {
protected:
RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::NONE)};
};
#endif
std::string build_json(const json_build_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
#ifdef USE_PSRAM
auto doc_allocator = SpiRamAllocator();
JsonDocument json_document(&doc_allocator);
#else
JsonDocument json_document;
#endif
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return "{}";
@@ -59,12 +52,8 @@ std::string build_json(const json_build_t &f) {
bool parse_json(const std::string &data, const json_parse_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
#ifdef USE_PSRAM
auto doc_allocator = SpiRamAllocator();
JsonDocument json_document(&doc_allocator);
#else
JsonDocument json_document;
#endif
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return false;

View File

@@ -117,6 +117,8 @@ UART_SELECTION_LIBRETINY = {
COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2],
}
ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG]
UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1]
UART_SELECTION_NRF52 = [USB_CDC, UART0]
@@ -151,7 +153,13 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True)
def uart_selection(value):
if CORE.is_esp32:
if CORE.using_arduino and value.upper() in ESP_ARDUINO_UNSUPPORTED_USB_UARTS:
raise cv.Invalid(f"Arduino framework does not support {value}.")
variant = get_esp32_variant()
if CORE.using_esp_idf and variant == VARIANT_ESP32C3 and value == USB_CDC:
raise cv.Invalid(
f"{value} is not supported for variant {variant} when using ESP-IDF."
)
if variant in UART_SELECTION_ESP32:
return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value)
if CORE.is_esp8266:
@@ -218,11 +226,14 @@ CONFIG_SCHEMA = cv.All(
esp8266=UART0,
esp32=UART0,
esp32_s2=USB_CDC,
esp32_s3=USB_SERIAL_JTAG,
esp32_c3=USB_SERIAL_JTAG,
esp32_c5=USB_SERIAL_JTAG,
esp32_c6=USB_SERIAL_JTAG,
esp32_p4=USB_SERIAL_JTAG,
esp32_s3_arduino=USB_CDC,
esp32_s3_idf=USB_SERIAL_JTAG,
esp32_c3_arduino=USB_CDC,
esp32_c3_idf=USB_SERIAL_JTAG,
esp32_c5_idf=USB_SERIAL_JTAG,
esp32_c6_arduino=USB_CDC,
esp32_c6_idf=USB_SERIAL_JTAG,
esp32_p4_idf=USB_SERIAL_JTAG,
rp2040=USB_CDC,
bk72xx=DEFAULT,
ln882x=DEFAULT,
@@ -335,7 +346,15 @@ async def to_code(config):
if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH):
cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH")
if CORE.is_esp32:
if CORE.using_arduino and config[CONF_HARDWARE_UART] == USB_CDC:
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1")
if CORE.is_esp32 and get_esp32_variant() in (
VARIANT_ESP32C3,
VARIANT_ESP32C6,
):
cg.add_build_flag("-DARDUINO_USB_MODE=1")
if CORE.using_esp_idf:
if config[CONF_HARDWARE_UART] == USB_CDC:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:

View File

@@ -173,8 +173,24 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
}
#endif
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
void Logger::loop() { this->process_messages_(); }
#ifndef USE_ZEPHYR
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32)
void Logger::loop() {
#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO)
if (this->uart_ == UART_SELECTION_USB_CDC) {
static bool opened = false;
if (opened == Serial) {
return;
}
if (false == opened) {
App.schedule_dump_config();
}
opened = !opened;
}
#endif
this->process_messages_();
}
#endif
#endif
void Logger::process_messages_() {

View File

@@ -16,18 +16,18 @@
#endif
#ifdef USE_ARDUINO
#if defined(USE_ESP8266)
#if defined(USE_ESP8266) || defined(USE_ESP32)
#include <HardwareSerial.h>
#endif // USE_ESP8266
#endif // USE_ESP8266 || USE_ESP32
#ifdef USE_RP2040
#include <HardwareSerial.h>
#include <SerialUSB.h>
#endif // USE_RP2040
#endif // USE_ARDUINO
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include <driver/uart.h>
#endif // USE_ESP32
#endif // USE_ESP_IDF
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
@@ -110,17 +110,19 @@ class Logger : public Component {
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
void init_log_buffer(size_t total_buffer_size);
#endif
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC))
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) || defined(USE_ZEPHYR)
void loop() override;
#endif
/// Manually set the baud rate for serial, set to 0 to disable.
void set_baud_rate(uint32_t baud_rate);
uint32_t get_baud_rate() const { return baud_rate_; }
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
#ifdef USE_ARDUINO
Stream *get_hw_serial() const { return hw_serial_; }
#endif
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
uart_port_t get_uart_num() const { return uart_num_; }
#endif
#ifdef USE_ESP32
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
#endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
@@ -230,7 +232,7 @@ class Logger : public Component {
// Group 4-byte aligned members first
uint32_t baud_rate_;
char *tx_buffer_{nullptr};
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
#ifdef USE_ARDUINO
Stream *hw_serial_{nullptr};
#endif
#if defined(USE_ZEPHYR)
@@ -244,7 +246,9 @@ class Logger : public Component {
// - Main task uses a dedicated member variable for efficiency
// - Other tasks use pthread TLS with a dynamically created key via pthread_key_create
pthread_key_t log_recursion_key_; // 4 bytes
uart_port_t uart_num_; // 4 bytes (enum defaults to int size)
#endif
#ifdef USE_ESP_IDF
uart_port_t uart_num_; // 4 bytes (enum defaults to int size)
#endif
// Large objects (internally aligned)
@@ -376,7 +380,15 @@ class Logger : public Component {
// will be processed on the next main loop iteration since:
// - disable_loop() takes effect immediately
// - enable_loop_soon_any_context() sets a pending flag that's checked at loop start
#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO)
// Only disable if not using USB CDC (which needs loop for connection detection)
if (this->uart_ != UART_SELECTION_USB_CDC) {
this->disable_loop();
}
#else
// No USB CDC support, always safe to disable
this->disable_loop();
#endif
}
#endif
};

View File

@@ -1,8 +1,11 @@
#ifdef USE_ESP32
#include "logger.h"
#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF)
#include <esp_log.h>
#endif // USE_ESP32_FRAMEWORK_ARDUINO || USE_ESP_IDF
#ifdef USE_ESP_IDF
#include <driver/uart.h>
#ifdef USE_LOGGER_USB_SERIAL_JTAG
@@ -22,12 +25,16 @@
#include <cstdint>
#include <cstdio>
#endif // USE_ESP_IDF
#include "esphome/core/log.h"
namespace esphome::logger {
static const char *const TAG = "logger";
#ifdef USE_ESP_IDF
#ifdef USE_LOGGER_USB_SERIAL_JTAG
static void init_usb_serial_jtag_() {
setvbuf(stdin, NULL, _IONBF, 0); // Disable buffering on stdin
@@ -82,8 +89,42 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
uart_driver_install(uart_num, uart_buffer_size, uart_buffer_size, 10, nullptr, 0);
}
#endif // USE_ESP_IDF
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
#ifdef USE_ARDUINO
switch (this->uart_) {
case UART_SELECTION_UART0:
#if ARDUINO_USB_CDC_ON_BOOT
this->hw_serial_ = &Serial0;
Serial0.begin(this->baud_rate_);
#else
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
#endif
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
#ifdef USE_ESP32_VARIANT_ESP32
case UART_SELECTION_UART2:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
#endif
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
break;
#endif
}
#endif // USE_ARDUINO
#ifdef USE_ESP_IDF
this->uart_num_ = UART_NUM_0;
switch (this->uart_) {
case UART_SELECTION_UART0:
@@ -110,17 +151,21 @@ void Logger::pre_setup() {
break;
#endif
}
#endif // USE_ESP_IDF
}
global_logger = this;
#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO)
esp_log_set_vprintf(esp_idf_log_vprintf_);
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
esp_log_level_set("*", ESP_LOG_VERBOSE);
}
#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO
ESP_LOGI(TAG, "Log initialized");
}
#ifdef USE_ESP_IDF
void HOT Logger::write_msg_(const char *msg) {
if (
#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
@@ -141,6 +186,9 @@ void HOT Logger::write_msg_(const char *msg) {
uart_write_bytes(this->uart_num_, "\n", 1);
}
}
#else
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
#endif
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {

View File

@@ -12,8 +12,8 @@ namespace esphome::logger {
static const char *const TAG = "logger";
#ifdef USE_LOGGER_USB_CDC
void Logger::loop() {
#ifdef USE_LOGGER_USB_CDC
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
return;
}
@@ -30,8 +30,9 @@ void Logger::loop() {
App.schedule_dump_config();
}
opened = !opened;
}
#endif
this->process_messages_();
}
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {

View File

@@ -11,7 +11,8 @@ from esphome.const import (
CONF_SERVICES,
PlatformFramework,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"]
@@ -72,7 +73,7 @@ def mdns_service(
)
@coroutine_with_priority(CoroPriority.COMMUNICATION)
@coroutine_with_priority(CoroPriority.NETWORK_SERVICES)
async def to_code(config):
if config[CONF_DISABLED] is True:
return

View File

@@ -47,13 +47,9 @@ async def to_code(config):
cg.add_define(
"USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT]
)
if CORE.is_esp32:
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6)
else:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", True)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", True)
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6)
add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6)
elif enable_ipv6:
cg.add_build_flag("-DCONFIG_LWIP_IPV6")
cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG")

View File

@@ -153,10 +153,10 @@ async def to_code(config):
if CONF_TFT_URL in config:
cg.add_define("USE_NEXTION_TFT_UPLOAD")
cg.add(var.set_tft_url(config[CONF_TFT_URL]))
if CORE.is_esp32:
if CORE.using_arduino:
cg.add_library("NetworkClientSecure", None)
cg.add_library("HTTPClient", None)
if CORE.is_esp32 and CORE.using_arduino:
cg.add_library("NetworkClientSecure", None)
cg.add_library("HTTPClient", None)
elif CORE.is_esp32 and CORE.using_esp_idf:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True

View File

@@ -10,7 +10,8 @@ from esphome.const import (
CONF_TRIGGER_ID,
PlatformFramework,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["md5", "safe_mode"]
@@ -82,7 +83,7 @@ BASE_OTA_SCHEMA = cv.Schema(
)
@coroutine_with_priority(CoroPriority.COMMUNICATION)
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config):
cg.add_define("USE_OTA")

View File

@@ -121,15 +121,11 @@ def transport_schema(cls):
return TRANSPORT_SCHEMA.extend({cv.GenerateID(): cv.declare_id(cls)})
# Build a list of sensors for this platform
CORE.data[DOMAIN] = {CONF_SENSORS: []}
def get_sensors(transport_id):
"""Return the list of sensors for this platform."""
return (
sensor
for sensor in CORE.data[DOMAIN][CONF_SENSORS]
for sensor in CORE.data.setdefault(DOMAIN, {}).setdefault(CONF_SENSORS, [])
if sensor[CONF_TRANSPORT_ID] == transport_id
)
@@ -137,7 +133,8 @@ def get_sensors(transport_id):
def validate_packet_transport_sensor(config):
if CONF_NAME in config and CONF_INTERNAL not in config:
raise cv.Invalid("Must provide internal: config when using name:")
CORE.data[DOMAIN][CONF_SENSORS].append(config)
conf_sensors = CORE.data.setdefault(DOMAIN, {}).setdefault(CONF_SENSORS, [])
conf_sensors.append(config)
return config

View File

@@ -270,7 +270,6 @@ void PacketTransport::add_binary_data_(uint8_t key, const char *id, bool data) {
auto len = 1 + 1 + 1 + strlen(id);
if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) {
this->flush_();
this->init_data_();
}
add(this->data_, key);
add(this->data_, (uint8_t) data);
@@ -285,7 +284,6 @@ void PacketTransport::add_data_(uint8_t key, const char *id, uint32_t data) {
auto len = 4 + 1 + 1 + strlen(id);
if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) {
this->flush_();
this->init_data_();
}
add(this->data_, key);
add(this->data_, data);

View File

@@ -121,30 +121,33 @@ async def to_code(config):
if config[CONF_MODE] == TYPE_OCTAL:
cg.add_platformio_option("board_build.arduino.memory_type", "qio_opi")
add_idf_sdkconfig_option(
f"CONFIG_{get_esp32_variant().upper()}_SPIRAM_SUPPORT", True
)
add_idf_sdkconfig_option("CONFIG_SOC_SPIRAM_SUPPORTED", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_USE", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_IGNORE_NOTFOUND", True)
if CORE.using_esp_idf:
add_idf_sdkconfig_option(
f"CONFIG_{get_esp32_variant().upper()}_SPIRAM_SUPPORT", True
)
add_idf_sdkconfig_option("CONFIG_SOC_SPIRAM_SUPPORTED", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_USE", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_IGNORE_NOTFOUND", True)
add_idf_sdkconfig_option(f"CONFIG_SPIRAM_MODE_{SDK_MODES[config[CONF_MODE]]}", True)
add_idf_sdkconfig_option(
f"CONFIG_SPIRAM_MODE_{SDK_MODES[config[CONF_MODE]]}", True
)
# Remove MHz suffix, convert to int
speed = int(config[CONF_SPEED][:-3])
add_idf_sdkconfig_option(f"CONFIG_SPIRAM_SPEED_{speed}M", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_SPEED", speed)
if config[CONF_MODE] == TYPE_OCTAL and speed == 120:
add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True)
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_FLASH_DC_AWARE", True)
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0):
add_idf_sdkconfig_option(
"CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True
)
if config[CONF_ENABLE_ECC]:
add_idf_sdkconfig_option("CONFIG_SPIRAM_ECC_ENABLE", True)
# Remove MHz suffix, convert to int
speed = int(config[CONF_SPEED][:-3])
add_idf_sdkconfig_option(f"CONFIG_SPIRAM_SPEED_{speed}M", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_SPEED", speed)
if config[CONF_MODE] == TYPE_OCTAL and speed == 120:
add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True)
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_FLASH_DC_AWARE", True)
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0):
add_idf_sdkconfig_option(
"CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True
)
if config[CONF_ENABLE_ECC]:
add_idf_sdkconfig_option("CONFIG_SPIRAM_ECC_ENABLE", True)
cg.add_define("USE_PSRAM")

View File

@@ -196,8 +196,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"remote_receiver.cpp": {
PlatformFramework.ESP8266_ARDUINO,
"remote_receiver_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"remote_receiver_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,

View File

@@ -3,12 +3,12 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#if defined(USE_LIBRETINY) || defined(USE_ESP8266)
#ifdef USE_ESP8266
namespace esphome {
namespace remote_receiver {
static const char *const TAG = "remote_receiver";
static const char *const TAG = "remote_receiver.esp8266";
void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) {
const uint32_t now = micros();

View File

@@ -0,0 +1,125 @@
#include "remote_receiver.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_LIBRETINY
namespace esphome {
namespace remote_receiver {
static const char *const TAG = "remote_receiver.libretiny";
void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) {
const uint32_t now = micros();
// If the lhs is 1 (rising edge) we should write to an uneven index and vice versa
const uint32_t next = (arg->buffer_write_at + 1) % arg->buffer_size;
const bool level = arg->pin.digital_read();
if (level != next % 2)
return;
// If next is buffer_read, we have hit an overflow
if (next == arg->buffer_read_at)
return;
const uint32_t last_change = arg->buffer[arg->buffer_write_at];
const uint32_t time_since_change = now - last_change;
if (time_since_change <= arg->filter_us)
return;
arg->buffer[arg->buffer_write_at = next] = now;
}
void RemoteReceiverComponent::setup() {
this->pin_->setup();
auto &s = this->store_;
s.filter_us = this->filter_us_;
s.pin = this->pin_->to_isr();
s.buffer_size = this->buffer_size_;
this->high_freq_.start();
if (s.buffer_size % 2 != 0) {
// Make sure divisible by two. This way, we know that every 0bxxx0 index is a space and every 0bxxx1 index is a mark
s.buffer_size++;
}
s.buffer = new uint32_t[s.buffer_size];
void *buf = (void *) s.buffer;
memset(buf, 0, s.buffer_size * sizeof(uint32_t));
// First index is a space.
if (this->pin_->digital_read()) {
s.buffer_write_at = s.buffer_read_at = 1;
} else {
s.buffer_write_at = s.buffer_read_at = 0;
}
this->pin_->attach_interrupt(RemoteReceiverComponentStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE);
}
void RemoteReceiverComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Remote Receiver:");
LOG_PIN(" Pin: ", this->pin_);
if (this->pin_->digital_read()) {
ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to "
"invert the signal using 'inverted: True' in the pin schema!");
}
ESP_LOGCONFIG(TAG,
" Buffer Size: %u\n"
" Tolerance: %u%s\n"
" Filter out pulses shorter than: %u us\n"
" Signal is done after %u us of no changes",
this->buffer_size_, this->tolerance_,
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_,
this->idle_us_);
}
void RemoteReceiverComponent::loop() {
auto &s = this->store_;
// copy write at to local variables, as it's volatile
const uint32_t write_at = s.buffer_write_at;
const uint32_t dist = (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size;
// signals must at least one rising and one leading edge
if (dist <= 1)
return;
const uint32_t now = micros();
if (now - s.buffer[write_at] < this->idle_us_) {
// The last change was fewer than the configured idle time ago.
return;
}
ESP_LOGVV(TAG, "read_at=%u write_at=%u dist=%u now=%u end=%u", s.buffer_read_at, write_at, dist, now,
s.buffer[write_at]);
// Skip first value, it's from the previous idle level
s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size;
uint32_t prev = s.buffer_read_at;
s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size;
const uint32_t reserve_size = 1 + (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size;
this->temp_.clear();
this->temp_.reserve(reserve_size);
int32_t multiplier = s.buffer_read_at % 2 == 0 ? 1 : -1;
for (uint32_t i = 0; prev != write_at; i++) {
int32_t delta = s.buffer[s.buffer_read_at] - s.buffer[prev];
if (uint32_t(delta) >= this->idle_us_) {
// already found a space longer than idle. There must have been two pulses
break;
}
ESP_LOGVV(TAG, " i=%u buffer[%u]=%u - buffer[%u]=%u -> %d", i, s.buffer_read_at, s.buffer[s.buffer_read_at], prev,
s.buffer[prev], multiplier * delta);
this->temp_.push_back(multiplier * delta);
prev = s.buffer_read_at;
s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size;
multiplier *= -1;
}
s.buffer_read_at = (s.buffer_size + s.buffer_read_at - 1) % s.buffer_size;
this->temp_.push_back(this->idle_us_ * multiplier);
this->call_listeners_dumpers_();
}
} // namespace remote_receiver
} // namespace esphome
#endif

View File

@@ -131,8 +131,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"remote_transmitter.cpp": {
PlatformFramework.ESP8266_ARDUINO,
"remote_transmitter_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"remote_transmitter_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,

View File

@@ -2,107 +2,10 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#if defined(USE_LIBRETINY) || defined(USE_ESP8266)
namespace esphome {
namespace remote_transmitter {
static const char *const TAG = "remote_transmitter";
void RemoteTransmitterComponent::setup() {
this->pin_->setup();
this->pin_->digital_write(false);
}
void RemoteTransmitterComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Remote Transmitter:\n"
" Carrier Duty: %u%%",
this->carrier_duty_percent_);
LOG_PIN(" Pin: ", this->pin_);
}
void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period,
uint32_t *off_time_period) {
if (carrier_frequency == 0) {
*on_time_period = 0;
*off_time_period = 0;
return;
}
uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq)
period = std::max(uint32_t(1), period);
*on_time_period = (period * this->carrier_duty_percent_) / 100;
*off_time_period = period - *on_time_period;
}
void RemoteTransmitterComponent::await_target_time_() {
const uint32_t current_time = micros();
if (this->target_time_ == 0) {
this->target_time_ = current_time;
} else if ((int32_t) (this->target_time_ - current_time) > 0) {
delayMicroseconds(this->target_time_ - current_time);
}
}
void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) {
this->await_target_time_();
this->pin_->digital_write(true);
const uint32_t target = this->target_time_ + usec;
if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) {
while (true) { // Modulate with carrier frequency
this->target_time_ += on_time;
if ((int32_t) (this->target_time_ - target) >= 0)
break;
this->await_target_time_();
this->pin_->digital_write(false);
this->target_time_ += off_time;
if ((int32_t) (this->target_time_ - target) >= 0)
break;
this->await_target_time_();
this->pin_->digital_write(true);
}
}
this->target_time_ = target;
}
void RemoteTransmitterComponent::space_(uint32_t usec) {
this->await_target_time_();
this->pin_->digital_write(false);
this->target_time_ += usec;
}
void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); }
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
ESP_LOGD(TAG, "Sending remote code");
uint32_t on_time, off_time;
this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
this->target_time_ = 0;
this->transmit_trigger_->trigger();
for (uint32_t i = 0; i < send_times; i++) {
InterruptLock lock;
for (int32_t item : this->temp_.get_data()) {
if (item > 0) {
const auto length = uint32_t(item);
this->mark_(on_time, off_time, length);
} else {
const auto length = uint32_t(-item);
this->space_(length);
}
App.feed_wdt();
}
this->await_target_time_(); // wait for duration of last pulse
this->pin_->digital_write(false);
if (i + 1 < send_times)
this->target_time_ += send_wait;
}
this->complete_trigger_->trigger();
}
} // namespace remote_transmitter
} // namespace esphome
#endif

View File

@@ -0,0 +1,107 @@
#include "remote_transmitter.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#ifdef USE_ESP8266
namespace esphome {
namespace remote_transmitter {
static const char *const TAG = "remote_transmitter";
void RemoteTransmitterComponent::setup() {
this->pin_->setup();
this->pin_->digital_write(false);
}
void RemoteTransmitterComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Remote Transmitter:\n"
" Carrier Duty: %u%%",
this->carrier_duty_percent_);
LOG_PIN(" Pin: ", this->pin_);
}
void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period,
uint32_t *off_time_period) {
if (carrier_frequency == 0) {
*on_time_period = 0;
*off_time_period = 0;
return;
}
uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq)
period = std::max(uint32_t(1), period);
*on_time_period = (period * this->carrier_duty_percent_) / 100;
*off_time_period = period - *on_time_period;
}
void RemoteTransmitterComponent::await_target_time_() {
const uint32_t current_time = micros();
if (this->target_time_ == 0) {
this->target_time_ = current_time;
} else if ((int32_t) (this->target_time_ - current_time) > 0) {
delayMicroseconds(this->target_time_ - current_time);
}
}
void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) {
this->await_target_time_();
this->pin_->digital_write(true);
const uint32_t target = this->target_time_ + usec;
if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) {
while (true) { // Modulate with carrier frequency
this->target_time_ += on_time;
if ((int32_t) (this->target_time_ - target) >= 0)
break;
this->await_target_time_();
this->pin_->digital_write(false);
this->target_time_ += off_time;
if ((int32_t) (this->target_time_ - target) >= 0)
break;
this->await_target_time_();
this->pin_->digital_write(true);
}
}
this->target_time_ = target;
}
void RemoteTransmitterComponent::space_(uint32_t usec) {
this->await_target_time_();
this->pin_->digital_write(false);
this->target_time_ += usec;
}
void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); }
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
ESP_LOGD(TAG, "Sending remote code");
uint32_t on_time, off_time;
this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
this->target_time_ = 0;
this->transmit_trigger_->trigger();
for (uint32_t i = 0; i < send_times; i++) {
for (int32_t item : this->temp_.get_data()) {
if (item > 0) {
const auto length = uint32_t(item);
this->mark_(on_time, off_time, length);
} else {
const auto length = uint32_t(-item);
this->space_(length);
}
App.feed_wdt();
}
this->await_target_time_(); // wait for duration of last pulse
this->pin_->digital_write(false);
if (i + 1 < send_times)
this->target_time_ += send_wait;
}
this->complete_trigger_->trigger();
}
} // namespace remote_transmitter
} // namespace esphome
#endif

View File

@@ -0,0 +1,110 @@
#include "remote_transmitter.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#ifdef USE_LIBRETINY
namespace esphome {
namespace remote_transmitter {
static const char *const TAG = "remote_transmitter";
void RemoteTransmitterComponent::setup() {
this->pin_->setup();
this->pin_->digital_write(false);
}
void RemoteTransmitterComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Remote Transmitter:\n"
" Carrier Duty: %u%%",
this->carrier_duty_percent_);
LOG_PIN(" Pin: ", this->pin_);
}
void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period,
uint32_t *off_time_period) {
if (carrier_frequency == 0) {
*on_time_period = 0;
*off_time_period = 0;
return;
}
uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq)
period = std::max(uint32_t(1), period);
*on_time_period = (period * this->carrier_duty_percent_) / 100;
*off_time_period = period - *on_time_period;
}
void RemoteTransmitterComponent::await_target_time_() {
const uint32_t current_time = micros();
if (this->target_time_ == 0) {
this->target_time_ = current_time;
} else {
while ((int32_t) (this->target_time_ - micros()) > 0) {
// busy loop that ensures micros is constantly called
}
}
}
void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) {
this->await_target_time_();
this->pin_->digital_write(true);
const uint32_t target = this->target_time_ + usec;
if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) {
while (true) { // Modulate with carrier frequency
this->target_time_ += on_time;
if ((int32_t) (this->target_time_ - target) >= 0)
break;
this->await_target_time_();
this->pin_->digital_write(false);
this->target_time_ += off_time;
if ((int32_t) (this->target_time_ - target) >= 0)
break;
this->await_target_time_();
this->pin_->digital_write(true);
}
}
this->target_time_ = target;
}
void RemoteTransmitterComponent::space_(uint32_t usec) {
this->await_target_time_();
this->pin_->digital_write(false);
this->target_time_ += usec;
}
void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); }
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
ESP_LOGD(TAG, "Sending remote code");
uint32_t on_time, off_time;
this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
this->target_time_ = 0;
this->transmit_trigger_->trigger();
for (uint32_t i = 0; i < send_times; i++) {
InterruptLock lock;
for (int32_t item : this->temp_.get_data()) {
if (item > 0) {
const auto length = uint32_t(item);
this->mark_(on_time, off_time, length);
} else {
const auto length = uint32_t(-item);
this->space_(length);
}
App.feed_wdt();
}
this->await_target_time_(); // wait for duration of last pulse
this->pin_->digital_write(false);
if (i + 1 < send_times)
this->target_time_ += send_wait;
}
this->complete_trigger_->trigger();
}
} // namespace remote_transmitter
} // namespace esphome
#endif

View File

@@ -16,6 +16,7 @@ from esphome.const import (
CONF_DUMMY_RECEIVER_ID,
CONF_ID,
CONF_INVERT,
CONF_INVERTED,
CONF_LAMBDA,
CONF_NUMBER,
CONF_PORT,
@@ -38,6 +39,9 @@ uart_ns = cg.esphome_ns.namespace("uart")
UARTComponent = uart_ns.class_("UARTComponent")
IDFUARTComponent = uart_ns.class_("IDFUARTComponent", UARTComponent, cg.Component)
ESP32ArduinoUARTComponent = uart_ns.class_(
"ESP32ArduinoUARTComponent", UARTComponent, cg.Component
)
ESP8266UartComponent = uart_ns.class_(
"ESP8266UartComponent", UARTComponent, cg.Component
)
@@ -49,6 +53,7 @@ HostUartComponent = uart_ns.class_("HostUartComponent", UARTComponent, cg.Compon
NATIVE_UART_CLASSES = (
str(IDFUARTComponent),
str(ESP32ArduinoUARTComponent),
str(ESP8266UartComponent),
str(RP2040UartComponent),
str(LibreTinyUARTComponent),
@@ -114,6 +119,20 @@ def validate_rx_pin(value):
return value
def validate_invert_esp32(config):
if (
CORE.is_esp32
and CORE.using_arduino
and CONF_TX_PIN in config
and CONF_RX_PIN in config
and config[CONF_TX_PIN][CONF_INVERTED] != config[CONF_RX_PIN][CONF_INVERTED]
):
raise cv.Invalid(
"Different invert values for TX and RX pin are not supported for ESP32 when using Arduino."
)
return config
def validate_host_config(config):
if CORE.is_host:
if CONF_TX_PIN in config or CONF_RX_PIN in config:
@@ -132,7 +151,10 @@ def _uart_declare_type(value):
if CORE.is_esp8266:
return cv.declare_id(ESP8266UartComponent)(value)
if CORE.is_esp32:
return cv.declare_id(IDFUARTComponent)(value)
if CORE.using_arduino:
return cv.declare_id(ESP32ArduinoUARTComponent)(value)
if CORE.using_esp_idf:
return cv.declare_id(IDFUARTComponent)(value)
if CORE.is_rp2040:
return cv.declare_id(RP2040UartComponent)(value)
if CORE.is_libretiny:
@@ -233,6 +255,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT),
validate_invert_esp32,
validate_host_config,
)
@@ -421,10 +444,8 @@ async def uart_write_to_code(config, action_id, template_arg, args):
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"uart_component_esp_idf.cpp": {
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
"uart_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
"uart_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
"uart_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"uart_component_host.cpp": {PlatformFramework.HOST_NATIVE},
"uart_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},

View File

@@ -0,0 +1,214 @@
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "uart_component_esp32_arduino.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
namespace esphome {
namespace uart {
static const char *const TAG = "uart.arduino_esp32";
static const uint32_t UART_PARITY_EVEN = 0 << 0;
static const uint32_t UART_PARITY_ODD = 1 << 0;
static const uint32_t UART_PARITY_ENABLE = 1 << 1;
static const uint32_t UART_NB_BIT_5 = 0 << 2;
static const uint32_t UART_NB_BIT_6 = 1 << 2;
static const uint32_t UART_NB_BIT_7 = 2 << 2;
static const uint32_t UART_NB_BIT_8 = 3 << 2;
static const uint32_t UART_NB_STOP_BIT_1 = 1 << 4;
static const uint32_t UART_NB_STOP_BIT_2 = 3 << 4;
static const uint32_t UART_TICK_APB_CLOCK = 1 << 27;
uint32_t ESP32ArduinoUARTComponent::get_config() {
uint32_t config = 0;
/*
* All bits numbers below come from
* framework-arduinoespressif32/cores/esp32/esp32-hal-uart.h
* And more specifically conf0 union in uart_dev_t.
*
* Below is bit used from conf0 union.
* <name>:<bits position> <values>
* parity:0 0:even 1:odd
* parity_en:1 Set this bit to enable uart parity check.
* bit_num:2-4 0:5bits 1:6bits 2:7bits 3:8bits
* stop_bit_num:4-6 stop bit. 1:1bit 2:1.5bits 3:2bits
* tick_ref_always_on:27 select the clock.1apb clockref_tick
*/
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
config |= UART_PARITY_EVEN | UART_PARITY_ENABLE;
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
config |= UART_PARITY_ODD | UART_PARITY_ENABLE;
}
switch (this->data_bits_) {
case 5:
config |= UART_NB_BIT_5;
break;
case 6:
config |= UART_NB_BIT_6;
break;
case 7:
config |= UART_NB_BIT_7;
break;
case 8:
config |= UART_NB_BIT_8;
break;
}
if (this->stop_bits_ == 1) {
config |= UART_NB_STOP_BIT_1;
} else {
config |= UART_NB_STOP_BIT_2;
}
config |= UART_TICK_APB_CLOCK;
return config;
}
void ESP32ArduinoUARTComponent::setup() {
// Use Arduino HardwareSerial UARTs if all used pins match the ones
// preconfigured by the platform. For example if RX disabled but TX pin
// is 1 we still want to use Serial.
bool is_default_tx, is_default_rx;
#ifdef CONFIG_IDF_TARGET_ESP32C3
is_default_tx = tx_pin_ == nullptr || tx_pin_->get_pin() == 21;
is_default_rx = rx_pin_ == nullptr || rx_pin_->get_pin() == 20;
#else
is_default_tx = tx_pin_ == nullptr || tx_pin_->get_pin() == 1;
is_default_rx = rx_pin_ == nullptr || rx_pin_->get_pin() == 3;
#endif
static uint8_t next_uart_num = 0;
if (is_default_tx && is_default_rx && next_uart_num == 0) {
#if ARDUINO_USB_CDC_ON_BOOT
this->hw_serial_ = &Serial0;
#else
this->hw_serial_ = &Serial;
#endif
next_uart_num++;
} else {
#ifdef USE_LOGGER
bool logger_uses_hardware_uart = true;
#ifdef USE_LOGGER_USB_CDC
if (logger::global_logger->get_uart() == logger::UART_SELECTION_USB_CDC) {
// this is not a hardware UART, ignore it
logger_uses_hardware_uart = false;
}
#endif // USE_LOGGER_USB_CDC
#ifdef USE_LOGGER_USB_SERIAL_JTAG
if (logger::global_logger->get_uart() == logger::UART_SELECTION_USB_SERIAL_JTAG) {
// this is not a hardware UART, ignore it
logger_uses_hardware_uart = false;
}
#endif // USE_LOGGER_USB_SERIAL_JTAG
if (logger_uses_hardware_uart && logger::global_logger->get_baud_rate() > 0 &&
logger::global_logger->get_uart() == next_uart_num) {
next_uart_num++;
}
#endif // USE_LOGGER
if (next_uart_num >= SOC_UART_NUM) {
ESP_LOGW(TAG, "Maximum number of UART components created already.");
this->mark_failed();
return;
}
this->number_ = next_uart_num;
this->hw_serial_ = new HardwareSerial(next_uart_num++); // NOLINT(cppcoreguidelines-owning-memory)
}
this->load_settings(false);
}
void ESP32ArduinoUARTComponent::load_settings(bool dump_config) {
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
bool invert = false;
if (tx_pin_ != nullptr && tx_pin_->is_inverted())
invert = true;
if (rx_pin_ != nullptr && rx_pin_->is_inverted())
invert = true;
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, invert);
if (dump_config) {
ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->number_);
this->dump_config();
}
}
void ESP32ArduinoUARTComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART Bus %d:", this->number_);
LOG_PIN(" TX Pin: ", tx_pin_);
LOG_PIN(" RX Pin: ", rx_pin_);
if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_);
}
ESP_LOGCONFIG(TAG,
" Baud Rate: %u baud\n"
" Data Bits: %u\n"
" Parity: %s\n"
" Stop bits: %u",
this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_);
this->check_logger_conflict();
}
void ESP32ArduinoUARTComponent::write_array(const uint8_t *data, size_t len) {
this->hw_serial_->write(data, len);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
}
#endif
}
bool ESP32ArduinoUARTComponent::peek_byte(uint8_t *data) {
if (!this->check_read_timeout_())
return false;
*data = this->hw_serial_->peek();
return true;
}
bool ESP32ArduinoUARTComponent::read_array(uint8_t *data, size_t len) {
if (!this->check_read_timeout_(len))
return false;
this->hw_serial_->readBytes(data, len);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
}
#endif
return true;
}
int ESP32ArduinoUARTComponent::available() { return this->hw_serial_->available(); }
void ESP32ArduinoUARTComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
this->hw_serial_->flush();
}
void ESP32ArduinoUARTComponent::check_logger_conflict() {
#ifdef USE_LOGGER
if (this->hw_serial_ == nullptr || logger::global_logger->get_baud_rate() == 0) {
return;
}
if (this->hw_serial_ == logger::global_logger->get_hw_serial()) {
ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please "
"disable logging over the serial port by setting logger->baud_rate to 0.");
}
#endif
}
} // namespace uart
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@@ -0,0 +1,60 @@
#pragma once
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include <driver/uart.h>
#include <HardwareSerial.h>
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "uart_component.h"
namespace esphome {
namespace uart {
class ESP32ArduinoUARTComponent : public UARTComponent, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
int available() override;
void flush() override;
uint32_t get_config();
HardwareSerial *get_hw_serial() { return this->hw_serial_; }
uint8_t get_hw_serial_number() { return this->number_; }
/**
* Load the UART with the current settings.
* @param dump_config (Optional, default `true`): True for displaying new settings or
* false to change it quitely
*
* Example:
* ```cpp
* id(uart1).load_settings();
* ```
*
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
*/
void load_settings(bool dump_config) override;
void load_settings() override { this->load_settings(true); }
protected:
void check_logger_conflict() override;
HardwareSerial *hw_serial_{nullptr};
uint8_t number_{0};
};
} // namespace uart
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@@ -1,4 +1,4 @@
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include "uart_component_esp_idf.h"
#include <cinttypes>

View File

@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include <driver/uart.h>
#include "esphome/core/component.h"
@@ -55,4 +55,4 @@ class IDFUARTComponent : public UARTComponent, public Component {
} // namespace uart
} // namespace esphome
#endif // USE_ESP32
#endif // USE_ESP_IDF

View File

@@ -3,7 +3,8 @@ from esphome.components.esp32 import add_idf_component
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network", "web_server_base"]
@@ -22,7 +23,7 @@ CONFIG_SCHEMA = (
)
@coroutine_with_priority(CoroPriority.COMMUNICATION)
@coroutine_with_priority(CoroPriority.WEB_SERVER_OTA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ota_to_code(var, config)

View File

@@ -1,7 +1,8 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"]
@@ -26,7 +27,7 @@ CONFIG_SCHEMA = cv.Schema(
)
@coroutine_with_priority(CoroPriority.COMMUNICATION)
@coroutine_with_priority(CoroPriority.WEB_SERVER_BASE)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -402,7 +402,7 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
# Disable Enterprise WiFi support if no EAP is configured
if CORE.is_esp32 and not has_eap:
if CORE.is_esp32 and CORE.using_esp_idf and not has_eap:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False)
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))

View File

@@ -118,7 +118,7 @@ async def to_code(config):
# Workaround for crash on IDF 5+
# See https://github.com/trombik/esp_wireguard/issues/33#issuecomment-1568503651
if CORE.is_esp32:
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_LWIP_PPP_SUPPORT", True)
# This flag is added here because the esp_wireguard library statically

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2025.10.0-dev"
__version__ = "2025.9.1"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
@@ -186,7 +186,6 @@ CONF_CHARACTERISTIC_UUID = "characteristic_uuid"
CONF_CHECK = "check"
CONF_CHIPSET = "chipset"
CONF_CLEAN_SESSION = "clean_session"
CONF_CLEAR = "clear"
CONF_CLEAR_IMPEDANCE = "clear_impedance"
CONF_CLIENT_CERTIFICATE = "client_certificate"
CONF_CLIENT_CERTIFICATE_KEY = "client_certificate_key"

View File

@@ -0,0 +1,12 @@
#include "string_ref.h"
namespace esphome {
#ifdef USE_JSON
// NOLINTNEXTLINE(readability-identifier-naming)
void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); }
#endif // USE_JSON
} // namespace esphome

View File

@@ -130,7 +130,7 @@ inline std::string operator+(const StringRef &lhs, const char *rhs) {
#ifdef USE_JSON
// NOLINTNEXTLINE(readability-identifier-naming)
inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); }
void convertToJson(const StringRef &src, JsonVariant dst);
#endif // USE_JSON
} // namespace esphome

View File

@@ -90,11 +90,30 @@ class CoroPriority(enum.IntEnum):
# Examples: status_led (80)
STATUS = 80
# Web server infrastructure
# Examples: web_server_base (65)
WEB_SERVER_BASE = 65
# Network portal services
# Examples: captive_portal (64)
CAPTIVE_PORTAL = 64
# Communication protocols and services
# Examples: web_server_base (65), captive_portal (64), wifi (60), ethernet (60),
# mdns (55), ota_updates (54), web_server_ota (52)
# Examples: wifi (60), ethernet (60)
COMMUNICATION = 60
# Network discovery and management services
# Examples: mdns (55)
NETWORK_SERVICES = 55
# OTA update services
# Examples: ota_updates (54)
OTA_UPDATES = 54
# Web-based OTA services
# Examples: web_server_ota (52)
WEB_SERVER_OTA = 52
# Application-level services
# Examples: safe_mode (50)
APPLICATION = 50

View File

@@ -70,8 +70,6 @@ FILTER_PLATFORMIO_LINES = [
r" - tool-esptool.* \(.*\)",
r" - toolchain-.* \(.*\)",
r"Creating BIN file .*",
r"Warning! Could not find file \".*.crt\"",
r"Warning! Arduino framework as an ESP-IDF component doesn't handle the `variant` field! The default `esp32` variant will be used.",
]

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "esphome"
license = "MIT"
license = {text = "MIT"}
description = "ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems."
readme = "README.md"
authors = [
@@ -15,6 +15,7 @@ classifiers = [
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Programming Language :: C++",
"Programming Language :: Python :: 3",
"Topic :: Home Automation",

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.0.2
click==8.1.7
esphome-dashboard==20250904.0
aioesphomeapi==41.1.0
aioesphomeapi==40.2.1
zeroconf==0.147.2
puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import

View File

@@ -1,14 +1,14 @@
pylint==3.3.8
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.13.0 # also change in .pre-commit-config.yaml when updating
ruff==0.12.12 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
pre-commit
# Unit tests
pytest==8.4.2
pytest-cov==7.0.0
pytest-mock==3.15.1
pytest-asyncio==1.2.0
pytest-mock==3.15.0
pytest-asyncio==1.1.0
pytest-xdist==3.8.0
asyncmock==0.4.2
hypothesis==6.92.1

View File

@@ -848,17 +848,10 @@ class FixedArrayBytesType(TypeInfo):
@property
def public_content(self) -> list[str]:
len_type = (
"uint8_t"
if self.array_size <= 255
else "uint16_t"
if self.array_size <= 65535
else "size_t"
)
# Add both the array and length fields
return [
f"uint8_t {self.field_name}[{self.array_size}]{{}};",
f"{len_type} {self.field_name}_len{{0}};",
f"uint8_t {self.field_name}_len{{0}};",
]
@property

View File

@@ -1,18 +1,14 @@
#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
import os
import subprocess
import sys
import tempfile
from esphome.components.esp32 import ESP_IDF_PLATFORM_VERSION as ver
from esphome.helpers import write_file_if_changed
version_str = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
root = Path(__file__).parent.parent
boards_file_path = root / "esphome" / "components" / "esp32" / "boards.py"
print(f"ESP32 Platform Version: {version_str}")
def get_boards():
@@ -21,9 +17,6 @@ def get_boards():
[
"git",
"clone",
"-q",
"-c",
"advice.detachedHead=false",
"--depth",
"1",
"--branch",
@@ -33,14 +26,16 @@ def get_boards():
],
check=True,
)
boards_directory = Path(tempdir) / "boards"
boards_file = os.path.join(tempdir, "boards")
boards = {}
for fname in boards_directory.glob("*.json"):
with fname.open(encoding="utf-8") as f:
for fname in os.listdir(boards_file):
if not fname.endswith(".json"):
continue
with open(os.path.join(boards_file, fname), encoding="utf-8") as f:
board_info = json.load(f)
mcu = board_info["build"]["mcu"]
name = board_info["name"]
board = fname.stem
board = fname[:-5]
variant = mcu.upper()
boards[board] = {
"name": name,
@@ -52,47 +47,33 @@ def get_boards():
TEMPLATE = """ "%s": {
"name": "%s",
"variant": %s,
},"""
},
"""
def main(check: bool):
def main():
boards = get_boards()
# open boards.py, delete existing BOARDS variable and write the new boards dict
existing_content = boards_file_path.read_text(encoding="UTF-8")
boards_file_path = os.path.join(
os.path.dirname(__file__), "..", "esphome", "components", "esp32", "boards.py"
)
with open(boards_file_path, encoding="UTF-8") as f:
lines = f.readlines()
parts: list[str] = []
for line in existing_content.splitlines():
if line == "BOARDS = {":
parts.append(line)
parts.extend(
TEMPLATE % (board, info["name"], info["variant"])
for board, info in sorted(boards.items())
)
parts.append("}")
parts.append("# DO NOT ADD ANYTHING BELOW THIS LINE")
break
with open(boards_file_path, "w", encoding="UTF-8") as f:
for line in lines:
if line.startswith("BOARDS = {"):
f.write("BOARDS = {\n")
f.writelines(
TEMPLATE % (board, info["name"], info["variant"])
for board, info in sorted(boards.items())
)
f.write("}\n")
break
parts.append(line)
parts.append("")
content = "\n".join(parts)
if check:
if existing_content != content:
print("boards.py file is not up to date.")
print("Please run `script/generate-esp32-boards.py`")
sys.exit(1)
print("boards.py file is up to date")
elif write_file_if_changed(boards_file_path, content):
print("ESP32 boards updated successfully.")
f.write(line)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--check",
help="Check if the boards.py file is up to date.",
action="store_true",
)
args = parser.parse_args()
main(args.check)
main()
print("ESP32 boards updated successfully.")

View File

@@ -0,0 +1,42 @@
# Comprehensive ESP8266 test for mdns with multiple network components
# Tests the complete priority chain:
# wifi (60) -> mdns (55) -> ota (54) -> web_server_ota (52)
esphome:
name: mdns-comprehensive-test
esp8266:
board: esp01_1m
logger:
level: DEBUG
wifi:
ssid: MySSID
password: password1
# web_server_base should run at priority 65 (before wifi)
web_server:
port: 80
# mdns should run at priority 55 (after wifi at 60)
mdns:
services:
- service: _http
protocol: _tcp
port: 80
# OTA should run at priority 54 (after mdns)
ota:
- platform: esphome
password: "otapassword"
# Test status LED at priority 80
status_led:
pin:
number: GPIO2
inverted: true
# Include API at priority 40
api:
password: "apipassword"

View File

@@ -1,4 +1,8 @@
substitutions:
network_enable_ipv6: "true"
bk72xx:
framework:
version: 1.7.0
<<: !include common.yaml

View File

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

View File

@@ -1,203 +0,0 @@
"""Tests for dashboard entries Path-related functionality."""
from __future__ import annotations
from pathlib import Path
import tempfile
from unittest.mock import MagicMock
import pytest
import pytest_asyncio
from esphome.core import CORE
from esphome.dashboard.entries import DashboardEntries, DashboardEntry
def create_cache_key() -> tuple[int, int, float, int]:
"""Helper to create a valid DashboardCacheKeyType."""
return (0, 0, 0.0, 0)
@pytest.fixture(autouse=True)
def setup_core():
"""Set up CORE for testing."""
with tempfile.TemporaryDirectory() as tmpdir:
CORE.config_path = str(Path(tmpdir) / "test.yaml")
yield
CORE.reset()
@pytest.fixture
def mock_settings() -> MagicMock:
"""Create mock dashboard settings."""
settings = MagicMock()
settings.config_dir = "/test/config"
settings.absolute_config_dir = Path("/test/config")
return settings
@pytest_asyncio.fixture
async def dashboard_entries(mock_settings: MagicMock) -> DashboardEntries:
"""Create a DashboardEntries instance for testing."""
return DashboardEntries(mock_settings)
def test_dashboard_entry_path_initialization() -> None:
"""Test DashboardEntry initializes with path correctly."""
test_path = "/test/config/device.yaml"
cache_key = create_cache_key()
entry = DashboardEntry(test_path, cache_key)
assert entry.path == test_path
assert entry.cache_key == cache_key
def test_dashboard_entry_path_with_absolute_path() -> None:
"""Test DashboardEntry handles absolute paths."""
# Use a truly absolute path for the platform
test_path = Path.cwd() / "absolute" / "path" / "to" / "config.yaml"
cache_key = create_cache_key()
entry = DashboardEntry(str(test_path), cache_key)
assert entry.path == str(test_path)
assert Path(entry.path).is_absolute()
def test_dashboard_entry_path_with_relative_path() -> None:
"""Test DashboardEntry handles relative paths."""
test_path = "configs/device.yaml"
cache_key = create_cache_key()
entry = DashboardEntry(test_path, cache_key)
assert entry.path == test_path
assert not Path(entry.path).is_absolute()
@pytest.mark.asyncio
async def test_dashboard_entries_get_by_path(
dashboard_entries: DashboardEntries,
) -> None:
"""Test getting entry by path."""
test_path = "/test/config/device.yaml"
entry = DashboardEntry(test_path, create_cache_key())
dashboard_entries._entries[test_path] = entry
result = dashboard_entries.get(test_path)
assert result == entry
@pytest.mark.asyncio
async def test_dashboard_entries_get_nonexistent_path(
dashboard_entries: DashboardEntries,
) -> None:
"""Test getting non-existent entry returns None."""
result = dashboard_entries.get("/nonexistent/path.yaml")
assert result is None
@pytest.mark.asyncio
async def test_dashboard_entries_path_normalization(
dashboard_entries: DashboardEntries,
) -> None:
"""Test that paths are handled consistently."""
path1 = "/test/config/device.yaml"
entry = DashboardEntry(path1, create_cache_key())
dashboard_entries._entries[path1] = entry
result = dashboard_entries.get(path1)
assert result == entry
@pytest.mark.asyncio
async def test_dashboard_entries_path_with_spaces(
dashboard_entries: DashboardEntries,
) -> None:
"""Test handling paths with spaces."""
test_path = "/test/config/my device.yaml"
entry = DashboardEntry(test_path, create_cache_key())
dashboard_entries._entries[test_path] = entry
result = dashboard_entries.get(test_path)
assert result == entry
assert result.path == test_path
@pytest.mark.asyncio
async def test_dashboard_entries_path_with_special_chars(
dashboard_entries: DashboardEntries,
) -> None:
"""Test handling paths with special characters."""
test_path = "/test/config/device-01_test.yaml"
entry = DashboardEntry(test_path, create_cache_key())
dashboard_entries._entries[test_path] = entry
result = dashboard_entries.get(test_path)
assert result == entry
def test_dashboard_entries_windows_path() -> None:
"""Test handling Windows-style paths."""
test_path = r"C:\Users\test\esphome\device.yaml"
cache_key = create_cache_key()
entry = DashboardEntry(test_path, cache_key)
assert entry.path == test_path
@pytest.mark.asyncio
async def test_dashboard_entries_path_to_cache_key_mapping(
dashboard_entries: DashboardEntries,
) -> None:
"""Test internal entries storage with paths and cache keys."""
path1 = "/test/config/device1.yaml"
path2 = "/test/config/device2.yaml"
entry1 = DashboardEntry(path1, create_cache_key())
entry2 = DashboardEntry(path2, (1, 1, 1.0, 1))
dashboard_entries._entries[path1] = entry1
dashboard_entries._entries[path2] = entry2
assert path1 in dashboard_entries._entries
assert path2 in dashboard_entries._entries
assert dashboard_entries._entries[path1].cache_key == create_cache_key()
assert dashboard_entries._entries[path2].cache_key == (1, 1, 1.0, 1)
def test_dashboard_entry_path_property() -> None:
"""Test that path property returns expected value."""
test_path = "/test/config/device.yaml"
entry = DashboardEntry(test_path, create_cache_key())
assert entry.path == test_path
assert isinstance(entry.path, str)
@pytest.mark.asyncio
async def test_dashboard_entries_all_returns_entries_with_paths(
dashboard_entries: DashboardEntries,
) -> None:
"""Test that all() returns entries with their paths intact."""
paths = [
"/test/config/device1.yaml",
"/test/config/device2.yaml",
"/test/config/subfolder/device3.yaml",
]
for path in paths:
entry = DashboardEntry(path, create_cache_key())
dashboard_entries._entries[path] = entry
all_entries = dashboard_entries.async_all()
assert len(all_entries) == len(paths)
retrieved_paths = [entry.path for entry in all_entries]
assert set(retrieved_paths) == set(paths)

View File

@@ -1,168 +0,0 @@
"""Tests for dashboard settings Path-related functionality."""
from __future__ import annotations
import os
from pathlib import Path
import tempfile
import pytest
from esphome.dashboard.settings import DashboardSettings
@pytest.fixture
def dashboard_settings(tmp_path: Path) -> DashboardSettings:
"""Create DashboardSettings instance with temp directory."""
settings = DashboardSettings()
# Resolve symlinks to ensure paths match
resolved_dir = tmp_path.resolve()
settings.config_dir = str(resolved_dir)
settings.absolute_config_dir = resolved_dir
return settings
def test_rel_path_simple(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path with simple relative path."""
result = dashboard_settings.rel_path("config.yaml")
expected = str(Path(dashboard_settings.config_dir) / "config.yaml")
assert result == expected
def test_rel_path_multiple_components(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path with multiple path components."""
result = dashboard_settings.rel_path("subfolder", "device", "config.yaml")
expected = str(
Path(dashboard_settings.config_dir) / "subfolder" / "device" / "config.yaml"
)
assert result == expected
def test_rel_path_with_dots(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path prevents directory traversal."""
# This should raise ValueError as it tries to go outside config_dir
with pytest.raises(ValueError):
dashboard_settings.rel_path("..", "outside.yaml")
def test_rel_path_absolute_path_within_config(
dashboard_settings: DashboardSettings,
) -> None:
"""Test rel_path with absolute path that's within config dir."""
internal_path = dashboard_settings.absolute_config_dir / "internal.yaml"
internal_path.touch()
result = dashboard_settings.rel_path("internal.yaml")
expected = str(Path(dashboard_settings.config_dir) / "internal.yaml")
assert result == expected
def test_rel_path_absolute_path_outside_config(
dashboard_settings: DashboardSettings,
) -> None:
"""Test rel_path with absolute path outside config dir raises error."""
outside_path = "/tmp/outside/config.yaml"
with pytest.raises(ValueError):
dashboard_settings.rel_path(outside_path)
def test_rel_path_empty_args(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path with no arguments returns config_dir."""
result = dashboard_settings.rel_path()
assert result == dashboard_settings.config_dir
def test_rel_path_with_pathlib_path(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path works with Path objects as arguments."""
path_obj = Path("subfolder") / "config.yaml"
result = dashboard_settings.rel_path(path_obj)
expected = str(Path(dashboard_settings.config_dir) / "subfolder" / "config.yaml")
assert result == expected
def test_rel_path_normalizes_slashes(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path normalizes path separators."""
# os.path.join normalizes slashes on Windows but preserves them on Unix
# Test that providing components separately gives same result
result1 = dashboard_settings.rel_path("folder", "subfolder", "file.yaml")
result2 = dashboard_settings.rel_path("folder", "subfolder", "file.yaml")
assert result1 == result2
# Also test that the result is as expected
expected = os.path.join(
dashboard_settings.config_dir, "folder", "subfolder", "file.yaml"
)
assert result1 == expected
def test_rel_path_handles_spaces(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path handles paths with spaces."""
result = dashboard_settings.rel_path("my folder", "my config.yaml")
expected = str(Path(dashboard_settings.config_dir) / "my folder" / "my config.yaml")
assert result == expected
def test_rel_path_handles_special_chars(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path handles paths with special characters."""
result = dashboard_settings.rel_path("device-01_test", "config.yaml")
expected = str(
Path(dashboard_settings.config_dir) / "device-01_test" / "config.yaml"
)
assert result == expected
def test_config_dir_as_path_property(dashboard_settings: DashboardSettings) -> None:
"""Test that config_dir can be accessed and used with Path operations."""
config_path = Path(dashboard_settings.config_dir)
assert config_path.exists()
assert config_path.is_dir()
assert config_path.is_absolute()
def test_absolute_config_dir_property(dashboard_settings: DashboardSettings) -> None:
"""Test absolute_config_dir is a Path object."""
assert isinstance(dashboard_settings.absolute_config_dir, Path)
assert dashboard_settings.absolute_config_dir.exists()
assert dashboard_settings.absolute_config_dir.is_dir()
assert dashboard_settings.absolute_config_dir.is_absolute()
def test_rel_path_symlink_inside_config(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path with symlink that points inside config dir."""
target = dashboard_settings.absolute_config_dir / "target.yaml"
target.touch()
symlink = dashboard_settings.absolute_config_dir / "link.yaml"
symlink.symlink_to(target)
result = dashboard_settings.rel_path("link.yaml")
expected = str(Path(dashboard_settings.config_dir) / "link.yaml")
assert result == expected
def test_rel_path_symlink_outside_config(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path with symlink that points outside config dir."""
with tempfile.NamedTemporaryFile(suffix=".yaml") as tmp:
symlink = dashboard_settings.absolute_config_dir / "external_link.yaml"
symlink.symlink_to(tmp.name)
with pytest.raises(ValueError):
dashboard_settings.rel_path("external_link.yaml")
def test_rel_path_with_none_arg(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path handles None arguments gracefully."""
result = dashboard_settings.rel_path("None")
expected = str(Path(dashboard_settings.config_dir) / "None")
assert result == expected
def test_rel_path_with_numeric_args(dashboard_settings: DashboardSettings) -> None:
"""Test rel_path handles numeric arguments."""
result = dashboard_settings.rel_path("123", "456.789")
expected = str(Path(dashboard_settings.config_dir) / "123" / "456.789")
assert result == expected

View File

@@ -1,230 +0,0 @@
"""Tests for dashboard web_server Path-related functionality."""
from __future__ import annotations
import gzip
import os
from pathlib import Path
from unittest.mock import MagicMock, patch
from esphome.dashboard import web_server
def test_get_base_frontend_path_production() -> None:
"""Test get_base_frontend_path in production mode."""
mock_module = MagicMock()
mock_module.where.return_value = "/usr/local/lib/esphome_dashboard"
with (
patch.dict(os.environ, {}, clear=True),
patch.dict("sys.modules", {"esphome_dashboard": mock_module}),
):
result = web_server.get_base_frontend_path()
assert result == "/usr/local/lib/esphome_dashboard"
mock_module.where.assert_called_once()
def test_get_base_frontend_path_dev_mode() -> None:
"""Test get_base_frontend_path in development mode."""
test_path = "/home/user/esphome/dashboard"
with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}):
result = web_server.get_base_frontend_path()
# The function uses os.path.abspath which doesn't resolve symlinks
# We need to match that behavior
# The actual function adds "/" to the path, so we simulate that
test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/"
expected = os.path.abspath(
os.path.join(os.getcwd(), test_path_with_slash, "esphome_dashboard")
)
assert result == expected
def test_get_base_frontend_path_dev_mode_with_trailing_slash() -> None:
"""Test get_base_frontend_path in dev mode with trailing slash."""
test_path = "/home/user/esphome/dashboard/"
with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}):
result = web_server.get_base_frontend_path()
# The function uses os.path.abspath which doesn't resolve symlinks
expected = os.path.abspath(str(Path.cwd() / test_path / "esphome_dashboard"))
assert result == expected
def test_get_base_frontend_path_dev_mode_relative_path() -> None:
"""Test get_base_frontend_path with relative dev path."""
test_path = "./dashboard"
with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}):
result = web_server.get_base_frontend_path()
# The function uses os.path.abspath which doesn't resolve symlinks
# We need to match that behavior
# The actual function adds "/" to the path, so we simulate that
test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/"
expected = os.path.abspath(
os.path.join(os.getcwd(), test_path_with_slash, "esphome_dashboard")
)
assert result == expected
assert Path(result).is_absolute()
def test_get_static_path_single_component() -> None:
"""Test get_static_path with single path component."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
result = web_server.get_static_path("file.js")
assert result == os.path.join("/base/frontend", "static", "file.js")
def test_get_static_path_multiple_components() -> None:
"""Test get_static_path with multiple path components."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
result = web_server.get_static_path("js", "esphome", "index.js")
assert result == os.path.join(
"/base/frontend", "static", "js", "esphome", "index.js"
)
def test_get_static_path_empty_args() -> None:
"""Test get_static_path with no arguments."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
result = web_server.get_static_path()
assert result == os.path.join("/base/frontend", "static")
def test_get_static_path_with_pathlib_path() -> None:
"""Test get_static_path with Path objects."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
path_obj = Path("js") / "app.js"
result = web_server.get_static_path(str(path_obj))
assert result == os.path.join("/base/frontend", "static", "js", "app.js")
def test_get_static_file_url_production() -> None:
"""Test get_static_file_url in production mode."""
web_server.get_static_file_url.cache_clear()
mock_module = MagicMock()
mock_file = MagicMock()
mock_file.read.return_value = b"test content"
mock_file.__enter__ = MagicMock(return_value=mock_file)
mock_file.__exit__ = MagicMock(return_value=None)
with (
patch.dict(os.environ, {}, clear=True),
patch.dict("sys.modules", {"esphome_dashboard": mock_module}),
patch("esphome.dashboard.web_server.get_static_path") as mock_get_path,
patch("esphome.dashboard.web_server.open", create=True, return_value=mock_file),
):
mock_get_path.return_value = "/fake/path/js/app.js"
result = web_server.get_static_file_url("js/app.js")
assert result.startswith("./static/js/app.js?hash=")
def test_get_static_file_url_dev_mode() -> None:
"""Test get_static_file_url in development mode."""
with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": "/dev/path"}):
web_server.get_static_file_url.cache_clear()
result = web_server.get_static_file_url("js/app.js")
assert result == "./static/js/app.js"
def test_get_static_file_url_index_js_special_case() -> None:
"""Test get_static_file_url replaces index.js with entrypoint."""
web_server.get_static_file_url.cache_clear()
mock_module = MagicMock()
mock_module.entrypoint.return_value = "main.js"
with (
patch.dict(os.environ, {}, clear=True),
patch.dict("sys.modules", {"esphome_dashboard": mock_module}),
):
result = web_server.get_static_file_url("js/esphome/index.js")
assert result == "./static/js/esphome/main.js"
def test_load_file_path(tmp_path: Path) -> None:
"""Test loading a file."""
test_file = tmp_path / "test.txt"
test_file.write_bytes(b"test content")
with open(test_file, "rb") as f:
content = f.read()
assert content == b"test content"
def test_load_file_compressed_path(tmp_path: Path) -> None:
"""Test loading a compressed file."""
test_file = tmp_path / "test.txt.gz"
with gzip.open(test_file, "wb") as gz:
gz.write(b"compressed content")
with gzip.open(test_file, "rb") as gz:
content = gz.read()
assert content == b"compressed content"
def test_path_normalization_in_static_path() -> None:
"""Test that paths are normalized correctly."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
# Test with separate components
result1 = web_server.get_static_path("js", "app.js")
result2 = web_server.get_static_path("js", "app.js")
assert result1 == result2
assert result1 == os.path.join("/base/frontend", "static", "js", "app.js")
def test_windows_path_handling() -> None:
"""Test handling of Windows-style paths."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = r"C:\Program Files\esphome\frontend"
result = web_server.get_static_path("js", "app.js")
# os.path.join should handle this correctly on the platform
expected = os.path.join(
r"C:\Program Files\esphome\frontend", "static", "js", "app.js"
)
assert result == expected
def test_path_with_special_characters() -> None:
"""Test paths with special characters."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/frontend"
result = web_server.get_static_path("js-modules", "app_v1.0.js")
assert result == os.path.join(
"/base/frontend", "static", "js-modules", "app_v1.0.js"
)
def test_path_with_spaces() -> None:
"""Test paths with spaces."""
with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
mock_base.return_value = "/base/my frontend"
result = web_server.get_static_path("my js", "my app.js")
assert result == os.path.join(
"/base/my frontend", "static", "my js", "my app.js"
)

View File

@@ -1,188 +0,0 @@
"""Tests for esphome.build_gen.platformio module."""
from __future__ import annotations
from collections.abc import Generator
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from esphome.build_gen import platformio
from esphome.core import CORE
@pytest.fixture
def mock_update_storage_json() -> Generator[MagicMock]:
"""Mock update_storage_json for all tests."""
with patch("esphome.build_gen.platformio.update_storage_json") as mock:
yield mock
@pytest.fixture
def mock_write_file_if_changed() -> Generator[MagicMock]:
"""Mock write_file_if_changed for tests."""
with patch("esphome.build_gen.platformio.write_file_if_changed") as mock:
yield mock
def test_write_ini_creates_new_file(
tmp_path: Path, mock_update_storage_json: MagicMock
) -> None:
"""Test write_ini creates a new platformio.ini file."""
CORE.build_path = str(tmp_path)
content = """
[env:test]
platform = espressif32
board = esp32dev
framework = arduino
"""
platformio.write_ini(content)
ini_file = tmp_path / "platformio.ini"
assert ini_file.exists()
file_content = ini_file.read_text()
assert content in file_content
assert platformio.INI_AUTO_GENERATE_BEGIN in file_content
assert platformio.INI_AUTO_GENERATE_END in file_content
def test_write_ini_updates_existing_file(
tmp_path: Path, mock_update_storage_json: MagicMock
) -> None:
"""Test write_ini updates existing platformio.ini file."""
CORE.build_path = str(tmp_path)
# Create existing file with custom content
ini_file = tmp_path / "platformio.ini"
existing_content = f"""
; Custom header
[platformio]
default_envs = test
{platformio.INI_AUTO_GENERATE_BEGIN}
; Old auto-generated content
[env:old]
platform = old
{platformio.INI_AUTO_GENERATE_END}
; Custom footer
"""
ini_file.write_text(existing_content)
# New content to write
new_content = """
[env:test]
platform = espressif32
board = esp32dev
framework = arduino
"""
platformio.write_ini(new_content)
file_content = ini_file.read_text()
# Check that custom parts are preserved
assert "; Custom header" in file_content
assert "[platformio]" in file_content
assert "default_envs = test" in file_content
assert "; Custom footer" in file_content
# Check that new content replaced old auto-generated content
assert new_content in file_content
assert "[env:old]" not in file_content
assert "platform = old" not in file_content
def test_write_ini_preserves_custom_sections(
tmp_path: Path, mock_update_storage_json: MagicMock
) -> None:
"""Test write_ini preserves custom sections outside auto-generate markers."""
CORE.build_path = str(tmp_path)
# Create existing file with multiple custom sections
ini_file = tmp_path / "platformio.ini"
existing_content = f"""
[platformio]
src_dir = .
include_dir = .
[common]
lib_deps =
Wire
SPI
{platformio.INI_AUTO_GENERATE_BEGIN}
[env:old]
platform = old
{platformio.INI_AUTO_GENERATE_END}
[env:custom]
upload_speed = 921600
monitor_speed = 115200
"""
ini_file.write_text(existing_content)
new_content = "[env:auto]\nplatform = new"
platformio.write_ini(new_content)
file_content = ini_file.read_text()
# All custom sections should be preserved
assert "[platformio]" in file_content
assert "src_dir = ." in file_content
assert "[common]" in file_content
assert "lib_deps" in file_content
assert "[env:custom]" in file_content
assert "upload_speed = 921600" in file_content
# New auto-generated content should replace old
assert "[env:auto]" in file_content
assert "platform = new" in file_content
assert "[env:old]" not in file_content
def test_write_ini_no_change_when_content_same(
tmp_path: Path,
mock_update_storage_json: MagicMock,
mock_write_file_if_changed: MagicMock,
) -> None:
"""Test write_ini doesn't rewrite file when content is unchanged."""
CORE.build_path = str(tmp_path)
content = "[env:test]\nplatform = esp32"
full_content = (
f"{platformio.INI_BASE_FORMAT[0]}"
f"{platformio.INI_AUTO_GENERATE_BEGIN}\n"
f"{content}"
f"{platformio.INI_AUTO_GENERATE_END}"
f"{platformio.INI_BASE_FORMAT[1]}"
)
ini_file = tmp_path / "platformio.ini"
ini_file.write_text(full_content)
mock_write_file_if_changed.return_value = False # Indicate no change
platformio.write_ini(content)
# write_file_if_changed should be called with the same content
mock_write_file_if_changed.assert_called_once()
call_args = mock_write_file_if_changed.call_args[0]
assert call_args[0] == str(ini_file)
assert content in call_args[1]
def test_write_ini_calls_update_storage_json(
tmp_path: Path, mock_update_storage_json: MagicMock
) -> None:
"""Test write_ini calls update_storage_json."""
CORE.build_path = str(tmp_path)
content = "[env:test]\nplatform = esp32"
platformio.write_ini(content)
mock_update_storage_json.assert_called_once()

View File

@@ -35,22 +35,6 @@ from .common import load_config_from_fixture
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "config"
@pytest.fixture
def mock_cg_with_include_capture() -> tuple[Mock, list[str]]:
"""Mock code generation with include capture."""
includes_added: list[str] = []
with patch("esphome.core.config.cg") as mock_cg:
mock_raw_statement = MagicMock()
def capture_include(text: str) -> MagicMock:
includes_added.append(text)
return mock_raw_statement
mock_cg.RawStatement.side_effect = capture_include
yield mock_cg, includes_added
def test_validate_area_config_with_string() -> None:
"""Test that string area config is converted to structured format."""
result = validate_area_config("Living Room")
@@ -593,262 +577,3 @@ def test_is_target_platform() -> None:
assert config._is_target_platform("rp2040") is True
assert config._is_target_platform("invalid_platform") is False
assert config._is_target_platform("api") is False # Component but not platform
@pytest.mark.asyncio
async def test_add_includes_with_single_file(
tmp_path: Path,
mock_copy_file_if_changed: Mock,
mock_cg_with_include_capture: tuple[Mock, list[str]],
) -> None:
"""Test add_includes copies a single header file to build directory."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
os.makedirs(CORE.build_path, exist_ok=True)
# Create include file
include_file = tmp_path / "my_header.h"
include_file.write_text("#define MY_CONSTANT 42")
mock_cg, includes_added = mock_cg_with_include_capture
await config.add_includes([str(include_file)])
# Verify copy_file_if_changed was called to copy the file
# Note: add_includes adds files to a src/ subdirectory
mock_copy_file_if_changed.assert_called_once_with(
str(include_file), str(Path(CORE.build_path) / "src" / "my_header.h")
)
# Verify include statement was added
assert any('#include "my_header.h"' in inc for inc in includes_added)
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test")
async def test_add_includes_with_directory_unix(
tmp_path: Path,
mock_copy_file_if_changed: Mock,
mock_cg_with_include_capture: tuple[Mock, list[str]],
) -> None:
"""Test add_includes copies all files from a directory on Unix."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
os.makedirs(CORE.build_path, exist_ok=True)
# Create include directory with files
include_dir = tmp_path / "includes"
include_dir.mkdir()
(include_dir / "header1.h").write_text("#define HEADER1")
(include_dir / "header2.hpp").write_text("#define HEADER2")
(include_dir / "source.cpp").write_text("// Implementation")
(include_dir / "README.md").write_text(
"# Documentation"
) # Should be copied but not included
# Create subdirectory with files
subdir = include_dir / "subdir"
subdir.mkdir()
(subdir / "nested.h").write_text("#define NESTED")
mock_cg, includes_added = mock_cg_with_include_capture
await config.add_includes([str(include_dir)])
# Verify copy_file_if_changed was called for all files
assert mock_copy_file_if_changed.call_count == 5 # 4 code files + 1 README
# Verify include statements were added for valid extensions
include_strings = " ".join(includes_added)
assert "includes/header1.h" in include_strings
assert "includes/header2.hpp" in include_strings
assert "includes/subdir/nested.h" in include_strings
# CPP files are copied but not included
assert "source.cpp" not in include_strings or "#include" not in include_strings
# README.md should not have an include statement
assert "README.md" not in include_strings
@pytest.mark.asyncio
@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test")
async def test_add_includes_with_directory_windows(
tmp_path: Path,
mock_copy_file_if_changed: Mock,
mock_cg_with_include_capture: tuple[Mock, list[str]],
) -> None:
"""Test add_includes copies all files from a directory on Windows."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
os.makedirs(CORE.build_path, exist_ok=True)
# Create include directory with files
include_dir = tmp_path / "includes"
include_dir.mkdir()
(include_dir / "header1.h").write_text("#define HEADER1")
(include_dir / "header2.hpp").write_text("#define HEADER2")
(include_dir / "source.cpp").write_text("// Implementation")
(include_dir / "README.md").write_text(
"# Documentation"
) # Should be copied but not included
# Create subdirectory with files
subdir = include_dir / "subdir"
subdir.mkdir()
(subdir / "nested.h").write_text("#define NESTED")
mock_cg, includes_added = mock_cg_with_include_capture
await config.add_includes([str(include_dir)])
# Verify copy_file_if_changed was called for all files
assert mock_copy_file_if_changed.call_count == 5 # 4 code files + 1 README
# Verify include statements were added for valid extensions
include_strings = " ".join(includes_added)
assert "includes\\header1.h" in include_strings
assert "includes\\header2.hpp" in include_strings
assert "includes\\subdir\\nested.h" in include_strings
# CPP files are copied but not included
assert "source.cpp" not in include_strings or "#include" not in include_strings
# README.md should not have an include statement
assert "README.md" not in include_strings
@pytest.mark.asyncio
async def test_add_includes_with_multiple_sources(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test add_includes with multiple files and directories."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
os.makedirs(CORE.build_path, exist_ok=True)
# Create various include sources
single_file = tmp_path / "single.h"
single_file.write_text("#define SINGLE")
dir1 = tmp_path / "dir1"
dir1.mkdir()
(dir1 / "file1.h").write_text("#define FILE1")
dir2 = tmp_path / "dir2"
dir2.mkdir()
(dir2 / "file2.cpp").write_text("// File2")
with patch("esphome.core.config.cg"):
await config.add_includes([str(single_file), str(dir1), str(dir2)])
# Verify copy_file_if_changed was called for all files
assert mock_copy_file_if_changed.call_count == 3 # 3 files total
@pytest.mark.asyncio
async def test_add_includes_empty_directory(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test add_includes with an empty directory doesn't fail."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
os.makedirs(CORE.build_path, exist_ok=True)
# Create empty directory
empty_dir = tmp_path / "empty"
empty_dir.mkdir()
with patch("esphome.core.config.cg"):
# Should not raise any errors
await config.add_includes([str(empty_dir)])
# No files to copy from empty directory
mock_copy_file_if_changed.assert_not_called()
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test")
async def test_add_includes_preserves_directory_structure_unix(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test that add_includes preserves relative directory structure on Unix."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
os.makedirs(CORE.build_path, exist_ok=True)
# Create nested directory structure
lib_dir = tmp_path / "lib"
lib_dir.mkdir()
src_dir = lib_dir / "src"
src_dir.mkdir()
(src_dir / "core.h").write_text("#define CORE")
utils_dir = lib_dir / "utils"
utils_dir.mkdir()
(utils_dir / "helper.h").write_text("#define HELPER")
with patch("esphome.core.config.cg"):
await config.add_includes([str(lib_dir)])
# Verify copy_file_if_changed was called with correct paths
calls = mock_copy_file_if_changed.call_args_list
dest_paths = [call[0][1] for call in calls]
# Check that relative paths are preserved
assert any("lib/src/core.h" in path for path in dest_paths)
assert any("lib/utils/helper.h" in path for path in dest_paths)
@pytest.mark.asyncio
@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test")
async def test_add_includes_preserves_directory_structure_windows(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test that add_includes preserves relative directory structure on Windows."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
os.makedirs(CORE.build_path, exist_ok=True)
# Create nested directory structure
lib_dir = tmp_path / "lib"
lib_dir.mkdir()
src_dir = lib_dir / "src"
src_dir.mkdir()
(src_dir / "core.h").write_text("#define CORE")
utils_dir = lib_dir / "utils"
utils_dir.mkdir()
(utils_dir / "helper.h").write_text("#define HELPER")
with patch("esphome.core.config.cg"):
await config.add_includes([str(lib_dir)])
# Verify copy_file_if_changed was called with correct paths
calls = mock_copy_file_if_changed.call_args_list
dest_paths = [call[0][1] for call in calls]
# Check that relative paths are preserved
assert any("lib\\src\\core.h" in path for path in dest_paths)
assert any("lib\\utils\\helper.h" in path for path in dest_paths)
@pytest.mark.asyncio
async def test_add_includes_overwrites_existing_files(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test that add_includes overwrites existing files in build directory."""
CORE.config_path = str(tmp_path / "config.yaml")
CORE.build_path = str(tmp_path / "build")
os.makedirs(CORE.build_path, exist_ok=True)
# Create include file
include_file = tmp_path / "header.h"
include_file.write_text("#define NEW_VALUE 42")
with patch("esphome.core.config.cg"):
await config.add_includes([str(include_file)])
# Verify copy_file_if_changed was called (it handles overwriting)
# Note: add_includes adds files to a src/ subdirectory
mock_copy_file_if_changed.assert_called_once_with(
str(include_file), str(Path(CORE.build_path) / "src" / "header.h")
)

View File

@@ -1,3 +0,0 @@
# This file should be ignored
platform: template
name: "Hidden Sensor"

View File

@@ -1 +0,0 @@
This is not a YAML file and should be ignored

View File

@@ -1,4 +0,0 @@
platform: template
name: "Sensor 1"
lambda: |-
return 42.0;

View File

@@ -1,4 +0,0 @@
platform: template
name: "Sensor 2"
lambda: |-
return 100.0;

View File

@@ -1,4 +0,0 @@
platform: template
name: "Sensor 3 in subdir"
lambda: |-
return 200.0;

View File

@@ -1,4 +0,0 @@
test_secret: "my_secret_value"
another_secret: "another_value"
wifi_password: "super_secret_wifi"
api_key: "0123456789abcdef"

View File

@@ -1,17 +0,0 @@
esphome:
name: test_device
platform: ESP32
board: esp32dev
wifi:
ssid: "TestNetwork"
password: !secret wifi_password
api:
encryption:
key: !secret api_key
sensor:
- platform: template
name: "Test Sensor"
id: !secret test_secret

View File

@@ -1,6 +1,3 @@
import os
from unittest.mock import patch
from hypothesis import given
import pytest
from strategies import mac_addr_strings
@@ -580,83 +577,3 @@ class TestEsphomeCore:
assert target.is_esp32 is False
assert target.is_esp8266 is True
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test")
def test_data_dir_default_unix(self, target):
"""Test data_dir returns .esphome in config directory by default on Unix."""
target.config_path = "/home/user/config.yaml"
assert target.data_dir == "/home/user/.esphome"
@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test")
def test_data_dir_default_windows(self, target):
"""Test data_dir returns .esphome in config directory by default on Windows."""
target.config_path = "D:\\home\\user\\config.yaml"
assert target.data_dir == "D:\\home\\user\\.esphome"
def test_data_dir_ha_addon(self, target):
"""Test data_dir returns /data when running as Home Assistant addon."""
target.config_path = "/config/test.yaml"
with patch.dict(os.environ, {"ESPHOME_IS_HA_ADDON": "true"}):
assert target.data_dir == "/data"
def test_data_dir_env_override(self, target):
"""Test data_dir uses ESPHOME_DATA_DIR environment variable when set."""
target.config_path = "/home/user/config.yaml"
with patch.dict(os.environ, {"ESPHOME_DATA_DIR": "/custom/data/path"}):
assert target.data_dir == "/custom/data/path"
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test")
def test_data_dir_priority_unix(self, target):
"""Test data_dir priority on Unix: HA addon > env var > default."""
target.config_path = "/config/test.yaml"
expected_default = "/config/.esphome"
# Test HA addon takes priority over env var
with patch.dict(
os.environ,
{"ESPHOME_IS_HA_ADDON": "true", "ESPHOME_DATA_DIR": "/custom/path"},
):
assert target.data_dir == "/data"
# Test env var is used when not HA addon
with patch.dict(
os.environ,
{"ESPHOME_IS_HA_ADDON": "false", "ESPHOME_DATA_DIR": "/custom/path"},
):
assert target.data_dir == "/custom/path"
# Test default when neither is set
with patch.dict(os.environ, {}, clear=True):
# Ensure these env vars are not set
os.environ.pop("ESPHOME_IS_HA_ADDON", None)
os.environ.pop("ESPHOME_DATA_DIR", None)
assert target.data_dir == expected_default
@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test")
def test_data_dir_priority_windows(self, target):
"""Test data_dir priority on Windows: HA addon > env var > default."""
target.config_path = "D:\\config\\test.yaml"
expected_default = "D:\\config\\.esphome"
# Test HA addon takes priority over env var
with patch.dict(
os.environ,
{"ESPHOME_IS_HA_ADDON": "true", "ESPHOME_DATA_DIR": "/custom/path"},
):
assert target.data_dir == "/data"
# Test env var is used when not HA addon
with patch.dict(
os.environ,
{"ESPHOME_IS_HA_ADDON": "false", "ESPHOME_DATA_DIR": "/custom/path"},
):
assert target.data_dir == "/custom/path"
# Test default when neither is set
with patch.dict(os.environ, {}, clear=True):
# Ensure these env vars are not set
os.environ.pop("ESPHOME_IS_HA_ADDON", None)
os.environ.pop("ESPHOME_DATA_DIR", None)
assert target.data_dir == expected_default

View File

@@ -13,7 +13,12 @@ def test_coro_priority_enum_values() -> None:
assert CoroPriority.CORE == 100
assert CoroPriority.DIAGNOSTICS == 90
assert CoroPriority.STATUS == 80
assert CoroPriority.WEB_SERVER_BASE == 65
assert CoroPriority.CAPTIVE_PORTAL == 64
assert CoroPriority.COMMUNICATION == 60
assert CoroPriority.NETWORK_SERVICES == 55
assert CoroPriority.OTA_UPDATES == 54
assert CoroPriority.WEB_SERVER_OTA == 52
assert CoroPriority.APPLICATION == 50
assert CoroPriority.WEB == 40
assert CoroPriority.AUTOMATION == 30
@@ -70,7 +75,12 @@ def test_float_and_enum_are_interchangeable() -> None:
(CoroPriority.CORE, 100.0),
(CoroPriority.DIAGNOSTICS, 90.0),
(CoroPriority.STATUS, 80.0),
(CoroPriority.WEB_SERVER_BASE, 65.0),
(CoroPriority.CAPTIVE_PORTAL, 64.0),
(CoroPriority.COMMUNICATION, 60.0),
(CoroPriority.NETWORK_SERVICES, 55.0),
(CoroPriority.OTA_UPDATES, 54.0),
(CoroPriority.WEB_SERVER_OTA, 52.0),
(CoroPriority.APPLICATION, 50.0),
(CoroPriority.WEB, 40.0),
(CoroPriority.AUTOMATION, 30.0),
@@ -164,8 +174,13 @@ def test_enum_priority_comparison() -> None:
assert CoroPriority.NETWORK_TRANSPORT > CoroPriority.CORE
assert CoroPriority.CORE > CoroPriority.DIAGNOSTICS
assert CoroPriority.DIAGNOSTICS > CoroPriority.STATUS
assert CoroPriority.STATUS > CoroPriority.COMMUNICATION
assert CoroPriority.COMMUNICATION > CoroPriority.APPLICATION
assert CoroPriority.STATUS > CoroPriority.WEB_SERVER_BASE
assert CoroPriority.WEB_SERVER_BASE > CoroPriority.CAPTIVE_PORTAL
assert CoroPriority.CAPTIVE_PORTAL > CoroPriority.COMMUNICATION
assert CoroPriority.COMMUNICATION > CoroPriority.NETWORK_SERVICES
assert CoroPriority.NETWORK_SERVICES > CoroPriority.OTA_UPDATES
assert CoroPriority.OTA_UPDATES > CoroPriority.WEB_SERVER_OTA
assert CoroPriority.WEB_SERVER_OTA > CoroPriority.APPLICATION
assert CoroPriority.APPLICATION > CoroPriority.WEB
assert CoroPriority.WEB > CoroPriority.AUTOMATION
assert CoroPriority.AUTOMATION > CoroPriority.BUS

View File

@@ -1,8 +1,5 @@
import logging
import os
from pathlib import Path
import socket
import stat
from unittest.mock import patch
from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr
@@ -557,239 +554,6 @@ def test_addr_preference_ipv6_link_local_with_scope() -> None:
assert helpers.addr_preference_(addr_info) == 1 # Has scope, so it's usable
def test_mkdir_p(tmp_path: Path) -> None:
"""Test mkdir_p creates directories recursively."""
# Test creating nested directories
nested_path = tmp_path / "level1" / "level2" / "level3"
helpers.mkdir_p(nested_path)
assert nested_path.exists()
assert nested_path.is_dir()
# Test that mkdir_p is idempotent (doesn't fail if directory exists)
helpers.mkdir_p(nested_path)
assert nested_path.exists()
# Test with empty path (should do nothing)
helpers.mkdir_p("")
# Test with existing directory
existing_dir = tmp_path / "existing"
existing_dir.mkdir()
helpers.mkdir_p(existing_dir)
assert existing_dir.exists()
def test_mkdir_p_file_exists_error(tmp_path: Path) -> None:
"""Test mkdir_p raises error when path is a file."""
# Create a file
file_path = tmp_path / "test_file.txt"
file_path.write_text("test content")
# Try to create directory with same name as existing file
with pytest.raises(EsphomeError, match=r"Error creating directories"):
helpers.mkdir_p(file_path)
def test_mkdir_p_with_existing_file_raises_error(tmp_path: Path) -> None:
"""Test mkdir_p raises error when trying to create dir over existing file."""
# Create a file where we want to create a directory
file_path = tmp_path / "existing_file"
file_path.write_text("content")
# Try to create a directory with a path that goes through the file
dir_path = file_path / "subdir"
with pytest.raises(EsphomeError, match=r"Error creating directories"):
helpers.mkdir_p(dir_path)
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test")
def test_read_file_unix(tmp_path: Path) -> None:
"""Test read_file reads file content correctly on Unix."""
# Test reading regular file
test_file = tmp_path / "test.txt"
expected_content = "Test content\nLine 2\n"
test_file.write_text(expected_content)
content = helpers.read_file(test_file)
assert content == expected_content
# Test reading file with UTF-8 characters
utf8_file = tmp_path / "utf8.txt"
utf8_content = "Hello 世界 🌍"
utf8_file.write_text(utf8_content, encoding="utf-8")
content = helpers.read_file(utf8_file)
assert content == utf8_content
@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test")
def test_read_file_windows(tmp_path: Path) -> None:
"""Test read_file reads file content correctly on Windows."""
# Test reading regular file
test_file = tmp_path / "test.txt"
expected_content = "Test content\nLine 2\n"
test_file.write_text(expected_content)
content = helpers.read_file(test_file)
# On Windows, text mode reading converts \n to \r\n
assert content == expected_content.replace("\n", "\r\n")
# Test reading file with UTF-8 characters
utf8_file = tmp_path / "utf8.txt"
utf8_content = "Hello 世界 🌍"
utf8_file.write_text(utf8_content, encoding="utf-8")
content = helpers.read_file(utf8_file)
assert content == utf8_content
def test_read_file_not_found() -> None:
"""Test read_file raises error for non-existent file."""
with pytest.raises(EsphomeError, match=r"Error reading file"):
helpers.read_file("/nonexistent/file.txt")
def test_read_file_unicode_decode_error(tmp_path: Path) -> None:
"""Test read_file raises error for invalid UTF-8."""
test_file = tmp_path / "invalid.txt"
# Write invalid UTF-8 bytes
test_file.write_bytes(b"\xff\xfe")
with pytest.raises(EsphomeError, match=r"Error reading file"):
helpers.read_file(test_file)
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test")
def test_write_file_unix(tmp_path: Path) -> None:
"""Test write_file writes content correctly on Unix."""
# Test writing string content
test_file = tmp_path / "test.txt"
content = "Test content\nLine 2"
helpers.write_file(test_file, content)
assert test_file.read_text() == content
# Check file permissions
assert oct(test_file.stat().st_mode)[-3:] == "644"
# Test overwriting existing file
new_content = "New content"
helpers.write_file(test_file, new_content)
assert test_file.read_text() == new_content
# Test writing to nested directories (should create them)
nested_file = tmp_path / "dir1" / "dir2" / "file.txt"
helpers.write_file(nested_file, content)
assert nested_file.read_text() == content
@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test")
def test_write_file_windows(tmp_path: Path) -> None:
"""Test write_file writes content correctly on Windows."""
# Test writing string content
test_file = tmp_path / "test.txt"
content = "Test content\nLine 2"
helpers.write_file(test_file, content)
assert test_file.read_text() == content
# Windows doesn't have Unix-style 644 permissions
# Test overwriting existing file
new_content = "New content"
helpers.write_file(test_file, new_content)
assert test_file.read_text() == new_content
# Test writing to nested directories (should create them)
nested_file = tmp_path / "dir1" / "dir2" / "file.txt"
helpers.write_file(nested_file, content)
assert nested_file.read_text() == content
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific permission test")
def test_write_file_to_non_writable_directory_unix(tmp_path: Path) -> None:
"""Test write_file raises error when directory is not writable on Unix."""
# Create a directory and make it read-only
read_only_dir = tmp_path / "readonly"
read_only_dir.mkdir()
test_file = read_only_dir / "test.txt"
# Make directory read-only (no write permission)
read_only_dir.chmod(0o555)
try:
with pytest.raises(EsphomeError, match=r"Could not write file"):
helpers.write_file(test_file, "content")
finally:
# Restore write permissions for cleanup
read_only_dir.chmod(0o755)
@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test")
def test_write_file_to_non_writable_directory_windows(tmp_path: Path) -> None:
"""Test write_file error handling on Windows."""
# Windows handles permissions differently - test a different error case
# Try to write to a file path that contains an existing file as a directory component
existing_file = tmp_path / "file.txt"
existing_file.write_text("content")
# Try to write to a path that treats the file as a directory
invalid_path = existing_file / "subdir" / "test.txt"
with pytest.raises(EsphomeError, match=r"Could not write file"):
helpers.write_file(invalid_path, "content")
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific permission test")
def test_write_file_with_permission_bits_unix(tmp_path: Path) -> None:
"""Test that write_file sets correct permissions on Unix."""
test_file = tmp_path / "test.txt"
helpers.write_file(test_file, "content")
# Check that file has 644 permissions
file_mode = test_file.stat().st_mode
assert stat.S_IMODE(file_mode) == 0o644
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific permission test")
def test_copy_file_if_changed_permission_recovery_unix(tmp_path: Path) -> None:
"""Test copy_file_if_changed handles permission errors correctly on Unix."""
# Test with read-only destination file
src = tmp_path / "source.txt"
dst = tmp_path / "dest.txt"
src.write_text("new content")
dst.write_text("old content")
dst.chmod(0o444) # Make destination read-only
try:
# Should handle permission error by deleting and retrying
helpers.copy_file_if_changed(src, dst)
assert dst.read_text() == "new content"
finally:
# Restore write permissions for cleanup
if dst.exists():
dst.chmod(0o644)
def test_copy_file_if_changed_creates_directories(tmp_path: Path) -> None:
"""Test copy_file_if_changed creates missing directories."""
src = tmp_path / "source.txt"
dst = tmp_path / "subdir" / "nested" / "dest.txt"
src.write_text("content")
helpers.copy_file_if_changed(src, dst)
assert dst.exists()
assert dst.read_text() == "content"
def test_copy_file_if_changed_nonexistent_source(tmp_path: Path) -> None:
"""Test copy_file_if_changed with non-existent source."""
src = tmp_path / "nonexistent.txt"
dst = tmp_path / "dest.txt"
with pytest.raises(EsphomeError, match=r"Error copying file"):
helpers.copy_file_if_changed(src, dst)
def test_resolve_ip_address_sorting() -> None:
"""Test that results are sorted by preference."""
# Create multiple address infos with different preferences

View File

@@ -1,7 +1,5 @@
"""Tests for esphome.util module."""
from __future__ import annotations
from pathlib import Path
import pytest
@@ -310,85 +308,3 @@ def test_filter_yaml_files_case_sensitive() -> None:
assert "/path/to/config.YAML" not in result
assert "/path/to/config.YML" not in result
assert "/path/to/config.Yaml" not in result
@pytest.mark.parametrize(
("input_str", "expected"),
[
# Empty string
("", "''"),
# Simple strings that don't need quoting
("hello", "hello"),
("test123", "test123"),
("file.txt", "file.txt"),
("/path/to/file", "/path/to/file"),
("user@host", "user@host"),
("value:123", "value:123"),
("item,list", "item,list"),
("path-with-dash", "path-with-dash"),
# Strings that need quoting
("hello world", "'hello world'"),
("test\ttab", "'test\ttab'"),
("line\nbreak", "'line\nbreak'"),
("semicolon;here", "'semicolon;here'"),
("pipe|symbol", "'pipe|symbol'"),
("redirect>file", "'redirect>file'"),
("redirect<file", "'redirect<file'"),
("background&", "'background&'"),
("dollar$sign", "'dollar$sign'"),
("backtick`cmd", "'backtick`cmd'"),
('double"quote', "'double\"quote'"),
("backslash\\path", "'backslash\\path'"),
("question?mark", "'question?mark'"),
("asterisk*wild", "'asterisk*wild'"),
("bracket[test]", "'bracket[test]'"),
("paren(test)", "'paren(test)'"),
("curly{brace}", "'curly{brace}'"),
# Single quotes in string (special escaping)
("it's", "'it'\"'\"'s'"),
("don't", "'don'\"'\"'t'"),
("'quoted'", "''\"'\"'quoted'\"'\"''"),
# Complex combinations
("test 'with' quotes", "'test '\"'\"'with'\"'\"' quotes'"),
("path/to/file's.txt", "'path/to/file'\"'\"'s.txt'"),
],
)
def test_shlex_quote(input_str: str, expected: str) -> None:
"""Test shlex_quote properly escapes shell arguments."""
assert util.shlex_quote(input_str) == expected
def test_shlex_quote_safe_characters() -> None:
"""Test that safe characters are not quoted."""
# These characters are considered safe and shouldn't be quoted
safe_chars = (
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%+=:,./-_"
)
for char in safe_chars:
assert util.shlex_quote(char) == char
assert util.shlex_quote(f"test{char}test") == f"test{char}test"
def test_shlex_quote_unsafe_characters() -> None:
"""Test that unsafe characters trigger quoting."""
# These characters should trigger quoting
unsafe_chars = ' \t\n;|>&<$`"\\?*[](){}!#~^'
for char in unsafe_chars:
result = util.shlex_quote(f"test{char}test")
assert result.startswith("'")
assert result.endswith("'")
def test_shlex_quote_edge_cases() -> None:
"""Test edge cases for shlex_quote."""
# Multiple single quotes
assert util.shlex_quote("'''") == "''\"'\"''\"'\"''\"'\"''"
# Mixed quotes
assert util.shlex_quote('"\'"') == "'\"'\"'\"'\"'"
# Only whitespace
assert util.shlex_quote(" ") == "' '"
assert util.shlex_quote("\t") == "'\t'"
assert util.shlex_quote("\n") == "'\n'"
assert util.shlex_quote(" ") == "' '"

View File

@@ -1,26 +1,9 @@
from pathlib import Path
import shutil
from unittest.mock import patch
import pytest
from esphome import core, yaml_util
from esphome import yaml_util
from esphome.components import substitutions
from esphome.core import EsphomeError
from esphome.util import OrderedDict
@pytest.fixture(autouse=True)
def clear_secrets_cache() -> None:
"""Clear the secrets cache before each test."""
yaml_util._SECRET_VALUES.clear()
yaml_util._SECRET_CACHE.clear()
yield
yaml_util._SECRET_VALUES.clear()
yaml_util._SECRET_CACHE.clear()
def test_include_with_vars(fixture_path: Path) -> None:
def test_include_with_vars(fixture_path):
yaml_file = fixture_path / "yaml_util" / "includetest.yaml"
actual = yaml_util.load_yaml(yaml_file)
@@ -79,202 +62,3 @@ def test_parsing_with_custom_loader(fixture_path):
assert loader_calls[0].endswith("includes/included.yaml")
assert loader_calls[1].endswith("includes/list.yaml")
assert loader_calls[2].endswith("includes/scalar.yaml")
def test_construct_secret_simple(fixture_path: Path) -> None:
"""Test loading a YAML file with !secret tags."""
yaml_file = fixture_path / "yaml_util" / "test_secret.yaml"
actual = yaml_util.load_yaml(yaml_file)
# Check that secrets were properly loaded
assert actual["wifi"]["password"] == "super_secret_wifi"
assert actual["api"]["encryption"]["key"] == "0123456789abcdef"
assert actual["sensor"][0]["id"] == "my_secret_value"
def test_construct_secret_missing(fixture_path: Path, tmp_path: Path) -> None:
"""Test that missing secrets raise proper errors."""
# Create a YAML file with a secret that doesn't exist
test_yaml = tmp_path / "test.yaml"
test_yaml.write_text("""
esphome:
name: test
wifi:
password: !secret nonexistent_secret
""")
# Create an empty secrets file
secrets_yaml = tmp_path / "secrets.yaml"
secrets_yaml.write_text("some_other_secret: value")
with pytest.raises(EsphomeError, match="Secret 'nonexistent_secret' not defined"):
yaml_util.load_yaml(str(test_yaml))
def test_construct_secret_no_secrets_file(tmp_path: Path) -> None:
"""Test that missing secrets.yaml file raises proper error."""
# Create a YAML file with a secret but no secrets.yaml
test_yaml = tmp_path / "test.yaml"
test_yaml.write_text("""
wifi:
password: !secret some_secret
""")
# Mock CORE.config_path to avoid NoneType error
with (
patch.object(core.CORE, "config_path", str(tmp_path / "main.yaml")),
pytest.raises(EsphomeError, match="secrets.yaml"),
):
yaml_util.load_yaml(str(test_yaml))
def test_construct_secret_fallback_to_main_config_dir(
fixture_path: Path, tmp_path: Path
) -> None:
"""Test fallback to main config directory for secrets."""
# Create a subdirectory with a YAML file that uses secrets
subdir = tmp_path / "subdir"
subdir.mkdir()
test_yaml = subdir / "test.yaml"
test_yaml.write_text("""
wifi:
password: !secret test_secret
""")
# Create secrets.yaml in the main directory
main_secrets = tmp_path / "secrets.yaml"
main_secrets.write_text("test_secret: main_secret_value")
# Mock CORE.config_path to point to main directory
with patch.object(core.CORE, "config_path", str(tmp_path / "main.yaml")):
actual = yaml_util.load_yaml(str(test_yaml))
assert actual["wifi"]["password"] == "main_secret_value"
def test_construct_include_dir_named(fixture_path: Path, tmp_path: Path) -> None:
"""Test !include_dir_named directive."""
# Copy fixture directory to temporary location
src_dir = fixture_path / "yaml_util"
dst_dir = tmp_path / "yaml_util"
shutil.copytree(src_dir, dst_dir)
# Create test YAML that uses include_dir_named
test_yaml = dst_dir / "test_include_named.yaml"
test_yaml.write_text("""
sensor: !include_dir_named named_dir
""")
actual = yaml_util.load_yaml(str(test_yaml))
actual_sensor = actual["sensor"]
# Check that files were loaded with their names as keys
assert isinstance(actual_sensor, OrderedDict)
assert "sensor1" in actual_sensor
assert "sensor2" in actual_sensor
assert "sensor3" in actual_sensor # Files from subdirs are included with basename
# Check content of loaded files
assert actual_sensor["sensor1"]["platform"] == "template"
assert actual_sensor["sensor1"]["name"] == "Sensor 1"
assert actual_sensor["sensor2"]["platform"] == "template"
assert actual_sensor["sensor2"]["name"] == "Sensor 2"
# Check that subdirectory files are included with their basename
assert actual_sensor["sensor3"]["platform"] == "template"
assert actual_sensor["sensor3"]["name"] == "Sensor 3 in subdir"
# Check that hidden files and non-YAML files are not included
assert ".hidden" not in actual_sensor
assert "not_yaml" not in actual_sensor
def test_construct_include_dir_named_empty_dir(tmp_path: Path) -> None:
"""Test !include_dir_named with empty directory."""
# Create empty directory
empty_dir = tmp_path / "empty_dir"
empty_dir.mkdir()
test_yaml = tmp_path / "test.yaml"
test_yaml.write_text("""
sensor: !include_dir_named empty_dir
""")
actual = yaml_util.load_yaml(str(test_yaml))
# Should return empty OrderedDict
assert isinstance(actual["sensor"], OrderedDict)
assert len(actual["sensor"]) == 0
def test_construct_include_dir_named_with_dots(tmp_path: Path) -> None:
"""Test that include_dir_named ignores files starting with dots."""
# Create directory with various files
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create visible file
visible_file = test_dir / "visible.yaml"
visible_file.write_text("key: visible_value")
# Create hidden file
hidden_file = test_dir / ".hidden.yaml"
hidden_file.write_text("key: hidden_value")
# Create hidden directory with files
hidden_dir = test_dir / ".hidden_dir"
hidden_dir.mkdir()
hidden_subfile = hidden_dir / "subfile.yaml"
hidden_subfile.write_text("key: hidden_subfile_value")
test_yaml = tmp_path / "test.yaml"
test_yaml.write_text("""
test: !include_dir_named test_dir
""")
actual = yaml_util.load_yaml(str(test_yaml))
# Should only include visible file
assert "visible" in actual["test"]
assert actual["test"]["visible"]["key"] == "visible_value"
# Should not include hidden files or directories
assert ".hidden" not in actual["test"]
assert ".hidden_dir" not in actual["test"]
def test_find_files_recursive(fixture_path: Path, tmp_path: Path) -> None:
"""Test that _find_files works recursively through include_dir_named."""
# Copy fixture directory to temporary location
src_dir = fixture_path / "yaml_util"
dst_dir = tmp_path / "yaml_util"
shutil.copytree(src_dir, dst_dir)
# This indirectly tests _find_files by using include_dir_named
test_yaml = dst_dir / "test_include_recursive.yaml"
test_yaml.write_text("""
all_sensors: !include_dir_named named_dir
""")
actual = yaml_util.load_yaml(str(test_yaml))
# Should find sensor1.yaml, sensor2.yaml, and subdir/sensor3.yaml (all flattened)
assert len(actual["all_sensors"]) == 3
assert "sensor1" in actual["all_sensors"]
assert "sensor2" in actual["all_sensors"]
assert "sensor3" in actual["all_sensors"]
def test_secret_values_tracking(fixture_path: Path) -> None:
"""Test that secret values are properly tracked for dumping."""
yaml_file = fixture_path / "yaml_util" / "test_secret.yaml"
yaml_util.load_yaml(yaml_file)
# Check that secret values are tracked
assert "super_secret_wifi" in yaml_util._SECRET_VALUES
assert yaml_util._SECRET_VALUES["super_secret_wifi"] == "wifi_password"
assert "0123456789abcdef" in yaml_util._SECRET_VALUES
assert yaml_util._SECRET_VALUES["0123456789abcdef"] == "api_key"