mirror of
https://github.com/esphome/esphome.git
synced 2025-09-19 09:58:47 +00:00
Compare commits
6 Commits
remove_esp
...
beta
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e47f4ef602 | ||
![]() |
961be7fd12 | ||
![]() |
a5a21f47d1 | ||
![]() |
a06cd84974 | ||
![]() |
e3703b43c1 | ||
![]() |
f6dc25c0ce |
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
@@ -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
|
||||
|
2
Doxyfile
2
Doxyfile
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -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 {}"); }
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
|
@@ -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 = "#"
|
||||
|
@@ -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()),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
@@ -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])
|
||||
|
||||
|
@@ -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"):
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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")
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
@@ -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]))
|
||||
|
@@ -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)
|
||||
|
@@ -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();
|
||||
|
@@ -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)
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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};
|
||||
|
@@ -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;
|
||||
|
@@ -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:
|
||||
|
@@ -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_() {
|
||||
|
@@ -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
|
||||
};
|
||||
|
@@ -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_) {
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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")
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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();
|
125
esphome/components/remote_receiver/remote_receiver_libretiny.cpp
Normal file
125
esphome/components/remote_receiver/remote_receiver_libretiny.cpp
Normal 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
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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
|
@@ -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
|
@@ -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},
|
||||
|
214
esphome/components/uart/uart_component_esp32_arduino.cpp
Normal file
214
esphome/components/uart/uart_component_esp32_arduino.cpp
Normal 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.1:apb clock:ref_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
|
60
esphome/components/uart/uart_component_esp32_arduino.h
Normal file
60
esphome/components/uart/uart_component_esp32_arduino.h
Normal 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
|
@@ -1,4 +1,4 @@
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
#include "uart_component_esp_idf.h"
|
||||
#include <cinttypes>
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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]))
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
|
12
esphome/core/string_ref.cpp
Normal file
12
esphome/core/string_ref.cpp
Normal 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
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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.",
|
||||
]
|
||||
|
||||
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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.")
|
||||
|
42
tests/components/mdns/test-comprehensive.esp8266-ard.yaml
Normal file
42
tests/components/mdns/test-comprehensive.esp8266-ard.yaml
Normal 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"
|
@@ -1,4 +1,8 @@
|
||||
substitutions:
|
||||
network_enable_ipv6: "true"
|
||||
|
||||
bk72xx:
|
||||
framework:
|
||||
version: 1.7.0
|
||||
|
||||
<<: !include common.yaml
|
||||
|
@@ -1 +0,0 @@
|
||||
<<: !include common.yaml
|
@@ -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)
|
@@ -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
|
@@ -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"
|
||||
)
|
@@ -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()
|
@@ -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")
|
||||
)
|
||||
|
@@ -1,3 +0,0 @@
|
||||
# This file should be ignored
|
||||
platform: template
|
||||
name: "Hidden Sensor"
|
@@ -1 +0,0 @@
|
||||
This is not a YAML file and should be ignored
|
@@ -1,4 +0,0 @@
|
||||
platform: template
|
||||
name: "Sensor 1"
|
||||
lambda: |-
|
||||
return 42.0;
|
@@ -1,4 +0,0 @@
|
||||
platform: template
|
||||
name: "Sensor 2"
|
||||
lambda: |-
|
||||
return 100.0;
|
@@ -1,4 +0,0 @@
|
||||
platform: template
|
||||
name: "Sensor 3 in subdir"
|
||||
lambda: |-
|
||||
return 200.0;
|
@@ -1,4 +0,0 @@
|
||||
test_secret: "my_secret_value"
|
||||
another_secret: "another_value"
|
||||
wifi_password: "super_secret_wifi"
|
||||
api_key: "0123456789abcdef"
|
@@ -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
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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(" ") == "' '"
|
||||
|
@@ -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"
|
||||
|
Reference in New Issue
Block a user